当注意力机制都已经变成很tasteless的手法的时候,使用/魔改注意力机制一定要专注讲好自己的故事…即为什么要用Attention,为什么要魔改Attention。
现阶段从传统的CF,FM等方法到NFM,DeepFM等等,虽然开始用深度学习DNN来处理深度的特征交叉,还缺少的主要有两点:
- 用户历史行为的特征挖掘。
- 特征冗余度问题。二阶or高阶的特征基本都是枚举式的。
DIN和DIEN都是阿里针对CTR预估的模型,都主要是对用户历史行为数据的进一步挖掘的工作。CTR预估任务是,根据给定广告/物品、用户和大量的上下文情况等信息,对点击进行预测,所以对用户的兴趣理解,历史行为数据非常重要。然后自从Transformer出现,BERT在NLP界屠榜,所以很自然在推荐系统上的应用也开始升级。本篇博文将整理四篇关于Attention的文章,从普通的Attention一路升级到BERT。
Deep Interest Network for Click-Through Rate Prediction(DIN)
DIN这篇文章的Attention故事出发点在于两点观察:
- Diversity:多样性是指用户的兴趣是广泛的,一个用户会对多个物品,多个领域感兴趣
- Local activation:部分对应是指只有部分历史数据与目前推荐的物品相关(如推荐零食物品就与用户以前买过什么装备无关)
那么怎么把这部分相关历史给动态的捕捉到呢?DIN的做法是,将用户的历史数据和当前的 item之间计算相似度,即计算Attention值,对每个用户的兴趣表示都赋予不同的权值,然后再加权求和。先看公式:
V
u
=
f
(
V
a
)
=
∑
i
=
1
N
w
i
∗
V
i
=
∑
i
=
1
N
g
(
V
i
,
V
a
)
∗
V
i
V_u=f(V_a)=\sum^N_{i=1}w_i * V_i=\sum^N_{i=1} g(V_i,V_a)*V_i
Vu=f(Va)=i=1∑Nwi∗Vi=i=1∑Ng(Vi,Va)∗Vi其中
V
u
V_u
Vu代表用户的特征向量,
V
i
V_i
Vi代表用户兴趣的特征向量(用户历史行为),
V
a
V_a
Va代表物品的特征向量。
w
i
w_i
wi是用户兴趣和候选物品的相关性
g
(
V
i
,
V
a
)
g(V_i,V_a)
g(Vi,Va),对齐了用户兴趣,实际上就是解决了Local Activation问题。然后用户特征向量就是所有加权后的历史行为和了。
整个模型框架如上图所示,左边是base模型,主要是将特征one-hot或multi-hot(主要是对用户历史行为数据)后再embedding,值得注意的是,每个用户的历史点击个数是不相等的,但需要变成一个固定长的向量,所以对于multi-hot的特征会多做一个element-wise(即图中的+号),即不管用户的行为序列有多长,都会pooling成同一个维度。最后拼接之后用MLP预测最终的分数。 但是这个base版本作者认为pooling的结果显然丢失了大量信息,很显然使用Attention能够提高用户行为特征的表达,所以右边的图就是加了Attention之后的版本。
加注意力的思路很简单。然后还有两个重要的Trick:
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是对数据进行均值归一化后(即利用数据的均值和方差进行调整)的结果,即把整个激活函数移动到了数据的均值处。
- 优点:根据数据分布灵活调整阶跃变化点,具有BN的优点
- 缺点:BN复杂度,比较耗时
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
Adaptive Regularization(自适应正则)
这个方法提出的动机是输入的数据长尾分布,非常稀疏维度高应该怎么防止过拟合。直接L1、L2、Dropout效果不佳,直接丢弃又损失了信息可能加重过拟合,怎么办?自适应的正则方法,按照出现的频率调整正则化的强化,即频率高的,正则化强度小,频率低的,正则化强度高。也就是说会惩罚那些出现频率低的item。
完整的源码笔记:https://github.com/nakaizura/Source-Code-Notebook/tree/master/DIN
paper:https://arxiv.org/abs/1706.06978
- DIN的设计对工业界的帮助更大,因为上线的时候受制于内存所以User Embedding不能很大,那么显然无法很好的表示用户特征,想表示用户的多兴趣(多峰)就很难了。此时DIN基于用户历史行为再加入Attention就很好的缓解了这个问题。
- 缺点:用到了历史行为数据,但是忽略了序列关系。
Deep Interest Evolution Network for Click-Through Rate Prediction(DIEN)
升级DIN,改进DIN中存在两个缺点:
- 用户兴趣应该是不断进化的。DIN抽取的用户兴趣是固定的,没有捕获到兴趣的这种进化性
- 如何保证通过用户的显式的行为得到的兴趣是有效的
所以DIEN中主要开发了兴趣抽取层Interest Extractor Layer、兴趣进化层Interest Evolution Layer以解决上面两个缺点。
Interest Extractor Layer
兴趣抽取层的主要目标是提取出兴趣序列,而用户在某一时刻的兴趣是具有时序关系的,所以设计了GRU with attentional update gate (AUGRU,这个是Evolution Layer中加入注意力后的形态),增强在兴趣变化中相关兴趣的影响,减弱不相关兴趣的影响。同时为了判定兴趣是否表示的合理,又增加了一个辅助loss,来提升兴趣表达的准确性:
L
a
u
x
=
−
1
N
(
∑
i
=
1
N
∑
t
l
o
g
σ
(
h
t
,
e
b
i
[
t
+
1
]
)
+
l
o
g
(
1
−
l
o
g
σ
(
h
t
,
e
b
i
[
t
+
1
]
′
)
)
L_{aux}=-\frac{1}{N}(\sum^N_{i=1}\sum_t log \sigma(h_t,e^i_b[t+1])+log(1-log \sigma(h_t,e^i_b[t+1]^{'}))
Laux=−N1(i=1∑Nt∑logσ(ht,ebi[t+1])+log(1−logσ(ht,ebi[t+1]′))如上图中左边的小块是辅助网络,输入用户下一时刻真实的行为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
Interest Evolution Layer
兴趣进化层目标是刻画用户兴趣的进化过程,这里的Attention故事是:
- interest drift:用户的兴趣具有偏向性。
- interest individual:用户的兴趣之间具有独立性。
所以需要注意力机制去增强在兴趣变化中相关兴趣的影响,减弱不相关兴趣的影响,即给 GRU计算Attention权重,如上图红色的部分。注意力有三种变体可以选择:
- GRU with attentional input (AIGRU):将注意力作为输入 i t ′ = h t ⋅ a t i_t^{'} = h_t\cdot a_t it′=ht⋅at
- Attention based GRU(AGRU):用注意力代替GRU的更新门 h t ’ = ( 1 − a t ) ∘ h t − 1 ’ + a t ∘ h ~ t ’ h_t^’=(1-a_t)\circ h_{t-1}^’+a_t\circ\tilde{h}_t^’ ht’=(1−at)∘ht−1’+at∘h~t’
- GRU with attentional update gate (AUGRU):对更新门加权 u t ‘ = u t ⋅ a t u_t^‘ = u_t\cdot a_t ut‘=ut⋅at
原code:https://github.com/mouna99/dien
paper:https://arxiv.org/pdf/1809.03672.pdf
注意力的其他玩法
推荐系统也算是很大的领域了,所以关于注意力的玩法也有很多,所以重点决定是为什么要用Attention。比如要融合各种特征(attention或者co-attention,cross-attention),特征是否分级(Hierarchical,level-attention),特征间是否存在交互(interactive attention),特征是否群组关系(Group-Attention),是否存在动态的变化(dynamic attention,可能一般的用法会按时间线算多次以捕捉这种动态性),然后或许仅仅算Attention已经不够了,那么魔改升级Attention变成High-order-Attention,Channel-wise-Attention,Spatial-Attention等等…还有其他的注意力变体博主以前也整理过了,就不再多说。
目前Attention的升级已经逐步暴力,从self-Attention到Transformer到BERT,效果也自然是变好了。
Behavior Sequence Transformer for E-commerce Recommendation in Alibaba
把Attention升级成Transformer之后真的简单粗暴,Transformer博主已经整理过了,不再赘述。BST把特征Embedding之后直接送进去就ok。主要看看输入的几个特征吧:
- 图最左边的other feature分为四个部分,用户特征、商品特征、上下文特征、交叉特征
- item里面的蓝色是位置编码,红色是行为序列中的物品
位置编码和Transformer里面不一样的是,会把表示位置的时间戳直接映射成向量而不是sin函数,最后就是一般的损失函数:
L = − 1 N ∑ ( x , y ) ∈ D ( y l o g p ( x ) + ( 1 − y ) l o g ( 1 − p ( x ) ) ) L=-\frac{1}{N}\sum_{(x,y)\in D}(ylogp(x)+(1-y)log(1-p(x))) L=−N1(x,y)∈D∑(ylogp(x)+(1−y)log(1−p(x)))
paper:https://arxiv.org/pdf/1905.06874.pdf
BERT4Rec: Sequential Recommendation with Bidirectional Encoder Representations from Transformer
有了Transformer,升级成BERT就很自然了。BERT首用于NLP,那么正好,用户的行为序列很像文本序列,于是BERT4Rec吧。模型结构如上图所示,输入是[v1,v2,…,vt]的序列,同时最后一个vt被mask掉,然后强制Transformer结合上下文预测vt,那么对于用户的行为序列,很自然就能得到最后的输出为推荐的结果。
有两个Trick需要注意:
- 输入不是所有的用户行为序列。因为不同用户的行为序列长度可能相差太大了,所以采用最近的N个行为序列做输入
- 在模型训练的时候并不是只mask最后一个,而是和BERT本身一样,随机mask,然后预测masked的部分
paper:https://arxiv.org/abs/1904.06690
Personalized Re-ranking for Recommendation
补一篇不那么正统的论文,这篇阿里ResSys’19文章的重点在于推荐后的重排序。贡献主要是利用Transformer+用户个性化重排:
- 用户和列表中物品的交互能更有倾向性
- Transformer的self-attention可以有效捕捉特征间的交互
具体模型架如上图,得到initial list之后把每个item的特征x和用户偏好p拼起来,这两个特征都是预训练得到的(比如用任意一个CRT的模型结合用户的历史行为,物品等等的信息进行训练就行):
E
′
′
=
[
x
i
1
;
p
v
i
1
x
i
2
;
p
v
i
2
⋯
x
i
n
;
p
v
i
n
]
+
[
p
e
i
1
p
e
i
2
⋯
p
e
i
n
]
E^{\prime \prime}=\left[\begin{array}{c} {x_{i_{1}} ; p v_{i_{1}}} \\ {x_{i_{2}} ; p v_{i_{2}}} \\ {\cdots} \\ {x_{i_{n}} ; p v_{i_{n}}} \end{array}\right]+\left[\begin{array}{c} {p e_{i_{1}}} \\ {p e_{i_{2}}} \\ {\cdots} \\ {p e_{i_{n}}} \end{array}\right]
E′′=⎣⎢⎢⎡xi1;pvi1xi2;pvi2⋯xin;pvin⎦⎥⎥⎤+⎣⎢⎢⎡pei1pei2⋯pein⎦⎥⎥⎤
再把目前item在列表中的位置编码加进去:
E
=
E
W
E
+
b
E
E=E W^{E}+b^{E}
E=EWE+bE再用Transformer来捕捉交互得到score就是重排的结果了。
paper:https://arxiv.org/pdf/1904.06813.pdf