天池-零基础入门推荐系统新闻推荐之多路召回02

什么TM的是召回

先来看推荐系统架构图
在这里插入图片描述
对于多路召回
在这里插入图片描述
   召回层是对原始的,大规模的数据,通过简单的特征与模型进行粗排,降低数据的规模而不失主要数据信息,返回待排序的候选集。多路召回每条路径可以采用不同特征与算法模型,路径之间是独立的。召回层是实际工程的产物,在实际生产中也会有相适应算法模型。最后在导入排序层时,可以分配不同的权重进行路径融合。
在这里插入图片描述
  本新闻推荐数据总共有25万行之多,对于建立召回层是有必要的。比赛中会涉及到三种调试模式:

  • debug模式:从训练集中抽取一部分数据进行调试,最低保证模型能够跑的通(baseline)
  • 线下模式:加载整个训练集进行训练
  • 线上模式:加载整个数据集(训练集+测试集),得出最后的提交结果

  召回层的内容包括:

  • 不同模式的数据加载
  • 各召回路径数据的加载
  • 各召回路径算法的设计
  • 召回路径的融合

debug模式的数据加载

#debug模式,获取训练数据
def get_train_sample(path, samples=10000):
	#获取不重复的user_id
    all_click = pd.read_csv(data_path + 'testA_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
#获取文章属性数据
def get_item_info(path):
    item_info = pd.read_csv(path + 'articles.csv')
    item_info = item_info.rename(columns={'article_id': 'click_article_id'})
    return item_info
#读取文章embeding内容
def get_item_emb_dict(path):
    item_emb = pd.read_csv(path + 'articles_emb.csv')
    #因为文章id属性
    item_emb_cols = [x for x in item_emb.columns if 'emb' in x]
    
    # 进行归一化,值除以二范
    # axis=1 :按行求二范
    # keepdims:是否保持原来数据的维度
    item_emb_np = item_emb[item_emb_cols].values
    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.load()
    pickle.dump(item_emb_dict, open(path + 'item_content_emb.pkl', 'wb'))
    
    return item_emb_dict

召回策略罗列

先罗列召回策略的目的一是召回方法的原理,二是寻找所需策略的数据需求是什么样子。
本文章涉及的召回策略:

  • Youtube DNN 召回
  • 文章的协同过滤
  • 用户的协同过滤

Youtube DNN 召回层重点:
重读Youtube深度学习推荐系统论文,字字珠玑,惊为神文
词向量word2vec模型解读

  1. 用word2vec方法对视频做了embeding处理
  2. 特征工程后新加入的特征与视频embeding拼接作为输入,并要求每位用户的输入特征长度相同
  3. 通过RELU神经网路训练
  4. 转化成了分类问题,通过负采样加快计算,输入softmax预测类别

文章的协同过滤重点:

  1. 给用户召回历史文章相似的文章
  2. 召回相似文章需要关联以下规则:
    1. 考虑相似文章与历史点击文章顺序的权重
    2. 考虑相似文章与历史点击文章创建时间差的权重
    3. 文章内容相似度权重(相似的文章与历史点击文章不存在相似度)

用户的协同过滤重点:

  1. 给用户推荐与其相似的用户历史点击文章
  2. 召回相似文章需要关联以下规则:
    1. 计算被推荐用户历史点击文章与相似用户历史点击文章的相似度
    2. 计算被推荐用户历史点击文章与相似用户历史点击文章创建时间差
    3. 计算被推荐用户历史点击文章与相似用户历史点击相对位置的总和

根据以上的需求与规则,大概知道了需要什么样的数据格式,下面是数据格式的准备。

各召回路径的数据准备

  1. 想要相似文章就需要相似物品矩阵
#据点击时间获取用户的点击文章序列   {user1: [(item1, time1), (item2, time2)..]...}
#历史物品之间是不计算相似度的,即在user1这里,item1与item2不计算相似度
'''
假如循环到了user1的item1,则计算item1与下一个用户的其它物品的相似度,计算相似度需要考虑到上方陈述的3方面的权重,即(2.1,2.2,2.3)
'''
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

#文章创建时间
def item_created_time(df):
    item_created_time_dict = dict(zip(df["click_article_id"] , df["created_at_ts"]))
    return item_created_time_dict
   
#难点
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 user_item_time_dict.items():
    	#{user1: [(item1, time1), (item2, time2)..]
    	#忽略user_id,获得后面的列表
        for loc1 , (i , i_click_time) in enumerate(item_time_list):
        	# loc1 是item所在的索引就是列表中的位置,列表中item按照用户点击顺序排列
            item_cnt[i] += 1
            i2i_sim.setdefault(i, {}) #原字典中没有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(path + 'itemcf_i2i_sim.pkl', 'wb'))
    
    return i2i_sim_

'''
最后i2i_sim_的结果格式如下
{195839: {191971: 0.17002324109946493,
  194300: 0.14028226677401606,
  166581: 0.004738731071607025,
  272143: 0.0030777894292628007,
  285298: 0.014267230873702937,
  39857: 0.03787405668573889,
  194717: 0.0762200687996933,
  233717: 0.010576962590031045,
  36399: 0.00891114964092883,
  195868: 0.0353609868198151,
  ...
  }
'''
  1. 按照物品相似矩阵的逻辑计算用户相似矩阵
#逻辑相同,权重计算有所不同
#根据时间获取商品被点击的用户序列  {item1: [(user1, time1), (user2, time2)...]...}
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


#计算用户的活跃度,一种权重的计算
def get_user_activate_degree_dict(all_click_df):
    all_click_df_ = all_click_df.groupby('user_id')['click_article_id'].count().reset_index()
    
    # 用户活跃度归一化
    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(click_df, user_activate_degree_dict):
    """
        用户相似性矩阵计算
        :param all_click_df: 数据表
        :param user_activate_degree_dict: 用户活跃度的字典
        return 用户相似性矩阵
    """
    item_user_time_dict = get_item_user_time_dict(click_df)
    
    u2u_sim = {}
    user_cnt = defaultdict(int)
    for item, user_time_list in 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)
    
    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(path + 'usercf_u2u_sim.pkl', 'wb'))

    return u2u_sim_

