1. 程式人生 > 實用技巧 >[阿里DIN] 深度興趣網路原始碼分析 之 整體程式碼結構

[阿里DIN] 深度興趣網路原始碼分析 之 整體程式碼結構

[阿里DIN] 深度興趣網路原始碼分析 之 整體程式碼結構

目錄

0x00 摘要

Deep Interest Network(DIN)是阿里媽媽精準定向檢索及基礎演算法團隊在2017年6月提出的。其針對電子商務領域(e-commerce industry)的CTR預估,重點在於充分利用/挖掘使用者歷史行為資料中的資訊。

本文為系列第三篇,將分析DIN原始碼整體思路。採用的是 https://github.com/mouna99/dien 中的實現。

因為此專案包括 DIN,DIEN 等幾個模型,所以部分檔案是 DIEN 模型使用,這裡也順帶提一下,後續會有專門文章講解。

0x01 檔案簡介

資料檔案主要包括:

  • uid_voc.pkl:使用者字典,使用者名稱對應的id;
  • mid_voc.pkl:movie字典,item對應的id;
  • cat_voc.pkl:種類字典,category對應的id;
  • item-info:item對應的category資訊;
  • reviews-info:review 元資料,格式為:userID,itemID,評分,時間戳,用於進行負取樣的資料;
  • local_train_splitByUser:訓練資料,一行格式為:label、使用者名稱、目標item、 目標item類別、歷史item、歷史item對應類別;
  • local_test_splitByUser:測試資料,格式同訓練資料;

程式碼主要包含:

  • rnn.py:對tensorflow中原始的rnn進行修改,目的是將attention同rnn進行結合
  • vecAttGruCell.py: 對GRU原始碼進行修改,將attention加入其中,設計AUGRU結構
  • data_iterator.py: 資料迭代器,用於資料的不斷輸入
  • utils.py:一些輔助函式,如dice啟用函式、attention score計算等
  • model.py:DIEN模型檔案
  • train.py:模型的入口,用於訓練資料、儲存模型和測試資料

0x02 總體架構

DIN 試圖捕獲之前點選的 item 和目標 item 之間的不同相似性。

首先還是要從論文中摘取架構圖進行說明。

  • Deep Interest NetWork有以下幾點創新:

    1. 針對Diversity: 針對使用者廣泛的興趣,DIN用an interest distribution去表示,即用 Pooling(weighted sum)對Diversity建模(對使用者多種多樣的興趣建模)。
    2. 針對Local Activation:利用attention機制實現 Local Activation,從使用者歷史行為中動態學習使用者興趣的embedding向量,針對不同的廣告構造不同的使用者抽象表示,從而實現了在資料維度一定的情況下,更精準地捕捉使用者當前的興趣。對使用者歷史行為進行了不同的加權處理,針對不同的廣告,使用者歷史行為的權重不一致。即針對當前候選Ad,去區域性的啟用(Local Activate)相關的歷史興趣資訊。和當前候選Ad相關性越高的歷史行為,會獲得更高的attention score,從而會主導這一次預測
    3. CTR中特徵稀疏而且維度高,通常利用L1、L2、Dropout等手段防止過擬合。由於傳統L2正則計算的是全部引數,CTR預估場景的模型引數往往數以億計。DIN提出了一種正則化方法,在每次小批量迭代中,給與不同頻次的特徵不同的正則權重;
    4. 由於傳統的啟用函式,如Relu在輸入小於0時輸出為0,將導致許多網路節點的迭代速度變慢。PRelu雖然加快了迭代速度,但是其分割點預設為0,實際上分割點應該由資料決定。因此,DIN提出了一種資料動態自適應啟用函式Dice。
    5. 針對大規模稀疏資料的模型訓練:當DNN深度比較深(引數非常多),輸入又非常稀疏的時候,很容易過擬合。DIN提出Adaptive regularizaion來防止過擬合,效果顯著。

0x03 總體程式碼

DIN程式碼是從train.py開始。train.py 先用初始模型評估一遍測試集,然後呼叫 train:

  • 獲取 訓練資料 和 測試資料,這兩個都是資料迭代器,用於資料的不斷輸入
  • 根據 model_type 生成相應的model
  • 按照batch訓練,每1000次評估測試集。

程式碼如下:

def train(
        train_file = "local_train_splitByUser",
        test_file = "local_test_splitByUser",
        uid_voc = "uid_voc.pkl",
        mid_voc = "mid_voc.pkl",
        cat_voc = "cat_voc.pkl",
        batch_size = 128,
        maxlen = 100,
        test_iter = 100,
        save_iter = 100,
        model_type = 'DNN',
        seed = 2,
):

    with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess:
        ## 訓練資料
        train_data = DataIterator(train_file, uid_voc, mid_voc, cat_voc, batch_size, maxlen, shuffle_each_epoch=False)
        ## 測試資料
        test_data = DataIterator(test_file, uid_voc, mid_voc, cat_voc, batch_size, maxlen)
        n_uid, n_mid, n_cat = train_data.get_n()

        ......
        
        elif model_type == 'DIN':
            model = Model_DIN(n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE)
        elif model_type == 'DIEN':
            model = Model_DIN_V2_Gru_Vec_attGru_Neg(n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE)
            
        ......    

        sess.run(tf.global_variables_initializer())
        sess.run(tf.local_variables_initializer())

        iter = 0
        lr = 0.001
        for itr in range(3):
            loss_sum = 0.0
            accuracy_sum = 0.
            aux_loss_sum = 0.
            for src, tgt in train_data:
                uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, noclk_mids, noclk_cats = prepare_data(src, tgt, maxlen, return_neg=True)
                loss, acc, aux_loss = model.train(sess, [uids, mids, cats, mid_his, cat_his, mid_mask, target, sl, lr, noclk_mids, noclk_cats])
                loss_sum += loss
                accuracy_sum += acc
                aux_loss_sum += aux_loss
                iter += 1
                if (iter % test_iter) == 0:
                    eval(sess, test_data, model, best_model_path)
                    loss_sum = 0.0
                    accuracy_sum = 0.0
                    aux_loss_sum = 0.0
                if (iter % save_iter) == 0:
                    model.save(sess, model_path+"--"+str(iter))
            lr *= 0.5

