Datawhale第23期组队学习—深度学习推荐系统—task4 NFM

文章参考来源:

  1. https://github.com/datawhalechina/team-learning-rs/blob/master/DeepRecommendationModel/NFM.md
  2. https://zhuanlan.zhihu.com/p/92293407
  3. https://www.jianshu.com/p/1d157d5146ad

1. 引言

在提出说明NFM之前,我们先来回顾一下FM的模型:
y ^ F M ( x ) = w 0 + ∑ i = 1 N w i x i + ∑ i = 1 N ∑ j = i + 1 N v i T v j ⋅ x i x j \hat y_{FM}(x) = w_0 + \sum_{i=1}^Nw_ix_i +\sum_{i=1}^N\sum_{j=i+1}^{N}v_i^Tv_j \cdot x_ix_j y^FM(x)=w0+i=1Nwixi+i=1Nj=i+1NviTvjxixj
由公式可以看出,FM模型虽然考虑了组合特征,但最多只到二阶交叉特征,而且其本质仍是一种线性模型,模型的表征能力终究是有限的。如果想突破这个局限性,就需要从模型本身动手了。

NFM(Neural Factorization Machines)是2017年由新加坡国立大学的何向南教授等人在SIGIR会议上提出的一个模型,传统的FM模型仅局限于线性表达和二阶交互, 无法胜任生活中各种具有复杂结构和规律性的真实数据, 针对FM的这点不足, 作者提出了一种将FM融合进DNN的策略,通过引进了一个特征交叉池化层的结构,使得FM与DNN进行了完美衔接,这样就组合了FM的建模低阶特征交互能力和DNN学习高阶特征交互和非线性的能力。

2. 模型结构与原理

首先看一下NFM的公式:
y ^ N F M ( x ) = w 0 + ∑ i = 1 n w i x i + f ( x ) \hat{y}_{N F M}(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+f(\mathbf{x}) y^NFM(x)=w0+i=1nwixi+f(x)
与FM相比,NFM仅仅在第三项发生了变化。FM的第三项为特征的两两组合,而NFM则换成了 f ( x ) f(x) f(x),这是否意味着模型更简单了?仅从公式上来看,确实好像变得简单了一些,那么这个FM究竟是什么呢?前面说到,FM的局限性在于线性表达和仅到二阶的特征交互,而NFM又是由FM和DNN的组合,所以这里的 f ( x ) f(x) f(x)由DNN来充当。理论上神经网络可以拟合任何复杂能力的函数, 当然不是一个简单的DNN, 而是依然底层考虑了交叉,然后高层使用的DNN网络, 这个也就是我们最终的NFM网络:
在这里插入图片描述
提到在线性模型里引入DNN,不免就会想到以往的类似模型,如Wide&Deep、DeepFM、DeepCrossing等。这些模型对于二阶交叉特征向量仅进行简单concatenation,然后送入后续DNN部分,将捕获高阶特征交叉信息的任务交给了DNN结构。这种简单拼接的方式,并没有将二阶交叉特征的信息完全表征出来,导致后期DNN的工作量很大,基于此结构学习更高阶交叉信息效率太低。而NFM使用Bi-Interaction Layer(Bi-linear interaction)结构来对二阶交叉信息进行处理,使交叉特征的信息能更好的被DNN结构学习,降低DNN学习更高阶交叉特征信息的难度。减轻DNN的负担,意味着不再需要更深的网络结构,从而模型参数量也减少了,模型训练更便捷。

Input 和Embedding层
Input和以往的很多输入一样,大多是one-hot稀疏向量,首先要通过embedding将其转化为低维,稠密向量。 这两层还是和之前的FM一样,利用FM对输入层特征进行提取,得到对应的交叉系数不为的交叉变量。最终输入特征向量是由输入特征 x i x_i xi值与 embedding vector v i v_i vi相乘得到,即 V x = { x 1 v 1 , … , x n v n } \mathcal{V}_{x} = \{x_1 \mathbf{v}_1, \ldots, x_n \mathbf{v}_n\} Vx={x1v1,,xnvn}

Bi-Interaction Pooling layer
NFM最大的特点便是在Embedding层和神经网络之间加入了Bi-Interacction Pooling layer(特征交叉池化层),通过这个结构,实现了FM与DNN的无缝连接, 组成了一个大的网络,且能够正常的反向传播。假设 V x \mathcal{V}_{x} Vx是所有特征embedding的集合, 那么在特征交叉池化层的操作:

f B I ( V x ) = ∑ i = 1 n ∑ j = i + 1 n x i v i ⊙ x j v j 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} fBI(Vx)=i=1nj=i+1nxivixjvj

⊙ \odot 表示两个向量的元素积操作,即两个向量对应维度相乘得到的元素积向量,例如其中第 k k k维的操作: ( v i ⊙ v j ) k = v i k v j k \left(v_{i} \odot v_{j}\right){k}=\boldsymbol{v}_{i k} \boldsymbol{v}_{j k} (vivj)k=vikvjk

