目录

  • NFM
    • B-Interactioin Pooling Layer
    • DNN
  • Pytorch实现
    • 模型实现
    • 代码实现
    • 实验
      • 实验参数
      • 实验结果
      • 实验结果可视化
      • 实验结果分析
      • 模型统计性描述
  • 思考题
  • 参考资料

NFM

 承接DeepFM,在DeepFM中我们讨论了利用FM进行自动的二阶特征交叉,从而解决手动构造二阶交叉特征需要花费大量人力物力的缺点。但是,对于推荐系统而言,可能我们需要更高阶的特征,而二阶对于我们来说是不够的,所以我们需要获取更高阶的特则,但是同样的,手动构建高阶的特征会花费大量的精力,那么如何构建高阶的特征组合是继DeepFM之后一个我们需要解决的问题。

 那么,考虑高阶特征的问题,我们将计算公式改写为:

y(\mathbf{x}) = b+\sum_{i=1}^{N}w_ix_i+f(\mathbf{x})

 相较于DeepFM,我们在这里用f(\mathbf{x})替换了\sum_{i=1}^N\sum_{j=1}^N<v_i,v_j>x_ix_j,这里的f(\mathbf{x})就是我们要尝试构建的高阶特征组合。
 考虑到学习高阶特征一直是DNN的强项,所以在这里我们利用DNN来实现f(\mathbf{x})。于是NFM就应时出现了,下面给出NFM的结构图。


NFM结构图

 下面对这个结构进行解读:

 稀疏特征转化为embedding向量就不过多阐述了。在NFM结构图中,比较新颖的就是中间的B-Interaction Layer。那么这个模块起到什么作用呢?

B-Interaction Pooling Layer

 看名字可以看出来这是一个特征池化层,那么在这一层中,f(\mathbf{x})的表现形式为:

f(\mathbf{x}) = \sum_{i=1}^N\sum_{j=i+1}^Nx_i \mathbf{v_i}\odot x_j \mathbf{v_j}

 这里的\odot代表矩阵之间的哈达玛积,又名舒尔积逐项积,在机器学习中我们又称其为元素积

 元素积的输入:两个相同形状的矩阵。
 元素积的输出:具有同样形状的、各个位置的元素等于两个输入矩阵相同位置元素的乘积的矩阵。
参考资料:数学中几种积:点积(数量积/标量积)、叉积(叉乘/向量积)、外积(张量积/KRONECKER积)、哈达玛积(元素积)

 由以上定义可知,x_i\mathbf{v_i} \odot x_j\mathbf{v_j}的输出仍是一个向量,则f(\mathbf{x})的输出就是一个形状不变的向量,而这个向量,则是我们后面DNN的输入。

 这种处理相当于进行了二阶特征信息的合并,按照论文的思路,只要将二阶的特征学习到,后面利用DNN学习高阶特征就会更容易。

 同样的,基于FM中将计算时间复杂度降低到O(kn)的方式,我们可以将f(x)的计算降到O(kn)。

 下面给出推导过程:

\begin{aligned}
f(\mathbf{x}) &= \sum_{i=1}^N\sum_{j=i+1}^Nx_i \mathbf{v_i}\odot x_j \mathbf{v_j}\\&= \frac{1}{2}(\sum_{i=1}^N\sum_{j=1}^Nx_i \mathbf{v_i}\odot x_j \mathbf{v_j} – \sum_{i=1}^Nx_i \mathbf{v_i}\odot x_i \mathbf{v_i})\\&= \frac{1}{2}[\sum_{i=1}^Nx_i \mathbf{v_i}\sum_{j=1}^Nx_j \mathbf{v_j} – \sum_{i=1}^N(x_i \mathbf{v_i})^2]\\&= \frac{1}{2}[(\sum_{i=1}^Nx_i \mathbf{v_i})^2 – \sum_{i=1}^N(x_i \mathbf{v_i})^2]
\end{aligned}