0x04 模型基類

模型的基類是 Model,其建構函式__init__可以理解為 行為序列層(Behavior Layer):主要作用是將使用者瀏覽過的商品轉換成對應的embedding,並且按照瀏覽時間做排序,即把原始的id類行為序列特徵轉換成Embedding行為序列。

4.1 基本邏輯

基本邏輯如下:

  • 在 'Inputs' scope下,構建各種 placeholder 變數;
  • 在 'Embedding_layer' scope下,構建user, item的embedding lookup table,將輸入資料轉換為對應的embedding;
  • 把 各種 embedding vector 結合起來,比如將item的id對應的embedding 以及 item對應的cateid的embedding進行拼接,共同作為item的embedding;

4.2 模組分析

下面的 B 是 batch size,T 是序列長度,H 是hidden size,程式中初始化變數如下:

EMBEDDING_DIM = 18
HIDDEN_SIZE = 18 * 2
ATTENTION_SIZE = 18 * 2
best_auc = 0.0

4.2.1 構建變數

首先是構建placeholder變數。

with tf.name_scope('Inputs'):
    # shape: [B, T] #使用者行為特徵(User Behavior)中的 movie id 歷史行為序列。T為序列長度
    self.mid_his_batch_ph = tf.placeholder(tf.int32, [None, None], name='mid_his_batch_ph')
    # shape: [B, T] #使用者行為特徵(User Behavior)中的 category id 歷史行為序列。T為序列長度
    self.cat_his_batch_ph = tf.placeholder(tf.int32, [None, None], name='cat_his_batch_ph')
    # shape: [B],  user id 序列。 (B:batch size)
    self.uid_batch_ph = tf.placeholder(tf.int32, [None, ], name='uid_batch_ph')
    # shape: [B],  movie id 序列。 (B:batch size)
    self.mid_batch_ph = tf.placeholder(tf.int32, [None, ], name='mid_batch_ph')
    # shape: [B],  category id 序列。 (B:batch size)
    self.cat_batch_ph = tf.placeholder(tf.int32, [None, ], name='cat_batch_ph')
    self.mask = tf.placeholder(tf.float32, [None, None], name='mask')
    # shape: [B]; sl:sequence length,User Behavior中序列的真實序列長度(?)
    self.seq_len_ph = tf.placeholder(tf.int32, [None], name='seq_len_ph')
    # shape: [B, T], y: 目標節點對應的 label 序列, 正樣本對應 1, 負樣本對應 0
    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') #generate 3 item IDs from negative sampling.
        self.noclk_cat_batch_ph = tf.placeholder(tf.int32, [None, None, None], name='noclk_cat_batch_ph')

具體各種shape可以參見下面執行時變數

self = {Model_DIN_V2_Gru_Vec_attGru_Neg} 
 cat_batch_ph = {Tensor} Tensor("Inputs/cat_batch_ph:0", shape=(?,), dtype=int32)
 uid_batch_ph = {Tensor} Tensor("Inputs/uid_batch_ph:0", shape=(?,), dtype=int32)
 mid_batch_ph = {Tensor} Tensor("Inputs/mid_batch_ph:0", shape=(?,), dtype=int32)
  
 cat_his_batch_ph = {Tensor} Tensor("Inputs/cat_his_batch_ph:0", shape=(?, ?), dtype=int32)
 mid_his_batch_ph = {Tensor} Tensor("Inputs/mid_his_batch_ph:0", shape=(?, ?), dtype=int32)
  
 lr = {Tensor} Tensor("Inputs/Placeholder:0", shape=(), dtype=float64)
 mask = {Tensor} Tensor("Inputs/mask:0", shape=(?, ?), dtype=float32)
 seq_len_ph = {Tensor} Tensor("Inputs/seq_len_ph:0", shape=(?,), dtype=int32)
 target_ph = {Tensor} Tensor("Inputs/target_ph:0", shape=(?, ?), dtype=float32)

 noclk_cat_batch_ph = {Tensor} Tensor("Inputs/noclk_cat_batch_ph:0", shape=(?, ?, ?), dtype=int32)
 noclk_mid_batch_ph = {Tensor} Tensor("Inputs/noclk_mid_batch_ph:0", shape=(?, ?, ?), dtype=int32)

 use_negsampling = {bool} True

4.2.2 構建embedding

然後是構建user, item的embedding lookup table,將輸入資料轉換為對應的embedding,就是把稀疏特徵轉換為稠密特徵。關於 embedding 層的原理和程式碼分析,本系列會有專文講解

後續的 U 是user_id的hash bucket size,I 是item_id的hash bucket size,C 是cat_id的hash bucket size。

注意 self.mid_his_batch_ph這樣的變數 儲存使用者的歷史行為序列, 大小為 [B, T],所以在進行 embedding_lookup 時,輸出大小為 [B, T, H/2];

