关于天池赛中零基础入门推荐系统 - 新闻推荐Task03多路召回

文章目录

  • 前言
  • 一、关于多路召回
    • 什么是多路召回:
  • 二、实现步骤
    • 1.读取数据:
    • 1.1 debug模式:从训练集中划出一部分数据来调试代码
    • 1.2 线下验证模式:可以只使用训练集
    • 1.3 线上模式:应该讲测试集中的点击数据合并到总的数据中
    • 2.定义多路召回字典:
    • 3.计算相似性矩阵:
    • 3.1 itemcf i2i_sim:
    • 3.2 usercf u2u_sim:
    • 3.3 什么是召回:
    • 3.4 召回常用的策略:
    • 3.4.1 关于基于文章的召回:
    • 3.4.2 关于基于usercf召回:
  • 总结


前言

经过上两篇文章,接下来本文将是此次赛题中的核心内容之一,即多路召回。相比之前两篇,个人认为会相对有趣一些。

| 打卡记录NO.3

一、关于多路召回

什么是多路召回:

  1. 采用不同的策略、特征或简单模型,分别召回一部分候选集,然后把候选集混合在一起供后续排序模型使用;
  2. “多路召回策略”是在“计算速度”和“召回率”之间进行权衡的结果。其中,各种简单策略保证候选集的快速召回,从不同角度设计的策略保证召回率接近理想的状态,不至于损伤排序效果。

举个例子:关于天池赛中零基础入门推荐系统 - 新闻推荐Task03多路召回_第1张图片
该图是多路召回的一个示意图,在多路召回中,每个策略之间毫不相关,所以一般可以写并发多线程同时进行,这样可以更加高效。

二、实现步骤

# 做召回评估的一个标志, 如果不进行评估就是直接使用全量数据进行召回
metric_recall = False

1.读取数据:

1.1 debug模式:从训练集中划出一部分数据来调试代码

由于推荐比赛的数据往往非常巨大, 如果直接采用全部的数据进行分析,搭建baseline框架, 往往会带来时间和设备上的损耗, 所以这时候我们往往需要从海量数据的训练集中随机抽取一部分样本来进行调试(train_click_log_sample), 先跑通一个baseline。

1.2 线下验证模式:可以只使用训练集

帮助我们在线下基于已有的训练集数据, 来选择好合适的模型和一些超参数。 所以我们这一块只需要加载整个训练集, 然后把整个训练集再分成训练集和验证集。

1.3 线上模式:应该讲测试集中的点击数据合并到总的数据中

我们用debug模式搭建起一个推荐系统比赛的baseline, 用线下验证模式选择好了模型和一些超参数, 这一部分就是真正的对于给定的测试集进行预测, 提交到线上, 所以这一块使用的训练数据集是全量的数据集(train_click_log+test_click_log)。

其中部分代码片段:

# 全量训练集
all_click_df = get_all_click_df(offline=False)

# 对时间戳进行归一化,用于在关联规则的时候计算权重
all_click_df['click_timestamp'] = all_click_df[['click_timestamp']].apply(max_min_scaler)

2.定义多路召回字典:

定义一个多路召回的字典:

# 定义一个多路召回的字典,将各路召回的结果都保存在这个字典当中
user_multi_recall_dict =  {'itemcf_sim_itemcf_recall': {},
                           'embedding_sim_item_recall': {},
                           'youtubednn_recall': {},
                           'youtubednn_usercf_recall': {}, 
                           'cold_start_recall': {}}
提取最后一次点击作为召回评估:
如果不需要做召回评估直接使用全量的训练集进行召回(线下验证模型);
如果不是召回评估,直接使用全量数据进行召回,不用将最后一次提取出来。

对召回效果进行评估:

# 依次评估召回的前10, 20, 30, 40, 50个文章中的击中率
def metrics_recall(user_recall_items_dict, trn_last_click_df, topk=5):
    last_click_item_dict = dict(zip(trn_last_click_df['user_id'], trn_last_click_df['click_article_id']))
    user_num = len(user_recall_items_dict)
    
    for k in range(10, topk+1, 10):
        hit_num = 0
        for user, item_list in user_recall_items_dict.items():
            # 获取前k个召回的结果
            tmp_recall_items = [x[0] for x in user_recall_items_dict[user][:k]]
            if last_click_item_dict[user] in set(tmp_recall_items):
                hit_num += 1
        
        hit_rate = round(hit_num * 1.0 / user_num, 5)
        print(' topk: ', k, ' : ', 'hit_num: ', hit_num, 'hit_rate: ', hit_rate, 'user_num : ', user_num)

3.计算相似性矩阵:

通过协同过滤以及向量检索得到相似性矩阵,相似性矩阵主要分为item2item和user2user。

