
当注意力机制都已经变成很tasteless的手法的时候,使用或者魔改注意力机制一定要专注讲好自己的故事:即『为什么要用Attention,为什么要魔改Attention』
现阶段从传统的CF,FM等方法到NFM,DeepFM等等,虽然开始用深度学习DNN来处理深度的特征交叉,还缺少的主要有两点:
DIN和DIEN都是阿里针对CTR预估的模型,都主要是对用户历史行为数据的进一步挖掘的工作。CTR预估任务是,根据给定广告/物品、用户和大量的上下文情况等信息,对点击进行预测,所以对用户的兴趣理解,历史行为数据非常重要。
然后自从Transformer出现,BERT在NLP界屠榜,所以很自然在推荐系统上的应用也开始升级。本篇博文将整理四篇关于Attention的文章,从普通的Attention一路升级到BERT。
DIN这篇文章的Attention故事出发点在于两点观察:

那么怎么把这部分相关历史给动态的捕捉到呢?DIN的做法是,将用户的历史数据和当前的 item之间计算相似度,即计算Attention值,对每个用户的兴趣表示都赋予不同的权值,然后再加权求和。先看公式:
其中
代表用户的特征向量,
代表用户兴趣的特征向量(用户历史行为),
代表物品的特征向量。
是用户兴趣和候选物品的相关性
,对齐了用户兴趣,实际上就是解决了Local Activation问题。然后用户特征向量就是所有加权后的历史行为和了。
整个模型框架如上图所示,左边是base模型,主要是将特征one-hot或multi-hot(主要是对用户历史行为数据)后再embedding,值得注意的是,每个用户的历史点击个数是不相等的,但需要变成一个固定长的向量,所以对于multi-hot的特征会多做一个element-wise(即图中的+号),即不管用户的行为序列有多长,都会pooling成同一个维度。最后拼接之后用MLP预测最终的分数。但是这个base版本作者认为pooling的结果显然丢失了大量信息,很显然使用Attention能够提高用户行为特征的表达,所以右边的图就是加了Attention之后的版本。
加注意力的思路很简单。然后还有两个重要的Trick:
「1. Data Dependent Activation Function(Dice激活函数)」
原来一般的Relu激活函数在值大于0时y=x,小于0时直接输出为0,这样导致了许多节点的“死亡”,更新缓慢。因此Leaky Relu在左边小于0的部分也给一定的梯度即y=ax。但这样仍然是不够的,因为它们的默认分割点都是在0这个地方(比0大的左边或者右边),不合理,分割点应该由数据决定,所以需要Dice。
第一个式子是Leaky的改版,但是此时在Leaky Relu左边的y=ax上会多一个控制的参数p,而p是对数据进行均值归一化后(即利用数据的均值和方差进行调整)的结果,即把整个激活函数移动到了数据的均值处。
def dice(_x, axis=-1, epsilon=0.000000001, name=''):
#Data Adaptive Activation Function
with tf.variable_scope(name_or_scope='', reuse=tf.AUTO_REUSE):
alphas = tf.get_variable('alpha'+name, _x.get_shape()[-1],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
beta = tf.get_variable('beta'+name, _x.get_shape()[-1],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
input_shape = list(_x.get_shape())
reduction_axes = list(range(len(input_shape)))
del reduction_axes[axis]
broadcast_shape = [1] * len(input_shape)
broadcast_shape[axis] = input_shape[axis]
# case: train mode (uses stats of the current batch)
#计算batch的均值和方差
mean = tf.reduce_mean(_x, axis=reduction_axes)
brodcast_mean = tf.reshape(mean, broadcast_shape)
std = tf.reduce_mean(tf.square(_x - brodcast_mean) + epsilon, axis=reduction_axes)
std = tf.sqrt(std)
brodcast_std = tf.reshape(std, broadcast_shape)
x_normed = tf.layers.batch_normalization(_x, center=False, scale=False, name=name, reuse=tf.AUTO_REUSE)
# x_normed = (_x - brodcast_mean) / (brodcast_std + epsilon)
x_p = tf.sigmoid(beta * x_normed)
return alphas * (1.0 - x_p) * _x + x_p * _x #根据原文中给的公式计算
def parametric_relu(_x):
#PRELU激活函数,形式上和leakReLU很像,只是它的alpha可学习
#alpha=0,退化成ReLU。alpha不更新,退化成Leak
with tf.variable_scope(name_or_scope='', reuse=tf.AUTO_REUSE):
alphas = tf.get_variable('alpha', _x.get_shape()[-1],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
pos = tf.nn.relu(_x)
neg = alphas * (_x - abs(_x)) * 0.5 #用alpha控制
return pos + neg
完整的源码笔记:https://github.com/nakaizura/Source-Code-Notebook/tree/master/DIN
「2. Adaptive Regularization(自适应正则)」
这个方法提出的动机是输入的数据长尾分布,非常稀疏维度高应该怎么防止过拟合。直接L1、L2、Dropout效果不佳,直接丢弃又损失了信息可能加重过拟合,怎么办?自适应的正则方法,按照出现的频率调整正则化的强化,即频率高的,正则化强度小,频率低的,正则化强度高。也就是说会惩罚那些出现频率低的item。
升级DIN,改进DIN中存在两个缺点:
所以DIEN中主要开发了兴趣抽取层Interest Extractor Layer、兴趣进化层Interest Evolution Layer以解决上面两个缺点。

兴趣抽取层的主要目标是提取出兴趣序列,而用户在某一时刻的兴趣是具有时序关系的,所以设计了GRU with attentional update gate (AUGRU,这个是Evolution Layer中加入注意力后的形态),增强在兴趣变化中相关兴趣的影响,减弱不相关兴趣的影响。同时为了判定兴趣是否表示的合理,又增加了一个辅助loss,来提升兴趣表达的准确性:
如上图中左边的小块是辅助网络,输入用户下一时刻真实的行为e(t+1)作为正例,负采样的行为为负例e(t+1)',分别与GRU抽取出的兴趣h(t)到辅助网络中即可,以充分的提升用户兴趣的表达。
def auxiliary_loss(self, h_states, click_seq, noclick_seq, mask, stag = None):
mask = tf.cast(mask, tf.float32)
click_input_ = tf.concat([h_states, click_seq], -1) #正例
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]
#计算loss
click_loss_ = - tf.reshape(tf.log(click_prop_), [-1, tf.shape(click_seq)[1]]) * mask
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_
#辅助网络的结构
def auxiliary_net(self, in_, stag='auxiliary_net'):
bn1 = tf.layers.batch_normalization(inputs=in_, name='bn1' + stag, reuse=tf.AUTO_REUSE)
dnn1 = tf.layers.dense(bn1, 100, activation=None, name='f1' + stag, reuse=tf.AUTO_REUSE)
dnn1 = tf.nn.sigmoid(dnn1)
dnn2 = tf.layers.dense(dnn1, 50, activation=None, name='f2' + stag, reuse=tf.AUTO_REUSE)
dnn2 = tf.nn.sigmoid(dnn2)
dnn3 = tf.layers.dense(dnn2, 2, activation=None, name='f3' + stag, reuse=tf.AUTO_REUSE)
y_hat = tf.nn.softmax(dnn3) + 0.00000001
return y_hat
兴趣进化层目标是刻画用户兴趣的进化过程,这里的Attention故事是:
所以需要注意力机制去增强在兴趣变化中相关兴趣的影响,减弱不相关兴趣的影响,即给 GRU计算Attention权重,如上图红色的部分。注意力有三种变体可以选择:

把Attention升级成Transformer之后真的简单粗暴,Transformer[1]博主已经整理过了,不再赘述。BST把特征Embedding之后直接送进去就ok。主要看看输入的几个特征吧:
位置编码和Transformer里面不一样的是,会把表示位置的时间戳直接映射成向量而不是sin函数,最后就是一般的损失函数:
有了Transformer,升级成BERT[2]就很自然了。BERT首用于NLP,那么正好,用户的行为序列很像文本序列,于是BERT4Rec吧。

模型结构如上图所示,输入是[v1,v2,...,vt]的序列,同时最后一个vt被mask掉,然后强制Transformer结合上下文预测vt,那么对于用户的行为序列,很自然就能得到最后的输出为推荐的结果。
有两个Trick需要注意:
这篇阿里ResSys'19文章的重点在于推荐后的重排序。贡献主要是利用Transformer+用户个性化重排:

具体模型架如上图,得到initial list之后把每个item的特征x和用户偏好p拼起来,这两个特征都是预训练得到的(比如用任意一个CRT的模型结合用户的历史行为,物品等等的信息进行训练就行):
再把目前item在列表中的位置编码加进去:
再用Transformer来捕捉交互得到score就是重排的结果了。
推荐系统也算是很大的领域了,所以关于注意力的玩法也有很多,所以重点决定是为什么要用Attention。比如
然后或许仅仅算Attention已经不够了,那么魔改升级Attention变成High-order-Attention,Channel-wise-Attention,Spatial-Attention等等.....还有其他的注意力变体[3]博主以前也整理过了,就不再多说。
目前Attention的升级已经逐步暴力,从self-Attention到Transformer到BERT,效果也自然是变好了。