diff --git a/docs/README.md b/docs/README.md index d74ec733..255fd66e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -61,10 +61,10 @@ - AutoInt【完成一半,待优化】 - FiBiNET【完成一半,待优化】 - **WideNDeep系列** - - Wide&Deep【已完成】 + - [Wide&Deep](/推荐算法基础/经典排序模型/Wide&Deep系列/Wide&Deep) - 改进Deep侧 - - NFM【已完成】 - - AFM【已完成】 + - [NFM](/推荐算法基础/经典排序模型/Wide&Deep系列/NFM) + - [AFM](/推荐算法基础/经典排序模型/Wide&Deep系列/AFM) - 改进Wide侧 - DeepFM【已完成】 - xDeepFM【未完成】 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index c481dc0b..a4c3b3b9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -40,10 +40,10 @@ * [AutoInt]() * [FiBiNet]() * [Wide&Deep系列]() - * [Wide&Deep]() + * [Wide&Deep](/推荐算法基础/经典排序模型/Wide&Deep系列/Wide&Deep) * [改进Deep侧]() - * [NFM]() - * [AFM]() + * [NFM](/推荐算法基础/经典排序模型/Wide&Deep系列/NFM) + * [AFM](/推荐算法基础/经典排序模型/Wide&Deep系列/AFM) * [改进Wide侧]() * [DeepFM]() * [xDeepFM]() diff --git a/docs/推荐算法基础/经典排序模型/Wide&Deep系列/AFM.md b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/AFM.md new file mode 100644 index 00000000..82164853 --- /dev/null +++ b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/AFM.md @@ -0,0 +1,127 @@ +# AFM +## AFM提出的动机 + +AFM的全称是Attentional Factorization Machines, 从模型的名称上来看是在FM的基础上加上了注意力机制,FM是通过特征隐向量的内积来对交叉特征进行建模,从公式中可以看出所有的交叉特征都具有相同的权重也就是1,没有考虑到不同的交叉特征的重要性程度: +$$ +y_{fm} = w_0+\sum_{i=1}^nw_ix_i+\sum_{i=1}^{n}\sum_{i+1}^n\lt v_i,v_j\gt x_ix_j +$$ +如何让不同的交叉特征具有不同的重要性就是AFM核心的贡献,在谈论AFM交叉特征注意力之前,对于FM交叉特征部分的改进还有FFM,其是考虑到了对于不同的其他特征,某个指定特征的隐向量应该是不同的(相比于FM对于所有的特征只有一个隐向量,FFM对于一个特征有多个不同的隐向量)。 + +## AFM模型原理 +
+image-20210131092744905 +
+上图表示的就是AFM交叉特征部分的模型结构(非交叉部分与FM是一样的,图中并没有给出)。AFM最核心的两个点分别是Pair-wise Interaction Layer和Attention-based Pooling。前者将输入的非零特征的隐向量两两计算element-wise product(哈达玛积,两个向量对应元素相乘,得到的还是一个向量),假如输入的特征中的非零向量的数量为m,那么经过Pair-wise Interaction Layer之后输出的就是$\frac{m(m-1)}{2}$个向量,再将前面得到的交叉特征向量组输入到Attention-based Pooling,该pooling层会先计算出每个特征组合的自适应权重(通过Attention Net进行计算),通过加权求和的方式将向量组压缩成一个向量,由于最终需要输出的是一个数值,所以还需要将前一步得到的向量通过另外一个向量将其映射成一个值,得到最终的基于注意力加权的二阶交叉特征的输出。(对于这部分如果不是很清楚,可以先看下面对两个核心层的介绍) + +### Pair-wise Interaction Layer + +FM二阶交叉项:所有非零特征对应的隐向量两两点积再求和,输出的是一个数值 +$$ +\sum_{i=1}^{n}\sum_{i+1}^n\lt v_i,v_j\gt x_ix_j +$$ +AFM二阶交叉项(无attention):所有非零特征对应的隐向量两两对应元素乘积,然后再向量求和,输出的还是一个向量。 +$$ +\sum_{i=1}^{n}\sum_{i+1}^n (v_i \odot v_j) x_ix_j +$$ +上述写法是为了更好的与FM进行对比,下面将公式变形方便与原论文中保持一致。首先是特征的隐向量。从上图中可以看出,作者对数值特征也对应了一个隐向量,不同的数值乘以对应的隐向量就可以得到不同的隐向量,相对于onehot编码的特征乘以1还是其本身(并没有什么变化),其实就是为了将公式进行统一。虽然论文中给出了对数值特征定义隐向量,但是在作者的代码中并没有发现有对数值特征进行embedding的过程([原论文代码链接](https://github.com/hexiangnan/attentional_factorization_machine/blob/master/code/AFM.py))具体原因不详。 + +按照论文的意思,特征的embedding可以表示为:$\varepsilon = {v_ix_i}$,经过Pair-wise Interaction Layer输出可得: +$$ +f_{PI}(\varepsilon)=\{(v_i \odot v_j) x_ix_j\}_{i,j \in R_x} +$$ +$R_x$表示的是有效特征集合。此时的$f_{PI}(\varepsilon)$表示的是一个向量集合,所以需要先将这些向量集合聚合成一个向量,然后在转换成一个数值: +$$ +\hat{y} = p^T \sum_{(i,j)\in R_x}(v_i \odot v_j) x_ix_j + b +$$ +上式中的求和部分就是将向量集合聚合成一个维度与隐向量维度相同的向量,通过向量$p$再将其转换成一个数值,b表示的是偏置。 + +从开始介绍Pair-wise Interaction Layer到现在解决的一个问题是,如何将使用哈达玛积得到的交叉特征转换成一个最终输出需要的数值,到目前为止交叉特征之间的注意力权重还没有出现。在没有详细介绍注意力之前先感性的认识一下如果现在已经有了每个交叉特征的注意力权重,那么交叉特征的输出可以表示为: +$$ +\hat{y} = p^T \sum_{(i,j)\in R_x}\alpha_{ij}(v_i \odot v_j) x_ix_j + b +$$ +就是在交叉特征得到的新向量前面乘以一个注意力权重$\alpha_{ij}$, 那么这个注意力权重如何计算得到呢? + +### Attention-based Pooling + +对于神经网络注意力相关的基础知识大家可以去看一下邱锡鹏老师的《神经网络与深度学习》第8章注意力机制与外部记忆。这里简单的叙述一下使用MLP实现注意力机制的计算。假设现在有n个交叉特征(假如维度是k),将nxk的数据输入到一个kx1的全连接网络中,输出的张量维度为nx1,使用softmax函数将nx1的向量的每个维度进行归一化,得到一个新的nx1的向量,这个向量所有维度加起来的和为1,每个维度上的值就可以表示原nxk数据每一行(即1xk的数据)的权重。用公式表示为: +$$ +\alpha_{ij}' = h^T ReLU(W(v_i \odot v_j)x_ix_j + b) +$$ +使用softmax归一化可得: +$$ +\alpha_{ij} = \frac{exp(\alpha_{ij}')}{\sum_{(i,j)\in R_x}exp(\alpha_{ij}')} +$$ +这样就得到了AFM二阶交叉部分的注意力权重,如果将AFM的一阶项写在一起,AFM模型用公式表示为: +$$ +\hat{y}_{afm}(x) = w_0+\sum_{i=1}^nw_ix_i+p^T \sum_{(i,j)\in R_x}\alpha_{ij}(v_i \odot v_j) x_ix_j + b +$$ +### AFM模型训练 + +AFM从最终的模型公式可以看出与FM的模型公式是非常相似的,所以也可以和FM一样应用于不同的任务,例如分类、回归及排序(不同的任务的损失函数是不一样的),AFM也有对防止过拟合进行处理: + +1. 在Pair-wise Interaction Layer层的输出结果上使用dropout防止过拟合,因为并不是所有的特征组合对预测结果都有用,所以随机的去除一些交叉特征,让剩下的特征去自适应的学习可以更好的防止过拟合。 +2. 对Attention-based Pooling层中的权重矩阵$W$使用L2正则,作者没有在这一层使用dropout的原因是发现同时在特征交叉层和注意力层加dropout会使得模型训练不稳定,并且性能还会下降。 + +加上正则参数之后的回归任务的损失函数表示为: +$$ +L = \sum_{x\in T} (\hat{y}_{afm}(x) - y(x))^2 + \lambda ||W||^2 +$$ +## AFM代码实现 + +1. linear part: 这部分是有关于线性计算,也就是FM的前半部分$w1x1+w2x2...wnxn+b$的计算。对于这一块的计算,我们用了一个get_linear_logits函数实现,后面再说,总之通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出 +2. dnn part: 这部分是后面交叉特征的那部分计算,这一部分需要使用注意力机制来将所有类别特征的embedding计算注意力权重,然后通过加权求和的方式将所有交叉之后的特征池化成一个向量,最终通过一个映射矩阵$p$将向量转化成一个logits值 +3. 最终将linear部分与dnn部分相加之后,通过sigmoid激活得到最终的输出 + +```python +def AFM(linear_feature_columns, dnn_feature_columns): + # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型 + dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns) + + # 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding + linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns)) + + # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式 + # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层 + input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values()) + + # linear_logits由两部分组成,分别是dense特征的logits和sparse特征的logits + linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns) + + # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型 + # embedding层用户构建FM交叉部分和DNN的输入部分 + embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False) + + # 将输入到dnn中的sparse特征筛选出来 + att_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns)) + + att_logits = get_attention_logits(sparse_input_dict, att_sparse_feature_columns, embedding_layers) # B x (n(n-1)/2) + + # 将linear,dnn的logits相加作为最终的logits + output_logits = Add()([linear_logits, att_logits]) + + # 这里的激活函数使用sigmoid + output_layers = Activation("sigmoid")(output_logits) + + model = Model(input_layers, output_layers) + return model +``` + +关于每一块的细节,这里就不解释了,在我们给出的GitHub代码中,我们已经加了非常详细的注释,大家看那个应该很容易看明白, 为了方便大家的阅读,我们这里还给大家画了一个整体的模型架构图,帮助大家更好的了解每一块以及前向传播(画的图不是很规范,先将就看一下,后面我们会统一在优化一下这个手工图)。 + +
+image-20210307200304199 +
+ +下面是一个通过keras画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在github中。 + +
+image-20210307200304199 +
+ +## 思考 +1. AFM与NFM优缺点对比。 + + +**参考资料** +[原论文](https://www.ijcai.org/Proceedings/2017/0435.pdf) +[deepctr](https://github.com/shenweichen/DeepCTR) \ No newline at end of file diff --git a/docs/推荐算法基础/经典排序模型/Wide&Deep系列/NFM.md b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/NFM.md new file mode 100644 index 00000000..0ccd2caa --- /dev/null +++ b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/NFM.md @@ -0,0 +1,146 @@ +# NFM +## 动机 + +NFM(Neural Factorization Machines)是2017年由新加坡国立大学的何向南教授等人在SIGIR会议上提出的一个模型,传统的FM模型仅局限于线性表达和二阶交互, 无法胜任生活中各种具有复杂结构和规律性的真实数据, 针对FM的这点不足, 作者提出了一种将FM融合进DNN的策略,通过引进了一个特征交叉池化层的结构,使得FM与DNN进行了完美衔接,这样就组合了FM的建模低阶特征交互能力和DNN学习高阶特征交互和非线性的能力,形成了深度学习时代的神经FM模型(NFM)。 + +那么NFM具体是怎么做的呢? 首先看一下NFM的公式: +$$ +\hat{y}_{N F M}(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+f(\mathbf{x}) +$$ +我们对比FM, 就会发现变化的是第三项,前两项还是原来的, 因为我们说FM的一个问题,就是只能到二阶交叉, 且是线性模型, 这是他本身的一个局限性, 而如果想突破这个局限性, 就需要从他的公式本身下点功夫, 于是乎,作者在这里改进的思路就是**用一个表达能力更强的函数来替代原FM中二阶隐向量内积的部分**。 + +
+ +
+而这个表达能力更强的函数呢, 我们很容易就可以想到神经网络来充当,因为神经网络理论上可以拟合任何复杂能力的函数, 所以作者真的就把这个$f(x)$换成了一个神经网络,当然不是一个简单的DNN, 而是依然底层考虑了交叉,然后高层使用的DNN网络, 这个也就是我们最终的NFM网络了: +
+ +
+这个结构,如果前面看过了PNN的伙伴会发现,这个结构和PNN非常像,只不过那里是一个product_layer, 而这里换成了Bi-Interaction Pooling了, 这个也是NFM的核心结构了。这里注意, 这个结构中,忽略了一阶部分,只可视化出来了$f(x)$, 我们还是下面从底层一点点的对这个网络进行剖析。 + +## 模型结构与原理 +### Input 和Embedding层 +输入层的特征, 文章指定了稀疏离散特征居多, 这种特征我们也知道一般是先one-hot, 然后会通过embedding,处理成稠密低维的。 所以这两层还是和之前一样,假设$\mathbf{v}_{\mathbf{i}} \in \mathbb{R}^{k}$为第$i$个特征的embedding向量, 那么$\mathcal{V}_{x}=\left\{x_{1} \mathbf{v}_{1}, \ldots, x_{n} \mathbf{v}_{n}\right\}$表示的下一层的输入特征。这里带上了$x_i$是因为很多$x_i$转成了One-hot之后,出现很多为0的, 这里的$\{x_iv_i\}$是$x_i$不等于0的那些特征向量。 + +### Bi-Interaction Pooling layer +在Embedding层和神经网络之间加入了特征交叉池化层是本网络的核心创新了,正是因为这个结构,实现了FM与DNN的无缝连接, 组成了一个大的网络,且能够正常的反向传播。假设$\mathcal{V}_{x}$是所有特征embedding的集合, 那么在特征交叉池化层的操作: +$$ +f_{B I}\left(\mathcal{V}_{x}\right)=\sum_{i=1}^{n} \sum_{j=i+1}^{n} x_{i} \mathbf{v}_{i} \odot x_{j} \mathbf{v}_{j} +$$ + +$\odot$表示两个向量的元素积操作,即两个向量对应维度相乘得到的元素积向量(可不是点乘呀),其中第$k$维的操作: +$$ +\left(v_{i} \odot v_{j}\right)_{k}=\boldsymbol{v}_{i k} \boldsymbol{v}_{j k} +$$ + +这便定义了在embedding空间特征的二阶交互,这个不仔细看会和感觉FM的最后一项很像,但是不一样,一定要注意这个地方不是两个隐向量的内积,而是元素积,也就是这一个交叉完了之后k个维度不求和,最后会得到一个$k$维向量,而FM那里内积的话最后得到一个数, 在进行两两Embedding元素积之后,对交叉特征向量取和, 得到该层的输出向量, 很显然, 输出是一个$k$维的向量。 + +注意, 之前的FM到这里其实就完事了, 上面就是输出了,而这里很大的一点改进就是加入特征池化层之后, 把二阶交互的信息合并, 且上面接了一个DNN网络, 这样就能够增强FM的表达能力了, 因为FM只能到二阶, 而这里的DNN可以进行多阶且非线性,只要FM把二阶的学习好了, DNN这块学习来会更加容易, 作者在论文中也说明了这一点,且通过后面的实验证实了这个观点。 + +如果不加DNN, NFM就退化成了FM,所以改进的关键就在于加了一个这样的层,组合了一下二阶交叉的信息,然后又给了DNN进行高阶交叉的学习,成了一种“加强版”的FM。 + +Bi-Interaction层不需要额外的模型学习参数,更重要的是它在一个线性的时间内完成计算,和FM一致的,即时间复杂度为$O\left(k N_{x}\right)$,$N_x$为embedding向量的数量。参考FM,可以将上式转化为: +$$ +f_{B I}\left(\mathcal{V}_{x}\right)=\frac{1}{2}\left[\left(\sum_{i=1}^{n} x_{i} \mathbf{v}_{i}\right)^{2}-\sum_{i=1}^{n}\left(x_{i} \mathbf{v}_{i}\right)^{2}\right] +$$ +后面代码复现NFM就是用的这个公式直接计算,比较简便且清晰。 + +### 隐藏层 +这一层就是全连接的神经网络, DNN在进行特征的高层非线性交互上有着天然的学习优势,公式如下: +$$ +\begin{aligned} +\mathbf{z}_{1}=&\sigma_{1}\left(\mathbf{W}_{1} f_{B I} +\left(\mathcal{V}_{x}\right)+\mathbf{b}_{1}\right) \\ +\mathbf{z}_{2}=& \sigma_{2}\left(\mathbf{W}_{2} \mathbf{z}_{1}+\mathbf{b}_{2}\right) \\ +\ldots \ldots \\ +\mathbf{z}_{L}=& \sigma_{L}\left(\mathbf{W}_{L} \mathbf{z}_{L-1}+\mathbf{b}_{L}\right) +\end{aligned} +$$ +这里的$\sigma_i$是第$i$层的激活函数,可不要理解成sigmoid激活函数。 + +### 预测层 +这个就是最后一层的结果直接过一个隐藏层,但注意由于这里是回归问题,没有加sigmoid激活: +$$ +f(\mathbf{x})=\mathbf{h}^{T} \mathbf{z}_{L} +$$ + +所以, NFM模型的前向传播过程总结如下: +$$ +\begin{aligned} +\hat{y}_{N F M}(\mathbf{x}) &=w_{0}+\sum_{i=1}^{n} w_{i} x_{i} \\ +&+\mathbf{h}^{T} \sigma_{L}\left(\mathbf{W}_{L}\left(\ldots \sigma_{1}\left(\mathbf{W}_{1} f_{B I}\left(\mathcal{V}_{x}\right)+\mathbf{b}_{1}\right) \ldots\right)+\mathbf{b}_{L}\right) +\end{aligned} +$$ +这就是NFM模型的全貌, NFM相比较于其他模型的核心创新点是特征交叉池化层,基于它,实现了FM和DNN的无缝连接,使得DNN可以在底层就学习到包含更多信息的组合特征,这时候,就会减少DNN的很多负担,只需要很少的隐藏层就可以学习到高阶特征信息。NFM相比之前的DNN, 模型结构更浅,更简单,但是性能更好,训练和调参更容易。集合FM二阶交叉线性和DNN高阶交叉非线性的优势,非常适合处理稀疏数据的场景任务。在对NFM的真实训练过程中,也会用到像Dropout和BatchNormalization这样的技术来缓解过拟合和在过大的改变数据分布。 + +下面通过代码看下NFM的具体实现过程, 学习一些细节。 + +## 代码实现 +下面我们看下NFM的代码复现,这里主要是给大家说一下这个模型的设计逻辑,参考了deepctr的函数API的编程风格, 具体的代码以及示例大家可以去参考后面的GitHub,里面已经给出了详细的注释, 这里主要分析模型的逻辑这块。关于函数API的编程式风格,我们还给出了一份文档, 大家可以先看这个,再看后面的代码部分,会更加舒服些。下面开始: + +这里主要说一下NFM模型的总体运行逻辑, 这样可以让大家从宏观的层面去把握模型的设计过程, 该模型所使用的数据集是criteo数据集,具体介绍参考后面的GitHub。 数据集的特征会分为dense特征(连续)和sparse特征(离散), 所以模型的输入层接收这两种输入。但是我们这里把输入分成了linear input和dnn input两种情况,而每种情况都有可能包含上面这两种输入。因为我们后面的模型逻辑会分这两部分走,这里有个细节要注意,就是光看上面那个NFM模型的话,是没有看到它线性特征处理的那部分的,也就是FM的前半部分公式那里图里面是没有的。但是这里我们要加上。 +$$ +\hat{y}_{N F M}(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+f(\mathbf{x}) +$$ +所以模型的逻辑我们分成了两大部分,这里我分别给大家解释下每一块做了什么事情: + +1. linear part: 这部分是有关于线性计算,也就是FM的前半部分$w1x1+w2x2...wnxn+b$的计算。对于这一块的计算,我们用了一个get_linear_logits函数实现,后面再说,总之通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出 +2. dnn part: 这部分是后面交叉特征的那部分计算,FM的最后那部分公式f(x)。 这一块主要是针对离散的特征,首先过embedding, 然后过特征交叉池化层,这个计算我们用了get_bi_interaction_pooling_output函数实现, 得到输出之后又过了DNN网络,最后得到dnn的输出 + +模型的最后输出结果,就是把这两个部分的输出结果加和(当然也可以加权),再过一个sigmoid得到。所以NFM的模型定义就出来了: + +```python +def NFM(linear_feature_columns, dnn_feature_columns): + """ + 搭建NFM模型,上面已经把所有组块都写好了,这里拼起来就好 + :param linear_feature_columns: A list. 里面的每个元素是namedtuple(元组的一种扩展类型,同时支持序号和属性名访问组件)类型,表示的是linear数据的特征封装版 + :param dnn_feature_columns: A list. 里面的每个元素是namedtuple(元组的一种扩展类型,同时支持序号和属性名访问组件)类型,表示的是DNN数据的特征封装版 + """ + # 构建输入层,即所有特征对应的Input()层, 这里使用字典的形式返回, 方便后续构建模型 + # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式 + # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层 + dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns+dnn_feature_columns) + input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values()) + + # 线性部分的计算 w1x1 + w2x2 + ..wnxn + b部分,dense特征和sparse两部分的计算结果组成,具体看上面细节 + linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_feature_columns) + + # DNN部分的计算 + # 首先,在这里构建DNN部分的embedding层,之所以写在这里,是为了灵活的迁移到其他网络上,这里用字典的形式返回 + # embedding层用于构建FM交叉部分以及DNN的输入部分 + embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False) + + # 过特征交叉池化层 + pooling_output = get_bi_interaction_pooling_output(sparse_input_dict, dnn_feature_columns, embedding_layers) + + # 加个BatchNormalization + pooling_output = BatchNormalization()(pooling_output) + + # dnn部分的计算 + dnn_logits = get_dnn_logits(pooling_output) + + # 线性部分和dnn部分的结果相加,最后再过个sigmoid + output_logits = Add()([linear_logits, dnn_logits]) + output_layers = Activation("sigmoid")(output_logits) + + model = Model(inputs=input_layers, outputs=output_layers) + + return model +``` + +有了上面的解释,这个模型的宏观层面相信就很容易理解了。关于这每一块的细节,这里就不解释了,在我们给出的GitHub代码中,我们已经加了非常详细的注释,大家看那个应该很容易看明白, 为了方便大家的阅读,我们这里还给大家画了一个整体的模型架构图,帮助大家更好的了解每一块以及前向传播。(画的图不是很规范,先将就看一下,后面我们会统一在优化一下这个手工图)。 +
+NFM_aaaa +
+下面是一个通过keras画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在github中。 +
+NFM_aaaa +
+ +## 思考题 +1. NFM中的特征交叉与FM中的特征交叉有何异同,分别从原理和代码实现上进行对比分析 + +**参考资料** +- [论文原文](https://arxiv.org/pdf/1708.05027.pdf) +- [deepctr](https://github.com/shenweichen/DeepCTR) +- [AI上推荐 之 FNN、DeepFM与NFM(FM在深度学习中的身影重现)](https://blog.csdn.net/wuzhongqiang/article/details/109532267?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161442951716780255224635%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=161442951716780255224635&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v1~rank_blog_v1-1-109532267.pc_v1_rank_blog_v1&utm_term=NFM) diff --git a/docs/推荐算法基础/经典排序模型/Wide&Deep系列/Wide&Deep.md b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/Wide&Deep.md new file mode 100644 index 00000000..1b9186f1 --- /dev/null +++ b/docs/推荐算法基础/经典排序模型/Wide&Deep系列/Wide&Deep.md @@ -0,0 +1,118 @@ +# Wide & Deep +## 动机 + +在CTR预估任务中利用手工构造的交叉组合特征来使线性模型具有“记忆性”,使模型记住共现频率较高的特征组合,往往也能达到一个不错的baseline,且可解释性强。但这种方式有着较为明显的缺点: + +1. 特征工程需要耗费太多精力。 +2. 模型是强行记住这些组合特征的,对于未曾出现过的特征组合,权重系数为0,无法进行泛化。 + +为了加强模型的泛化能力,研究者引入了DNN结构,将高维稀疏特征编码为低维稠密的Embedding vector,这种基于Embedding的方式能够有效提高模型的泛化能力。但是,基于Embedding的方式可能因为数据长尾分布,导致长尾的一些特征值无法被充分学习,其对应的Embedding vector是不准确的,这便会造成模型泛化过度。 + +Wide&Deep模型就是围绕记忆性和泛化性进行讨论的,模型能够从历史数据中学习到高频共现的特征组合的能力,称为是模型的Memorization。能够利用特征之间的传递性去探索历史数据中从未出现过的特征组合,称为是模型的Generalization。Wide&Deep兼顾Memorization与Generalization并在Google Play store的场景中成功落地。 + +## 模型结构及原理 +
+image-20200910214310877 +
+ +其实wide&deep模型本身的结构是非常简单的,对于有点机器学习基础和深度学习基础的人来说都非常的容易看懂,但是如何根据自己的场景去选择那些特征放在Wide部分,哪些特征放在Deep部分就需要理解这篇论文提出者当时对于设计该模型不同结构时的意图了,所以这也是用好这个模型的一个前提。 + +**如何理解Wide部分有利于增强模型的“记忆能力”,Deep部分有利于增强模型的“泛化能力”?** + +- wide部分是一个广义的线性模型,输入的特征主要有两部分组成,一部分是原始的部分特征,另一部分是原始特征的交叉特征(cross-product transformation),对于交互特征可以定义为: + $$ + \phi_{k}(x)=\prod_{i=1}^d x_i^{c_{ki}}, c_{ki}\in \{0,1\} + $$ + $c_{ki}$是一个布尔变量,当第i个特征属于第k个特征组合时,$c_{ki}$的值为1,否则为0,$x_i$是第i个特征的值,大体意思就是两个特征都同时为1这个新的特征才能为1,否则就是0,说白了就是一个特征组合。用原论文的例子举例: + + > AND(user_installed_app=QQ, impression_app=WeChat),当特征user_installed_app=QQ,和特征impression_app=WeChat取值都为1的时候,组合特征AND(user_installed_app=QQ, impression_app=WeChat)的取值才为1,否则为0。 + + 对于wide部分训练时候使用的优化器是带$L_1$正则的FTRL算法(Follow-the-regularized-leader),而L1 FTLR是非常注重模型稀疏性质的,也就是说W&D模型采用L1 FTRL是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0,这就大大压缩了模型权重及特征向量的维度。**Wide部分模型训练完之后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力。**例如Google W&D期望wide部分发现这样的规则:**用户安装了应用A,此时曝光应用B,用户安装应用B的概率大。** + +- Deep部分是一个DNN模型,输入的特征主要分为两大类,一类是数值特征(可直接输入DNN),一类是类别特征(需要经过Embedding之后才能输入到DNN中),Deep部分的数学形式如下: + $$ + a^{(l+1)} = f(W^{l}a^{(l)} + b^{l}) + $$ + **我们知道DNN模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。**对于Deep部分的DNN模型作者使用了深度学习常用的优化器AdaGrad,这也是为了使得模型可以得到更精确的解。 + +**Wide部分与Deep部分的结合** + +W&D模型是将两部分输出的结果结合起来联合训练,将deep和wide部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。联合训练的数学形式如下:需要注意的是,因为Wide侧的数据是高维稀疏的,所以作者使用了FTRL算法优化,而Deep侧使用的是 Adagrad。 +$$ +P(Y=1|x)=\delta(w_{wide}^T[x,\phi(x)] + w_{deep}^T a^{(lf)} + b) +$$ + +## 代码实现 + +Wide侧记住的是历史数据中那些**常见、高频**的模式,是推荐系统中的“**红海**”。实际上,Wide侧没有发现新的模式,只是学习到这些模式之间的权重,做一些模式的筛选。正因为Wide侧不能发现新模式,因此我们需要**根据人工经验、业务背景,将我们认为有价值的、显而易见的特征及特征组合,喂入Wide侧** + +Deep侧就是DNN,通过embedding的方式将categorical/id特征映射成稠密向量,让DNN学习到这些特征之间的**深层交叉**,以增强扩展能力。 + +模型的实现与模型结构类似由deep和wide两部分组成,这两部分结构所需要的特征在上面已经说过了,针对当前数据集实现,我们在wide部分加入了所有可能的一阶特征,包括数值特征和类别特征的onehot都加进去了,其实也可以加入一些与wide&deep原论文中类似交叉特征。只要能够发现高频、常见模式的特征都可以放在wide侧,对于Deep部分,在本数据中放入了数值特征和类别特征的embedding特征,实际应用也需要根据需求进行选择。 + +```python +# Wide&Deep 模型的wide部分及Deep部分的特征选择,应该根据实际的业务场景去确定哪些特征应该放在Wide部分,哪些特征应该放在Deep部分 +def WideNDeep(linear_feature_columns, dnn_feature_columns): + # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型 + dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns) + + # 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding + linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns)) + + # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式 + # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层 + input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values()) + + # Wide&Deep模型论文中Wide部分使用的特征比较简单,并且得到的特征非常的稀疏,所以使用了FTRL优化Wide部分(这里没有实现FTRL) + # 但是是根据他们业务进行选择的,我们这里将所有可能用到的特征都输入到Wide部分,具体的细节可以根据需求进行修改 + linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns) + + # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型 + embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False) + + dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns)) + + # 在Wide&Deep模型中,deep部分的输入是将dense特征和embedding特征拼在一起输入到dnn中 + dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers) + + # 将linear,dnn的logits相加作为最终的logits + output_logits = Add()([linear_logits, dnn_logits]) + + # 这里的激活函数使用sigmoid + output_layer = Activation("sigmoid")(output_logits) + + model = Model(input_layers, output_layer) + return model +``` + +关于每一块的细节,这里就不解释了,在我们给出的GitHub代码中,我们已经加了非常详细的注释,大家看那个应该很容易看明白, 为了方便大家的阅读,我们这里还给大家画了一个整体的模型架构图,帮助大家更好的了解每一块以及前向传播。(画的图不是很规范,先将就看一下,后面我们会统一在优化一下这个手工图)。 + +
+image-20210228160557072 +
+ +下面是一个通过keras画的模型结构图,为了更好的显示,数值特征和类别特征都只是选择了一小部分,画图的代码也在github中。 + +
+image-20210228160557072 +
+ +## 思考 +1. 在你的应用场景中,哪些特征适合放在Wide侧,哪些特征适合放在Deep侧,为什么呢? +2. 为什么Wide部分要用L1 FTRL训练? +3. 为什么Deep部分不特别考虑稀疏性的问题? + +思考题可以参考[见微知著,你真的搞懂Google的Wide&Deep模型了吗?](https://zhuanlan.zhihu.com/p/142958834) + + +**参考资料** +- [论文原文](https://arxiv.org/pdf/1606.07792.pdf) +- [deepctr](https://github.com/shenweichen/DeepCTR) +- [看Google如何实现Wide & Deep模型(1)](https://zhuanlan.zhihu.com/p/47293765) +- [推荐系统系列(六):Wide&Deep理论与实践](https://zhuanlan.zhihu.com/p/92279796?utm_source=wechat_session&utm_medium=social&utm_oi=753565305866829824&utm_campaign=shareopn) +- [见微知著,你真的搞懂Google的Wide&Deep模型了吗?](https://zhuanlan.zhihu.com/p/142958834) +- [用NumPy手工打造 Wide & Deep](https://zhuanlan.zhihu.com/p/53110408) +- [tensorflow官网的WideDeepModel](https://www.tensorflow.org/api_docs/python/tf/keras/experimental/WideDeepModel) +- [详解 Wide & Deep 结构背后的动机](https://zhuanlan.zhihu.com/p/53361519) + +