个人参加的第一次数据科学比赛,是天池上的一个新人赛,是Datawhale与天池联合发起的0基础入门系列赛事第五场——零基础入门推荐系统之新闻推荐场景下的用户行为预测挑战赛。
1. 赛题介绍
以新闻APP中的新闻推荐为背景,要求根据用户历史浏览点击新闻文章的数据信息预测用户未来点击行为,即用户的最后一次点击的新闻文章,测试集中对最后一个点击行为进行了剔除。
2. 赛题数据
以预测用户未来点击新闻文章为任务数据。来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。
2.1 输入和输出
输入:用户
输出:预测
预测任务:预测用户点击的Top5文章
2.2 训练集和测试集分割
训练集:20万用户的点击日志数据
测试集A:5万用户的点击日志数据
测试集B:5万用户的点击日志数据
2.3 数据表信息
数据表:
数据表名 | 内容介绍 |
---|---|
train_click_log.csv | 训练集用户点击日志 |
testA_click_log.csv | 测试集用户点击日志 |
articles.csv | 新闻文章信息数据表 |
articles_emb.csv | 新闻文章embedding向量表示 |
sample_submit.csv | 提交样例文件 |
字段介绍:
字段 | 描述 |
---|---|
user_id | 用户id |
click_article_id | 点击文章id |
click_timestamp | 点击时间戳 |
click_environment | 点击环境 |
click_deviceGroup | 点击设备组 |
click_os | 点击操作系统 |
click_country | 点击城市 |
click_region | 点击地区 |
click_referrer_type | 点击来源类型 |
article_id | 文章id,与click_article_id相对应 |
category_id | 文章类型id |
created_at_ts | 文章创建时间戳 |
words_count | 文章字数 |
emb_1,emb_2,…,emb_249 | 文章embedding向量表示 |
2.4 评分指标
采用的评分方式为MRR(Mean Reciprocal Rank)
%20%3D%20%5Csum%7Bk%3D1%7D%5E5%20%5Cfrac%7Bs(user%2C%20k)%7D%7Bk%7D%5C%5C%5C%5C%0ATotalScore%20%3D%20%5Cfrac%7B1%7D%7BN%7D%5Csum%7Bi%3D1%7D%5ENscore(useri)%0A#card=math&code=score%28user%29%20%3D%20%5Csum%7Bk%3D1%7D%5E5%20%5Cfrac%7Bs%28user%2C%20k%29%7D%7Bk%7D%5C%5C%5C%5C%0ATotalScore%20%3D%20%5Cfrac%7B1%7D%7BN%7D%5Csum_%7Bi%3D1%7D%5ENscore%28user_i%29%0A)
其中s(user, k)
为预测结果是否命中最后一条点击数据,命中的话s(user, k)=1
,未命中则s(user, k)=0
。TotalScore
为最终的模型性能评分。
3. 使用算法(模型)
3.1 Item-based CF
最开始当然是使用协同过滤,采用基于物品的协同过滤。
基本步骤比较明确:
- 建立倒排表
- 选择相似度矩阵(在此选择了余弦相似度),对所有的用户(测试+验证)求得物品相似度矩阵
- 构造物品召回函数,输入用户id和其它参数,返回召回列表
对于测试集,进行物品召回
# 建立倒排表
def create_user_item_time_dict(df):
click_df = df.sort_values(by='click_timestamp')
click_df = click_df.groupby('user_id')
click_df = click_df['click_article_id', 'click_timestamp'].apply(lambda df: list(zip(df['click_article_id'], df['click_timestamp'])))
click_df = click_df.reset_index().rename(columns={0: 'item_time_list'})
user_item_time_dict = dict(zip(click_df['user_id'], click_df['item_time_list']))
pickle.dump(user_item_time_dict, open(os.path.join(save_path, 'user_item_time_dict.pkl'), 'wb'))
print('{:=^40}'.format('用户-物品、时间倒排表生成结束,已写入到离线文件!'))
return user_item_time_dict
# 计算商品间相似度矩阵
def cal_item_item_sim(df, user_item_time_dict):
item_item_sim = {}
item_counts = collections.defaultdict(int)
for user, item_time_list in tqdm(user_item_time_dict.items()):
for item, time in item_time_list:
item_counts[item] += 1
item_item_sim.setdefault(item, {})
for other_item, other_item_time in item_time_list:
if item == other_item:
continue
item_item_sim[item].setdefault(other_item, 0)
item_item_sim[item][other_item] += 1
for item, sim_items in tqdm(item_item_sim.items()):
for other_item, coo_counts in sim_items.items():
item_item_sim[item][other_item] /= math.sqrt(item_counts[item] * item_counts[other_item])
pickle.dump(item_item_sim, open(os.path.join(save_path, 'item_item_similarity.pkl'), 'wb'))
print('{:=^40}'.format('物品相似度矩阵计算成功,已写入到离线文件!'))
return item_item_sim
# 基于ItemCF的商品推荐
def itemcf_rec(user_id, recall_top_k, sim_top_k, user_item_time_dict, item_item_sim, hot_top_k_items):
user_clicked_items_with_time = user_item_time_dict[user_id]
user_clicked_items = {item_id for item_id, _ in user_clicked_items_with_time}
ranked_items = {}
# 计算未点击过商品的权重,计算方式为:与物品I的相似度 * 物品I被点击的次数
for index, (item, click_time) in enumerate(user_clicked_items_with_time):
for other_item, sim in sorted(item_item_sim[item].items(),
key=lambda x: x[1],
reverse=True)[:sim_top_k]:
if other_item not in user_clicked_items:
ranked_items.setdefault(other_item, 0)
ranked_items[other_item] += sim
# 不足需要召回的数量,用热门商品填充
if len(ranked_items) < recall_top_k:
for index, item in enumerate(hot_top_k_items):
if item not in ranked_items:
ranked_items[item] = -index
if len(ranked_items) == recall_top_k:
break
return sorted(ranked_items.items(), key=lambda x:x[1], reverse=True)[:recall_top_k]
通过调用itemcf_rec
即可实现对指定用户的物品推荐。
4. 数据分析
见代码
数据分析对于实际问题非常重要,是充分了解数据、挖掘信息、指导模型实现的关键,通过数据分析可知:
- 训练集和测试集的用户id没有重复,也就是测试集里面的用户模型是没有见过的
- 训练集中用户最少的点击文章数是2, 而测试集里面用户最少的点击文章数是1
- 用户对于文章存在重复点击的情况, 但这个都存在于训练集里面
- 同一用户的点击环境存在不唯一的情况,后面做这部分特征的时候可以采用统计特征
- 用户点击文章的次数有很大的区分度,后面可以根据这个制作衡量用户活跃度的特征
- 文章被用户点击的次数也有很大的区分度,后面可以根据这个制作衡量文章热度的特征
- 用户看的新闻,相关性是比较强的,所以往往我们判断用户是否对某篇文章感兴趣的时候, 在很大程度上会和他历史点击过的文章有关
- 用户点击的文章字数有比较大的区别, 这个可以反映用户对于文章字数的区别
- 用户点击过的文章主题也有很大的区别, 这个可以反映用户的主题偏好 10.不同用户点击文章的时间差也会有所区别, 这个可以反映用户对于文章时效性的偏好
5. 多路召回
5.1 什么是多路召回,为什么需要多路召回?
多路召回,指的是通过不同的策略、特征或模型,分别召回一部分的候选集,然后把候选集混合在一起过排序模型。简而言之,就是同时独立的进行多次召回操作。
使用多路召回的目的我任务包括两部分:召回率、计算速度
首先得回到召回层的作用上,召回层是为了召回相对大的一部分候选集,供排序层使用,是为了避免排序层直接操作全量数据从而减少运算成本。因此召回层即要召回候选集又要尽可能全的召回候选集,即使得召回率较高,而不同策略、特征或模型通常有其考虑的侧重点,通过混合多路召回能尽量使召回结果比较稳定全面。
另外,由于多路召回的各模型独立运行,通过分布式或并发多线程可以同时执行,会提高整体计算速度。
多路召回通常包括:兴趣标签、兴趣Topic、兴趣实体、协同过滤、热门等等
5.2 根据数据构建的召回
- itemcf 物品协同(带优化策略的)
- embedding 根据物品embedding进行召回
- youtubednn 召回
- 根据youtubednn得出的用户embedding进行召回
- 冷启动召回
5.2.1 冷启动问题
冷启动问题可以分成三类:文章冷启动,用户冷启动,系统冷启动。
- 文章冷启动:对于一个平台系统新加入的文章,该文章没有任何的交互记录,如何推荐给用户的问题。(对于我们场景可以认为是,日志数据中没有出现过的文章都可以认为是冷启动的文章)
- 用户冷启动:对于一个平台系统新来的用户,该用户还没有文章的交互信息,如何给该用户进行推荐。(对于我们场景就是,测试集中的用户是否在测试集对应的log数据中出现过,如果没有出现过,那么可以认为该用户是冷启动用户。但是有时候并没有这么严格,我们也可以自己设定某些指标来判别哪些用户是冷启动用户,比如通过使用时长,点击率,留存率等等)
- 系统冷启动:就是对于一个平台刚上线,还没有任何的相关历史数据,此时就是系统冷启动,其实也就是前面两种的一个综合。
对与冷启动问题的分析
文章冷启动(没有冷启动的探索问题)
这里不是为了做文章的冷启动而做冷启动,而是猜测用户可能会点击一些没有在log数据中出现的文章,要做的就是如何从将近27万的文章中选择一些文章作为用户冷启动的文章,这里其实也可以看成是一种召回策略,我们这里就采用简单的比较好理解的基于规则的召回策略来获取用户可能点击的未出现在log数据中的文章。 现在的问题变成了:如何给每个用户考虑从27万个商品中获取一小部分商品?随机选一些可能是一种方案。下面给出一些参考的方案。- 首先基于Embedding召回一部分与用户历史相似的文章
- 从基于Embedding召回的文章中通过一些规则过滤掉一些文章,使得留下的文章用户更可能点击。我们这里的规则,可以是,留下那些与用户历史点击文章主题相同的文章,或者字数相差不大的文章。并且留下的文章尽量是与测试集用户最后一次点击时间更接近的文章,或者是当天的文章也行。
- 用户冷启动
这里对测试集中的用户点击数据进行分析会发现,测试集中有百分之20的用户只有一次点击,那么这些点击特别少的用户的召回是不是可以单独做一些策略上的补充呢?或者是在排序后直接基于规则加上一些文章呢?这些都可以去尝试。
5.3 多路召回的合并
对多路合并的各路给定权重,比如都给相同权重1
,然后形成整体的一个召回结果