天池新闻推荐入门赛持续跟进——First one!

文章目录

  • 前言
  • 一、赛题
  • 二、赛题理解
    • 1.推荐系统背景
    • 2.数据概况
    • 3.评价方式理解
    • 4.预测模型选择
  • baseline及代码实现
    • 导包
    • 节约内存函数
    • 导入数据的两种方式
    • 据点击时间获取用户的点击文章序列
    • 获取近期点击最多的文章
    • 文章与文章之间的相似性矩阵计算
    • 生成全量训练集
    • 生成文章之间的相似性矩阵
    • 基于文章协同过滤的召回
    • 基于协同过滤给用户推荐文章
    • 将字典的形式转换成df
    • 自定义函数生成提交文件
    • 生成提交文件
  • 总结


前言

DataWhale组队学习 Task01 欢迎各方小伙伴一起交流讨论

本文主要内容为针对阿里云天池大赛—— “零基础入门推荐系统之新闻推荐场景下的用户行为预测挑战赛”所做的赛题分析及baseline的初步探究。

一、赛题

赛题以新闻APP中的新闻推荐为背景,要求选手根据用户历史浏览点击新闻文章的数据信息预测用户未来点击行为,即用户的最后一次点击的新闻文章,测试集对最后一次点击行为进行了剔除。
通过这道赛题来引导大家了解推荐系统中的一些业务背景,解决实际问题,帮助竞赛新人进行自我练习、自我提高。

二、赛题理解

1.推荐系统背景

移动联通互联网、人工智能等技术的迅速发展为人们的工作生活带来了很多便利,但是同时也带来了信息过载问题。搜索引擎和推荐系统是解决信息过载的代表技术。传统的搜索引擎在本质上来讲是帮助用户过滤和筛选信息,这种方式满足了大多数人的需求,但没有提供个性化的服务。相对于传统搜索引擎来说,推荐系统可以兼顾个性化需求和解决信息过载问题。推荐系统是信息过滤系统的一个子集,目的在于根据用户的喜好、习惯、个性化需求以及商品的特性来预测用户对商品的喜好,为用户推荐最合适的商品,帮助用户快速地做出决策,提高用户满意度。推荐系统的价值在于能够提供尽量合适的选择或者是推荐而不需要用户明确提供他们所想要的内容。随着大数据时代的到来,传统推荐系统在挖掘数据价值上存在的问题正在限制其性能发挥。知识图谱的出现为大数据环境下的推荐系统设计提供了一种有效途径。
  目前,推荐系统已经成为产业界和学术界关注、研究的热点问题,应用领域十分广泛,在电子商务、社交网络、视频音乐推荐等领域都有所应用。例如亚马逊网站、京东、淘宝网站为用户推荐商品,MovieLens推荐电影的功能等。

2.数据概况

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

train_click_log.csv:训练集用户点击日志

testA_click_log.csv:测试集用户点击日志

articles.csv:新闻文章信息数据表

articles_emb.csv:新闻文章embedding向量表示

sample_submit.csv:提交样例文件

代码如下(示例):

3.评价方式理解

根据给出的提交样例sample.submit.csv可以看出,提交的最终结果是一个csv文件,针对测试集中的每一个用户,给出点击概率由大到小排序的文章id,如下:

user1, article1, article2, article3, article4, article5.
其中user_id,article_1,article_2,article_3,article_4,article_5为预测用户点击新闻文章Top5的article_id依概率从高到低排序

赛题给出的评价指标为
MRR(Mean Reciprocal Rank)

首先对选手提交的表格中的每个用户计算用户得分
在这里插入图片描述
其中,k为文章的排名,如果选手对该user的预测结果命中该user的最后一条购买数据则s(user,k)=1; 否则s(user,k)=0.因此,最终的得分为该选手命中的文章排名的倒数,求和。我们希望预测的第一条数据就命中结果,这样score便最高为1.

4.预测模型选择

拿到赛题,首先要考虑的是要解决什么问题,其次是如何解决这个问题。

  1. 在本赛题中,我们要做的就是给用户推荐新闻文章,当然这个文章是他们爱看的,大概率会选择看的文章。不同于以往的数据分析赛题,这次并非是预测分类问题或者回归问题,而是推荐问题。
  2. 数据集与以往的也有差异。我们得到的数据集包含用户的行为信息,文章的信息以及文章的词向量信息。训练集和测试集指标完全相同,不是特征+标签形式。之前的机器学习算法显然能力有限。

我们的思考方向就是结合我们的目标,把该预测问题转成一个**监督学习的问题(特征+标签)**,然后我们才能进行机器学习、深度学习等建模预测。

考虑到在这里使用分类模型过于庞大,我们首先使用简捷且更适于此问题的基于协同过滤的推荐系统算法进行用户新闻推荐如下文。

baseline及代码实现

导包

# import packages
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 = 'F:/data/DataWhale_data/'
save_path = 'F:/data/DataWhale_data/tmp_results/'

节约内存函数

# 节约内存的一个标配函数
def reduce_mem(df):
    starttime = time.time()
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
    start_mem = df.memory_usage().sum() / 1024**2
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if pd.isnull(c_min) or pd.isnull(c_max):
                continue
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)
    end_mem = df.memory_usage().sum() / 1024**2
    print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
                                                                                                           100*(start_mem-end_mem)/start_mem,
                                                                                                           (time.time()-starttime)/60))
    return df

导入数据的两种方式

# debug模式:从训练集中划出一部分数据来调试代码
def get_all_click_sample(data_path, sample_nums=10000):
    """
        训练集中采样一部分数据调试
        data_path: 原数据的存储路径
        sample_nums: 采样数目(这里由于机器的内存限制,可以采样用户做)
    """
    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

# 读取点击数据,这里分成线上和线下,如果是为了获取线上提交结果应该讲测试集中的点击数据合并到总的数据中
# 如果是为了线下验证模型的有效性或者特征的有效性,可以只使用训练集
def get_all_click_df(data_path='F:/data/DataWhale_data/', 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

据点击时间获取用户的点击文章序列

# 根据点击时间获取用户的点击文章序列   {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

获取近期点击最多的文章

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_

生成全量训练集

# 全量训练集
all_click_df = get_all_click_df(offline=False)

生成文章之间的相似性矩阵

i2i_sim = itemcf_sim(all_click_df)

在这里插入图片描述

基于文章协同过滤的召回

# 基于商品的召回i2i
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] # 注意,此时获取得到的是一个元组列表,需要将里面的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
                
            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

基于协同过滤给用户推荐文章

# 定义
user_recall_items_dict = collections.defaultdict(dict)

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

# 去取文章相似度
i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))

# 相似文章的数量
sim_item_topk = 10

# 召回文章数量
recall_item_num = 10

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

for user in tqdm(all_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)

将字典的形式转换成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'])

自定义函数生成提交文件

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)

生成提交文件

# 获取测试集
tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
tst_users = tst_click['user_id'].unique()

# 从所有的召回数据中将测试集中的用户选出来
tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]

# 生成提交文件
submit(tst_recall, topk=5, model_name='itemcf_baseline')

总结

以上就是今天Task01的全部内容啦,参照大佬的baseline进行复现,也结合自己的思想深入理解了此类赛题的处理流程和思维,领悟到了推荐算法的奇妙之处。接下来会持续更新哟!

你可能感兴趣的:(推荐系统,大数据,python)