# Embedding layer
with tf.name_scope('Embedding_layer'):
    # shape: [U, H/2], user_id的embedding weight. U是user_id的hash bucket size,即user count
    self.uid_embeddings_var = tf.get_variable("uid_embedding_var", [n_uid, EMBEDDING_DIM])
    # 從uid embedding weight 中取出 uid embedding vector
    self.uid_batch_embedded = tf.nn.embedding_lookup(self.uid_embeddings_var, self.uid_batch_ph)

    # shape: [I, H/2], item_id的embedding weight. I是item_id的hash bucket size,即movie count
    self.mid_embeddings_var = tf.get_variable("mid_embedding_var", [n_mid, EMBEDDING_DIM])
    # 從mid embedding weight 中取出 uid embedding vector
    self.mid_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var, self.mid_batch_ph)
    # 從mid embedding weight 中取出 mid history embedding vector,是正樣本
    # 注意 self.mid_his_batch_ph這樣的變數 儲存使用者的歷史行為序列, 大小為 [B, T],所以在進行 embedding_lookup 時,輸出大小為 [B, T, H/2]; 
    self.mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var, self.mid_his_batch_ph)
    # 從mid embedding weight 中取出 mid history embedding vector,是負樣本
    if self.use_negsampling:
        self.noclk_mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var, self.noclk_mid_batch_ph)

    # shape: [C, H/2], cate_id的embedding weight. C是cat_id的hash bucket size
    self.cat_embeddings_var = tf.get_variable("cat_embedding_var", [n_cat, EMBEDDING_DIM])
    # 從 cid embedding weight 中取出 cid history embedding vector,是正樣本
    # 比如cat_embeddings_var 是(1601, 18),cat_batch_ph 是(?,),則cat_batch_embedded 就是 (?, 18)
    self.cat_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_batch_ph)
    # 從 cid embedding weight 中取出 cid embedding vector,是正樣本
    self.cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_his_batch_ph)
    # 從 cid embedding weight 中取出 cid history embedding vector,是負樣本
    if self.use_negsampling:
        self.noclk_cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.noclk_cat_batch_ph)

具體各種shape可以參見下面執行時變數

self = {Model_DIN_V2_Gru_Vec_attGru_Neg} 
 cat_embeddings_var = {Variable} <tf.Variable 'cat_embedding_var:0' shape=(1601, 18) dtype=float32_ref>
 uid_embeddings_var = {Variable} <tf.Variable 'uid_embedding_var:0' shape=(543060, 18) dtype=float32_ref>
 mid_embeddings_var = {Variable} <tf.Variable 'mid_embedding_var:0' shape=(367983, 18) dtype=float32_ref>
  
 cat_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_4:0", shape=(?, 18), dtype=float32)
 mid_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_1:0", shape=(?, 18), dtype=float32)
 uid_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup:0", shape=(?, 18), dtype=float32)

 cat_his_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_5:0", shape=(?, ?, 18), dtype=float32)
 mid_his_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_2:0", shape=(?, ?, 18), dtype=float32)

 noclk_cat_his_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_6:0", shape=(?, ?, ?, 18), dtype=float32)
 noclk_mid_his_batch_embedded = {Tensor} Tensor("Embedding_layer/embedding_lookup_3:0", shape=(?, ?, ?, 18), dtype=float32)

4.2.3 拼接embedding

這部分是把 各種 embedding vector 結合起來,比如將 item的id對應的embedding 以及 item對應的cateid的embedding 進行拼接,共同作為item的embedding;

關於shape的說明:

  • 注意上一步中,self.mid_his_batch_ph這樣的變數 儲存使用者的歷史行為序列, 大小為 [B, T],所以在進行 embedding_lookup 時,輸出大小為 [B, T, H/2]。
  • 這裡將 Goods 和 Cate 的 embedding 進行 concat, 得到 [B, T, H] 大小. 注意到 tf.concat 中的 axis 引數值為 2。

關於邏輯的說明:

第一步是 self.item_eb = tf.concat([self.mid_batch_embedded, self.cat_batch_embedded], 1) 即獲取一個 Batch 中目標節點對應的 embedding, 儲存在 i_emb 中, 它由商品 (Goods) 和類目 (Cate) embedding 進行 concatenation。比如 [[mid1, mid2] , [mid3, mid4]][[cid1, cid2], [cid3, cid4]],拼接得到 [[mid1, mid2,cid1, cid2] , [mid3, mid4,cid3, cid4]]

對應了架構圖的:

第二步是 self.item_his_eb = tf.concat([self.mid_his_batch_embedded, self.cat_his_batch_embedded], 2) 邏輯上是 對 兩個歷史矩陣 進行處理, 這兩個歷史矩陣儲存了使用者的歷史行為序列, 大小為 [B, T],所以在進行 embedding_lookup 時, 輸出大小為 [B, T, H/2]。之後將 Goods 和 Cate 的 embedding 進行 concat, 得到 [B, T, H] 大小. 注意到 tf.concat 中的 axis 引數值為 2。比如 [[[mid1, mid2]]][[[cid1, cid2]]],拼接得到 [[[mid1, mid2, cid1, cid2]]]

對應了架構圖的:

第三步是用 tf.reduce_sum(self.item_his_eb, 1) 按照第一維度求和,會降維。

比如 [[[mid1, mid2,cid1, cid2] , [mid3, mid4,cid3, cid4]]] 得到 [[mid1 + mid3, mid2 + mid4, cid1 + cid3, cid2 + cid4]]

具體程式碼如下:

# 正樣本的embedding拼接,正樣本包括item和cate。即將目標節點對應的商品 embedding 和類目 embedding 進行 concatenation
self.item_eb = tf.concat([self.mid_batch_embedded, self.cat_batch_embedded], 1)
# 將 Goods 和 Cate 的 embedding 進行 concat, 得到 [B, T, H] 大小. 注意到 tf.concat 中的 axis 引數值為 2
self.item_his_eb = tf.concat([self.mid_his_batch_embedded, self.cat_his_batch_embedded], 2)
# 按照第一維度求和,會降維
self.item_his_eb_sum = tf.reduce_sum(self.item_his_eb, 1)
# 舉例如下,item_eb是 (128, 36),item_his_eb是(128, ?, 36),這個是從真實資料讀取出來的,比如可能是 (128, 6, 36)。

