学习小白的推荐系统路——Task 01 Baseline
学习目标:
三天时间,理解赛题和理解Baseline
学习内容:
1、 理解和掌握各种库和包的含义和用法
2、 掌握节约内存的标配函数
3、 看懂Baseline的代码
4、 掌握协同过滤算法
学习过程:
导入各种库和包
# 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
from collections import defaultdict
warnings.filterwarnings('ignore')
不太明白defaultdict函数的作用,所以去查了一下,大致就是说:defaultdict的作用是在于当字典里的key不存在但被查找时,返回的不是keyError而是一个默认值。具体返回什么默认值要看传入的参数是什么。
比如下面代码用到的:
item_cnt = defaultdict(int)
这个时候传入的参数为int型,则当查找的key值不存在时,返回0
节约内存的一个标配函数
def reduce_mem(df):
starttime = time.time() #获取当前的时间
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
start_mem = df.memory_usage().sum() / 1024**2 #df.memory_usage():返回每列的内存使用情况(以字节为单位)
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, sample_nums=10000):
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)
#从数组中随机抽取元素,replace参数:True表示可以取相同数字,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
random.choice函数:随机返回一个列表,元组或字符串的随机项。
由于没有参加天池往期的学习,而本次Baseline使用的又是协同过滤算法,所以我去找了一下上一期组队学习推荐系统基础部分的协同过滤进行学习。下面简单介绍一下协同过滤算法。
协同过滤算法
协同过滤算法主要有下面两种算法:
协同过滤算法 |
---|
基于用户的协同过滤算法(UserCF) |
基于物品的协同过滤算法(ItemCF) |
本次Baseline用到的方法是基于物品的协同过滤算法,所以主要介绍一下基于物品的协同过滤。基于物品的协同过滤(ItemCF)的基本思想是预先根据所有用户的历史偏好数据计算物品之间的相似性,然后把与用户喜欢的物品相类似的物品推荐给用户。比如物品a和c非常相似,因为喜欢a的用户同时也喜欢c,而用户A喜欢a,所以把c推荐给用户A。ItemCF算法并不利用物品的内容属性计算物品之间的相似度,主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c。
基于物品的协同过滤算法主要分为两步:
1、 计算物品之间的相似度
2、根据物品的相似度和用户的历史行为给用户生成推荐列表(购买了该商品的用户也经常购买的其他商品)
首先看一下计算物品之间相似度的代码:
similarity_matrix = pd.DataFrame(np.ones((len(items), len(items))), index=['A', 'B', 'C', 'D', 'E'], columns=['A', 'B', 'C', 'D', 'E'])
# 遍历每条物品-用户评分数据
for itemId in items:
for otheritemId in items:
vec_item = [] # 定义列表, 保存当前两个物品的向量值
vec_otheritem = []
#userRagingPairCount = 0 # 两件物品均评过分的用户数
if itemId != otheritemId: # 物品不同
for userId in users: # 遍历用户-物品评分数据
userRatings = users[userId] # 每条数据为该用户对所有物品的评分, 这也是个字典
if itemId in userRatings and otheritemId in userRatings: # 用户对这两个物品都评过分
#userRagingPairCount += 1
vec_item.append(userRatings[itemId])
vec_otheritem.append(userRatings[otheritemId])
# 这里可以获得相似性矩阵(共现矩阵)
similarity_matrix[itemId][otheritemId] = np.corrcoef(np.array(vec_item), np.array(vec_otheritem))[0][1]
#similarity_matrix[itemId][otheritemId] = cosine_similarity(np.array(vec_item), np.array(vec_otheritem))[0][1]
最终得到了一个相似性矩阵
接着看一下根据物品的相似度和用户的历史行为给用户生成推荐列表的代码:
"""得到与物品5相似的前n个物品"""
n = 2
similarity_items = similarity_matrix['E'].sort_values(ascending=False)[1:n+1].index.tolist() # ['A', 'D']
"""计算最终得分"""
base_score = np.mean(np.array([value for value in items['E'].values()]))
weighted_scores = 0.
corr_values_sum = 0.
for item in similarity_items: # ['A', 'D']
corr_value = similarity_matrix['E'][item] # 两个物品之间的相似性
mean_item_score = np.mean(np.array([value for value in items[item].values()])) # 每个物品的打分平均值
weighted_scores += corr_value * (users[1][item]-mean_item_score) # 加权分数
corr_values_sum += corr_value
final_scores = base_score + weighted_scores / corr_values_sum
print('用户Alice对物品5的打分: ', final_scores)
user_df.loc[1]['E'] = final_scores
user_df
代码中用到的公式有余弦相似度和皮尔逊相关系数。
我们现在重新看回Baseline的代码:
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) #当查找的key值不存在时,返回0
for user, item_time_list in tqdm(user_item_time_dict.items()): #Tqdm 是一个快速,可扩展的Python进度条,可以在 Python 长循环中添加一个进度提示信息
# 在基于商品的协同过滤优化的时候可以考虑时间因素
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_
这里计算了文章与文章之间的相似性矩阵
setdefault函数:我觉得和上面的defaultdict有点像,这个函数是如果键不存在于字典中,将会添加键并将值设为默认值。
注意:一开始运行这段代码的时候报错了,后来发现是
item_cnt = collections.defaultdict(int)的写法有问题,上面已经import过defaultdict函数了,所以不能再写成collections.defaultdict(int),而是应该直接调用defaultdict函数。
下面看一下基于商品的召回:
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
生成提交文件代码块:
# 生成提交文件
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)
总结:
其实本次学习就是抱着学习推荐系统的目的来的,因为之前完全没有接触过,结果在参加一个数据分析比赛的时候遇到一道推荐系统的题,结果这道题完全空了…
所以心有不甘的几位小白决定来攻一攻这个推荐系统,目前Baseline的代码大致看懂了,接下来要好好调一下参数,提高一下结果数据。