推薦閱讀:
文章推薦系統 | 一、推薦流程設計
文章推薦系統 | 二、同步業務數據
文章推薦系統 | 三、收集用戶行為數據
文章推薦系統 | 四、構建離線文章畫像
文章推薦系統 | 五、計算文章相似度
文章推薦系統 | 六、構建離線用戶畫像
文章推薦系統 | 七、構建離線文章特征和用戶特征
文章推薦系統 | 八、基于模型的離線召回
在上篇文章中,我們實現了基于模型的離線召回,屬于基于協同過濾的召回算法。接下來,本文就講一下另一個經典的召回方式,那就是如何實現基于內容的離線召回。相比于協同過濾來說,基于內容的召回會簡單很多,主要思路就是召回用戶點擊過的文章的相似文章,通常也被叫做 u2i2i。
離線召回
首先,讀取用戶歷史行為數據,得到用戶歷史點擊過的文章
spark.sql('use profile')
user_article_basic = spark.sql("select * from user_article_basic")
user_article_basic = user_article_basic.filter('clicked=True')
user_article_basic
結果如下所示
接下來,遍歷用戶歷史點擊過的文章,獲取與之相似度最高的 K 篇文章即可。可以根據之前計算好的文章相似度表 article_similar 進行相似文章查詢,接著根據歷史召回結果進行過濾,防止重復推薦。最后將召回結果按照頻道分別存入召回結果表及歷史召回結果表
user_article_basic.foreachPartition(get_clicked_similar_article)
def get_clicked_similar_article(partition):
"""召回用戶點擊文章的相似文章
"""
import happybase
pool = happybase.ConnectionPool(size=10, host='hadoop-master')
with pool.connection() as conn:
similar_table = conn.table('article_similar')
for row in partition:
# 讀取文章相似度表,根據文章ID獲取相似文章
similar_article = similar_table.row(str(row.article_id).encode(),
columns=[b'similar'])
# 按照相似度進行排序
similar_article_sorted = sorted(similar_article.items(), key=lambda item: item[1], reverse=True)
if similar_article_sorted:
# 每次行為推薦10篇文章
similar_article_topk = [int(i[0].split(b':')[1]) for i in similar_article_sorted][:10]
# 根據歷史召回結果進行過濾
history_table = conn.table('history_recall')
history_article_data = history_table.cells('reco:his:{}'.format(row.user_id).encode(), 'channel:{}'.format(row.channel_id).encode())
# 將多個版本都加入歷史文章ID列表
history_article = []
if len(history_article_data) >= 2:
for article in history_article_data[:-1]:
history_article.extend(eval(article))
else:
history_article = []
# 過濾history_article
recall_article = list(set(similar_article_topk) - set(history_article))
# 存儲到召回結果表及歷史召回結果表
if recall_article:
content_table = conn.table('cb_recall')
content_table.put("recall:user:{}".format(row.user_id).encode(), {'content:{}'.format(row.channel_id).encode(): str(recall_article).encode()})
# 放入歷史召回結果表
history_table.put("reco:his:{}".format(row.user_id).encode(), {'channel:{}'.format(row.channel_id).encode(): str(recall_article).encode()})
可以根據用戶 ID 和頻道 ID 來查詢召回結果
hbase(main):028:0> get 'cb_recall', 'recall:user:2'
COLUMN CELL
content:13 timestamp=1558041569201, value=[141431,14381, 17966, 17454, 14125, 16174]
最后,使用 Apscheduler 定時更新。在用戶召回方法 update_user_recall()
中,增加基于內容的離線召回方法 update_content_recall()
,首先讀取用戶行為日志,并篩選用戶點擊的文章,接著讀取文章相似表,獲取相似度最高的 K 篇文章,然后根據歷史召回結果進行過濾,防止重復推薦,最后,按頻道分別存入召回結果表及歷史召回結果表
def update_user_recall():
"""
用戶的頻道推薦召回結果更新邏輯
:return:
"""
ur = UpdateRecall(500)
ur.update_als_recall()
ur.update_content_recall()
之前已經添加好了定時更新用戶召回結果的任務,每隔 3 小時運行一次,這樣就完成了基于內容的離線召回。
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.executors.pool import ProcessPoolExecutor
# 創建scheduler,多進程執行
executors = {
'default': ProcessPoolExecutor(3)
}
scheduler = BlockingScheduler(executors=executors)
# 添加一個定時運行文章畫像更新的任務, 每隔1個小時運行一次
scheduler.add_job(update_article_profile, trigger='interval', hours=1)
# 添加一個定時運行用戶畫像更新的任務, 每隔2個小時運行一次
scheduler.add_job(update_user_profile, trigger='interval', hours=2)
# 添加一個定時運行用戶召回更新的任務,每隔3小時運行一次
scheduler.add_job(update_user_recall, trigger='interval', hours=3)
# 添加一個定時運行特征中心平臺的任務,每隔4小時更新一次
scheduler.add_job(update_ctr_feature, trigger='interval', hours=4)
scheduler.start()
在線召回
前面我們實現了基于內容的離線召回,接下來我們將實現基于內容的在線召回。在線召回的實時性更好,能夠根據用戶的線上行為實時反饋,快速跟蹤用戶的偏好,也能夠解決用戶冷啟動問題。離線召回和在線召回唯一的不同就是,離線召回讀取的是用戶歷史行為數據,而在線召回讀取的是用戶實時的行為數據,從而召回用戶當前正在閱讀的文章的相似文章。
首先,我們通過 Spark Streaming 讀取 Kafka 中的用戶實時行為數據,Spark Streaming 配置如下
from pyspark import SparkConf
from pyspark.sql import SparkSession
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
from pyspark.streaming.kafka import KafkaUtils
from setting.default import DefaultConfig
import happybase
SPARK_ONLINE_CONFIG = (
("spark.app.name", "onlineUpdate"),
("spark.master", "yarn"),
("spark.executor.instances", 4)
)
KAFKA_SERVER = "192.168.19.137:9092"
# 用于讀取hbase緩存結果配置
pool = happybase.ConnectionPool(size=10, host='hadoop-master', port=9090)
conf = SparkConf()
conf.setAll(SPARK_ONLINE_CONFIG)
sc = SparkContext(conf=conf)
stream_c = StreamingContext(sc, 60)
# 基于內容召回配置,用于收集用戶行為
similar_kafkaParams = {"metadata.broker.list": DefaultConfig.KAFKA_SERVER, "group.id": 'similar'}
SIMILAR_DS = KafkaUtils.createDirectStream(stream_c, ['click-trace'], similar_kafkaParams)
Kafka 中的用戶行為數據,如下所示
{"actionTime":"2019-12-10 21:04:39","readTime":"","channelId":18,"param":{"action": "click", "userId": "2", "articleId": "116644", "algorithmCombine": "C2"}}
接下來,利用 Spark Streaming 將用戶行為數據傳入到 get_similar_online_recall()
方法中,這里利用 json.loads()
方法先將其轉換為了 json 格式,注意用戶行為數據在每條 Kafka 消息的第二個位置
SIMILAR_DS.map(lambda x: json.loads(x[1])).foreachRDD(get_similar_online_recall)
接著,遍歷用戶行為數據,這里可能每次讀取到多條用戶行為數據。篩選出被點擊、收藏或分享過的文章,并獲取與其相似度最高的 K 篇文章,再根據歷史召回結果表進行過濾,防止重復推薦,最后,按頻道分別存入召回結果表及歷史召回結果表
def get_online_similar_recall(rdd):
"""
獲取在線相似文章
:param rdd:
:return:
"""
import happybase
topk = 10
# 初始化happybase連接
pool = happybase.ConnectionPool(size=10, host='hadoop-master', port=9090)
for data in rdd.collect():
# 根據用戶行為篩選文章
if data['param']['action'] in ["click", "collect", "share"]:
with pool.connection() as conn:
similar_table = conn.table("article_similar")
# 根據用戶行為數據涉及文章找出與之最相似文章(基于內容的相似)
similar_article = similar_table.row(str(data["param"]["articleId"]).encode(), columns=[b"similar"])
similar_article = sorted(similar_article.items(), key=lambda x: x[1], reverse=True) # 按相似度排序
if similar_article:
similar_article_topk = [int(i[0].split(b":")[1]) for i in similar_article[:topk]] # 選取K篇作為召回推薦結果
# 根據歷史召回結果進行過濾
history_table = conn.table('history_recall')
history_article_data = history_table.cells(b"reco:his:%s" % data["param"]["userId"].encode(), b"channel:%d" % data["channelId"])
# 將多個版本都加入歷史文章ID列表
history_article = []
if len(history_article_data) >1:
for article in history_article_data[:-1]:
history_article.extend(eval(article))
else:
history_article = []
# 過濾history_article
recall_article = list(set(similar_article_topk) - set(history_article))
# 如果有召回結果,按頻道分別存入召回結果表及歷史召回結果表
if recall_article:
recall_table = conn.table("cb_recall")
recall_table.put(b"recall:user:%s" % data["param"]["userId"].encode(), {b"online:%d" % data["channelId"]: str(recall_article).encode()})
history_table.put(b"reco:his:%s" % data["param"]["userId"].encode(), {b"channel:%d" % data["channelId"]: str(recall_article).encode()})
conn.close()
可以根據用戶 ID 和頻道 ID 來查詢召回結果
hbase(main):028:0> get 'cb_recall', 'recall:user:2'
COLUMN CELL
online:13 timestamp=1558041569201, value=[141431,14381, 17966, 17454, 14125, 16174]
創建 online_update.py
,加入基于內容的在線召回邏輯
if __name__ == '__main__':
ore = OnlineRecall()
ore.update_content_recall()
stream_sc.start()
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
pass
利用 Supervisor 進行進程管理,并開啟實時運行,配置如下,其中 environment 需要指定運行所需環境
[program:online]
environment=JAVA_HOME=/root/bigdata/jdk,SPARK_HOME=/root/bigdata/spark,HADOOP_HOME=/root/bigdata/hadoop,PYSPARK_PYTHON=/miniconda2/envs/reco_sys/bin/python ,PYSPARK_DRIVER_PYTHON=/miniconda2/envs/reco_sys/bin/python,PYSPARK_SUBMIT_ARGS='--packages org.apache.spark:spark-streaming-kafka-0-8_2.11:2.2.2 pyspark-shell'
command=/miniconda2/envs/reco_sys/bin/python /root/toutiao_project/reco_sys/online/online_update.py
directory=/root/toutiao_project/reco_sys/online
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/root/logs/onlinesuper.log
loglevel=info
stopsignal=KILL
stopasgroup=true
killasgroup=true
參考
https://www.bilibili.com/video/av68356229
https://pan.baidu.com/s/1-uvGJ-mEskjhtaial0Xmgw(學習資源已保存至網盤, 提取碼:eakp)