这里相较于DeepFM,少了一个\sum_{k=1}^K的求和,但是针对K个维度都需要求一次乘积,所以时间复杂度仍是O(kn)。

DNN

 那么既然特征交互层已经完成了特征信息的交互,于是利用DNN就可以进行高阶特征的组合以及非线性的变换。
 此时DNN的输出是B-Interaction Pooling Layer的输出,即1×k的向量,经过DNN输出得到高阶特征组合的输出,考虑到此处DNN是做回归问题,所以最后的输出层不需要添加激活层,从而最终得到:

y(\mathbf{x}) = b + \sum_{i=1}^Nw_ix_i + \mathbf{h}^TG(f(\mathbf{x}))

 其中\mathbf{h}^T为输出层的转移矩阵, G(f(x))为不包括输出层的DNN的隐藏状态。

Pytorch实现

 尝试使用Pytorch复现,全部代码可在我的GitHub找到,https://github.com/EternalStarICe/recommendation-system-model

模型实现

  1. 对于一阶特征的计算,数值特征直接使用一个Linear层完成计算,类别特征利用emb_len=1的embedding层来模拟其系数,最后将两个值相加则得到一阶特征的最终结果
  2. 对于B-Interaction Pooling Layer,编写BInteraction Pooling函数,输入大小为(batch_size, feature_num, emb_len),按照公式编写处理的代码,输出结果的大小为(batch_size, emb_len)
  3. DNN输入之前先将Pooling的结果进行BatchNorm,再送入DNN中进行计算
  4. 将DNN的结果和一阶特征的结果相加,利用Sigmoid求出最终的结果

代码实现

import torch
import torch.nn as nn


def BInteractionPolling(x): #  (batch_size, feature_num, emb_len)
    left = torch.square(torch.sum(x, dim=1)) # (batch_size, emb_len)
    right = torch.sum(torch.square(x), dim=1) # (batch_size, emb_len)
    return 0.5 * (left - right) # (batch_size, emb_len)


class NFM(nn.Module):
    def __init__(self, num_cols, cat_cols, cat_tuple_list, emb_len=4):
        super(NFM, self).__init__()
        self.num_cols = num_cols
        self.cat_cols = cat_cols
        self.num_col_len = len(self.num_cols)
        self.cat_col_len = len(self.cat_cols)
        self.emb_len = emb_len
        self.cat_tuple_list = cat_tuple_list

        # 一阶特征交叉
        self.degree1_linear_num = nn.Linear(self.num_col_len, 1)
        self.degree1_linear_cat = nn.ModuleList()
        for fc in cat_tuple_list:
            self.degree1_linear_cat.append(nn.Embedding(fc.vocab_size, 1))

        # Pooling层
        self.degree2_cat = nn.ModuleList()
        for fc in cat_tuple_list:
            self.degree2_cat.append(nn.Embedding(fc.vocab_size, self.emb_len))

        # DNN
        self.linear1 = nn.Linear(emb_len, 1024)
        self.linear2 = nn.Linear(1024, 512)
        self.linear3 = nn.Linear(512, 256)
        self.linear4 = nn.Linear(256, 1)
        self.relu = nn.ReLU()
        self.dropout1 = nn.Dropout(0.5)
        self.dropout2 = nn.Dropout(0.3)
        self.dropout3 = nn.Dropout(0.1)
        self.sigmoid = nn.Sigmoid()
        self.batchnorm = nn.BatchNorm1d(num_features=self.emb_len)

    def forward(self, x):
        cat_x = x[:, :self.cat_col_len]
        num_x = x[:, self.cat_col_len:]

        # 一阶
        degree1_num_output = self.degree1_linear_num(num_x)
        degree1_cat_output_list = []
        for i in range(self.cat_col_len):
            degree1_cat_output_list.append(self.degree1_linear_cat[i](cat_x[:, i].long()))
        degree1_cat_output = degree1_cat_output_list[0]
        for i in range(1, self.cat_col_len):
            degree1_cat_output += degree1_cat_output_list[i]

        degree1_output = degree1_cat_output + degree1_num_output

        # 高阶
        pooling_input = self.degree2_cat[0](cat_x[:, 0].long()).unsqueeze(1)
        for i in range(1, self.cat_col_len):
            pooling_input = torch.cat([pooling_input, self.degree2_cat[i](cat_x[:, i].long()).unsqueeze(1)], dim=1)
        pooling_output = BInteractionPolling(pooling_input) # (batch_size, emb_len)

        # DNN
        pooling_output = self.batchnorm(pooling_output)
        highrank_output = self.dropout1(self.relu(self.linear1(pooling_output)))
        highrank_output = self.dropout2(self.relu(self.linear2(highrank_output)))
        highrank_output = self.dropout3(self.relu(self.linear3(highrank_output)))
        highrank_output = self.linear4(highrank_output)

        ouput = self.sigmoid(highrank_output+degree1_output)
        output = torch.cat([ouput, 1-ouput], dim=1)
        return output