这便定义了在embedding空间特征的二阶交互,这个不仔细看会和感觉FM的最后一项很像,但是不一样,一定要注意这个地方不是两个隐向量的内积,而是元素积,也就是这一个交叉完了之后k个维度不求和,最后会得到一个k维向量,而FM最后得到一个数, 在进行两两Embedding元素积之后,对交叉特征向量取和, 得到该层的输出向量, 很显然, 输出是一个 k k k维的向量。

FM模型到这个位置就结束了,剩下的就是输出。而NFM在加入特征池化层之后, 把二阶交互的信息合并, 再接上一个DNN网络, 这样就能够增强FM的表达能力了。这里的DNN可以进行多阶且非线性,只要FM把二阶的学习好了,DNN这块学习来会更加容易,这就很好的解决了开始提出的FM的两个问题:①线性②特征交叉只到二阶

Bi-Interaction层不需要额外的模型学习参数,更重要的是它在一个线性的时间内完成计算,和FM一致的,即时间复杂度为 O ( k N x ) O\left(k N_{x}\right) O(kNx) N x N_x Nx为embedding向量的数量。参考FM,可以将上式转化为: f B I ( V x ) = 1 2 [ ( ∑ i = 1 n x i v i ) 2 − ∑ i = 1 n ( x i v i ) 2 ] 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] fBI(Vx)=21[(i=1nxivi)2i=1n(xivi)2] 后面代码复现NFM就是用的这个公式直接计算,比较简便且清晰。

Hidden layers(隐藏层)
这一层就是全连接的神经网络, DNN在进行特征的高层非线性交互上有着天然的学习优势,公式如下: z 1 = σ 1 ( W 1 f B I ( V x ) + b 1 )   z 2 = σ 2 ( W 2 z 1 + b 2 )   … …   z L = σ L ( W L z L − 1 + b L ) \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} z1=σ1(W1fBI(Vx)+b1) z2=σ2(W2z1+b2)  zL=σL(WLzL1+bL) 这里的 σ i \sigma_i σi是第 i i i层的激活函数,可不要理解成sigmoid激活函数。

预测层
这个就是最后一层的结果直接过一个隐藏层,但注意由于这里是回归问题,没有加sigmoid激活: f ( x ) = h T z L f(\mathbf{x})=\mathbf{h}^{T} \mathbf{z}_{L} f(x)=hTzL

NFM模型的前向传播过程总结如下:
y ^ N F M ( X ) = w 0 + ∑ i = 1 n w i x i + f ( X ) = w 0 + ∑ i = 1 n w i x i + h T σ L ( W L ( … σ 1 ( W 1 f B I ( V x ) + b 1 ) … ) + b L ) \begin{aligned} \hat{y}_{NFM}(\mathbf{X}) =w_0+\sum_{i=1}^{n}w_ix_i+f(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} y^NFM(X)=w0+i=1nwixi+f(X)=w0+i=1nwixi+hTσL(WL(σ1(W1fBI(Vx)+b1))+bL)
NFM相比较于其他模型的核心创新点是特征交叉池化层,基于它,实现了FM和DNN的无缝连接,使得DNN可以在底层就学习到包含更多信息的组合特征,这时候,就会减少DNN的很多负担,只需要很少的隐藏层就可以学习到高阶特征信息。NFM相比之前的DNN, 模型结构更浅,更简单,但是性能更好,训练和调参更容易。集合FM二阶交叉线性和DNN高阶交叉非线性的优势,非常适合处理稀疏数据的场景任务。

3. 代码实现

由NFM的公式可知,NFM的输入由两部分组成,分别为线性部分 ∑ i = 1 n w i x i \sum_{i=1}^nw_ix_i i=1nwixi和非线性部分 f ( x ) f(x) f(x),而在NFM中,非线性部分是用DNN实现的,所以在代码部分的输入对应为linear_feature_colums和dnn_feature_colums

linear part: 这部分是有关于线性计算,也就是FM的前半部分 w 1 x 1 + w 2 x 2... w n x n + b w1x1+w2x2...wnxn+b w1x1+w2x2...wnxn+b的计算。对于这一块的计算,我们用了一个get_linear_logits函数实现,通过这个函数,我们就可以实现上面这个公式的计算过程,得到linear的输出。

dnn part: 这部分是后面交叉特征的那部分计算,FM的最后那部分公式f(x)。 这一块主要是针对离散的特征,首先过embedding, 然后过特征交叉池化层,这个计算我们用了get_bi_interaction_pooling_output函数实现, 得到输出之后又过了DNN网络,最后得到dnn的输出。

模型的最后输出结果,就是把这两个部分的输出结果加和(当然也可以加权),再过一个sigmoid得到。

def NFM(linear_feature_columns, dnn_feature_columns):
    """
    :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的输入部分
    # 可以发现DNN部分的输入除了dnn_feature_columns以外,还有sparse_input部分
    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部分的结果相加
    output_logits = Add()([linear_logits, dnn_logits])

	#最后再过个sigmoid	
    output_layers = Activation("sigmoid")(output_logits)
    
    #保存模型
    model = Model(inputs=input_layers, outputs=output_layers)
    
    return model
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值