1. 程式人生 > >推薦系統遇上深度學習(二十)--貝葉斯個性化排序(BPR)演算法原理及實戰

推薦系統遇上深度學習(二十)--貝葉斯個性化排序(BPR)演算法原理及實戰

原創:石曉文 小小挖掘機 2018-06-29

推薦系統遇上深度學習系列:


排序推薦演算法大體上可以分為三類,第一類排序演算法類別是點對方法(Pointwise Approach),這類演算法將排序問題被轉化為分類、迴歸之類的問題,並使用現有分類、迴歸等方法進行實現。第二類排序演算法是成對方法(Pairwise Approach),在序列方法中,排序被轉化為對序列分類或對序列迴歸。所謂的pair就是成對的排序,比如(a,b)一組表明a比b排的靠前。第三類排序演算法是列表方法(Listwise Approach),它採用更加直接的方法對排序問題進行了處理。它在學習和預測過程中都將排序列表作為一個樣本。排序的組結構被保持。

之前我們介紹的演算法大都是Pointwise的方法,今天我們來介紹一種Pairwise的方法:貝葉斯個性化排序(Bayesian Personalized Ranking, 以下簡稱BPR)

1、BPR演算法簡介

1.1 基本思路

在BPR演算法中,我們將任意使用者u對應的物品進行標記,如果使用者u在同時有物品i和j的時候點選了i,那麼我們就得到了一個三元組<u,i,j>,它表示對使用者u來說,i的排序要比j靠前。如果對於使用者u來說我們有m組這樣的反饋,那麼我們就可以得到m組使用者u對應的訓練樣本。

這裡,我們做出兩個假設:

  1. 每個使用者之間的偏好行為相互獨立,即使用者u在商品i和j之間的偏好和其他使用者無關。
  2. 同一使用者對不同物品的偏序相互獨立,也就是使用者u在商品i和j之間的偏好和其他的商品無關。

為了便於表述,我們用>u符號表示使用者u的偏好,上面的<u,i,j>可以表示為:i >u j。

在BPR中,我們也用到了類似矩陣分解的思想,對於使用者集U和物品集I對應的U*I的預測排序矩陣,我們期望得到兩個分解後的使用者矩陣W(|U|×k)和物品矩陣H(|I|×k),滿足:

那麼對於任意一個使用者u,對應的任意一個物品i,我們預測得出的使用者對該物品的偏好計算如下:

而模型的最終目標是尋找合適的矩陣W和H,讓X-(公式打不出來,這裡代表的是X上面有一個橫線,即W和H矩陣相乘後的結果)和X(實際的評分矩陣)最相似。看到這裡,也許你會說,BPR和矩陣分解沒有什區別呀?是的,到目前為止的基本思想是一致的,但是具體的演算法運算思路,確實千差萬別的,我們慢慢道來。

1.2 演算法運算思路

BPR 基於最大後驗估計P(W,H|>u)來求解模型引數W,H,這裡我們用θ來表示引數W和H, >u代表使用者u對應的所有商品的全序關係,則優化目標是P(θ|>u)。根據貝葉斯公式,我們有:

由於我們求解假設了使用者的排序和其他使用者無關,那麼對於任意一個使用者u來說,P(>u)對所有的物品一樣,所以有:

這個優化目標轉化為兩部分。第一部分和樣本資料集D有關,第二部分和樣本資料集D無關。

第一部分

對於第一部分,由於我們假設每個使用者之間的偏好行為相互獨立,同一使用者對不同物品的偏序相互獨立,所以有:

上面的式子類似於極大似然估計,若使用者u相比於j來說更偏向i,那麼我們就希望P(i >u j|θ)出現的概率越大越好。

上面的式子可以進一步改寫成:

而對於P(i >u j|θ)這個概率,我們可以使用下面這個式子來代替:

其中,σ(x)是sigmoid函式,σ裡面的項我們可以理解為使用者u對i和j偏好程度的差異,我們當然希望i和j的差異越大越好,這種差異如何體現,最簡單的就是差值:

省略θ我們可以將式子簡略的寫為:

因此優化目標的第一項可以寫作:

哇,是不是很簡單的思想,對於訓練資料中的<u,i,j>,使用者更偏好於i,那麼我們當然希望在X-矩陣中ui對應的值比uj對應的值大,而且差距越大越好!

當θ的先驗分佈是正態分佈時,其實就是給損失函式加入了正則項,因此我們可以假定θ的先驗分佈是正態分佈:

所以:

因此,最終的最大對數後驗估計函式可以寫作:

剩下的我們就可以通過梯度上升法(因為是要讓上式最大化)來求解了。我們這裡就略過了,BPR的思想已經很明白了吧,哈哈!讓我們來看一看如何實現吧。

2、演算法實現

資料預處理

首先,我們需要處理一下資料,得到每個使用者打分過的電影,同時,還需要得到使用者的數量和電影的數量。

def load_data():
    user_ratings = defaultdict(set)
    max_u_id = -1
    max_i_id = -1
    with open('data/u.data','r') as f:
        for line in f.readlines():
            u,i,_,_ = line.split("\t")
            u = int(u)
            i = int(i)
            user_ratings[u].add(i)
            max_u_id = max(u,max_u_id)
            max_i_id = max(i,max_i_id)


    print("max_u_id:",max_u_id)
    print("max_i_idL",max_i_id)

    return max_u_id,max_i_id,user_ratings

下面我們會對每一個使用者u,在user_ratings中隨機找到他評分過的一部電影i,儲存在user_ratings_test,後面構造訓練集和測試集需要用到。

def generate_test(user_ratings):
    """
    對每一個使用者u,在user_ratings中隨機找到他評分過的一部電影i,儲存在user_ratings_test,我們為每個使用者取出的這一個電影,是不會在訓練集中訓練到的,作為測試集用。
    """
    user_test = dict()
    for u,i_list in user_ratings.items():
        user_test[u] = random.sample(user_ratings[u],1)[0]
    return user_test

構建訓練資料
我們構造的訓練資料是<u,i,j>的三元組,i可以根據剛才生成的使用者評分字典得到,j可以利用負取樣的思想,認為使用者沒有看過的電影都是負樣本:

def generate_train_batch(user_ratings,user_ratings_test,item_count,batch_size=512):
    """
    構造訓練用的三元組
    對於隨機抽出的使用者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]
        while i==user_ratings_test[u]:
            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)

構造測試資料
同樣構造三元組,我們剛才給每個使用者單獨抽出了一部電影,這個電影作為i,而使用者所有沒有評分過的電影都是負樣本j:

def generate_test_batch(user_ratings,user_ratings_test,item_count):
    """
    對於每個使用者u,它的評分電影i是我們在user_ratings_test中隨機抽取的,它的j是使用者u所有沒有評分過的電影集合,
    比如使用者u有1000部電影沒有評分,那麼這裡該使用者的測試集樣本就有1000個
    """
    for u in user_ratings.keys():
        t = []
        i = user_ratings_test[u]
        for j in range(1,item_count + 1):
            if not(j in user_ratings[u]):
                t.append([u,i,j])
        yield np.asarray(t)

模型構建
首先回憶一下我們需要學習的引數θ,其實就是使用者矩陣W(|U|×k)和物品矩陣H(|I|×k)對應的值,對於我們的模型來說,可以簡單理解為由id到embedding的轉化,因此有:

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))
item_emb_w = tf.get_variable("item_emb_w", [item_count + 1, hidden_dim],
                             initializer=tf.random_normal_initializer(0, 0.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)

回想一下我們要優化的目標,第一部分是ui和uj對應的預測值的評分之差,再經由sigmoid變換得到的[0,1]值,我們希望這個值越大越好,對於損失來說,當然是越小越好。因此,計算如下:

x = tf.reduce_sum(tf.multiply(u_emb,(i_emb-j_emb)),1,keep_dims=True)
loss1 = - tf.reduce_mean(tf.log(tf.sigmoid(x)))

第二部分是我們的正則項,引數就是我們的embedding值,所以正則項計算如下:

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.0001
bprloss = regulation_rate * l2_norm - tf.reduce_mean(tf.log(tf.sigmoid(x)))

train_op = tf.train.GradientDescentOptimizer(0.01).minimize(bprloss)

至此,我們整個模型就介紹完了,如果大家想要了解完整的程式碼實現,可以參考github喲。

3、總結

1.BPR是基於矩陣分解的一種排序演算法,它不是做全域性的評分優化,而是針對每一個使用者自己的商品喜好分貝做排序優化。
2.它是一種pairwise的排序演算法,對於每一個三元組<u,i,j>,模型希望能夠使使用者u對物品i和j的差異更明顯。
3.同時,引入了貝葉斯先驗,假設引數服從正態分佈,在轉換後變為了L2正則,減小了模型的過擬合。

參考文獻