NFM 网络介绍与源码浅析
前言
OK, 周末继续肝!!! 昨晚完成了 FNN 网络介绍与源码浅析
广而告之
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;
NFM (Neural Factorization Machines)
文章信息
- 论文标题: Neural Factorization Machines for Sparse Predictive Analytics
- 论文地址: https://arxiv.org/abs/1708.05027
- 代码地址: https://github.com/hexiangnan/neural_factorization_machine
- 发表时间: SIGIR 2017
- 论文作者: Xiangnan He, Tat-Seng Chua
- 作者单位: National University of Singapore
核心观点
本文主要介绍了 Bi-Interaction Pooling Layer, 用于对二阶交叉特征进行建模. 和 FM 使用内积对二阶交叉特征建模的思路不同的是, Bi-Interaction Pooling Layer 是将两两特征进行 element-wise product, 生成了 n × ( n − 1 ) 2 \frac{n\times (n - 1)}{2} 2n×(n−1) 个交叉特征, 然后再对这些交叉特征进行累加 (这应该就是名字中 Pooling 的含义), 当然在实际计算中, 是无需将 n × ( n − 1 ) 2 \frac{n\times (n - 1)}{2} 2n×(n−1) 个交叉特征全部算出来的, 而是类似 FM 的思路, 对计算公式做了调整, 降低了计算复杂度. Bi-Interaction Pooling Layer 的输出结果之后会进一步输入到 MLP 中, 以学习更丰富高阶的特征.
另外, 为了利用到低阶特征, 作者设计的网络中也包含了线性部分 (网络结构图中没有画出来), 类似于 Wide & Deep 的结构.
还有, 我觉得网络的 Deep 部分最后的效果其实就是将 FM 的二阶交叉特征输入到 DNN 网络中~~
核心观点介绍
NFM 网络的 Deep 部分结构图如下所示 (注意这是 Deep 部分, 线性部分没有在图中体现):
![](https://i-blog.csdnimg.cn/blog_migrate/e4dafe660af97f5a04a117aeb05fa521.png#pic_center)
其中 V x = { x 1 v 1 , … , x n v n } \mathcal{V}_{x} = \left\{x_{1} \mathbf{v}_{1}, \ldots, x_{n} \mathbf{v}_{n}\right\} Vx={x1v1,…,xnvn} 表示输入特征 x \bm{x} x 对应的 embedding 向量. 之后特征的 embeddings 输入到 Bi-Interaction Layer 中, 得到的结果如下:
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=1∑nj=i+1∑nxivi⊙xjvj
其中 ⊙ \odot ⊙ 表示 element-wise product; 注意到 Bi-Interaction Layer 效果和 Pooling operation 是一样的, 将多个交叉特征累加, 转换为一个向量. 上式可以用线性时间计算出来, 只需要将公式改为:
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=1∑nxivi)2−i=1∑n(xivi)2⎦⎤
其中 v 2 \mathbf{v}^2 v2 表示 v ⊙ v \mathbf{v}\odot\mathbf{v} v⊙v. 是不是嗅到了熟悉的 FM 的味道 🤣 🤣 🤣
之后, Bi-Interaction Layer 的输出结果进一步输入到 MLP 中, 得到向量 z L \bm{z}_L zL, 为了在输出层得到预测 score, 还需要使用权重 h T \bm{h}^T hT 将 z L \bm{z}_L zL 转化为数值: h T ⋅ z L \bm{h}^T\cdot\bm{z}_L hT⋅zL.
另外, NFM 还用线性层对低阶特征进行了处理. 因此, NFM 的完整结构可以公式表示为:
y ^ N F M ( 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}_{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} y^NFM(x)=w0+i=1∑nwixi+hTσL(WL(…σ1(W1fBI(Vx)+b1)…)+bL)
整个模型的参数为: Θ = { w 0 , { w i , v i } , h , { W l , b l } } \Theta=\left\{w_{0},\left\{w_{i}, \mathbf{v}_{i}\right\}, \mathbf{h},\left\{\mathbf{W}_{l}, \mathbf{b}_{l}\right\}\right\} Θ={w0,{wi,vi},h,{Wl,bl}}, 和 FM 相比, NFM 多出的参数主要是 { W l , b l } \left\{\mathbf{W}_{l}, \mathbf{b}_{l}\right\} {Wl,bl}, 即 MLP 的参数, 主要用来对特征间更高阶的交叉进行学习.
下面看看代码:
来自 https://github.com/hexiangnan/neural_factorization_machine/blob/master/NeuralFM.py
其中 NFM 的核心实现如下:
# Model.
# _________ sum_square part _____________
# get the summed up embeddings of features.
nonzero_embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'], self.train_features)
self.summed_features_emb = tf.reduce_sum(nonzero_embeddings, 1) # None * K
# get the element-multiplication
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K
# _________ square_sum part _____________
self.squared_features_emb = tf.square(nonzero_embeddings)
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K
# ________ FM __________
self.FM = 0.5 * tf.sub(self.summed_features_emb_square, self.squared_sum_features_emb) # None * K
if self.batch_norm:
self.FM = self.batch_norm_layer(self.FM, train_phase=self.train_phase, scope_bn='bn_fm')
self.FM = tf.nn.dropout(self.FM, self.dropout_keep[-1]) # dropout at the bilinear interactin layer
# ________ Deep Layers __________
for i in range(0, len(self.layers)):
self.FM = tf.add(tf.matmul(self.FM, self.weights['layer_%d' %i]), self.weights['bias_%d'%i]) # None * layer[i] * 1
if self.batch_norm:
self.FM = self.batch_norm_layer(self.FM, train_phase=self.train_phase, scope_bn='bn_%d' %i) # None * layer[i] * 1
self.FM = self.activation_function(self.FM)
self.FM = tf.nn.dropout(self.FM, self.dropout_keep[i]) # dropout at each Deep layer
self.FM = tf.matmul(self.FM, self.weights['prediction']) # None * 1
# _________out _________
Bilinear = tf.reduce_sum(self.FM, 1, keep_dims=True) # None * 1
self.Feature_bias = tf.reduce_sum(tf.nn.embedding_lookup(self.weights['feature_bias'], self.train_features) , 1) # None * 1
Bias = self.weights['bias'] * tf.ones_like(self.train_labels) # None * 1
self.out = tf.add_n([Bilinear, self.Feature_bias, Bias]) # None * 1
代码并不复杂, 其中:
nonzero_embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'], self.train_features)
self.summed_features_emb = tf.reduce_sum(nonzero_embeddings, 1) # None * K
# get the element-multiplication
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K
# _________ square_sum part _____________
self.squared_features_emb = tf.square(nonzero_embeddings)
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K
# ________ FM __________
self.FM = 0.5 * tf.sub(self.summed_features_emb_square, self.squared_sum_features_emb) # None * K
和 FM 就是一模一样的~, 只不过 FM 最后还要对 embedding 这个维度 (就是注释中 K
这个维度) 进行累加.