3.1 itemcf i2i_sim:

  1. 用户点击的时间权重
  2. 用户点击的顺序权重
  3. 文章创建的时间权重
# 计算物品相似度
i2i_sim = {}
item_cnt = defaultdict(int)
for user, item_time_list in tqdm(user_item_time_dict.items()):
# 在基于商品的协同过滤优化的时候可以考虑时间因素
    for loc1, (i, i_click_time) in enumerate(item_time_list):
        item_cnt[i] += 1
        i2i_sim.setdefault(i, {})
        for loc2, (j, j_click_time) in enumerate(item_time_list):
            if(i == j):
               continue
                    
            # 考虑文章的正向顺序点击和反向顺序点击    
            loc_alpha = 1.0 if loc2 > loc1 else 0.7
            # 位置信息权重,其中的参数可以调节
            loc_weight = loc_alpha * (0.9 ** (np.abs(loc2 - loc1) - 1))
            # 点击时间权重,其中的参数可以调节
            click_time_weight = np.exp(0.7 ** np.abs(i_click_time - j_click_time))
            # 两篇文章创建时间的权重,其中的参数可以调节
            created_time_weight = np.exp(0.8 ** np.abs(item_created_time_dict[i] - item_created_time_dict[j]))
            i2i_sim[i].setdefault(j, 0)
            # 考虑多种因素的权重计算最终的文章之间的相似度
            i2i_sim[i][j] += loc_weight * click_time_weight * created_time_weight / math.log(len(item_time_list) + 1)

3.2 usercf u2u_sim:

使用一些简单的关联规则,比如用户活跃度权重,这里将用户的点击次数作为用户活跃度的指标。

item_user_time_dict = get_item_user_time_dict(all_click_df)
    
u2u_sim = {}
user_cnt = defaultdict(int)
for item, user_time_list in tqdm(item_user_time_dict.items()):
    for u, click_time in user_time_list:
        user_cnt[u] += 1
        u2u_sim.setdefault(u, {})
        for v, click_time in user_time_list:
           u2u_sim[u].setdefault(v, 0)
           if u == v:
              continue
           # 用户平均活跃度作为活跃度的权重,这里的式子也可以改善
           activate_weight = 100 * 0.5 * (user_activate_degree_dict[u] + user_activate_degree_dict[v])   
           u2u_sim[u][v] += activate_weight / math.log(len(user_time_list) + 1)

3.3 什么是召回:

例如有36万篇文章, 20万用户的推荐, 我们又有哪些策略来缩减问题的规模?
可以在召回阶段筛选出用户对于点击文章的候选集合, 从而降低问题的规模。

3.4 召回常用的策略:

  1. Youtube DNN 召回
  2. 基于文章的召回
  3. 文章的协同过滤
  4. 基于文章embedding的召回
  5. 基于用户的召回
  6. 用户的协同过滤
  7. 用户embedding

3.4.1 关于基于文章的召回:

  1. 考虑相似文章与历史点击文章顺序的权重(细节看代码)
  2. 考虑文章创建时间的权重,也就是考虑相似文章与历史点击文章创建时间差的权重
  3. 考虑文章内容相似度权重(使用Embedding计算相似文章相似度,但是这里需要注意,在Embedding的时候并没有计算所有商品两两之间的相似度,所以相似的文章与历史点击文章不存在相似度,需要做特殊处理)
# 基于商品的召回i2i
def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click, item_created_time_dict, emb_i2i_sim):
    """
        基于文章协同过滤的召回
        :param user_id: 用户id
        :param user_item_time_dict: 字典, 根据点击时间获取用户的点击文章序列   {user1: {item1: time1, item2: time2..}...}
        :param i2i_sim: 字典,文章相似性矩阵
        :param sim_item_topk: 整数, 选择与当前文章最相似的前k篇文章
        :param recall_item_num: 整数, 最后的召回文章数量
        :param item_topk_click: 列表,点击次数最多的文章列表,用户召回补全
        :param emb_i2i_sim: 字典基于内容embedding算的文章相似矩阵
        
        return: 召回的文章列表 {item1:score1, item2: score2...}
        
    """
    # 获取用户历史交互的文章
    user_hist_items = user_item_time_dict[user_id]
    
    item_rank = {}
    for loc, (i, click_time) in enumerate(user_hist_items):
        for j, wij in sorted(i2i_sim[i].items(), key=lambda x: x[1], reverse=True)[:sim_item_topk]:
            if j in user_hist_items:
                continue
            
            # 文章创建时间差权重
            created_time_weight = np.exp(0.8 ** np.abs(item_created_time_dict[i] - item_created_time_dict[j]))
            # 相似文章和历史点击文章序列中历史文章所在的位置权重
            loc_weight = (0.9 ** (len(user_hist_items) - loc))
            
            content_weight = 1.0
            if emb_i2i_sim.get(i, {}).get(j, None) is not None:
                content_weight += emb_i2i_sim[i][j]
            if emb_i2i_sim.get(j, {}).get(i, None) is not None:
                content_weight += emb_i2i_sim[j][i]
                
            item_rank.setdefault(j, 0)
            item_rank[j] += created_time_weight * loc_weight * content_weight * wij
    
    # 不足10个,用热门商品补全
    if len(item_rank) < recall_item_num:
        for i, item in enumerate(item_topk_click):
            if item in item_rank.items(): # 填充的item应该不在原来的列表中
                continue
            item_rank[item] = - i - 100 # 随便给个负数就行
            if len(item_rank) == recall_item_num:
                break
    
    item_rank = sorted(item_rank.items(), key=lambda x: x[1], reverse=True)[:recall_item_num]
        
    return item_rank

