推荐系统系列博客:
写在前面:
建议大家看DIN论文的时候看arxiv上的V1版本,传送门链接:DIN v1版本,不要看发表在KDD’18上的正式论文,个人认为正式版本的论文包装的太过厉害,尤其模型部分的图,看完之后完全不理解模型细节。而arxiv上的v1版本则朴实无华,非常容易理解,模型细节呈现的比较清晰。强烈建议看v1版本。
DIN(Deep Interest Network)模型是阿里妈妈盖坤团队发表在KDD’18上的文章,因为有阿里的光环,因此,这个模型在业界还是比较有名气的,至于最终在其他公司场景下有没有效果,取决于对比的baseline,如果你的baseline足够弱,理论上会有一定效果的提升,当然,如果你的baseline够强,可能一点效果都没有。之前博客介绍的模型都在解决如何有效的学到高阶交叉特征,而DIN的核心思想是把attention机制引入了到用户兴趣建模上。
工业界当前广告/推荐的模型基本都是遵循Embedding&MLP(embedding+mlp)的范式,在embedding阶段,所有的离散特征都要被映射成一个固定长度的低维稠密向量。离散特征一般有单值特征和多值特征,分别要做one-hot编码和multi-hot编码,单值特征没什么问题,直接映射成embedding向量即可,而对于多值特征,比如:用户点击过的item序列,用户点击过item类目的序列,通常的操作都是每个item映射成一个embedding向量,然后做一个sum/average pooling,最终得到一个embedding向量。当然,有条件的厂可能还会对这个序列使用一个LSTM,效果嘛,看天吃饭,反正也是有的没的。下图展示了单值特征和多值特征做embedding向量时的区别。
接下来,重点来了,我们在对多值特征做pooling的时候,只是简单的做sum或者average,这相当于认为序列中的每个item的重要性是一样的,所以最好的方式是给每个item一个权重,问题是权重怎么给?依据什么给?DIN这篇paper则非常朴素的认为,因为你的task是CTR预估,那么显然每个item的权重应该由目标广告(商品)与该item之间的相关性决定。
用论文中的一个例子来说明,假设某个用户在最近XX内点击(收藏、或者其他行为)序列为:泳衣、泳帽、薯片、坚果、书籍。候选广告是:护目镜。那么,显然在用户的点击行为序列中,决定这个用户是否点击推荐的护目镜是泳衣和泳帽,因为这两个item与目标候选广告之间相关性更强,因此所做的贡献也就更大。所以,DIN做的就是把传统多值特征做pooling的方式加了个权重,而这个权重是由item个候选广告之间的相关性决定的。
这里我们延伸一下,思考一下,有过工业界广告/推荐经验的同学应该很清楚,在Embedding&MLP的范式架构下,依然需要大量的人工特征工程,其中包含了大量的交叉特征。我个人酷爱的一类交叉特征是:目标与行为的交集及数量。比如上面那个例子,用户历史行为为:泳衣、泳帽、薯片、坚果、书籍,候选广告为护目镜。泳衣、泳帽与护目镜的类目都是相同的,因此我们可以用:护目镜类目与上述用户点击过item的类目做交集及数量,得到的结果为:泳衣类#2,2为交集集合长度(泳帽、泳衣、护目镜都属于泳衣类)。因此,如果有了这个交叉特征,理论上普通的Embedding&MLP效果应该不比DIN差多少,所以,如果你打算尝试DIN,不妨先试试上面我说的方法。
相比较上面我说的方式,DIN里的做法更加soft,更加丝滑。这里不得不感叹,这种小细节,经过比较好的包装,外加一些其他方面的创新,就能够产生一篇高质量的论文,因此,阿里妈妈每年能产出这么多paper也不是没有原因的。
说了这么多,我们来具体剖析下DIN这篇论文,接下来将会从以下几个方面来介绍DIN:
直接上图,来看看DIN的网络结构(图片来自arxiv上DIN v1版论文,KDD的正式版包装的都不认识了,必须吐槽下~)
DIN网络结构整体上来看是在embedding层与MLP(全连接层)之间加入了activation unit。从上图,我们能够看出,用户历史点击过的商品id(good id)只与候选广告的商品id算相关性(上图中是element-wise减,当然你也可以算内积等,甚至可以都算),用户历史点击过的商铺id只与候选广告的商铺id算相关性。
这个模块是整个DIN网络的核心模块,也是该模型的创新之处,如下图所示。
上图比较清晰的展示了activation unit模块是如何工作的,以用户历史行为的商品id序列与候选广告id举例子。
假设用户历史行为:
候选广告:
则activation unit的做法为:
用论文中形式化的公式表达为(虽然个人认为有了上面的例子,公式没必要了,但还是贴出来吧):
V u = f ( V a ) = ∑ i = 1 N w i ∗ V i = ∑ i = 1 N g ( V i , V a ) ∗ V i (1) V_u = f(V_a) = \sum_{i=1}^Nw_i*V_i = \sum_{i=1}^Ng(V_i,V_a)*V_i \tag{1} Vu=f(Va)=i=1∑Nwi∗Vi=i=1∑Ng(Vi,Va)∗Vi(1)
其中, V u V_u Vu表示用户 u u u的向量, V a V_a Va表示广告 a a a的向量, V i V_i Vi表示行为 i i i的向量(比如商品id,商铺id), w i w_i wi表示行为 i i i的attention得分。
论文中给出的是同类id做向量减 ⊝ \circleddash ⊝,当然你可以做内积,甚至都要。DIN作者给出的实现中就使用了内积,参见:https://github.com/zhougr1993/DeepInterestNetwork/blob/master/din/model_dice.py#L241
另外一点是,论文中明确说了他们没有把权重做归一化,论文原文为:
That is, normalization with softmax on the output of a(·) is abandoned. Instead, value of ∑ i w i \sum_iw_i ∑iwi is treated as an approximation of the intensity of activated user interests to some degree.
但在实现的时候,却用了softmax做归一化,参见:https://github.com/zhougr1993/DeepInterestNetwork/blob/master/din/model_dice.py#L221
我们来看下具体的实现(paddle):
"""
hist_item_seq: 用户点击过的商品序列,Tensor(shape=[32, 2],
2-->由该batch内用户点击过的商品序列最大长度决定,比如20。
不够的用0(不在id集合内的任意数字都可以)补全
hist_cat_seq: Tensor(shape=[32, 2], 用户点击过的商品类目序列,同hist_item_seq
target_item: Tensor(shape=[32],推荐广告商品
target_cat: Tensor(shape=[32],推荐广告商品类目
label: Tensor(shape=[32, 1],点击标记
mask: Tensor(shape=[32, 2, 1],mask矩阵,用于hist_item_seq和hist_cat_seq中补 0的部分失效,对于补齐的网格部分,初始化为-INF,从而在sigmoid后,使之失效为0; 目前做法是有数据的为0,补齐的用-INF
target_item_seq: Tensor(shape=[32, 2],因为候选打分广告item要与用户点击过的每个item做减法(或内积,或加法),因此,需要把target_item扩展成与hist_item_seq维度一样,即把target_item重复len(hist_item_seq)次
target_cat_seq: Tensor(shape=[32, 2],同target_item_seq
"""
# Tensor(shape=[32, 2, 128])
hist_seq_concat = paddle.concat([hist_item_emb, hist_cat_emb], axis=2)
# Tensor(shape=[32, 2, 128])
target_seq_concat = paddle.concat(
[target_item_seq_emb, target_cat_seq_emb], axis=2)
# Tensor(shape=[32, 128]
target_concat = paddle.concat(
[target_item_emb, target_cat_emb], axis=1)
# shape=[32, 2, 512]
# 1. 这里不光做element-wise减,还做内积
concat = paddle.concat(
[
hist_seq_concat, target_seq_concat, # shape=[32, 2, 128], shape=[32, 2, 128]
hist_seq_concat - target_seq_concat, # shape=[32, 2, 128]
hist_seq_concat * target_seq_concat # shape=[32, 2, 128]
],
axis=2)
# 2. 输入一个全连接网络
# [Linear(in_features=512, out_features=80, dtype=float32), Sigmoid(),
# Linear(in_features=80, out_features=40, dtype=float32), Sigmoid(),
# Linear(in_features=40, out_features=1, dtype=float32)]
for attlayer in self.attention_layer:
concat = attlayer(concat)
# concat: Tensor(shape=[32, 2, 1]
atten_fc3 = concat + mask
# Tensor(shape=[32, 1, 2]
atten_fc3 = paddle.transpose(atten_fc3, perm=[0, 2, 1])
# Out=scale*X
# Tensor(shape=[32, 1, 2]
atten_fc3 = paddle.scale(atten_fc3, scale=self.firInDim**-0.5)
# 3. 权重归一化
# Tensor(shape=[32, 1, 2]
weight = paddle.nn.functional.softmax(atten_fc3)
# [32, 1, 2]*[32, 2, 128] = [32, 1, 128]
# 4. 权重与用户历史行为序列(商品,商铺)相乘
# Tensor(shape=[32, 1, 128]
output = paddle.matmul(weight, hist_seq_concat)
# Tensor(shape=[32, 128]
output = paddle.reshape(output, shape=[0, self.firInDim])
最后,论文中给出了一张示意图比较清晰的展示了用户历史行为item与广告item之间的attention得分,DIN这种基于attention score的设计比较soft的得到了用户历史行为中每个item对于目标广告item的贡献度。
这部分在KDD版本论文里被称为Mini-batch Aware Regularization,行吧,又是一个高大上的名词。既然是正则技术,显然是为了防止过拟合的,常用的防止过拟合的技术有:L1正则、L2正则和dropout等。不用L2正则的原因一来是计算复杂度相对较高,二来是在广告/推荐领域中导致过拟合的主要原因是数据的长尾分布导致的,很多特征值在整个数据集出现的频率非常低。当然,最常见的处理办法是在训练之前设置个频率阈值,过滤掉低于阈值的特征值,但这种方法多少有点僵硬。
阿里在这篇论文里提出了Adaptive Regularization Technique,即根据特征值的出现频率给其施加不同的正则话强度。当然这个技术基本上就是把李沐大佬论文DiFacto — Distributed Factorization Machines的东西搬过来微调了下,事后来看这么调的原因应该也是实践锤炼出来的。先来看李沐论文里的公式:
w i = w i − η [ 1 b ∑ ( x j , y j ) ∈ B ∂ L ( f ( x j ) , y j ) ∂ w i + λ n i w i I i ] (2) w_i = w_i - \eta [\frac{1}{b}\sum_{(x_j,y_j)\in B}\frac{\partial L(f(x_j),y_j)}{\partial w_i} + \lambda n_iw_iI_i] \tag{2} wi=wi−η[b1(xj,yj)∈B∑∂wi∂L(f(xj),yj)+λniwiIi](2)
其中, B B B表示mini-batch内的样本集合, b b b 表示样本数量,等于batch size。 n i n_i ni表示特征 i i i出现的频率, λ \lambda λ为正则化系数。 I i I_i Ii表示该mini-batch内的样本 j j j是否包含特征 i i i(就是非缺失值),包含则为1,否则为0。
再来看看阿里DIN论文改造之后的公式:
w i = w i − η [ 1 b ∑ ( x j , y j ) ∈ B ∂ L ( f ( x j ) , y j ) ∂ w i + λ 1 n i w i I i ] (3) w_i = w_i - \eta [\frac{1}{b}\sum_{(x_j,y_j)\in B}\frac{\partial L(f(x_j),y_j)}{\partial w_i} + \lambda \frac{1}{n_i}w_iI_i] \tag{3} wi=wi−η[b1(xj,yj)∈B∑∂wi∂L(f(xj),yj)+λni1wiIi](3)
公式(2)和公式(1)唯一的区别就在于 n i n_i ni和 1 n i \frac{1}{n_i} ni1,也就是对频率高的特征究竟是该较大的惩罚,还是较小的惩罚。李沐论文里(公式1)是对出现频率高的特征给予较大的惩罚,而DIN则是对出现频率高的特征给予较小的惩罚。两种方法孰优孰劣,还是要看具体的场景,适用场景的才是最好的,就像公式(2)适用阿里的场景,因为高频率的特征对于用户兴趣的刻画更加准确。
DIN论文里给出了几种方法的比较:
这里主要是对PReLU的改进,PReLU是何凯明大神发表在ICCV’15上的一篇论文中提出的,咱们先看LeakyReLU:
f ( x ) = { x , x > = 0 α x , x < 0 (4) f(x) = \left\{\begin{matrix} x , & x>=0\\ \alpha x , & x<0 \\ \end{matrix}\right. \tag{4} f(x)={x,αx,x>=0x<0(4)
其中, α \alpha α通常设置为0.01。这也是LeakyReLU的引入的一个缺点,即 α \alpha α是个常量,在深度学习的年代里,因为数据量的有恃无恐,一切常量都可变变量,交给网络date-driven的学,因此就有了PReLU, α \alpha α不再是一个固定的值,而是一个超参数,交给网络去学。为了后面和Dice做对比,我们把公式(3)换个等价的形式:
f ( x ) = m a x ( 0 , x ) + α ∗ m i n ( 0 , x ) (5) f(x)=max(0,x)+\alpha∗min(0,x) \tag{5} f(x)=max(0,x)+α∗min(0,x)(5)
下面来看看Dice的公式:
f ( x ) = α ( 1 − p ) x + p x (5) f(x)=\alpha(1-p)x + px \tag{5} f(x)=α(1−p)x+px(5)
上式中的 p = 1 1 + e − x − E ( x ) v a r ( x ) + ϵ p=\frac{1}{1+e^{-\frac{x-E(x)}{\sqrt{var(x)+\epsilon }}}} p=1+e−var(x)+ϵx−E(x)1,其中 E ( x ) E(x) E(x)和 v a r ( x ) var(x) var(x)为mini-batch内的均值和方差,在训练过程中直接计算即可。
GAUC也算是这篇文章的亮点之一,在业界也有比较大的影响。显然GAUC是对AUC的一个改进,GAUC细化到每个用户维度,假设有 n n n个用户,则GAUC的公式:
G A U C = ∑ i = 1 n w i ∗ A U C i ∑ i = 1 n w i = ∑ i = 1 n i m p r e s s i ∗ A U C i ∑ i = 1 n i m p r e s s i (6) GAUC=\frac{\sum_{i=1}^nw_i * AUC_i}{\sum_{i=1}^nw_i } =\frac{\sum_{i=1}^nimpress_i * AUC_i}{\sum_{i=1}^nimpress_i } \tag{6} GAUC=∑i=1nwi∑i=1nwi∗AUCi=∑i=1nimpressi∑i=1nimpressi∗AUCi(6)
其中 w i w_i wi可以是用户的点击数量或者曝光数量。