在眾多召回策略里面,基於Item與基於User(可參考:https://www.cnblogs.com/SysoCjs/p/11466424.html)在實現上非常相似。所以這里使用了跟基於User協同過濾的數據u.data。
u.data數據格式(user_id, item_id, rating, timestamp)
實現原理:
區別於User,先根據User已經購買過,或者評價過的Items,基於算法,對其他Items做一個相似度計算,來獲取基於該User的Items的相似Items,這樣每個User_Item都可以得到一堆相似Items,根據相似度進行排序,每個User_Item取出topN個Items,然后將所有的User_item的各自topN個Items,共同組成一個大的Item_List,Item_list中的每個item的相似度乘上對應的User_item的權重,得到新的權重值,根據新的權重值排序,取topK個Items,作為最終的Items候選集。
具體實現:
因為是使用Python來實現,所以還是選擇最簡單的算法:傑卡爾德算法。在User協同過濾中:通過兩個用戶共同擁有的物品集合數量,除以兩個用戶的物品的平均數量。這里,需要倒過來:通過兩個item共同用戶數量,除以兩個item各自的用戶的平均數量。所以要做的是,想方設法獲取這三個參數值:共同用戶數量、兩個item各自的用戶數量。
一、創建源數據的結構
timestamp列數據在尋找相似用戶里面,意義不大,可以不使用;對於pyspark程序,u.data數據是沒有結構的,所以第一時間是讀取u.data,並定義數據的結構,可以將數據的結構定義為:
dict{user_id:{item_id:rating}}
import pandas as pd def generate_train_data(nrows=10): # 處理訓練數據 -> dict{user_id:{item_id:rating}}
df = pd.read_csv('../row_data/u.data', sep='\t', nrows=nrows, names=['user_id', 'item_id', 'rating', 'timestamp']) # d為每個用戶的商品及對應打分的列表
d = dict() for _, row in df.iterrows(): # 類型轉換
user_id = str(row['user_id']) item_id = str(row['item_id']) rating = row['rating'] if user_id not in d.keys(): d[user_id] = {item_id: rating} else: d[user_id][item_id] = rating return d
二、統計傑卡爾德的參數
基於Item的協同過濾,在遍歷train_data的時候,需要同時統計每個item的User數量,以及item與item之間共同的User數量,因為,相對於基於User的協同過濾,item的User數量不可以直接通過len(train_data[i])來獲取,需要另外定義一個類型為dict的N來存儲,N的數據結構為{item: userNum}。
N = dict() # N{item:userNum},統計item的user數量
在遍歷train_data的同時,可以將N和C都收集得到:
N = dict() # N{item:userNum},統計item的user數量
def item_sim(train_data): C = dict() # C{item:{iten:simUserNum}}
for u,items in train_data.items(): for i in items.keys(): # 對於每一個u的items,N[i]只有一個
if N.get(i,-1)==-1: N[i] = 0 N[i] += 1
if C.get(i,-1)==-1: C[i] = dict() for j in items.keys(): if i==j: continue
if C[i].get(j,-1)==-1: C[i][j] = 0 C[i][j] += 1
return C
這個時候,共同用戶數量存儲在C里面,而每個item對應的用戶數量存儲在N里面,那么接下來可以借用傑卡爾德公式計算Item-Item相似度矩陣。
# 計算相似度矩陣,傑卡爾德算法
def item_item(C): for i, related_items in C.items(): for j, cij in related_items.items(): C[i][j] = 2 * cij/(N[i]+N[j]*1.0) return C
最終結果還是保存在C里面,此時C的數據結構變成:
C = dict() # C{item:{iten:sim_score}}
二、過濾,統計推薦候選集
最終是要得到Item候選集,所以先定義一個字典,用於存儲item,及其對應的打分:
rank = dict() # rank{item:score}
Item候選集仍然是指代推薦給指定User的Items,一般會根據業務需求,將該User購買過或評價過的Item進行過濾,這時,需要取出該User對應的r購買過或評價過的Item列表:
Ru = train_data[user_id]
過濾掉User購買過或評價過的Item后,需要計算候選集中每個Item的打分,通過Item在該User的rating,乘上其相似Item的sim_score,得到最終打分score:
def recommendation(train_data, user_id, C, k): rank = dict() # rank{item:score}
# 用戶user_id有很多個已經購買的item,每個item都有好多個相似item,
# 每個item取k個相似item
Ru = train_data[user_id] for i, rating in Ru.items(): # 相對於user-based,item-based的一個用戶對應於多個item,所以大循環,要遍歷item
for j, sim_score in sorted(C[i].items(), key=lambda x:x[1],reverse=True)[0:k]: # 相對於user-based,每個user還要遍歷各自的item,item-based則不需要,需要它的矩陣就是item-iten
# 過濾這個user已經打分過的item
if j in Ru: continue
if rank.get(j,-1)==-1: rank[j] = 0 rank[j] += sim_score*rating return rank
三、根據指定User,得出其推薦Item候選集
if __name__ == '__main__': train_data = dict() with open(train_data_path,'r') as ft: train_data = eval(ft.read()) C = dict() with open(sim_item_path, 'r') as fs: C = eval(fs.read()) user_id = '196' k = 5 rank = recommendation(user_id, C, train_data, k) print(sorted(rank.items(), key=lambda x: x[1], reverse=True)[0:k])
