在最近举办的零基础入门推荐系统中,我们在召回部分布置了多个召回算法的任务,在本文我们也将介绍这些实现的逻辑:
基于用户相似的召回逻辑 基于文章相似的召回逻辑 基于文章内容相似的召回逻辑 基于文章热度的召回逻辑
步骤1:用户文章打分矩阵
不管在进行用户相似还是文章相似的计算过程中,都需要构建用户与文章的打分矩阵,然后借助这个矩阵计算用户与用户、文章与文章的相似度。
可以使用交叉表来统计评分矩阵,计算逻辑如下(如果用户对文章阅读次数越多,则认为评分越高):
pd.crosstab(
[1, 1, 2, 2, 3, 3],
[4, 5, 4, 4, 5, 6]
)
但使用交叉表会直接导致内存爆炸,本次比赛中我们需要构建一个维度为250000 * 35380维度的矩阵,直接构造会导致内存爆炸。
一种替代方法是使用稀疏矩阵,稀疏矩阵可以使用更少的内存来存储稀疏的数值。具体代码参考如下:
from scipy.sparse import lil_matrix
user_atricle_matrix = lil_matrix((250000, 35380), dtype=np.uint8)
user_atricle_matrix[
train_user_article_count['user_id'].values[:],
train_user_article_count['click_article_id_encode'].values[:]
] = train_user_article_count['click_timestamp'].values[:]
这里我们选择使用Row-based list of lists sparse matrix(基于行的列表列表稀疏矩阵),其支持支持加法、减法、乘法、除法和矩阵幂。
当然你也可以选择其他的稀疏矩阵格式,这里稀疏矩阵的优缺点对比我们后续展开讲解。
用户与文章打分矩阵中,使用稀疏矩阵的格式有两个优点:
对内存友好,只需要额外的200M内存 对性能友好,稀疏矩阵的内积运算比稠密句子的内积运算更快
步骤2:用户相似召回
在计算得到用户与文章的评分矩阵后,接下来我们就可以直接使用矩阵去计算用户与用户之间的相似度。注意这里的每一行是一个用户对所有文章的评分:
# 对评分矩阵进行归一化
from sklearn.preprocessing import normalize
user_atricle_matrix = normalize(user_atricle_matrix, norm='l2')
# 计算用户与用户之间的相似度
user2user_distance = []
for idx in range(len(val_user_idx) // 10000):
user2user_distance.append(
user_atricle_matrix[val_user_idx[idx*10000:(idx+1)*10000]].dot(user_atricle_matrix.T)
)
接下来我们需要按照用户的相似度得到待选文章的列表,具体的逻辑如下:使用用户相似度找到每个用户阅读过的文章,然后将文章进行评分排序。
top_n = 25
for user_id, distance in zip(val_user_idx[:10], user2user_distance):
distance = distance.toarray()[0]
ids = distance.argsort()[::-1]
# 待选相似用户
similar_user_id = ids[1:top_n+1]
similar_user_distance = distance[ids[1:top_n+1]]
similar_user_distance = lil_matrix(similar_user_distance)
# 计算得到待选文章的评分
similar_doc_distance = similar_user_distance.dot(user_atricle_matrix[similar_user_id])
similar_doc_top50 = similar_doc_distance.toarray()[0].argsort()[::-1][:50]
similar_doc_top50 = [x for x in similar_doc_top50 if x not in train_history_doc[user_id]][:50]
print(user_id, mrr(val_label[user_id], similar_doc_top50))
步骤3:文章相似召回
文章相似召回的逻辑与用户相似召回的逻辑相同,首先也需要有用户与文章的评分矩阵,然后需要取计算文章与文章之间的相似度。
# 计算文章与文章之间的相似度
doc2doc_distance = []
for idx in range(len(article_lbl.classes_) // 10000 + 1):
doc2doc_distance.append(
user_atricle_matrix.T.dot(user_atricle_matrix[:, idx*10000:(idx+1)*10000])
)
接下来使用文章相似结果可以直接得到待选的文章:
top_n = 25
val_mrr = []
for row in train_clicks.loc[train_clicks_val_idx].iloc[:1000].itertuples():
user_id = row.user_id
distance = doc2doc_distance[train_history_doc.loc[user_id]].sum(0)
distance = np.array(distance)[0]
similar_doc_top = distance.argsort()[::-1][1:]
similar_doc_top = [x for x in similar_doc_top if x not in train_history_doc.loc[user_id]][:top_n]
步骤4:文章内容召回
文章内容相似可以从文章的嵌入矩阵来进行计算,计算完成后继续文章相似度的召回。文章嵌入矩阵的相似度计算代码如下:
doc2doc_content_distance = []
for idx in range(len(article_lbl.classes_) // 100 + 1):
doc2doc_content_distance.append(
articles_emb_selected.dot(articles_emb_selected[idx*100:(idx+1)*100].T)
)
break
但上述计算代码速度会非常慢,因为文章嵌入句子是一个稠密矩阵,在计算内积时速度很慢很慢。
可以考虑如下相似度加速逻辑:
先聚类,在同一类别中进行相似度 使用近似搜索逻辑,如HNSW
步骤5:文章热度召回
文章热度可以理解为排行榜,其可以代表文章在一段时间内的流行程度。按照新闻平台的内容分发逻辑,热门文章曝光量更大。
首先计算得到文章发部时间,计算得到小时和天:
train_clicks['click_ts_hour'] = train_clicks['click_timestamp'] // 3600
train_clicks['click_ts_day'] = train_clicks['click_timestamp'] // 86400
train_clicks['click_ts_hour'] = train_clicks['click_ts_hour'].astype(int)
train_clicks['click_ts_day'] = train_clicks['click_ts_day'].astype(int)
计算发布小时&天内,文章的阅读次数,然后将热门的文章推荐给用户:
article_day_hot = train_clicks.loc[train_clicks_train_idx].groupby('click_ts_day')['click_article_id_encode'].value_counts()
article_hour_hot = train_clicks.loc[train_clicks_train_idx].groupby('click_ts_hour')['click_article_id_encode'].value_counts()
思考与总结
在本文我们尝试了多种内容召回逻辑,但每种逻辑得到的效果其实存在交叉区别。按照计算基于文章相似度召回和热度召回的逻辑效果最好。
后续同学们也可以参考如下思路进行进一步尝试:使用上述方法进行综合打分,比如每个思路得到的文章权重为0.25,得到综合的文章排序。
本文代码资料:https://coggle.club/blog/30days-of-ml-202212
本文比赛链接:https://tianchi.aliyun.com/competition/entrance/531842/information
# 竞赛交流群 邀请函 #

与 28000+来自竞赛爱好者一起交流~




