零基礎天池新聞推薦初學-01-賽題理解&Baseline
阿新 • • 發佈:2020-11-27
賽題地址
天池 新人賽 新聞推薦
01 賽題簡介
型別: 是新聞推薦場景下的使用者行為預測挑戰賽,新聞APP,目的:要求根據使用者歷史瀏覽點選新聞文章的資料資訊來預測使用者未來的點選行為;
02 評分方式理解
提交的格式是針對每個使用者, 我們都會給出五篇文章的推薦結果,按照點選概率從前往後排序。 而真實的每個使用者最後一次點選的文章只會有一篇的真實答案, 所以我們就看我們推薦的這五篇裡面是否有命中真實答案的。比如對於user1來說, 我們的提交會是:
user1, article1, article2, article3, article4, article5.
評價指標的公式如下:(k代表從前向後的文章排序值,當命中時:s(user, k)為1,否則為0;第一篇文章命中可以拿1分,第二篇命中可以拿1/2分,依次類推)
\[score(user) = \sum_{k=1}^5 \frac{s(user, k)}{k} \]03 賽題理解
與之前遇到的普通的結構化比賽的區別:
- 首先是目標上, 要預測最後一次點選的新聞文章,也就是我們給使用者推薦的是新聞文章, 並不是像之前那種預測一個數或者預測資料哪一類那樣的問題
- 資料上, 通過給出的資料我們會發現, 這種資料也不是我們之前遇到的那種特徵+標籤的資料,而是基於了真實的業務場景, 拿到的使用者的點選日誌
把該預測問題轉成一個監督學習的問題(特徵+標籤),然後我們才能進行ML,DL等建模預測。
那麼我們自然而然的就應該在心裡會有這麼幾個問題:
- 如何轉成一個監督學習問題呢?
- 轉換為使用者對某一篇文章的點選概率,然後進行排序輸出;這樣就把原問題變成了一個點選率預測的問題(使用者, 文章) --> 點選的概率(軟分類)
- 轉成一個什麼樣的監督學習問題呢?
- 36萬的文章數量,可能會有36萬類的文章,可以轉換成一個多分類的問題
- 我們能利用的特徵又有哪些呢?
- 又有哪些模型可以嘗試呢?
- gbdt+lr
- 此次面對數萬級別的文章推薦,我們又有哪些策略呢?
- 多執行緒加速
04 Baseline
基於全量資料的itermCFTop推薦策略,不足Top5的按熱度文章推薦
#%% md
新聞APP的使用者行為預測挑戰賽
目的是:要求我們根據使用者歷史瀏覽點選新聞文章的資料資訊預測使用者未來的點選行為, 即使用者的最後一次點選的新聞文章
#%% 00 導包
import collections
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')
data_path = 'E:\\阿里雲開發者-天池比賽\\06_天池新聞APP推薦\\'
save_path = 'E:\\PycharmProjects\\TianChiProject\\00_山楓葉紛飛\\competitions\\006_dw_RecommandNews\\predict_results\\'
#%%
# 節約記憶體的一個標配函式
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
#%% 01 讀取取樣或全量資料
# 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, 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(data_path, offline=False)
#%% 02 獲取 使用者 - 文章 - 點選時間字典
# 根據點選時間獲取使用者的點選文章序列
# {user1: [(item1: time1), (item2: time2)...]...}
def make_item_time_pair(df):
return list(zip(df['click_article_id'], df['click_timestamp']))
def get_user_item_time(click_df_params):
click_df = click_df_params.sort_values('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
#%% 03 獲取點選最多的Topk個文章
# 獲取近期點選最多的文章
def get_item_topk_click(click_df, k):
topk_click = click_df['click_article_id'].value_counts().index[:k]
return topk_click
#%% 04 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_
#%% 04-2
# i2i_sim = itemcf_sim(all_click_df)
i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl','rb'))
#%% 05 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_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
#%% 06 給每個使用者根據物品的協同過濾推薦文章
# 定義
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)
#%% 07 召回字典轉換成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'])
#%% 08 生成提交檔案
# 生成提交檔案
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_%H_%M') + '.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')
輸出結果
100%|██████████| 250000/250000 [34:53<00:00, 119.40it/s]
100%|██████████| 250000/250000 [00:04<00:00, 61288.40it/s]
原文學習連結
- Datawhale team-learning-rs RecommandNews (github)