# 負樣本的embedding拼接,負樣本包括item和cate。即將目標節點對應的商品 embedding 和類目 embedding 進行 concatenation
if self.use_negsampling:
    # 0 means only using the first negative item ID. 3 item IDs are inputed in the line 24.
    self.noclk_item_his_eb = tf.concat(   
        [self.noclk_mid_his_batch_embedded[:, :, 0, :], self.noclk_cat_his_batch_embedded[:, :, 0, :]], -1)
    # cat embedding 18 concate item embedding 18.
    self.noclk_item_his_eb = tf.reshape(self.noclk_item_his_eb,
                                        [-1, tf.shape(self.noclk_mid_his_batch_embedded)[1], 36])
    self.noclk_his_eb = tf.concat([self.noclk_mid_his_batch_embedded, self.noclk_cat_his_batch_embedded], -1)
    self.noclk_his_eb_sum_1 = tf.reduce_sum(self.noclk_his_eb, 2)
    self.noclk_his_eb_sum = tf.reduce_sum(self.noclk_his_eb_sum_1, 1)

具體各種shape可以參見下面執行時變數

self = {Model_DIN_V2_Gru_Vec_attGru_Neg} 
 item_eb = {Tensor} Tensor("concat:0", shape=(?, 36), dtype=float32)
 item_his_eb = {Tensor} Tensor("concat_1:0", shape=(?, ?, 36), dtype=float32)
 item_his_eb_sum = {Tensor} Tensor("Sum:0", shape=(?, 36), dtype=float32)
  
 noclk_item_his_eb = {Tensor} Tensor("Reshape:0", shape=(?, ?, 36), dtype=float32)
 noclk_his_eb = {Tensor} Tensor("concat_3:0", shape=(?, ?, ?, 36), dtype=float32)
 noclk_his_eb_sum = {Tensor} Tensor("Sum_2:0", shape=(?, 36), dtype=float32)
 noclk_his_eb_sum_1 = {Tensor} Tensor("Sum_1:0", shape=(?, ?, 36), dtype=float32)

0x05 Model_DIN

Model_DIN 是DIN實現的模型。

class Model_DIN(Model):
    def __init__(self, n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE, use_negsampling=False):
        super(Model_DIN, self).__init__(n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE,
                                           ATTENTION_SIZE,
                                           use_negsampling)

        # Attention layer
        with tf.name_scope('Attention_layer'):
            attention_output = din_attention(self.item_eb, self.item_his_eb, ATTENTION_SIZE, self.mask)
            att_fea = tf.reduce_sum(attention_output, 1)

        inp = tf.concat([self.uid_batch_embedded, self.item_eb, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum, att_fea], -1)
        # Fully connected layer
        self.build_fcn_net(inp, use_dice=True)

整體思路比較簡單:

  • Attention layer
  • Fully connected layer

具體分析如下。

5.1 Attention機制

Attention機制是 :將Source中的構成元素想象成是由一系列的< Key,Value >資料對構成,此時給定Target中的某個元素Query,通過計算Query和各個Key的相似性或者相關性,得到每個Key對應Value的權重係數,然後對Value進行加權求和,即得到了最終的Attention數值。所以本質上Attention機制是對Source中元素的Value值進行加權求和,而Query和Key用來計算對應Value的權重係數。即可以將其本質思想改寫為如下公式:

當然,從概念上理解,把Attention仍然理解為從大量資訊中有選擇地篩選出少量重要資訊並聚焦到這些重要資訊上,忽略大多不重要的資訊,這種思路仍然成立。聚焦的過程體現在權重係數的計算上,權重越大越聚焦於其對應的Value值上,即權重代表了資訊的重要性,而Value是其對應的資訊。

另外一種理解是:也可以將Attention機制看作一種軟定址(Soft Addressing):Source可以看作儲存器記憶體儲的內容,元素由地址Key和值Value組成,當前有個Key=Query的查詢,目的是取出儲存器中對應的Value值,即Attention數值。通過Query和儲存器內元素Key的地址進行相似性比較來定址,之所以說是軟定址,指的不像一般定址只從儲存內容裡面找出一條內容,而是可能從每個Key地址都會取出內容,取出內容的重要性根據Query和Key的相似性來決定,之後對Value進行加權求和,這樣就可以取出最終的Value值,也即Attention值。所以不少研究人員將Attention機制看作軟定址的一種特例,這也是非常有道理的。

至於Attention機制的具體計算過程,如果對目前大多數方法進行抽象的話,可以將其歸納為兩個過程:

  • 第一個過程是根據Query和Key計算權重係數;
  • 第二個過程根據權重係數對Value進行加權求和;

而第一個過程又可以細分為兩個階段:

  • 第一個小階段根據Query和Key計算兩者的相似性或者相關性;
  • 第二個小階段對第一小階段的原始分值進行歸一化處理;

這樣,可以將Attention的計算過程抽象為如圖展示的三個階段。

在第一個階段,可以引入不同的函式和計算機制,根據Query和某個Keyi,計算兩者的相似性或者相關性。最常見的方法包括:求兩者的向量點積、求兩者的向量Cosine相似性或者通過再引入額外的神經網路來求值,即如下方式:

第一階段產生的分值根據具體產生的方法不同其數值取值範圍也不一樣,第二階段引入類似SoftMax的計算方式對第一階段的得分進行數值轉換,一方面可以進行歸一化,將原始計算分值整理成所有元素權重之和為1的概率分佈;另一方面也可以通過SoftMax的內在機制更加突出重要元素的權重。即一般採用如下公式計算:

