新闻推荐之Baseline

新闻推荐

问题导向:

  1. 如何构建Baseline模型?
  2. ItemCF和UserCF的区别?
  3. 用户推荐结果怎么给定?
  4. 评价指标怎么评测?

问题描述:赛题以新闻APP中的新闻推荐为背景,要求选手根据用户历史浏览点击新闻文章的数据信息预测用户未来点击行为,即用户的最后一次点击的新闻文章,测试集对最后一次点击行为进行了剔除。

评价指标:
s c o r e ( u s e r ) = ∑ k = 1 5 s ( u s e r , k ) k score(user) = \sum_{k=1}^5 \frac{s(user, k)}{k} score(user)=k=15ks(user,k)
评估:假如article1就是真实的用户点击文章,也就是article1命中, 则s(user1,1)=1, s(user1,2-4)都是0, 如果article2是用户点击的文章, 则s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第几条的倒数。如果都没中, 则score(user1)=0。 这个是合理的, 因为我们希望的就是命中的结果尽量靠前, 而此时分数正好比较高。

业务背景知识:新闻三大特性:1、真实性。即反映的内容必须是真实的;2、时效性。即具有时间限制;3、准确性。即报道的时间、地点、人物必须与事实相符合。
数据概况: 该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B

使用方法:
Baseline使用协同过滤的方法进行确定
方法流程:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BlSuwpR-1610616393315)(./imgs/profile.png)

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np 
import pandas as pd
import time
from tqdm import tqdm
from datetime import datetime
import collections
from collections import defaultdict
import math
import pickle
import warnings
warnings.filterwarnings("ignore")


DATA_PATH = "./data/"
SAVE_PATH = "./checkpoints/"

节约内存的一个标配函数

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

读取采样或全量数据

def get_all_click_sample(data_path, samples = 10000):
    """对数据进行采样"""
    
    all_click = pd.read_csv(data_path + "train_click_log.csv")
    all_user_id = all_click["user_id"].unique()
    sample_user_id =  np.random.choice(all_user_id, 10000)  # 进行无放回的采样数据
    all_click = all_click[all_click["user_id"].isin(sample_user_id)]

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

获取用户-文章-点击时间字典

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

ItemCF的物品相似度计算

算法依据:ItemCF算法并不利用物品的内容属性计算物品之间的相似度, 主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c。改进之后,基于公式四进行编写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VrOGOLqx-1610420260845)(./imgs/item.png)]

def itemcf_sim(df):
    """计算物品相似度""" 
    
    user_item_time_df = get_user_item_time(df)
    
    i2i_sim = {}
    item_cnt = defaultdict(int)  # 初始值为0

    # 改进权重的基于物品的协同过滤算法
    for user, item_time_list in tqdm(user_item_time_df.items()):
            for i, i_click_time in item_time_list:
                item_cnt[i] += 1  # 统计喜欢item的用户数目
                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)  # 既喜欢商品i,又喜欢商品j, 同一个用户喜欢的物品就是都喜欢
                    
    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_sim.pkl", "wb"))
    
    return i2i_sim_
i2i_sim = itemcf_sim(all_click_df)
100%|██████████| 250000/250000 [00:24<00:00, 10295.64it/s]

ItemCF的文章推荐

def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
    """
    """
    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]:  # 选择与当前文章最相似的k篇文章
            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():
                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_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)
100%|██████████| 250000/250000 [43:22<00:00, 96.04it/s]  

装换数据到DataFrame

# 将字典的形式转换成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'])
recall_df
user_idclick_article_idpred_score
01999992769700.172377
11999991585360.106969
21999992863210.097774
31999991088550.092462
41999991626550.091407
............
24999952000001870050.071191
2499996200000505730.071180
2499997200000633440.071180
24999982000002551530.068034
24999992000001956030.065900

生成submit提交文件

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_users
array([249999, 249998, 249997, ..., 200002, 200001, 200000])
# 从所有的召回数据中将测试集中的用户选出来
tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]
tst_recall
user_idclick_article_idpred_score
20000002499992346980.280279
2000001249999957160.245980
20000022499993362230.244608
20000032499991601320.227058
2000004249999590570.205233
............
24999952000001870050.071191
2499996200000505730.071180
2499997200000633440.071180
24999982000002551530.068034
24999992000001956030.065900
# 生成提交文件
submit(tst_recall, topk=5, model_name='itemcf_baseline')

参考

pandas内存优化

!head -10 checkpoints/itemcf_baseline_12-08.csv
user_id,article_1,article_2,article_3,article_4,article_5
200000,237870,194619,194935,314048,195773
200001,64329,272143,199198,324823,166581
200002,300128,300923,61375,293301,298035
200003,337143,272143,156619,235230,158536
200004,235870,235616,336223,261612,156964
200005,69932,160974,156964,160417,158536
200006,199197,284547,235230,183176,206934
200007,289003,157478,97530,218028,66672
200008,235870,300082,156560,64409,336223
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值