AI上推荐 之 AFM与DIN模型(当推荐系统遇上了注意力机制)

1. 前言

随着信息技术和互联网的发展, 我们已经步入了一个信息过载的时代,这个时代,无论是信息消费者还是信息生产者都遇到了很大的挑战:

  • 信息消费者:如何从大量的信息中找到自己感兴趣的信息?
  • 信息生产者:如何让自己生产的信息脱颖而出, 受到广大用户的关注?

为了解决这个矛盾, 推荐系统应时而生, 并飞速前进,在用户和信息之间架起了一道桥梁,一方面帮助用户发现对自己有价值的信息, 一方面让信息能够展现在对它感兴趣的用户前面。 推荐系统近几年有了深度学习的助推发展之势迅猛, 从前深度学习的传统推荐模型(协同过滤,矩阵分解,LR, FM, FFM, GBDT)到深度学习的浪潮之巅(DNN, Deep Crossing, DIN, DIEN, Wide&Deep, Deep&Cross, DeepFM, AFM, NFM, PNN, FNN, DRN), 现在正无时无刻不影响着大众的生活。

推荐系统通过分析用户的历史行为给用户的兴趣建模, 从而主动给用户推荐给能够满足他们兴趣和需求的信息, 能够真正的“懂你”。 想上网购物的时候, 推荐系统在帮我们挑选商品, 想看资讯的时候, 推荐系统为我们准备了感兴趣的新闻, 想学习充电的时候, 推荐系统为我们提供最合适的课程, 想消遣放松的时候, 推荐系统为我们奉上欲罢不能的短视频…, 所以当我们淹没在信息的海洋时, 推荐系统正在拨开一层层波浪, 为我们追寻多姿多彩的生活!

这段时间刚好开始学习推荐系统, 通过王喆老师的《深度学习推荐系统》已经梳理好了知识体系, 了解了当前推荐系统领域各种主流的模型架构和技术。 所以接下来的时间就开始对这棵大树开枝散叶,对每一块知识点进行学习总结。 所以接下来一块目睹推荐系统的风采吧!

这次整理重点放在推荐系统的模型方面, 前面已经把传统的推荐模型梳理完毕, 下面正式进入深度学习的浪潮之巅。在2016年, 随着微软的Deep Crossing, 谷歌的Wide&Deep以及FNN、PNN等一大批优秀的深度学习模型被提出, 推挤系统和计算广告领域全面进入了深度学习时代, 时至今日, 依然是主流。 在进入深度学习时代, 推荐模型主要有下面两个进展:

  1. 与传统的机器学习模型相比, 深度学习模型的表达能力更强, 能够挖掘更多数据中隐藏的模式
  2. 深度学习模型结构非常灵活, 能够根据业务场景和数据特点, 灵活调整模型结构, 使模型与应用场景完美契合

所以, 后面开始尝试整理深度学习推荐模型,它们以多层感知机(MLP)为核心, 通过改变神经网络结构进行演化,它们的演化关系依然拿书上的一张图片, 便于梳理关系脉络, 对知识有个宏观的把握:

在这里插入图片描述
今天是深度学习模型的第五篇,这篇文章会介绍两个带有注意力机制神经网络的两个模型, 一个是FM的增强变体AFM(Attentional Factorization Machines), 这个其实算是NFM的一个延伸,在NFM的特征交叉层与池化层中间加了一个注意力网络, 为低阶特征交互特征根据其对预测结果的不同影响程度加上了注意力权重以更加符合实际的推荐场景, 本来想把AFM和FM的那几个哥们放一块,但是感觉那样上一篇篇幅太长了,所以就放到了注意力这部分, 算是承上启下吧, 毕竟从AFM开始, 大佬们不再仅仅局限于特征交互了,而是探索更新的一些结构,与时俱进了嘛。 第二个模型是来自于阿里的知名推荐模型DIN(Deep Interest Network),这个模型基于业务观察的模型改进, 相比较与学术派的深度模型,这个模型更加有业务气息, 当然也是加入了Attention机制, 所以今天整理一下Attention的这两个模型了。

"Attention Mechanism"这个词现在已经不是新东西了,它来源于人类自然的选择注意习惯, 最典型的例子就是我们观察一些物体或者浏览网页时,不会聚焦于整个物体或者页面,而是会选择性的注意某些特定区域,忽视一些区域,往往会把注意力放到某些显眼的地方。 如果在建模过程中考虑到注意力机制对预测结果的影响,往往效果会更好。 近年来,注意力机制在各个领域大放异彩,比如NLP,CV等, 2017年开始,推荐领域也开始尝试将注意力机制加入模型, 就比如今天的这俩哥们, 和前面的逻辑一样, 首先对这两个模型的原理和结构进行描述,然后使用pytorch进行复现进一步加深细节上的理解。

这篇文章的篇幅很长(破4万字了),也是第一次写这么长貌似,因为DIN模型非常之重要(面试常考),所以这里花了点时间看了一些细节,也用了大量篇幅进行整理,包括理论和复现的相关细节,所以依然还是基于大纲,各取所需即可 😉

大纲如下

  • AFM模型的模型原理及论文细节
  • AFM模型的pytorch复现
  • DIN模型的原理及论文细节
  • DIN模型的tensorflow复现
  • 总结

Ok, let’s go!

2. AFM模型的模型原理及论文细节

2.1 AFM模型

AFM(Attentional Factorization Machines)模型也是2017年由浙江大学和新加坡国立大学研究员提出的一个模型, 依然来自何向南教授的团队, 如果看了之前的NFM模型, 理解AFM模型就比较容易了, 该模型是和NFM模型结构上非常相似, 算是NFM模型的一个延伸,在NFM中, 不同特征域的特征embedding向量经过特征交叉池化层的交叉,将各个交叉特征向量进行“加和”, 然后后面跟了一个DNN网络, 这里面的问题是这个加和池化,它相当于“一视同仁”地对待所有交叉特征, 没有考虑不同特征对结果的影响程度,作者认为这可能会影响最后的预测效果, 因为不是所有的交互特征都能够对最后的预测起作用。 没有用的交互特征可能会产生噪声。
在这里插入图片描述
所以作者在提出NFM之后, 又对其进行了改进, 把注意力机制引入到了里面去, 来学习不同交叉特征对于结果的不同影响程度, 这样就使得模型更加符合真实的业务场景, 王喆老师再书中给的例子感觉非常贴切

如果应用场景是预测一位男性用户是否购买一款键盘的可能性, 那么“性别=男且购买历史包含鼠标”这个交叉特征, 很可能比“性别=男且用户年龄=30”这一个交叉特征重要

所以对于NFM来说, 把所有的交叉特征对于预测结果的重要性同等看待,就不是那么合理了。所以通过引入注意力机制, 模型就会在“性别=男且购买历史包含鼠标”的这一个交叉特征上投入更多的“注意力”, 这样两者的结合显得理所应当了,Attention+NFM的组合, 就是AFM模型了。

具体来讲, AFM模型是通过在特征交叉层和最终的输出层之间加入注意力网络来引入了注意力机制, 该模型的网络架构如下:

在这里插入图片描述

2.1.1 Input和embedding层

这个和NFM模型的一样,也是大部分深度学习模型的标配了, 这里为了简单,他们的输入把连续型的特征给省去了, 输入的是稀疏特征, 然后进入embedding层, 得到相应稀疏特征的embedding向量, 这一块的原理这里不做过多赘述。

2.1.2 Pair-wise Interaction Layer

这里的这个和NFM是一样的,采用的也是每对Embedding向量进行各个元素对应相乘(element-wise product)交互, 这个和FM有点不太一样, 那里是每对embedding的内积, 而这里是对应元素相乘(不想加),这个要注意一下。 公式长下面这样子:
f P I ( E ) = { ( v i ⊙ v j ) x i x j } ( i , j ) ∈ R x f_{P I}(\mathcal{E})=\left\{\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}\right\}_{(i, j) \in \mathcal{R}_{x}} fPI(E)={(vivj)xixj}(i,j)Rx

这里的 ⊙ \odot 表示元素对应相乘, R x = { ( i , j ) } i ∈ X , j ∈ X , j > i \mathcal{R}_{x}=\{(i, j)\}_{i \in \mathcal{X}, j \in \mathcal{X}, j>i} Rx={(i,j)}iX,jX,j>i, 这里的 X \mathcal{X} X是非零特征经过embedding层之后得到的embedding集合。 若有 m m m个特征向量的话,那就会产生 m ( m − 1 ) 2 \frac{m(m-1)}{2} 2m(m1)个交互向量。通过定义一个这样的pair-wise Interaction Layer, 就能够在把FM集成到神经网络架构中, 将经过两两交叉的 m ( m − 1 ) 2 \frac{m(m-1)}{2} 2m(m1)个交互向量进行一个sum pooling(聚合),得到输出后再经过一个全连接层,得到最后的预测结果:
y ^ = p T ∑ ( i , j ) ∈ R x ( v i ⊙ v j ) x i x j + b \hat{y}=\mathbf{p}^{T} \sum_{(i, j) \in \mathcal{R}_{x}}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}+b y^=pT(i,j)Rx(vivj)xixj+b

这个 p T p^T pT属于 k k k维的, 因为上面的聚合结合是个 k k k维的隐向量了,如果这里的 p = 1 p=1 p=1的话, 那么结构就是一个FM模型了。但是,这个模型在实际中很难被应用,对于FM模型,我们知道它进行二阶特征交互只需要一个线性时间,而此处该层却是 O ( n 2 ) O(n^2) O(n2) n n n为embedding向量的个数,这损失了原有FM模型的效率。

到这里, 会发现上面这些计算会和NFM非常像, NFM也是在得到稀疏特征的embedding之后, 进行两两交叉然后再求和,送入一个DNN, 所以如果单看到这里,其实就是NFM的这波操作,所以下面,才是本篇论文的一个核心创新 — Attention based Pooling layer。

2.1.3 Attention based Pooling layer