第二階段的計算結果ai即為Valuei對應的權重係數,然後進行加權求和即可得到Attention數值:

通過如上三個階段的計算,即可求出針對Query的Attention數值,目前絕大多數具體的注意力機制計算方法都符合上述的三階段抽象計算過程。

5.2 Attention實現

DIN中會對於使用者的行為序列,將其中每個item的所有field特徵concat後形成該item的臨時emb之後,不再是對序列中所有臨時item emb做簡單的sum pooling,而是對每個item emb計算和候選item emb的相關性權重,即activation unit模組。

這部分功能實現在attention中:

5.2.1 呼叫

如何呼叫:

attention_output = din_attention(self.item_eb, self.item_his_eb, ATTENTION_SIZE, self.mask)

其中,相關引數等:

  • query :候選廣告對應的 embedding,shape: [B, H], 即 i_emb;
  • facts :使用者歷史行為對應的 embedding,shape: [B, T, H], 即 h_emb;
  • mask : Batch中每個行為的真實意義,shape: [B, H],由於一個 Batch 中的使用者行為序列不一定都相同,但是輸入的keys維度是固定的(都是歷史行為最大的長度),其真實長度儲存在 self.sl 中,所以之前產生了 masks 來選擇真正的歷史行為,以告訴模型哪些行為是沒用的,哪些是用來計算使用者興趣分佈的;
  • B:batch size; T: 使用者歷史行為序列的最大長度;H:embedding size;
  • attention_output :輸出為使用者興趣的表徵;

引數變數動態如下 :

self = {Model_DIN_V2_Gru_Vec_attGru_Neg} 
 item_eb = {Tensor} Tensor("concat:0", shape=(?, 36), dtype=float32)
 item_his_eb = {Tensor} Tensor("concat_1:0", shape=(?, ?, 36), dtype=float32)
 mask = {Tensor} Tensor("Inputs/mask:0", shape=(?, ?), dtype=float32)

5.2.2 mask的作用

關於mask的作用,這裡結合 Transformer 再說一下:

mask 表示掩碼,它對某些值進行掩蓋,使其在引數更新時不產生效果。Transformer 模型裡面涉及兩種 mask,分別是 padding mask 和 sequence mask。其中,padding mask 在所有的 scaled dot-product attention 裡面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 裡面用到。

Padding Mask

什麼是 padding mask 呢?因為每個批次輸入序列長度是不一樣的也就是說,我們要對輸入序列進行對齊。具體來說,就是給在較短的序列後面填充 0。但是如果輸入的序列太長,則是擷取左邊的內容,把多餘的直接捨棄。因為這些填充的位置,其實是沒什麼意義的,所以attention機制不應該把注意力放在這些位置上,需要進行一些處理。

具體的做法是,把這些位置的值加上一個非常大的負數(負無窮),這樣的話,經過 softmax,這些位置的概率就會接近0!而我們的 padding mask 實際上是一個張量,每個值都是一個Boolean,值為 false 的地方就是我們要進行處理的地方。

Sequence mask

sequence mask 是為了使得 decoder 不能看見未來的資訊。也就是對於一個序列,在 time_step 為 t 的時刻,我們的解碼輸出應該只能依賴於 t 時刻之前的輸出,而不能依賴 t 之後的輸出。因此我們需要想一個辦法,把 t 之後的資訊給隱藏起來。

那麼具體怎麼做呢?也很簡單:產生一個上三角矩陣,上三角的值全為0。把這個矩陣作用在每一個序列上,就可以達到我們的目的。

對於 decoder 的 self-attention,裡面使用到的 scaled dot-product attention,同時需要padding mask 和 sequence mask 作為 attn_mask,具體實現就是兩個mask相加作為attn_mask。

其他情況,attn_mask 一律等於 padding mask。

DIN這裡使用的是padding mask

5.2.3 基本邏輯

