新聞推薦03——多路召回

多路召回

所謂的“多路召回”策略,就是指采用不同的策略、特征或簡(jiǎn)單模型外遇,分別召回一部分候選集,然后把候選集混合在一起供后續(xù)排序模型使用契吉,可以明顯的看出跳仿,“多路召回策略”是在“計(jì)算速度”和“召回率”之間進(jìn)行權(quán)衡的結(jié)果。其中捐晶,各種簡(jiǎn)單策略保證候選集的快速召回菲语,從不同角度設(shè)計(jì)的策略保證召回率接近理想的狀態(tài),不至于損傷排序效果惑灵。如下圖是多路召回的一個(gè)示意圖山上,在多路召回中,每個(gè)策略之間毫不相關(guān)英支,所以一般可以寫(xiě)并發(fā)多線(xiàn)程同時(shí)進(jìn)行佩憾,這樣可以更加高效。

image-20201128162342666

上圖只是一個(gè)多路召回的例子干花,也就是說(shuō)可以使用多種不同的策略來(lái)獲取用戶(hù)排序的候選商品集合妄帘,而具體使用哪些召回策略其實(shí)是與業(yè)務(wù)強(qiáng)相關(guān)的 ,針對(duì)不同的任務(wù)就會(huì)有對(duì)于該業(yè)務(wù)真實(shí)場(chǎng)景下需要考慮的召回規(guī)則池凄。例如新聞推薦抡驼,召回規(guī)則可以是“熱門(mén)視頻”、“導(dǎo)演召回”肿仑、“演員召回”致盟、“最近上映“、”流行趨勢(shì)“柏副、”類(lèi)型召回“等等勾邦。

導(dǎo)包

import pandas as pd  
import numpy as np
from tqdm import tqdm  
from collections import defaultdict  
import os, math, warnings, math, pickle
from tqdm import tqdm
import faiss
import collections
import random
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import LabelEncoder
from datetime import datetime
from deepctr.feature_column import SparseFeat, VarLenSparseFeat
from sklearn.preprocessing import LabelEncoder
from tensorflow.python.keras import backend as K
from tensorflow.python.keras.models import Model
from tensorflow.python.keras.preprocessing.sequence import pad_sequences

from deepmatch.models import *
from deepmatch.utils import sampledsoftmaxloss
warnings.filterwarnings('ignore')
data_path = './data_raw/'
save_path = './temp_results/'
# 做召回評(píng)估的一個(gè)標(biāo)志, 如果不進(jìn)行評(píng)估就是直接使用全量數(shù)據(jù)進(jìn)行召回
metric_recall = False

讀取數(shù)據(jù)

在一般的推薦系統(tǒng)比賽中讀取數(shù)據(jù)部分主要分為三種模式, 不同的模式對(duì)應(yīng)的不同的數(shù)據(jù)集:

  1. Debug模式: 這個(gè)的目的是幫助我們基于數(shù)據(jù)先搭建一個(gè)簡(jiǎn)易的baseline并跑通割择, 保證寫(xiě)的baseline代碼沒(méi)有什么問(wèn)題眷篇。 由于推薦比賽的數(shù)據(jù)往往非常巨大, 如果一上來(lái)直接采用全部的數(shù)據(jù)進(jìn)行分析荔泳,搭建baseline框架蕉饼, 往往會(huì)帶來(lái)時(shí)間和設(shè)備上的損耗, 所以這時(shí)候我們往往需要從海量數(shù)據(jù)的訓(xùn)練集中隨機(jī)抽取一部分樣本來(lái)進(jìn)行調(diào)試(train_click_log_sample)玛歌, 先跑通一個(gè)baseline昧港。
  2. 線(xiàn)下驗(yàn)證模式: 這個(gè)的目的是幫助我們?cè)诰€(xiàn)下基于已有的訓(xùn)練集數(shù)據(jù), 來(lái)選擇好合適的模型和一些超參數(shù)支子。 所以我們這一塊只需要加載整個(gè)訓(xùn)練集(train_click_log)创肥, 然后把整個(gè)訓(xùn)練集再分成訓(xùn)練集和驗(yàn)證集。 訓(xùn)練集是模型的訓(xùn)練數(shù)據(jù), 驗(yàn)證集部分幫助我們調(diào)整模型的參數(shù)和其他的一些超參數(shù)叹侄。
  3. 線(xiàn)上模式: 我們用debug模式搭建起一個(gè)推薦系統(tǒng)比賽的baseline巩搏, 用線(xiàn)下驗(yàn)證模式選擇好了模型和一些超參數(shù), 這一部分就是真正的對(duì)于給定的測(cè)試集進(jìn)行預(yù)測(cè)趾代, 提交到線(xiàn)上贯底, 所以這一塊使用的訓(xùn)練數(shù)據(jù)集是全量的數(shù)據(jù)集(train_click_log+test_click_log)

下面就分別對(duì)這三種不同的數(shù)據(jù)讀取模式先建立不同的代導(dǎo)入函數(shù), 方便后面針對(duì)不同的模式下導(dǎo)入數(shù)據(jù)撒强。

# debug模式: 從訓(xùn)練集中劃出一部分?jǐn)?shù)據(jù)來(lái)調(diào)試代碼
def get_all_click_sample(data_path, sample_nums=10000):
    """
        訓(xùn)練集中采樣一部分?jǐn)?shù)據(jù)調(diào)試
        data_path: 原數(shù)據(jù)的存儲(chǔ)路徑
        sample_nums: 采樣數(shù)目(這里由于機(jī)器的內(nèi)存限制禽捆,可以采樣用戶(hù)做)
    """
    all_click = pd.read_csv(data_path + 'train_click_log.csv')
    all_user_ids = all_click.user_id.unique()

    sample_user_ids = np.random.choice(all_user_ids, size=sample_nums, replace=False) 
    all_click = all_click[all_click['user_id'].isin(sample_user_ids)]
    
    all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
    return all_click

# 讀取點(diǎn)擊數(shù)據(jù),這里分成線(xiàn)上和線(xiàn)下飘哨,如果是為了獲取線(xiàn)上提交結(jié)果應(yīng)該講測(cè)試集中的點(diǎn)擊數(shù)據(jù)合并到總的數(shù)據(jù)中
# 如果是為了線(xiàn)下驗(yàn)證模型的有效性或者特征的有效性胚想,可以只使用訓(xùn)練集
def get_all_click_df(data_path='./data_raw/', offline=True):
    if offline:
        all_click = pd.read_csv(data_path + 'train_click_log.csv')
    else:
        trn_click = pd.read_csv(data_path + 'train_click_log.csv')
        tst_click = pd.read_csv(data_path + 'testA_click_log.csv')

        all_click = trn_click.append(tst_click)
    
    all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
    return all_click
# 讀取文章的基本屬性
def get_item_info_df(data_path):
    item_info_df = pd.read_csv(data_path + 'articles.csv')
    
    # 為了方便與訓(xùn)練集中的click_article_id拼接,需要把a(bǔ)rticle_id修改成click_article_id
    item_info_df = item_info_df.rename(columns={'article_id': 'click_article_id'})
    
    return item_info_df
# 讀取文章的Embedding數(shù)據(jù)
def get_item_emb_dict(data_path):
    item_emb_df = pd.read_csv(data_path + 'articles_emb.csv')
    
    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])
    # 進(jìn)行歸一化
    item_emb_np = item_emb_np / np.linalg.norm(item_emb_np, axis=1, keepdims=True)

    item_emb_dict = dict(zip(item_emb_df['article_id'], item_emb_np))
    pickle.dump(item_emb_dict, open(save_path + 'item_content_emb.pkl', 'wb'))
    
    return item_emb_dict
max_min_scaler = lambda x : (x-np.min(x))/(np.max(x)-np.min(x))
# 采樣數(shù)據(jù)
# all_click_df = get_all_click_sample(data_path)

# 全量訓(xùn)練集
all_click_df = get_all_click_df(offline=False)

# 對(duì)時(shí)間戳進(jìn)行歸一化,用于在關(guān)聯(lián)規(guī)則的時(shí)候計(jì)算權(quán)重
all_click_df['click_timestamp'] = all_click_df[['click_timestamp']].apply(max_min_scaler)
item_info_df = get_item_info_df(data_path)
item_emb_dict = get_item_emb_dict(data_path)

工具函數(shù)

獲取用戶(hù)-文章-時(shí)間函數(shù)

這個(gè)在基于關(guān)聯(lián)規(guī)則的用戶(hù)協(xié)同過(guò)濾的時(shí)候會(huì)用到

# 根據(jù)點(diǎn)擊時(shí)間獲取用戶(hù)的點(diǎn)擊文章序列   {user1: {item1: time1, item2: time2..}...}
def get_user_item_time(click_df):
    
    click_df = click_df.sort_values('click_timestamp')
    
    def make_item_time_pair(df):
        return list(zip(df['click_article_id'], df['click_timestamp']))
    
    user_item_time_df = click_df.groupby('user_id')['click_article_id', 'click_timestamp'].apply(lambda x: make_item_time_pair(x))\
                                                            .reset_index().rename(columns={0: 'item_time_list'})
    user_item_time_dict = dict(zip(user_item_time_df['user_id'], user_item_time_df['item_time_list']))
    
    return user_item_time_dict