实验

实验参数

参数
epochs 200
optimizer Adam
lr 0.0001
loss_fn CrossEntropyLoss
metric Accuracy
batch_size 128

实验结果

epoch 1 loss 0.005983 train acc 0.544090
epoch 2 loss 0.005523 train acc 0.609131
epoch 3 loss 0.004828 train acc 0.699812
epoch 4 loss 0.004467 train acc 0.760475
epoch 5 loss 0.004299 train acc 0.787367
...
epoch 195 loss 0.004080 train acc 0.813634
epoch 196 loss 0.004067 train acc 0.811757
epoch 197 loss 0.004057 train acc 0.815510
epoch 198 loss 0.004123 train acc 0.813008
epoch 199 loss 0.004045 train acc 0.813634
epoch 200 loss 0.004086 train acc 0.814884

实验结果可视化


准确率迭代图


损失迭代图

实验结果分析

 总体而言,准确率在逐渐上升而损失逐渐下降,呈现正确的收敛趋势。但是可以观察到收敛曲线是不稳定的,论文中给出这种不稳定的可能性是因为同时添加了BN层和dropout方法。下面是论文原文的解释:

Furthermore, we notice that BN makes the learning less stable, as evidenced by the larger performance fluctuation of blue lines. This is caused by our use of dropout and BN together, as randomly dropping neurons can change the input distribution normalized by BN. It is an interesting direction to effectively combine BN and dropout.

