基于协同过滤算法进行文章推荐。
- 基于物品的协同过滤+热门补足进行topK推荐。
- 基于物品的协同过滤(时间衰减)+热门补足进行topK推荐。
- 基于物品的协同过滤(时间衰减)+embedding内容相关补足进行topK推荐。
import os
import gc
import time
import math
import pickle
import random
import collections
from datetime import datetime
from tqdm import tqdm_notebook
from collections import defaultdict, OrderedDict
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
def timmer(func):
""" 装饰器,监控运行时间 """
def wrapper(*args, **kwargs):
before_time = time.time()
f = func(*args, **kwargs)
print("--> RUN TIME: <%s> : %s" % (func.__name__, time.time() - before_time))
return f
return wrapper
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_df():
""" 合并训练集和测试集的点击日志 """
train_df = pd.read_csv('./inputs/train_click_log.csv')
test_df = pd.read_csv('./inputs/testA_click_log.csv')
all_click_df = train_df.append(test_df)
all_click_df = all_click_df.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
all_click_df = reduce_mem(all_click_df)
return all_click_df
all_click_df = get_all_click_df()
all_click_df = all_click_df.sort_values(['user_id', 'click_timestamp'], ascending=[True, True])
all_click_df['click_article_id'] = all_click_df['click_article_id'].astype(str)
all_click_df['click_timestamp'] = all_click_df['click_timestamp'].astype(str)
data = all_click_df.groupby(['user_id'])[['click_article_id', 'click_timestamp']].agg({'click_article_id': lambda x: ','.join(list(x)), 'click_timestamp': lambda x: ','.join(list(x))}).reset_index()
def get_item_dict(x):
return OrderedDict(zip(x.click_article_id.split(','), x.click_timestamp.split(',')))
data['item_dict'] = data.apply(get_item_dict, axis=1)
user_items_dict = dict(zip(data.user_id, data.item_dict))
基于物品的协同过滤是业界使用最多的算法。
物品之间的相似度公式为:
,
是喜欢i物品的用户数,
是同时喜欢物品i和物品j的用户数。
当j很热门的时候,趋近于1,故提出改进公式:
,惩罚热门物品j的权重。
基于物品的协同过滤,主要包括如下步骤:
1) 建立用户到消费物品列表。
2) 建立物品矩阵c[i][j],c[i][j]=k代表物品i和物品j共同消费用户k个。
2.1 遍历每个用户的消费物品列表。
2.2 每个用户对应的物品列表中,两两物品列表中c[i][j]+=1。
2.3 得到物品之间不为0的共现矩阵C[i][j],近而根据余弦相似度得到W[i][j]。
3) 得到物品相似度矩阵之后,根据用户消费物品给他推荐最相似物品。
@timmer
def calucate_i2i_sim(i2i_sim_path):
# 物品相关性矩阵
if os.path.exists(i2i_sim_path):
W = pickle.load(open(i2i_sim_path, 'rb'))
return W
C = defaultdict(int) # 物品矩阵,记录物品i和j相同用户数量
N = defaultdict(int) # 记录每个物品的用户数量
for user, item_time_dict in tqdm_notebook(user_items_dict.items()):
for item_i, time_i in item_time_dict.items():
N[item_i] += 1
C.setdefault(item_i, {})
for item_j, time_j in item_time_dict.items():
if item_i == item_j:
continue
C[item_i].setdefault(item_j, 0)
C[item_i][item_j] += 1.0
W = C.copy() # 物品相似度矩阵
for i, related_items in C.items():
for j, cij in related_items.items():
W[i][j] = cij / math.sqrt(N[i] * N[j])
pickle.dump(W, open(i2i_sim_path, 'wb'))
return W
W = calucate_i2i_sim(i2i_sim_path='./outputs/i2i_sim.pkl')
def item_based_recommender(user_id, user_items_dict, hot_items, W, K=5, N=10):
""" 根据用户消费物品最相似的k个物品进行推荐,推荐数量为N """
# 用户历史上有交互的所有文章
user_history_items = user_items_dict[user_id]
rec_items_dict = {}
for idx, (item_i, time_i) in enumerate(user_history_items.items()):
for item_j, wij in sorted(W[item_i].items(), key=lambda x: x[1], reverse=True)[:K]:
if item_j in user_history_items:
continue
rec_items_dict.setdefault(item_j, 0)
rec_items_dict[item_j] += wij
# 热门物品补足
if len(rec_items_dict) < N:
for i, hot_item in enumerate(hot_items):
if hot_item in rec_items_dict.items():
continue
rec_items_dict[hot_item] = -i-100
if len(rec_items_dict) == N:
break
rec_items_dict = sorted(rec_items_dict.items(), key=lambda x: x[1], reverse=True)[:N]
return rec_items_dict
# 相似文章数量
K = 10
# 召回数量
N = 10
# 前100热门物品列表
hot_items = list(all_click_df.click_article_id.value_counts().index[:].values)[:100]
# i2i召回结果集
i2i_recall_dict = defaultdict(dict)
# 对所有用户进行推荐
for user_id in tqdm_notebook(all_click_df.user_id.unique()):
i2i_recall_dict[user_id] = item_based_recommender(user_id, user_items_dict, hot_items, W, K, N)
res = []
for user_id, items in tqdm_notebook(i2i_recall_dict.items()):
for item, score in items:
res.append([user_id, item, score])
res_df = pd.DataFrame(res, columns=['user_id', 'item', 'score'])
# 每个用户取前5的文章
res_df.sort_values(by=['user_id', 'score'], axis=0, ascending=[True, False], inplace=True)
res_df['score_rank'] = res_df['score'].groupby([res_df['user_id']]).rank(ascending=False, method='first')
# 按提交格式定义dataframe
submit_df = res_df[res_df.score_rank<=5][['user_id', 'item', 'score_rank']].set_index(['user_id', 'score_rank']).unstack(-1).reset_index()
submit_df.columns = [int(col) if isinstance(col, int) else col for col in submit_df.columns.droplevel(0)]
submit_df = submit_df.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', 3: 'article_3', 4: 'article_4', 5: 'article_5'})
output_path = './outputs/{}.csv'.format('i2i_' + datetime.today().strftime('%m-%d'))
test_df = pd.read_csv('./inputs/testA_click_log.csv')
test_uids = set(test_df.user_id.values)
submit_df[submit_df.user_id.isin(test_uids)].to_csv(output_path, index=False, header=True)