獲取文章-用戶(hù)-時(shí)間函數(shù)

這個(gè)在基于關(guān)聯(lián)規(guī)則的文章協(xié)同過(guò)濾的時(shí)候會(huì)用到

# 根據(jù)時(shí)間獲取商品被點(diǎn)擊的用戶(hù)序列  {item1: {user1: time1, user2: time2...}...}
# 這里的時(shí)間是用戶(hù)點(diǎn)擊當(dāng)前商品的時(shí)間芽隆,好像沒(méi)有直接的關(guān)系顿仇。
def get_item_user_time_dict(click_df):
    def make_user_time_pair(df):
        return list(zip(df['user_id'], df['click_timestamp']))
    
    click_df = click_df.sort_values('click_timestamp')
    item_user_time_df = click_df.groupby('click_article_id')['user_id', 'click_timestamp'].apply(lambda x: make_user_time_pair(x))\
                                                            .reset_index().rename(columns={0: 'user_time_list'})
    
    item_user_time_dict = dict(zip(item_user_time_df['click_article_id'], item_user_time_df['user_time_list']))
    return item_user_time_dict

獲取歷史和最后一次點(diǎn)擊

這個(gè)在評(píng)估召回結(jié)果, 特征工程和制作標(biāo)簽轉(zhuǎn)成監(jiān)督學(xué)習(xí)測(cè)試集的時(shí)候回用到

# 獲取當(dāng)前數(shù)據(jù)的歷史點(diǎn)擊和最后一次點(diǎn)擊
def get_hist_and_last_click(all_click):
    
    all_click = all_click.sort_values(by=['user_id', 'click_timestamp'])
    click_last_df = all_click.groupby('user_id').tail(1)

    # 如果用戶(hù)只有一個(gè)點(diǎn)擊摆马,hist為空了,會(huì)導(dǎo)致訓(xùn)練的時(shí)候這個(gè)用戶(hù)不可見(jiàn)鸿吆,此時(shí)默認(rèn)泄露一下
    def hist_func(user_df):
        if len(user_df) == 1:
            return user_df
        else:
            return user_df[:-1]

    click_hist_df = all_click.groupby('user_id').apply(hist_func).reset_index(drop=True)

    return click_hist_df, click_last_df

獲取文章屬性特征

# 獲取文章id對(duì)應(yīng)的基本屬性囤采,保存成字典的形式,方便后面召回階段惩淳,冷啟動(dòng)階段直接使用
def get_item_info_dict(item_info_df):
    max_min_scaler = lambda x : (x-np.min(x))/(np.max(x)-np.min(x))
    item_info_df['created_at_ts'] = item_info_df[['created_at_ts']].apply(max_min_scaler)
    
    item_type_dict = dict(zip(item_info_df['click_article_id'], item_info_df['category_id']))
    item_words_dict = dict(zip(item_info_df['click_article_id'], item_info_df['words_count']))
    item_created_time_dict = dict(zip(item_info_df['click_article_id'], item_info_df['created_at_ts']))
    
    return item_type_dict, item_words_dict, item_created_time_dict

獲取用戶(hù)歷史點(diǎn)擊的文章信息

def get_user_hist_item_info_dict(all_click):
    
    # 獲取user_id對(duì)應(yīng)的用戶(hù)歷史點(diǎn)擊文章類(lèi)型的集合字典
    user_hist_item_typs = all_click.groupby('user_id')['category_id'].agg(set).reset_index()
    user_hist_item_typs_dict = dict(zip(user_hist_item_typs['user_id'], user_hist_item_typs['category_id']))
    
    # 獲取user_id對(duì)應(yīng)的用戶(hù)點(diǎn)擊文章的集合
    user_hist_item_ids_dict = all_click.groupby('user_id')['click_article_id'].agg(set).reset_index()
    user_hist_item_ids_dict = dict(zip(user_hist_item_ids_dict['user_id'], user_hist_item_ids_dict['click_article_id']))
    
    # 獲取user_id對(duì)應(yīng)的用戶(hù)歷史點(diǎn)擊的文章的平均字?jǐn)?shù)字典
    user_hist_item_words = all_click.groupby('user_id')['words_count'].agg('mean').reset_index()
    user_hist_item_words_dict = dict(zip(user_hist_item_words['user_id'], user_hist_item_words['words_count']))
    
    # 獲取user_id對(duì)應(yīng)的用戶(hù)最后一次點(diǎn)擊的文章的創(chuàng)建時(shí)間
    all_click_ = all_click.sort_values('click_timestamp')
    user_last_item_created_time = all_click_.groupby('user_id')['created_at_ts'].apply(lambda x: x.iloc[-1]).reset_index()
    
    max_min_scaler = lambda x : (x-np.min(x))/(np.max(x)-np.min(x))
    user_last_item_created_time['created_at_ts'] = user_last_item_created_time[['created_at_ts']].apply(max_min_scaler)
    
    user_last_item_created_time_dict = dict(zip(user_last_item_created_time['user_id'], \
                                                user_last_item_created_time['created_at_ts']))
    
    return user_hist_item_typs_dict, user_hist_item_ids_dict, user_hist_item_words_dict, user_last_item_created_time_dict

獲取點(diǎn)擊次數(shù)最多的Top-k個(gè)文章

# 獲取近期點(diǎn)擊最多的文章
def get_item_topk_click(click_df, k):
    topk_click = click_df['click_article_id'].value_counts().index[:k]
    return topk_click

定義多路召回字典

# 獲取文章的屬性信息蕉毯,保存成字典的形式方便查詢(xún)
item_type_dict, item_words_dict, item_created_time_dict = get_item_info_dict(item_info_df)
# 定義一個(gè)多路召回的字典,將各路召回的結(jié)果都保存在這個(gè)字典當(dāng)中
user_multi_recall_dict =  {'itemcf_sim_itemcf_recall': {},
                           'embedding_sim_item_recall': {},
                           'youtubednn_recall': {},
                           'youtubednn_usercf_recall': {}, 
                           'cold_start_recall': {}}
# 提取最后一次點(diǎn)擊作為召回評(píng)估思犁,如果不需要做召回評(píng)估直接使用全量的訓(xùn)練集進(jìn)行召回(線(xiàn)下驗(yàn)證模型)
# 如果不是召回評(píng)估代虾,直接使用全量數(shù)據(jù)進(jìn)行召回,不用將最后一次提取出來(lái)
trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)

召回效果評(píng)估

做完了召回有時(shí)候也需要對(duì)當(dāng)前的召回方法或者參數(shù)進(jìn)行調(diào)整以達(dá)到更好的召回效果激蹲,因?yàn)檎倩氐慕Y(jié)果決定了最終排序的上限棉磨,下面也會(huì)提供一個(gè)召回評(píng)估的方法

# 依次評(píng)估召回的前10, 20, 30, 40, 50個(gè)文章中的擊中率
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個(gè)召回的結(jié)果
            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)

計(jì)算相似性矩陣

這一部分主要是通過(guò)協(xié)同過(guò)濾以及向量檢索得到相似性矩陣,相似性矩陣主要分為user2user和item2item学辱,下面依次獲取基于itemCF的item2item的相似性矩陣乘瓤。

itemCF i2i_sim

借鑒KDD2020的去偏商品推薦,在計(jì)算item2item相似性矩陣時(shí)策泣,使用關(guān)聯(lián)規(guī)則衙傀,使得計(jì)算的文章的相似性還考慮到了:

  1. 用戶(hù)點(diǎn)擊的時(shí)間權(quán)重
  2. 用戶(hù)點(diǎn)擊的順序權(quán)重
  3. 文章創(chuàng)建的時(shí)間權(quán)重
def itemcf_sim(df, item_created_time_dict):
    """
        文章與文章之間的相似性矩陣計(jì)算
        :param df: 數(shù)據(jù)表
        :item_created_time_dict:  文章創(chuàng)建時(shí)間的字典
        return : 文章與文章的相似性矩陣
        
        思路: 基于物品的協(xié)同過(guò)濾(詳細(xì)請(qǐng)參考上一期推薦系統(tǒng)基礎(chǔ)的組隊(duì)學(xué)習(xí)) + 關(guān)聯(lián)規(guī)則
    """
    
    user_item_time_dict = get_user_item_time(df)
    
    # 計(jì)算物品相似度
    i2i_sim = {}
    item_cnt = defaultdict(int)
    for user, item_time_list in tqdm(user_item_time_dict.items()):
        # 在基于商品的協(xié)同過(guò)濾優(yōu)化的時(shí)候可以考慮時(shí)間因素
        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
                    
                # 考慮文章的正向順序點(diǎn)擊和反向順序點(diǎn)擊    
                loc_alpha = 1.0 if loc2 > loc1 else 0.7
                # 位置信息權(quán)重,其中的參數(shù)可以調(diào)節(jié)
                loc_weight = loc_alpha * (0.9 ** (np.abs(loc2 - loc1) - 1))
                # 點(diǎn)擊時(shí)間權(quán)重萨咕,其中的參數(shù)可以調(diào)節(jié)
                click_time_weight = np.exp(0.7 ** np.abs(i_click_time - j_click_time))
                # 兩篇文章創(chuàng)建時(shí)間的權(quán)重统抬,其中的參數(shù)可以調(diào)節(jié)
                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)
                # 考慮多種因素的權(quán)重計(jì)算最終的文章之間的相似度
                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_
