DeepFM: Deep+FM的pytorch实现

一、前言

高阶特征和低阶特征的学习都非常的重要。 推荐模型很多,基本上是从最简单的线性模型(LR), 到考虑低阶特征交叉的FM, 到考虑高度交叉的神经网络,再到两者都考虑的W&D组合模型。 这样一串联就会发现前面这些模型存在的问题了:

1、简单的线性模型虽然简单,同样这样是它的不足,就是限制了模型的表达能力,随着数据的大且复杂,这种模型并不能充分挖掘数据中的隐含信息,且忽略了特征间的交互,如果想交互,需要复杂的特征工程。
2、FM模型考虑了特征的二阶交叉,但是这种交叉仅停留在了二阶层次,虽然说能够进行高阶,但是计算量和复杂性一下子随着阶数的增加一下子就上来了。所以二阶是最常见的情况,会忽略高阶特征交叉的信息
3、DNN,适合天然的高阶交叉信息的学习,但是低阶的交叉会忽略掉
那么如果把上面这几种结构组合一下子,是不是效果会强大一些呢? 所以W&D模型在这个思路上进行了一个伟大的尝试,把简单的LR模型和DNN模型进行了组合, 使得模型既能够学习高阶组合特征,又能够学习低阶的特征模式,但是W&D的wide部分是用了LR模型, 这一块依然是需要一些经验性的特征工程的,且Wide部分和Deep部分需要两种不同的输入模式, 这个在具体实际应用中需要很强的业务经验。

所以DeepFM看这个名字也能够看出来,就是Deep+FM的组合思路

二、DeepFM模型

DeepFM的模型吧, 长下面这个样子:

DeepFM的运算过程也比较简单, 左边的FM和右边的DNN共享相同的Embedding层的输入, 左侧的FM对不同特征域的Embedding进行了两两交叉(这里的Embedding向量当成了原FM的特征隐向量), 右边的DNN对特征Embedding进行了深度交叉, 最后将FM的输出与Deep部分的输出一起送入最后的输出层,参与最后的目标拟合

1、输入和Embedding部分

关于输入,包括离散的分类特征域(如性别、地区等)和连续的数值特征域(如年龄等)。分类特征域一般通过one-hot或者multi-hot(如用户的浏览历史)进行处理后作为输入特征;数值特征域可以直接作为输入特征,也可以进行离散化进行one-hot编码后作为输入特征。

对于每一个特征域,需要单独的进行Embedding操作,因为每个特征域几乎没有任何的关联,如性别和地区。而数值特征无需进行Embedding。


2、FM部分

 FM负责特征之间的低阶交互过程,FM的输出是Addition单元和Inner Product units的加和, Addition单元反映1阶特征各自的影响, 而Inner product代表2阶特征交互的影响。

 3、Deep部分

依然是一个DNN,负责学习高阶特征之间的交互
 

 原始的特征通过Embedding层之后,会从高维稀疏性转成低维稠密性的特征, 这时候concat,就可以直接作为DNN的输入

如果隐向量的维度是k kk维的话,这里会是一个n × m × k的三维矩阵,作为神经网络的输入。神经网络的前向传播公式:

 这里的∣ H ∣表了神经网络的层数。

4、其他项

特征交叉或者交互信息,这个玩意到底是个啥呢? 

在CTR预测中, 学习用户点击行为背后的特征隐式交互非常重要。

二阶特征交互原来是这个意思:
通过对主流应用市场的研究,我们发现人们经常在用餐时间下载送餐的应用程序,这就表明应用类别和时间戳之间的(阶数-2)交互作用是CTR预测的一个信号。
三阶或者高阶特征交互是这个意思:
我们还发现男性青少年喜欢射击游戏和RPG游戏,这意味着应用类别、用户性别和年龄的(阶数-3)交互是CTR的另一个信号。
根据谷歌的W&D模型的应用, 作者发现同时考虑低阶和高阶的交互特征,比单独考虑其中之一有更多的改进

三、torch代码实现

1、FM部分

 二阶代码中用最后的形式 

