最近一段時間,看完了項亮大佬的《推薦系統實踐》,然后開始看和實踐王喆老師的新書《深度學習推薦系統》,整篇博客對自己的代碼整理和知識點的回顧。在初次接觸后,我對推薦系統的初印象是並不僅僅是算法的學習,還有架構和其他數據處理的知識需要掌握。
推薦系統前沿 之 協同過濾
初次接觸推薦系統,看到最多的字眼便是協同過濾,這也是最基本的推薦系統模型,重在理解推薦系統的基本概念。
《深度學習推薦系統》中對協同過濾的定義是:協同大家的反饋、評價和意見一起對海量信息進行過濾,從中篩選出目標用戶可能感興趣的信息的推薦過程。我對此的理解是一個用戶或者物品的屬性來源於所有的用戶和物品,集體的信息,利用集體的智慧編程找出最合適的結果(靈感來源於《集體智慧編程》)。

協同過濾主要包括基於用戶相似度的推薦(UserCF)和基於物品相似度推薦(ItemCF)。因為在生活實際中,一個用戶一般來說只與少部分物品和少部分用戶有關聯,所以共現矩陣是稀疏的。利用矩陣分解可以加強處理稀疏矩陣的能力。
基本概念
| 名稱 | 介紹 |
|---|---|
| 共現矩陣 | 所有用戶和所有物品之間關系的矩陣。 |
| 相似度矩陣 | 利用共現矩陣中的用戶向量和物品向量以及余弦相似度,皮爾遜相關系數公式求出兩兩之間的相似度,准確地說是方陣。 |
下圖為共現矩陣,行向量為用戶向量,列向量為物品向量。
UserCF & ItemCF
協同過濾步驟總體上分為兩步,一是計算用戶(物品)相似度矩陣,二是利用相似度矩陣給用戶推薦物品。
上面圖片中對於協同過濾的介紹來自於螞蟻學python
原理其實就是這么多,主要是實現上的細節問題。我利用Movielens中的100k的數據集進行了驗證,代碼是能跑通的。利用代碼和添加注釋來回顧我的實現過程。
| 最后推薦的公式 | 詳細 | 參數 |
|---|---|---|
| UserCF | \(R_{u,p}=\frac{\sum_{s\in S}(w_{u,s}\cdot R_{x,p})}{\sum_{s\in S}w_{u,s}}\) | \(w_{u,s}\)是用戶u對用戶s的相似度,\(R_{s.p}是用戶s對物品p的評分\) |
| ItemCF | \(R_{u,p}=\sum_{h\in H}(w_{p,h}\cdot R_{u,h})\) | \(w_{p,h}\)是物品p與物品h的物品相似度,\(R_{u,h}是用戶u對物品h的已有評分\) |
這里的用戶相似度、物品相似度公式是比較簡單的,一些為了防止熱門物品和充分挖掘數據長尾能力的協同過濾方法就會修改相似度公式。
用戶相似度的計算使用的是用戶對各種物品的評分或者購買情況而不是用戶明顯的興趣向量,同理物品相似度使用的是各種用戶的對該物品的購買情況而不是物品的內容向量。由此可見是協同過濾,使用的是交互信息。
import pandas as pd
import numpy as np
from tqdm import tqdm
import matplotlib.pyplot as plt
import fire
import ipdb
# 沒有任何歧義時,u表示某一用戶,i表示某一電影,v表示除u以外的另一用戶
def Get_data(file,need_cols=['user_id','item_id'],ratings=False):
'''
讀取相應的文件
@params
file: 需要打開的文件
need_cols: 需要的列名稱,不考慮上下文信息,默認只需要用戶和電影名
@return
train:用於訓練的共現矩陣
test:用於驗證的共現矩陣
'''
print('成功打開{}號文件'.format(file))
train_file,test_file='./ml-100k/u{}.base'.format(file),'./ml-100k/u{}.test'.format(file)
train,test=pd.read_csv(train_file,delimiter='\t',header=None),pd.read_csv(test_file,delimiter='\t',header=None)
train.columns=['user_id','item_id','ratings','timestap']
test.columns=['user_id','item_id','ratings','timestap']
if ratings:
train=pd.crosstab(train['user_id'],train['item_id'],values=train['ratings'],aggfunc=sum)
else:
train=pd.crosstab(train['user_id'],train['item_id']) # 交叉表表示u是否看過了i
test=pd.crosstab(test['user_id'],test['item_id'])
return train,test
def Precision_and_Recall(pred_dict,test):
'''
計算預測率和召回率
@parmas
pred_dict:為用戶推薦的電影
test:用於測試的共現矩陣
@return
pre:准確率
rec:召回率
'''
all_pre=0
all_rec=sum(test==1) # 所有應該被預測出來的數,即用戶真實看了的電影數
shot=0
for user in pred_dict.keys():
if user not in list(test.index):
continue
for info in pred_dict[user]:
all_pre+=1
if info not in list(test.columns):
continue
if test.loc[user,info]==1:
shot+=1 # 程序運行到這里,表示推薦的電影i,u是將會看的,即推薦成功
return 1.0*shot/all_pre,1.0*shot/all_rec
def Cal_similarty(matrix,categorical='user',method='cosine'):
'''
計算相似度矩陣
@params
matrix:用於訓練的共現矩陣
categorical:計算用戶相似度矩陣還是物品相似度矩陣,即UseCF還是ItemCF
method:計算相似度的方法,cosine表示利用余弦相似度方法計算相似度
@return
sim_matrix:相似度方陣
'''
print('開始計算相似度矩陣...',end="")
sim_matrix=None
if categorical=='user':
if method=='cosine':
sim_matrix=pd.DataFrame(np.zeros((matrix.shape[0],matrix.shape[0])),index=list(matrix.index),columns=list(matrix.index)) # 初始化用戶相似度矩陣
for u in tqdm(list(matrix.index)):
for v in list(matrix.index):
if u==v: # 保持對角線上的元素為0,因為接下來要選擇最相似的幾個用戶,不能包括自己
continue
vector_u,vector_v=sim_matrix.loc[u,:].values,sim_matrix.loc[v,:].values # 用戶u,v的用戶向量
sim_matrix.loc[u,v]=1.0*np.dot(vector_u,vector_v)/(np.linalg.norm(vector_u,ord=2)*np.linalg.norm(vector_v,ord=2)) # 余弦相似度公式
else:
if method=='cosine':
sim_matrix=pd.DataFrame(np.zeros((matrix.shape[1],matrix.shape[1])),index=list(matrix.columns),columns=list(matrix.columns))
for u in tqdm(list(matrix.columns)):
for v in list(matrix.columns):
if u==v:
continue
vector_u,vector_v=sim_matrix.loc[:,u].values,sim_matrix.loc[:,v].values
sim_matrix.loc[u,v]=1.0*np.dot(vector_u,vector_v)/(np.linalg.norm(vector_u,ord=2)*np.linalg.norm(vector_v,ord=2))
print('DONE')
return sim_matrix
def Recommend(matrix,sim_matrix,N,k=100):
'''
向用戶推薦
@params
matrix:用於訓練的共現矩陣
sim_matrix:相似度矩陣,現在為指明是何種相似度矩陣
N:每個用戶推薦的個數
k:在推薦錢需要選擇k個最相似的用戶或者物品
@return
rec_dict:為用戶推薦的電影
'''
print('開始推薦內容...',end="")
rec=matrix.copy()
rec.loc[:,:]=0 # 保持最好的表示用戶行為的方式,共現矩陣
if matrix.shape[0]==sim_matrix.shape[0]: # 指明為UserCF
m_values=matrix.values
for user in rec.index:
sim_values=sim_matrix.loc[user,:].values.reshape(sim_matrix.shape[0],1) # 用戶u的相似度向量
val=sorted(sim_values)[-k]
sim_values[sim_values<val]=0 # 選擇k個最相似的用戶,其他的用戶在推薦物品的時候置為0
rec.loc[user,:]=np.sum(sim_values*m_values,axis=0)/np.sum(sim_values) # 利用用戶相似度和相似用戶評價的加權平均獲得目標用戶u對物品i的得分,w_u,s和R_s,p為0的進行了計算也不會影響結果
rec.loc[user,(matrix.loc[user,:]>0).tolist()]=0 # 不推薦user已經看過了的
else: # 指明為ItemCF
sim_values=sim_matrix.values
temp_val=np.sort(sim_values,axis=1)[:,-k]
sim_values[sim_values<temp_val]=0 # 選擇相似度矩陣中分別和每個物品最相似的前k個物品
for user in rec.index:
m_values=matrix.loc[user,:].values.reshape(matrix.shape[1],1) # 利用用戶對物品的興趣程度R_u,h和該物品對其他物品的相似度w_p,h的乘積,然后對物品維度進行求和表示用戶對其他物品的興趣程度
rec.loc[user,:]=np.sum(m_values*sim_values,axis=0)
rec.loc[user,(matrix.loc[user,:]>0).tolist()]=0 # 不推薦user已經看過了的
# 排序,取前N個推薦結果
rec_dict={}
for user in matrix.index:
rec_dict[user]=list(rec.loc[user,].sort_values(ascending=False).index)[:N]
print('DONE')
return rec_dict
def User_and_Item_CF(need_cols=['user_id','item_id'],categorcial='user',method='cosine',N=50,k=100):
'''
綜合成程序
'''
for i in range(1,6):
train,test=Get_data(i)
sim_matrix=Cal_similarty(train,categorcial,method)
pred_dict=Recommend(train,sim_matrix,N,k)
pre,rec=Precision_and_Recall(pred_dict,test)
pd.DataFrame(pred_dict).to_csv('./CF_Recommend/{}_recommend_res{}.csv'.format(categorcial,i))
print('第{}th折:\n准確率:{},召回率:{}'.format(i,pre,rec))
Matrix Fatorization
矩陣分解也是來自於協同過濾,矩陣分解中的矩陣是指共現矩陣,傳統的方法有特征值分解,奇異值分解和梯度下降。這里主要是實現了梯度下降方法。
矩陣分解是《深度學習推薦系統》中的說法,而在《推薦系統實踐》中LFM(latent fator model,隱語義模型)與其十分相似。模型主要是為了每個用戶和每個物品找到自己的隱向量(來源於矩陣分解),用戶u與物品i的隱向量內積便是u對i的興趣程度。同時矩陣分解是全局擬合的過程,隱向量是利用全局信息生成。而不像協同過濾,如果兩個用戶沒有相同的歷史行為或者兩個物品沒有相同的人購買,那么兩個用戶或者兩個物品之間的相似度為0,這是很片面的。舉個例子,老王購買了聯想鼠標和Macbook,李華購買了惠普鼠標和Ipad,那么老王和李華的相似度為0,但是他們的相似度應該很高才對,在隱向量中就可以發現他們對於同一類(電子產品)的興趣相似,因為隱向量類似於Embedding,對於這個四件物品來說他們相似度很高。

