美文网首页推荐算法
特征交叉系列:NFM原理和实践,使用交叉池化连接FM和DNN

特征交叉系列:NFM原理和实践,使用交叉池化连接FM和DNN

作者: xiaogp | 来源:发表于2023-09-17 07:43 被阅读0次

    关键词:NFMFMDMM推荐算法,特征交叉

    内容摘要

    • NFM介绍和结构简述
    • 交叉池化层解析
    • NFM和FNN,PNN的联系和区别
    • NFM在PyTorch下的实践和效果对比

    NFM介绍和结构简述

    在上一节介绍了PNN特征交叉系列:PNN向量积模型理论和实践,FM和DNN的串联,本节继续介绍另一种FM和DNN串行合并的算法NFM。
    NFM(Neural Factorization Machines)发表于2017年,NFM针对FM的二阶交互部分进行了升级优化,NFM认为现实任务中数据具有高度的非线性关系,而FM虽然能够学习到二阶交互作用但依旧是一个线性模型,而DNN通过多层感知机激活函数具有强大的非线性能力,因此NFM提出利用DNN来赠与FM非线性能力,实现一个增强版的FM。
    另外点乘聚合的方式使得FM丢失部分交叉信息,NFM引入交叉池化层保留了交叉之后的向量乘积,给到下游的DNN做信息高阶聚合。
    NFM的一阶部分和FM一样,特征交叉部分网络结构图如下

    NFM网络结构

    底层输入依次进入Embedding层,B-Interaction层,和最后的多层感知机层,其中Embedding层等同于FM的隐向量嵌入层,在B-Interaction层中完成向量交叉池化操作,然后进入全连接层得到特征交叉层的输出,再和线性层的结果相加得到NFM的最终输出。


    交叉池化层解析

    NFM主要的创新点是通过Bi-Interaction Pooling特征交叉池化层来连接FM和DNN,该层的计算公式如下


    特征交叉池化操作公式

    简单而言将Embedding层输出的向量做两两哈达马积计算(element-wise相乘),再将结果向量全部同位置相加池化,最终输出一个[batch, emb_size]的二维向量作为交叉的结果,计算示意图如下

    交叉池化层的计算示意图

    本质上是让FM只做向量对应位置相乘,不做点积,点积是对相乘结果的降维和信息浓缩,NFM保留了向量相乘的结果做相加池化直接输出给下一层的全连接,由全连接完成更高阶的信息浓缩。显然NFM作者认为如果在FM层就点乘将信息浓缩为标量会造成交叉信息的丢失,因此NFM只做对应位置相乘保留交叉后的完整向量,期望包含更多的交叉信息信息。
    交叉池化层的计算公式和FM一样可以优化为如下形式

    交叉池化层的优化公式

    和FM的公式相比在1/2后面少一个∑求和,其他和FM都是一致的。


    NFM和FNN,PNN的联系和区别

    NFM,FNN,PNN三者都是FM和DNN串行合并的算法,目标都是引入DNN对FM的二阶结果做进一步的高阶学习,区别在于连接FM和DNN的方式不同。
    FNN:将FM的隐向量结果直接拼接拉直输入给DNN。
    PNN:通过内积,外积的方式将两两向量的交叉浓缩为一个标量,再将标量拼接拉直输入给DNN,而原本的FM是会将所有浓缩结果是直接相加作为输出的。
    NFM:两向量之间仅进行对应位置相乘,不做信息浓缩,将所有向量进行相加池化后输入给DNN,而DNN的任务就是基于输入的二阶融合信息向量,继续进行高阶的信息抽取和浓缩。


    NFM在PyTorch下的实践和效果对比

    本次实践的数据集和上一篇特征交叉系列:完全理解FM因子分解机原理和代码实战一致,采用用户的购买记录流水作为训练数据,用户侧特征是年龄,性别,会员年限等离散特征,商品侧特征采用商品的二级类目,产地,品牌三个离散特征,随机构造负样本,一共有10个特征域,全部是离散特征,对于枚举值过多的特征采用hash分箱,得到一共72个特征。
    PyTorch代码实现如下

    class Embedding(nn.Module):
        def __init__(self, feat_dim, emb_dim):
            super(Embedding, self).__init__()
            self.embedding = nn.Embedding(feat_dim, emb_dim)
            nn.init.xavier_normal_(self.embedding.weight.data)
    
        def forward(self, x):
            # [None, field_num] => [None, field_num, emb_dim]
            return self.embedding(x)
    
    
    class FM(nn.Module):
        def __init__(self):
            super(FM, self).__init__()
    
        def forward(self, x):
            # x=[None, field_num, emb_dim]
            square_of_sum = torch.square(torch.sum(x, dim=1))
            sum_of_square = torch.sum(torch.square(x), dim=1)
            return 0.5 * (square_of_sum - sum_of_square)
    
    
    class Linear(nn.Module):
        def __init__(self, feat_dim):
            super(Linear, self).__init__()
            self.embedding = nn.Embedding(feat_dim, 1)
            self.bias = nn.Parameter(torch.zeros(1))
            nn.init.xavier_normal_(self.embedding.weight.data)
    
        def forward(self, x):
            # [None, field_num] => [None, field_num, 1] => [None, 1]
            return self.embedding(x).sum(dim=1) + self.bias
    
    
    class NFM(nn.Module):
        def __init__(self, feat_num, emb_dim, fc_dims=(64, 16), dropout=0.1):
            super(NFM, self).__init__()
            self.linear = Linear(feat_dim=feat_num)
            self.embedding = Embedding(feat_dim=feat_num, emb_dim=emb_dim)
            self.fm = FM()
            self.fm_pool = nn.Sequential(self.fm, nn.BatchNorm1d(emb_dim), nn.Dropout(dropout))
            layers = []
            fc_input_dim = emb_dim
            for fc_dim in fc_dims:
                layers.append(nn.Linear(fc_input_dim, fc_dim))
                layers.append(nn.BatchNorm1d(fc_dim))
                layers.append(nn.ReLU())
                layers.append(nn.Dropout(dropout))
                fc_input_dim = fc_dim
            layers.append(nn.Linear(fc_input_dim, 1))
            self.mlp = torch.nn.Sequential(*layers)
    
        def forward(self, x):
            emb = self.embedding(x)
            linear = self.linear(x)
            fm_pool = self.fm_pool(emb)
            out = linear + self.mlp(fm_pool)
            return torch.sigmoid(out).squeeze(dim=1)
    

    交叉池化操作在FM子模块中,仅需要删除一般FM对第二维度的求和即可,删除求和输出二维向量,保留求和输出标量。

    # 一般FM的输出
    0.5 * torch.sum(square_of_sum - sum_of_square, dim=1, keepdim=True)
    

    原论文中添加了BatchNormalizationDropout技巧,在代码中也有体现,分别添加到交叉池化层和最后的全连接层。
    本例全部是离散分箱变量,所有有值的特征都是1,因此只要输入有值位置的索引即可,一条输入例如

    >>> train_data[0]
    Out[120]: (tensor([ 2, 10, 14, 18, 34, 39, 47, 51, 58, 64]), tensor(0))
    

    其中x的长度10代表10个特征域,每个域的值是特征的全局位置索引,从0到71,一共72个特征。其中FM和DNN共用了Embedding对象。
    采用10次验证集AUC不上升作为早停条件,对比NFM,PNN,FM,FFM,DeepFM的验证集的平均AUC如下

    算法 AUC 参数数量
    FM 0.6274 361
    FFM 0.6317 2953
    IPNN 0.6322 15553
    OPNN 0.6326 27073
    PNN* 0.6342 29953
    DeepFM 0.6322 12746
    NFM 0.6329 10186

    在本数据集上NFM相比于FM有明显提升(0.005),在千分之一位上相比FFM有提升,万分之一位略高于DeepFM,PNN但不明显,低于融合内外积的PNN*。
    NFM的参数数量是所有带有DNN结构里面最少的,综合来看NFM有着不错的分类效果,复杂度也不高。

    相关文章

      网友评论

        本文标题:特征交叉系列:NFM原理和实践,使用交叉池化连接FM和DNN

        本文链接:https://www.haomeiwen.com/subject/fiqpvdtx.html