推荐算法

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

2023-09-17  本文已影响0人  xiaogp

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

内容摘要


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有着不错的分类效果,复杂度也不高。

上一篇下一篇

猜你喜欢

热点阅读