i2i_sim = itemcf_sim(all_click_df, item_created_time_dict)
100%|██████████| 250000/250000 [14:20<00:00, 290.38it/s]

userCF u2u_sim

在計(jì)算用戶(hù)之間的相似度的時(shí)候,也可以使用一些簡(jiǎn)單的關(guān)聯(lián)規(guī)則,比如用戶(hù)活躍度權(quán)重聪建,這里將用戶(hù)的點(diǎn)擊次數(shù)作為用戶(hù)活躍度的指標(biāo)

def get_user_activate_degree_dict(all_click_df):
    all_click_df_ = all_click_df.groupby('user_id')['click_article_id'].count().reset_index()
    
    # 用戶(hù)活躍度歸一化
    mm = MinMaxScaler()
    all_click_df_['click_article_id'] = mm.fit_transform(all_click_df_[['click_article_id']])
    user_activate_degree_dict = dict(zip(all_click_df_['user_id'], all_click_df_['click_article_id']))
    
    return user_activate_degree_dict
def usercf_sim(all_click_df, user_activate_degree_dict):
    """
        用戶(hù)相似性矩陣計(jì)算
        :param all_click_df: 數(shù)據(jù)表
        :param user_activate_degree_dict: 用戶(hù)活躍度的字典
        return 用戶(hù)相似性矩陣
        
        思路: 基于用戶(hù)的協(xié)同過(guò)濾(詳細(xì)請(qǐng)參考上一期推薦系統(tǒng)基礎(chǔ)的組隊(duì)學(xué)習(xí)) + 關(guān)聯(lián)規(guī)則
    """
    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
                # 用戶(hù)平均活躍度作為活躍度的權(quán)重钙畔,這里的式子也可以改善
                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)
    
    u2u_sim_ = u2u_sim.copy()
    for u, related_users in u2u_sim.items():
        for v, wij in related_users.items():
            u2u_sim_[u][v] = wij / math.sqrt(user_cnt[u] * user_cnt[v])
    
    # 將得到的相似性矩陣保存到本地
    pickle.dump(u2u_sim_, open(save_path + 'usercf_u2u_sim.pkl', 'wb'))

    return u2u_sim_
# 由于usercf計(jì)算時(shí)候太耗費(fèi)內(nèi)存了,這里就不直接運(yùn)行了
# 如果是采樣的話(huà)妆偏,是可以運(yùn)行的
user_activate_degree_dict = get_user_activate_degree_dict(all_click_df)
u2u_sim = usercf_sim(all_click_df, user_activate_degree_dict)

item embedding sim

使用Embedding計(jì)算item之間的相似度是為了后續(xù)冷啟動(dòng)的時(shí)候可以獲取未出現(xiàn)在點(diǎn)擊數(shù)據(jù)中的文章刃鳄,后面有對(duì)冷啟動(dòng)專(zhuān)門(mén)的介紹,這里簡(jiǎn)單的說(shuō)一下faiss钱骂。

aiss是Facebook的AI團(tuán)隊(duì)開(kāi)源的一套用于做聚類(lèi)或者相似性搜索的軟件庫(kù)叔锐,底層是用C++實(shí)現(xiàn)。Faiss因?yàn)槌?jí)優(yōu)越的性能见秽,被廣泛應(yīng)用于推薦相關(guān)的業(yè)務(wù)當(dāng)中.

faiss工具包一般使用在推薦系統(tǒng)中的向量召回部分愉烙。在做向量召回的時(shí)候要么是u2u,u2i或者i2i,這里的u和i指的是user和item.我們知道在實(shí)際的場(chǎng)景中user和item的數(shù)量都是海量的解取,我們最容易想到的基于向量相似度的召回就是使用兩層循環(huán)遍歷user列表或者item列表計(jì)算兩個(gè)向量的相似度步责,但是這樣做在面對(duì)海量數(shù)據(jù)是不切實(shí)際的,faiss就是用來(lái)加速計(jì)算某個(gè)查詢(xún)向量最相似的topk個(gè)索引向量禀苦。

faiss查詢(xún)的原理:

faiss使用了PCA和PQ(Product quantization乘積量化)兩種技術(shù)進(jìn)行向量壓縮和編碼蔓肯,當(dāng)然還使用了其他的技術(shù)進(jìn)行優(yōu)化,但是PCA和PQ是其中最核心部分振乏。

  1. PCA降維算法細(xì)節(jié)參考下面這個(gè)鏈接進(jìn)行學(xué)習(xí)
    主成分分析(PCA)原理總結(jié)

  2. PQ編碼的細(xì)節(jié)下面這個(gè)鏈接進(jìn)行學(xué)習(xí)
    實(shí)例理解product quantization算法

faiss使用

faiss官方教程

# 向量檢索相似度計(jì)算
# topk指的是每個(gè)item, faiss搜索后返回最相似的topk個(gè)item
def embdding_sim(click_df, item_emb_df, save_path, topk):
    """
        基于內(nèi)容的文章embedding相似性矩陣計(jì)算
        :param click_df: 數(shù)據(jù)表
        :param item_emb_df: 文章的embedding
        :param save_path: 保存路徑
        :patam topk: 找最相似的topk篇
        return 文章相似性矩陣
        
        思路: 對(duì)于每一篇文章蔗包, 基于embedding的相似性返回topk個(gè)與其最相似的文章, 只不過(guò)由于文章數(shù)量太多慧邮,這里用了faiss進(jìn)行加速
    """
    
    # 文章索引與文章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)
    # 向量進(jìn)行單位化
    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)
    # 相似度查詢(xún)调限,給每個(gè)索引位置上的向量返回topk個(gè)item以及相似度
    sim, idx = item_index.search(item_emb_np, topk) # 返回的是列表
    
    # 將向量檢索的結(jié)果保存成原始id的對(duì)應(yīng)關(guān)系
    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開(kāi)始是為了去掉商品本身, 所以最終獲得的相似商品只有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
item_emb_df = pd.read_csv(data_path + '/articles_emb.csv')
emb_i2i_sim = embdding_sim(all_click_df, item_emb_df, save_path, topk=10) # topk可以自行設(shè)置
364047it [00:23, 15292.14it/s]

召回

這個(gè)就是我們開(kāi)篇提到的那個(gè)問(wèn)題, 面的36萬(wàn)篇文章误澳, 20多萬(wàn)用戶(hù)的推薦耻矮, 我們又有哪些策略來(lái)縮減問(wèn)題的規(guī)模? 我們就可以再召回階段篩選出用戶(hù)對(duì)于點(diǎn)擊文章的候選集合忆谓, 從而降低問(wèn)題的規(guī)模裆装。召回常用的策略:

  • Youtube DNN 召回
  • 基于文章的召回
    • 文章的協(xié)同過(guò)濾
    • 基于文章embedding的召回
  • 基于用戶(hù)的召回
    • 用戶(hù)的協(xié)同過(guò)濾
    • 用戶(hù)embedding

上面的各種召回方式一部分在基于用戶(hù)已經(jīng)看得文章的基礎(chǔ)上去召回與這些文章相似的一些文章, 而這個(gè)相似性的計(jì)算方式不同倡缠, 就得到了不同的召回方式米母, 比如文章的協(xié)同過(guò)濾, 文章內(nèi)容的embedding等毡琉。還有一部分是根據(jù)用戶(hù)的相似性進(jìn)行推薦铁瞒,對(duì)于某用戶(hù)推薦與其相似的其他用戶(hù)看過(guò)的文章,比如用戶(hù)的協(xié)同過(guò)濾和用戶(hù)embedding桅滋。 還有一種思路是類(lèi)似矩陣分解的思路慧耍,先計(jì)算出用戶(hù)和文章的embedding之后身辨,就可以直接算用戶(hù)和文章的相似度, 根據(jù)這個(gè)相似度進(jìn)行推薦芍碧, 比如YouTube DNN煌珊。 我們下面詳細(xì)來(lái)看一下每一個(gè)召回方法:

YoutubeDNN召回

(這一步是直接獲取用戶(hù)召回的候選文章列表)

論文下載地址

Youtubednn召回架構(gòu)

image-20201111160516562

關(guān)于YoutubeDNN原理和應(yīng)用推薦看王喆的兩篇博客:

  1. 重讀Youtube深度學(xué)習(xí)推薦系統(tǒng)論文,字字珠璣泌豆,驚為神文
  2. YouTube深度學(xué)習(xí)推薦系統(tǒng)的十大工程問(wèn)題

