推荐系统之NFM

前言

CTR预测任务中, 高阶特征和低阶特征的学习都非常的重要。 推荐模型我们也学习了很多,基本上是从最简单的线性模型(LR), 到考虑低阶特征交叉的FM, 到考虑高度交叉的神经网络,再到两者都考虑的W&D组合模型。 其实这些模型又存在着自己的问题,也是后面模型不断需要进行改进的原理,主要有下面几点:

简单的线性模型虽然简单,同样这样是它的不足,就是限制了模型的表达能力,随着数据的大且复杂,这种模型并不能充分挖掘数据中的隐含信息,且忽略了特征间的交互,如果想交互,需要复杂的特征工程。
FM模型考虑了特征的二阶交叉,但是这种交叉仅停留在了二阶层次,虽然说能够进行高阶,但是计算量和复杂性一下子随着阶数的增加一下子就上来了。所以二阶是最常见的情况,会忽略高阶特征交叉的信息
DNN,适合天然的高阶交叉信息的学习,但是低阶的交叉会忽略掉,并且不能够实时的进行更新参数,而且记忆能力较弱。
W&D模型进行了一个伟大的尝试,把简单的LR模型和DNN模型进行了组合, 使得模型既能够学习高阶组合特征,又能够学习低阶的特征模式,但是W&D的wide部分是用了LR模型, 这一块依然是需要一些经验性的特征工程的,且Wide部分和Deep部分需要两种不同的输入模式, 这个在具体实际应用中需要很强的业务经验。
所以相应的算法也就应运而生,关于FNN、DeepFM前面都已经讲解过了,今天讲一下NFM,希望大家千万不要混淆了,hhh。

一、NFM模型的原理和论文细节

NFM(Neural Factorization Machines)是2017年由新加坡国立大学的何向南教授等人在SIGIR会议上提出的一个模型。 在介绍DeepFM的时候, 作者就首先分析了一下FM存在的问题,没法考虑高阶特征交互的问题, 这个在模拟复杂内在结构和规律性的真实数据时,FM的能力会受到限制,所以DeepFM的作者才想到了用FM和DNN网络进行一种并联的方式, 两者接收同样的输入, 但是各自学习不同的特征(一个负责低阶交互,一个负责高阶交互), 最后再把学习到的结果合并得到最终的输出,并通过实验也证明了这种策略的有效性。

虽然FM已经是公认的是稀疏数据中进行预测最有效的方法之一,但是真实的数据往往是非线性的。FM虽然能够比较好的处理稀疏数据, 也能学习稀疏数据间的二阶交互, 但说白了,这个还是个线性模型, 且交互仅仅限于二阶交互。

所以,作者这里的想法是利用神经网络的非线性和强表达能力来改进一下FM模型,得到一个增强版的FM模型,所以还是 老套路就是将这两个模型进行组合一下。毕竟FM和DNN在稀疏数据的优势非常明显且正好又互补, 且W&D开辟了组合模型的先河,关键是如何组合的问题? FNN给出了一种思路, DeepFM给出了一种思路, NFM这里同样是有着组合的味道,但是NFM设计了一种结构,把FM和DNN竖向拼接了起来。这样一来同样是利用了FM和DNN的优势。

先来看一下NFM的公式:

 其实在这里并不是将FM和DNN进行最后结果的结合,二十将这两个模型进行组合,使用DNN代替高阶的特征交互。

 上面这个公式就有点FM的味道了,只不过在这里f(x)只是代表了二阶的特征交互,而使用FM进行更加高阶的特征交互就会使得计算复杂度变大,所以在这里使用DNN来代替高阶的交互。先看一下网络的模型。

是不是发现和前面讲过的FNN很相似,不过这里和FNN还有很大的区别的,首先在模型上的区别就是FNN将FM的embedding向量进行了预训练,然后将这些embedding向量进行结合,输入DNN中进行无差别交叉。但是在这里就是类似于和PNN一样,只不过这里讲PNN的Product_layer换成了Bi-interaction  Pooling层。

二、Bi-Interaction Pooling layer

首先是embedding层,这一层其实就是和其余的模型一样的embedding。不过这里需要注意的是embdding是对不同的特征域来进行编码的,不同的特征有着相同的特征域。 这个地方真正实现的时候,往往先LabelEncoder一下(而不是one-hot encoder), 这样就直接能够得到那些取值非0的特征对应的embedding向量了,毕竟LabelEncoder一下就相当于为某个特征的所有特征域建立了一个字典, 我们知道在取某个值的embedding向量的时候,直接去字典的索引值就好了。

然后就是本文最大的创新点:Bi-interaction  Pooling。其实就是二阶特征交叉池化层。首先来看一下公式:

 ⊙表示两个向量的元素积操作,即两个向量对应维度相乘得到的元素积向量,其中第k 维的操作:

 其实上面那个公式就是将两个特征域的embedding向量进行了交叉,这里和FM二阶交叉不同的是,在FM中两个特征的隐向量交叉完毕之后是一个具体的数值,但是在这里就是一个k维的向量,当把所有的特征域两两交叉完毕之后就进行相加(池化),最后将k维的向量输入到DNN中去。

