自学记录:阿里云天池推荐系统实践 - 多路召回

关于多路召回

多路召回是指采用不同的策略、特征或者简单模型,分别召回一部分候选集,然后再把这些候选集混合在一起供后续排序模型使用的策略。
关于为啥要采用多路召回,是因为在召回的时候,计算速度和召回率这两会互相矛盾,若想要提高计算速度就要简化召回策略,而简化召回策略往往会使召回率下降,同理,若想要提高召回率的话就必须得使用复杂的召回策略,这时计算速度又会相应降低了。我们要鱼和熊掌兼得,就得权衡两者,故采用多个简单召回策略叠加的多路召回策略。
在这里插入图片描述
如图为多路召回示意图,在多路召回中,每个策略直接相互独立,并发多线程同时进行。在新闻类推荐中,我们可以按类别、作者、tag,热门新闻等分别实施召回,在选择召回策略时也需要充分考虑相关业务的特点,以实现更加贴切现实的结果。

代码部分

代码前几部分是导包、读取数据和定义各种工具函数,工具函数分别用于获取各种数据,如获取用户-文章-时间函数、获取文章-用户-时间函数、获取历史和最后一次点击、获取文章属性特征等。

召回效果评价函数
# 依次评估召回的前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)

如上所示为评估召回效果的函数代码,代码通过验证每个用户最后点击的文章是否存在于召回的数据之中,若存在则算是一次击中,击中次数加1,最后用击中次数除以用户数以算出击中率用来衡量召回效果。

itemcf i2i_sim和usercf u2u_sim
def itemcf_sim(df, item_created_time_dict):
    """
        文章与文章之间的相似性矩阵计算
        :param df: 数据表
        :item_created_time_dict:  文章创建时间的字典
        return : 文章与文章的相似性矩阵
        
        思路: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习) + 关联规则
    """
    
    user_item_time_dict = get_user_item_time(df)
    
    # 计算物品相似度
    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)
                
    i2i_sim_ = i2i_sim.copy()
    for i, related_items in i2i_sim.items():
        for j, wij in related_items.items():
            i2i_sim_[i][j] = wij / math.sqrt(item_cnt[i] * item_cnt[j])
    
    # 将得到的相似性矩阵保存到本地
        pickle.dump(i2i_sim_, open(save_path + 'itemcf_i2i_sim.pkl', 'wb'))
    
    return i2i_sim_

如上所示为itemcf i2i_sim函数,它与先前的itemcf物品相似度计算函数思路差不多,不同的是在itemcf i2i_sim中需要考虑计算新闻各个属性的权重,最后将所有属性的权重计算结果相乘并代替掉原itemcf物品相似度计算中公式上半部分中的1,而计算公式的下半部分不变,依然是将上半部分的计算结果除以喜欢两物品总人数乘积的根号值。
usercf u2u_sim函数相对于itemcf i2i_sim函数在思路上也是大体相同,不同的是itemcf i2i_sim是基于物品的,故需要考虑新闻的各个属性权重,而在usercf u2u_sim中,本次代码只涉及到了用户活跃度的考虑,故把用户点击次数作为用户的活跃度进行输入,计算用户活跃度的权重,最后代入公式上半部分替换掉原公式上半部分中的1,其余计算思路均和itemcf i2i_sim相似。