class FM(nn.Module):
    """FM part"""

    def __init__(self, latent_dim, fea_num):
        """
        latent_dim: 各个离散特征隐向量的维度  8
        input_shape: 这个最后离散特征embedding之后的拼接和dense拼接的总特征个数   221
        """
        super(FM, self).__init__()

        self.latent_dim = latent_dim
        # 定义三个矩阵, 一个是全局偏置,一个是一阶权重矩阵, 一个是二阶交叉矩阵,注意这里的参数由于是可学习参数,需要用nn.Parameter进行定义
        self.w0 = nn.Parameter(torch.zeros([1, ]))   #1,
        self.w1 = nn.Parameter(torch.rand([fea_num, 1]))   #221*1
        self.w2 = nn.Parameter(torch.rand([fea_num, latent_dim]))   #221*8

    def forward(self, inputs):  #32*221
        # 一阶交叉
        first_order = self.w0 + torch.mm(inputs, self.w1)  # (samples_num, 1)   w1=221*1    fin 32*1
        # 二阶交叉  这个用FM的最终化简公式
        second_order = 1 / 2 * torch.sum(
            torch.pow(torch.mm(inputs, self.w2), 2) - torch.mm(torch.pow(inputs, 2), torch.pow(self.w2, 2)),
            dim=1,
            keepdim=True
        )  # (samples_num, 1)  32*8     行求和后 32*1

        return first_order + second_order

2、DNN部分

class Dnn(nn.Module):
    """Dnn part"""

    def __init__(self, hidden_units, dropout=0.):
        """
        hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度
        dropout = 0.
        """
        super(Dnn, self).__init__()

        self.dnn_network = nn.ModuleList(
            [nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))])
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        for linear in self.dnn_network:
            x = linear(x)
            x = F.relu(x)
        x = self.dropout(x)
        return x

 3、DeepFM

class DeepFM(nn.Module):
    def __init__(self, feature_columns, hidden_units, dnn_dropout=0.):
        """
        DeepFM:
        :param feature_columns: 特征信息, 这个传入的是fea_cols
        :param hidden_units: 隐藏单元个数, 一个列表的形式, 列表的长度代表层数, 每个元素代表每一层神经元个数
        """
        super(DeepFM, 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) + len(self.sparse_feature_cols) * self.sparse_feature_cols[0][
            'embed_dim']  #13+26*8   221
        hidden_units.insert(0, self.fea_num)  #[221, 128, 64, 32]

        self.fm = FM(self.sparse_feature_cols[0]['embed_dim'], self.fea_num)
        self.dnn_network = Dnn(hidden_units, dnn_dropout)
        self.nn_final_linear = nn.Linear(hidden_units[-1], 1)   #32*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.cat(sparse_embeds, dim=-1)  #32*208

        # 把离散特征和连续特征进行拼接作为FM和DNN的输入
        x = torch.cat([sparse_embeds, dense_inputs], dim=-1)#32*211
        # Wide
        wide_outputs = self.fm(x)  #32*1
        # deep
        deep_outputs = self.nn_final_linear(self.dnn_network(x))  #32*1

        # 模型的最后输出
        outputs = F.sigmoid(torch.add(wide_outputs, deep_outputs))  #32*1

        return outputs

完整的架构图

4、模型训练

5、 模型的保存和使用

我们训练好了模型,

怎么存?

存后又怎么用呢?

下面简单的整理下,这里以训练完的DeepFM模型为例

(1)模型的保存

torch.save(model, '../model/DeepFM.pkl')

 可以看到保存的模型在我们创建的文件里

(2)模型的使用

net_clone = torch.load('../model/DeepFM.pkl')

 是的没错,又是一行,下面时把提取出来的

y_pred_probs = model(torch.tensor(test_x).float())
y_pred = torch.where(y_pred_probs>0.5, torch.ones_like(y_pred_probs), torch.zeros_like(y_pred_probs))
print(y_pred)

 

 总结

根据上面的分析,可以发现DeepFM还是一个非常重要的模型的,既不需要预训练,又同时考虑了高阶交叉和低阶交互, 且省去了复杂的特征工程实现了一个end-to-end的效果。

那么它真的就完美了吗?

肯定不是呀,要不然就不会再有后面的一些改进了,DeepFM也只是给出了提升FM模型的一种思路而已, 其实模型对于高阶特征和高阶特征的交互学习探索, 还有很多的思路, 下面的NFM就提供了另外的一种组合思路。 它重点是考虑了FM本身的一种限制,然后对这种限制进行了突破, 通过加入特征池化层, 把FM融入到了神经网络当中使其成为了一个整体结构, 使得FM与DNN进行了一种无缝的衔接, 又进行了串联起来。
 

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值