推荐系统(十二)阿里深度兴趣网络(二):DIEN模型(Deep Interest Evolution Network)
推荐系统(十二)阿里深度兴趣网络(二):DIEN模型(Deep Interest Evolution Network)
推荐系统系列博客:
-
推荐系统(一)推荐系统整体概览
-
推荐系统(二)GBDT+LR模型
-
推荐系统(三)Factorization Machines(FM)
-
推荐系统(四)Field-aware Factorization Machines(FFM)
-
推荐系统(五)wide&deep
-
推荐系统(六)Deep & Cross Network(DCN)
-
推荐系统(七)xDeepFM模型
-
推荐系统(八)FNN模型(FM+MLP=FNN)
-
推荐系统(九)PNN模型(Product-based Neural Networks)
-
推荐系统(十)DeepFM模型
-
推荐系统(十一)阿里深度兴趣网络(一):DIN模型(Deep Interest Network)
阿里关于深度兴趣网络的探索共有3篇论文发表,按照发表年份的先后分别是:DIN、DIEN和DSIN。上一篇博客介绍了开篇之作DIN,本篇博客来介绍DIEN。DIEN相比较DIN多了个E(Evolution),即兴趣的进化转移。
本篇博客将会从以下几个方面介绍DIEN:
- 动机
- 整体网络结构
- Interest Extractor层及辅助loss
- Interest Evolving层
- GRU的改进AUGRU
- 在线工程
一、动机
论文中描述的动机认为:目前的CTR预估模型都直接把用户行为当做用户兴趣,简单的做个embedding后就进入到MLP中,并没有挖掘潜藏在这些行为之后用户的兴趣。原文如下:
most interest models including DIN regard the behavior as the interest directly, while latent interest is hard to be fully reflected by explicit behavior. Previous methods neglect to dig the true user interest behind behavior. Moreover, user interest keeps evolving, capturing the dynamic of inter- est is important for interest representation.
以下一段话,纯属个人瞎猜,各位看官姑且看看就好:
个人在这里盲猜最本质的动机还是总觉得用户行为是个序列,提到序列嘛,第一想法肯定是RNN啊,但是吧,用户行为序列还和文本序列不太一样,文本序列有明确的语法制约,比如【我是好人】就是一句有明确含义的话,打乱下顺序【人我是好】就明显不具有含义了。但是用户行为序列,比如点击行为序列【牛仔裤–>哈伦裤–>阔腿裤】和【哈伦裤–>牛仔裤–>阔腿裤】顺序改变下,其实是没什么影响的,也就是说用户行为序列对于『顺序』实际上不是很敏感的,这里再去硬套RNN似乎有些牵强附会。但是吧,你得装啊(王蒙语录,哈哈哈哈),所以,DIEN说我是对用户行为背后兴趣进化过程建模,那么RNN总该可以用了吧(当然也不是普通的RNN,而是阿里改良后的),毕竟兴趣进化过程(注意是进化过程)这种东西你可以说和时序无关,也可以说和时序有关。
言归正传,阿里在KDD版本DIN的论文中也提到了一段话,与我上面的想法不谋而合,DIN论文原话:
We have tried LSTM to model user historical behavior data in the sequential manner. But it shows no improvement. Different from text which is under the constraint of grammar in NLP task, the sequence of user historical behaviors may contain multiple concurrent interests. Rapid jumping and sudden ending over these interests causes the sequence data of user behaviors to seem to be noisy. A possible direction is to design special structures to model such data in a sequence way. We leave it for future research.
从这里可以看出,实际上阿里早在DIN版本中就尝试过了用RNN来对用户行为序列建模,但并没有什么效果,原因也正如我上面所说的。阿里在这里说了一句『 A possible direction is to design special structures to model such data in a sequence way』这也为DIEN的诞生埋下了伏笔,也就是DIEN中AUGRU的设计,所以还是不得不佩服阿里妈妈盖坤团队不停思考不停实践的精神,点赞。
下面,我们就来一一拆解这篇论文。
二、整体网络结构
图1. DINE整体网络结构
从图1中能够看出,DIEN的整体结构过程为:1)特征(User Profile feature, User Behavior feature, Ad feature and Context feature四大类),2)经过Embedding层得到低维稠密的embedding向量,3)然后进入到Interest Extractor层,这里就是个普通的GRU,为了更好的训练,引入了辅助Loss。4)然后进入到Interest Evolving层,5)最终与ad feature,Context feature和User Profile feature的向量做concat,进入到多层全连接层(MLP),然后BP训练。
因为引入了辅助loss,因此整个DIEN模型的loss为:
L
=
L
t
a
r
g
e
t
+
α
∗
L
a
u
x
(1)
L = L_{target} + \alpha * L_{aux} \tag{1}
L
=
L
t
a
r
g
e
t
+
α
∗
L
a
u
x
(
1
)
其中
L
t
a
r
g
e
t
L_{target}
L
t
a
r
g
e
t
为全连接层后的交叉熵loss,公式为:
L
t
a
r
g
e
t
=
−
1
N
∑
(
x
,
y
)
∈
D
(
y
log
p
(
x
)
+
(
1
−
y
)
log
(
1
−
p
(
x
)
)
)
(2)
L_{target} = -\frac{1}{N}\sum_{(x,y)\in D}(y\log p(x) + (1-y)\log (1-p(x))) \tag{2}
L
t
a
r
g
e
t
=
−
N
1
(
x
,
y
)
∈
D
∑
(
y
lo
g
p
(
x
)
+
(
1
−
y
)
lo
g
(
1
−
p
(
x
)
)
)
(
2
)
L
a
u
x
L_{aux}
L
a
u
x
为辅助loss,为了更好的训练Interest Extractor层,其公式为:
L
a
u
x
=
−
1
N
∑
i
=
1
N
∑
t
(
log
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
+
log
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
)
(3)
L_{aux} = -\frac{1}{N}\sum_{i=1}^N\sum_{t}(\log \sigma(h_t^i, e^i_b[t+1]) + \log (1-\sigma(h_t^i, \hat{e}^i_b[t+1]))) \tag{3}
L
a
u
x
=
−
N
1
i
=
1
∑
N
t
∑
(
lo
g
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
+
lo
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
)
(
3
)
关于这个公式会在第四部分进行详细解释,这里大家就知道有这个公式的存在就可以了。
三、Interest Extractor层及辅助loss
interest extractor是为了从用户行为序列中抽取用户兴趣。首先,阿里这里使用了用户过去两周即14天的点击商品与商品类目序列(按照目前淘宝的体量,个人估计这个序列应该会在100以上)。前面也说过到,既然是序列,肯定是RNN,从效果与效率的角度考虑首选肯定是GRU,这里再来温习下GRU吧,毕竟如果不是一直搞NLP的同学,可能只会GRU有个大体的印象了,关于GRU的具体细节,参见我之前的博客:
GRU
,在这里,我只列出公式(为了与本博客贴合,符合表达与DIEN里一致):
u
t
=
σ
(
W
u
i
t
+
U
u
h
t
−
1
+
b
u
)
r
t
=
σ
(
W
r
i
t
+
U
r
h
t
−
1
+
b
r
)
h
t
^
=
t
a
n
h
(
W
h
i
t
+
r
t
∘
U
h
h
t
−
1
+
b
b
)
h
t
=
(
1
−
u
t
)
∘
h
t
−
1
+
u
t
∘
h
t
^
(4)
\begin{aligned} u_t &= \sigma(W^ui_t + U^uh_{t-1} + b^u) \\ r_t &= \sigma(W^ri_t + U^rh_{t-1} + b^r) \\ \hat{h_t} &= tanh(W^hi_t + r_t \circ U^h h_{t-1} + b^b) \\ h_t &= (1-u_t) \circ h_{t-1} + u_t \circ \hat{h_t} \end{aligned} \tag{4}
u
t
r
t
h
t
^
h
t
=
σ
(
W
u
i
t
+
U
u
h
t
−
1
+
b
u
)
=
σ
(
W
r
i
t
+
U
r
h
t
−
1
+
b
r
)
=
t
a
n
h
(
W
h
i
t
+
r
t
∘
U
h
h
t
−
1
+
b
b
)
=
(
1
−
u
t
)
∘
h
t
−
1
+
u
t
∘
h
t
^
(
4
)
其中
u
t
u_t
u
t
为更新门,
r
t
r_t
r
t
为重置门,
∘
\circ
∘
为点乘,即对应元素相乘。
如果就这么直接硬套GRU,那么就又回到前面开头说的。直接用GRU显然存在两个问题:
-
隐藏状态
ht
h_t
h
t
仅仅只是抓住了用户行为序列的依赖关系并不能有效的反映出用户兴趣,前面也说了用户行为序列的『顺序』其实没啥用(大家可以实验试一试,除了增加了点复杂度,估计和直接pooling没啥区别)。 -
用户点击的广告商品肯定是反应了用户的兴趣,如果仅靠最后全连接层后的loss,只能学到用户最终综合的兴趣(因为最终综合的兴趣导致用户发生了这一次点击行为),而中间状态
ht
h_t
h
t
无法得到有效的监督信号指导。
DIEN在这里给出了解决办法,即增加了一个辅助loss,用于指导中间状态
h
t
h_t
h
t
的学习,直接来看看图:
图2. DINE辅助loss
首先明确下正负样本:
-
正样本:用户点击商品序列,记作
eb
i
e^i_b
e
b
i
,
eb
i
[
t
]
e^i_b[t]
e
b
i
[
t
]
表示用户
ii
i
点击序列中第
tt
t
个item的embedding向量。为什么选择用户点击商品作为正样本,因为很明显用户点击了一定表达了用户某种兴趣。 -
负样本:从用户未点击过的商品里采样得到(整个item集合减去用户点击过的item),记作
eb
i
^
\hat{e^i_b}
e
b
i
^
,表示用户
ii
i
第
tt
t
个item对应的负样本。
从图2中能够很清晰看出是正负样本分别与中间状态
h
t
h_t
h
t
做了内积,因此辅助loss为:
L
a
u
x
=
−
1
N
(
∑
i
=
1
N
∑
t
log
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
+
log
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
)
(5)
L_{aux} = -\frac{1}{N}(\sum_{i=1}^N\sum_t\log\sigma(h_t^i, e^i_b[t+1]) + \log(1-\sigma(h_t^i, \hat{e}^i_b[t+1]))) \tag{5}
L
a
u
x
=
−
N
1
(
i
=
1
∑
N
t
∑
lo
g
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
+
lo
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
)
(
5
)
其中
(
h
t
i
,
e
b
i
[
t
+
1
]
)
(h_t^i, e^i_b[t+1])
(
h
t
i
,
e
b
i
[
t
+
1
]
)
表示做内积,
σ
\sigma
σ
为sigmoid函数。
从这个loss设计我们能够看出是为了迫使中间隐藏状态
h
t
h_t
h
t
拟合用户的点击行为,进而更好的捕捉用户兴趣。从公式(5)能够看出,当
h
t
i
h_t^i
h
t
i
与
e
b
i
[
t
+
1
]
e^i_b[t+1]
e
b
i
[
t
+
1
]
越相似,其内积越大,那么
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
\sigma(h_t^i, e^i_b[t+1])
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
则接近于1,
log
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
\log\sigma(h_t^i, e^i_b[t+1])
lo
g
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
则趋向于0;而
l
o
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
log(1-\sigma(h_t^i, \hat{e}^i_b[t+1]))
l
o
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
也趋向于0,因此
L
a
u
x
L_{aux}
L
a
u
x
趋向于0。符合我们的
min
(
l
o
s
s
)
\min(loss)
min
(
l
o
s
s
)
目标。相反,如果
h
t
i
h_t^i
h
t
i
与
e
b
i
[
t
+
1
]
e^i_b[t+1]
e
b
i
[
t
+
1
]
不相似,
log
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
\log\sigma(h_t^i, e^i_b[t+1])
lo
g
σ
(
h
t
i
,
e
b
i
[
t
+
1
]
)
趋向于负无穷,
l
o
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
log(1-\sigma(h_t^i, \hat{e}^i_b[t+1]))
l
o
g
(
1
−
σ
(
h
t
i
,
e
^
b
i
[
t
+
1
]
)
)
也趋向于负无穷,此时loss趋向于正无穷。
所以,DIEN最终的损失函数就变成了:
L
=
L
t
a
r
g
e
t
+
α
∗
L
a
u
x
(4)
L = L_{target} + \alpha * L_{aux} \tag{4}
L
=
L
t
a
r
g
e
t
+
α
∗
L
a
u
x
(
4
)
α
\alpha
α
为超参数,用于平衡
L
t
a
r
g
e
t
L_{target}
L
t
a
r
g
e
t
与
L
a
u
x
L_{aux}
L
a
u
x
。
四、 Interest Evolving层
用户点击行为是用户各种兴趣的体现,因为用户不可能是单一兴趣的,可能一段时间对书籍感兴趣,一段时间对衣服感兴趣,所以用户兴趣的进化是相互独立的。因为我们最终的目标是预估目标广告的CTR,所以我们只需要重点关注和目标广告相关的兴趣就可以了,那么就又回到了DIN中做的事情,通过引入attention来学习每个兴趣与最终广告之间的权重(相关性)。其示意图如下图所示:
图3. DINE Interest Evolving层
从上图能够看出,经过Interest Extractor层得到的中间隐藏状态
h
t
h_t
h
t
与目标广告(target ad)之间通过attention计算得到一个权重分数,然后该权重分数与
h
t
h_t
h
t
一同进入AUGRU。
关于这个attention score怎么计算,论文中给出了计算公式:
a
t
=
e
x
p
(
h
t
W
e
a
)
∑
j
=
1
T
e
x
p
(
h
j
W
e
a
)
(5)
a_t = \frac{exp(h_tWe_a)}{\sum_{j=1}^Texp(h_jWe_a)} \tag{5}
a
t
=
∑
j
=
1
T
e
x
p
(
h
j
W
e
a
)
e
x
p
(
h
t
W
e
a
)
(
5
)
其中
h
t
h_t
h
t
即为Interest Extractor层抽取出来的中间隐藏状态,
e
a
e_a
e
a
为目标广告各个特征的contact,比如广告的[good_id, cate_id],然后使用了个softmax做了权重归一化。
我们在实现的时候,也很简单,基本上和DIN一样,目标广告与用户点击序列做个减法和内积,然后一同concat后输入到一个全连接网络里,代码如下:
"""
hist_seq_concat: 用户点击序列
target_seq_concat: 目标广告id与目标广告cate的向量拼接
"""
concat = paddle.concat(
[
hist_seq_concat, target_seq_concat,
# element-wise sub
paddle.subtract(hist_seq_concat, target_seq_concat),
# inner product
paddle.multiply(hist_seq_concat, target_seq_concat)
],
axis=2)
# 全连接网络
for attlayer in self.attention_layer:
concat = attlayer(concat)
# 因为要根据batch内用户最大点击序列补齐,mask矩阵,用于hist_item_se和hist_cat_seq
# 中补0的部分失效,对于补齐的网格部分,初始化为-INF,从而在sigmoid后,使之失效为0;
# 目前做法是有数 据的为0,补齐的用-INF
atten_fc3 = paddle.add(concat, mask)
# [32, 1, 2] 32-->batchsize, 2-> user click sequence num
atten_fc3 = paddle.transpose(atten_fc3, perm=[0, 2, 1])
# 做一个放缩
# [32, 1, 2]
atten_fc3 = paddle.scale(
atten_fc3, scale=(self.item_emb_size + self.cat_emb_size)**-0.5)
# 权重归一化
# Tensor(shape=[32, 1, 2])
weight = paddle.nn.functional.softmax(atten_fc3)
五、GRU的改进AUGRU
我们有attention score后,如何这个分数加到GRU中呢?DIEN论文中比较了三种改进版的GRU,分别为:
- AIGRU(GRU with attentional input)
- AGRU(Attention based GRU)
- AUGRU(GRU with attentional update gate)
5.1 AIGRU(GRU with attentional input)
AIGRU比较直白,直接把attention分数与输入
h
t
h_t
h
t
相乘,即:
i
t
′
=
α
t
∗
h
t
(6)
i^{‘}_t = \alpha_t * h_t \tag{6}
i
t
′
=
α
t
∗
h
t
(
6
)
关于AIGRU的缺点:
In AIGRU, the scale of less related interest can be reduced by the attention score. Ideally, the input value of less re- lated interest can be reduced to zero. However, AIGRU works not very well. Because even zero input can also change the hidden state of GRU, so the less relative inter- ests also affect the learning of interest evolving.
5.2 AGRU(Attention based GRU)
AGRU是被NLP中QA领域中提出的,其直接用attention分数来替换了更新们(update gate),即:
h
t
′
=
(
1
−
α
t
)
∗
h
t
−
1
′
+
α
t
∗
h
^
t
′
(7)
h^{‘}_t = (1-\alpha_t)*h^{‘}_{t-1} + \alpha_t*\hat{h}_t^{‘} \tag{7}
h
t
′
=
(
1
−
α
t
)
∗
h
t
−
1
′
+
α
t
∗
h
^
t
′
(
7
)
AGRU的优缺点如下:
AGRU weakens the effect from less related interest during interest evolving. The embedding of attention into GRU improves the influence of attention mechanism, and helps AGRU overcome the defects of AIGRU.
5.3 AUGRU(GRU with attentional update gate)
因为AGRU用了一个标量(attention分数)替代了update gate,而update gate是个向量,所以存在一定的信息损失。因此,最好的方式就是即保留update gate,又能把attention分数用上,DIEN提出了AUGRU,其公式如下:
u
^
t
′
=
α
t
∗
u
t
′
h
t
′
=
(
1
−
u
^
t
)
∘
h
t
−
1
′
+
u
^
t
′
∘
h
^
t
′
(8)
\hat{u}_t^{‘} = \alpha_t * u^{‘}_t \\ h^{‘}_t = (1-\hat{u}_t) \circ h^{‘}_{t-1} + \hat{u}_t^{‘} \circ \hat{h}^{‘}_t \tag{8}
u
^
t
′
=
α
t
∗
u
t
′
h
t
′
=
(
1
−
u
^
t
)
∘
h
t
−
1
′
+
u
^
t
′
∘
h
^
t
′
(
8
)
关于AUGRU的实现也非常简单,大家实现的是只需要继承下基本的GRU,然后稍作修改即可,我这里用paddle写了下AUGRU:
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle.nn import initializer as I
import math
class AUGRUCell(nn.GRUCell):
def __init__(self,
input_size,
hidden_size,
attention_score,
weight_ih_attr=None,
weight_hh_attr=None,
bias_ih_attr=None,
bias_hh_attr=None,
name=None):
super(AUGRUCell, self).__init__()
if hidden_size <= 0:
raise ValueError(
"hidden_size of {} must be greater than 0, but now equals to {}".
format(self.__class__.__name__, hidden_size))
std = 1.0 / math.sqrt(hidden_size)
self.weight_ih = self.create_parameter(
(3 * hidden_size, input_size),
weight_ih_attr,
default_initializer=I.Uniform(-std, std))
self.weight_hh = self.create_parameter(
(3 * hidden_size, hidden_size),
weight_hh_attr,
default_initializer=I.Uniform(-std, std))
self.bias_ih = self.create_parameter(
(3 * hidden_size, ),
bias_ih_attr,
is_bias=True,
default_initializer=I.Uniform(-std, std))
self.bias_hh = self.create_parameter(
(3 * hidden_size, ),
bias_hh_attr,
is_bias=True,
default_initializer=I.Uniform(-std, std))
self.hidden_size = hidden_size
self.input_size = input_size
self._gate_activation = F.sigmoid
self._activation = paddle.tanh
self.attention_score = attention_score
def forward(self, inputs, states=None):
if states is None:
states = self.get_initial_states(inputs, self.state_shape)
pre_hidden = states
x_gates = paddle.matmul(inputs, self.weight_ih, transpose_y=True)
if self.bias_ih is not None:
x_gates = x_gates + self.bias_ih
h_gates = paddle.matmul(pre_hidden, self.weight_hh, transpose_y=True)
if self.bias_hh is not None:
h_gates = h_gates + self.bias_hh
x_r, x_z, x_c = paddle.split(x_gates, num_or_sections=3, axis=1)
h_r, h_z, h_c = paddle.split(h_gates, num_or_sections=3, axis=1)
# reset gate
r = self._gate_activation(x_r + h_r)
# update gate
z = self._gate_activation(x_z + h_z) * self.attention_score
c = self._activation(x_c + r * h_c) # apply reset gate after mm
h = (pre_hidden - c) * z + c
return h, h
@property
def state_dtype(self):
pass
六、在线工程
纵观整个DIEN模型,其复杂度相比较DIN上升了太多太多,所以线上做infer的时候,势必会增加系统的平响,阿里这里做了非常多的优化,才使得DIEN在线上的平响达到可用状态。所以,如果你的工程团队能力不够强,谨慎使用DIEN。
又到了那句经典的话:决定广告/推荐系统收益的往往是团队的工程能力。
参考文献