推薦系統遇上深度學習(二十三)--大一統信息檢索模型IRGAN在推薦領域的應用

1、引言

信息檢索領域的一個重要任務就是針對用戶的一個請求query,返回一組排好序的召回列表。

經典的IR流派認爲query和document之間存在着一種生成過程,即q -> d 。舉一個例子,搜索“哈登”,我們可以聯想到“保羅”,“火箭”,“MVP”等等,每一個聯想出來的document有一個生成概率p(d|q),然後根據這個生成概率進行排序,這種模型被稱作生成模型。人們在研究生成模型的時候,設計了一系列基於query和document的特徵,比方說TF-IDF,BM25。這些特徵能非常客觀的描述query和document的相關性,但沒有考慮document的質量,用戶的反饋,pagerank等信息。

現代的IR流派則利用了機器學習,將query和document的特徵放在一起,通過機器學習方法來計算query和document之間的匹配相關性: r=f(q,d)。舉個現實的例子,我們知道“小白”更喜歡“吃雞”而不是“王者榮耀”,pointwise會優化f(小白,吃雞)=1,f(小白,王者榮耀)=0;pairwise會優化f(小白,吃雞)>f(小白,王者榮耀);listwise會考慮很多其他遊戲,一起進行優化。機器學習的判別模型能夠很好地利用文本統計信息,用戶點擊信息等特徵,但模型本身侷限於標註數據的質量和大小,模型常常會在訓練數據上過擬合,或陷入某一個局部最優解。

受到GAN的啓發,將生成模型和判別模型結合在一起,學者們便提出了IRGAN模型。

2、IRGAN介紹

定義問題

假定我們又一些列的query{q1,...qN}並且有一系列的文檔document結合{d1,...dM},對於一個特定的query,我們有一系列標記的真實相關的文檔,但是這個數量是遠遠小於文檔總數量M的。query和document之間潛在的概率分佈可以表示爲條件概率分佈ptrue(d|q,r)。給定一堆從真實條件分佈ptrue(d|q,r)觀察到的樣本, 我們可以定義兩種類型的IR model。

生成式檢索模型:該模型的目標是學習pθ(d|q,r),使其更接近於ptrue(d|q,r)。

判別式檢索模型:該模型的目標是學習fΦ(q,d),即儘量能夠準確的判別q和d的相關程度

因此,受到GAN的啓發,我們將上述的兩種IR模型結合起來做一個最大最小化的博弈:生成式模型的任務是儘可能的產生和query相關的document,以此來混淆判別式模型;判別式模型的任務是儘可能準確區分真正相關的document和生成模型生成的document,因此,我們總體的目標就是:

在上式中,生成式模型G爲pθ(d|qn,r),生成式模型D對d是否與q相關進行判定,通過下面的式子給出相關性得分:

優化判別模型D

判別器的主要目標是最大化我們的對數似然,即正確的區分真正相關的文檔和生成器生成的文檔。最優的參數通過下面的式子得到:

優化生成模型G

生成器的主要目標是產生能夠混淆判別器的document,判別器直接從給定的document池中選擇document。在固定判別器參數fΦ(q,d)的情況下,生成器的學習目標是(第一項不包含θ,因此可以省略):

我們把生成器的優化目標寫作JG(qn)。

