BLOG

个人博客,记录学习与生活

天池推荐系统之新闻推荐

Published Nov. 25, 2020, 10:09 p.m. by kkk

个人参加的第一次数据科学比赛,是天池上的一个新人赛,是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) $$ score(user) = \sum_{k=1}^5 \frac{s(user, k)}{k}\\ TotalScore = \frac{1}{N}\sum_{i=1}^Nscore(user_i) $$ 其中s(user, k)为预测结果是否命中最后一条点击数据,命中的话s(user, k)=1,未命中则s(user, k)=0TotalScore为最终的模型性能评分。

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. 数据分析

代码

数据分析对于实际问题非常重要,是充分了解数据、挖掘信息、指导模型实现的关键,通过数据分析可知:

  1. 训练集和测试集的用户id没有重复,也就是测试集里面的用户模型是没有见过的
  2. 训练集中用户最少的点击文章数是2, 而测试集里面用户最少的点击文章数是1
  3. 用户对于文章存在重复点击的情况, 但这个都存在于训练集里面
  4. 同一用户的点击环境存在不唯一的情况,后面做这部分特征的时候可以采用统计特征
  5. 用户点击文章的次数有很大的区分度,后面可以根据这个制作衡量用户活跃度的特征
  6. 文章被用户点击的次数也有很大的区分度,后面可以根据这个制作衡量文章热度的特征
  7. 用户看的新闻,相关性是比较强的,所以往往我们判断用户是否对某篇文章感兴趣的时候, 在很大程度上会和他历史点击过的文章有关
  8. 用户点击的文章字数有比较大的区别, 这个可以反映用户对于文章字数的区别
  9. 用户点击过的文章主题也有很大的区别, 这个可以反映用户的主题偏好 10.不同用户点击文章的时间差也会有所区别, 这个可以反映用户对于文章时效性的偏好

5. 多路召回

### 5.1 什么是多路召回,为什么需要多路召回?

多路召回,指的是通过不同的策略、特征或模型,分别召回一部分的候选集,然后把候选集混合在一起过排序模型。简而言之,就是同时独立的进行多次召回操作。

使用多路召回的目的我任务包括两部分:召回率、计算速度

首先得回到召回层的作用上,召回层是为了召回相对大的一部分候选集,供排序层使用,是为了避免排序层直接操作全量数据从而减少运算成本。因此召回层即要召回候选集又要尽可能的召回候选集,即使得召回率较高,而不同策略、特征或模型通常有其考虑的侧重点,通过混合多路召回能尽量使召回结果比较稳定全面。

另外,由于多路召回的各模型独立运行,通过分布式或并发多线程可以同时执行,会提高整体计算速度。

多路召回通常包括:兴趣标签、兴趣Topic、兴趣实体、协同过滤、热门等等

5.2 根据数据构建的召回

  • itemcf 物品协同(带优化策略的)
  • embedding 根据物品embedding进行召回
  • youtubednn 召回
  • 根据youtubednn得出的用户embedding进行召回
  • 冷启动召回

5.3 多路召回的合并

对多路合并的各路给定权重,比如都给相同权重1,然后形成整体的一个召回结果


Share this post
< Pre: 集成学习 Pos: 机器学习损失函数 >
434 comments
Similar posts
Add a new comment