这个想法是不同的特征交互向量在将它们压缩为单个表示时根据对预测结果的影响程度给其加上不同权重, 然后在对其进行求和, 可以看一下上面的结构图, 就是在Pair-wise Interaction Layer和Output layer中间加入了一个Attention注意力网络。计算公式如下:
f A t t ( f P I ( E ) ) = ∑ ( i , j ) ∈ R x a i j ( v i ⊙ v j ) x i x j f_{A t t}\left(f_{P I}(\mathcal{E})\right)=\sum_{(i, j) \in \mathcal{R}_{x}} a_{i j}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j} fAtt(fPI(E))=(i,j)Rxaij(vivj)xixj

其中 a i j a_{ij} aij表示 v i ⊙ v j \mathbf{v}_{i} \odot \mathbf{v}_{j} vivj对的注意力分数, 表示该交互特征对于预测目标的重要性程度。 在直观上讲, 这个注意力分数可以作为参数然后通过最小化预测损失来进行学习,但是对于从未在训练数据中共同出现的特征,就无法估计其交互作用的注意力得分。所以为了解决泛化问题,这里才使用了一个多层感知器(MLP)将注意力得分参数化,就是上面的那个Attention Net。

该注意力网络的结构是一个简单的单全连接层加softmax输出层的结构, 数学表示如下:
a i j = ′ h T Re ⁡ L U ( W ( v i ⊙ v j ) x i x j + b ) a i j = exp ⁡ ( a i j ′ ) ∑ ( i , j ) ∈ R x exp ⁡ ( a i j ′ ) \begin{array}{c} a_{i j=}^{\prime} \boldsymbol{h}^{\mathrm{T}} \operatorname{Re} \mathrm{LU}\left(\boldsymbol{W}\left(\boldsymbol{v}_{i} \odot \boldsymbol{v}_{j}\right) x_{i} x_{j}+\boldsymbol{b}\right) \\ a_{i j}=\frac{\exp \left(a_{i j}^{\prime}\right)}{\sum_{(i, j) \in \mathcal{R}_{x}} \exp \left(a_{i j}^{\prime}\right)} \end{array} aij=hTReLU(W(vivj)xixj+b)aij=(i,j)Rxexp(aij)exp(aij)
学习的模型参数是特征交叉层到注意力网络全连接层的权重矩阵 W W W和偏置向量 b b b以及全连接层到softmax输出层的权重向量 h h h,这三个的维度分别是 W ∈ R t × k , b ∈ R t , h ∈ R t \mathbf{W} \in \mathbb{R}^{t \times k}, b \in \mathbb{R}^{t}, h \in \mathbb{R}^{t} WRt×k,bRt,hRt, 这里的 t t t表示注意力网络隐藏层的单元个数。 a i j a_{ij} aij就表示了每个交互特征的重要性程度,通过softmax之后, 这是一个0-1之间的数值了。注意力网络与整个模型一起参与反向传播过程, 得到最终的权重参数。

2.1.4 Output

基于注意力的池化层的输出是一个 k k k维向量,该向量是所有特征交互向量根据重要性程度进行了区分了之后的一个聚合效果,然后我们将其映射到最终的预测得分中。所以AFM的总体公式如下:
y ^ A F M ( x ) = w 0 + ∑ i = 1 n w i x i + p T ∑ i = 1 n ∑ j = i + 1 n a i j ( v i ⊙ v j ) x i x j \hat{y}_{A F M}(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\mathbf{p}^{T} \sum_{i=1}^{n} \sum_{j=i+1}^{n} a_{i j}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j} y^AFM(x)=w0+i=1nwixi+pTi=1nj=i+1naij(vivj)xixj

关于模型的学习部分, 当然是根据不同的任务来了, 这个模型也是回归任务和分类任务皆可, 并且相对于NFM, 目前上面暂时没有用到DNN网络来学习高阶的交互了, 这个暂定为了作者未来的研究工作。这几本上就是整个AFM的全貌了, 下面依然是在整理一些论文中的细节部分, 方便以后学习。

2.2 论文的一些其他细节

该篇文章的核心思路就是提出了一个新结构 — 注意力网络来对不同的特征交互加权,改善了NFM的特征交互平等性的限制, 其实这里的想法呼应了NFM最后作者提到的,希望在一些结构上进行探索来改进模型,而不是单纯的增加神经网络的层次, 这里真的做到了, 这篇文章中还有一些细节,感觉也是值得学习或者借鉴的,也稍微总结了一下:

  • 模型训练上缓解过拟合,依然是dropout和L2技术,dropout用在了特征交互层, 而L2用在了注意力网络上以防止模型过拟合, 并且发现dropout用较小的神经网络进行模型的平均,会潜在的提高性能, 这个在NFM中也提到过, dropout可以看成是很多较小神经网络的平均,类似于一种bagging了, 而模型的平均效应, 往往使得dropout优于l2正则了
  • 通过相关工作部分,了解了之前其他的两个模型GBFM, 通过梯度提升选择 "好的 "特征,并且只对好的特征之间的交互作用进行建模。对于所选特征之间的交互作用,GBFM用与FM相同的权重对其进行加总,本质上是一种特征选择算法。HOFM模型,使用一组分离的嵌入来模拟每个阶级的特征交互作用, 虽然比FM效果好一些,但是计算复杂度非常高
  • dropout和Batch normalization或许会起冲突, 这个是作者在实验中提到的一个猜想
  • 写作手法上发现了作者在描述实验结果的时候, 先说总体现象(通过表,图等),然后再说具体的变化,最后说结论, 这里拿个例子来看:
    在这里插入图片描述
    这种描述实验的写作手法感觉也可以学习一下。
  • 作者在这里又是做了大量的实验, 和NFM开展实验的方式差不多,也是先抛出3个问题, 然后一一通过实验来进行回答,验证提出的Attention架构, 一些过拟合的预防技术等的作用, 并且还进行了一些调参工作,使得实验看起来非常的饱满,这个感觉也算是一个小经验吧:

    让实验工作看起来更加饱满的小技巧:第一个就是可以把训练的一些技术放进去, 然后进行超参的调整作为一些实验,多说一些结论和对比。 比如这里他们就把正则化的两个技术都用上,然后进行调参。 如果自己改进了一些结构或提出了一些想法,一定要验证有效性, 可以进行各种消融实验,比如提出了一种方法, 那么可以针对不同的模型进行实验,验证方法的有效性,如果是提出了一种结构, 可以可视化这个结构等。然后就是和其他模型的对比实验。如果不够,多对比一些模型。

  • 作者还给出了一个未来的可研究的一个方向感觉也是挺有意思的:
    在这里插入图片描述

AFM到这里就基本上差不多了, 如果对其细节感兴趣,建议读一读原论文。 这里用作者最后的一句话总结感觉最合适不过了:
在这里插入图片描述
这里记录一下子耀大佬的一个总结(具体看下面第四篇链接), 说了一个实际上应用的问题, 增长了一波知识, 感谢一下:

AFM在NFM的基础上,对sum pooling进行改进,通过加入区分不同特征的重要程度的attention网络来提高模型的性能。不过最近通过与一位PDD大佬交流,目前即使做召回也基本不会用AFM、NFM,理由文章中也提到了,AFM太慢。大佬说,目前做召回用的比较多的便是PNN,原因是快。毕竟召回需要的是快速的缩小候选物品的范围,然后再通过复杂的精排模型进行筛选。

AFM是研究人员对改进模型结构角度出发进行的一次有益的尝试, 虽然是考虑到了实际的使用场景, 但是依然是学术上的一个模型,而后面要介绍的DIN, 是阿里巴巴基于实际的业务观察, 将注意力机制与深度学习模型进行了一次融合, 后面我们就可以看看,同样是注意力机制,DIN在实际场景的基础上, 使用注意力又需要考虑哪些因素, 这两种注意力机制又会有哪些不同? 当然在具体介绍DIN之前, 我们再从代码的角度来看一下AFM模型, 在从细节上对其进行一个把握, 尤其是看看这个注意力网络到底是个啥玩意, 又是如何去衡量交互特种的重要性程度的。

3. AFM模型的pytorch复现

看完了理论之后, 我们看一下AFM的实现代码, 这里依然是criteo数据集,由于AFM和NFM很类似,所以可以直接从之前的NFM基础上进行修改, 但是还要需要明白几点不同,这些都在编程上有所体现:

  • 第一点就是NFM那里, embedding交叉完毕之后我们有一个求和的操作, 所以根据之前FM的那个化简公式, 在特征交叉池化层我们只需要一个公式就能搞定交叉和求和的操作。 而这个地方, 我们embedding两两交叉之后, 不能进行求和,需要加注意力。 所以这里我们需要求出两两交叉后的embedding矩阵来,也就是
    f P I ( E ) = { ( v i ⊙ v j ) x i x j } ( i , j ) ∈ R x f_{P I}(\mathcal{E})=\left\{\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}\right\}_{(i, j) \in \mathcal{R}_{x}} fPI(E)={(vivj)xixj}(i,j)Rx
    这个在代码实现上还有点技巧, 可以不写两个for循环。
  • AFM这里的亮点就是注意力网络, 所以这里会实现一个注意力网络, 把上面两两交叉后的embedding进行一个加权操作,这个也是重点,注意力网络说白了就是一个全连接的操作, 在前向传播的过程中, 维度变化要时刻把握着, 这里的公式是
    a i j = ′ h T Re ⁡ L U ( W ( v i ⊙ v j ) x i x j + b ) a i j = exp ⁡ ( a i j ′ ) ∑ ( i , j ) ∈ R x exp ⁡ ( a i j ′ ) \begin{array}{c} a_{i j=}^{\prime} \boldsymbol{h}^{\mathrm{T}} \operatorname{Re} \mathrm{LU}\left(\boldsymbol{W}\left(\boldsymbol{v}_{i} \odot \boldsymbol{v}_{j}\right) x_{i} x_{j}+\boldsymbol{b}\right) \\ a_{i j}=\frac{\exp \left(a_{i j}^{\prime}\right)}{\sum_{(i, j) \in \mathcal{R}_{x}} \exp \left(a_{i j}^{\prime}\right)} \end{array} aij=hTReLU(W(vivj)xixj+b)aij=(i,j)Rxexp(aij)exp(aij)
  • 第三个就是DNN网络, AFM里面目前是没有用DNN网络的,也就是注意力加权之后求和, 然后映射到输出, 由于我这里是拿过来的NFM的代码,所以这里可以灵活地加一个DNN的操作, 当然也可以不加, 我尝试了一下, 加入DNN的时候, 效果要靠谱些。

