DMIN (Deep Multi-Interest Network) 模型介绍与源码分析
文章目录
零.前言 (与正文无关, 请忽略)
对自己之前分析过的文章做一个简单的总结:
- 机器学习基础: LR / LibFM
- 特征交叉: DCN / PNN / DeepMCP / xDeepFM / FiBiNet / AFM
- 用户行为建模: DSIN / DMR / DMIN / DIN
- 多任务建模: MMOE
- Graph 建模: GraphSage / GAT
广而告之
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中. CSDN 上的阅读体验会更好一些, 地址是: https://blog.csdn.net/eric_1993/category_9900024.html
一. 文章信息
- 论文标题: Deep Multi-Interest Network for Click-through Rate Prediction
- 论文地址: https://www.researchgate.net/publication/345125472_Deep_Multi-Interest_Network_for_Click-through_Rate_Prediction
- 代码地址: https://github.com/mengxiaozhibo/DMIN
- 发表时间: 2020
- 论文作者: Xiao Zhibo, Luwei Yang
- 作者单位: Alibaba
二. 核心观点
文章认为用户在同一时间会存在多种兴趣, 但潜在的主要兴趣最终会通过用户行为表现出来. 为了建模用户潜在的主要兴趣, 文章提出 DMIN 网络,
其中 Behavior Refiner Layer 采用 multi-head self-attention 获取更好的用户历史行为表达, 而 Multi-Interest Extractor Layer 也采用 Multi-Head Self-Attention 来建模用户多种潜在的主要兴趣, 计算权重系数时引入了 Position Encoding, 各个子空间(或者说各个 Head)对应的 embedding 即表示用户的 multi interest.
三. 核心观点解读
DMIN 网络结构设计如下图:
四. 源码分析
DMIN 模型定义在 https://github.com/mengxiaozhibo/DMIN/blob/master/code/model.py 文件中, 类名为 Model_DNN_Multi_Head
.
Behavior Refiner Layer
采用 Multi-Head Self-Attention 完成对用户兴趣的初步提取:
maxlen = 20
other_embedding_size = 2
## 生成 Position Embedding
self.position_his = tf.range(maxlen)
self.position_embeddings_var = tf.get_variable("position_embeddings_var", [maxlen, other_embedding_size])
self.position_his_eb = tf.nn.embedding_lookup(self.position_embeddings_var, self.position_his) # T,E
self.position_his_eb = tf.tile(self.position_his_eb, [tf.shape(self.item_his_eb)[0], 1]) # B*T,E
self.position_his_eb = tf.reshape(self.position_his_eb, [tf.shape(self.item_his_eb)[0], -1, self.position_his_eb.get_shape().as_list()[1]]) # B,T,E
with tf.name_scope("multi_head_attention"):
multihead_attention_outputs = self_multi_head_attn(self.item_his_eb, num_units=EMBEDDING_DIM*2, num_heads=4,dropout_rate=0,is_training=True)
print('multihead_attention_outputs.get_shape()',multihead_attention_outputs.get_shape())
multihead_attention_outputs1 = tf.compat.v1.layers.dense(multihead_attention_outputs,EMBEDDING_DIM*4,activation=tf.nn.relu)
multihead_attention_outputs1 = tf.compat.v1.layers.dense(multihead_attention_outputs1,EMBEDDING_DIM*2)
multihead_attention_outputs = multihead_attention_outputs1 + multihead_attention_outputs
self_multi_head_attn
函数定义如下:
def self_multi_head_attn(inputs, num_units, num_heads, dropout_rate, name="", is_training=True, is_layer_norm=True):
"""
Args:
inputs(query): A 3d tensor with shape of [B, T, 2*E]
inputs(keys): A 3d tensor with shape of [B, T, 2*E]
num_units: 2*E
num_heads: 4
"""
## 生成 Query, Key 以及 Value
Q_K_V = tf.layers.dense(inputs, 3*num_units) ## [B, T, 6*E]
Q, K, V = tf.split(Q_K_V, 3, -1) ## [Tensor(B, T, 2*E), Tensor(B, T, 2*E), Tensor(B, T, 2*E)]
## 生成 Multi-Head
Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*B, T, 2*E/h)
K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*B, T, 2*E/h)
V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*B, T, 2*E/h)
## 生成子空间中的 Attention 权重系数
outputs = tf.matmul(Q_, tf.transpose(K_, [0, 2, 1])) # [h*B, T, T]
align= outputs / (36 ** 0.5) ## 代码中 E 默认是 18, 2*E 就是 36, Query/Key 的 emb 大小为 2*E
diag_val = tf.ones_like(align[0, :, :]) ## [T, T]
"""
tril 为下三角矩阵:
[
[1, 0, 0, ...],
[1, 1, 0, ...],
[1, 1, 1, ...]
...
]
使用下三角函数的目的是, 每一次只计算前 i 个行为的加权求和结果. 比如对第 T 个行为, 它只和第 T-1, T-2, ..., 0 个行为来计算 Attention 的结果.
"""
tril = tf.linalg.LinearOperatorLowerTriangular(diag_val).to_dense() # [T, T]
key_masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(align)[0], 1, 1]) ## [h*B, T, T]
padding = tf.ones_like(key_masks) * (-2 ** 32 + 1) ## [h*B, T, T]
outputs = tf.where(tf.equal(key_masks, 0), padding, align) # [h*B, T, T]
outputs = tf.nn.softmax(outputs)
outputs = tf.layers.dropout(outputs, dropout_rate, training=is_training)
## 对用户行为进行加权求和, 做 Self-Attention
outputs = tf.matmul(outputs, V_) ## [h*B, T, 2*E/h]
## 子空间的 emb 重新进行 concat
outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2) ## [B, T, 2*E]
# output linear
outputs = tf.layers.dense(outputs, num_units) ## [B, T, 2*E]
# drop_out before residual and layernorm
outputs = tf.layers.dropout(outputs, dropout_rate, training=is_training)
# Residual connection
outputs += inputs ## [B, T, 2*E]
# Normalize
if is_layer_norm:
outputs = layer_norm(outputs,name=name) ## [B, T, 2*E]
return outputs
最后得到 self_multi_head_attn
输出的大小为 [B, T, 2*E]
, 再将结果经过两个全连接层, 并做 Residual Connection, 得到大小为 [B, T, 2*E]
的 multihead_attention_outputs
, 表示初步提取的用户兴趣.
同时还使用辅助 Loss 来提供更多的监督信息, 辅助 loss 代码如下:
aux_loss_1 = self.auxiliary_loss(multihead_attention_outputs[:, :-1, :], self.item_his_eb[:, 1:, :],
self.noclk_item_his_eb[:, 1:, :],
self.mask[:, 1:], stag="gru")
self.aux_loss = aux_loss_1
其中 auxiliary_loss
定义在: https://github.com/mengxiaozhibo/DMIN/blob/master/code/model.py, 代码如下:
def auxiliary_loss(self, h_states, click_seq, noclick_seq, mask, stag = None):
"""
调用 auxiliary_loss 的过程中, 传入参数如下:
+ h_states: 传入 multihead_attention_outputs[:, :-1, :], 大小为 [B, T - 1, 2*E]
+ click_seq: 传入 item_his_eb[:, 1:, :], 大小为 [B, T - 1, 2*E], 为用户历史行为序列, 作为正样本
+ noclick_seq: 传入 noclk_item_his_eb[:, 1:, :]
auxiliary_net 为 3 层 DNN, 输出节点个数为 2, 输出结果会经过 softmax
"""
mask = tf.cast(mask, tf.float32)
click_input_ = tf.concat([h_states, click_seq], -1) ## [B, T - 1, 4*E]
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]
## 注意不要忘了乘上 mask
click_loss_ = - tf.reshape(tf.log(click_prop_), [-1, tf.shape(click_seq)[1]]) * mask ## [B, T - 1]
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_
Multi-Interest Extractor Layer
接着再次使用 Multi-Head Self-Attention 完成对用户多兴趣的提取, 用户的兴趣数目等于 Head 的数量, 设为 H E H_{E} HE, 对于用户序列中的每一个行为, 在不同的 Head 中都有对应的表达. 代码位于: https://github.com/mengxiaozhibo/DMIN/blob/master/code/model.py
inp = tf.concat([self.uid_batch_embedded, self.item_eb, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum], 1)
with tf.name_scope("multi_head_attention"):
"""
对于 self_multi_head_attn_v2 函数, 其参数中:
+ multihead_attention_outputs: 来自上一层 Behavior Refiner Layer 的输出, 大小为 [B, T, 2*E]
+ num_units: 显式传入 36, 代码中 2*E 就等于 36 (E 默认为 18), 为了表示方便, 后面注释将 num_units 表示为 2*E
函数的输出结果 multihead_attention_outputss 是一个 list, 长度为 num_heads (即 4 个元素), 元素大小为 [B, T, 2*E]
"""
multihead_attention_outputss = self_multi_head_attn_v2(multihead_attention_outputs, num_units=36, num_heads=4,dropout_rate=0,is_training=True)
## 对 multihead_attention_outputss 中的每一个元素, 均经过两层 DNN, 元素大小仍为 [B, T, 2*E]
## 注意到此时对于每个行为, 它在 num_heads (4 个) 个 Head 中均有自己的特征表示
for i, multihead_attention_outputs_v2 in enumerate(multihead_attention_outputss):
multihead_attention_outputs3 = tf.compat.v1.layers.dense(multihead_attention_outputs_v2, EMBEDDING_DIM*4,activation=tf.nn.relu)
multihead_attention_outputs3 = tf.compat.v1.layers.dense(multihead_attention_outputs3, EMBEDDING_DIM*2)
multihead_attention_outputs_v2 = multihead_attention_outputs3 + multihead_attention_outputs_v2
with tf.name_scope('Attention_layer'+str(i)):
#这里使用position embedding来算attention, attention_output 的结果大小为 [B, 1, 2*E]
attention_output, attention_score, attention_scores_no_softmax = din_attention_new(self.item_eb, multihead_attention_outputs_v2, self.position_his_eb, ATTENTION_SIZE, self.mask, stag=str(i))
att_fea = tf.reduce_sum(attention_output, 1) ## [B, 2*E]
inp = tf.concat([inp, att_fea],1)
self_multi_head_attn_v2
函数不过多介绍了, 它里面大部分逻辑和 self_multi_head_attn
一样, 只是在最后输出时, 将不同 Head 的结果作为 list 给返回. 模型最后将用户的多兴趣与 inp
进行拼接, 再将 inp
输入到 MLP 中得到预估值.
五. 总结
可以和 DMR (Deep Match to Rank) 网络介绍与源码浅析 一起看, 或者同时看看 DIEN 的源码 (先立个 Flag, 之前立过, 如今再立一次), 加深印象.