前言
之前我們已經陸續完成了數據集、評價指標和經典算法MF推薦的實戰,接下來本章將介紹另一種應用也非常廣泛的推薦技術:貝葉斯個性化排序(Bayesian Personalized Ranking, BPR)。將基於BPR矩陣分解進行推薦實戰。
如果你對本系列(未寫完,持續更新中)感興趣,可接以下傳送門:
【推薦算法】從零開始做推薦(一)——認識推薦、認識數據
【推薦算法】從零開始做推薦(二)——推薦系統的評價指標,計算原理與實現樣例
【推薦算法】從零開始做推薦(三)——傳統矩陣分解的TopK推薦
【推薦算法】從零開始做推薦(四)——python Keras框架 利用Embedding實現矩陣分解TopK推薦
【推薦算法】從零開始做推薦(五)——貝葉斯個性化排序矩陣分解 (BPRMF) 推薦實戰
什麼是BPR?
這就要提到本人神交已久的引路人劉建平先生文章的介紹:貝葉斯個性化排序(BPR)算法小結。中文講解,十分詳細。
本文重在實現,這裏只做一個簡單的介紹,以下爲本人對BPR的理解:假設用戶 能觀測到所有的項目,那麼對於用戶最終點擊訪問的項目 , 和最終未訪問的項目 ,有偏好 。因此,BPR尋求一批參數 ,使得概率最大.
這裏舉個例子,比如網易雲給你推薦了一個歌單,裏面有10首歌,10首你都看到了,然後選了第一首,其他沒選。那麼在這10首歌裏,相比於其他9首你更喜歡第一首,即訪問第一首的概率要比其他9首要大,學出來的參數就要能很好地描述這一點。
那麼說直白點,就是期望用戶訪問過的項目,其評分要高於未訪問過的,我們的目標即爲最大化兩者之間的差距。
在這個過程中,我們可以明確幾點:
1. BPR不是具體的推薦方法,而是一種思想,因此它可以應用在多種推薦模型上,本文將其應用到矩陣分解上。
2. BPR是一種參數學習的方法,得到的參數是基於BPR思想的。
核心算法
以下內容以劉建平老師的博客爲基準進行實現。
損失函數
求解過程
求解算法
def train(train_data,all_user,all_item,step,iters,dimension):
# userVec,itemVec=initVec(all_user,all_item,dimension)
user_cnt = max(all_user)+1
item_cnt = max(all_item)+1
userVec = np.random.uniform(0,1,size=(user_cnt,dimension))#*0.01
itemVec = np.random.uniform(0,1,size=(item_cnt,dimension))#*0.01
lr = 0.005
reg = 0.01
f = open('./BPR/train_record.txt','w')
sum_t = 0
train_user = np.unique(train_data['user'])
L = []
for i in range(iters):
loss = 0
cnt = 0
st = time.time()
for j in range(step):
user = int(np.random.choice(train_user,1))
visited = np.array(train_data[train_data['user']==user]['item'])
# print(visited)
itemI = int(np.random.choice(visited,1))
itemJ = int(np.random.choice(all_item,1))
while itemJ in visited:
itemJ = int(np.random.choice(all_item,1))
# print(user,itemI,itemJ)
# print(userVec[user],userVec[user].shape)
# print(itemVec[itemI],itemVec[itemI].shape)
r_ui = np.dot(userVec[user], itemVec[itemI].T)
r_uj = np.dot(userVec[user], itemVec[itemJ].T)
r_uij = r_ui - r_uj
factor = 1.0 / (1 + np.exp(r_uij))
# update U and V
userVec[user] += lr * (factor * (itemVec[itemI] - itemVec[itemJ]) + reg * userVec[user])
itemVec[itemI] += lr * (factor * userVec[user] + reg * itemVec[itemI])
itemVec[itemJ] += lr * (factor * (-userVec[user]) + reg * itemVec[itemJ])
loss += (1.0 / (1 + np.exp(-r_uij)))
cnt = cnt + 1
# loss = 1.0 * loss / cnt
loss += + reg * (
np.power(np.linalg.norm(userVec,ord=2),2)
+ np.power(np.linalg.norm(itemVec,ord=2),2)
+ np.power(np.linalg.norm(itemVec,ord=2),2)
)
L.append(loss)
pt = time.time()-st
sum_t = sum_t + pt
print("The "+str(i+1)+" is done! cost time:",pt)
print("Loss:",loss)
f.write("The "+str(i+1)+" is done! cost time:"+str(pt))
L = np.array(L)
plt.plot(np.arange(iters),L,c = 'r',marker = 'o')
print("\nLoss圖像爲:")
plt.show()
f.write('Training cost time: '+str(sum_t))
f.close()
np.savetxt('./BPR/userVec.txt',userVec,delimiter=',',newline='\n')
np.savetxt('./BPR/itemVec.txt',itemVec,delimiter=',',newline='\n')
實驗結果
在ML100K上進行實驗,數據集在本系列前文有。結果如下:
與矩陣分解進行對比,可以發現各項指標都高於傳統矩陣分解。
進階!靈魂拷問
拷問1. BPR矩陣分解(BPRMF)和傳統矩陣分解(MF)有哪些異同?
相同點:都是得出用戶潛在向量和項目潛在向量,之後用其點積大小進行推薦。
不同點:
1) 目標不一樣。BPRMF是通過最大化後驗概率進行學習,目標是爲了讓訪問過的項目要優於未訪問過的項目,而MF是通過最小化預測矩陣與原始矩陣的差值進行學習,目標是爲了預測與真實值更接近。因此,BPRMF最後得到的矩陣和原始矩陣並無太大關聯,同時僅用用戶點擊矩陣(0-1)而非評分矩陣。
2) 訓練數據不一樣。BPRMF是通過構造三元組的方式進行訓練,使用的數據並非是全部數據(畢竟遍歷出所有的三元組數目過於龐大)。此外,論文中有講,不遍歷所有用戶或者項目,而是隨機構成三元組會更快收斂,詳見核心算法。而MF的訓練數據則完全依賴於用戶交互過的那些項目。因此,就效率而言,BPRMF是會高於MF的。
拷問2. BPRMF有哪些缺點?
1) 貝葉斯方法的假設侷限。衆所周知,一提到概率就少不了假設,像相互獨立的假設、數據分佈的假設,這些假設會使這類方法有一個天花板。不過在其他方法沒有明顯優勢的時候,貝葉斯方法還是很能打的,理論深度也高,利於發論文。
2) 極端冷啓動或使該方法失效。想象一下這個場景,規定只能推薦新的項目給用戶,那麼在學習的時候,老項目評分會比新項目高,那麼推薦系統會一直選老項目推薦給用戶,按規定,我們把老項目全部去掉再推薦,對於新項目該方法基本就等於猜,其推薦質量就可想而知了。
儘管有這些問題存在,但BPR的思想運用和發展還是很有前景的(論文數量多),而BPRMF也不失爲一個好的Baseline。
完整代碼
# -*- coding: utf-8 -*-
"""
Created on Wed Oct 16 09:53:08 2019
@author: YLC
"""
import os
import numpy as np
import pandas as pd
import time
import matplotlib.pyplot as plt #用於畫圖
#顯示所有列
pd.set_option('display.max_columns', None)
#顯示所有行
pd.set_option('display.max_rows', None)
def getUI(dsname,dformat): #獲取全部用戶和項目
train = pd.read_csv(dsname+'_train.txt',header = None,names = dformat)
test = pd.read_csv(dsname+'_test.txt',header = None,names = dformat)
rating = pd.concat([train,test])
all_user = np.unique(rating['user'])
num_user = max(all_user)#len(all_user)
all_item = np.unique(rating['item'])
num_item = max(all_item)#len(np.unique(rating['item']))
# print(num_user,num_item)
# print(len(train),len(test))
if os.path.exists('./BPR'):
pass
else:
os.mkdir('./BPR')
train.sort_values(by=['user','time'],axis=0,inplace=True) #先按時間、再按用戶排序
train.to_csv('./BPR/train.txt',index = False,header=0)
test.sort_values(by=['user','time'],axis=0,inplace=True) #先按時間、再按用戶排序
test.to_csv('./BPR/test.txt',index = False,header=0)
return all_user,all_item,train,test
def train(train_data,all_user,all_item,step,iters,dimension):
# userVec,itemVec=initVec(all_user,all_item,dimension)
user_cnt = max(all_user)+1
item_cnt = max(all_item)+1
userVec = np.random.uniform(0,1,size=(user_cnt,dimension))#*0.01
itemVec = np.random.uniform(0,1,size=(item_cnt,dimension))#*0.01
lr = 0.005
reg = 0.01
f = open('./BPR/train_record.txt','w')
sum_t = 0
train_user = np.unique(train_data['user'])
L = []
for i in range(iters):
loss = 0
cnt = 0
st = time.time()
for j in range(step):
user = int(np.random.choice(train_user,1))
visited = np.array(train_data[train_data['user']==user]['item'])
# print(visited)
itemI = int(np.random.choice(visited,1))
itemJ = int(np.random.choice(all_item,1))
while itemJ in visited:
itemJ = int(np.random.choice(all_item,1))
# print(user,itemI,itemJ)
# print(userVec[user],userVec[user].shape)
# print(itemVec[itemI],itemVec[itemI].shape)
r_ui = np.dot(userVec[user], itemVec[itemI].T)
r_uj = np.dot(userVec[user], itemVec[itemJ].T)
r_uij = r_ui - r_uj
factor = 1.0 / (1 + np.exp(r_uij))
# update U and V
userVec[user] += lr * (factor * (itemVec[itemI] - itemVec[itemJ]) + reg * userVec[user])
itemVec[itemI] += lr * (factor * userVec[user] + reg * itemVec[itemI])
itemVec[itemJ] += lr * (factor * (-userVec[user]) + reg * itemVec[itemJ])
loss += (1.0 / (1 + np.exp(-r_uij)))
cnt = cnt + 1
# loss = 1.0 * loss / cnt
loss += + reg * (
np.power(np.linalg.norm(userVec,ord=2),2)
+ np.power(np.linalg.norm(itemVec,ord=2),2)
+ np.power(np.linalg.norm(itemVec,ord=2),2)
)
L.append(loss)
pt = time.time()-st
sum_t = sum_t + pt
print("The "+str(i+1)+" is done! cost time:",pt)
print("Loss:",loss)
f.write("The "+str(i+1)+" is done! cost time:"+str(pt))
L = np.array(L)
plt.plot(np.arange(iters),L,c = 'r',marker = 'o')
print("\nLoss圖像爲:")
plt.show()
f.write('Training cost time: '+str(sum_t))
f.close()
np.savetxt('./BPR/userVec.txt',userVec,delimiter=',',newline='\n')
np.savetxt('./BPR/itemVec.txt',itemVec,delimiter=',',newline='\n')
def topk(dic,k):
keys = []
values = []
for i in range(0,k):
key,value = max(dic.items(),key=lambda x: x[1])
keys.append(key)
values.append(value)
dic.pop(key)
return keys,values
def cal_indicators(rankedlist, testlist):
HITS_i = 0
sum_precs = 0
AP_i = 0
len_R = 0
len_T = 0
MRR_i = 0
ranked_score = []
for n in range(len(rankedlist)):
if rankedlist[n] in testlist:
HITS_i += 1
sum_precs += HITS_i / (n + 1.0)
if MRR_i == 0:
MRR_i = 1.0/(rankedlist.index(rankedlist[n])+1)
else:
ranked_score.append(0)
if HITS_i > 0:
AP_i = sum_precs/len(testlist)
len_R = len(rankedlist)
len_T = len(testlist)
return AP_i,len_R,len_T,MRR_i,HITS_i
def test(all_user,all_item,train_data,test_data,dimension,k):
userP = np.loadtxt('./BPR/userVec.txt',delimiter=',',dtype=float)
itemP = np.loadtxt('./BPR/itemVec.txt',delimiter=',',dtype=float)
PRE = 0
REC = 0
MAP = 0
MRR = 0
AP = 0
HITS = 0
sum_R = 0
sum_T = 0
valid_cnt = 0
stime = time.time()
test_user = np.unique(test_data['user'])
for user in test_user:
# user = 0
visited_item = list(train_data[train_data['user']==user]['item'])
# print('訪問過的item:',visited_item)
if len(visited_item)==0: #沒有訓練數據,跳過
continue
per_st = time.time()
testlist = list(test_data[test_data['user']==user]['item'].drop_duplicates()) #去重保留第一個
testlist = list(set(testlist)-set(testlist).intersection(set(visited_item))) #去掉訪問過的item
if len(testlist)==0: #過濾後爲空,跳過
continue
# print("對用戶",user)
valid_cnt = valid_cnt + 1 #有效測試數
poss = {}
for item in all_item:
if item in visited_item:
continue
else:
poss[item] = np.dot(userP[user],itemP[item])
# print(poss)
rankedlist,test_score = topk(poss,k)
# print("Topk推薦:",rankedlist)
# print("實際訪問:",testlist)
# print("單條推薦耗時:",time.time() - per_st)
AP_i,len_R,len_T,MRR_i,HITS_i= cal_indicators(rankedlist, testlist)
AP += AP_i
sum_R += len_R
sum_T += len_T
MRR += MRR_i
HITS += HITS_i
# print(test_score)
# print('--------')
# break
etime = time.time()
PRE = HITS/(sum_R*1.0)
REC = HITS/(sum_T*1.0)
MAP = AP/(valid_cnt*1.0)
MRR = MRR/(valid_cnt*1.0)
p_time = (etime-stime)/valid_cnt
print('評價指標如下:')
print('PRE@',k,':',PRE)
print('REC@',k,':',REC)
print('MAP@',k,':',MAP)
print('MRR@',k,':',MRR)
print('平均每條推薦耗時:',p_time)
print('總耗時:',etime-stime)
with open('./BPR/result_'+dsname+'.txt','w') as f:
f.write('評價指標如下:\n')
f.write('PRE@'+str(k)+':'+str(PRE)+'\n')
f.write('REC@'+str(k)+':'+str(REC)+'\n')
f.write('MAP@'+str(k)+':'+str(MAP)+'\n')
f.write('MRR@'+str(k)+':'+str(MRR)+'\n')
f.write('平均每條推薦耗時@:'+str(k)+':'+str(p_time)+'\n')
f.write('總耗時@:'+str(k)+':'+str(etime-stime)+'s\n')
if __name__ == '__main__':
dsname = 'ML100K'
dformat = ['user','item','rating','time']
iters = 32
step = 10000
all_user,all_item,train_data,test_data = getUI(dsname,dformat)
dimension = 60
train(train_data,all_user,all_item,step,iters,dimension)
k = 10
test(all_user,all_item,train_data,test_data,dimension,k)