前言
之前我們已經介紹了貝葉斯個性化排序(Bayesian Personalized Ranking, BPR),並基於BPR矩陣分解進行推薦實戰。同傳統矩陣分解一樣,BPRMF也有神經網絡的版本。此方法在劉建平老師的博客上已有介紹,但其評價指標和數據集劃分與本系列不同。因此,本文在其基礎上進行修改,方便與本系列方法一同進行比較。
如果你對本系列(未寫完,持續更新中)感興趣,可接以下傳送門:
【推薦算法】從零開始做推薦(一)——認識推薦、認識數據
【推薦算法】從零開始做推薦(二)——推薦系統的評價指標,計算原理與實現樣例
【推薦算法】從零開始做推薦(三)——傳統矩陣分解的TopK推薦
【推薦算法】從零開始做推薦(四)——python Keras框架 利用Embedding實現矩陣分解TopK推薦
【推薦算法】從零開始做推薦(五)——貝葉斯個性化排序矩陣分解 (BPRMF) 推薦實戰
【推薦算法】從零開始做推薦(六)——貝葉斯性化排序矩陣分解 (BPRMF) 的Tensorflow版
核心算法
以下內容以劉建平老師的博客爲基準進行實現。
損失函數
求解算法
此處寫的是Tensorflow的計算圖,與劉老師不一樣的地方在於,此處修改了參數(與傳統BPRMF的參數設定成一致),並將損失函數完全按上文提及的定義。最大化損失等於最小化損失的負數,用梯度下降進行訓練。
def bpr_mf(user_count,item_count,hidden_dim):
u = tf.placeholder(tf.int32,[None])
i = tf.placeholder(tf.int32,[None])
j = tf.placeholder(tf.int32,[None])
user_emb_w = tf.get_variable("user_emb_w", [user_count + 1, hidden_dim],
initializer=tf.random_normal_initializer(0, 0.001))
item_emb_w = tf.get_variable("item_emb_w", [item_count + 1, hidden_dim],
initializer=tf.random_normal_initializer(0, 0.001))
u_emb = tf.nn.embedding_lookup(user_emb_w, u)
i_emb = tf.nn.embedding_lookup(item_emb_w, i)
j_emb = tf.nn.embedding_lookup(item_emb_w, j)
x = tf.reduce_sum(tf.multiply(u_emb,(i_emb-j_emb)),1,keepdims=True)
l2_norm = tf.add_n([
tf.reduce_sum(tf.multiply(u_emb, u_emb)),
tf.reduce_sum(tf.multiply(i_emb, i_emb)),
tf.reduce_sum(tf.multiply(j_emb, j_emb))
])
regulation_rate = 0.01
bprloss = tf.reduce_mean(tf.log(tf.sigmoid(x))) + regulation_rate * l2_norm
train_op = tf.train.GradientDescentOptimizer(0.005).minimize(-bprloss)
return u, i, j, bprloss, train_op
實驗結果
在ML100K上進行實驗,數據集在本系列前文有。結果如下:
傳統的BPRMF如下,目前的實驗結果來看,傳統的實現效果會更好。
進階!靈魂拷問
拷問1. BPR的Tensorflow版本(下稱NN-BPRMF)與普通的BPRMF有何異同?
NN-BPRMF參數更多(尤其是batchsize),調參更難。BPRMF受初值影響較低,而NN-BPRMF受初值影響較大(正態分佈、均勻分佈、正負,都有影響),會很容易陷入梯度消失或梯度爆炸。NN-BPR在調成有效結果(即指標表現較好時),其LOSS曲線會是凹,這可能是由正則過大導致的;但當調成凸圖像時,其結果又會較差。
因此,無論從過程看,還是結果看,更推薦使用傳統的BPRMF。
拷問2. 本文的NN-BPRMF的與劉老師版本有何不同?
完全基於劉老師版本在我們的四個指標上效果較差,因此進行了參數上的調整,主要調了學習率和正則係數。其次,調整了損失函數,使其形式和理論總結的Loss公式一致。
完整代碼
# -*- coding: utf-8 -*-
"""
Created on Sun Mar 24 09:59:50 2019
@author: Yang Lechuan
"""
import numpy as np
import pandas as pd
import tensorflow as tf
import os
import random
import time
from collections import defaultdict
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)
data = pd.concat([train,test])
all_user = np.unique(data['user'])
all_item = np.unique(data['item'])
if os.path.exists('./NN-BPRMF'):
pass
else:
os.mkdir('./NN-BPRMF')
train.to_csv('./NN-BPRMF/train.txt',index = False,header=0)
test.to_csv('./NN-BPRMF/test.txt',index = False,header=0)
return all_user,all_item,train,test
def load_data(train_data,all_user,all_item):
user_ratings = defaultdict(set)
#user_ratings = {}
max_u_id = max(all_user)
max_i_id = max(all_item)
for i in range(0,len(train_data)):
u = int(train_data.iloc[i]['user'])
i = int(train_data.iloc[i]['item'])
#user_ratings[u] = i
user_ratings[u].add(i)
print('max_u_id:',max_u_id)
print('max_i_id:',max_i_id)
#print(user_ratings)
return max_u_id,max_i_id,user_ratings
def generate_train_batch(user_ratings,item_count,batch_size):
"""
構造訓練用的三元組
對於隨機抽出的用戶u,i可以從user_ratings隨機抽出,而j也是從總的電影集中隨機抽出,當然j必須保證(u,j)不在user_ratings中
"""
t = []
for b in range(batch_size):
u = random.sample(user_ratings.keys(),1)[0]
i = random.sample(user_ratings[u],1)[0]
j = random.randint(1,item_count)
while j in user_ratings[u]:
j = random.randint(1,item_count)
t.append([u,i,j])
return np.asarray(t)
def bpr_mf(user_count,item_count,hidden_dim):
u = tf.placeholder(tf.int32,[None])
i = tf.placeholder(tf.int32,[None])
j = tf.placeholder(tf.int32,[None])
user_emb_w = tf.get_variable("user_emb_w", [user_count + 1, hidden_dim],
initializer=tf.random_normal_initializer(0, 0.1),
#initializer = tf.random_uniform_initializer(-1,1)
)
item_emb_w = tf.get_variable("item_emb_w", [item_count + 1, hidden_dim],
initializer=tf.random_normal_initializer(0, 0.1)
#initializer = tf.random_uniform_initializer(-1,1)
)
u_emb = tf.nn.embedding_lookup(user_emb_w, u)
i_emb = tf.nn.embedding_lookup(item_emb_w, i)
j_emb = tf.nn.embedding_lookup(item_emb_w, j)
x = tf.reduce_sum(tf.multiply(u_emb,(i_emb-j_emb)),1,keepdims=True)
l2_norm = tf.add_n([
tf.reduce_sum(tf.multiply(u_emb, u_emb)),
tf.reduce_sum(tf.multiply(i_emb, i_emb)),
tf.reduce_sum(tf.multiply(j_emb, j_emb))
])
regulation_rate = 0.01
bprloss = tf.reduce_mean(tf.log(tf.sigmoid(x))) + regulation_rate * l2_norm
train_op = tf.train.GradientDescentOptimizer(0.005).minimize(-bprloss)
return u, i, j, bprloss, train_op
def topk(dic,k):
keys = []
values = []
for i in range(0,k):
key,value = min(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 train(iters,step,batch_size):
user_count,item_count,user_ratings = load_data(train_data,all_user,all_item)
L = []
with tf.Graph().as_default(), tf.Session() as sess:
u,i,j,bprloss,train_op = bpr_mf(user_count,item_count,dimension)
sess.run(tf.global_variables_initializer())
for epoch in range(iters):
st = time.time()
print('----------------')
print("epoch",epoch+1,'start')
_batch_bprloss = 0
for k in range(step):
uij = generate_train_batch(user_ratings,item_count,batch_size)
_bprloss,_train_op = sess.run([bprloss,train_op],
feed_dict={u:uij[:,0],i:uij[:,1],j:uij[:,2]})
_batch_bprloss += _bprloss
print("bpr_loss:",_batch_bprloss)
L.append(_batch_bprloss)
print("epoch",epoch+1,"spend :",time.time()-st,'s')
variable_names = [v.name for v in tf.trainable_variables()]
values = sess.run(variable_names)
cnt = 0
for k, v in zip(variable_names, values):
print('================')
print("Variable: ", k)
print("Shape: ", v.shape)
if cnt == 0:
np.savetxt('./NN-BPRMF/userVec.txt',v,delimiter=',',newline='\n')
cnt = cnt + 1
else:
np.savetxt('./NN-BPRMF/itemVec.txt',v,delimiter=',',newline='\n')
L = np.array(L)
def test(all_user,all_item,train_data,test_data,dimension,k):
userP = np.loadtxt('./NN-BPRMF/userVec.txt',delimiter=',',dtype=float)
itemP = np.loadtxt('./NN-BPRMF/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 = 24
step = 10000
dimension = 60
batch_size = 16
all_user,all_item,train_data,test_data = getUI(dsname,dformat)
train(iters,step,batch_size)
k = 10
test(all_user,all_item,train_data,test_data,dimension,k)
# k = 10
# test(all_user,all_item,train_data,test_data,dimension,k)
# k = 20
# test(all_user,all_item,train_data,test_data,dimension,k)