模型统计性描述

 其中,利用torchkeras提供的工具对整个模型进行统计性描述,结果如下:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                    [-1, 1]              14
         Embedding-2                    [-1, 1]              73
         Embedding-3                    [-1, 1]             230
         Embedding-4                    [-1, 1]           1,054
         Embedding-5                    [-1, 1]             854
         Embedding-6                    [-1, 1]              30
         Embedding-7                    [-1, 1]               7
         Embedding-8                    [-1, 1]             986
         Embedding-9                    [-1, 1]              34
        Embedding-10                    [-1, 1]               2
        Embedding-11                    [-1, 1]             766
        Embedding-12                    [-1, 1]             799
        Embedding-13                    [-1, 1]           1,011
        Embedding-14                    [-1, 1]             713
        Embedding-15                    [-1, 1]              19
        Embedding-16                    [-1, 1]             715
        Embedding-17                    [-1, 1]             949
        Embedding-18                    [-1, 1]               9
        Embedding-19                    [-1, 1]             469
        Embedding-20                    [-1, 1]             173
        Embedding-21                    [-1, 1]               4
        Embedding-22                    [-1, 1]             985
        Embedding-23                    [-1, 1]               7
        Embedding-24                    [-1, 1]              12
        Embedding-25                    [-1, 1]             605
        Embedding-26                    [-1, 1]              32
        Embedding-27                    [-1, 1]             455
        Embedding-28                    [-1, 4]             292
        Embedding-29                    [-1, 4]             920
        Embedding-30                    [-1, 4]           4,216
        Embedding-31                    [-1, 4]           3,416
        Embedding-32                    [-1, 4]             120
        Embedding-33                    [-1, 4]              28
        Embedding-34                    [-1, 4]           3,944
        Embedding-35                    [-1, 4]             136
        Embedding-36                    [-1, 4]               8
        Embedding-37                    [-1, 4]           3,064
        Embedding-38                    [-1, 4]           3,196
        Embedding-39                    [-1, 4]           4,044
        Embedding-40                    [-1, 4]           2,852
        Embedding-41                    [-1, 4]              76
        Embedding-42                    [-1, 4]           2,860
        Embedding-43                    [-1, 4]           3,796
        Embedding-44                    [-1, 4]              36
        Embedding-45                    [-1, 4]           1,876
        Embedding-46                    [-1, 4]             692
        Embedding-47                    [-1, 4]              16
        Embedding-48                    [-1, 4]           3,940
        Embedding-49                    [-1, 4]              28
        Embedding-50                    [-1, 4]              48
        Embedding-51                    [-1, 4]           2,420
        Embedding-52                    [-1, 4]             128
        Embedding-53                    [-1, 4]           1,820
      BatchNorm1d-54                    [-1, 4]               8
           Linear-55                 [-1, 1024]           5,120
             ReLU-56                 [-1, 1024]               0
          Dropout-57                 [-1, 1024]               0
           Linear-58                  [-1, 512]         524,800
             ReLU-59                  [-1, 512]               0
          Dropout-60                  [-1, 512]               0
           Linear-61                  [-1, 256]         131,328
             ReLU-62                  [-1, 256]               0
          Dropout-63                  [-1, 256]               0
           Linear-64                    [-1, 1]             257
          Sigmoid-65                    [-1, 1]               0
================================================================
Total params: 716,492
Trainable params: 716,492
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.000149
Forward/backward pass size (MB): 0.042061
Params size (MB): 2.733200
Estimated Total Size (MB): 2.775410
----------------------------------------------------------------

思考题

  1. NFM中的特征交叉与FM中的特征交叉有何异同,分别从原理和代码实现上进行对比分析
FM NFM
特征交叉方式 传统的FM 采用B-Interaction Pooling Layer和DNN
交叉结果 可以自动进行特征交叉,得到二阶特征 合并二阶特征的信息,利用DNN进行更高阶特征的学习
f(\mathbf{x})的输出 一个值 一条向量
代码实现 将Embedding送入FM模块中,按照公式计算出FM的值,利用误差更新FM中的V,其中公式计算的为内积,且最终需要对K个维度进行求和 NFM利用B-Interaction Pooling得到高阶特征的向量,再送入DNN进行计算,其中公式计算的是元素积,且最终不需要对K个维度进行求和

 给出FM和NFM的B-Interaction Pooling的代码展示:

# FM
def FM(x): # x:(batch, feature_num, emb_len)
    left = torch.square(torch.sum(x, dim=1, keepdim=True)) # (batch, 1, emb_len)
    right = torch.sum(torch.square(x), dim=1, keepdim=True) # (batch, 1, emb_len)
    res = 0.5 * torch.sum((left - right), dim=2) # (batch, 1)
    return res

# BInteractionPolling
def BInteractionPolling(x): #  (batch_size, feature_num, emb_len)
    left = torch.square(torch.sum(x, dim=1)) # (batch_size, emb_len)
    right = torch.sum(torch.square(x), dim=1) # (batch_size, emb_len)
    return 0.5 * (left - right) # (batch_size, emb_len)

参考资料

  1. datawhale深度推荐系统
  2. 论文原文-Neural Factorization Machines for Sparse Predictive Analytics
  3. https://github.com/zhongqiangwu960812/AI-RecommenderSystem
  4. 数学中几种积:点积(数量积/标量积)、叉积(叉乘/向量积)、外积(张量积/KRONECKER积)、哈达玛积(元素积)
  5. AI上推荐 之 FNN、DeepFM与NFM(FM在深度学习中的身影重现)

0 条评论

发表评论