由於生成的document是離散的,無法直接通過梯度下降法進行優化,一種通常的做法是使用強化學習中的策略梯度方法,我們將qn作爲state,pθ(d|qn,r)作爲對應的策略,而log(1+exp(fΦ(d,qn))作爲對應的reward:

其中,第二步到第三步的變換利用了log函數求導的性質,而在最後一步則基於採樣的document做了一個近似。

總體流程

IRGAN的整體訓練流程如下:

Pair-wise的情況

在很多IR問題中,我們的數據是對一個query的一系列排序文檔對,因爲相比判斷一個文檔的相關性,更容易判斷用戶對一對文檔的相對偏好(比如說通過點擊數據,如果兩篇document同時展示給用戶,用戶點擊了a而沒有點擊b,則可以說明用戶對a的偏好大於對b的偏好),此外,如果我們使用相關性進行分級(用來表明不同文檔對同一個query的匹配程度)而不是使用是否相關,訓練數據也可以自然的表示成有序的文檔對。

IRGAN在pairwise情況下是同樣適用的,假設我有一堆帶標記的document組合Rn = {<di,dj>|di > dj}。生成器G的任務是儘量生成正確的排序組合來混淆判別器D,判別器D的任務是儘可能區分真正的排序組合和生成器生成的排序組合。基於下面的式子來進行最大最小化博弈:

其中,o=<du,dv>,o'=<d'u,d'v>分別代表正確的組合和生成器生成的組合。而D(du,dv|q)計算公式如下:

接下來我們就來講一下生成器的生成策略。首先我們選擇一個正確的組合 <di,dj>,我們首先選取dj,然後根據當前的生成器G的策略pθ(d|q,r),選擇比dj生成概率大的dk,組成一組<dk,dj>。

有關更多的IRGAN的細節,大家可以閱讀原論文,接下來,我們來看一個簡單的Demo吧。

3、IRGAN的TF實現

本文的github代碼參考: https://github.com/geek-ai/irgan/tree/master/item_recommendation

源代碼是python2.7版本的,修改爲python3版本的代碼之後存放地址爲: https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-IRGAN-Demo

數據

先來說說數據吧,數據用的是ml-100k的數據,每一行的格式爲“uid iid score",我們把評分大於等於4分的電影作爲用戶真正感興趣的電影。

Generator

對於訓練Generator,我們需要輸入的有三部分:uid,iid以及reward,我們首先定義user和item的embedding,然後獲取uid和iid的item。同時,我們這裏還給每個item定義了一個特徵值:

self.user_embeddings = tf.Variable(tf.random_uniform([self.userNum,self.emb_dim],
                                                     minval=-initdelta,maxval=self.initdelta,
                                                     dtype =tf.float32))
self.item_embeddings = tf.Variable(tf.random_uniform([self.itemNum,self.emb_dim],
                                                     minval=-initdelta,maxval=self.initdelta,
                                                     dtype=tf.float32))
self.item_bias = tf.Variable(tf.zeros([self.itemNum]))

self.u = tf.placeholder(tf.int32)
self.i = tf.placeholder(tf.int32)
self.reward = tf.placeholder(tf.float32)

self.u_embedding = tf.nn.embedding_lookup(self.user_embeddings,self.u)
self.i_embedding = tf.nn.embedding_lookup(self.item_embeddings,self.i)
self.i_bias = tf.gather(self.item_bias,self.i)

接下來,我們需要計算傳入的user和item之間的相關性,並通過傳入的reward來更新我們的策略:

self.all_logits = tf.reduce_sum(tf.multiply(self.u_embedding,self.item_embeddings),1) + self.item_bias
self.i_prob = tf.gather(
    tf.reshape(tf.nn.softmax(tf.reshape(self.all_logits, [1, -1])), [-1]),
    self.i)

self.gan_loss = -tf.reduce_mean(tf.log(self.i_prob) * self.reward) + self.lamda * (
    tf.nn.l2_loss(self.u_embedding) + tf.nn.l2_loss(self.i_embedding) + tf.nn.l2_loss(self.i_bias)
)

g_opt = tf.train.GradientDescentOptimizer(self.learning_rate)
self.gan_updates = g_opt.minimize(self.gan_loss,var_list=self.g_params)

Discriminator

傳入D的同樣有三部分,分別是uid,iid以及label值,與G一樣,我們也首先得到embedding值:

self.user_embeddings = tf.Variable(tf.random_uniform([self.userNum,self.emb_dim],
                                                    minval=-self.initdelta,maxval=self.initdelta,
                                                    dtype=tf.float32))
self.item_embeddings = tf.Variable(tf.random_uniform([self.itemNum,self.emb_dim],
                                                    minval=-self.initdelta,maxval=self.initdelta,
                                                    dtype=tf.float32))
self.item_bias = tf.Variable(tf.zeros(self.itemNum))

self.u = tf.placeholder(tf.int32)
self.i = tf.placeholder(tf.int32)
self.label = tf.placeholder(tf.float32)

self.u_embedding = tf.nn.embedding_lookup(self.user_embeddings,self.u)
self.i_embedding = tf.nn.embedding_lookup(self.item_embeddings,self.i)
self.i_bias = tf.gather(self.item_bias,self.i)

隨後,我們通過對數損失函數來更新D:

self.pre_logits = tf.reduce_sum(tf.multiply(self.u_embedding, self.i_embedding), 1) + self.i_bias
self.pre_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels = self.label,
                                                        logits = self.pre_logits) + self.lamda * (
    tf.nn.l2_loss(self.u_embedding) + tf.nn.l2_loss(self.i_embedding) + tf.nn.l2_loss(self.i_bias)
)

