3. 召回模型
多路召回:
- itemCF召回
3.1 协同过滤-itemCF召回
3.1.1 召回准备
获取历史点击中点击数前k篇文章id:get_item_topk_click(trn_hist_click_df, k=50)
对每个用户进行item_based_recommend
3.1.2 item_based_recommend
├── 逻辑
│ ├── 获取该用户历史交互的文章 (user_hist_items)
│ ├── 遍历用户历史文章
│ │ ├── 检查文章是否在i2i_sim相似性矩阵中
│ │ ├── 获取与当前文章最相似的前sim_item_topk篇文章&用户没点过
│ │ └── 计算权重并更新评分created_time_weight * loc_weight * content_weight * wij
│ │ ├── 文章创建时间差权重 (created_time_weight)
│ │ ├── 位置权重 (loc_weight)
│ │ └── 内容相似权重 (content_weight)
│ └── 使用热门文章补全不足的推荐项
│
└── 输出
└── 召回的文章列表,按分数排序 {item1: score1, item2: score2, …}
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 item_created_time_dict: 文章创建时间字典
: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):
if i not in i2i_sim:
print(f"Item {i} not found in i2i_sim.")
continue # 跳过这个循环
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.1.3 召回评估
├── 提取用户最后一次点击的文章字典 (last_click_item_dict)
├── 获取用户数量 (user_num)
├── 遍历每10个k值(从10到topk)
│ ├── 初始化击中数计数器 (hit_num)
│ ├── 遍历用户的召回文章
│ │ ├── 提取前k个召回文章
│ │ ├── 检查用户是否在最后一次点击字典中
│ │ ├── 检查最后点击的文章是否在召回列表中,若命中则计数
│ └── 计算并打印当前k值的击中率
def metrics_recall(user_recall_items_dict, trn_last_click_df, topk=50):
"""
依次评估召回的前10, 20, 30, 40, 50个文章中的击中率
:param user_recall_items_dict: 召回字典
:param trn_last_click_df: 用户最后一次点击字典
:param topk:
:return:
"""
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 user not in (last_click_item_dict.keys()): # 该用户只有一次点击,当作了训练集
continue
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)
topk: 10 : hit_num: 6499 hit_rate: 0.6499 user_num : 10000
topk: 20 : hit_num: 8003 hit_rate: 0.8003 user_num : 10000
topk: 30 : hit_num: 8662 hit_rate: 0.8662 user_num : 10000
topk: 40 : hit_num: 9036 hit_rate: 0.9036 user_num : 10000
topk: 50 : hit_num: 9273 hit_rate: 0.9273 user_num : 10000
3.2 embedding_sim召回
获取历史点击中点击数前k篇文章id:get_item_topk_click(trn_hist_click_df, k=50)
对每个用户进行item_based_recommend
步骤与3.1相同,把i2i_sim矩阵替换为embbedding_sim矩阵,
topk: 10 : hit_num: 182 hit_rate: 0.0182 user_num : 10000
topk: 20 : hit_num: 623 hit_rate: 0.0623 user_num : 10000
topk: 30 : hit_num: 972 hit_rate: 0.0972 user_num : 10000
topk: 40 : hit_num: 1332 hit_rate: 0.1332 user_num : 10000
topk: 50 : hit_num: 1699 hit_rate: 0.1699 user_num : 10000
3.3 youtubeDNN召回
3.3.1 召回准备
定义SEQ_LEN为30,用于控制用户点击序列的长度
用户ID和文章ID进行类别编码
保存特征最大索引字典 (feature_max_idx)
用户和物品画像:提取user和item的画像信息,生成原始id和编码id之间的映射 (user_index_2_rawid, item_index_2_rawid)
数据集划分与输入整理:滑动窗口划分训练集和测试集 (train_set, test_set)
def gen_data_set(data, negsample=0):
"""
获取双塔召回时的训练验证数据
:param data:
:param negsample: 通过滑窗构建样本的时候,负样本的数量
:return:
"""
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)) # 用户没看过的文章里面选择负样本
neg_list = np.random.choice(candidate_set, size=len(pos_list) * negsample, replace=True)
# 长度只有一个的时候,需要把这条数据也放到训练集中,不然的话最终学到的embedding就会有缺失
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)))
# 滑窗构造正负样本
for i in range(1, len(pos_list)):
hist = pos_list[:i]
if i != len(pos_list) - 1:
# 正样本 [user_id, his_item, pos_item, label, len(his_item)]
train_set.append((reviewerID, hist[::-1], pos_list[i], 1, len(hist[::-1])))
for negi in range(negsample):
# 负样本 [user_id, his_item, neg_item, label, len(his_item)]
train_set.append((reviewerID, hist[::-1], neg_list[i * negsample + negi], 0, len(hist[::-1])))
else:
# 将最长的那一个序列长度作为测试数据
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
调整输入数据格式 (train_model_input, test_model_input)
def gen_model_input(train_set, 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
特征列与Embedding:
确定Embedding维度 (embedding_dim)
定义用户和物品特征列 (user_feature_columns, item_feature_columns)
embedding_dim = 16
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)]
3.3.2 youtubeDNN
├── 模型定义与训练
│ ├── 设置负采样 (sampler_config)
│ └── 定义YouTubeDNN模型,训练并编译 (model)
│ ├── 训练模型 (model.fit)
│ └── 提取user和item的Embedding向量
├── 向量归一化与索引构建
│ ├── 归一化user和item的Embedding
│ └── 使用Faiss构建索引并进行相似度搜索 (sim, idx)
├── 生成召回字典 (user_recall_items_dict)
├── 结果保存
│ ├── 保存user和item的Embedding字典
│ └── 保存召回结果
def youtubednn_u2i_dict(data, trn_last_click_df, save_path, metric_recall, user_multi_recall_dict, topk=20):
SEQ_LEN = 30 # 用户点击序列的长度,短的填充,长的截断
user_profile_ = data[["user_id"]].drop_duplicates('user_id')
item_profile_ = data[["click_article_id"]].drop_duplicates('click_article_id')
# 类别编码
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的画像,这里具体选择哪些特征还需要进一步的分析和考虑
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']))
# 划分训练和测试集
# 由于深度学习需要的数据量通常都是非常大的,所以为了保证召回的效果,往往会通过滑窗的形式扩充训练样本
train_set, test_set = gen_data_set(data, 0)
# 整理输入数据,具体的操作可以看上面的函数
train_model_input, train_label = gen_model_input(train_set, SEQ_LEN)
test_model_input, test_label = gen_model_input(test_set, SEQ_LEN)
# 确定Embedding的维度
embedding_dim = 16
# 将数据整理成模型可以直接输入的形式
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)]
# 模型的定义
train_counter = Counter(train_model_input['click_article_id'])
item_count = [train_counter.get(i, 0) for i in range(item_feature_columns[0].vocabulary_size)]
sampler_config = NegativeSampler('frequency', num_sampled=5, item_name="click_article_id", item_count=item_count)
if tf.__version__ >= '2.0.0':
tf.compat.v1.disable_eager_execution()
else:
K.set_learning_phase(True)
# model = YoutubeDNN(user_feature_columns, item_feature_columns, num_sampled=5, user_dnn_hidden_units=(64, embedding_dim))
model = YoutubeDNN(user_feature_columns, item_feature_columns, user_dnn_hidden_units=(64, 16, embedding_dim),
sampler_config=sampler_config) # 模型编译
model.compile(optimizer="adam", loss=sampledsoftmaxloss)
# 模型训练,这里可以定义验证集的比例,如果设置为0的话就是全量数据直接进行训练
history = model.fit(train_model_input, train_label, batch_size=256, epochs=2, verbose=1, validation_split=0.0)
# 训练完模型之后,提取训练的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)
# 保存当前的item_embedding 和 user_embedding 排序的时候可能能够用到,但是需要注意保存的时候需要和原始的id对应
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转换成字典的形式方便查询
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紧邻搜索,通过user_embedding 搜索与其相似性最高的topk个item
index = faiss.IndexFlatIP(embedding_dim)
index.add(item_embs) # 将item向量构建索引
sim, idx = index.search(np.ascontiguousarray(user_embs), topk) # 通过user去查询最相似的topk个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开始是为了去掉商品本身, 所以最终获得的相似商品只有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()}
# 保存召回的结果
# 这里是直接通过向量的方式得到了召回结果,相比于上面的召回方法,上面的只是得到了i2i及u2u的相似性矩阵,还需要进行协同过滤召回才能得到召回结果
user_multi_recall_dict['youtubednn_recall'] = user_recall_items_dict
pickle.dump(user_recall_items_dict, open(save_path + 'youtubednn_recall.pkl', 'wb'))
if metric_recall:
metrics_recall(user_multi_recall_dict['youtubednn_recall'], trn_last_click_df, topk=50)
return user_multi_recall_dict
3.3.3 召回评估 (metrics_recall)
同3.1
3.4 协同过滤-userCF召回
3.4.1 召回准备
同3.1.1:
设置 sim_user_topk
= 100:每个用户选择的最相似用户数量。
设置 recall_item_num
= 50:为每个用户召回的项目数量。
使用 get_item_topk_click
函数,从 trn_hist_click_df
中获得点击量最高的前50个项目,存储为 item_topk_click
。
遍历每个用户:user_based_recommend
3.4.2 user_based_recommend
基于用户协同过滤的推荐方法
获取用户历史交互项目:从 user_item_time_dict
中提取目标用户的项目点击时间序列 user_item_time_list
,并去重生成 user_hist_items
相似用户项目遍历与权重计算:遍历 u2u_sim 中与目标用户最相似的前 sim_user_topk 个用户:
- 遍历相似用户的点击文章 i:
- 若项目
i
在用户历史交互项目user_hist_items
中,跳过此项目。 - 初始化项目
i
的得分为 0。 - 定义三个权重因子:
- 位置权重
loc_weight
:根据项目在用户点击序列中的位置,越靠后权重越高。 - 内容权重
content_weight
:基于内容相似度矩阵emb_i2i_sim
,若项目i
与用户历史交互项目存在内容相似度,加权累加。 - 创建时间权重
created_time_weight
:计算项目创建时间差,时间差越小权重越高。
- 位置权重
- 将项目
i
的加权得分累加到items_rank[i]
。
- 若项目
热度补全:若 items_rank 中的项目数量不足 :
- 使用
item_topk_click
补充项目,逐项添加到items_rank
中,为这些项目分配负得分,确保不会影响排序。
排序与返回
- 按得分从高到低对
items_rank
排序,选取前recall_item_num
项。返回推荐项目列表items_rank
。
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
3.4.3 召回评估
同上
3.5 冷启动召回
3.5.1 召回准备
为冷启动用户生成推荐项,通过基于文章的召回方法并结合多种筛选规则筛选冷启动项目。
加载预计算相似度数据:加载 i2i_sim
初始化关键参数:
设置 sim_item_topk
= 150:每个文章考虑的相似文章数量。
设置 recall_item_num
= 100:为每个用户召回的文章数量。
itemCF召回得到 user_recall_items_dict:遍历每个用户:使用 item_based_recommend
get_user_hist_item_info_dict 获取以下字典:
- 用户历史中的文章类型。
- 用户历史中的文章ID。
- 用户历史中的文章单词数。
- 用户最近交互的文章的时间戳。
- 提取唯一的文章ID集合。
def cold_recall(all_click_df, user_item_time_dict, item_info_df, item_created_time_dict, item_type_dict, item_words_dict, emb_i2i_sim, weight_dict, user_multi_recall_dict, save_path):
##########冷启动
# 先进行itemcf召回,这里不需要做召回评估,这里只是一种策略
trn_hist_click_df = all_click_df
i2i_sim = pickle.load(open(save_path + 'emb_i2i_sim.pkl', 'rb'))
sim_item_topk = 200
recall_item_num = 150 # 稍微召回多一点文章,便于后续的规则筛选
user_recall_items_dict = collections.defaultdict(dict)
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'))
all_click_df_ = copy.deepcopy(all_click_df)
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)
# 需要注意的是
# 这里使用了很多规则来筛选冷启动的文章,所以前面再召回的阶段就应该尽可能的多召回一些文章,否则很容易被删掉
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, save_path)
user_multi_recall_dict['cold_start_recall'] = cold_start_user_items_dict
return user_multi_recall_dict
3.5.2 cold_start_items
- 对每个用户及其召回文章列表进行遍历:
- 获取用户的历史文章信息:
hist_item_type_set
:用户点击过的文章主题集合。hist_mean_words
:用户历史文章的平均字数。hist_last_item_created_time
:用户最后点击文章的创建时间(转为日期时间对象)。
- 获取用户的历史文章信息:
筛选适合的当前召回文章
- 对于每篇召回文章:
- 获取文章信息:
curr_item_type
:当前文章主题。curr_item_words
:当前文章字数。curr_item_created_time
:当前文章创建时间(转为日期时间对象)。
- 进行筛选:
- 当前文章不能出现在用户的历史点击中。
- 当前文章的主题必须在用户历史文章主题集合中。
- 当前文章的字数与用户历史文章的平均字数差距不能超过 200。
- 当前文章的创建时间与用户最后点击文章的创建时间差距不能超过 90 天。
- 获取文章信息:
添加符合条件的文章
- 如果文章通过了上述所有筛选条件,将其添加到
cold_start_user_items_dict
中。
控制召回数量
- 对每个用户的推荐文章列表按得分排序,保留前
recall_item_num
个文章。
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, save_path):
"""
冷启动的情况下召回一些文章
:param user_recall_items_dict: 基于内容embedding相似性召回来的很多文章, 字典, {user1: [item1, item2, ..], }
:param user_hist_item_typs_dict: 字典, 用户点击的文章的主题映射
:param user_hist_item_words_dict: 字典, 用户点击的历史文章的字数映射
:param user_last_item_created_time_dict: 字典,用户点击的历史文章创建时间映射
:param item_type_dict: 字典,文章主题映射
:param item_words_dict: 字典,文章字数映射
:param item_created_time_dict: 字典, 文章创建时间映射
:param click_article_ids_set: 集合,用户点击过得文章, 也就是日志里面出现过的文章
:param recall_item_num: 召回文章的数量, 这个指的是没有出现在日志里面的文章数量
"""
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)
# 获取当前召回文章的信息
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)
# 首先,文章不能出现在用户的历史点击中, 然后根据文章主题,文章单词数,文章创建时间进行筛选
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)..]...}
# 需要控制一下冷启动召回的数量
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
标签:items,id,item,阿里,dict,user,召回,天池,click
From: https://blog.csdn.net/qq_34517343/article/details/143276226