其实如果不加那个DNN, 这个NFM就退化成了FM, 所以改进的关键就在于加了一个这样的层,组合了一下二阶交叉的信息,然后又给了DNN进行高阶交叉的学习。其实这种形式的组合在FNN那里也出现过(底层是FM, 上层是DNN),只不过那里并不是把FM融进了DNN, 而是先各自训练各自的,然后再用训练好的向量初始化整体,然后进行的微调,那里看似是FM和DNN的组合,但其实是一个离合体, 而NFM也是采用了这样的一种组合思路,但是人家设计了特征池化,使得FM真正的和DNN成了一个整体,且可以完整的正向和反向传播式训练,这样才真正的利用好了FM的二阶交叉线性和DNN的高阶交叉非线性的优势。作者指出,Bi-Interaction层不需要额外的模型学习参数,更重要的是它在一个线性的时间内完成计算。参考FM可以将计算公式转化为:

 另外就是作者在DNN中还应用了dropout和BN。一个是为了过拟合,一个是为了避免embedding向量的更新将输入层的分布更改为隐藏层或输出层。

三、其余细节

最后再来总结一下NFM:

NFM相比较于其他模型的核心创新点是特征交叉池化层,有了它,实现了FM和DNN的无缝连接,NN可以在low level就学习到包含更多信息的组合特征。集合了FM二阶交叉线性和DNN高阶交叉非线性的优势,非常适合处理稀疏数据的场景任务。

在特征交叉层和隐藏层加入dropout技术,有利于缓解过拟合,dropout也是线性隐向量模型过拟合的策略。

在NFM中,使用BN+Dropout的组合会使得学习的稳定性下降, 具体使用的时候要注意。

特征交叉池化层能够较好的对二阶特征信息的交互进行学习编码,这时候,就会减少DNN的很多负担,只需要很少的隐藏层就可以学习到高阶特征信息, 也就是NFM相比之前的DNN, 模型结构更浅,更简单,但是性能更好,训练和调参更容易。

NFM对参数初始化相对不敏感,也就是不会过度依赖于预训练,模型的鲁棒性较强。

深度学习模型的层数不总是越深越好, 太深了会产生过拟合的问题,且优化起来也会困难。

demo复现

class NFM(nn.Module):
    def __init__(self, feature_columns, hidden_units, dnn_dropout=0.):
        """
        NFM:
        :param feature_columns: 特征信息, 这个传入的是fea_cols
        :param hidden_units: 隐藏单元个数, 一个列表的形式, 列表的长度代表层数, 每个元素代表每一层神经元个数
        """
        super(NFM, self).__init__()
        self.dense_feature_cols, self.sparse_feature_cols = feature_columns
        
        # embedding
        self.embed_layers = nn.ModuleDict({
            'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim'])
            for i, feat in enumerate(self.sparse_feature_cols)
        })
        
        # 这里要注意Pytorch的linear和tf的dense的不同之处, 前者的linear需要输入特征和输出特征维度, 而传入的hidden_units的第一个是第一层隐藏的神经单元个数,这里需要加个输入维度
        self.fea_num = len(self.dense_feature_cols) + self.sparse_feature_cols[0]['embed_dim']
        hidden_units.insert(0, self.fea_num)
        
        self.bn = nn.BatchNorm1d(self.fea_num)     
        self.dnn_network = Dnn(hidden_units, dnn_dropout)
        self.nn_final_linear = nn.Linear(hidden_units[-1], 1)
    
    def forward(self, x):
        dense_inputs, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):]
        sparse_inputs = sparse_inputs.long()       # 转成long类型才能作为nn.embedding的输入
        sparse_embeds = [self.embed_layers['embed_'+str(i)](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
        sparse_embeds = torch.stack(sparse_embeds)     # embedding堆起来, (field_dim, None, embed_dim)
        sparse_embeds = sparse_embeds.permute((1, 0, 2))
        # 这里得到embedding向量之后 sparse_embeds(None, field_num, embed_dim), 进行特征交叉层,按照那个公式
        embed_cross = 1/2 * (
            torch.pow(torch.sum(sparse_embeds, dim=1),2) - torch.sum(torch.pow(sparse_embeds, 2), dim=1)
        )  # (None, embed_dim)
        
        # 把离散特征和连续特征进行拼接作为FM和DNN的输入
        x = torch.cat([embed_cross, dense_inputs], dim=-1)
        # BatchNormalization
        x = self.bn(x)
        # deep
        dnn_outputs = self.nn_final_linear(self.dnn_network(x))
        outputs = F.sigmoid(dnn_outputs)
        
        return outputs

参考:翻滚的小强

猜你喜欢

转载自blog.csdn.net/qq_38375203/article/details/125428079