DMIN 模型介绍与源码分析

DMIN (Deep Multi-Interest Network) 模型介绍与源码分析

零.前言 (与正文无关, 请忽略)

对自己之前分析过的文章做一个简单的总结:

广而告之

可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中. CSDN 上的阅读体验会更好一些, 地址是: https://blog.csdn.net/eric_1993/category_9900024.html

一. 文章信息

二. 核心观点

文章认为用户在同一时间会存在多种兴趣, 但潜在的主要兴趣最终会通过用户行为表现出来. 为了建模用户潜在的主要兴趣, 文章提出 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, 之前立过, 如今再立一次), 加深印象.

猜你喜欢

转载自blog.csdn.net/Eric_1993/article/details/120902786
今日推荐