关键词:MIMN
,DIN
,Dice
,Batch normalization
内容摘要
该系列的前两期已经介绍了MIMN原理源码,以及消费Kafka部署的实战,链接如下:
行为序列建模:MIMN系列1——原理初探和源码解析
行为序列建模:MIMN系列2——消费Kafka实时预测代码实战
本期回过头来看MIMN的一些细节,前文也提到,MIMN其实是几个技术的拼接,并没有发明新的技术,通过这几个技术的组合完成了一个CTR任务,文本主要简单介绍下其中的DIN Attention
和Dice
激活函数
研究内容概述
本文涉及DIN Attention和Dice,出自《Deep Interest Network for Click-Through Rate Prediction》阿里这篇论文提及的深度兴趣网络DIN算法,MIMN借鉴了DIN里面的注意力机制和自适应分布激活函数
- DIN Attention:基于目标物品计算历史行为序列的物品embedding的加权求和,对历史行为中相关的物品加权,而不是采用平均池化
- Dice:优化传统的PRelu激活函数,使得训练过程能自适应不同分布的数据,类似Batch Normalization使得模型更加容易收敛
DIN Attention
在DIN网络结构中存在Activation Unit,如下图所示
DIN模型结构
历史序列中的每一个good 1-N都会进入Activation Unit计算得到每一个good的权重,该权重再和good的原始embedding进行加权进入SUM Pooling进行求和。
这个Activation Unit是所有goods共享参数的,Activation Unit结构如下
Activation Unit结构
左边Input from User是历史序列每个good,右边Input from Ad是目标good/Ad,两个进行Out Product操作,从源码来看Out Product是element-wise操作,及两个向量对应位置进行相乘和相减操作,然后和序列good,目标good的embedding一起拼接,输入全连接层,最终输入给一个神经元Linear(1)得到一个权重值的输出。
看下源码的实现会更加清晰
# query是目标物品 [batch_size, emb_size]
# facts是历史序列物品 [batch_size, seq_size, emb_size]
queries = tf.tile(query, [1, tf.shape(facts)[1]])
queries = tf.reshape(queries, tf.shape(facts))
din_all = tf.concat([queries, facts, queries - facts, queries * facts], axis=-1)
先通过复制tf.tile和tf.reshape将目标物品全部平铺开来和历史序列物品一一对应,然后进行对应位置相减queries - facts和相乘queries * facts操作拼接原始输入得到din的原始特征表达,最终din_all维度是[batch_size, seq_size, emb_size * 4]。
然后进入全连接层得到最终每个good的权重值
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 = tf.reshape(d_layer_3_all, [-1, 1, tf.shape(facts)[1]])
scores = d_layer_3_all
下面对权重进行softmax,权重和为1
if softmax_stag:
scores = tf.nn.softmax(scores) # [B, 1, T]
然后进行加权组合
# Weighted sum
if mode == 'SUM':
# TODO 加权求和
# [256, 1, 4] * [256, 4, 32] => [256, 1, 32]
output = tf.matmul(scores, facts) # [B, 1, H]
# output = tf.reshape(output, [-1, tf.shape(facts)[-1]])
else:
# [256, 1, 4] => [256, 4]
scores = tf.reshape(scores, [-1, tf.shape(facts)[1]])
# [256, 4, 32] * [256, 4, 1] => [256, 4, 32]
output = facts * tf.expand_dims(scores, -1)
# [256, 4, 32] => [256, 4, 32]
output = tf.reshape(output, tf.shape(facts))
如果不是Sum Pool则仅返回加权embedding结果,不相加聚合。
最后回到应用层,din attention得到的加权求和的历史所有物品embedding结果合并成1个embedding拼接到总输入(用户特征,候选物品特征,历史行为物品特征,上下文特征)
multi_channel_hist = din_attention(self.target_rule_batch, channel_memory_tensor, self.rnn_hidden_size, None, stag='pal')
inp = tf.concat([read_out, tf.squeeze(multi_channel_hist)], 1)
本质上就是候选物品和目标物品的向量,两两之间基于element-wise操作和全连接自己迭代学习一个权重。
Dice激活函数
Dice是在PRelu的基础上改进而来,是PRelu带有数据标准化的平滑版本。
PRelu公式如下,想比于Relu在s<=0时带有一个可学习的参数控制斜率
在上一层有多少个神经元,就有多少个可学习的参数,每个神经元都有一个可学习的参数不共享,PRelu的代码如下
def prelu(_x, scope='', default_name='prelu'):
"""parametric ReLU activation"""
with tf.variable_scope(name_or_scope=scope, default_name=default_name, reuse=tf.AUTO_REUSE):
_alpha = tf.get_variable("prelu_" + scope + "_" + default_name, shape=_x.get_shape()[-1],
dtype=_x.dtype, initializer=tf.constant_initializer(0.1))
return tf.maximum(0.0, _x) + _alpha * tf.minimum(0.0, _x)
Dice将PRelu的指示函数改为了带有标准化的sigmoid
Dice公式
首先sigmoid的加入使得函数平滑而不是一刀切在0的左右都是一个斜率,然后引入类似BN的操作,在每个训练的Batch内对数据做标准化操作,使得可以自适应地根据数据分布调整整流点的位置而不是固定为0,如图
个人感觉Dice本质和BN是一样的,有了BN可以不需要额外在激活函数里面做类似Dice这样的操作了,注意和BN一样,训练和预测是两套策略,训练使用训练的mini-batch计算得到,预测使用之前累计的所有样本进行估计,网上有Dice如下
def Dice(_x, axis=-1, epsilon=0.000000001, name='dice', training=True):
alphas = tf.get_variable('alpha_'+name, _x.get_shape()[-1],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
inputs_normed = tf.layers.batch_normalization(
inputs=_x,
axis=axis,
epsilon=epsilon,
center=False,
scale=False,
training=training)
x_p = tf.sigmoid(inputs_normed)
return alphas * (1.0 - x_p) * _x + x_p * _x
参考深度学习中Batch Normalization和Dice激活函数,本质上就是BN+Sigmoid
网友评论