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