3.4.2 关于基于usercf召回:

思路:
计算被推荐用户历史点击文章与相似用户历史点击文章的相似度,文章创建时间差,相对位置的总和,并将其作为各自的权重。

# 基于用户的召回 u2u2i
def user_based_recommend(user_id, user_item_time_dict, u2u_sim, sim_user_topk, recall_item_num, 
                       item_topk_click, item_created_time_dict, emb_i2i_sim):
  """
      基于文章协同过滤的召回
      :param user_id: 用户id
      :param user_item_time_dict: 字典, 根据点击时间获取用户的点击文章序列   {user1: {item1: time1, item2: time2..}...}
      :param u2u_sim: 字典,文章相似性矩阵
      :param sim_user_topk: 整数, 选择与当前用户最相似的前k个用户
      :param recall_item_num: 整数, 最后的召回文章数量
      :param item_topk_click: 列表,点击次数最多的文章列表,用户召回补全
      :param item_created_time_dict: 文章创建时间列表
      :param emb_i2i_sim: 字典基于内容embedding算的文章相似矩阵
      
      return: 召回的文章列表 {item1:score1, item2: score2...}
  """
  # 历史交互
  user_item_time_list = user_item_time_dict[user_id]    # {item1: time1, item2: time2...}
  user_hist_items = set([i for i, t in user_item_time_list])   # 存在一个用户与某篇文章的多次交互, 这里得去重
  
  items_rank = {}
  for sim_u, wuv in sorted(u2u_sim[user_id].items(), key=lambda x: x[1], reverse=True)[:sim_user_topk]:
      for i, click_time in user_item_time_dict[sim_u]:
          if i in user_hist_items:
              continue
          items_rank.setdefault(i, 0)
          
          loc_weight = 1.0
          content_weight = 1.0
          created_time_weight = 1.0
          
          # 当前文章与该用户看的历史文章进行一个权重交互
          for loc, (j, click_time) in enumerate(user_item_time_list):
              # 点击时的相对位置权重
              loc_weight += 0.9 ** (len(user_item_time_list) - loc)
              # 内容相似性权重
              if emb_i2i_sim.get(i, {}).get(j, None) is not None:
                  content_weight += emb_i2i_sim[i][j]
              if emb_i2i_sim.get(j, {}).get(i, None) is not None:
                  content_weight += emb_i2i_sim[j][i]
              
              # 创建时间差权重
              created_time_weight += np.exp(0.8 * np.abs(item_created_time_dict[i] - item_created_time_dict[j]))
              
          items_rank[i] += loc_weight * content_weight * created_time_weight * wuv
      
  # 热度补全
  if len(items_rank) < recall_item_num:
      for i, item in enumerate(item_topk_click):
          if item in items_rank.items(): # 填充的item应该不在原来的列表中
              continue
          items_rank[item] = - i - 100 # 随便给个复数就行
          if len(items_rank) == recall_item_num:
              break
      
  items_rank = sorted(items_rank.items(), key=lambda x: x[1], reverse=True)[:recall_item_num]    
  
  return items_rank

| 未完待续…


总结

通过此次文章中对多路召回的大致理解,目前可以理解的内容还不够透彻,感觉理解的还很片面,接下来分享一些自己get到的一些小知识叭!

  1. 基于关联规则的itemcf:
    考虑相似文章与历史点击文章顺序的权重;
    考虑文章创建时间的权重,即考虑相似文章与历史点击文章创建时间差的权重;
    考虑文章内容相似度权重。
  2. 基于关联规则的usercf:
    给用户推荐与其相似的用户历史点击文章,因为涉及到了相似用户的历史文章,因此仍然可以加上一些关联规则来给用户可能点击的文章进行加权;
    使用的关联规则主要是考虑相似用户的历史点击文章与被推荐用户历史点击商品的关系权重。

你可能感兴趣的:(学习分享,python,大数据,机器学习)