这次赛题是以新闻APP的新闻推荐为背景,目的是要我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为,即用户最后一次点击的新闻文章。
该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的文章,同时每篇新闻有对应的embedding向量表示。
https://tianchi.aliyun.com/competition/entrance/531842/learn
理解评价方式,我们需要结合最后提交的文件来看,根据sample.submit.csv来看,我们最后提交的格式是针对每个用户给出5篇推荐文章,按照点击概率从前往后排序。而真实的每个用户最后点击的文章只会有一篇是真实答案,所以就看我们推荐的这5篇文章用户是否有真实答案。比如,对于用户user1来说,我们提交的结果是:user1,article1,article2,article3,article4,article5。评价的指标公式如下
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=1∑5ks(user,k)
如果用户最后的点击文章是article1,那么s(user1,1)=1,s(user1,2-5)=0。如果article2是用户最后的点击文章,那么s(user1,2)=1。也就是score(user)=命中第几条的倒数,这符合我们的要求,因为我们希望用户的点击文章靠前。
根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。
如何把预测问题转换成一个监督学习的问题(特征+标签),然后再进行DL、ML建模。
要先转成一个分类问题来做,而分类的标签是用户是否会点击某篇文章,分类问题的特征中会有用户、文章,我们要训练一个分类模型,对用户最后一次点击某篇文章的概率进行预测。
首先,我们需要知道什么是baseline,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 = './data_raw/'
save_path = './tmp_results/'
def get_all_click_sample(data_path,sample_nums =10000):
all_click = pd.read_csv(data_path+'train_click_log.csv')
all_users_id = all_click.user_id.unique()
sample_user_ids = np.random.choice(all_users_id,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 = './data_raw/',offline = True):
if offline:
all_click = pd.read_csv(data_path+'train_click_log.csv',encoding='utf-8')
else:
trn_click = pd.read_csv(data_path + 'train_click_log.csv',encoding='utf-8')
tst_click = pd.read_csv(data_path + 'testA_click_log.csv',encoding='utf-8')
all_click = trn_click.append(tst_click)
all_click = all_click.drop_duplicates((['user_id','click_article_id','click_timestamp']))
return all_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']))
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
get_user_item_time(all_click_sample)
def get_item_topk_click(click_df,k):
topk_click = click_df['click_article_id'].value_counts().index[:k]
return topk_click
首先介绍一下物品相似度的数学原理,根据用户行为计算用户的兴趣相似度,John S.Breese提出了如下公式
可以看到,该公式通过
1 l o g ( 1 + N ( i ) ) \frac{1}{log(1+N(i))} log(1+N(i))1
惩罚了用户u和v共同兴趣列表中热门物品对他们相似度的影响。
def itemcf_sim(df):
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):
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
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_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)
通过对赛题的初步分析,我们得到了一个最基本的推荐结果,我们后续会在此基础上进行模型的优化,得到更好的推荐结果。