參考文獻(xiàn):

  1. https://zhuanlan.zhihu.com/p/52169807 (YouTubeDNN原理)
  2. https://zhuanlan.zhihu.com/p/26306795 (Word2Vec知乎眾贊文章) --- word2vec放到排序中的w2v的介紹部分
# 獲取雙塔召回時(shí)的訓(xùn)練驗(yàn)證數(shù)據(jù)
# negsample指的是通過(guò)滑窗構(gòu)建樣本的時(shí)候定庵,負(fù)樣本的數(shù)量
def gen_data_set(data, negsample=0):
    data.sort_values("click_timestamp", inplace=True)
    item_ids = data['click_article_id'].unique()

    train_set = []
    test_set = []
    for reviewerID, hist in tqdm(data.groupby('user_id')):
        pos_list = hist['click_article_id'].tolist()
        
        if negsample > 0:
            candidate_set = list(set(item_ids) - set(pos_list))   # 用戶(hù)沒(méi)看過(guò)的文章里面選擇負(fù)樣本
            neg_list = np.random.choice(candidate_set,size=len(pos_list)*negsample,replace=True)  # 對(duì)于每個(gè)正樣本,選擇n個(gè)負(fù)樣本
            
        # 長(zhǎng)度只有一個(gè)的時(shí)候踪危,需要把這條數(shù)據(jù)也放到訓(xùn)練集中蔬浙,不然的話(huà)最終學(xué)到的embedding就會(huì)有缺失
        if len(pos_list) == 1:
            train_set.append((reviewerID, [pos_list[0]], pos_list[0],1,len(pos_list)))
            test_set.append((reviewerID, [pos_list[0]], pos_list[0],1,len(pos_list)))
            
        # 滑窗構(gòu)造正負(fù)樣本
        for i in range(1, len(pos_list)):
            hist = pos_list[:i]
            
            if i != len(pos_list) - 1:
                train_set.append((reviewerID, hist[::-1], pos_list[i], 1, len(hist[::-1])))  # 正樣本 [user_id, his_item, pos_item, label, len(his_item)]
                for negi in range(negsample):
                    train_set.append((reviewerID, hist[::-1], neg_list[i*negsample+negi], 0,len(hist[::-1]))) # 負(fù)樣本 [user_id, his_item, neg_item, label, len(his_item)]
            else:
                # 將最長(zhǎng)的那一個(gè)序列長(zhǎng)度作為測(cè)試數(shù)據(jù)
                test_set.append((reviewerID, hist[::-1], pos_list[i],1,len(hist[::-1])))
                
    random.shuffle(train_set)
    random.shuffle(test_set)
    
    return train_set, test_set

# 將輸入的數(shù)據(jù)進(jìn)行padding,使得序列特征的長(zhǎng)度都一致
def gen_model_input(train_set,user_profile,seq_max_len):

    train_uid = np.array([line[0] for line in train_set])
    train_seq = [line[1] for line in train_set]
    train_iid = np.array([line[2] for line in train_set])
    train_label = np.array([line[3] for line in train_set])
    train_hist_len = np.array([line[4] for line in train_set])

    train_seq_pad = pad_sequences(train_seq, maxlen=seq_max_len, padding='post', truncating='post', value=0)
    train_model_input = {"user_id": train_uid, "click_article_id": train_iid, "hist_article_id": train_seq_pad,
                         "hist_len": train_hist_len}

    return train_model_input, train_label
def youtubednn_u2i_dict(data, topk=20):    
    sparse_features = ["click_article_id", "user_id"]
    SEQ_LEN = 30 # 用戶(hù)點(diǎn)擊序列的長(zhǎng)度贞远,短的填充畴博,長(zhǎng)的截?cái)?    
    user_profile_ = data[["user_id"]].drop_duplicates('user_id')
    item_profile_ = data[["click_article_id"]].drop_duplicates('click_article_id')  
    
    # 類(lèi)別編碼
    features = ["click_article_id", "user_id"]
    feature_max_idx = {}
    
    for feature in features:
        lbe = LabelEncoder()
        data[feature] = lbe.fit_transform(data[feature])
        feature_max_idx[feature] = data[feature].max() + 1
    
    # 提取user和item的畫(huà)像,這里具體選擇哪些特征還需要進(jìn)一步的分析和考慮
    user_profile = data[["user_id"]].drop_duplicates('user_id')
    item_profile = data[["click_article_id"]].drop_duplicates('click_article_id')  
    
    user_index_2_rawid = dict(zip(user_profile['user_id'], user_profile_['user_id']))
    item_index_2_rawid = dict(zip(item_profile['click_article_id'], item_profile_['click_article_id']))
    
    # 劃分訓(xùn)練和測(cè)試集
    # 由于深度學(xué)習(xí)需要的數(shù)據(jù)量通常都是非常大的蓝仲,所以為了保證召回的效果俱病,往往會(huì)通過(guò)滑窗的形式擴(kuò)充訓(xùn)練樣本
    train_set, test_set = gen_data_set(data, 0)
    # 整理輸入數(shù)據(jù),具體的操作可以看上面的函數(shù)
    train_model_input, train_label = gen_model_input(train_set, user_profile, SEQ_LEN)
    test_model_input, test_label = gen_model_input(test_set, user_profile, SEQ_LEN)
    
    # 確定Embedding的維度
    embedding_dim = 16
    
    # 將數(shù)據(jù)整理成模型可以直接輸入的形式
    user_feature_columns = [SparseFeat('user_id', feature_max_idx['user_id'], embedding_dim),
                            VarLenSparseFeat(SparseFeat('hist_article_id', feature_max_idx['click_article_id'], embedding_dim,
                                                        embedding_name="click_article_id"), SEQ_LEN, 'mean', 'hist_len'),]
    item_feature_columns = [SparseFeat('click_article_id', feature_max_idx['click_article_id'], embedding_dim)]
    
    # 模型的定義 
    # num_sampled: 負(fù)采樣時(shí)的樣本數(shù)量
    model = YoutubeDNN(user_feature_columns, item_feature_columns, num_sampled=5, user_dnn_hidden_units=(64, embedding_dim))
    # 模型編譯
    model.compile(optimizer="adam", loss=sampledsoftmaxloss)  
    
    # 模型訓(xùn)練袱结,這里可以定義驗(yàn)證集的比例亮隙,如果設(shè)置為0的話(huà)就是全量數(shù)據(jù)直接進(jìn)行訓(xùn)練
    history = model.fit(train_model_input, train_label, batch_size=256, epochs=1, verbose=1, validation_split=0.0)
    
    # 訓(xùn)練完模型之后,提取訓(xùn)練的Embedding,包括user端和item端
    test_user_model_input = test_model_input
    all_item_model_input = {"click_article_id": item_profile['click_article_id'].values}

    user_embedding_model = Model(inputs=model.user_input, outputs=model.user_embedding)
    item_embedding_model = Model(inputs=model.item_input, outputs=model.item_embedding)
    
    # 保存當(dāng)前的item_embedding 和 user_embedding 排序的時(shí)候可能能夠用到垢夹,但是需要注意保存的時(shí)候需要和原始的id對(duì)應(yīng)
    user_embs = user_embedding_model.predict(test_user_model_input, batch_size=2 ** 12)
    item_embs = item_embedding_model.predict(all_item_model_input, batch_size=2 ** 12)
    
    # embedding保存之前歸一化一下
    user_embs = user_embs / np.linalg.norm(user_embs, axis=1, keepdims=True)
    item_embs = item_embs / np.linalg.norm(item_embs, axis=1, keepdims=True)
    
    # 將Embedding轉(zhuǎn)換成字典的形式方便查詢(xún)
    raw_user_id_emb_dict = {user_index_2_rawid[k]: \
                                v for k, v in zip(user_profile['user_id'], user_embs)}
    raw_item_id_emb_dict = {item_index_2_rawid[k]: \
                                v for k, v in zip(item_profile['click_article_id'], item_embs)}
    # 將Embedding保存到本地
    pickle.dump(raw_user_id_emb_dict, open(save_path + 'user_youtube_emb.pkl', 'wb'))
    pickle.dump(raw_item_id_emb_dict, open(save_path + 'item_youtube_emb.pkl', 'wb'))
    
    # faiss緊鄰搜索咱揍,通過(guò)user_embedding 搜索與其相似性最高的topk個(gè)item
    index = faiss.IndexFlatIP(embedding_dim)
    # 上面已經(jīng)進(jìn)行了歸一化,這里可以不進(jìn)行歸一化了
