推荐系统入门一:赛题理解+Baseline

赛题理解
根据赛题简介,我们首先要明确我们此次比赛的目标: 根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。从这个目标上看, 会发现此次比赛和我们之前遇到的普通的结构化比赛不太一样, 主要有两点:

首先是目标上, 要预测最后一次点击的新闻文章,也就是我们给用户推荐的是新闻文章, 并不是像之前那种预测一个数或者预测数据哪一类那样的问题
数据上, 通过给出的数据我们会发现, 这种数据也不是我们之前遇到的那种特征+标签的数据,而是基于了真实的业务场景, 拿到的用户的点击日志
所以拿到这个题目,我们的思考方向就是结合我们的目标,把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行ML,DL等建模预测。那么我们自然而然的就应该在心里会有这么几个问题:如何转成一个监督学习问题呢? 转成一个什么样的监督学习问题呢? 我们能利用的特征又有哪些呢? 又有哪些模型可以尝试呢? 此次面对数万级别的文章推荐,我们又有哪些策略呢?

当然这些问题不会在我们刚看到赛题之后就一下出来答案, 但是只要有了问题之后, 我们就能想办法解决问题了, 比如上面的第二个问题,转成一个什么样的监督学习问题? 由于我们是预测用户最后一次点击的新闻文章,从36万篇文章中预测某一篇的话我们首先可能会想到这可能是一个多分类的问题(36万类里面选1), 但是如此庞大的分类问题, 我们做起来可能比较困难, 那么能不能转化一下? 既然是要预测最后一次点击的文章, 那么如果我们能预测出某个用户最后一次对于某一篇文章会进行点击的概率, 是不是就间接性的解决了这个问题呢?概率最大的那篇文章不就是用户最后一次可能点击的新闻文章吗? 这样就把原问题变成了一个点击率预测的问题(用户, 文章) --> 点击的概率(软分类), 而这个问题, 就是我们所熟悉的监督学习领域分类问题了, 这样我们后面建模的时候, 对于模型的选择就基本上有大致方向了,比如最简单的逻辑回归模型。

这样, 我们对于该赛题的解决方案应该有了一个大致的解决思路,要先转成一个分类问题来做, 而分类的标签就是用户是否会点击某篇文章,分类问题的特征中会有用户和文章,我们要训练一个分类模型, 对某用户最后一次点击某篇文章的概率进行预测。 那么又会有几个问题:如何转成监督学习问题? 训练集和测试集怎么制作? 我们又能利用哪些特征? 我们又可以尝试哪些模型? 面对36万篇文章, 20多万用户的推荐, 我们又有哪些策略来缩减问题的规模?如何进行最后的预测?

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/’
df节省内存函数

节约内存的一个标配函数

def reduce_mem(df):
starttime = time.time()
numerics = [‘int16’, ‘int32’, ‘int64’, ‘float16’, ‘float32’, ‘float64’]
start_mem = df.memory_usage().sum() / 10242
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=’./data_raw/’, 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

获取点击最多的Topk个文章

获取近期点击最多的文章

def get_item_topk_click(click_df, k):
topk_click = click_df[‘click_article_id’].value_counts().index[:k]
return topk_click
itemCF的物品相似度计算
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_

i2i_sim = itemcf_sim(all_click_df)
itemCF 的文章推荐

基于商品的召回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

将字典的形式转换成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’)
运行结果:

0.1026

参考:http://datawhale.club/t/topic/196

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页