faiss的使用
# 向量检索相似度计算
# topk指的是每个item, faiss搜索后返回最相似的topk个item
def embdding_sim(click_df, item_emb_df, save_path, topk):
    """
        基于内容的文章embedding相似性矩阵计算
        :param click_df: 数据表
        :param item_emb_df: 文章的embedding
        :param save_path: 保存路径
        :patam topk: 找最相似的topk篇
        return 文章相似性矩阵
        
        思路: 对于每一篇文章, 基于embedding的相似性返回topk个与其最相似的文章, 只不过由于文章数量太多,这里用了faiss进行加速
    """
    
    # 文章索引与文章id的字典映射
    item_idx_2_rawid_dict = dict(zip(item_emb_df.index, item_emb_df['article_id']))
    
    item_emb_cols = [x for x in item_emb_df.columns if 'emb' in x]
    item_emb_np = np.ascontiguousarray(item_emb_df[item_emb_cols].values, dtype=np.float32)
    # 向量进行单位化
    item_emb_np = item_emb_np / np.linalg.norm(item_emb_np, axis=1, keepdims=True)
    
    # 建立faiss索引
    item_index = faiss.IndexFlatIP(item_emb_np.shape[1])
    item_index.add(item_emb_np)
    # 相似度查询,给每个索引位置上的向量返回topk个item以及相似度
    sim, idx = item_index.search(item_emb_np, topk) # 返回的是列表
    
    # 将向量检索的结果保存成原始id的对应关系
    item_sim_dict = collections.defaultdict(dict)
    for target_idx, sim_value_list, rele_idx_list in tqdm(zip(range(len(item_emb_np)), sim, idx)):
        target_raw_id = item_idx_2_rawid_dict[target_idx]
        # 从1开始是为了去掉商品本身, 所以最终获得的相似商品只有topk-1
        for rele_idx, sim_value in zip(rele_idx_list[1:], sim_value_list[1:]): 
            rele_raw_id = item_idx_2_rawid_dict[rele_idx]
            item_sim_dict[target_raw_id][rele_raw_id] = item_sim_dict.get(target_raw_id, {}).get(rele_raw_id, 0) + sim_value
    
    # 保存i2i相似度矩阵
    pickle.dump(item_sim_dict, open(save_path + 'emb_i2i_sim.pkl', 'wb'))   
    
    return item_sim_dict

如上所示为用faiss计算向量检索相似度的函数。函数首先建立文章索引和文章id的字典映射,之后对向量进行单位化并建立faiss索引,接下来是相似度查询,相似度查询返回的是索引值以及每个索引位置上的topk个item以及相似度的列表。最后将向量检索的结果与原始id一一对应并保存。

itemcf召回
# 基于商品的召回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]
    user_hist_items_ = {user_id for user_id, _ in user_hist_items}
    
    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

如上所示为itemcf召回函数。该函数的思路与task1中的召回函数思路大致一样,区别还是在与多了权重计算,在task1的召回中,对于某用户只需在根据相似度排序后的列表中查找不存在与用户历史点击新闻列表中的新闻并添加到候选集中即可。而此处的召回函数需要考虑文章创建时间差权重、相似文章和历史点击文章序列中历史文章所在的位置权重和新闻的embedding相似度,故最后添加到候选集的是原相似度矩阵中对应的值与这三者的乘积。最后对不足召回数的用户用热门新闻补充。
usercf召回也跟itemcf类似,usercf则考虑的是点击时的相对位置权重、内容相似性权重和创建时间差权重,最后将三者与相似度矩阵中对应的值共四者的乘积添加到候选集,不足召回数的则用热度补全。

多路召回合并

将基于itemcf计算的相似度矩阵召回结果、基于embedding搜索得到的item相似度矩阵进行召回的结果、YoutubeDNN召回结果、YoutubeDNN得到的user之间相似度进行召回的结果和基于冷启动策略召回的结果,五者进行最终的相似度融合。由于各个召回的效果不一,有好有坏,故还需要对每一路召回结果定义一些权重进行计算,再进行相似度融合。代码部分由于时间和知识量不足的关系,并没有完全看懂,以后会找时间补上来。

关于冷启动

推荐系统的主要目标是将大量的物品推荐给可能喜欢的海量用户。任何互联网推荐产品, 物品和用户都是不断增长变化的,所以一定会频繁面对新物品和新用户, 推荐系统冷启动问题指的就是对于新注册的用户或者新入库的物品, 该怎么给新用户推荐的物品让用户满意,怎么将新的物品分发出去,推荐给喜欢它的用户。
冷启动有三类,分别是文章冷启动,用户冷启动和系统冷启动。
文章冷启动:对于一个新的文章,没有任何交互记录的数据,如何对该文章进行推荐。
用户冷启动:对于一个新注册的用户,在平台没有任何点击记录,应该如何对该用户进行推荐。
系统冷启动:平台刚上线,无任何历史交互数据,要怎么对各用户推荐或向用户推荐商品。

小结

此次学习进行到了多路召回部分,在此部分开始难度也有了一个跨度的提升,对于代码前面的部分因为与先前任务有些相似所以大致看得懂,到后面embedding向量的itemcf相似度计算、YoutubeDNN、冷启动这些部分就因为知识储备不够而逐渐看不懂了。受限于时间,目前学习进度到这里,接下来的日子会好好弥补知识空缺,争取早日看懂所有代码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值