# %%time = 54.1 s

'''
最后结果u2u_sim_格式:
{203172: {203172: 0.0,
  238503: 0.14830159613688895,
  230262: 0.43069329645723503,
  228301: 0.1689511714607187,
  242286: 0.1329408251817319,
  245198: 0.15484630639404126,
  227057: 0.41426036891152124,
  238944: 0.07430792178193243,
  241560: 0.08650412384989443,
  228013: 0.08088853325519581,
  ,,,
  }
'''
  1. YoutubeDNN召回所需要的数据

要求每个用户的数据长度是一样的,将每个用户的数据维度转化为1*N维即一行。

'''
构造YoutubeDNN输入数据
通过gen_data_set函数筛选数据特征,构造正负样本,输出训练测试集
通过gen_model_input函数将数据维度调整一致,输出模型的输入训练集与训练集标签
'''
def gen_data_set(data, negsample=2):
    data.sort_values("click_timestamp", inplace=True)
    #获取所有点击文章的id
    item_ids = data['click_article_id'].unique()

    train_set = [] #训练集
    test_set = [] #测试集
    #每次遍历用户id和该用户的行动
    for reviewerID, hist in data.groupby('user_id'):
        #用户过点击文章列表
        pos_list = hist['click_article_id'].tolist()
        #负样本
        if negsample > 0:
        	# 用户没看过的文章里面选择负样本
            candidate_set = list(set(item_ids) - set(pos_list))  
            # 对于每个正样本,选择n个负样本
            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)]
            	# 说明[用户id,历史点击文章,最后一次点击的文章,标签1或者0,历史点击文章数量]
                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

#获取模型输入
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])

	#填充数据,将数据维度拉成一致
	#没有加入label
    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 # 用户点击序列的长度,短的填充,长的截断
    
    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, user_profile, SEQ_LEN)
    test_model_input, test_label = gen_model_input(test_set, user_profile, 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)]
    
    # 模型的定义 
    # num_sampled: 负采样时的样本数量
    # YoutubeDNN来自于deepmatch
    model = YoutubeDNN(user_feature_columns, item_feature_columns, num_sampled=5, user_dnn_hidden_units=(64, embedding_dim))
    # 模型编译
    model.compile(optimizer="adam", loss=sampledsoftmaxloss)  
    
    # 模型训练,这里可以定义验证集的比例,如果设置为0的话就是全量数据直接进行训练
    history = model.fit(train_model_input, train_label, batch_size=256, epochs=1, 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(path + 'user_youtube_emb.pkl', 'wb'))
    pickle.dump(raw_item_id_emb_dict, open(path + 'item_youtube_emb.pkl', 'wb'))
    
    # faiss紧邻搜索,通过user_embedding 搜索与其相似性最高的topk个item
    index = faiss.IndexFlatIP(embedding_dim)
    # 上面已经进行了归一化,这里可以不进行归一化了
#     faiss.normalize_L2(user_embs)
#     faiss.normalize_L2(item_embs)
    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的相似性矩阵,还需要进行协同过滤召回才能得到召回结果
    # 可以直接对这个召回结果进行评估,为了方便可以统一写一个评估函数对所有的召回结果进行评估
    pickle.dump(user_recall_items_dict, open(path + 'youtube_u2i_dict.pkl', 'wb'))
    return user_recall_items_dict

