賽題簡介
此次比賽是新聞推薦場景下的用戶行為預(yù)測挑戰(zhàn)賽, 該賽題是以新聞APP中的新聞推薦為背景漾唉, 目的是要求我們根據(jù)用戶歷史瀏覽點(diǎn)擊新聞文章的數(shù)據(jù)信息預(yù)測用戶未來的點(diǎn)擊行為冕房, 即用戶的最后一次點(diǎn)擊的新聞文章
數(shù)據(jù)概況
該數(shù)據(jù)來自某新聞APP平臺(tái)的用戶交互數(shù)據(jù)龄捡,包括30萬用戶耘成,近300萬次點(diǎn)擊赞厕,共36萬多篇不同的新聞文章艳狐,同時(shí)每篇新聞文章有對(duì)應(yīng)的embedding向量表示。為了保證比賽的公平性皿桑,從中抽取20萬用戶的點(diǎn)擊日志數(shù)據(jù)作為訓(xùn)練集毫目,5萬用戶的點(diǎn)擊日志數(shù)據(jù)作為測試集A,5萬用戶的點(diǎn)擊日志數(shù)據(jù)作為測試集B诲侮。
數(shù)據(jù)下載鏈接:
articles.csv
articles_emb.csv
testA_click_log.csv
train_click_log.csv
評(píng)價(jià)方式理解
理解評(píng)價(jià)方式镀虐, 我們需要結(jié)合著最后的提交文件sample.submit.csv來看, 我們最后提交的格式是針對(duì)每個(gè)用戶沟绪, 我們都會(huì)給出五篇文章的推薦結(jié)果刮便,按照點(diǎn)擊概率從前往后排序。 而真實(shí)的每個(gè)用戶最后一次點(diǎn)擊的文章只會(huì)有一篇的真實(shí)答案近零, 所以我們就看我們推薦的這五篇里面是否有命中真實(shí)答案的。
比如對(duì)于user1來說抄肖, 我們的提交會(huì)是:
user1, article1, article2, article3, article4, article5.
評(píng)價(jià)指標(biāo)的公式如下:
假如article1就是真實(shí)的用戶點(diǎn)擊文章久信,也就是article1命中, 則s(user1,1)=1, s(user1,2-4)都是0漓摩, 如果article2是用戶點(diǎn)擊的文章裙士, 則s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第幾條的倒數(shù)管毙。如果都沒中腿椎, 則score(user1)=0。 這個(gè)是合理的夭咬, 因?yàn)?strong>我們希望的就是命中的結(jié)果盡量靠前啃炸, 而此時(shí)分?jǐn)?shù)正好比較高。
賽題理解
首先要明確我們此次比賽的目標(biāo): 根據(jù)用戶歷史瀏覽點(diǎn)擊新聞的數(shù)據(jù)信息預(yù)測用戶最后一次點(diǎn)擊的新聞文章, 所以拿到這個(gè)題目卓舵,我們的思考方向就是結(jié)合我們的目標(biāo)南用,把該預(yù)測問題轉(zhuǎn)成一個(gè)監(jiān)督學(xué)習(xí)的問題(特征+標(biāo)簽),然后我們才能進(jìn)行ML,DL等建模預(yù)測裹虫。
那么肿嘲,如何轉(zhuǎn)化成一個(gè)監(jiān)督學(xué)習(xí)的問題呢?如此龐大的分類問題筑公, 我們做起來可能比較困難雳窟, 那么能不能轉(zhuǎn)化一下? 既然是要預(yù)測最后一次點(diǎn)擊的文章匣屡, 那么如果我們能預(yù)測出某個(gè)用戶最后一次對(duì)于某一篇文章會(huì)進(jìn)行點(diǎn)擊的概率封救, 是不是就間接性的解決了這個(gè)問題呢?概率最大的那篇文章不就是用戶最后一次可能點(diǎn)擊的新聞文章嗎耸采? 這樣就把原問題變成了一個(gè)點(diǎn)擊率預(yù)測的問題(用戶, 文章) --> 點(diǎn)擊的概率(軟分類)兴泥。
這樣, 我們對(duì)于該賽題的解決方案應(yīng)該有了一個(gè)大致的解決思路虾宇,要先轉(zhuǎn)成一個(gè)分類問題來做搓彻, 而分類的標(biāo)簽就是用戶是否會(huì)點(diǎn)擊某篇文章,分類問題的特征中會(huì)有用戶和文章嘱朽,我們要訓(xùn)練一個(gè)分類模型旭贬, 對(duì)某用戶最后一次點(diǎn)擊某篇文章的概率進(jìn)行預(yù)測。
L掠尽稀轨!由于沒參加上一次組隊(duì)學(xué)習(xí),在這里總結(jié)一下協(xié)同過濾的知識(shí)0毒奋刽!
召回層與排序?qū)拥奶攸c(diǎn):召回階段負(fù)責(zé)將海量的候選集快速縮小為幾萬到幾千的規(guī)模;而排序?qū)觿t負(fù)責(zé)對(duì)縮小后的候選集進(jìn)行精準(zhǔn)排序
- 召回層: 待計(jì)算的候選集合大艰赞、計(jì)算速度快佣谐、模型簡單、特征較少方妖,盡量讓用戶感興趣的物品在這個(gè)階段能夠被快速召回狭魂,即保證相關(guān)物品的召回率。
- 排序?qū)樱?首要目標(biāo)是得到精準(zhǔn)的排序結(jié)果党觅。需要處理的物品數(shù)量少雌澄,可以利用較多的特征,使用比較復(fù)雜的模型杯瞻。
Embedding是什么镐牺?
Embedding其實(shí)是一種思想,主要目的是將稀疏的向量(如one-hot編碼)表示轉(zhuǎn)換成稠密的向量魁莉,下圖直觀的顯示了one-hot編碼和Embedding表示的區(qū)別于聯(lián)系任柜,即Embedding相當(dāng)于是對(duì)one-hot做了平滑卒废,而onehot相當(dāng)于是對(duì)Embedding做了max pooling。
對(duì)于非文本的id類特征宙地,可以先將其轉(zhuǎn)化成id序列再使用text embedding的技術(shù)獲取id的embedding再做召回
1. 協(xié)同過濾算法
協(xié)同過濾(Collaborative Filtering)推薦算法是最經(jīng)典摔认、最常用的推薦算法。
所謂協(xié)同過濾宅粥, 基本思想是根據(jù)用戶之前的喜好以及其他興趣相近的用戶的選擇來給用戶推薦物品(基于對(duì)用戶歷史行為數(shù)據(jù)的挖掘發(fā)現(xiàn)用戶的喜好偏向参袱, 并預(yù)測用戶可能喜好的產(chǎn)品進(jìn)行推薦),一般是僅僅基于用戶的行為數(shù)據(jù)(評(píng)價(jià)秽梅、購買抹蚀、下載等), 而不依賴于項(xiàng)的任何附加信息(物品自身特征)或者用戶的任何附加信息(年齡, 性別等)企垦。目前應(yīng)用比較廣泛的協(xié)同過濾算法是基于鄰域的方法环壤, 而這種方法主要有下面兩種算法:
- 基于用戶的協(xié)同過濾算法(UserCF): 給用戶推薦和他興趣相似的其他用戶喜歡的產(chǎn)品
- 基于物品的協(xié)同過濾算法(ItemCF): 給用戶推薦和他之前喜歡的物品相似的物品。
不管是UserCF還是ItemCF算法钞诡, 非常重要的步驟之一就是計(jì)算用戶和用戶或者物品和物品之間的相似度郑现, 所以下面先整理常用的相似性度量方法。
2. 相似性度量方法
(1)杰卡德(Jaccard)相似系數(shù)
這是衡量兩個(gè)集合相似度的一種指標(biāo)荧降,兩個(gè)用戶 u 和 v 交互商品交集的數(shù)量占這兩個(gè)用戶交互商品并集的數(shù)量的比例接箫,稱為兩個(gè)集合的杰卡德相似系數(shù),用符號(hào) 表示朵诫,其中 N(u), N(v) 分別表示用戶 u 和用戶 v 交互商品的集合辛友。
由于杰卡德相似系數(shù)一般無法反映具體用戶的評(píng)分喜好信息, 所以常用來評(píng)估用戶是否會(huì)對(duì)某商品進(jìn)行打分剪返, 而不是預(yù)估用戶會(huì)對(duì)某商品打多少分废累。
(2)余弦相似度
余弦相似度衡量了兩個(gè)向量的夾角,夾角越小越相似脱盲。首先從集合的角度描述余弦相似度邑滨,相比于Jaccard公式來說就是分母有差異,不是兩個(gè)用戶交互商品的并集的數(shù)量宾毒,而是兩個(gè)用戶分別交互的商品數(shù)量的乘積驼修,公式如下:
從向量的角度進(jìn)行描述殿遂,令矩陣 A 為用戶-商品交互矩陣诈铛,即矩陣的每一行表示一個(gè)用戶對(duì)所有商品的交互情況,有交互的商品值為1沒有交互的商品值為0墨礁,矩陣的列表示所有商品幢竹。若用戶和商品數(shù)量分別為 m,n 的話,交互矩陣 A 就是一個(gè) m 行 n 列的矩陣恩静。此時(shí)用戶的相似度可以表示為(其中 u?v 指的是向量點(diǎn)積):
這個(gè)在具體實(shí)現(xiàn)的時(shí)候焕毫, 可以使用cosine_similarity進(jìn)行實(shí)現(xiàn):
from sklearn.metrics.pairwise import cosine_similarity
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
consine_similarity([i, j])
(2)皮爾遜相關(guān)系數(shù)
皮爾遜相關(guān)系數(shù)的公式與余弦相似度的計(jì)算公式非常的類似蹲坷,首先對(duì)于上述的余弦相似度的計(jì)算公式寫成求和的形式,其中,
分別表示用戶
和用戶
對(duì)商品
是否有交互(或者具體的評(píng)分值):
如下是皮爾遜相關(guān)系數(shù)計(jì)算公式,其中 ,
分別表示用戶
和用戶
對(duì)商品
是否有交互(或者具體的評(píng)分值)邑飒,
,
分別表示用戶
和用戶
交互的所有商品交互數(shù)量或者具體評(píng)分的平均值循签。(在簡書中,平均符號(hào)用上劃線表:\overline{r} _{ui} )
python實(shí)現(xiàn):
from scipy.stats import pearsonr
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
pearsonr(i, j)
下面是基于用戶協(xié)同過濾和基于物品協(xié)同過濾的原理講解疙咸。
3. 基于用戶的協(xié)同過濾(userCF)
當(dāng)一個(gè)用戶A需要個(gè)性化推薦的時(shí)候县匠, 我們可以先找到和他有相似興趣的其他用戶, 然后把那些用戶喜歡的撒轮, 而用戶A沒有聽說過的物品推薦給A乞旦。
UserCF算法主要包括兩個(gè)步驟:
1. 找到和目標(biāo)用戶興趣相似的集合
2. 找到這個(gè)集合中的用戶喜歡的, 且目標(biāo)用戶沒有聽說過的物品推薦給目標(biāo)用戶题山。
上面的兩個(gè)步驟中兰粉, 第一個(gè)步驟里面, 我們會(huì)基于前面給出的相似性度量的方法找出與目標(biāo)用戶興趣相似的用戶顶瞳, 而第二個(gè)步驟里面玖姑, 如何基于相似用戶喜歡的物品來對(duì)目標(biāo)用戶進(jìn)行推薦呢? 這個(gè)要依賴于目標(biāo)用戶對(duì)相似用戶喜歡的物品的一個(gè)喜好程度浊仆, 那么如何衡量這個(gè)程度大小呢客峭? 為了更好理解上面的兩個(gè)步驟, 下面拿一個(gè)具體的例子把兩個(gè)步驟具體化抡柿。
以下圖為例舔琅,此例將會(huì)用于本文各種算法中:
給用戶推薦物品的過程可以形象化為一個(gè)猜測用戶對(duì)商品進(jìn)行打分的任務(wù),上面表格里面是5個(gè)用戶對(duì)于5件物品的一個(gè)打分情況洲劣,就可以理解為用戶對(duì)物品的喜歡程度
應(yīng)用UserCF算法的兩個(gè)步驟:
- 首先根據(jù)前面的這些打分情況(或者說已有的用戶向量)計(jì)算一下Alice和用戶1备蚓, 2, 3囱稽, 4的相似程度郊尝, 找出與Alice最相似的n個(gè)用戶
- 根據(jù)這n個(gè)用戶對(duì)物品5的評(píng)分情況和與Alice的相似程度會(huì)猜測出Alice對(duì)物品5的評(píng)分, 如果評(píng)分比較高的話战惊, 就把物品5推薦給用戶Alice流昏, 否則不推薦。
關(guān)于第一個(gè)步驟吞获, 上面已經(jīng)給出了計(jì)算兩個(gè)用戶相似性的方法况凉, 這里不再過多贅述, 這里主要解決第二個(gè)問題各拷, 如何產(chǎn)生最終結(jié)果的預(yù)測刁绒。
最終結(jié)果的預(yù)測
根據(jù)上面的幾種方法, 我們可以計(jì)算出向量之間的相似程度烤黍, 也就是可以計(jì)算出Alice和其他用戶的相近程度知市, 這時(shí)候我們就可以選出與Alice最相近的前n個(gè)用戶傻盟, 基于他們對(duì)物品5的評(píng)價(jià)猜測出Alice的打分值, 那么是怎么計(jì)算的呢嫂丙?
這里常用的方式之一是利用用戶相似度和相似用戶的評(píng)價(jià)加權(quán)平均獲得用戶的評(píng)價(jià)預(yù)測娘赴, 用下面式子表示:
這個(gè)式子里面, 權(quán)重 是用戶
和用戶
的相似度跟啤,
是用戶
對(duì)物品
的評(píng)分筝闹。
還有一種方式如下, 這種方式考慮的更加全面腥光, 依然是用戶相似度作為權(quán)值关顷, 但后面不單純的是其他用戶對(duì)物品的評(píng)分, 而是相似用戶對(duì)該物品的評(píng)分與此用戶的所有評(píng)分平均值的差值進(jìn)行加權(quán)平均武福, 這時(shí)候考慮到了有的用戶內(nèi)心的評(píng)分標(biāo)準(zhǔn)不一的情況议双, 即有的用戶喜歡打高分, 有的用戶喜歡打低分的情況捉片。
表示的是用戶
對(duì)物品
的評(píng)分平痰,
表示的是用戶
的所有評(píng)分的平均值,
表示的是與用戶
相似的
個(gè)用戶伍纫,
表示的是用戶
和用戶
的相似度,
表示的是用戶 k 對(duì)物品 j 的評(píng)分宗雇,
表示的是用戶
的所有評(píng)分的平均值。所以這一種計(jì)算方式更為推薦莹规。下面的計(jì)算將使用這個(gè)方式赔蒲。
在獲得用戶 對(duì)不同物品的評(píng)價(jià)預(yù)測后, 最終的推薦列表根據(jù)預(yù)測評(píng)分進(jìn)行排序得到良漱。 至此舞虱,基于用戶的協(xié)同過濾算法的推薦過程完成。
4. UserCF優(yōu)缺點(diǎn)
User-based算法存在兩個(gè)重大問題:
數(shù)據(jù)稀疏性母市。
一個(gè)大型的電子商務(wù)推薦系統(tǒng)一般有非常多的物品矾兜,用戶可能買的其中不到1%的物品,不同用戶之間買的物品重疊性較低患久,導(dǎo)致算法無法找到一個(gè)用戶的鄰居椅寺,即偏好相似的用戶。這導(dǎo)致UserCF不適用于那些正反饋獲取較困難的應(yīng)用場景(如酒店預(yù)訂蒋失, 大件商品購買等低頻應(yīng)用)算法擴(kuò)展性返帕。
基于用戶的協(xié)同過濾需要維護(hù)用戶相似度矩陣以便快速的找出Topn相似用戶, 該矩陣的存儲(chǔ)開銷非常大高镐,存儲(chǔ)空間隨著用戶數(shù)量的增加而增加溉旋,不適合用戶數(shù)據(jù)量大的情況使用畸冲。
由于UserCF技術(shù)上的兩點(diǎn)缺陷嫉髓, 導(dǎo)致很多電商平臺(tái)并沒有采用這種算法观腊, 而是采用了ItemCF算法實(shí)現(xiàn)最初的推薦系統(tǒng)。
5. 基于物品的協(xié)同過濾
預(yù)先根據(jù)所有用戶的歷史偏好數(shù)據(jù)計(jì)算物品之間的相似性算行,然后把與用戶喜歡的物品相類似的物品推薦給用戶梧油。比如物品a和c非常相似,因?yàn)橄矚ga的用戶同時(shí)也喜歡c州邢,而用戶A喜歡a儡陨,所以把c推薦給用戶A。ItemCF算法并不利用物品的內(nèi)容屬性計(jì)算物品之間的相似度量淌, 主要通過分析用戶的行為記錄計(jì)算物品之間的相似度骗村, 該算法認(rèn)為, 物品a和物品c具有很大的相似度是因?yàn)橄矚g物品a的用戶大都喜歡物品c呀枢。
基于物品的協(xié)同過濾算法主要分為兩步:
- 計(jì)算物品之間的相似度
- 根據(jù)物品的相似度和用戶的歷史行為給用戶生成推薦列表(購買了該商品的用戶也經(jīng)常購買的其他商品)
基于物品的協(xié)同過濾算法和基于用戶的協(xié)同過濾算法很像胚股, 所以我們這里直接還是拿上面Alice的那個(gè)例子來看。
從計(jì)算的角度看裙秋,就是將所有用戶對(duì)某個(gè)物品的偏好作為一個(gè)向量來計(jì)算物品之間的相似度琅拌,得到物品的相似物品后,根據(jù)用戶歷史的偏好預(yù)測當(dāng)前用戶還沒有表示偏好的物品摘刑,計(jì)算得到一個(gè)排序的物品列表作為推薦进宝。
如果想知道Alice對(duì)物品5打多少分, 基于物品的協(xié)同過濾算法會(huì)這么做:
- 首先計(jì)算一下物品5和物品1枷恕, 2党晋, 3, 4之間的相似性(它們也是向量的形式徐块, 每一列的值就是它們的向量表示隶校, 因?yàn)镮temCF認(rèn)為物品a和物品c具有很大的相似度是因?yàn)橄矚g物品a的用戶大都喜歡物品c, 所以就可以基于每個(gè)用戶對(duì)該物品的打分或者說喜歡程度來向量化物品)
- 找出與物品5最相近的n個(gè)物品
- 根據(jù)Alice對(duì)最相近的n個(gè)物品的打分去計(jì)算對(duì)物品5的打分情況
Baseline
Baseline使用的是簡單的協(xié)同過濾蛹锰,這里直接寫的代碼深胳,詳細(xì)內(nèi)容可以參考上一期組隊(duì)學(xué)習(xí)推薦系統(tǒng)基礎(chǔ)部分的協(xié)同過濾,對(duì)應(yīng)的Github鏈接已經(jīng)放在下面了铜犬。
Github推薦系統(tǒng)基礎(chǔ)
DataWhale推薦系統(tǒng)基礎(chǔ)
先看一下train_click_log.csv的列名
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 = './Data/'
save_path = './tmp_results/'
# 節(jié)約內(nèi)存的一個(gè)標(biāo)配函數(shù)
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
# debug模式:從訓(xùn)練集中劃出一部分?jǐn)?shù)據(jù)來調(diào)試代碼
def get_all_click_sample(data_path, sample_nums=10000):
"""
訓(xùn)練集中采樣一部分?jǐn)?shù)據(jù)調(diào)試
data_path: 原數(shù)據(jù)的存儲(chǔ)路徑
sample_nums: 采樣數(shù)目(這里由于機(jī)器的內(nèi)存限制舞终,可以采樣用戶做)
"""
all_click = pd.read_csv(data_path + 'train_click_log.csv')
all_user_ids = all_click.user_id.unique() # uniuqe的id
sample_user_ids = np.random.choice(all_user_ids, size=sample_nums, replace=False)# 從unique id中隨機(jī)選10000個(gè)id
all_click = all_click[all_click['user_id'].isin(sample_user_ids)]# 從原來的all_click中取出包括在sample_user_id中的項(xiàng)
all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp'])) # 去除重復(fù)項(xiàng),只考慮這三列
return all_click
# 讀取點(diǎn)擊數(shù)據(jù)癣猾,這里分成線上和線下敛劝,如果是為了獲取線上提交結(jié)果應(yīng)該講測試集中的點(diǎn)擊數(shù)據(jù)合并到總的數(shù)據(jù)中
# 如果是為了線下驗(yàn)證模型的有效性或者特征的有效性,可以只使用訓(xùn)練集
def get_all_click_df(data_path='./Data/', 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
# 全量訓(xùn)練集
all_click_df = get_all_click_df(offline=False)
# 根據(jù)點(diǎn)擊時(shí)間獲取用戶的點(diǎn)擊文章序列 {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
# 獲取近期點(diǎn)擊最多的k篇文章
def get_item_topk_click(click_df, k):
topk_click = click_df['click_article_id'].value_counts().index[:k]
return topk_click
# itemCF的物品相似度計(jì)算
def itemcf_sim(df):
"""
文章與文章之間的相似性矩陣計(jì)算
:param df: 數(shù)據(jù)表
:item_created_time_dict: 文章創(chuàng)建時(shí)間的字典
return : 文章與文章的相似性矩陣
思路: 基于物品的協(xié)同過濾(詳細(xì)請(qǐng)參考上一期推薦系統(tǒng)基礎(chǔ)的組隊(duì)學(xué)習(xí))纷宇, 在多路召回部分會(huì)加上關(guān)聯(lián)規(guī)則的召回策略
"""
user_item_time_dict = get_user_item_time(df)
# 計(jì)算物品相似度
i2i_sim = {}
item_cnt = defaultdict(int)
for user, item_time_list in tqdm(user_item_time_dict.items()):
# 在基于商品的協(xié)同過濾優(yōu)化的時(shí)候可以考慮時(shí)間因素
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)
100%|████████████████████████████████████████████████████████████████████████| 250000/250000 [00:33<00:00, 7461.80it/s]
# itemCF 的文章推薦
# 基于商品的召回i2i
def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
"""
基于文章協(xié)同過濾的召回
:param user_id: 用戶id
:param user_item_time_dict: 字典, 根據(jù)點(diǎn)擊時(shí)間獲取用戶的點(diǎn)擊文章序列{user1: [(item1, time1), (item2, time2)..]...}
:param i2i_sim: 字典夸盟,文章相似性矩陣
:param sim_item_topk: 整數(shù), 選擇與當(dāng)前文章最相似的前k篇文章
:param recall_item_num: 整數(shù)像捶, 最后的召回文章數(shù)量
:param item_topk_click: 列表上陕,點(diǎn)擊次數(shù)最多的文章列表桩砰,用戶召回補(bǔ)全
return: 召回的文章列表 [item1:score1, item2: score2...]
注意: 基于物品的協(xié)同過濾(詳細(xì)請(qǐng)參考上一期推薦系統(tǒng)基礎(chǔ)的組隊(duì)學(xué)習(xí)), 在多路召回部分會(huì)加上關(guān)聯(lián)規(guī)則的召回策略
"""
# 獲取用戶歷史交互的文章
user_hist_items = user_item_time_dict[user_id] # 注意释簿,此時(shí)獲取得到的是一個(gè)元組列表亚隅,需要將里面的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個(gè),用熱門商品補(bǔ)全
if len(item_rank) < recall_item_num:
for i, item in enumerate(item_topk_click):
if item in item_rank.items(): # 填充的item應(yīng)該不在原來的列表中
continue
item_rank[item] = - i - 100 # 隨便給個(gè)負(fù)數(shù)就行
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
# 給每個(gè)用戶根據(jù)物品的協(xié)同過濾推薦文章
# 定義
user_recall_items_dict = defaultdict(dict)
# 獲取 用戶 - 文章 - 點(diǎn)擊時(shí)間的字典
user_item_time_dict = get_user_item_time(all_click_df)
# 去取文章相似度
i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))
# 相似文章的數(shù)量
sim_item_topk = 10
# 召回文章數(shù)量
recall_item_num = 10
# 用戶熱度補(bǔ)全
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)
100%|█████████████████████████████████████████████████████████████████████████| 250000/250000 [38:32<00:00, 108.12it/s]
# 將字典的形式轉(zhuǎn)換成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'])
100%|███████████████████████████████████████████████████████████████████████| 250000/250000 [00:05<00:00, 42040.61it/s]
# 生成提交文件
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')
# 判斷是不是每個(gè)用戶都有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()
# 從所有的召回?cái)?shù)據(jù)中將測試集中的用戶選出來
tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]
# 生成提交文件
submit(tst_recall, topk=5, model_name='itemcf_baseline')
最后 結(jié)果長這樣: