【推薦系統(tǒng)】手寫ItemCF/UserCF代碼,你會嗎?
前言
之前朋友說有同學(xué)在面字節(jié)算法實(shí)習(xí)時讓復(fù)現(xiàn)DeepFM算法(包括訓(xùn)練),然后就懵了。因此最近在整理傳統(tǒng)推薦算法的一些內(nèi)容時,大概是這樣的:

就想到「基于鄰域的協(xié)同過濾(UserCF與ItemCF),除了了解原理、應(yīng)用場景的區(qū)別外,如果現(xiàn)場寫實(shí)現(xiàn)偽代碼你會么?」 有很多文章在講協(xié)同過濾的原理,很少具體到代碼,所以這次把以前寫的CF代碼優(yōu)化下進(jìn)行分享。
本文約1.7k字,預(yù)計(jì)閱讀5分鐘.
概要
協(xié)同過濾是「基于用戶行為」設(shè)計(jì)的推薦算法,具體來說,是「通過群體的行為來找到某種相似性」(用戶之間的相似性或者物品之間的相似性),通過相似性來為用戶做決策和推薦。當(dāng)然,協(xié)同過濾有基于鄰域的、隱語義模型等,這里我們主要指的是基于鄰域的ItemCF和UserCF。具體的原理就不多做解釋了,見下圖:

