【推薦算法】從零開始做推薦(六)——貝葉斯性化排序 (BPRMF) 的Tensorflow版

前言

   之前我們已經介紹了貝葉斯個性化排序(Bayesian Personalized Ranking, BPR,並基於BPR矩陣分解進行推薦實戰。同傳統矩陣分解一樣,BPRMF也有神經網絡的版本。此方法在劉建平老師的博客上已有介紹,但其評價指標和數據集劃分與本系列不同。因此,本文在其基礎上進行修改,方便與本系列方法一同進行比較。

  如果你對本系列(未寫完,持續更新中)感興趣,可接以下傳送門:
  【推薦算法】從零開始做推薦(一)——認識推薦、認識數據
  【推薦算法】從零開始做推薦(二)——推薦系統的評價指標,計算原理與實現樣例
  【推薦算法】從零開始做推薦(三)——傳統矩陣分解的TopK推薦
  【推薦算法】從零開始做推薦(四)——python Keras框架 利用Embedding實現矩陣分解TopK推薦
  【推薦算法】從零開始做推薦(五)——貝葉斯個性化排序矩陣分解 (BPRMF) 推薦實戰
  【推薦算法】從零開始做推薦(六)——貝葉斯性化排序矩陣分解 (BPRMF) 的Tensorflow版

核心算法

  以下內容以劉建平老師的博客爲基準進行實現。

損失函數

argmaxθl(θ)=(u,i,j)Dlnσ(xuixuj)+λθ2,σ(x)=11+ex\mathop{\arg\max_{θ}}\mathcal{l}(θ)=\sum_{(u,i,j)\in D} ln \sigma(\overline{x}_{ui}-\overline{x}_{uj})+\lambda||\theta||^2,\\ \sigma(x) = \frac{1}{1+e^{-x}}

求解算法

  此處寫的是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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章