#     faiss.normalize_L2(user_embs)
#     faiss.normalize_L2(item_embs)
    index.add(item_embs) # 將item向量構(gòu)建索引
    sim, idx = index.search(np.ascontiguousarray(user_embs), topk) # 通過(guò)user去查詢(xún)最相似的topk個(gè)item
    
    user_recall_items_dict = collections.defaultdict(dict)
    for target_idx, sim_value_list, rele_idx_list in tqdm(zip(test_user_model_input['user_id'], sim, idx)):
        target_raw_id = user_index_2_rawid[target_idx]
        # 從1開(kāi)始是為了去掉商品本身, 所以最終獲得的相似商品只有topk-1
        for rele_idx, sim_value in zip(rele_idx_list[1:], sim_value_list[1:]): 
            rele_raw_id = item_index_2_rawid[rele_idx]
            user_recall_items_dict[target_raw_id][rele_raw_id] = user_recall_items_dict.get(target_raw_id, {})\
                                                                    .get(rele_raw_id, 0) + sim_value
            
    user_recall_items_dict = {k: sorted(v.items(), key=lambda x: x[1], reverse=True) for k, v in user_recall_items_dict.items()}
    # 將召回的結(jié)果進(jìn)行排序
    
    # 保存召回的結(jié)果
    # 這里是直接通過(guò)向量的方式得到了召回結(jié)果棚饵,相比于上面的召回方法,上面的只是得到了i2i及u2u的相似性矩陣掩完,還需要進(jìn)行協(xié)同過(guò)濾召回才能得到召回結(jié)果
    # 可以直接對(duì)這個(gè)召回結(jié)果進(jìn)行評(píng)估噪漾,為了方便可以統(tǒng)一寫(xiě)一個(gè)評(píng)估函數(shù)對(duì)所有的召回結(jié)果進(jìn)行評(píng)估
    pickle.dump(user_recall_items_dict, open(save_path + 'youtube_u2i_dict.pkl', 'wb'))
    return user_recall_items_dict
# 由于這里需要做召回評(píng)估,所以講訓(xùn)練集中的最后一次點(diǎn)擊都提取了出來(lái)
if not metric_recall:
    user_multi_recall_dict['youtubednn_recall'] = youtubednn_u2i_dict(all_click_df, topk=20)
else:
    trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)
    user_multi_recall_dict['youtubednn_recall'] = youtubednn_u2i_dict(trn_hist_click_df, topk=20)
    # 召回效果評(píng)估
    metrics_recall(user_multi_recall_dict['youtubednn_recall'], trn_last_click_df, topk=20)
100%|██████████| 250000/250000 [02:02<00:00, 2038.57it/s]


WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/keras/initializers.py:143: calling RandomNormal.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
Instructions for updating:
Call initializer instance with the dtype argument instead of passing it to the constructor
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/autograph/impl/api.py:253: calling reduce_sum_v1 (from tensorflow.python.ops.math_ops) with keep_dims is deprecated and will be removed in a future version.
Instructions for updating:
keep_dims is deprecated, use keepdims instead
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/autograph/impl/api.py:253: div (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Deprecated in favor of operator or tf.math.divide.
WARNING:tensorflow:From /home/ryluo/anaconda3/lib/python3.6/site-packages/tensorflow/python/ops/init_ops.py:1288: calling VarianceScaling.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
Instructions for updating:
Call initializer instance with the dtype argument instead of passing it to the constructor
1149673/1149673 [==============================] - 216s 188us/sample - loss: 0.1326


250000it [00:32, 7720.75it/s]

itemCF recall

上面已經(jīng)通過(guò)協(xié)同過(guò)濾且蓬,Embedding檢索的方式得到了文章的相似度矩陣欣硼,下面使用協(xié)同過(guò)濾的思想,給用戶(hù)召回與其歷史文章相似的文章恶阴。
這里在召回的時(shí)候诈胜,也是用了關(guān)聯(lián)規(guī)則的方式:

  1. 考慮相似文章與歷史點(diǎn)擊文章順序的權(quán)重(細(xì)節(jié)看代碼)
  2. 考慮文章創(chuàng)建時(shí)間的權(quán)重,也就是考慮相似文章與歷史點(diǎn)擊文章創(chuàng)建時(shí)間差的權(quán)重
  3. 考慮文章內(nèi)容相似度權(quán)重(使用Embedding計(jì)算相似文章相似度冯事,但是這里需要注意焦匈,在Embedding的時(shí)候并沒(méi)有計(jì)算所有商品兩兩之間的相似度,所以相似的文章與歷史點(diǎn)擊文章不存在相似度昵仅,需要做特殊處理)
# 基于商品的召回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):
    """
        基于文章協(xié)同過(guò)濾的召回
        :param user_id: 用戶(hù)id
        :param user_item_time_dict: 字典, 根據(jù)點(diǎn)擊時(shí)間獲取用戶(hù)的點(diǎn)擊文章序列   {user1: {item1: time1, item2: time2..}...}
        :param i2i_sim: 字典缓熟,文章相似性矩陣
        :param sim_item_topk: 整數(shù)累魔, 選擇與當(dāng)前文章最相似的前k篇文章
        :param recall_item_num: 整數(shù), 最后的召回文章數(shù)量
        :param item_topk_click: 列表够滑,點(diǎn)擊次數(shù)最多的文章列表垦写,用戶(hù)召回補(bǔ)全
        :param emb_i2i_sim: 字典基于內(nèi)容embedding算的文章相似矩陣
        
        return: 召回的文章列表 {item1:score1, item2: score2...}
        
    """
    # 獲取用戶(hù)歷史交互的文章
    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
            
            # 文章創(chuàng)建時(shí)間差權(quán)重
            created_time_weight = np.exp(0.8 ** np.abs(item_created_time_dict[i] - item_created_time_dict[j]))
            # 相似文章和歷史點(diǎn)擊文章序列中歷史文章所在的位置權(quán)重
            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個(gè),用熱門(mén)商品補(bǔ)全
    if len(item_rank) < recall_item_num:
        for i, item in enumerate(item_topk_click):
            if item in item_rank.items(): # 填充的item應(yīng)該不在原來(lái)的列表中
                continue
            item_rank[item] = - i - 100 # 隨便給個(gè)負(fù)數(shù)就行
            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 sim召回

# 先進(jìn)行itemcf召回, 為了召回評(píng)估彰触,所以提取最后一次點(diǎn)擊

if metric_recall:
    trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)
else:
    trn_hist_click_df = all_click_df

user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)

i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))
emb_i2i_sim = pickle.load(open(save_path + 'emb_i2i_sim.pkl', 'rb'))

sim_item_topk = 20
recall_item_num = 10
item_topk_click = get_item_topk_click(trn_hist_click_df, k=50)

for user in tqdm(trn_hist_click_df['user_id'].unique()):
    user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, \
                                                        i2i_sim, sim_item_topk, recall_item_num, \
                                                        item_topk_click, item_created_time_dict, emb_i2i_sim)

user_multi_recall_dict['itemcf_sim_itemcf_recall'] = user_recall_items_dict
pickle.dump(user_multi_recall_dict['itemcf_sim_itemcf_recall'], open(save_path + 'itemcf_recall_dict.pkl', 'wb'))

if metric_recall:
    # 召回效果評(píng)估
    metrics_recall(user_multi_recall_dict['itemcf_sim_itemcf_recall'], trn_last_click_df, topk=recall_item_num)
100%|██████████| 250000/250000 [2:51:13<00:00, 24.33it/s]  

embedding sim 召回

# 這里是為了召回評(píng)估梯投,所以提取最后一次點(diǎn)擊
if metric_recall:
    trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)
else:
    trn_hist_click_df = all_click_df

user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)
i2i_sim = pickle.load(open(save_path + 'emb_i2i_sim.pkl','rb'))

sim_item_topk = 20
recall_item_num = 10

item_topk_click = get_item_topk_click(trn_hist_click_df, k=50)

for user in tqdm(trn_hist_click_df['user_id'].unique()):
    user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, i2i_sim, sim_item_topk, 
                                                        recall_item_num, item_topk_click, item_created_time_dict, emb_i2i_sim)
    
user_multi_recall_dict['embedding_sim_item_recall'] = user_recall_items_dict
pickle.dump(user_multi_recall_dict['embedding_sim_item_recall'], open(save_path + 'embedding_sim_item_recall.pkl', 'wb'))

if metric_recall:
    # 召回效果評(píng)估
    metrics_recall(user_multi_recall_dict['embedding_sim_item_recall'], trn_last_click_df, topk=recall_item_num)
100%|██████████| 250000/250000 [04:35<00:00, 905.85it/s] 

userCF召回

基于用戶(hù)協(xié)同過(guò)濾,核心思想是給用戶(hù)推薦與其相似的用戶(hù)歷史點(diǎn)擊文章况毅,因?yàn)檫@里涉及到了相似用戶(hù)的歷史文章分蓖,這里仍然可以加上一些關(guān)聯(lián)規(guī)則來(lái)給用戶(hù)可能點(diǎn)擊的文章進(jìn)行加權(quán),這里使用的關(guān)聯(lián)規(guī)則主要是考慮相似用戶(hù)的歷史點(diǎn)擊文章與被推薦用戶(hù)歷史點(diǎn)擊商品的關(guān)系權(quán)重俭茧,而這里的關(guān)系就可以直接借鑒基于物品的協(xié)同過(guò)濾相似的做法咆疗,只不過(guò)這里是對(duì)被推薦物品關(guān)系的一個(gè)累加的過(guò)程,下面是使用的一些關(guān)系權(quán)重母债,及相關(guān)的代碼:

  1. 計(jì)算被推薦用戶(hù)歷史點(diǎn)擊文章與相似用戶(hù)歷史點(diǎn)擊文章的相似度医窿,文章創(chuàng)建時(shí)間差,相對(duì)位置的總和爽蝴,作為各自的權(quán)重