各路径召回

三种召回方式,需要评定召回效果,所以添加召回函数metrics_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)

# 获取当前数据的历史点击和最后一次点击
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)

    # 如果用户只有一个点击,hist为空了,会导致训练的时候这个用户不可见,此时默认泄露一下
    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

#获取热门商品
def get_item_topk_click(click_df, k):
    topk_click = click_df['click_article_id'].value_counts().index[:k]
    return topk_click
#Youtube DNN召回
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)
# 召回效果评估
metrics_recall(user_multi_recall_dict['youtubednn_recall'], trn_last_click_df, topk=20)

#itemCF召回
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



user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)
i2i_sim = pickle.load(open(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 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(path + 'embedding_sim_item_recall.pkl', 'wb'))
#召回评估
metrics_recall(user_multi_recall_dict['embedding_sim_item_recall'], trn_last_click_df, topk=recall_item_num)
#UserCF
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
user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(trn_hist_click_df)

u2u_sim = pickle.load(open(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(path + 'usercf_u2u2i_recall.pkl', 'wb'))
#召回评估
metrics_recall(user_recall_items_dict, trn_last_click_df, topk=recall_item_num)

召回合并

def combine_recall_results(user_multi_recall_dict, weight_dict=None, topk=25):
    final_recall_items_dict = {}
    
    # 对每一种召回结果按照用户进行归一化,方便后面多种召回结果,相同用户的物品之间权重相加
    def norm_user_recall_items_sim(sorted_item_list):
        # 如果冷启动中没有文章或者只有一篇文章,直接返回,出现这种情况的原因可能是冷启动召回的文章数量太少了,
        # 基于规则筛选之后就没有文章了, 这里还可以做一些其他的策略性的筛选
        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 + '...')
        # 在计算最终召回结果的时候,也可以为每一种召回结果设置一个权重
        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(): # 进行归一化
            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 = {}
    # 多路召回时也可以控制最终的召回数量
    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]

    # 将多路召回后的最终结果字典保存到本地
    pickle.dump(final_recall_items_dict_rank, open(os.path.join(path, 'final_recall_items_dict.pkl'),'wb'))

    return final_recall_items_dict_rank

参考资料:
天池零基础入门推荐系统

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
心跳信号分类预测是一个基于数据挖掘的重要任务,本次回答将介绍在天池-基础入门数据挖掘比赛中心跳信号分类预测项目中的EDA(探索性数据分析)分析过程和相应代码。 首先,我们需要导入所需的库和数据集,如下所示: ```python import pandas as pd import numpy as np # 导入训练集 train_df = pd.read_csv('train.csv') # 导入测试集 test_df = pd.read_csv('test.csv') ``` 接下来,我们可以进行一些基本的数据探索,如查看数据集的形状和前几行数据等: ```python # 查看训练集形状 train_df.shape # 查看训练集前几行数据 train_df.head() ``` 然后,我们可以对数据集进行一些统计性分析,如计算各个特征的缺失值数量、平均值、标准差等: ```python # 计算训练集特征的缺失值数量 train_df.isnull().sum() # 计算训练集特征的均值 train_df.mean() # 计算训练集特征的标准差 train_df.std() ``` 接下来,我们可以对数据集中的特征进行可视化分析,以便更好地理解数据: ```python import matplotlib.pyplot as plt # 绘制训练集中特征的直方图 train_df.hist(figsize=(10, 10), bins=50) plt.show() # 绘制训练集中特征之间的相关性热图 correlation = train_df.corr() plt.figure(figsize=(10, 10)) plt.imshow(correlation, cmap='hot', interpolation='nearest') plt.colorbar() plt.xticks(np.arange(len(correlation.columns)), correlation.columns, rotation=90) plt.yticks(np.arange(len(correlation.columns)), correlation.columns) plt.show() ``` 最后,我们可以对数据集中的特征进行预处理和特征工程,以提高模型的性能: ```python from sklearn.preprocessing import StandardScaler # 对训练集的特征进行标准化 scaler = StandardScaler() scaled_features = scaler.fit_transform(train_df.drop('target', axis=1)) # 构建新的训练集 new_train_df = pd.DataFrame(scaled_features, columns=train_df.columns[:-1]) new_train_df['target'] = train_df['target'] ``` 以上就是在天池-基础入门数据挖掘比赛中心跳信号分类预测项目中的EDA分析过程和相应代码。通过探索性数据分析,我们可以更好地理解数据集,并为后续的特征工程和模型训练做好准备。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值