天池新闻推荐比赛1:赛题理解+baseline

天池新闻推荐比赛1:赛题理解+baseline

一、比赛信息

比赛链接:

https://tianchi.aliyun.com/competition/entrance/531842/information

比赛简介:

​ 赛题以预测用户未来点击新闻文章为任务,该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,将会从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B。

评价指标:

​ 以MRR作为评价指标,MRR(Mean reciprocal rank)是一个国际上通用的对搜索算法进行评价的机制,即第一个结果匹配,分数为1,第二个匹配分数为0.5,第n个匹配分数为1/n,如果没有匹配的句子分数为0。最终的分数为所有得分之和。对于本次比赛,每个用户都会向推荐Top5的文章,故每个用户的MRR定义如下:

( ext {score}(u s e r)=sum_{k=1}^{5} frac{s(u s e r, k)}{k} )

​ 最终的得分为所有用户分数的平均值。

二、赛题理解

​ 本次比赛是新闻背景下的推荐系统,可用的特征为新闻特征+用户特征。新闻特征主要是文章的长度、文章类型和文章创建时间,而用户特征为用户本身的信息+之前的点击日志构成。最终的预测值用户的最后一次点击的文章。关键是如何将真实场景下的用户日志转化为可以作为模型输入的特征,这是本次比赛的一个挑战。

三、赛题Baseline

​ Baseline采用item-based协同过滤,item-based协同过滤常见思路如下:

  1. 构建物品相似度矩阵

  2. 根据相似度矩阵和用户之前购买过的物品,找出TopN个相似物品

  3. 对TopN个物品排序

    具体的代码和思路如下:

    导入相关的库

    import time, math, os
    from tqdm import tqdm
    import gc
    import pickle
    import random
    from datetime import datetime
    from operator import itemgetter
    import numpy as np
    import pandas as pd
    import warnings
    import collections
    from collections import defaultdict
    warnings.filterwarnings('ignore')
    
    # 数据读取,用训练集+测试集的物品一起构建物品相似度矩阵
    data_path = './data_raw/'
    save_path = './tmp_results/'
    train_click = pd.read_csv(data_path + 'train_click_log.csv')
    test_click =  pd.read_csv(data_path + 'testA_click_log.csv')
    all_click_df = train_click.append(test_click)
    

    按点击时间排序,获取用户:[点击文章,点击时间] 字典

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']))
    # 以用户id为分组,进行[点击文章,点击时间]的配对
    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 get_item_topk_click(click_df, k):
    topk_click = click_df['click_article_id'].value_counts().index[:k]
    return topk_click

用全部数据集,构建物品相似度矩阵

def itemcf_sim(df):
    """
        文章与文章之间的相似性矩阵计算
        :param df: 数据表
        :item_created_time_dict:  文章创建时间的字典
        return : 文章与文章的相似性矩阵
        思路: 基于物品的协同过滤
    """
    
    user_item_time_dict = get_user_item_time(df)
    
    # 计算物品相似度
    i2i_sim = {}
    item_cnt = defaultdict(int)
    for user, item_time_list in tqdm(user_item_time_dict.items()):
        # 在基于商品的协同过滤优化的时候可以考虑时间因素
        for i, i_click_time in item_time_list:
            item_cnt[i] += 1
            i2i_sim.setdefault(i, {})
            for j, j_click_time in item_time_list:
                if(i == j):
                    continue
                i2i_sim[i].setdefault(j, 0)
                #对活跃的用户进行惩罚
                i2i_sim[i][j] += 1 / 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_

基于相似度矩阵进行召回

def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
    """
        基于文章协同过滤的召回
        :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: 列表,点击次数最多的文章列表,用户召回补全        
        return: 召回的文章列表 {item1:score1, item2: score2...}
        注意: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略
    """
    
    # 获取用户历史交互的文章
    user_hist_items = user_item_time_dict[user_id]
    
    item_rank = {}
    for loc, (i, click_time) in enumerate(user_hist_items):
        for j, wij in sorted(i2i_sim[i].items(), key=lambda x: x[1], reverse=True)[:sim_item_topk]:
            if j in user_hist_items:
                continue
                
            item_rank.setdefault(j, 0)
            item_rank[j] +=  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

定义提交文件函数

# 生成提交文件
def submit(recall_df, topk=5, model_name=None):
    recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])
    recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
    
    # 判断是不是每个用户都有5篇文章及以上
    tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
    assert tmp.min() >= topk
    
    del recall_df['pred_score']
    submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id', 'rank']).unstack(-1).reset_index()
    
    submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]
    # 按照提交格式定义列名
    submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', 
                                                  3: 'article_3', 4: 'article_4', 5: 'article_5'})
    
    save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'
    submit.to_csv(save_name, index=False, header=True)

设置基本的参数

# 定义
user_recall_items_dict = collections.defaultdict(dict)

# 获取 用户 - 文章 - 点击时间的字典
user_item_time_dict = get_user_item_time(all_click_df)

# 去取文章相似度
if os.path.exists(save_path + 'itemcf_i2i_sim.pkl'):
    i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))
else:
    i2i_sim = itemcf_sim(all_click_df)
    
# 相似文章的数量
sim_item_topk = 10

# 召回文章数量
recall_item_num = 10

# 用户热度补全
item_topk_click = get_item_topk_click(all_click_df, k=50)

得到测试集结果

# 获取测试集的结果
for user in tqdm(test_click['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) 
    
# 将字典的形式转换成df
user_item_score_list = []
for user, items in tqdm(user_recall_items_dict.items()):
    for item, score in items:
        user_item_score_list.append([user, item, score])
        
recall_df = pd.DataFrame(user_item_score_list, columns=['user_id', 'click_article_id', 'pred_score'])
# 生成提交文件
submit(recall_df , topk=5, model_name='itemcf_baseline')

四、总结

这个baseline虽然只是最基础的模型,而且还有许多的用户和新闻的信息并没有利用起来。但让我们熟悉了新闻推荐的基本流程,了解了常见的数据处理方法,最终从用户日志中得到可以使用的特征。

原文地址:https://www.cnblogs.com/zwrAI/p/14038181.html