ItemCF與UserCF的代碼其實(shí)很相近,因此以ItemCF為例,兩個算法的具體代碼可以在github:https://github.com/ZiyaoGeng/SimpleCF(閱讀原文)中找到。
ItemCF
對于ItemCF算法的實(shí)踐,我采用類的方式進(jìn)行建立,主要內(nèi)容包括:
模型的輸入; 初始化【計(jì)算相似度矩陣】; 對單個用戶進(jìn)行推薦; 對測試集的所有內(nèi)容進(jìn)行推薦;
因此大體框架為:
class ItemCF:
def __init__(self, user_item_dict, item_hot_list, sim_item_topK, topN, i2i_sim=None):
"""
Item-based collaborative filtering
:param user_item_dict: A dict. {user1: [(item1, score),...], user2: ...}
:param item_hot_list: A list. The popular movies list.
:param sim_item_topK: A scalar. Choose topK items for calculate.
:param topN: A scalar. The number of recommender list.
:param i2i_sim: dict. If None, the model should calculate similarity matrix.
"""
def __get_item_sim(self):
"""
calculate item similarity weight matrix
:return: i2i_sim
"""
def recommend(self, user_id):
def recommend_all(self, test):
輸入
user_item_dict:首先最重要的是建立user-item共現(xiàn)矩陣作為「輸入」,當(dāng)然在實(shí)際應(yīng)用中,我們采用字典的形式減少不必要的空間。對于ItemCF,需要user-item-dict字典,而對于UserCF,需要user-item-dict和item-user-dict兩個字典:item-user-dict: user-item-dict: 【注】元組部分score可以去除或者替換為其他內(nèi)容;
item_hot_list:熱門物品的列表,主要是當(dāng)推薦物品數(shù)量不夠時(冷啟動等),用熱門物品進(jìn)行填充,計(jì)算也比較簡單;sim_item_topK:選取某個物品最相似的TopK個物品,不然選擇所有物品會產(chǎn)生很大的計(jì)算量;topN:推薦列表的大?。?/p>i2i_sim:物品相似度矩陣。一般計(jì)算相似度矩陣后會在本地進(jìn)行保存,因此如果之前計(jì)算過,則只需讀取,不用重復(fù)計(jì)算;
物品相似度矩陣
ItemCF算法認(rèn)為「物品A和物品B具有很大的相似度是因?yàn)橄矚g物品A的用戶也大多喜歡物品B」,因此需要計(jì)算物品相似度矩陣,主要分為兩步:
統(tǒng)計(jì)兩兩物品之間的共現(xiàn)次數(shù),即「用戶同時喜歡兩個物品」; 通過Jaccard距離、余弦相似度等方式計(jì)算兩個物品的相似性;
當(dāng)然對于1來說,需要對于活躍的用戶進(jìn)行懲罰,通過增加IUF(Inverse User Frequence),用戶活躍度對數(shù)倒數(shù)的參數(shù),對應(yīng)代碼中:
i2i_sim[i][j]?+=?1?/?math.log(len(items)?+?1)
在2中,通過余弦相似度的計(jì)算可以降低熱門物品會和很多物品相似的可能性,因?yàn)榛谖锲返耐扑]主要是挖掘長尾信息。
i2i_sim[i][j]?=?wij?/?math.sqrt(item_cnt[i]?*?item_cnt[j])?
具體代碼如下:
????def?__get_item_sim(self):
????????"""
????????calculate?item?similarity?weight?matrix
????????:return:?i2i_sim
????????"""
????????i2i_sim?=?dict()
????????item_cnt?=?defaultdict(int)??#?Count?the?number?of?visits?to?the?item
????????for?user,?items?in?tqdm(self.user_item_dict.items()):
????????????for?i,?score_i?in?items:
????????????????item_cnt[i]?+=?1
????????????????i2i_sim.setdefault(i,?{})
????????????????for?j,?score_j?in?items:
????????????????????if?i?==?j:
????????????????????????continue
????????????????????i2i_sim[i].setdefault(j,?0)
????????????????????i2i_sim[i][j]?+=?1?/?math.log(len(items)?+?1)??
????????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])??
????????return?i2i_sim
推薦
使用之前計(jì)算的相似度矩陣對每個用戶進(jìn)行推薦。主要分為兩步:
獲取推薦用戶的歷史行為,在相似度矩陣中選取每個歷史物品(遍歷)最相似的 topk個物品來計(jì)算每個物品(未出現(xiàn)在歷史行為中)的「累積權(quán)重」;若1中所有物品數(shù)量小于推薦列表,則采用其他策略進(jìn)行填充,如選擇熱門物品,但權(quán)重分?jǐn)?shù)是最小的,可以為負(fù)數(shù)。然后對權(quán)重列表進(jìn)行排序,選取權(quán)重分?jǐn)?shù)最高的 TopN個物品;
具體代碼如下:
????def?recommend(self,?user_id):
????????"""
????????recommend?one?user
????????:param?user_id:?user's?ID
????????:return:
????????"""
????????item_rank?=?dict()
????????user_hist_items?=?self.user_item_dict[user_id]
????????for?i,?score_i?in?user_hist_items:
????????????for?j,?wij?in?sorted(self.i2i_sim[i].items(),?key=lambda?x:?x[1],?reverse=True)[:self.sim_item_topK]:
????????????????if?j?in?user_hist_items:
????????????????????continue
????????????????item_rank.setdefault(j,?0)
????????????????item_rank[j]?+=?1?*?wij
????????if?len(item_rank)?????????????for?i,?item?in?enumerate(self.item_hot_list):
????????????????if?item?in?item_rank:
????????????????????continue
????????????????item_rank[item]?=?-?i?-?1??#?rank?score?0
????????????????if?len(item_rank)?==?self.topN:
????????????????????break
????????item_rank?=?sorted(item_rank.items(),?key=lambda?x:?x[1],?reverse=True)[:self.topN]
????????return?[i?for?i,?score?in?item_rank]
【注】在計(jì)算每個物品的權(quán)重時,選擇了1 * wij,并沒有用到分?jǐn)?shù)和其他信息。
評估
「數(shù)據(jù)集」:ml-1m,當(dāng)然也可以選擇其它。預(yù)處理主要包括:
選擇評分大于x的作為正樣本(當(dāng)然可以直接將所有交互過的物品都作為正樣本,將評分融入推薦時權(quán)重分?jǐn)?shù)的計(jì)算); 選取每個用戶的最后一次發(fā)生交互的物品作為測試集(所以評估的結(jié)果會比項(xiàng)亮《推薦系統(tǒng)實(shí)戰(zhàn)》的結(jié)果差很多); 建立 user-item和item-user字典;
「評估指標(biāo)」:
HR(點(diǎn)擊率)與NDCG,當(dāng)每個用戶真實(shí)的交互物品只有一個時,HR等價于Recall;
評估代碼如下:
def?evaluate_model(model,?test):
????"""
????evaluate?model
????:param?model:?model?of?CF
????:param?test:?dict.
????:return:?hit?rate,?ndcg
????"""
????hit,?ndcg?=?0,?0
????for?user_id,?item_id?in?tqdm(test.items()):
????????item_rank?=?model.recommend(user_id)
????????if?item_id?in?item_rank:
????????????hit?+=?1
????????????ndcg?+=?1?/?np.log2(item_rank.index(item_id)?+?2)
????return?hit?/?len(test),?ndcg?/?len(test)
實(shí)驗(yàn)
對于ItemCF算法,超參數(shù):
sim_item_topK = 20; topN = 20;
使用自己的筆記本,結(jié)果如下:
數(shù)據(jù)讀?。?s;
模型建立(計(jì)算相似度矩陣):3min52s;
模型評估:19min47s,HR = 0.065232, NDCG = 0.024265;
總結(jié)
之前參考《推薦算法實(shí)踐》進(jìn)行過復(fù)現(xiàn),然后這次結(jié)合了Datawhale中新聞推薦的Baseline進(jìn)行優(yōu)化,確實(shí)對算法中如何懲罰熱門物品與活躍用戶有了更深的認(rèn)識。所有代碼在github “https://github.com/ZiyaoGeng/SimpleCF”中,如果存在一些Bug或者更好的優(yōu)化(評估時間太長),歡迎提出Issues。
往期精彩回顧
本站知識星球“黃博的機(jī)器學(xué)習(xí)圈子”(92416895)
本站qq群704220115。
加入微信群請掃碼:
