关键词:NFM
,FM
,DMM
,推荐算法
,特征交叉
内容摘要
- 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一样,特征交叉部分网络结构图如下
底层输入依次进入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)
原论文中添加了BatchNormalization,Dropout技巧,在代码中也有体现,分别添加到交叉池化层和最后的全连接层。
本例全部是离散分箱变量,所有有值的特征都是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有着不错的分类效果,复杂度也不高。
网友评论