赛题:天池大赛—零基础入门推荐系统—新闻推荐
天池新闻推荐入门赛之【赛题理解+Baseline】
推荐系统组队学习之协同过滤
推荐系统基本流程
赛题数据
数据表
- train_click_log.csv:训练集用户点击日志(20W)
- testA_click_log.csv:测试集用户点击日志(5W)
- articles.csv:新闻文章信息数据表
- articles_emb.csv:新闻文章embedding向量表示
- sample_submit.csv:提交样例文件
字段表
Field | Description | Field | Description |
---|---|---|---|
user_id | 用户id | article_id | 文章id,与click_article_id相对应 |
click_article_id | 点击文章id | category_id | 文章类型id |
click_timestamp | 点击时间戳 | created_at_ts | 文章创建时间戳 |
click_environment | 点击环境 | words_count | 文章字数 |
click_deviceGroup | 点击设备组 | emb_1,emb_2,…,emb_249 | 文章embedding向量表示 |
click_os | 点击操作系统 | ||
click_country | 点击城市 | ||
click_region | 点击地区 | ||
click_referrer_type | 点击来源类型 |
算法
Baseline使用基于物品的协同过滤(ItemCF)算法,仅使用到点击日志文件(train_click_log.csv、testA_click_log.csv)中的user_id
、click_article_id
、click_timestamp
三个字段。
基于物品的协同过滤算法主要分为两步。
- 计算物品之间的相似度
- 根据物品之间相似度和用户的历史行为给用户生产推荐列表。
文章相似度
计算物品之间相似度——余弦相似度:
S
i
,
j
=
∣
N
(
i
)
∩
N
(
j
)
∣
∣
N
(
i
)
∣
∣
N
(
j
)
∣
S_{i,j}=\frac{\lvert N(i)\cap N(j)\rvert}{\sqrt{\lvert N(i)\rvert\lvert N(j)\rvert}}
Si,j=∣N(i)∣∣N(j)∣∣N(i)∩N(j)∣
其中, N ( i ) N(i) N(i)表示的是喜欢物品 i i i 的用户集合, ∣ N ( i ) ∣ \lvert N(i)\rvert ∣N(i)∣表示的是喜欢物品 i i i 的用户数量。
Code1
存在的问题:存在无用计算。计算了任意两个物品之间的余弦相似度,包括没有交互的物品。
'''
item_user_dict={
item1:{user1,user2,...},
item2:{user1,user2,...},
...
}
'''
i2i_sim={}
for i1 in item_user_dict.keys():
for i2 in item_user_dict.keys():
if i1==i2:
continue
i2i_sim[i1][i2]=len(item_user_dict[i1]&item_user_dict[i2])
i2i_sim[i1][i2]/=math.sqrt(len(item_user_dict[i1])*len(item_user_dict[i2]))
Code2
文 章 A 和 文 章 B 的 相 似 度 = ∑ i ( 用 户 i 点 击 文 章 A 的 次 数 × 用 户 i 点 击 文 章 B 的 次 数 ) / log ( 用 户 i 点 击 文 章 的 总 次 数 + 1 ) 文 章 A 被 点 击 的 总 次 数 × 文 章 B 被 点 击 的 总 次 数 文章A和文章B的相似度=\frac{\sum_{i}(用户i点击文章A的次数\times 用户i点击文章B的次数)/\log(用户i点击文章的总次数+1)}{\sqrt{文章A被点击的总次数\times 文章B被点击的总次数}} 文章A和文章B的相似度=文章A被点击的总次数×文章B被点击的总次数∑i(用户i点击文章A的次数×用户i点击文章B的次数)/log(用户i点击文章的总次数+1)
- 用 户 i 点 击 文 章 A 的 次 数 × 用 户 i 点 击 文 章 B 的 次 数 用户i点击文章A的次数\times 用户i点击文章B的次数 用户i点击文章A的次数×用户i点击文章B的次数:同一用户不同的点击时间重复计算。
- log ( 用 户 i 点 击 文 章 的 总 次 数 + 1 ) \log(用户i点击文章的总次数+1) log(用户i点击文章的总次数+1):惩罚文章看得太多的用户,例如爬虫。
def itemcf_sim(df):
"""
文章与文章之间的相似性矩阵计算
:param df: 数据表
:item_created_time_dict: 文章点击时间的字典
return : 文章与文章的相似性矩阵
思路: 基于物品的协同过滤, 在多路召回部分会加上关联规则的召回策略
"""
user_item_time_dict = get_user_item_time(df) # {user1: [(item1, time1), (item2, time2)..]...}
# 计算物品相似度
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, {}) # 如果存在key=i,则不会置为空字典
for j, j_click_time in item_time_list:
if(i == j):
continue
i2i_sim[i].setdefault(j, 0) # 如果存在key=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 get_item_topk_click(click_df, k):
topk_click = click_df['click_article_id'].value_counts().index[:k]
return topk_click
为用户user_id推荐文章
1. 基于用户user_id的历史交互行为为每个文章打分
对用户的每一个交互记录,例如(文章i,点击时间),对与文章 i 相似度前 sim_item_topk 的每一篇文章 j,如果该用户没有点击过文章 j,则文章 j 的评分加 wij。
2. 热门文章补全
如果评分列表item_rank中的元素数量小于需要召回的文章数量,极端情形,对用户访问的每一篇文章 i,如果该用户访问过所有与文章 i 交互的相似度前 sim_item_topk 的文章,则评分列表item_rank是空的。此时可用热门文章补全。
# 基于商品的召回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] # [(item1,time1), (item2,time2)..]
user_hist_items_={user_id for user_id,_ in user_hist_items} # 用户点击的文章集合{item1,item2,...}
item_rank = {}
for loc, (i, click_time) in enumerate(user_hist_items):
# 与文章i相似度前sim_item_topk的文章:文章j、相似度wij
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
Code
1. 读取采样数据
# 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
2. DataFrame 内存优化
因为训练数据集往往比较大,而内存会出现不够用的情况,可以通过修改特征的数据类型,从而达到优化压缩的目的。
# 节约内存的一个标配函数
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
代码注释
思路:如果数据没有那么大,就不要申请那么大的空间,应将其格式进行转换。
DataFrame.info()
DataFrame.info(verbose=None, buf=None, max_cols=None, memory_usage=None, null_counts=None)
在调用DataFrame的info()
方法时,会打印出该DataFrame的内存使用情况。
memory_usage:bool, str, optional
- 该参数用于指定是否显示DataFrame元素总的内存消耗。也可通过
pandas.options.display.memory_usage
来设置,True则总是显示内存消耗,False则从不显示内存消耗。 memory_usage='deep'
将显示详细的内存使用报告。
import numpy as np
import pandas as pd
n=5000
dtypes = ['int64', 'float64', 'datetime64[ns]', 'timedelta64[ns]','complex128', 'object', 'bool']
data = {t: np.random.randint(100, size=n).astype(t) for t in dtypes}
df = pd.DataFrame(data)
df['categorical'] = df['object'].astype('category')
print(df.info())
print(df.info(memory_usage='deep'))
pandas.DataFrame.info
Frequently Asked Questions (FAQ)——DataFrame memory usage
DataFrame.memory_usage()
DataFrame.memory_usage(index=True, deep=False)
返回每一列的内存使用情况。参数index
用于指定是否返回DataFrame的index列的内存使用情况。
# 每一列的内存使用情况
print(df.memory_usage())
# 总的内存使用情况
print('总的内存使用情况:{}'.format(df.memory_usage().sum()))
np.random.randint
numpy.random.randint(low, high=None, size=None, dtype='l')
返回一个随机整型数,范围 [ l o w , h i g h ) [low, high) [low,high)。
low: int
生成的数值最低要大于等于low。(hign = None时,生成的数值要在[0, low)区间内)high: int (可选)
如果使用这个值,则生成的数值在[low, high)区间。size: int or tuple of ints(可选)
输出随机数的尺寸。默认是None的,仅仅返回满足要求的单一随机数。dtype: dtype(可选)
:想要输出的格式。如int64、int等等。
np.iinfo(type)/np.finfo(type)
返回整数类型/浮点数类型的机器限制。
>>> ii32 = np.iinfo(np.int32)
>>> ii32.min
-2147483648
>>> ii32.max
2147483647
class numpy.iinfo(type)
class numpy.finfo(dtype)
更多方法
- 上述方法:https://www.kaggle.com/gemartin/load-data-reduce-memory-usage
- 封装成类:https://www.kaggle.com/wkirgsn/fail-safe-parallel-memory-reduction
- 转换为 feature 格式,降低内存占用:
# 生成一个feather文件
your_df.to_feather(path)
# 读取feather文件
pd.read_feather(path)
3. tqdm库
tqdm 是 Python 进度条库,可以在 Python 长循环中添加一个进度提示信息。
tqdm(iterable)
trange(i) 是 tqdm(range(i)) 的简单写法。
可以总结为三个方法:
方法一
# 方法1:
import time
from tqdm import tqdm
for i in tqdm(range(100)):
time.sleep(0.01)
方法1+:
import time
from tqdm import trange
for i in trange(100):
time.sleep(0.01)
方法二:为进度条设置描述
import time
from tqdm import tqdm
pbar = tqdm(["a", "b", "c", "d"])
for char in pbar:
# 设置描述
pbar.set_description("Processing %s" % char)
time.sleep(0.2)
方法三:手动更新
import time
from tqdm import tqdm
# 一共200个,每次更新10,一共更新20次
with tqdm(total=200) as pbar:
pbar.set_description("Processing")
for i in range(20):
pbar.update(10)
time.sleep(0.1)
#方法2:
pbar = tqdm(total=200)
for i in range(20):
pbar.update(10)
time.sleep(0.1)
pbar.close()