程式碼經過以下幾個步驟得到使用者的興趣分佈,可以理解為,一個query過來了,先根據此query和一系列候選物的key(facts) 計算相似度,然後根據相似度計算候選物的具體value:

  • 如果time_major,則會進行轉換:(T,B,D) => (B,T,D);
  • 轉換mask。
    • 使用 tf.ones_like(mask) 構建一個和mask維度一樣,元素都是 1 的張量;
    • 使用 tf.equal 把mask從 int 型別轉成 bool 型別。tf.equal作用是判斷兩個輸入是否相等,相等是True,不等就是False;
  • 轉換query維度,將query變為和 facts 同樣的形狀B * T * H;這裡 T 隨著每個具體訓練資料不同而不同,比如某一個使用者的某一個時間序列長度是5,另一個時間序列是15;
    • query是[B, H],轉換到 queries 維度為(B, T, H)。
    • 為了讓pos_item和使用者行為序列中每個元素計算權重。這裡是用了 tf.tile(query, [1, tf.shape(facts)[1]])。tf.shape(keys)[1] 結果就是 T,query是[B, H],經過 tile,就是把第一維按照 T 展開,得到[B, T * H] ;
    • 把 queries 進行 reshape ,轉換成和 facts 相同的大小: [B, T, H];
  • 在MLP之前多做一些捕獲行為item和候選item之間關係的操作:加減乘除等。然後得到了Local Activation Unit 的輸入。即 候選廣告 queries 對應的 emb,使用者歷史行為序列 facts 對應的 embed,再加上它們之間的交叉特徵, 進行 concat 後的結果;
  • attention操作,目的是計算query和key的相關程度。具體是通過三層神經網路得到queries和facts 中每個key的權重,這個DNN 網路的輸出節點為 1;
    • 最後一步 d_layer_3_all 的 shape 為 [B, T, 1];
    • 然後 reshape 為 [B, 1, T],axis=2 這一維表示 T 個使用者行為序列分別對應的權重引數;
    • attention的輸出, [B, 1, T];
  • 得到有真實意義的score;
    • 使用 key_masks = tf.expand_dims(mask, 1) 把mask擴充套件維度,從 [B, T] 擴充套件到 [B, 1, T];
    • 使用 tf.ones_like(scores) 構建一個和scores維度一樣,元素都是 1 的張量;
    • padding的mask後補一個很小的負數,這樣後面計算 softmax 時, e^{x} 結果就約等於 0;
    • 進行 [B, 1, T] padding操作。為了忽略了padding對總體的影響,程式碼中利用tf.where將padding的向量(每個樣本序列中空缺的商品)權重置為極小值(-2 ** 32 + 1),而不是0;
    • 利用 tf.where(key_masks, scores, paddings) 來得到真正有意義的score;
  • Scale 是 attention的標準操作,做完scaled後再送入softmax得到最終的權重。但是程式碼中沒有用這部分,登出掉了;
  • 經過softmax進行標準化,得到歸一化後的權重;
  • 這裡已經得到了正確的權重 scores 以及使用者歷史行為序列 facts,所以通過weighted sum得到終端使用者的興趣表徵;
    • 如果是 SUM mode,則進行矩陣相乘得到使用者的興趣表徵;具體是scores 的大小為 [B, 1, T], 表示每條歷史行為的權重,facts 為歷史行為序列, 大小為 [B, T, H],兩者用矩陣乘法做, 得到的結果 output 就是 [B, 1, H]。
    • 否則 進行哈達碼相乘
      • 首先把 scores 進行reshape,從 [B, 1, H] 變化成 Batch * Time;
      • 並且用expand_dims來把scores在最後增加一維;
      • 然後進行哈達碼積,[B, T, H] x [B, T, 1] = [B, T, H];
      • 最後 reshape 成 Batch * Time * Hidden Size;

具體程式碼如下:

def din_attention(query, facts, attention_size, mask, stag='null', mode='SUM', softmax_stag=1, time_major=False, return_alphas=False):
    '''
    query :候選廣告,shape: [B, H], 即i_emb;
    facts :使用者歷史行為,shape: [B, T, H], 即h_emb,T是padding後的長度,每個長H的emb代表一個item;
    mask : Batch中每個行為的真實意義,shape: [B, H];    
    '''     
    if isinstance(facts, tuple):
        # In case of Bi-RNN, concatenate the forward and the backward RNN outputs.
        facts = tf.concat(facts, 2)
        print ("querry_size mismatch")
        query = tf.concat(values = [
        query,
        query,
        ], axis=1)

    if time_major:
        # (T,B,D) => (B,T,D)
        facts = tf.array_ops.transpose(facts, [1, 0, 2])  
      
    # 轉換mask  
    mask = tf.equal(mask, tf.ones_like(mask))
    facts_size = facts.get_shape().as_list()[-1]  # D value - hidden size of the RNN layer,
    querry_size = query.get_shape().as_list()[-1] # H,這裡是36
    
    # 1. 轉換query維度,變成歷史維度T 
    # query是[B, H],轉換到 queries 維度為(B, T, H),為了讓pos_item和使用者行為序列中每個元素計算權重
    # 此時query是 Tensor("concat:0", shape=(?, 36), dtype=float32)
    # tf.shape(keys)[1] 結果就是 T,query是[B, H],經過tile,就是把第一維按照 T 展開,得到[B, T * H] 
    queries = tf.tile(query, [1, tf.shape(facts)[1]]) # [B, T * H], 想象成貼瓷磚
    # 此時 queries 是 Tensor("Attention_layer/Tile:0", shape=(?, ?), dtype=float32)
    # queries 需要 reshape 成和 facts 相同的大小: [B, T, H]
    queries = tf.reshape(queries, tf.shape(facts)) # [B, T * H] -> [B, T, H]
    # 此時 queries 是 Tensor("Attention_layer/Reshape:0", shape=(?, ?, 36), dtype=float32) 
    
    # 2. 這部分目的就是為了在MLP之前多做一些捕獲行為item和候選item之間關係的操作:加減乘除等。
    # 得到 Local Activation Unit 的輸入。即 候選廣告 queries 對應的 emb,使用者歷史行為序列 facts
    # 對應的 embed, 再加上它們之間的交叉特徵, 進行 concat 後的結果
    din_all = tf.concat([queries, facts, queries-facts, queries*facts], axis=-1) # T*[B,H] ->[B, T, H]
    
    # 3. attention操作,通過幾層MLP獲取權重,這個DNN 網路的輸出節點為 1
    d_layer_1_all = tf.layers.dense(din_all, 80, activation=tf.nn.sigmoid, name='f1_att' + stag)
    d_layer_2_all = tf.layers.dense(d_layer_1_all, 40, activation=tf.nn.sigmoid, name='f2_att' + stag)
    d_layer_3_all = tf.layers.dense(d_layer_2_all, 1, activation=None, name='f3_att' + stag)
  	# 上一層 d_layer_3_all 的 shape 為 [B, T, 1]
 	  # 下一步 reshape 為 [B, 1, T], axis=2 這一維表示 T 個使用者行為序列分別對應的權重引數    
    d_layer_3_all = tf.reshape(d_layer_3_all, [-1, 1, tf.shape(facts)[1]])
    scores = d_layer_3_all # attention的輸出, [B, 1, T]
    
    # 4. 得到有真實意義的score
    # key_masks = tf.sequence_mask(facts_length, tf.shape(facts)[1])   # [B, T]
    key_masks = tf.expand_dims(mask, 1) # [B, 1, T]
    # padding的mask後補一個很小的負數,這樣後面計算 softmax 時, e^{x} 結果就約等於 0
    paddings = tf.ones_like(scores) * (-2 ** 32 + 1) # 注意初始化為極小值
    # [B, 1, T] padding操作,為了忽略了padding對總體的影響,程式碼中利用tf.where將padding的向量(每個樣本序列中空缺的商品)權重置為極小值(-2 ** 32 + 1),而不是0
    scores = tf.where(key_masks, scores, paddings)  # [B, 1, T]

    # 5. Scale # attention的標準操作,做完scaled後再送入softmax得到最終的權重。
    # scores = scores / (facts.get_shape().as_list()[-1] ** 0.5)

    # 6. Activation,得到歸一化後的權重
    if softmax_stag:
        scores = tf.nn.softmax(scores)  # [B, 1, T]

    # 7. 得到了正確的權重 scores 以及使用者歷史行為序列 facts, 再進行矩陣相乘得到使用者的興趣表徵
    # Weighted sum,
    if mode == 'SUM':
        # scores 的大小為 [B, 1, T], 表示每條歷史行為的權重,
        # facts 為歷史行為序列, 大小為 [B, T, H];
        # 兩者用矩陣乘法做, 得到的結果 output 就是 [B, 1, H]
        # B * 1 * H 三維矩陣相乘,相乘發生在後兩維,即 B * (( 1 * T ) * ( T * H ))
        # 這裡的output是attention計算出來的權重,即論文公式(3)裡的w,
        output = tf.matmul(scores, facts)  # [B, 1, H]
        # output = tf.reshape(output, [-1, tf.shape(facts)[-1]])
    else:
        # 從 [B, 1, H] 變化成 Batch * Time
        scores = tf.reshape(scores, [-1, tf.shape(facts)[1]]) 
        # 先把scores在最後增加一維,然後進行哈達碼積,[B, T, H] x [B, T, 1] =  [B, T, H]
        output = facts * tf.expand_dims(scores, -1) 
        output = tf.reshape(output, tf.shape(facts)) # Batch * Time * Hidden Size
    return output