基于上面的三点分析, 我们来实现AFM网络, 首先是DNN, 这个和NFM一模一样, 不再过多赘述了。直接上代码:

class Dnn(nn.Module):
    def __init__(self, hidden_units, dropout=0.):
        """
        hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度
        dropout = 0.
        """
        super(Dnn, self).__init__()
        
        self.dnn_network = nn.ModuleList([nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))])
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x):  
        for linear in self.dnn_network:
            x = linear(x)
            x = F.relu(x)    
        x = self.dropout(x) 
        return x

这个代码的好处是层数和神经元可调多变, 只需要传入不同的hidden_units即可。

接下来是我们的重头戏, Attention网络, 这个的实现方式是一个全连接网络, 这里主要是分析一下这个维度的变化情况。这个网络接收的输入是一个三维张量bi_interaction, 也就是各个特征的embedding两两交叉后的向量。 假设离散特征的数目是field_num, embedding的维度是embed_dim, 那么这个三维张量的维度就是(batch_size, (field_num*(field_num-1)/2, embed_dim), 我们前向传播的逻辑是先经过一个全连接, 也就是公式里面的W矩阵, 这里是一个nn.Linear层, 输入维度是embed_dim, 输出维度是隐藏单元的个数,然后relu函数激活, 接下来加入一个1维的全连接得到每个特征交叉向量的分数, 再用softmax激活就得到了每个特征交叉向量的权重, 权重乘上相应的特征交叉向量即可得到Attention 层最后的输出。这就是前向传播的逻辑了, 下面看具体代码感受 一下:

class Attention_layer(nn.Module):
    def __init__(self, att_units):
        """
        :param att_units: [embed_dim, att_vector]
        """
        super(Attention_layer, self).__init__()
        
        self.att_w = nn.Linear(att_units[0], att_units[1])
        self.att_dense = nn.Linear(att_units[1], 1)
    
    def forward(self, bi_interaction):     # bi_interaction (None, (field_num*(field_num-1)_/2, embed_dim)
        a = self.att_w(bi_interaction)    # (None, (field_num*(field_num-1)_/2, t)
        a = F.relu(a)             # (None, (field_num*(field_num-1)_/2, t)
        att_scores = self.att_dense(a)  # (None, (field_num*(field_num-1)_/2, 1)
        att_weight = F.softmax(att_scores, dim=1)  #  (None, (field_num*(field_num-1)_/2, 1)

        att_out = torch.sum(att_weight * bi_interaction, dim=1)   # (None, embed_dim)
        return att_out     

有了前面的两个组块, 后面就是AFM模型的架构了, 这里采用了比较灵活的方式, att可选, DNN可选, 这里还有一个小技巧就是特征embedding两两交叉的那个结果矩阵, 两两交叉可以使用两个for循环的方式,类似

for i in range(len(field_num)):
	for j in range(i, len(field_num)):
		第i个embedding和第j个embedding交叉存入到结果   # 这样得到的就是field_num*(field_num-1) /2 的组合数

这里没有采用这种for循环,才是采用了排列组合的方式,求得组合数,然后选出相应位置的embedding,最后相乘得到的。 假设有4个特征embedding的话,下标位置是[0,1,2,3], 考虑两两位置交叉, 那么位置就是[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]的6种交叉,这个会发现正好是个组合数, 我们直接用itertools.combinations函数产生上面的组合数,然后把左边位置上的这些embedding存到一个p矩阵, 右边位置上的embedding存入一个q矩阵, 然后两者对应位置的embedding相乘就是交叉结果了。 这样应该会快不少, 其他的就没有啥新鲜的了, 直接看代码吧:

class AFM(nn.Module):
    def __init__(self, feature_columns, mode, hidden_units, att_vector=8, dropout=0.5, useDNN=False):
        """
        AFM:
        :param feature_columns: 特征信息, 这个传入的是fea_cols array[0] dense_info  array[1] sparse_info
        :param mode: A string, 三种模式, 'max': max pooling, 'avg': average pooling 'att', Attention
        :param att_vector: 注意力网络的隐藏层单元个数
        :param hidden_units: DNN网络的隐藏单元个数, 一个列表的形式, 列表的长度代表层数, 每个元素代表每一层神经元个数, lambda文里面没加
        :param dropout: Dropout比率
        :param useDNN: 默认不使用DNN网络
        """
        super(AFM, self).__init__()
        self.dense_feature_cols, self.sparse_feature_cols = feature_columns
        self.mode = mode
        self.useDNN = useDNN
        
        # embedding
        self.embed_layers = nn.ModuleDict({
            'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim'])
            for i, feat in enumerate(self.sparse_feature_cols)
        })
        
        # 如果是注意机制的话,这里需要加一个注意力网络
        if self.mode == 'att':   
            self.attention = Attention_layer([self.sparse_feature_cols[0]['embed_dim'], att_vector])
            
        # 如果使用DNN的话, 这里需要初始化DNN网络
        if self.useDNN:
            # 这里要注意Pytorch的linear和tf的dense的不同之处, 前者的linear需要输入特征和输出特征维度, 而传入的hidden_units的第一个是第一层隐藏的神经单元个数,这里需要加个输入维度
            self.fea_num = len(self.dense_feature_cols) + self.sparse_feature_cols[0]['embed_dim']
            hidden_units.insert(0, self.fea_num)

            self.bn = nn.BatchNorm1d(self.fea_num)     
            self.dnn_network = Dnn(hidden_units, dropout)
            self.nn_final_linear = nn.Linear(hidden_units[-1], 1)
        else:
            self.fea_num = len(self.dense_feature_cols) + self.sparse_feature_cols[0]['embed_dim']
            self.nn_final_linear = nn.Linear(self.fea_num, 1)
    
    def forward(self, x):
        dense_inputs, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):]
        sparse_inputs = sparse_inputs.long()       # 转成long类型才能作为nn.embedding的输入
        sparse_embeds = [self.embed_layers['embed_'+str(i)](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
        sparse_embeds = torch.stack(sparse_embeds)     # embedding堆起来, (field_dim, None, embed_dim)
        sparse_embeds = sparse_embeds.permute((1, 0, 2))
        # 这里得到embedding向量之后 sparse_embeds(None, field_num, embed_dim)
        # 下面进行两两交叉, 注意这时候不能加和了,也就是NFM的那个计算公式不能用, 这里两两交叉的结果要进入Attention
        # 两两交叉enbedding之后的结果是一个(None, (field_num*field_num-1)/2, embed_dim) 
        # 这里实现的时候采用一个技巧就是组合
        #比如fild_num有4个的话,那么组合embeding就是[0,1] [0,2],[0,3],[1,2],[1,3],[2,3]位置的embedding乘积操作
        first = []
        second = []
        for f, s in itertools.combinations(range(sparse_embeds.shape[1]), 2):
            first.append(f)
            second.append(s)
        # 取出first位置的embedding  假设field是3的话,就是[0, 0, 0, 1, 1, 2]位置的embedding
        p = sparse_embeds[:, first, :]     # (None, (field_num*(field_num-1)_/2, embed_dim)
        q = sparse_embeds[:, second, :]   # (None, (field_num*(field_num-1)_/2, embed_dim)
        bi_interaction = p * q    # (None, (field_num*(field_num-1)_/2, embed_dim)
        
        if self.mode == 'max':
            att_out = torch.sum(bi_interaction, dim=1)  #  (None, embed_dim)
        elif self.mode == 'avg':
            att_out = torch.mean(bi_interaction, dim=1)  # (None, embed_dim)
        else:
            # 注意力网络
            att_out = self.attention(bi_interaction)  # (None, embed_dim)
        
        # 把离散特征和连续特征进行拼接
        x = torch.cat([att_out, dense_inputs], dim=-1)
        
        if not self.useDNN:
            outputs = F.sigmoid(self.nn_final_linear(x))
        else:
            # BatchNormalization
            x = self.bn(x)
            # deep
            dnn_outputs = self.nn_final_linear(self.dnn_network(x))
            outputs = F.sigmoid(dnn_outputs)
        
        return outputs

后面使用的时候非常简单, 直接

# 建立模型
hidden_units = [128, 64, 32]
dnn_dropout = 0.

model = AFM(fea_cols, 'att', hidden_units, dropout=dnn_dropout, useDNN=True)

这样就把AFM模型建立完毕了。 依然是具体的细节这里不介绍, 我已经放后面的GitHub链接了。

关于AFM模型的pytorch实现就到这里了, 上面也说过了AFM是Attention在推荐系统中的伟大尝试, 但没有用到具体的应用场景, 阿里在2018年的时候, 基于业务观察, 在深度学习中引入了注意力机制, 这就是业界非常知名的DIN模型了。下面来看看这个吧。

4. DIN模型的原理及论文细节

这个模型非常重要, 基于业务的观察,从实际应用的角度进行的模型改进,完全符合以需求为导向的创新原则, 这个模型的论文写得非常精彩,建议去读原文, 里面不仅提出了DIN模型,并基于真实场景下大规模数据集的模型训练问题,提出了两种重要的训练技术。建议去读原文, 这里我也会用稍微大点的篇幅来整理这篇论文, 写的是真好,读完之后受益良多。 由于我这是第一遍阅读,难免会有疏漏,欢迎帮忙指出来,后面也会结合工业上的一些实际应用经验再进行一些补充,下面开始。

Deep Interest Network(DIN)是2018年阿里巴巴提出来的模型, 相比于之前很多“学术风”的深度模型, 该模型更加具有业务气息。该模型的应用场景是阿里巴巴的电商广告推荐业务, 这样的场景下一般会有大量的用户历史行为信息, 这个其实是很关键的,因为DIN模型的创新点或者解决的问题就是使用了注意力机制来对用户的兴趣动态模拟, 而这个模拟过程存在的前提就是用户之前有大量的历史行为了,这样我们在预测某个商品广告用户是否点击的时候,就可以参考他之前购买过或者查看过的商品,这样就能猜测出用户的大致兴趣来,这样我们的推荐才能做的更加到位,所以这个模型的使用场景是非常注重用户的历史行为特征(历史购买过的商品或者类别信息)

对于电商广告的推荐场景,还要了解下面三点:

  1. 用户兴趣多种多样,并变化多端
  2. 捕捉用户兴趣点非常重要
  3. 用户的兴趣往往可以在其历史行为中进行学习

了解完了使用场景之后,下面我们得看看之前的深度模型在这样的一个场景下出现的一个瓶颈, 之前的模型作者这里给了个统称叫做Embeding&MLP模型,也就是后面要介绍的基线模型, 这样的模型对于这种推荐任务一般有着差不多的固定处理套路,就是大量稀疏特征先经过embedding层, 转成低维稠密的,然后进行拼接,最后喂入到多层神经网络中去。 这些模型在这种个性化广告点击预测任务中存在的问题就是无法表达用户广泛的兴趣,因为这些模型在得到各个特征的embedding之后,就蛮力拼接了,然后就各种交叉等。这时候根本没有考虑之前用户历史行为商品具体是什么,究竟用户历史行为中的哪个会对当前的点击预测带来积极的作用。 而实际上,对于用户点不点击当前的商品广告,很大程度上是依赖于他的历史行为的,王喆老师举了个例子

假设广告中的商品是键盘, 如果用户历史点击的商品中有化妆品, 包包,衣服, 洗面奶等商品, 那么大概率上该用户可能是对键盘不感兴趣的, 而如果用户历史行为中的商品有鼠标, 电脑,iPad,手机等, 那么大概率该用户对键盘是感兴趣的, 而如果用户历史商品中有鼠标, 化妆品, T-shirt和洗面奶, 鼠标这个商品embedding对预测“键盘”广告的点击率的重要程度应该大于后面的那三个。

这里也就是说如果是之前的那些深度学习模型,是没法很好的去表达出用户这广泛多样的兴趣的,如果想表达的准确些, 那么就得加大隐向量的维度,让每个特征的信息更加丰富, 那这样带来的问题就是计算量上去了,毕竟真实情景尤其是电商广告推荐的场景,特征维度的规模是非常大的。 并且根据上面的例子, 也并不是用户所有的历史行为特征都会对某个商品广告点击预测起到作用。所以对于当前某个商品广告的点击预测任务,没必要考虑之前所有的用户历史行为。

这样一个对模型改进的动机就出来了,在业务的角度,我们应该自适应的去捕捉用户的兴趣变化,这样才能较为准确的实施广告推荐;而放到模型的角度, 我们应该考虑到用户的历史行为商品与当前商品广告的一个关联性,如果用户历史商品中很多与当前商品关联,那么说明该商品可能符合用户的品味,就把该广告推荐给他。

而一谈到关联性的话, 我们就容易想到“注意力”的思想了, 所以为了更好的从用户的历史行为中学习到与当前商品广告的关联性,学习到用户的兴趣变化, 作者把注意力引入到了模型,设计了一个"local activation unit"结构,利用候选商品和历史问题商品之间的相关性计算出权重,这个就代表了对于当前商品广告的预测,用户历史行为的各个商品的重要程度大小, 而加入了注意力权重的深度学习网络,就是这次的主角DIN。

在这里插入图片描述
所以上面这些就是DIN模型的相关应用场景和提出动机了。下面再来看下背景,也就是电商广告推荐到底是怎么回事, 毕竟这篇论文偏工程些,有些背景得了解,这里作者在论文中介绍了下。

4.1 电商广告推荐背景

在电商网站,比如阿里巴巴, 广告也是一种商品, 所以广告推荐类似于商品推荐,一般这种广告推荐也是两个主要的阶段:召回和排序
在这里插入图片描述

  • matching stage: 这里知道了原来召回阶段就是matching呀,之前听到过好多次,但一直不知道这个matching是个啥, 这个阶段任务就是通过协同过滤等方法生成与访问用户相关的候选广告列表
  • 排序阶段: 就是通过排序模型来预测用户对于候选广告的点击概率,然后根据这个概率生成一个广告推荐列表

大部分的推荐场景下都会有这两大步骤, 而商品广告推荐和一些其他推荐有些区别的是很注重用户的历史行为,因为这个直接与用户的兴趣相关, 而用户兴趣又反过来和商品挂钩。作者这里举了个例子:

在这里插入图片描述
这里我一直在强调用户的历史行为,其实是暗示该模型的一个应用场景,因为看完之后,就会发现DIN模型的应用很重要的一类特征是User Behavior features,也就是用户历史购买过或者点击过的商品特征, 所以后面我们会用到一个新的数据集,和之前那些深度学习模型用的数据集criteo的最大不同之处就在于此。

好了, 铺垫工作结束,下面我们就来看这个神秘的DIN网络了。

4.2 DIN模型

在具体分析DIN模型之前, 我们还得先介绍两块小内容,一个是DIN模型的数据集和特征表示, 一个是上面提到的之前深度学习模型的基线模型, 有了这两个, 再看DIN模型,就感觉是水到渠成了。

4.2.1 特征表示

工业上的CTR预测数据集一般都是multi-group categorial form的形式,就是类别型特征最为常见,这种数据集一般长这样:

在这里插入图片描述
这里的亮点就是框出来的那个特征,这个包含着丰富的用户兴趣信息。

对于特征编码,作者这里举了个例子:[weekday=Friday, gender=Female, visited_cate_ids={Bag,Book}, ad_cate_id=Book], 这种情况我们知道一般是通过one-hot的形式对其编码, 转成系数的二值特征的形式。但是这里我们会发现一个visted_cate_ids, 也就是用户的历史商品列表, 对于某个用户来讲,这个值是个多值型的特征, 而且还要知道这个特征的长度不一样长,也就是用户购买的历史商品个数不一样多,这个显然。这个特征的话,我们一般是用到multi-hot编码,也就是可能不止1个1了,有哪个商品,对应位置就是1, 所以经过编码后的数据长下面这个样子:
在这里插入图片描述
这个就是喂入模型的数据格式了,这里还要注意一点 就是上面的特征里面没有任何的交互组合,也就是没有做特征交叉。这个交互信息交给后面的神经网络去搞。

4.2.2 基线模型

这里的base 模型,就是上面提到过的Embedding&MLP的形式, 这个之所以要介绍,就是因为DIN网络的基准也是他,只不过在这个的基础上添加了一个新结构(注意力网络)来学习当前候选广告与用户历史行为特征的相关性,从而动态捕捉用户的兴趣。

基准模型的结构相对比较简单,我们前面也一直用这个基准, 分为三大模块:Embedding layer,Pooling & Concat layer和MLP, 结构如下:
在这里插入图片描述
前面的大部分深度模型结构也是遵循着这个范式套路, 由于前面也都介绍的差不多,这里就简介一下各个模块。

  1. Embedding layer:这个层的作用是把高维稀疏的输入转成低维稠密向量, 每个离散特征下面都会对应着一个embedding词典, 维度是 D × K D\times K D×K, 这里的 D D D表示的是隐向量的维度, 而 K K K表示的是当前离散特征的唯一取值个数nunique(), 这里为了好理解,直接举个例子说明,这是第一次在这里剖析embedding的计算细节,就比如上面的weekday特征:

    假设某个用户的weekday特征就是周五,化成one-hot编码的时候,就是[0,0,0,0,1,0,0]表示,这里如果再假设隐向量维度是D, 那么这个特征对应的embedding词典是一个 D × 7 D\times7 D×7的一个矩阵(每一列代表一个embedding,7列正好7个embedding向量,对应周一到周日),那么该用户这个one-hot向量经过embedding层之后会得到一个 D × 1 D\times1 D×1的向量,也就是周五对应的那个embedding,怎么算的,其实就是 e m b e d d i n g 矩 阵 ∗ [ 0 , 0 , 0 , 0 , 1 , 0 , 0 ] T embedding矩阵* [0,0,0,0,1,0,0]^T embedding[0,0,0,0,1,0,0]T 。其实也就是直接把embedding矩阵中one-hot向量为1的那个位置的embedding向量拿出来。 这样就得到了稀疏特征的稠密向量了。

    其他离散特征也是同理,只不过上面那个multi-hot编码的那个,会得到一个embedding向量的列表,因为他开始的那个multi-hot向量不止有一个是1,这样乘以embedding矩阵,就会得到一个列表了。所以作者这里这样说:
    在这里插入图片描述
    通过这个层,上面的输入特征都可以拿到相应的稠密embedding向量了。

  2. pooling layer and Concat layer: pooling层的作用是将用户的历史行为embedding这个最终变成一个定长的向量,因为每个用户历史购买的商品数是不一样的, 也就是每个用户multi-hot中1个个数不一致,这样经过embedding层,得到的用户历史行为embedding的个数不一样多,也就是上面的embedding列表 t i t_i ti不一样长, 那么这样的话,每个用户的历史行为特征拼起来就不一样长了。 而后面如果加全连接网络的话,我们知道,他需要定长的特征输入。 所以往往用一个pooling layer先把用户历史行为embedding变成固定长度(统一长度),所以有了这个公式:
    e i = p o o l i n g ( e i 1 , e i 2 , . . . e i k ) e_i=pooling(e_{i1}, e_{i2}, ...e_{ik}) ei=pooling(ei1,ei2,...eik)
    这里的 e i j e_{ij} eij是用户历史行为的那些embedding。 e i e_i ei就变成了定长的向量, 这里的 i i i表示第 i i i个历史特征组(是历史行为,比如历史的商品id,历史的商品类别id等), 这里的 k k k表示对应历史特种组里面用户购买过的商品数量,也就是历史embedding的数量,看上面图里面的user behaviors系列,就是那个过程了。 Concat layer层的作用就是拼接了,就是把这所有的特征embedding向量,如果再有连续特征的话也算上,从特征维度拼接整合,作为MLP的输入。

  3. MLP:这个就不多说了, 这个就是普通的全连接,用了学习特征之间的各种交互。

  4. Loss: 由于这里是点击率预测任务, 二分类的问题,所以这里的损失函数用的负的log对数似然:
    L = − 1 N ∑ ( x , y ) ∈ S ( y log ⁡ p ( x ) + ( 1 − y ) log ⁡ ( 1 − p ( x ) ) ) L=-\frac{1}{N} \sum_{(\boldsymbol{x}, y) \in \mathcal{S}}(y \log p(\boldsymbol{x})+(1-y) \log (1-p(\boldsymbol{x}))) L=N1(x,y)S(ylogp(x)+(1y)log(1p(x)))

这就是base 模型的全貌, 这里我们其实就可以看到这种模型的不足之处了,作者这里一语中的:

在这里插入图片描述
通过上面的图也能看出来, 用户的历史行为特征和当前的候选广告特征在全都拼起来给神经网络之前,是一点交互的过程都没有, 而拼起来之后给神经网络,虽然是有了交互了,但是原来的一些信息,比如,每个历史商品的信息会丢失了一部分,因为这个与当前候选广告商品交互的是池化后的历史特征embedding, 这个embedding是综合了所有的历史商品信息, 这个通过我们前面的分析,对于预测当前广告点击率,并不是所有历史商品都有用,综合所有的商品信息反而会增加一些噪声性的信息,可以联想上面举得那个键盘鼠标的例子,如果加上了各种洗面奶,衣服啥的反而会起到反作用。其次就是这样综合起来,已经没法再看出到底用户历史行为中的哪个商品与当前商品比较相关,也就是丢失了历史行为中各个商品对当前预测的重要性程度。最后一点就是如果所有用户浏览过的历史行为商品,最后都通过embedding和pooling转换成了固定长度的embedding,这样会限制模型学习用户的多样化兴趣。

那么改进这个问题的思路有哪些呢? 第一个就是加大embedding的维度,增加之前各个商品的表达能力,这样即使综合起来,embedding的表达能力也会加强, 能够蕴涵用户的兴趣信息,但是这个在大规模的真实推荐场景计算量超级大,不可取。 另外一个思路就是在当前候选广告和用户的历史行为之间引入注意力的机制,这样在预测当前广告是否点击的时候,让模型更关注于与当前广告相关的那些用户历史产品,也就是说与当前商品更加相关的历史行为更能促进用户的点击行为。 作者这里又举了之前的一个例子:

想象一下,当一个年轻母亲访问电子商务网站时,她发现展示的新手袋很可爱,就点击它。让我们来分析一下点击行为的驱动力。

展示的广告通过软搜索这位年轻母亲的历史行为,发现她最近曾浏览过类似的商品,如大手提袋和皮包,从而击中了她的相关兴趣

第二个思路就是DIN的改进之处了。作者这里的这句话最能表达DIN的原理或者功能:

在这里插入图片描述
DIN通过给定一个候选广告,然后去注意与该广告相关的局部兴趣的表示来模拟此过程。 DIN不会通过使用同一向量来表达所有用户的不同兴趣,而是通过考虑历史行为的相关性来自适应地计算用户兴趣的表示向量(对于给的的广告)。 该表示向量随不同广告而变化。下面看一下DIN模型。

4.2.3 DIN模型架构

上面分析完了base模型的不足和改进思路之后,DIN模型的结构就呼之欲出了,首先,它依然是采用了基模型的结构,只不过是在这个的基础上加了一个注意力机制来学习用户兴趣与当前候选广告间的关联程度, 用论文里面的话是,引入了一个新的local activation unit, 这个东西用在了用户历史行为特征上面, 能够根据用户历史行为特征和当前广告的相关性给用户历史行为特征embedding进行加权。我们先看一下它的结构,然后看一下这个加权公式。
在这里插入图片描述
这里改进的地方我已经框出来了,这里会发现相比于base model, 这里加了一个local activation unit, 这里面是一个前馈神经网络,输入是用户历史行为商品和当前的候选商品, 输出是它俩之间的相关性, 这个相关性相当于每个历史商品的权重,把这个权重与原来的历史行为embedding相乘求和就得到了用户的兴趣表示 v U ( A ) \boldsymbol{v}_{U}(A) vU(A), 这个东西的计算公式如下:
v U ( A ) = f ( v A , e 1 , e 2 , … , e H ) = ∑ j = 1 H a ( e j , v A ) e j = ∑ j = 1 H w j e j \boldsymbol{v}_{U}(A)=f\left(\boldsymbol{v}_{A}, \boldsymbol{e}_{1}, \boldsymbol{e}_{2}, \ldots, \boldsymbol{e}_{H}\right)=\sum_{j=1}^{H} a\left(\boldsymbol{e}_{j}, \boldsymbol{v}_{A}\right) \boldsymbol{e}_{j}=\sum_{j=1}^{H} \boldsymbol{w}_{j} \boldsymbol{e}_{j} vU(A)=f(vA,e1,e2,,eH)=j=1Ha(ej,vA)ej=j=1Hwjej
这里的 { v A , e 1 , e 2 , … , e H } \{\boldsymbol{v}_{A}, \boldsymbol{e}_{1}, \boldsymbol{e}_{2}, \ldots, \boldsymbol{e}_{H}\} {vA,e1,e2,,eH}是用户 U U U的历史行为特征embedding, v A v_{A} vA表示的是候选广告 A A A的embedding向量, a ( e j , v A ) = w j a(e_j, v_A)=w_j a(ej,vA)=wj表示的权重或者历史行为商品与当前广告 A A A的相关性程度。 a ( ⋅ ) a(\cdot) a()表示的上面那个前馈神经网络,也就是那个所谓的注意力机制, 当然,看图里的话,输入除了历史行为向量和候选广告向量外,还加了一个它俩的外积操作,作者说这里是有利于模型相关性建模的显性知识。

这里有一点需要特别注意,就是这里的权重加和不是1, 准确的说这里不是权重, 而是直接算的相关性的那种分数作为了权重,也就是平时的那种scores(softmax之前的那个值),这个是为了保留用户的兴趣强度,作者在这里还举了一个例子:

在这里插入图片描述

这就是DIN的全貌了,关于DIN模型的原理方面介绍到这里,应该把这个模型说明白了吧。

接下来介绍作者提到的两个训练技术,这两个也是本篇论文的创新之处, 这个是基于真实的工业实战经验提出来的, 所以在真实大数据应用层面上会非常有价值(这个在实验室是体会不到的)

4.3 两个训练技术

在阿里巴巴的广告系统中,商品和用户的数量达到数亿。在实践中,训练具有大规模稀疏输入特征的工业深度网络是一个很大的挑战。下面介绍两种重要的技术,它们在实践中被证明是有帮助的。

4.3.1 Mini-batch Aware Regularization

这个是一种正则化的方式,防止模型过拟合用的, 它中文名字不知道叫啥,感觉翻译的不是太好,所以还是用英文名字吧,简称MAR方法,这个方法是针对于L2正则进行改进的,这里作者指出了大规模数据集下L2正则的不合理性:
在这里插入图片描述
这里简单的理解就是为了防止模型过拟合,我们一般会加入正则化, 而L2正则化加入的时候,是对于所有的参数都会起作用, 而像这种真实数据集中,每个mini-batch的样本特征是非常稀疏的,我们看之前那个样本编码的例子就能看到,这种情况下根本就没有必要考虑所有的参数进去,这个复杂度会非常大, 而仅仅约束那些在当前mini-batch样本中出现的特征(不为0的那些特征)embedding就可以啦。所以这个就是作者的改进思路,这个方法就叫做mini-batch aware 正则。
在这里插入图片描述

作者首先说模型训练过程中,造成复杂度的主要原因是每个特征对应的embedding字典矩阵的更新,就是那个 D × K i D\times K_i D×Ki的大矩阵, 如果我们能把这个的计算量降下去, 模型训练就不成问题了,所以这里的正则也主要是放到了这个东西上去。而按照作者提出的这个改进思路, 在全部样本上的正则化公式如下:
L 2 ( W ) = ∥ W ∥ 2 2 = ∑ j = 1 K ∥ w j ∥ 2 2 = ∑ ( x , y ) ∈ S ∑ j = 1 K I ( x j ≠ 0 ) n j ∥ w j ∥ 2 2 L_{2}(\mathbf{W})=\|\mathbf{W}\|_{2}^{2}=\sum_{j=1}^{K}\left\|\boldsymbol{w}_{j}\right\|_{2}^{2}=\sum_{(\boldsymbol{x}, y) \in \mathcal{S}} \sum_{j=1}^{K} \frac{I\left(\boldsymbol{x}_{j} \neq 0\right)}{n_{j}}\left\|\boldsymbol{w}_{j}\right\|_{2}^{2} L2(W)=W22=j=1Kwj22=(x,y)Sj=1KnjI(xj=0)wj22
这个公式看懂了, 下面的mini-batch上的就好说了, 首先正则化, 是要约束embedding参数,不让它太大。如果我们对比之前的L2正则来看的话, 关键地方就是后面的 I ( x j ≠ 0 ) n j \frac{I\left(\boldsymbol{x}_{j} \neq 0\right)}{n_{j}} njI(xj=0)项, 这个也正是作者说的那个对出现过(不为0)的特征embedding参数加的约束, 像L2的话是对所有特征embedding参数都加约束的。 而这里由于每个样本里面有大量特征为0,对于这样的样本,作者说没有必要对其所有特征embedding参数都加约束。所以上面那个项中,分子是一个示性函数, 如果样本的某个特征取值不是0,其对应的embedding参数才加约束, 而分母是表示当前特征不是0(出现过)的样本个数。这里的 K K K是特征个数。 这个公式也正好是只对出现过的特征embedding的参数施加正则约束。 如果换成mini-batch的话,是下面这样子:
L 2 ( W ) = ∑ j = 1 K ∑ m = 1 B ∑ ( x , y ) ∈ B m I ( x j ≠ 0 ) n j ∥ w j ∥ 2 2 L_{2}(\mathbf{W})=\sum_{j=1}^{K} \sum_{m=1}^{B} \sum_{(\boldsymbol{x}, y) \in \mathcal{B}_{m}} \frac{I\left(\boldsymbol{x}_{j} \neq 0\right)}{n_{j}}\left\|\boldsymbol{w}_{j}\right\|_{2}^{2} L2(W)=j=1Km=1B(x,y)BmnjI(xj=0)wj22
上面理解了,这个的话很容易理解, 加了一个mini-batch, 也就是mini-batch中只对出现过的特征embedding参数加约束。上面公式的 ( x , y ) (x,y) (x,y)属于整个训练集,这里的属于某个mini-batch,所以前面才有了一个mini-batch之间的遍历加和项, 不再过多解释,而这里作者为了简单, 把示性函数这个换成了一个固定值,毕竟各个样本在每个不同的特征下取值为1的个数会不同, 算起来会有些麻烦,所以作者在这里取了个近似,让 α m j = max ⁡ ( x , y ) ∈ B m I ( x j ≠ 0 ) \alpha_{m j}=\max _{(x, y) \in \mathcal{B}_{m}} I\left(x_{j} \neq 0\right) \mathrm{} αmj=max(x,y)BmI(xj=0), 也就是在各个样本中出现最多的那个特征出现的次数(感觉不太好描述,想象每一行算一个样本,每一列算一个特征, 竖着看,每一列1最多的那个特征就是对应的 x j x_j xj, 而1出现的个数就是这里的 a m j a_{mj} amj), 这样计算就统一起来了,所以公式约等于了下面这个:
L 2 ( W ) ≈ ∑ j = 1 K ∑ m = 1 B α m j n j ∥ w j ∥ 2 2 L_{2}(\mathbf{W}) \approx \sum_{j=1}^{K} \sum_{m=1}^{B} \frac{\alpha_{m j}}{n_{j}}\left\|\boldsymbol{w}_{j}\right\|_{2}^{2} L2(W)j=1Km=1Bnjαmjwj22
也就是以那个出现次数最多的那个特征为基准进行embedding参数约束了,这样的计算方式能使得工业上模型训练成为可能。

4.3.2 Data Adaptive Activation Function

这里提出了一个随着数据分布而动态调整的自适应激活函数, 是泛化的PRelu,这个是在神经网络中非常常用的一个激活函数, 作者把它用在了注意力的那个网络里面, 这个激活函数的公式如下:
f ( s ) = { s  if  s > 0 α s  if  s ≤ 0 = p ( s ) ⋅ s + ( 1 − p ( s ) ) ⋅ α s f(s)=\left\{\begin{array}{ll} s & \text { if } s>0 \\ \alpha s & \text { if } s \leq 0 \end{array}=p(s) \cdot s+(1-p(s)) \cdot \alpha s\right. f(s)={sαs if s>0 if s0=p(s)s+(1p(s))αs
这里 p ( s ) p(s) p(s)的函数图像如下(左):
在这里插入图片描述
作者这里之处了这个激活函数的不足就是PReLU采用一个值为0的硬矫正点,当每个层的输入遵循不同的分布时,这可能不适合。所以作者这里设计了一个新的自适应激活函数叫做Dice,它的 p ( s ) p(s) p(s)是右边这个图, 公式如下:
f ( s ) = p ( s ) ⋅ s + ( 1 − p ( s ) ) ⋅ α s , p ( s ) = 1 1 + e − s − E [ s ] Var ⁡ [ s ] + ϵ f(s)=p(s) \cdot s+(1-p(s)) \cdot \alpha s, p(s)=\frac{1}{1+e^{-\frac{s-E[s]}{\sqrt{\operatorname{Var}[s]+\epsilon}}}} f(s)=p(s)s+(1p(s))αs,p(s)=1+eVar[s]+ϵ sE[s]1
这里的 E ( s ) E(s) E(s) V a r ( s ) Var(s) Var(s)是每个mini-batch里面样本的均值和方差,当然这是训练集部分,测试集的时候采用的是在数据上平滑的均值和方差。 由于把均值和方差考虑进去了, 那么这个函数的调整就可以根据数据的分布进行自适应,这样会更加的灵活且合理,另外看到了大名鼎鼎的sigmoid的身影了吗?。
在这里插入图片描述
这就是论文里面的两大技术了,这两个技术也在某种程度上增强了模型的表达效果,具体可以参考论文的实验部分。

4.4 论文的一些其他细节

这里再整理一些其他细节, 这部分主要是来自于作者的实验了, 首先是数据集上。这里作者采用了是三个数据集,阿里巴巴的一个真实场景数据,两个公共数据集,一个是亚马逊的产品数据集,一个是movielens数据集, 当然具体描述可以参考原论文,后面我们也用Pytorch实现DIN模型,并用亚马逊的数据集走一下,到那里再解释这个数据集。 这里只是想说movieslen数据集, 竟然还能玩点击率预测任务,之前我也用过这个,但是看见的是电影的评分,并且也没有考虑用户的历史行为评分, 而这里作者为了转成二分类,把评分大于3的算作点击,小于3的算作不点击,然后转成了0和1的分类任务,并考虑进了用户的历史点击行为,后面如果有时间,也搞搞这个数据集试一下。

第二个细节就是评估上,这里认识到了一个衡量模型改进程度的一个东西叫RelaImpr, 公式计算如下:
 RelaImpr  = (  AUC  (  measured model  ) − 0.5  AUC(base model  ) − 0.5 − 1 ) × 100 % \text { RelaImpr }=\left(\frac{\text { AUC }(\text { measured model })-0.5}{\text { AUC(base model })-0.5}-1\right) \times 100 \%  RelaImpr =( AUC(base model )0.5 AUC ( measured model )0.51)×100%
这个东西是基于base model看模型的提高程度的。 还认识到了一个加权的AUC, 公式如下:
A U C = ∑ i = 1 n # i m p r e s s i o n i × A U C i ∑ i = 1 n # i m p r e s s i o n i \mathrm{AUC}=\frac{\sum_{i=1}^{n} \# i m p r e s s i o n_{i} \times \mathrm{AUC}_{i}}{\sum_{i=1}^{n} \# i m p r e s s i o n_{i}} AUC=i=1n#impressionii=1n#impressioni×AUCi

这里的 n n n是用户数量, # i n p r e s s i o n g i \#inpressiong_i #inpressiongi A U C i AUC_i AUCi分别是 i i i用户的喜好和AUC。作者说这个是通过平均用户的AUC来衡量用户内部顺序的好坏,并被证明与显示广告系统的在线性能更相关。

接下来就是一系列实验,实验结果可以用论文的一句话概况:
在这里插入图片描述
当然这里的实验也是非常精彩的, 通过各种对比也消融,作者验证了提出模型的有效性,并还通过了A/B 测试,将模型部署到了真实业务场景中, 我觉得没有啥比这个更有说服力了吧,人家都在用了,并且也产生好的收益了,哈哈。

最后,作者可视化的注意力权重和embedding向量的结果也非常有意思, 也再次证明了作者之前的论述有效性和我们上面分析的合理性。 比如注意力权重这个
在这里插入图片描述
这个就是基于某个用户之前的购买商品行为预测对于当前这个衣服广告的点击率, 就会发现,模型更加关注于历史商品中的有关衣服的这些商品。而通过下面embedding的可视化这个,可以看到,同一类别的商品几乎都属于一个聚类:
在这里插入图片描述

这清楚地显示了DIN学习到的嵌入向量的聚类属性。这个也是非常有意思的,看到这里,又让我联想到了淘宝的“千人千面”界面, 怀疑是不是就是用的这么个原理哈哈。

好了, 这就是DIN所有的理论内容了,这里放上论文里面的结论作为总结,人家实在是总结太好了,我就不单独总结了:
在这里插入图片描述

5. DIN模型的复现

理论梳理完了之后,我们看具体的DIN模型的复现过程,上面反复强调那个应用场景这里也得到了呼应,就是我们这次DIN的数据集终于换了,换成了一个有着丰富用户历史行为的亚马逊的数据集, 数据集可以在这里下载,这个数据比较大,我这里依然是进行了采样的过程。 并且这个数据还需要进行一些数据预处理的工作,所以复现这块,大致上分为两块,一个是走一遍数据处理的逻辑,另一个就是DIN模型全貌的逻辑,关于具体的实现细节和训练细节, 我依然把代码放到了GitHub上,可以去那里查看。下面先说数据预处理。

5.1 数据预处理

关于数据集的详细介绍, 作者这里介绍了:
在这里插入图片描述
下面主要是说一下上面这段话,也就是具体的数据预处理细节在代码中是怎么体现的。 这样后面再看总体代码时候会舒服一点哈哈。

原始数据是两个json文件(meta_Electronics.jsonreviews_Electronics.json), 首先,我们需要对评论的这个数据json处理,把它转成pd,然后保存成reviews.pkl文件,这个可以去看我GitHub里面的数据预处理里面的代码,这里不写了。这个reviews文件长这样:
在这里插入图片描述
然后处理原数据的json文件, 这里面只保留在上面reviews里面出现过的商品,这个文件长下面这样:

在这里插入图片描述
然后基于上面的两个文件, 我们再处理,上面两个文件是reviews_df和meta_df

  1. reviews_df保留’reviewerID’【用户ID】, ‘asin’【产品ID】, ‘unixReviewTime’【浏览时间】三列
  2. meta_df保留’asin’【产品ID】, ‘categories’【种类】两列

这时候发现数据太大了,我电脑跑不起来,所以进行了采样:

在这里插入图片描述
后面给物品ID,物品种类ID和用户ID做了一个值 -> 索引的映射操作,最后保存了四个文件到了remap.pkl里面。

  • reviews_df: 用户评论的数据集,这里面用户ID,产品ID和浏览时间三列
  • cate_list: 这个是各个产品对应的类别列表
  • (user_count, item_count, cate_count, example_count): 统计的用户个数,产品个数,类别个数和总评论的个数
  • (asin_key, cate_key, revi_key): 产品ID的取值,类别的取值和用户id的唯一取值

后面制作数据集的时候,就基于这个remap.pkl文件里面的这四个文件来创建。这个在preprocessed_data文件夹下面。下面重新导入之后,开始制作数据集。这个是基于reviews_df数据集,这个长下面这样:

在这里插入图片描述
下面是制作数据集的过程,按照论文里面描述的,我们首先是根据用户id分组,拿到每个用户购买过的商品id, 这个是正样本数据,也就是用户真正点击过的。 对于每个正样本数据,我们随机生成一个该用户没有买过的商品当做负样本数据。 所以就有了正样本列表和负样本列表。产生数据集的逻辑就是用户正样本数据的索引遍历, 把第i位置的商品当做当前候选广告, 把第i位置之前的当做历史产品序列, 这样分布产生训练集,测试集,验证集。 这个还是看代码比较容易看:

train_data, val_data, test_data = [], [], []
for user_id, hist in tqdm(reviews_df.groupby('user_id')):
    pos_list = hist['item_id'].tolist()    # pos_list就是用户真实购买的商品, 下面针对每个购买的商品, 产生一个用户没有购买过的产品

    def gen_neg():
        neg = pos_list[0]
        while neg in pos_list: 
            neg = random.randint(0, item_count-1)       # 这儿产生一个不在真实用户购买的里面的
        return neg
    
    neg_list = [gen_neg() for i in range(len(pos_list))]
    hist = []   # 历史购买商品
    for i in range(1, len(pos_list)):
        hist.append([pos_list[i-1]])
        if i == len(pos_list) - 1:                   # 最后一个的时候
            test_data.append([hist, [pos_list[i]], 1])
            test_data.append([hist, [neg_list[i]], 0])
        elif i == len(pos_list) - 2:           # 倒数第二个的时候
            val_data.append([hist, [pos_list[i]], 1])
            val_data.append([hist, [neg_list[i]], 0])
        else:
            train_data.append([hist, [pos_list[i]], 1])
            train_data.append([hist, [neg_list[i]], 0])

上面这个可以举一个例子,比如某个用户1买的商品有[10, 8, 5, 4, 9], 那么这个算作正样本列表,也就是真正点击过的商品,label会是1, 根据这个列表,生成一个负列表,label都是0, 比如[11, 17, 12, 14, 13], 那么在构建数据集的时候, 可以构造:

hist_id, target_item , label

[[10]], [8], 1
[[10],[8]], [5], 1
[[10], [8], [5]], [4], 1
[[10], [8], [5], [4]], [9], 1
[[10]], [17], 0
[[10],[8]], [12], 0
[[10], [8], [5]], [14], 0
[[10], [8], [5], [4]], [13], 0

只不过后面这两个长的,分别给了验证集和测试集

这样制作完了的数据集如下格式:

在这里插入图片描述
这里会发现,对于每个用户, 历史序列长度不一样长, 所以我们需要进行填充成一样的长度, 可以用keras的pad_sequence函数。

# 由于每个用户的购买历史序列都不一样长, 所以需要进行填充, 让他一样了
train_X = [np.array([0.] * len(train)), np.array([0]*len(train)), pad_sequences(train['hist'], maxlen=maxlen), np.array(train['target_item'].tolist())]
train_y = train['label'].values

train_X, val_X, test_X都是四个部分组成, 首先[0]和[1],都是0的一维数组,这个模拟了一波连续特征和其他的离散特征, [2]是历史购买序列, 填充到了一样的长度, [3]是目前序列, 需要先把数据处理成这样。因为在DIN的前向传播里面是先把这四类数据给进行拆分的,每类特征的处理方式不一样:

dense_inputs, sparse_inputs, seq_inputs, item_inputs = inputs

DIN模型的话这个输入一定要弄明白,且必须要符合这样的一种格式才行, 数据到了这里差不多完事,下面是建立DIN模型的细节了。

5.2 DIN模型

DIN模型这里本来是想写成Pytorch的,但是发现目前我家里的电脑这个数据集跑不动, 所以没法调试写的对不对。 所以这里我放了之前写完,调试过的tf代码,这个是参考了子耀大佬的代码,注意力机制的部分可能不太一样, 因为当时设计新闻推荐的时候写的,那时候我调试子耀大佬的代码出了bug, 后来就用自己的方式调通了,这里也加了详细的注释, 等回到学校,再想办法搞成Pytorch代码吧,其实都差不多,尤其是tf2.0之后。

这里开始梳理这个代码的逻辑:

5.2.1 Attention层

这里首先是Attention层, 就是图里面那个局部激活单元,这个是一个全连接的神经网络,接收的输入是4部分[item_embed, seq_embed, seq_embed, mask]

  • item_embed: 这个是候选商品的embedding向量, 维度是(None, embedding_dim * behavior_num), 由于这里能表示的用户行为特征个数,behavior_num能表示用户行为的特征个数 这里是1
  • seq_embed: 这个是用户历史商品序列的embedding向量, 维度是(None, max_len, embedding_dim * behavior_num)
  • mask: 维度是(None, max_len) 这个里面每一行是[False, False, True, True, ....]的形式, False的长度表示样本填充的那部分, 填充为0的那些得标识出来,后面计算的时候,填充的那些去掉

后面的前向传播逻辑看下面代码吧, 每一行都加了注释了:

class Attention_layer(Layer):
    """
    自定义Attention层, 这个就是一个全连接神经网络
    """
    def __init__(self, att_hidden_units, activation='sigmoid'):
        super(Attention_layer, self).__init__()
        self.att_dense = [Dense(unit, activation=activation) for unit in att_hidden_units]
        self.att_final_dense = Dense(1)
    
    # forward
    def call(self, inputs):
        """
        这里的inputs包含四部分: [item_embed, seq_embed, seq_embed, mask]
        
        item_embed: 这个是候选商品的embedding向量   维度是(None, embedding_dim * behavior_num)   # behavior_num能表示用户行为的特征个数 这里是1, 所以(None, embed_dim)
        seq_embed: 这个是用户历史商品序列的embedding向量, 维度是(None, max_len, embedding_dim * behavior_num)  (None, max_len, embed_dim)
        mask:  维度是(None, max_len)   这个里面每一行是[False, False, True, True, ....]的形式, False的长度表示样本填充的那部分
        """
        q, k, v, key_masks = inputs
        q = tf.tile(q, multiples=[1, k.shape[1]])   # (None, max_len*embedding)       # 沿着k.shap[1]的维度复制  毕竟每个历史行为都要和当前的商品计算相似关系
        q = tf.reshape(q, shape=[-1, k.shape[1], k.shape[2]])      # (None, max_len, emebdding_dim
        
        # q, k, out product should concat
        info = tf.concat([q, k, q-k, q*k], axis=-1)   # (None, max_len, 4*emebdding_dim)
        
        # n层全连接
        for dense in self.att_dense:
            info = dense(info)
        
        outputs = self.att_final_dense(info)      # (None,  max_len, 1)
        outputs = tf.squeeze(outputs, axis=-1)    # (None, max_len)
        
        # mask 把每个行为序列填充的那部分替换成很小的一个值
        paddings = tf.ones_like(outputs) * (-2**32+1)      # (None, max_len)  这个就是之前填充的那个地方, 我们补一个很小的值
        outputs = tf.where(tf.equal(key_masks, 0), paddings, outputs)
        
        # softmax
        outputs = tf.nn.softmax(logits=outputs) # (None, max_len)
        outputs = tf.expand_dims(outputs, axis=1)   # (None, 1, max_len) 
        
        outputs = tf.matmul(outputs, v)   # 三维矩阵相乘, 相乘发生在后两维   (None, 1, max_len) * (None, max_len, embed_dim) = (None, 1, embed_dim)
        outputs = tf.squeeze(outputs, axis=1)  # (None, embed_dim)
        
        return outputs

这一块完成的DIN结构图里面绿框里面的整个操作过程。 有两点需要注意,第一个是这里的Dense的激活函数用的sigmoid,没有Prelu或者Dice, 这里是看的大部分网上代码里面这块没有用, 把这两个激活函数用到了后面的MLP里面了。 第二个就是论文里面虽然说不用softmax, 但是这里用了softmax了, 这个看具体的应用场景吧还是。

5.2.2 Dice 激活函数

这是作者那个训练技术上的第二个创新,改得prelu激活函数,自己提出了dice激活函数, 下面我们看看这个函数的具体实现过程:

class Dice(Layer):
    def __init__(self):
        super(Dice, self).__init__()
        self.bn = BatchNormalization(center=False, scale=False)
        self.alpha = self.add_weight(shape=(), dtype=tf.float32, name='alpha')
    
    def call(self, x):
        x_normed = self.bn(x)
        x_p = tf.sigmoid(x_normed)
        
        return self.alpha * (1.0-x_p) * x + x_p * x

具体实现的时候,这里是用了一个BatchNormalization层的操作, 把x进行了归一化处理,然后这个取了sigmoid就得到了 p ( s ) p(s) p(s)

5.2.3 DIN 模型

有了前面的两套, 实现DIN模型就相对简单了,因为我们说DIN模型的原始架构是base model, 然后再这个基础上加了上面的两个新结构模块,所以这里相当于是先有一个base model,然后再前向传播的时候,加入那两个结构就OK了,具体看一下:

class DIN(Model):
    def __init__(self, feature_columns, behavior_feature_list, att_hidden_units=(80, 40), ffn_hidden_units=(80, 40), att_activation='sigmoid', 
                 ffn_activation='prelu', maxlen=40, dnn_dropout=0., embed_reg=1e-4):
        """
        DIN:
        feature_columns:列表, [dense_feature_columns,sparse_feature_columns],dense_feature_columns是[{'feat': 'feat_name'}], 而sparse_feature_columns是[{'feat':'feat_name', 'feat_num': 'nunique', 'embed_dim'}]
        behavior_feature_list: 列表. 能表示用户历史行为的特征, 比如商品id, 店铺id ['item', 'cat']
        att_hidden_units: 注意力层的隐藏单元个数.可以是一个列表或者元组,毕竟注意力层也是一个全连接的网络嘛
        ffn_hidden_units:全连接层的隐藏单元个数和层数,可以是一个列表或者元组  (80, 40)  就表示两层神经网络, 第一层隐藏层单元80个, 第二层40个
        att_activation: 激活单元的名称, 字符串
        ffn_activation: 激活单元的名称, 用'prelu'或者'Dice'  
        maxlen: 标量. 用户历史行为序列的最大长度
        dropout: 标量,失活率
        embed_reg: 标量. 正则系数
        """
        super(DIN, self).__init__()      # 初始化网络
        self.maxlen = maxlen
        
        self.dense_feature_columns, self.sparse_feature_columns = feature_columns           # 这里把连续特征和离散特征分别取出来, 因为后面两者的处理方式不同
        
        # len
        self.other_sparse_len = len(self.sparse_feature_columns) - len(behavior_feature_list)      # 这个other_sparse就是离散特征中去掉了能表示用户行为的特征列
        self.dense_len = len(self.dense_feature_columns)    
        self.behavior_num = len(behavior_feature_list)
        
        # embedding层, 这里分为两部分的embedding, 第一部分是普通的离散特征, 第二部分是能表示用户历史行为的离散特征, 这一块后面要进注意力和当前的商品计算相关性
        self.embed_sparse_layers = [Embedding(input_dim=feat['feat_num'], 
                                              input_length=1, 
                                              output_dim=feat['embed_dim'],
                                              embeddings_initializer='random_uniform',
                                              embeddings_regularizer=l2(embed_reg)
                                             ) for feat in self.sparse_feature_columns if feat['feat'] not in behavior_feature_list]
        # behavior embedding layers, item id and catetory id
        self.embed_seq_layers = [Embedding(input_dim=feat['feat_num'], 
                                           input_length=1, 
                                           output_dim=feat['embed_dim'], 
                                           embeddings_initializer='random_uniform',
                                           embeddings_regularizer=l2(embed_reg)
                                          ) for feat in self.sparse_feature_columns if feat['feat'] in behavior_feature_list]
        
        # 注意力机制
        self.attention_layer = Attention_layer(att_hidden_units, att_activation)
        
        self.bn = BatchNormalization(trainable=True)
        
        # 全连接网络
        self.ffn = [Dense(unit, activation=PReLU() if ffn_activation=='prelu' else Dice()) for unit in ffn_hidden_units]
        self.dropout = Dropout(dnn_dropout)
        self.dense_final = Dense(1)
        
    def call(self, inputs):
        """
        inputs: [dense_input, sparse_input, seq_input, item_input]  , 第二部分是离散型的特征输入, 第三部分是用户的历史行为, 第四部分是当前商品的输入
    
        dense_input: 连续型的特征输入, 维度是(None, dense_len)
        sparse_input: 离散型的特征输入, 维度是(None, other_sparse_len)
        seq_inputs: 用户的历史行为序列(None, maxlen, behavior_len)
        item_inputs: 当前的候选商品序列 (None, behavior_len)
        """
        
        dense_inputs, sparse_inputs, seq_inputs, item_inputs = inputs
        
        # attention --->mask, if the element of seq_inputs is equal 0, it must be filled in  这是因为很多序列由于不够长用0进行了填充,并且是前面补的0
        mask = tf.cast(tf.not_equal(seq_inputs[:, :, 0], 0), dtype=tf.float32)  # (None, maxlen)  类型转换函数, 把seq_input中不等于0的值转成float32
        # 这个函数的作用就是每一行样本中, 不为0的值返回1, 为0的值返回0, 这样就把填充的那部分值都给标记了出来
        
        # 下面把连续型特征和行为无关的离散型特征拼到一块先
        other_info = dense_inputs   # (None, dense_len)
        for i in range(self.other_sparse_len):
            other_info = tf.concat([other_info, self.embed_sparse_layers[i](sparse_inputs[:, i])], axis=-1)      # (None, dense_len+other_sparse_len)
        
        # 下面把候选的商品和用户历史行为商品也各自的拼接起来
        seq_embed = tf.concat([self.embed_seq_layers[i](seq_inputs[:, :, i]) for i in range(self.behavior_num)], axis=-1)   # [None, max_len, embed_dim]
        item_embed = tf.concat([self.embed_seq_layers[i](item_inputs[:, i]) for i in range(self.behavior_num)], axis=-1)  # [None, embed_dim]
        
    
        # 下面进行attention_layer的计算
        user_info = self.attention_layer([item_embed, seq_embed, seq_embed, mask])   # (None, embed_dim) 
        
        # 所有特征拼起来了
        if self.dense_len > 0 or self.other_sparse_len > 0:
            info_all = tf.concat([user_info, item_embed, other_info], axis=-1)   # (None, dense_len + other_sparse_len + embed_dim+embed_dim)  
        else:
            info_all = tf.concat([user_info, item_embed], axis=-1) # (None, embed_dim+embed_dim)
        
        info_all = self.bn(info_all)
        
        # ffn
        for dense in self.ffn:
            info_all = dense(info_all)
        
        info_all = self.dropout(info_all)
        outputs = tf.nn.sigmoid(self.dense_final(info_all))
        return outputs

依然是每一行都加了注释了, 简单的提几个细节就可以啦,第一个细节就是我们的输入分为四类, 连续特征, 普通离散特征, 用户历史行为的离散特征,和候选商品特征。这四类处理的方式不一样。对于连续特征和普通离散特征来说,这两块这里就直接普通离散经过embedding,和连续特征拼接到了一块,先不管。 而候选商品特征和用户历史行为特征要经过一个Attention layer之后, 把得到的embedding和上面的那两个拼到一块作为了神经网络的输入。第二个细节,就是把Dice和prelu的激活用到了后面的全连接神经网络中。

下面我们看一下这个模型建立的时候,构建的格式:

def test_model():
    dense_features = [{'feat': 'a'}, {'feat': 'b'}]
    sparse_features = [{'feat': 'item_id', 'feat_num': 100, 'embed_dim': 8},
                       {'feat': 'cate_id', 'feat_num': 100, 'embed_dim': 8},
                       {'feat': 'adv_id', 'feat_num': 100, 'embed_dim': 8}
                      ]
    behavior_list = ['item_id', 'cate_id']
    features = [dense_features, sparse_features]
    model = DIN(features, behavior_list)

这个dense_features是连续特征, sparse_features是稀疏的特征, 都是列表,然后里面是那样字典的格式,这个具体细节看GitHub里面的代码吧。 这里还需要一个behavior_list, 也就是用户的历史行为特征, 这个要单独传入进去。

按照这样的方式,就可以建立DIN模型, 而上面已经把trainx, trainy处理成对应的数据格式了, 这样就能看训练DIN了。 可以到后面的GitHub看具体细节。

关于DIN模型的代码部分就是这些了, 这里还缺一个Pytorch复现的代码, 目前设备有限制,这个没法写。

6. 总结

哇,到了这里终于看到了曙光了,终于完事了, 这篇文章篇幅很长,依然是简单的梳理回顾。 这次主要是整理了两个带有注意力机制的两个模型AFM和DIN,虽然这两个模型都引入了注意力机制,但相信读过来之后,这两个模型的差距还是非常大的, 注意力起作用的原理和出发点都是不一样的。

首先,介绍了AFM的全貌,包括原理,架构和Pytorch实现,这个是基于了NFM进行改的,解决的痛点问题是各个特征交叉之后的embedding向量被同等看待,赋予对预测相同重要性的问题, 所以这里加了一个注意力机制,给各个特征交叉后的embedding向量不同的权重,这样表示了他们对预测结果的重要程度。

然后,介绍了DIN的全貌,包括原理,架构和tf实现, 这里对于这个模型扣得比较细了一些, 基本上按照它论文的章节分析过来的, 整理了更多的细节部分, DIN模型是基于真实的业务场景搞的, 解决的痛点是深度学习模型无法表达用户多样化的兴趣。 它可以通过考虑【给定的候选广告】和【用户的历史行为】的相关性,来计算用户兴趣的表示向量。具体来说就是通过引入局部激活单元,通过软搜索历史行为的相关部分来关注相关的用户兴趣,并采用加权和来获得有关候选广告的用户兴趣的表示。与候选广告相关性较高的行为会获得较高的激活权重,并支配着用户兴趣。该表示向量在不同广告上有所不同,大大提高了模型的表达能力。 这个模型我一直在强调应用场景, 是因为并不是任何时候都适合这个模型的,很大一个前提是丰富的用户历史行为数据。之前在设计新闻推荐比赛的时候也用过这个模型,可以去看看那个数据集的情况来这里。 所以从前面的FM家族到DIN可能会有一种画风突变的感觉了,也能够看出AFM和DIN的两个注意力其实是不太一样的东西。

作者在DIN这篇论文里面提到了尝试用LSTM对用户历史行为数据进行建模。 但是发现没有改善效果。因为用户历史行为的序列可能包含多个并发兴趣, 这些兴趣的快速跳跃和突然结束导致用户行为的序列数据很嘈杂, 但是提供了一个研究的方向, 而在一年之后,作者真的找到了合适的结构,搞定了这个问题, 这个就是2019年的DIEN了,在下一篇中我们看看这个模型, 到底作者是如何改进的。

这里想在梳理一下这些模型的发展时间线了,继NerualCF(PNN那篇)往后:

在这里插入图片描述

参考

整理这篇文章的同时, 也刚建立了一个GitHub项目, 准备后面把各种主流的推荐模型用复现一遍,并用通俗易懂的语言进行注释和逻辑整理, 今天的两个模型AFM和DIN都已经上传, 参考的子耀的TF2.0的复现过程, 写成了Pytorch代码(DIN模型目前是tf代码,待完成Pytorch), 感兴趣的可以看一下 😉

筋斗云:https://github.com/zhongqiangwu960812/AI-RecommenderSystem

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页