d_opt = tf.train.GradientDescentOptimizer(self.learning_rate)
self.d_updates = d_opt.minimize(self.pre_loss,var_list=self.d_params)

D中還有很重要的一步就是,計算reward:

self.reward_logits = tf.reduce_sum(tf.multiply(self.u_embedding,self.i_embedding),1) + self.i_bias
self.reward = 2 * (tf.sigmoid(self.reward_logits) - 0.5)

模型訓練

我們的G和D是交叉訓練的,D的訓練過程如下,每隔5輪,我們就要調用generate_for_d函數產生一批新的訓練樣本。

for d_epoch in range(100):
    if d_epoch % 5 == 0:
        generate_for_d(sess,generator,DIS_TRAIN_FILE)
        train_size = ut.file_len(DIS_TRAIN_FILE)
    index = 1
    while True:
        if index > train_size:
            break
        if index + BATCH_SIZE <= train_size + 1:
            input_user,input_item,input_label = ut.get_batch_data(DIS_TRAIN_FILE,index,BATCH_SIZE)
        else:
            input_user,input_item,input_label = ut.get_batch_data(DIS_TRAIN_FILE,index,train_size-index+1)
        index += BATCH_SIZE

        _ = sess.run(discriminator.d_updates,feed_dict={
            discriminator.u:input_user,discriminator.i:input_item,discriminator.label:input_label
        })

generate_for_d函數形式如下,其根據G的策略,生成一批樣本。

def generate_for_d(sess,model,filename):
    data = []
    for u in user_pos_train:
        pos = user_pos_train[u]

        rating = sess.run(model.all_rating,{model.u:[u]})
        rating = np.array(rating[0]) / 0.2
        exp_rating = np.exp(rating)
        prob = exp_rating / np.sum(exp_rating)

        neg = np.random.choice(np.arange(ITEM_NUM),size=len(pos),p=prob)
        # 1:1 的正負樣本
        for i in range(len(pos)):
            data.append(str(u) + '\t' + str(pos[i]) + '\t' + str(neg[i]))

    with open(filename,'w') as fout:
        fout.write('\n'.join(data))

G的訓練過程首先要通過D得到對應的reward,隨後更新自己的策略:

for g_epoch in range(50):
    for u in user_pos_train:
        sample_lambda = 0.2
        pos = user_pos_train[u]

        rating = sess.run(generator.all_logits,{generator.u:u})
        exp_rating = np.exp(rating)
        prob = exp_rating / np.sum(exp_rating)

        pn = (1-sample_lambda) * prob
        pn[pos] += sample_lambda * 1.0 / len(pos)

        sample = np.random.choice(np.arange(ITEM_NUM), 2 * len(pos), p=pn)

        reward = sess.run(discriminator.reward, {discriminator.u: u, discriminator.i: sample})
        reward = reward * prob[sample] / pn[sample]

        _ = sess.run(generator.gan_updates,
                     {generator.u: u, generator.i: sample, generator.reward: reward})

參考文獻: 1、論文地址:https://arxiv.org/abs/1705.10513 2、https://github.com/geek-ai/irgan/tree/master/item_recommendation

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