程式執行時候的變數如下:

attention_size = {int} 36
d_layer_1_all = {Tensor} Tensor("Attention_layer/f1_attnull/Sigmoid:0", shape=(?, ?, 80), dtype=float32)
d_layer_2_all = {Tensor} Tensor("Attention_layer/f2_attnull/Sigmoid:0", shape=(?, ?, 40), dtype=float32)
d_layer_3_all = {Tensor} Tensor("Attention_layer/Reshape_1:0", shape=(?, 1, ?), dtype=float32)
din_all = {Tensor} Tensor("Attention_layer/concat:0", shape=(?, ?, 144), dtype=float32)
facts = {Tensor} Tensor("concat_1:0", shape=(?, ?, 36), dtype=float32)
facts_size = {int} 36
key_masks = {Tensor} Tensor("Attention_layer/ExpandDims:0", shape=(?, 1, ?), dtype=bool)
mask = {Tensor} Tensor("Attention_layer/Equal:0", shape=(?, ?), dtype=bool)
mode = {str} 'SUM'
output = {Tensor} Tensor("Attention_layer/MatMul:0", shape=(?, 1, 36), dtype=float32)
paddings = {Tensor} Tensor("Attention_layer/mul_1:0", shape=(?, 1, ?), dtype=float32)
queries = {Tensor} Tensor("Attention_layer/Reshape:0", shape=(?, ?, 36), dtype=float32)
querry_size = {int} 36
query = {Tensor} Tensor("concat:0", shape=(?, 36), dtype=float32)
return_alphas = {bool} False
scores = {Tensor} Tensor("Attention_layer/Reshape_3:0", shape=(?, 1, ?), dtype=float32)
softmax_stag = {int} 1
stag = {str} 'null'
time_major = {bool} False

0x06 全連線層

現在我們得到了連線後的稠密表示向量,接下來就是利用全連通層自動學習特徵之間的非線性關係組合。

於是通過一個多層神經網路,得到最終的ctr預估值,這部分就是一個函式呼叫。

# Attention layers
with tf.name_scope('Attention_layer'):
  attention_output = din_attention(self.item_eb, self.item_his_eb, ATTENTION_SIZE, self.mask)
  att_fea = tf.reduce_sum(attention_output, 1)
  tf.summary.histogram('att_fea', att_fea)
  inp = tf.concat([self.uid_batch_embedded, self.item_eb, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum, att_fea], -1)
# Fully connected layer
self.build_fcn_net(inp, use_dice=True) # 呼叫多層神經網路

對應論文中的:

這個多層神經網路包含了多個全連線層,全連線層本質就是由一個特徵空間線性變換到另一個特徵空間。目標空間的任一維——也就是隱層的一個 cell——都認為會受到源空間的每一維的影響。可以不嚴謹的說,目標向量是源向量的加權和。

其中邏輯如下 :

  • 首先進行Batch Normalization;
  • 加入一個全連線層 tf.layers.dense(bn1, 200, activation=None, name='f1')
  • 用 dice 或者 prelu 進行啟用;
  • 加入一個全連線層 tf.layers.dense(dnn1, 80, activation=None, name='f2')
  • 用 dice 或者 prelu 進行啟用;
  • 加入一個全連線層 tf.layers.dense(dnn2, 2, activation=None, name='f3')
  • 得到輸出 y_hat = tf.nn.softmax(dnn3) + 0.00000001
  • 進行交叉熵和optimizer初始化;
    • 得到交叉熵- tf.reduce_mean(tf.log(self.y_hat) * self.target_ph)
    • 如果有負取樣,需要加上輔助損失;
    • 使用 AdamOptimizer,tf.train.AdamOptimizer(learning_rate=self.lr).minimize(self.loss),這樣後續就會通過這個minimize來進行優化;
  • 計算 Accuracy;