# 基于用戶(hù)的召回 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):
    """
        基于文章協(xié)同過(guò)濾的召回
        :param user_id: 用戶(hù)id
        :param user_item_time_dict: 字典, 根據(jù)點(diǎn)擊時(shí)間獲取用戶(hù)的點(diǎn)擊文章序列   {user1: {item1: time1, item2: time2..}...}
        :param u2u_sim: 字典春宣,文章相似性矩陣
        :param sim_user_topk: 整數(shù), 選擇與當(dāng)前用戶(hù)最相似的前k個(gè)用戶(hù)
        :param recall_item_num: 整數(shù)衙熔, 最后的召回文章數(shù)量
        :param item_topk_click: 列表登颓,點(diǎn)擊次數(shù)最多的文章列表,用戶(hù)召回補(bǔ)全
        :param item_created_time_dict: 文章創(chuàng)建時(shí)間列表
        :param emb_i2i_sim: 字典基于內(nèi)容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])   # 存在一個(gè)用戶(hù)與某篇文章的多次交互红氯, 這里得去重
    
    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
            
            # 當(dāng)前文章與該用戶(hù)看的歷史文章進(jìn)行一個(gè)權(quán)重交互
            for loc, (j, click_time) in enumerate(user_item_time_list):
                # 點(diǎn)擊時(shí)的相對(duì)位置權(quán)重
                loc_weight += 0.9 ** (len(user_item_time_list) - loc)
                # 內(nèi)容相似性權(quán)重
                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]
                
                # 創(chuàng)建時(shí)間差權(quán)重
                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
        
    # 熱度補(bǔ)全
    if len(items_rank) < recall_item_num:
        for i, item in enumerate(item_topk_click):
            if item in items_rank.items(): # 填充的item應(yīng)該不在原來(lái)的列表中
                continue
            items_rank[item] = - i - 100 # 隨便給個(gè)復(fù)數(shù)就行
            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

userCF sim召回

# 這里是為了召回評(píng)估框咙,所以提取最后一次點(diǎn)擊
# 由于usercf中計(jì)算user之間的相似度的過(guò)程太費(fèi)內(nèi)存了,全量數(shù)據(jù)這里就沒(méi)有跑痢甘,跑了一個(gè)采樣之后的數(shù)據(jù)
if metric_recall:
    trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)
else:
    trn_hist_click_df = all_click_df
    
user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)

u2u_sim = pickle.load(open(save_path + 'usercf_u2u_sim.pkl', 'rb'))

sim_user_topk = 20
recall_item_num = 10
item_topk_click = get_item_topk_click(trn_hist_click_df, k=50)

for user in tqdm(trn_hist_click_df['user_id'].unique()):
    user_recall_items_dict[user] = user_based_recommend(user, user_item_time_dict, u2u_sim, sim_user_topk, \
                                                        recall_item_num, item_topk_click, item_created_time_dict, emb_i2i_sim)    

pickle.dump(user_recall_items_dict, open(save_path + 'usercf_u2u2i_recall.pkl', 'wb'))

if metric_recall:
    # 召回效果評(píng)估
    metrics_recall(user_recall_items_dict, trn_last_click_df, topk=recall_item_num)

user embedding sim召回

雖然沒(méi)有直接跑usercf的計(jì)算用戶(hù)之間的相似度喇嘱,為了驗(yàn)證上述基于用戶(hù)的協(xié)同過(guò)濾的代碼,下面使用了YoutubeDNN過(guò)程中產(chǎn)生的user embedding來(lái)進(jìn)行向量檢索每個(gè)user最相似的topk個(gè)user塞栅,在使用這里得到的u2u的相似性矩陣者铜,使用usercf進(jìn)行召回,具體代碼如下

# 使用Embedding的方式獲取u2u的相似性矩陣
# topk指的是每個(gè)user, faiss搜索后返回最相似的topk個(gè)user
def u2u_embdding_sim(click_df, user_emb_dict, save_path, topk):
    
    user_list = []
    user_emb_list = []
    for user_id, user_emb in user_emb_dict.items():
        user_list.append(user_id)
        user_emb_list.append(user_emb)
        
    user_index_2_rawid_dict = {k: v for k, v in zip(range(len(user_list)), user_list)}    
    
    user_emb_np = np.array(user_emb_list, dtype=np.float32)
    
    # 建立faiss索引
    user_index = faiss.IndexFlatIP(user_emb_np.shape[1])
    user_index.add(user_emb_np)
    # 相似度查詢(xún)放椰,給每個(gè)索引位置上的向量返回topk個(gè)item以及相似度
    sim, idx = user_index.search(user_emb_np, topk) # 返回的是列表
   
    # 將向量檢索的結(jié)果保存成原始id的對(duì)應(yīng)關(guān)系
    user_sim_dict = collections.defaultdict(dict)
    for target_idx, sim_value_list, rele_idx_list in tqdm(zip(range(len(user_emb_np)), sim, idx)):
        target_raw_id = user_index_2_rawid_dict[target_idx]
        # 從1開(kāi)始是為了去掉商品本身, 所以最終獲得的相似商品只有topk-1
        for rele_idx, sim_value in zip(rele_idx_list[1:], sim_value_list[1:]): 
            rele_raw_id = user_index_2_rawid_dict[rele_idx]
            user_sim_dict[target_raw_id][rele_raw_id] = user_sim_dict.get(target_raw_id, {}).get(rele_raw_id, 0) + sim_value
    
    # 保存i2i相似度矩陣
    pickle.dump(user_sim_dict, open(save_path + 'youtube_u2u_sim.pkl', 'wb'))   
    return user_sim_dict
# 讀取YoutubeDNN過(guò)程中產(chǎn)生的user embedding, 然后使用faiss計(jì)算用戶(hù)之間的相似度
# 這里需要注意作烟,這里得到的user embedding其實(shí)并不是很好,因?yàn)閅outubeDNN中使用的是用戶(hù)點(diǎn)擊序列來(lái)訓(xùn)練的user embedding,
# 如果序列普遍都比較短的話(huà)砾医,其實(shí)效果并不是很好
user_emb_dict = pickle.load(open(save_path + 'user_youtube_emb.pkl', 'rb'))
u2u_sim = u2u_embdding_sim(all_click_df, user_emb_dict, save_path, topk=10)
250000it [00:23, 10507.45it/s]

通過(guò)YoutubeDNN得到的user_embedding

# 使用召回評(píng)估函數(shù)驗(yàn)證當(dāng)前召回方式的效果
if metric_recall:
    trn_hist_click_df, trn_last_click_df = get_hist_and_last_click(all_click_df)
else:
    trn_hist_click_df = all_click_df

user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)
u2u_sim = pickle.load(open(save_path + 'youtube_u2u_sim.pkl', 'rb'))

sim_user_topk = 20
recall_item_num = 10

item_topk_click = get_item_topk_click(trn_hist_click_df, k=50)
for user in tqdm(trn_hist_click_df['user_id'].unique()):
    user_recall_items_dict[user] = user_based_recommend(user, user_item_time_dict, u2u_sim, sim_user_topk, \
                                                        recall_item_num, item_topk_click, item_created_time_dict, emb_i2i_sim)
    
user_multi_recall_dict['youtubednn_usercf_recall'] = user_recall_items_dict
pickle.dump(user_multi_recall_dict['youtubednn_usercf_recall'], open(save_path + 'youtubednn_usercf_recall.pkl', 'wb'))

if metric_recall:
    # 召回效果評(píng)估
    metrics_recall(user_multi_recall_dict['youtubednn_usercf_recall'], trn_last_click_df, topk=recall_item_num)
100%|██████████| 250000/250000 [19:43<00:00, 211.22it/s]

冷啟動(dòng)問(wèn)題

冷啟動(dòng)問(wèn)題可以分成三類(lèi):文章冷啟動(dòng)拿撩,用戶(hù)冷啟動(dòng),系統(tǒng)冷啟動(dòng)如蚜。

  • 文章冷啟動(dòng):對(duì)于一個(gè)平臺(tái)系統(tǒng)新加入的文章绷雏,該文章沒(méi)有任何的交互記錄头滔,如何推薦給用戶(hù)的問(wèn)題。(對(duì)于我們場(chǎng)景可以認(rèn)為是涎显,日志數(shù)據(jù)中沒(méi)有出現(xiàn)過(guò)的文章都可以認(rèn)為是冷啟動(dòng)的文章)
  • 用戶(hù)冷啟動(dòng):對(duì)于一個(gè)平臺(tái)系統(tǒng)新來(lái)的用戶(hù)坤检,該用戶(hù)還沒(méi)有文章的交互信息,如何給該用戶(hù)進(jìn)行推薦期吓。(對(duì)于我們場(chǎng)景就是早歇,測(cè)試集中的用戶(hù)是否在測(cè)試集對(duì)應(yīng)的log數(shù)據(jù)中出現(xiàn)過(guò),如果沒(méi)有出現(xiàn)過(guò)讨勤,那么可以認(rèn)為該用戶(hù)是冷啟動(dòng)用戶(hù)箭跳。但是有時(shí)候并沒(méi)有這么嚴(yán)格,我們也可以自己設(shè)定某些指標(biāo)來(lái)判別哪些用戶(hù)是冷啟動(dòng)用戶(hù)潭千,比如通過(guò)使用時(shí)長(zhǎng)谱姓,點(diǎn)擊率,留存率等等)
  • 系統(tǒng)冷啟動(dòng):就是對(duì)于一個(gè)平臺(tái)剛上線(xiàn)刨晴,還沒(méi)有任何的相關(guān)歷史數(shù)據(jù)屉来,此時(shí)就是系統(tǒng)冷啟動(dòng),其實(shí)也就是前面兩種的一個(gè)綜合狈癞。

當(dāng)前場(chǎng)景下冷啟動(dòng)問(wèn)題的分析:

對(duì)當(dāng)前的數(shù)據(jù)進(jìn)行分析會(huì)發(fā)現(xiàn)茄靠,日志中所有出現(xiàn)過(guò)的點(diǎn)擊文章只有3w多個(gè),而整個(gè)文章庫(kù)中卻有30多萬(wàn)蝶桶,那么測(cè)試集中的用戶(hù)最后一次點(diǎn)擊是否會(huì)點(diǎn)擊沒(méi)有出現(xiàn)在日志中的文章呢慨绳?如果存在這種情況,說(shuō)明用戶(hù)點(diǎn)擊的文章之前沒(méi)有任何的交互信息真竖,這也就是我們所說(shuō)的文章冷啟動(dòng)脐雪。通過(guò)數(shù)據(jù)分析還可以發(fā)現(xiàn),測(cè)試集用戶(hù)只有一次點(diǎn)擊的數(shù)據(jù)占得比例還不少恢共,其實(shí)僅僅通過(guò)用戶(hù)的一次點(diǎn)擊就給用戶(hù)推薦文章使用模型的方式也是比較難的战秋,這里其實(shí)也可以考慮用戶(hù)冷啟動(dòng)的問(wèn)題,但是這里只給出物品冷啟動(dòng)的一些解決方案及代碼旁振,關(guān)于用戶(hù)冷啟動(dòng)的話(huà)提一些可行性的做法。

  1. 文章冷啟動(dòng)(沒(méi)有冷啟動(dòng)的探索問(wèn)題)
    其實(shí)我們這里不是為了做文章的冷啟動(dòng)而做冷啟動(dòng)涨岁,而是猜測(cè)用戶(hù)可能會(huì)點(diǎn)擊一些沒(méi)有在log數(shù)據(jù)中出現(xiàn)的文章拐袜,我們要做的就是如何從將近27萬(wàn)的文章中選擇一些文章作為用戶(hù)冷啟動(dòng)的文章,這里其實(shí)也可以看成是一種召回策略梢薪,我們這里就采用簡(jiǎn)單的比較好理解的基于規(guī)則的召回策略來(lái)獲取用戶(hù)可能點(diǎn)擊的未出現(xiàn)在log數(shù)據(jù)中的文章蹬铺。
    現(xiàn)在的問(wèn)題變成了:如何給每個(gè)用戶(hù)考慮從27萬(wàn)個(gè)商品中獲取一小部分商品?隨機(jī)選一些可能是一種方案秉撇。下面給出一些參考的方案甜攀。
    1. 首先基于Embedding召回一部分與用戶(hù)歷史相似的文章
    2. 從基于Embedding召回的文章中通過(guò)一些規(guī)則過(guò)濾掉一些文章秋泄,使得留下的文章用戶(hù)更可能點(diǎn)擊。我們這里的規(guī)則规阀,可以是恒序,留下那些與用戶(hù)歷史點(diǎn)擊文章主題相同的文章,或者字?jǐn)?shù)相差不大的文章谁撼。并且留下的文章盡量是與測(cè)試集用戶(hù)最后一次點(diǎn)擊時(shí)間更接近的文章歧胁,或者是當(dāng)天的文章也行。
  2. 用戶(hù)冷啟動(dòng)
    這里對(duì)測(cè)試集中的用戶(hù)點(diǎn)擊數(shù)據(jù)進(jìn)行分析會(huì)發(fā)現(xiàn)厉碟,測(cè)試集中有百分之20的用戶(hù)只有一次點(diǎn)擊喊巍,那么這些點(diǎn)擊特別少的用戶(hù)的召回是不是可以單獨(dú)做一些策略上的補(bǔ)充呢?或者是在排序后直接基于規(guī)則加上一些文章呢箍鼓?這些都可以去嘗試崭参,這里沒(méi)有提供具體的做法。

注意:

這里看似和基于embedding計(jì)算的item之間相似度然后做itemcf是一致的款咖,但是現(xiàn)在我們的目的不一樣何暮,我們這里的目的是找到相似的向量,并且還沒(méi)有出現(xiàn)在log日志中的商品之剧,再加上一些其他的冷啟動(dòng)的策略郭卫,這里需要找回的數(shù)量會(huì)偏多一點(diǎn),不然被篩選完之后可能都沒(méi)有文章了

# 先進(jìn)行itemcf召回背稼,這里不需要做召回評(píng)估贰军,這里只是一種策略
trn_hist_click_df = all_click_df

user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)
i2i_sim = pickle.load(open(save_path + 'emb_i2i_sim.pkl','rb'))

sim_item_topk = 150
recall_item_num = 100 # 稍微召回多一點(diǎn)文章,便于后續(xù)的規(guī)則篩選

item_topk_click = get_item_topk_click(trn_hist_click_df, k=50)
for user in tqdm(trn_hist_click_df['user_id'].unique()):
    user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, i2i_sim, sim_item_topk, 
                                                        recall_item_num, item_topk_click,item_created_time_dict, emb_i2i_sim)
pickle.dump(user_recall_items_dict, open(save_path + 'cold_start_items_raw_dict.pkl', 'wb'))
100%|██████████| 250000/250000 [05:01<00:00, 828.60it/s] 
# 基于規(guī)則進(jìn)行文章過(guò)濾
# 保留文章主題與用戶(hù)歷史瀏覽主題相似的文章
# 保留文章字?jǐn)?shù)與用戶(hù)歷史瀏覽文章字?jǐn)?shù)相差不大的文章
# 保留最后一次點(diǎn)擊當(dāng)天的文章
# 按照相似度返回最終的結(jié)果

def get_click_article_ids_set(all_click_df):
    return set(all_click_df.click_article_id.values)

def cold_start_items(user_recall_items_dict, user_hist_item_typs_dict, user_hist_item_words_dict, \
                     user_last_item_created_time_dict, item_type_dict, item_words_dict, 
                     item_created_time_dict, click_article_ids_set, recall_item_num):
    """
        冷啟動(dòng)的情況下召回一些文章
        :param user_recall_items_dict: 基于內(nèi)容embedding相似性召回來(lái)的很多文章蟹肘, 字典词疼, {user1: [item1, item2, ..], }
        :param user_hist_item_typs_dict: 字典, 用戶(hù)點(diǎn)擊的文章的主題映射
        :param user_hist_item_words_dict: 字典帘腹, 用戶(hù)點(diǎn)擊的歷史文章的字?jǐn)?shù)映射
        :param user_last_item_created_time_idct: 字典贰盗,用戶(hù)點(diǎn)擊的歷史文章創(chuàng)建時(shí)間映射
        :param item_tpye_idct: 字典,文章主題映射
        :param item_words_dict: 字典阳欲,文章字?jǐn)?shù)映射
        :param item_created_time_dict: 字典舵盈, 文章創(chuàng)建時(shí)間映射
        :param click_article_ids_set: 集合,用戶(hù)點(diǎn)擊過(guò)得文章, 也就是日志里面出現(xiàn)過(guò)的文章
        :param recall_item_num: 召回文章的數(shù)量球化, 這個(gè)指的是沒(méi)有出現(xiàn)在日志里面的文章數(shù)量
    """
    
    cold_start_user_items_dict = {}
    for user, item_list in tqdm(user_recall_items_dict.items()):
        cold_start_user_items_dict.setdefault(user, [])
        for item, score in item_list:
            # 獲取歷史文章信息
            hist_item_type_set = user_hist_item_typs_dict[user]
            hist_mean_words = user_hist_item_words_dict[user]
            hist_last_item_created_time = user_last_item_created_time_dict[user]
            hist_last_item_created_time = datetime.fromtimestamp(hist_last_item_created_time)
            
            # 獲取當(dāng)前召回文章的信息
            curr_item_type = item_type_dict[item]
            curr_item_words = item_words_dict[item]
            curr_item_created_time = item_created_time_dict[item]
            curr_item_created_time = datetime.fromtimestamp(curr_item_created_time)

            # 首先秽晚,文章不能出現(xiàn)在用戶(hù)的歷史點(diǎn)擊中, 然后根據(jù)文章主題筒愚,文章單詞數(shù)赴蝇,文章創(chuàng)建時(shí)間進(jìn)行篩選
            if curr_item_type not in hist_item_type_set or \
                item in click_article_ids_set or \
                abs(curr_item_words - hist_mean_words) > 200 or \
                abs((curr_item_created_time - hist_last_item_created_time).days) > 90: 
                continue
                
            cold_start_user_items_dict[user].append((item, score))      # {user1: [(item1, score1), (item2, score2)..]...}
    
    # 需要控制一下冷啟動(dòng)召回的數(shù)量
    cold_start_user_items_dict = {k: sorted(v, key=lambda x:x[1], reverse=True)[:recall_item_num] \
                                  for k, v in cold_start_user_items_dict.items()}
    
    pickle.dump(cold_start_user_items_dict, open(save_path + 'cold_start_user_items_dict.pkl', 'wb'))
    
    return cold_start_user_items_dict
all_click_df_ = all_click_df.copy()
all_click_df_ = all_click_df_.merge(item_info_df, how='left', on='click_article_id')
user_hist_item_typs_dict, user_hist_item_ids_dict, user_hist_item_words_dict, user_last_item_created_time_dict = get_user_hist_item_info_dict(all_click_df_)
click_article_ids_set = get_click_article_ids_set(all_click_df)
# 需要注意的是
# 這里使用了很多規(guī)則來(lái)篩選冷啟動(dòng)的文章,所以前面再召回的階段就應(yīng)該盡可能的多召回一些文章巢掺,否則很容易被刪掉
cold_start_user_items_dict = cold_start_items(user_recall_items_dict, user_hist_item_typs_dict, user_hist_item_words_dict, \
                                              user_last_item_created_time_dict, item_type_dict, item_words_dict, \
                                              item_created_time_dict, click_article_ids_set, recall_item_num)

user_multi_recall_dict['cold_start_recall'] = cold_start_user_items_dict
100%|██████████| 250000/250000 [01:49<00:00, 2293.37it/s]

多路召回合并

多路召回合并就是將前面所有的召回策略得到的用戶(hù)文章列表合并起來(lái)句伶,下面是對(duì)前面所有召回結(jié)果的匯總

  1. 基于itemcf計(jì)算的item之間的相似度sim進(jìn)行的召回
  2. 基于embedding搜索得到的item之間的相似度進(jìn)行的召回
  3. YoutubeDNN召回
  4. YoutubeDNN得到的user之間的相似度進(jìn)行的召回
  5. 基于冷啟動(dòng)策略的召回

注意:
在做召回評(píng)估的時(shí)候就會(huì)發(fā)現(xiàn)有些召回的效果不錯(cuò)有些召回的效果很差劲蜻,所以對(duì)每一路召回的結(jié)果,我們可以認(rèn)為的定義一些權(quán)重考余,來(lái)做最終的相似度融合

def combine_recall_results(user_multi_recall_dict, weight_dict=None, topk=25):
    final_recall_items_dict = {}
    
    # 對(duì)每一種召回結(jié)果按照用戶(hù)進(jìn)行歸一化先嬉,方便后面多種召回結(jié)果,相同用戶(hù)的物品之間權(quán)重相加
    def norm_user_recall_items_sim(sorted_item_list):
        # 如果冷啟動(dòng)中沒(méi)有文章或者只有一篇文章秃殉,直接返回坝初,出現(xiàn)這種情況的原因可能是冷啟動(dòng)召回的文章數(shù)量太少了,
        # 基于規(guī)則篩選之后就沒(méi)有文章了, 這里還可以做一些其他的策略性的篩選
        if len(sorted_item_list) < 2:
            return sorted_item_list
        
        min_sim = sorted_item_list[-1][1]
        max_sim = sorted_item_list[0][1]
        
        norm_sorted_item_list = []
        for item, score in sorted_item_list:
            if max_sim > 0:
                norm_score = 1.0 * (score - min_sim) / (max_sim - min_sim) if max_sim > min_sim else 1.0
            else:
                norm_score = 0.0
            norm_sorted_item_list.append((item, norm_score))
            
        return norm_sorted_item_list
    
    print('多路召回合并...')
    for method, user_recall_items in tqdm(user_multi_recall_dict.items()):
        print(method + '...')
        # 在計(jì)算最終召回結(jié)果的時(shí)候钾军,也可以為每一種召回結(jié)果設(shè)置一個(gè)權(quán)重
        if weight_dict == None:
            recall_method_weight = 1
        else:
            recall_method_weight = weight_dict[method]
        
        for user_id, sorted_item_list in user_recall_items.items(): # 進(jìn)行歸一化
            user_recall_items[user_id] = norm_user_recall_items_sim(sorted_item_list)
        
        for user_id, sorted_item_list in user_recall_items.items():
            # print('user_id')
            final_recall_items_dict.setdefault(user_id, {})
            for item, score in sorted_item_list:
                final_recall_items_dict[user_id].setdefault(item, 0)
                final_recall_items_dict[user_id][item] += recall_method_weight * score  
    
    final_recall_items_dict_rank = {}
    # 多路召回時(shí)也可以控制最終的召回?cái)?shù)量
    for user, recall_item_dict in final_recall_items_dict.items():
        final_recall_items_dict_rank[user] = sorted(recall_item_dict.items(), key=lambda x: x[1], reverse=True)[:topk]

    # 將多路召回后的最終結(jié)果字典保存到本地
    pickle.dump(final_recall_items_dict, open(os.path.join(save_path, 'final_recall_items_dict.pkl'),'wb'))

    return final_recall_items_dict_rank
# 這里直接對(duì)多路召回的權(quán)重給了一個(gè)相同的值鳄袍,其實(shí)可以根據(jù)前面召回的情況來(lái)調(diào)整參數(shù)的值
weight_dict = {'itemcf_sim_itemcf_recall': 1.0,
               'embedding_sim_item_recall': 1.0,
               'youtubednn_recall': 1.0,
               'youtubednn_usercf_recall': 1.0, 
               'cold_start_recall': 1.0}
# 最終合并之后每個(gè)用戶(hù)召回150個(gè)商品進(jìn)行排序
final_recall_items_dict_rank = combine_recall_results(user_multi_recall_dict, weight_dict, topk=150)
  0%|          | 0/5 [00:00<?, ?it/s]

多路召回合并...
itemcf_sim_itemcf_recall...


 20%|██        | 1/5 [00:08<00:34,  8.66s/it]

embedding_sim_item_recall...


 40%|████      | 2/5 [00:16<00:24,  8.29s/it]

youtubednn_recall...
youtubednn_usercf_recall...


 80%|████████  | 4/5 [00:23<00:06,  6.98s/it]

cold_start_recall...


100%|██████████| 5/5 [00:42<00:00,  8.40s/it]

總結(jié)

上述實(shí)現(xiàn)了如下召回策略:

  1. 基于關(guān)聯(lián)規(guī)則的itemcf
  2. 基于關(guān)聯(lián)規(guī)則的usercf
  3. youtubednn召回
  4. 冷啟動(dòng)召回

對(duì)于上述實(shí)現(xiàn)的召回策略其實(shí)都不是最優(yōu)的結(jié)果,我們只是做了個(gè)簡(jiǎn)單的嘗試吏恭,其中還有很多地方可以?xún)?yōu)化拗小,包括已經(jīng)實(shí)現(xiàn)的這些召回策略的參數(shù)或者新加一些,修改一些關(guān)聯(lián)規(guī)則都可以樱哼。當(dāng)然還可以嘗試更多的召回策略哀九,比如對(duì)新聞進(jìn)行熱度召回等等。

這一節(jié)先欠著搅幅,后面有時(shí)間再慢慢學(xué)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阅束,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子茄唐,更是在濱河造成了極大的恐慌息裸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪编,死亡現(xiàn)場(chǎng)離奇詭異呼盆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蚁廓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)访圃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人相嵌,你說(shuō)我怎么就攤上這事腿时。” “怎么了饭宾?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵批糟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捏雌,道長(zhǎng)跃赚,這世上最難降的妖魔是什么笆搓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任性湿,我火速辦了婚禮纬傲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肤频。我一直安慰自己叹括,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布宵荒。 她就那樣靜靜地躺著汁雷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪报咳。 梳的紋絲不亂的頭發(fā)上侠讯,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音暑刃,去河邊找鬼厢漩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岩臣,可吹牛的內(nèi)容都是我干的溜嗜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼架谎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼炸宵!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起谷扣,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤土全,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后抑钟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體涯曲,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年在塔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了幻件。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛔溃,死狀恐怖绰沥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贺待,我是刑警寧澤徽曲,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站麸塞,受9級(jí)特大地震影響秃臣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一奥此、第九天 我趴在偏房一處隱蔽的房頂上張望弧哎。 院中可真熱鬧,春花似錦稚虎、人聲如沸撤嫩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)序攘。三九已至,卻和暖如春寻拂,著一層夾襖步出監(jiān)牢的瞬間程奠,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工祭钉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留梦染,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓朴皆,卻偏偏與公主長(zhǎng)得像帕识,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遂铡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容