【推薦算法】從零開始做推薦(五)——貝葉斯個性化排序矩陣分解 (BPRMF) 推薦實戰

前言

   之前我們已經陸續完成了數據集、評價指標和經典算法MF推薦的實戰,接下來本章將介紹另一種應用也非常廣泛的推薦技術:貝葉斯個性化排序(Bayesian Personalized Ranking, BPR。將基於BPR矩陣分解進行推薦實戰。

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

什麼是BPR?

  這就要提到本人神交已久的引路人劉建平先生文章的介紹:貝葉斯個性化排序(BPR)算法小結。中文講解,十分詳細。
  本文重在實現,這裏只做一個簡單的介紹,以下爲本人對BPR的理解:假設用戶 uu 能觀測到所有的項目,那麼對於用戶最終點擊訪問的項目 ii, 和最終未訪問的項目 jj,有偏好 i>uji>_uj。因此,BPR尋求一批參數 Θ\Theta,使得概率P(Θ>u)P(\Theta|>_u)最大.
  這裏舉個例子,比如網易雲給你推薦了一個歌單,裏面有10首歌,10首你都看到了,然後選了第一首,其他沒選。那麼在這10首歌裏,相比於其他9首你更喜歡第一首,即訪問第一首的概率要比其他9首要大,學出來的參數就要能很好地描述這一點。
  那麼說直白點,就是期望用戶訪問過的項目,其評分要高於未訪問過的,我們的目標即爲最大化兩者之間的差距。
  在這個過程中,我們可以明確幾點:
  1. BPR不是具體的推薦方法,而是一種思想,因此它可以應用在多種推薦模型上,本文將其應用到矩陣分解上。
  2. BPR是一種參數學習的方法,得到的參數是基於BPR思想的。

核心算法

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

損失函數

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}}

求解過程

在這裏插入圖片描述

求解算法

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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章