具體程式碼參見如下:

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, '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'):
        # Cross-entropy loss and optimizer initialization
        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)

        # Accuracy metric
        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()

0x07 訓練模型

通過 model.train 來訓練模型。

model.train 的輸入資料有:

  • 使用者id;
  • target的item id;
  • target item對應的cateid;
  • 使用者歷史行為的item id list;
  • 使用者歷史行為item對應的cate id list;
  • 歷史行為的mask;
  • 目標值;
  • 歷史行為的長度;
  • learning rate;
  • 負取樣的資料;

train程式碼具體如下:

def train(self, sess, inps):
    if self.use_negsampling:
        loss, accuracy, aux_loss, _ = sess.run([self.loss, self.accuracy, self.aux_loss, self.optimizer], feed_dict={
            self.uid_batch_ph: inps[0],
            self.mid_batch_ph: inps[1],
            self.cat_batch_ph: inps[2],
            self.mid_his_batch_ph: inps[3],
            self.cat_his_batch_ph: inps[4],
            self.mask: inps[5],
            self.target_ph: inps[6],
            self.seq_len_ph: inps[7],
            self.lr: inps[8],
            self.noclk_mid_batch_ph: inps[9],
            self.noclk_cat_batch_ph: inps[10],
        })
        return loss, accuracy, aux_loss
    else:
        loss, accuracy, _ = sess.run([self.loss, self.accuracy, self.optimizer], feed_dict={
            self.uid_batch_ph: inps[0],
            self.mid_batch_ph: inps[1],
            self.cat_batch_ph: inps[2],
            self.mid_his_batch_ph: inps[3],
            self.cat_his_batch_ph: inps[4],
            self.mask: inps[5],
            self.target_ph: inps[6],
            self.seq_len_ph: inps[7],
            self.lr: inps[8],
        })
        return loss, accuracy, 0

0x08 AUC

提一下auc這個函式,起初以為是複雜演算法,後來發現原來就是最淳樸的實現方式。

def calc_auc(raw_arr):
    arr = sorted(raw_arr, key=lambda d:d[0], reverse=True)
    pos, neg = 0., 0.
    for record in arr: # 先計算正樣本,負樣本個數
        if record[1] == 1.:
            pos += 1
        else:
            neg += 1

    fp, tp = 0., 0.
    xy_arr = []
    for record in arr:
        if record[1] == 1.:
            tp += 1
        else:
            fp += 1
        xy_arr.append([fp/neg, tp/pos])

    auc = 0.
    prev_x = 0.
    prev_y = 0.
    # 就是計算auc面積,y + prev_y = prev_y + prev_y + (y - prev_y)
    # y + prev_y 再乘以 delta_x,就是 2 * delta_x * prev_y + 2 * delta_x * prev_y
    # 再除以 2,正好就是梯形面積 
    for x, y in xy_arr: 
        if x != prev_x:
            auc += ((x - prev_x) * (y + prev_y) / 2.)
            prev_x = x
            prev_y = y

    return auc

0xFF 參考

用NumPy手工打造 Wide & Deep

看Google如何實現Wide & Deep模型(1)

看Youtube怎麼利用深度學習做推薦

也評Deep Interest Evolution Network

從DIN到DIEN看阿里CTR演算法的進化脈絡

第七章 人工智慧,7.6 DNN在搜尋場景中的應用(作者:仁重)

#Paper Reading# Deep Interest Network for Click-Through Rate Prediction

【paper reading】Deep Interest Evolution Network for Click-Through Rate Prediction

也評Deep Interest Evolution Network

論文閱讀:《Deep Interest Evolution Network for Click-Through Rate Prediction》

【論文筆記】Deep Interest Evolution Network(AAAI 2019)

【讀書筆記】Deep Interest Evolution Network for Click-Through Rate Prediction

DIN(Deep Interest Network):核心思想+原始碼閱讀註釋

計算廣告CTR預估系列(五)--阿里Deep Interest Network理論

CTR預估之Deep Interest NetWork模型原理詳解

人人都能看懂的LSTM

從動圖中理解 RNN,LSTM 和 GRU

臺大李巨集毅機器學習(一)——RNN&LSTM

李巨集毅機器學習(2016)

推薦系統遇上深度學習(二十四)--深度興趣進化網路DIEN原理及實戰!

from google.protobuf.pyext import _message,使用tensorflow出現 ImportError: DLL load failed

DIN 深度興趣網路介紹以及原始碼淺析

CTR預估 論文精讀(八)--Deep Interest Network for Click-Through Rate Prediction

阿里CTR預估三部曲(1):Deep Interest Network for Click-Through Rate Prediction簡析

阿里CTR預估三部曲(2):Deep Interest Evolution Network for Click-Through Rate Prediction簡析

Deep Interest Network解讀

深度興趣網路(DIN,Deep Interest Network)

DIN論文官方實現解析

阿里DIN原始碼之如何建模使用者序列(1):base方案

阿里DIN原始碼之如何建模使用者序列(2):DIN以及特徵工程看法

阿里深度興趣網路(DIN)論文翻譯

推薦系統遇上深度學習(二十四)--深度興趣進化網路DIEN原理及實戰!

推薦系統遇上深度學習(十八)--探祕阿里之深度興趣網路(DIN)淺析及實現

【論文導讀】2018阿里CTR預估模型---DIN(深度興趣網路),後附TF2.0復現程式碼

【論文導讀】2019阿里CTR預估模型---DIEN(深度興趣演化網路)

深度學習中的attention機制

Transform詳解(超詳細) Attention is all you need論文