[阿里DIN] 深度興趣網絡源碼分析 之 整體代碼結構
[阿里DIN] 深度興趣網絡源碼分析 之 整體代碼結構
0x00 摘要
Deep Interest Network(DIN)是阿里媽媽精準定向檢索及基礎算法團隊在2017年6月提出的。其針對電子商務領域(e-commerce industry)的CTR預估,重點在於充分利用/挖掘用戶歷史行為數據中的信息。
本文為系列第三篇,將分析DIN源碼整體思路。採用的是 //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有以下幾點創新:
- 針對Diversity: 針對用戶廣泛的興趣,DIN用an interest distribution去表示,即用 Pooling(weighted sum)對Diversity建模(對用戶多種多樣的興趣建模)。
- 針對Local Activation:利用attention機制實現 Local Activation,從用戶歷史行為中動態學習用戶興趣的embedding向量,針對不同的廣告構造不同的用戶抽象表示,從而實現了在數據維度一定的情況下,更精準地捕捉用戶當前的興趣。對用戶歷史行為進行了不同的加權處理,針對不同的廣告,用戶歷史行為的權重不一致。即針對當前候選Ad,去局部的激活(Local Activate)相關的歷史興趣信息。和當前候選Ad相關性越高的歷史行為,會獲得更高的attention score,從而會主導這一次預測。
- CTR中特徵稀疏而且維度高,通常利用L1、L2、Dropout等手段防止過擬合。由於傳統L2正則計算的是全部參數,CTR預估場景的模型參數往往數以億計。DIN提出了一種正則化方法,在每次小批量迭代中,給與不同頻次的特徵不同的正則權重;
- 由於傳統的激活函數,如Relu在輸入小於0時輸出為0,將導致許多網絡節點的迭代速度變慢。PRelu雖然加快了迭代速度,但是其分割點默認為0,實際上分割點應該由數據決定。因此,DIN提出了一種數據動態自適應激活函數Dice。
- 針對大規模稀疏數據的模型訓練:當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 參考
也評Deep Interest Evolution Network
第七章 人工智能,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模型原理詳解
推薦系統遇上深度學習(二十四)–深度興趣進化網絡DIEN原理及實戰!
from google.protobuf.pyext import _message,使用tensorflow出現 ImportError: DLL load failed
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簡析
深度興趣網絡(DIN,Deep Interest Network)
阿里DIN源碼之如何建模用戶序列(2):DIN以及特徵工程看法
推薦系統遇上深度學習(二十四)–深度興趣進化網絡DIEN原理及實戰!
推薦系統遇上深度學習(十八)–探秘阿里之深度興趣網絡(DIN)淺析及實現
【論文導讀】2018阿里CTR預估模型—DIN(深度興趣網絡),後附TF2.0復現代碼
【論文導讀】2019阿里CTR預估模型—DIEN(深度興趣演化網絡)
Transform詳解(超詳細) Attention is all you need論文