在本系列的推薦系統遇上深度學習(十八)--探祕阿里之深度興趣網絡(DIN)淺析及實現中,我們介紹了阿里的深度興趣網絡(Deep Interest Network,以下簡稱DIN),時隔一年,阿里再次升級其模型,提出了深度興趣進化網絡(Deep Interest Evolution Network,以下簡稱DIEN,論文地址:https://arxiv.org/pdf/1809.03672.pdf),並將其應用於淘寶的廣告系統中,獲得了20.7%的CTR的提升。本篇,我們一同來探祕DIEN的原理及實現。
1、背景
在大多數非搜索電商場景下,用戶並不會實時表達目前的興趣偏好。因此通過設計模型來捕獲用戶的動態變化的興趣,是提升CTR預估效果的關鍵。阿里之前的DIN模型將用戶的歷史行爲來表示用戶的興趣,並強調了用戶興趣的多樣性和動態變化性,因此通過attention-based model來捕獲和目標物品相關的興趣。雖然DIN模型將用戶的歷史行爲來表示興趣,但存在兩個缺點: 1)用戶的興趣是不斷進化的,而DIN抽取的用戶興趣之間是獨立無關聯的,沒有捕獲到興趣的動態進化性 2)通過用戶的顯式的行爲來表達用戶隱含的興趣,這一準確性無法得到保證。
基於以上兩點,阿里提出了深度興趣演化網絡DIEN來CTR預估的性能。DIEN模型的主要貢獻點在於: 1)模型關注電商系統中興趣演化的過程,並提出了新的網絡結果來建模興趣進化的過程,這個模型能夠更精確的表達用戶興趣,同時帶來更高的CTR預估準確率。 2)設計了興趣抽取層,並通過計算一個輔助loss,來提升興趣表達的準確性。 3)設計了興趣進化層,來更加準確的表達用戶興趣的動態變化性。
接下來,我們來一起看一下DIEN模型的原理。
2、DIEN模型原理
2.1 模型總體結構
我們先來對比一下DIN和DIEN的結構。 DIN的模型結構如下:
DIEN的模型結構如下:
可以看到,DIN和DIEN的最底層都是Embedding Layer,User profile, target AD和context feature的處理方式是一致的。不同的是,DIEN將user behavior組織成了序列數據的形式,並把簡單的使用外積完成的activation unit變成了一個attention-based GRU網絡。
2.2 興趣抽取層Interest Extractor Layer
興趣抽取層Interest Extractor Layer的主要目標是從embedding數據中提取出interest。但一個用戶在某一時間的interest不僅與當前的behavior有關,也與之前的behavior相關,所以作者們使用GRU單元來提取interest。GRU單元的表達式如下:
這裏我們可以認爲ht是提取出的用戶興趣,但是這個地方興趣是否表示的合理呢?文中別出心裁的增加了一個輔助loss,來提升興趣表達的準確性:
這裏,作者設計了一個二分類模型來計算興趣抽取的準確性,我們將用戶下一時刻真實的行爲e(t+1)作爲正例,負採樣得到的行爲作爲負例e(t+1)',分別與抽取出的興趣h(t)結合輸入到設計的輔助網絡中,得到預測結果,並通過logloss計算一個輔助的損失:
2.3 興趣進化層Interest Evolution Layer
興趣進化層Interest Evolution Layer的主要目標是刻畫用戶興趣的進化過程。舉個簡單的例子:
以用戶對衣服的interest爲例,隨着季節和時尚風潮的不斷變化,用戶的interest也會不斷變化。這種變化會直接影響用戶的點擊決策。建模用戶興趣的進化過程有兩方面的好處: 1)追蹤用戶的interest可以使我們學習final interest的表達時包含更多的歷史信息。 2)可以根據interest的變化趨勢更好地進行CTR預測。
而interest在變化過程中遵循如下規律: 1)interest drift:用戶在某一段時間的interest會有一定的集中性。比如用戶可能在一段時間內不斷買書,在另一段時間內不斷買衣服。 2)interest individual:一種interest有自己的發展趨勢,不同種類的interest之間很少相互影響,例如買書和買衣服的interest基本互不相關。
爲了利用這兩個時序特徵,我們需要再增加一層GRU的變種,並加上attention機制以找到與target AD相關的interest。
attention的計算方式如下:
而Attention和GRU結合起來的機制有很多,文中介紹了一下三種:
GRU with attentional input (AIGRU) 這種方式將attention直接作用於輸入,無需修改GRU的結構:
Attention based GRU(AGRU) 這種方式需要修改GRU的結構,此時hidden state的輸出變爲:
GRU with attentional update gate (AUGRU) 這種方式需要修改GRU的結構,此時hidden state的輸出變爲:
2.4 模型試驗
文章在公共數據和自己的數據集上都做了實驗,並選取了不同的對比模型:
離線實驗的結果如下:
DIEN使用了輔助loss和AUGRU結構,而BaseModel + GRU + AUGRU與DIEN的不同之處就是沒有增加輔助loss。可以看到,DIEN的實驗效果遠好於其他模型。
3、DIEN模型實現
本文模型的實現參考代碼是:https://github.com/mouna99/dien 本文代碼的地址爲:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-DIEN-Demo 本文數據的地址爲:https://github.com/mouna99/dien
3.1 數據介紹
根據github中提供的數據,解壓後的文件如下: uid_voc.pkl: 用戶名對應的id mid_voc.pkl: item對應的id cat_voc.pkl:category對應的id item-info:item對應的category信息 reviews-info:用於進行負採樣的數據 local_train_splitByUser:訓練數據,一行格式爲:label、用戶名、目標item、 目標item類別、歷史item、歷史item對應類別。 local_test_splitByUser:測試數據,格式同訓練數據
3.2 代碼實現
本文的代碼主要包含以下幾個文件: rnn.py:對tensorflow中原始的rnn進行修改,目的是將attention同rnn進行結合。 vecAttGruCell.py: 對GRU源碼進行修改,將attention加入其中,設計AUGRU結構 data_iterator.py:數據迭代器,用於數據的不斷輸入 utils.py:一些輔助函數,如dice激活函數、attention score計算等 model.py:DIEN模型文件 train.py:模型的入口,用於訓練數據、保存模型和測試數據
好了,接下來我們介紹一些關鍵的代碼。
輸入數據介紹
輸入的數據有用戶id、target的item id、target item對應的cateid、用戶歷史行爲的item id list、用戶歷史行爲item對應的cate id list、歷史行爲的長度、歷史行爲的mask、目標值、負採樣的數據。
對於每一個用戶的歷史行爲,代碼中選取了5個樣本作爲負樣本。
self.mid_his_batch_ph = tf.placeholder(tf.int32,[None,None],name='mid_his_batch_ph') self.cat_his_batch_ph = tf.placeholder(tf.int32,[None,None],name='cat_his_batch_ph') self.uid_batch_ph = tf.placeholder(tf.int32,[None,],name='uid_batch_ph') self.mid_batch_ph = tf.placeholder(tf.int32,[None,],name='mid_batch_ph') self.cat_batch_ph = tf.placeholder(tf.int32,[None,],name='cat_batch_ph') self.mask = tf.placeholder(tf.float32,[None,None],name='mask') self.seq_len_ph = tf.placeholder(tf.int32,[None],name='seq_len_ph') self.target_ph = tf.placeholder(tf.float32,[None,None],name='target_ph') self.lr = tf.placeholder(tf.float64,[]) self.use_negsampling = use_negsampling if use_negsampling: self.noclk_mid_batch_ph = tf.placeholder(tf.int32, [None, None, None], name='noclk_mid_batch_ph') self.noclk_cat_batch_ph = tf.placeholder(tf.int32, [None, None, None], name='noclk_cat_batch_ph')
輸入數據轉換爲對應的embedding
接下來,輸入數據將轉換爲對應的embedding:
with tf.name_scope("Embedding_layer"): self.uid_embeddings_var = tf.get_variable("uid_embedding_var",[n_uid,EMBEDDING_DIM]) tf.summary.histogram('uid_embeddings_var', self.uid_embeddings_var) self.uid_batch_embedded = tf.nn.embedding_lookup(self.uid_embeddings_var,self.uid_batch_ph) self.mid_embeddings_var = tf.get_variable("mid_embedding_var",[n_mid,EMBEDDING_DIM]) tf.summary.histogram('mid_embeddings_var',self.mid_embeddings_var) self.mid_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var,self.mid_batch_ph) self.mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var,self.mid_his_batch_ph) if self.use_negsampling: self.noclk_mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var, self.noclk_mid_batch_ph) self.cat_embeddings_var = tf.get_variable("cat_embedding_var", [n_cat, EMBEDDING_DIM]) tf.summary.histogram('cat_embeddings_var', self.cat_embeddings_var) self.cat_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_batch_ph) self.cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_his_batch_ph) if self.use_negsampling: self.noclk_cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.noclk_cat_batch_ph)
接下來,將item的id對應的embedding 以及 item對應的cateid的embedding進行拼接,共同作爲item的embedding:
self.item_eb = tf.concat([self.mid_batch_embedded,self.cat_batch_embedded],1) self.item_his_eb = tf.concat([self.mid_his_batch_embedded,self.cat_his_batch_embedded],2) if self.use_negsampling: self.noclk_item_his_eb = tf.concat( [self.noclk_mid_his_batch_embedded[:, :, 0, :], self.noclk_cat_his_batch_embedded[:, :, 0, :]], -1) self.noclk_item_his_eb = tf.reshape(self.noclk_item_his_eb, [-1, tf.shape(self.noclk_mid_his_batch_embedded)[1], EMBEDDING_DIM * 2]) # 負採樣的item選第一個 self.noclk_his_eb = tf.concat([self.noclk_mid_his_batch_embedded, self.noclk_cat_his_batch_embedded], -1)
第一層GRU
接下來,我們要將用戶行爲歷史的item embedding輸入到dynamic rnn中,同時計算輔助loss:
with tf.name_scope('rnn_1'): rnn_outputs,_ = dynamic_rnn(GRUCell(HIDDEN_SIZE),inputs = self.item_his_eb,sequence_length=self.seq_len_ph,dtype=tf.float32,scope='gru1') tf.summary.histogram("GRU_outputs",rnn_outputs) aux_loss_1 = self.auxiliary_loss(rnn_outputs[:,:-1,:],self.item_his_eb[:,1:,:],self.noclk_item_his_eb[:,1:,:],self.mask[:,1:],stag="gru") self.aux_loss = aux_loss_1
輔助loss的計算其實是一個二分類模型,代碼如下:
def auxiliary_loss(self,h_states,click_seq,noclick_seq,mask,stag=None): mask = tf.cast(mask,tf.float32) click_input = tf.concat([h_states,click_seq],-1) noclick_input = tf.concat([h_states,noclick_seq],-1) click_prop_ = self.auxiliary_net(click_input,stag=stag)[:,:,0] noclick_prop_ = self.auxiliary_net(noclick_input,stag=stag)[:,:,0] click_loss_ = -tf.reshape(tf.log(click_prop_),[-1,tf.shape(click_seq)[1]]) * mask noclick_loss_ = - tf.reshape(tf.log(1.0 - noclick_prop_), [-1, tf.shape(noclick_seq)[1]]) * mask loss_ = tf.reduce_mean(click_loss_ + noclick_loss_) return loss_ def auxiliary_net(self,input,stag='auxiliary_net'): bn1 = tf.layers.batch_normalization(inputs=input, name='bn1' + stag, reuse=tf.AUTO_REUSE) dnn1 = tf.layers.dense(bn1, 100, activation=None, name='f1' + stag, reuse=tf.AUTO_REUSE) dnn1 = tf.nn.sigmoid(dnn1) dnn2 = tf.layers.dense(dnn1, 50, activation=None, name='f2' + stag, reuse=tf.AUTO_REUSE) dnn2 = tf.nn.sigmoid(dnn2) dnn3 = tf.layers.dense(dnn2, 2, activation=None, name='f3' + stag, reuse=tf.AUTO_REUSE) y_hat = tf.nn.softmax(dnn3) + 0.00000001 return y_hat
AUGRU
我們首先需要計算attention的score,然後將其作爲GRU的一部分輸入:
with tf.name_scope('Attention_layer_1'): att_outputs,alphas = din_fcn_attention(self.item_eb,rnn_outputs,ATTENTION_SIZE,self.mask, softmax_stag=1,stag='1_1',mode='LIST',return_alphas=True) tf.summary.histogram('alpha_outputs',alphas)
接下來,就是AUGRU的結構,這裏我們需要設計一個新的VecAttGRUCell結構,相比於GRUCell,修改的地方如下:
上圖中左側是GRU的源碼,右側是VecAttGRUCell的代碼,我們主要修改了call函數中的代碼,在GRU中,hidden state的計算爲:
new_h = u * state + (1 - u) * c
AUGRU中,hidden state的計算爲:
u = (1.0 - att_score) * u new_h = u * state + (1 - u) * c
代碼中給出的hidden state計算可能與文中有些出入,不過核心的思想都是,對於attention score大的,保存的當前的c就多一些。
設計好了新的GRU Cell,我們就能計算興趣的進化過程:
with tf.name_scope('rnn_2'): rnn_outputs2,final_state2 = dynamic_rnn(VecAttGRUCell(HIDDEN_SIZE),inputs=rnn_outputs, att_scores=tf.expand_dims(alphas,-1), sequence_length = self.seq_len_ph,dtype=tf.float32, scope="gru2" ) tf.summary.histogram("GRU2_Final_State",final_state2)
得到興趣進化的結果final_state2之後,需要與其他的embedding進行拼接,得到全聯接層的輸入:
inp = tf.concat([self.uid_batch_embedded,self.item_eb,self.item_his_eb_sum,self.item_eb * self.item_his_eb_sum,final_state2],1)
全聯接層得到最終輸出
最後我們通過一個多層神經網絡,得到最終的ctr預估值:
def build_fcn_net(self,inp,use_dice=False): bn1 = tf.layers.batch_normalization(inputs=inp,name='bn1') dnn1 = tf.layers.dense(bn1,200,activation=None,name='f1') if use_dice: dnn1 = dice(dnn1,name='dice_1') else: dnn1 = prelu(dnn1,'prelu1') dnn2 = tf.layers.dense(dnn1,80,activation=None,name='f2') if use_dice: dnn2 = dice(dnn2,name='dice_2') else: dnn2 = prelu(dnn2,name='prelu2') dnn3 = tf.layers.dense(dnn2,2,activation=None,name='f3') self.y_hat = tf.nn.softmax(dnn3) + 0.00000001 with tf.name_scope('Metrics'): ctr_loss = -tf.reduce_mean(tf.log(self.y_hat) * self.target_ph) self.loss = ctr_loss if self.use_negsampling: self.loss += self.aux_loss tf.summary.scalar('loss',self.loss) self.optimizer = tf.train.AdamOptimizer(learning_rate=self.lr).minimize(self.loss) self.accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.round(self.y_hat),self.target_ph),tf.float32)) tf.summary.scalar('accuracy',self.accuracy) self.merged = tf.summary.merge_all()
這樣,一個DIEN的模型就設計好了,其中的細節還是很多的,希望大家都能動手實現一下!
參考文獻
1、https://blog.csdn.net/friyal/article/details/83115900 2、https://arxiv.org/pdf/1809.03672.pdf 3、https://github.com/mouna99/dien