如上圖所示。不過還有在隱語義模型中還有一種理解,對於k維(人工決定的)可以視作對所有物品和用戶的分成k個維度來描述,用戶隱向量是用戶分別對k個維度興趣程度,物品隱向量是物品分別在k個維度的權重,比如對於電影就是科幻,玄幻,愛情等,對於音樂就是電音,輕音樂,R&B等(注意真正學習到的不能保證是這么清楚的分類,有可能是第一類是0.7的電音加上0.3的輕音樂,但是能保證如果兩個物品的隱向量相似度大則他們的類型一定相似)
| 參數 | 解釋 |
|---|---|
| \(\hat{r}_{ui}\) | 用戶u對物品i的(預測)興趣程度 |
| \(q_i\) | 物品i的隱向量 |
| \(p_u\) | 用戶u的隱向量 |
| \(K\) | 用戶評分樣本集合 |
| \(mu\) | 全局偏差系數 |
| \(b_u\) | 用戶偏差系數 |
| \(b_i\) | 物品偏差系數 |
| \(\lambda\) | 正則化系數 |
| \(\alpha\) | 學習速率 |
目標是訓練數據集中\(r_{ui}\)和\(\hat{r}_{ui}\)的差距最小,即\(\min \sum_ \limits{(u,i)\in K}(r_{ui}-\hat{r}_{ui})^2\)
| 公式 | 解釋 |
|---|---|
| \(\min_ \limits{q^*,p^*}\sum_ \limits{(u,i)\in K}(r_{ui}-\hat{r}_{ui})^2\) | 基本的目標公式 |
| \(\min_ \limits{q^*,p^*}\sum_ \limits{(u,i\in K)}(r_{ui}-q_i^Tp_u)^2+\lambda(\|q_i\|+\|p_u\|)^2\) | 基本的目標梯度下降公式 |
| \(\min_ \limits{q^*,p^*,b^*}\sum_ \limits{(u,i\in K)}(r_{ui}-\mu-b_u-b_i-q_i^Tp_u)^2+\lambda({\|q_i\|}^2+{\|p_u\|}^2+b_u^2+b_i^2)\) | 加上偏置的目標梯度下降公式 |
| ...梯度公式不怎么好描述... | 我一般靠畫圖和矩陣維度對稱(狗頭) |
代碼注釋
def Initial_Vector(train,latent_dim):
'''
初始化隱向量
@params
train:共現矩陣
latent_dim:隱向量的維度
@return
user_matrix:用戶隱向量
item_matrix:物品隱向量
mu:全局偏差系數
bias_user:用戶偏差系數
bias_item:物品偏差系數
'''
user_dim,item_dim=train.shape
user_matrix=np.random.randn(user_dim,latent_dim)
item_matrix=np.random.randn(latent_dim,item_dim)
mu=train.mean()
bias_user=np.mean(train,axis=1).reshape((user_dim,1))
bias_item=np.mean(train,axis=0).reshape((1,item_dim))
return user_matrix,item_matrix,mu,bias_user,bias_item
def Cal_loss(train,u_matrix,i_matrix,mu,bias_user,bias_item,lambd):
'''
計算損失
@param
lambd:正則化系數
@return
punish:計算公式造成的損失
regula:正則化造成的損失
'''
pq=train-u_matrix@i_matrix-mu-bias_user-bias_item # 這里將train==0的位置,也就是train集中用戶並沒有看到的電影無法打分,
pq[train==0]=0 # 在訓練集中標識為0,那么在這個計算出來的評分中也要標識為0,避免造成多余的損失
punish=np.sum(np.power(pq,2)) # 注意不能將ratings=0而不做上述操作進行運算(偷換了概念,用戶沒有看計算出來的損失不能用於梯度下降)
regula=lambd*(np.sum(np.power(bias_user,2))+np.sum(np.power(bias_item,2))+np.sum(np.power(u_matrix,2))+np.sum(np.power(i_matrix,2)))
return punish+regula
def Cal_gradient(train,u_matrix,i_matrix,mu,bias_user,bias_item,lambd):
'''
計算梯度
'''
pq=train-u_matrix@i_matrix-mu-bias_user-bias_item
pq[train==0]=0
bu_num=np.sum((train!=0),axis=1) # 對用戶和物品偏差系數進行求梯度時,要注意用戶並沒有進行評價的信息(空白信息)
bi_num=np.sum((train!=0),axis=0) # 上面兩行剔除了空白信息的梯度附加
dera_u=-2*pq@i_matrix.T+2*lambd*u_matrix
dera_i=-2*u_matrix.T@pq+2*lambd*i_matrix
dera_bu=np.sum((-2*pq),axis=1)+2*lambd*np.multiply(bu_num,bias_user.squeeze())
dera_bi=np.sum((-2*pq),axis=0)+2*lambd*np.multiply(bi_num,bias_item.squeeze())
return dera_u,dera_i,dera_bu.reshape((dera_bu.shape[0],1)),dera_bi.reshape((1,dera_bi.shape[0]))
def Re_train(train,latent_dim=5,epoches=50,lr=0.1):
'''
訓練,進行梯度下降
@params
epoches:迭代次數
lr:學習速率
@return
res_dict:矩陣分解的參數,用戶物品隱向量和各種偏差系數
'''
u_matrix,i_matrix,mu,bu,bi=Initial_Vector(train,latent_dim)
loss=[]
for i in tqdm(range(1,epoches+1)):
loss.append(Cal_loss(train,u_matrix,i_matrix,mu,bu,bi,lr))
dera_u,dera_i,dera_bu,dera_bi=Cal_gradient(train,u_matrix,i_matrix,mu,bu,bi,lr)
u_matrix=u_matrix-lr*dera_u
i_matrix=i_matrix-lr*dera_i
bu=bu-lr*dera_bu
bi=bi-lr*dera_bi
if i%10==0:
print('{}th Epoch Loss is {}'.format(i,loss[-1]))
plt.figure()
plt.plot(np.arange(1,len(loss)+1),loss,'go-')
plt.xlabel('EPOCH')
plt.ylabel("LOSS")
plt.title("Training Loss")
plt.show()
res_dict={'u_matrix':u_matrix,'i_matrix':i_matrix,'bu':bu,'bi':bi,'mu':mu}
return res_dict
def Predict(train_df,res_dict,N=40):
'''
為所有用戶推薦物品
@param
train_df:共現矩陣,此時是DataFrame格式
res_dict:矩陣分解的參數
N:每個用戶推薦的個數
@return
result:為所有用戶推薦的電影
'''
u_matrix,i_matrix,bu,bi,mu=res_dict['u_matrix'],res_dict['i_matrix'],res_dict['bu'],res_dict['bi'],res_dict['mu']
pred=pd.DataFrame((u_matrix@i_matrix+bu+bi+mu),index=train_df.index,columns=train_df.columns)
pred[train_df>0]=0
result=dict()
for user in list(pred.index):
result[user]=list(pred.loc[user,:].sort_values(ascending=False).index)[:N]
return result
def Matrix_seperate(lr=0.0000001,latent_dim=5,epoches=50,N=40):
# ipdb.set_trace()
for i in range(1,6):
print("開始讀取數據...",end="")
train_df,test=Get_data(i,ratings=True)
train_df=train_df.fillna(0)
print('DONE')
train=train_df.values
print("開始第{}次訓練...".format(i))
res_dict=Re_train(train,latent_dim,epoches,lr)
result=Predict(train_df,res_dict,N)
pd.DataFrame(result).to_csv('./CF_Recommend/Matrix_seperate_{}.csv'.format(i))
pre,rec=Precision_and_Recall(result,test)
print("{}th 交叉驗證\n准確率:{},召回率:{}".format(i,pre,rec))
總結一下:這個矩陣分解中就是數學上的運算轉換成代碼有點難度,注意理解和分析(我是在寫這篇博客才發現之前的代碼錯了,如果還是錯了,請指正)
數據集
這里將我的數據打包放在百度網盤了,需要自取。密碼: gwoa
人生此處,絕對樂觀
