目录

  • 背景
  • DeepFM
    • FM
    • Deep
  • Pytorch实现
    • 模型实现
    • DeepFM代码
  • 其他
    • 思考题
    • 小知识点
  • 参考资料

背景

 承接Wide&Deep。在Wide&Deep的博客中,我们已经讨论了它的缺点,在于需要花费大量的人力去构造新的特征,所以为了解决这个问题,提出了DeepFM模型。

DeepFM


DeepFM结构图

 DeepFM与W&D相比,DeepFM将Wide中简单的linear变换变为FM层。对于W&D中的linear层而言,我们需要人工去构建特征,对于高纬度的稀疏特征而言,可能需要花费的人力资源就很多,而FM可以自动进行二阶特征的交叉

FM

 FM全称是Factorization Machine,在这里可以用于一阶特征的自动交叉。

 对于ctr点击率预测而言,我们可以写出如下式子:

y(x)=b+\sum_{i=1}^{N}w_ix_i+\sum_{i=1}^{N}\sum_{j=i+1}^{N}w_{ij}x_ix_j

 其中,我们将w_{ij}改写为内积的形式<v_i,v_j>。因为对于足够大的N,都可以有一个V \in R^{N × l}使得VV^T=W,其中w_{ij}是W第i行第j列的元素。

 此时公式变为:

y(x)=b+\sum_{i=1}^{N}w_ix_i+\sum_{i=1}^{N}\sum_{j=i+1}^{N}<v_i,v_j>x_ix_j

 那么,将w_{ij}改写为内积的形式之后,参数复杂度由O(n^2)变为O(kn)。

 同时,对于两个求和结果的时间复杂度,可以证明复杂度可以从O(n^2)变为O(kn)。证明如下:

\begin{aligned}
\sum_{i=1}^{N}\sum_{j=i+1}^{N}<v_i,v_j>x_ix_j &= \sum_{i=1}^{N}\sum_{j=i+1}^{N}\sum_{k=1}^{K}v_{ik}v_{jk}x_ix_j\\ &= \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N}\sum_{k=1}^{K}v_{ik}v_{jk}x_ix_j – \frac{1}{2}\sum_{i=1}^{N}\sum_{k=1}^{K}v_{ik}v_{ik}x_ix_i\\&= \frac{1}{2}\sum_{k=1}^{K}(\sum_{i=1}^{N}\sum_{j=1}^{N}v_{ik}v_{jk}x_ix_j – \sum_{i=1}^Nv_{ik}v_{ik}x_ix_i)\\&= \frac{1}{2}\sum_{k=1}^{K}[\sum_{i=1}^Nv_{ik}x_i\sum_{j=1}^Nv_{jk}x_j – \sum_{i=1}^N(v_{ik}x_i)^2]\\&= \frac{1}{2}\sum_{k=1}^{K}[(\sum_{i=1}^Nv_{ik}x_i)^2 – \sum_{i=1}^N(v_{ik}x_i)^2]
\end{aligned}

 以上则为FM的推导(纯靠理解一点点打的公式,反馈感很足),此时的公式形式变为:

y(x)=b+\sum_{i=1}^{N}w_ix_i+\frac{1}{2}\sum_{k=1}^{K}[(\sum_{i=1}^Nv_{ik}x_i)^2 – \sum_{i=1}^N(v_{ik}x_i)^2]

 其中b为偏置常数,w_i为特征i的系数,x_i为特征i的值,v_{ik}为特征i对应的V的第i行(W=VV^T)。

 给出FM的架构图:

 对FM进行部分架构解读:

 输入数据为稀疏的类别特征,利用embedding技术将其转化为稠密向量(降维)。其中黄色的节点表示某一个类别特征中出现的值,转化为稠密向量之后进入FM layer进行计算,最后得到一个logits。

Deep

 Deep部分与W&D中的DNN架构相同,直接参考W&D的博客即可,下面给出Deep部分的架构图:

 对Deep部分进行部分架构解读:

将类别特征输入之后,利用embedding技术获取其每个特征的值的embedding表示,然后将所有的embedding进行concat操作,再经过DNN网络得到输出的logits。

Pytorch实现

 尝试使用pytorch完成模型复现,代码思路参考tensorflow版本的学习代码。我的仓库地址https://github.com/EternalStarICe/recommendation-system-model
 对于DeepFM,其主要由两个部分组成,FM和Deep部分。其中FM计算时可以分为一阶特征和交叉特征,所以我们可以将计算过程分为三个部分:

  • linear (一阶特征)
  • FM(二阶特征)
  • Deep (DNN获取高阶特征)

模型实现

  1. 对于linear层,将dense特征通过一层线性变换得到linear_num_logits,再将其余的稀疏特征通过emb_len=1的embedding进行表示,此处emb_len=1的Embedding相当于一个全连接层。将稀疏特征的embedding相加得到linear_sparse_logits。将两个logit相加则得到linear的输出。
  2. 对于FM,将稀疏特征通过embedding表示之后,写出计算的数学形式,如函数FM,即计算出交叉特征的结果。
  3. 对于Deep,仍是构建DNN深层网络,最后输出输出两个维度的值分别表示两个类别的得分,因为选用nn.CrossEntropyLoss计算损失。

DeepFM网络

import torch
import torch.nn as nn

def FM(x): # x:(batch, feature_num, emb_len)
    left = torch.square(torch.sum(x, dim=1, keepdim=True)) # (batch, 1, emb_len)
    right = torch.sum(torch.square(x), dim=1, keepdim=True) # (batch, 1, emb_len)
    res = 0.5 * torch.sum((left - right), dim=2) # (batch, 1)
    return res




class DeepFM(nn.Module):
    def __init__(self, num_cols, cat_cols, cat_tuple_list, emb_len=4):
        super(DeepFM, self).__init__()
        self.cat_col_len = len(cat_cols)
        self.num_col_len = len(num_cols)
        self.cat_tuple_list = cat_tuple_list
        self.emb_len = emb_len
        self.num_cols = num_cols
        self.cat_cols = cat_cols
        self.deep_input_dim = self.num_col_len + self.emb_len * self.cat_col_len
        # three part: linear, fm, dnn

        # linear
        self.fm_linear_embeddings = nn.ModuleList()
        for fc in self.cat_tuple_list:
            self.fm_linear_embeddings.append(nn.Embedding(fc.vocab_size, 1))

        self.linear_dense = nn.Linear(self.num_col_len, 1)

        # FM
        self.fm_embeddings = nn.ModuleList()
        for fc in self.cat_tuple_list:
            self.fm_embeddings.append(nn.Embedding(fc.vocab_size, emb_len))

        # DNN
        self.deep_linear1 = nn.Linear(self.deep_input_dim, 1024)
        self.deep_linear2 = nn.Linear(1024, 512)
        self.deep_linear3 = nn.Linear(512, 256)
        self.deep_final_linear = nn.Linear(256, 1)
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()
        self.dropout1 = nn.Dropout(0.5)
        self.dropout2 = nn.Dropout(0.3)
        self.dropout3 = nn.Dropout(0.1)

        # total dense
        self.final_linear = nn.Linear(3, 2)

    def forward(self, x):
        # 分离出num和cat数据
        cat_x = x[:, :self.cat_col_len]
        num_x = x[:, self.cat_col_len:]

        # linear
        linear_num_output = self.linear_dense(num_x)
        linear_output_list = []
        for i in range(self.cat_col_len):
            linear_output_list.append(self.fm_linear_embeddings[i](cat_x[:, i].long()))
        linear_cat_output = linear_output_list[0]
        for i in range(1, self.cat_col_len):
            linear_cat_output += linear_output_list[i]
        linear_output = linear_cat_output + linear_num_output

        # FM
        fm_input = self.fm_embeddings[0](cat_x[:, 0].long()).unsqueeze(dim=1)
        for i in range(1, self.cat_col_len):
            fm_input = torch.cat([fm_input, self.fm_embeddings[i](cat_x[:, i].long()).unsqueeze(dim=1)], dim=1)
        fm_output = FM(fm_input)

        # dnn

        for i in range(self.cat_col_len):
            num_x = torch.cat([num_x, self.fm_embeddings[i](cat_x[:, i].long())], dim=1)
        # print(num_x.shape)
        deep_output = self.dropout1(self.relu(self.deep_linear1(num_x)))
        deep_output = self.dropout2(self.relu(self.deep_linear2(deep_output)))
        deep_output = self.dropout3(self.relu(self.deep_linear3(deep_output)))
        deep_output = self.deep_final_linear(deep_output)

        # 总和
        return self.sigmoid(self.final_linear(torch.cat([linear_output, fm_output, deep_output], dim=1))) # 修正

其他

思考题

  1. 如果对于FM采用随机梯度下降SGD训练模型参数,请写出模型各个参数的梯度和FM参数训练的复杂度
  2. 对于下图所示,根据你的理解Sparse Feature中的不同颜色节点分别表示什么意思
    黄色节点表示该sparse feature中出现的值,如食物这个特征中,若某个样本出现的是“鸡肉”,则这个黄色的点则是代表“鸡肉”,其他点则代表未在该样本中出现的值。

小知识点

 分桶离散化:很多原始的dense特征通常也会被分桶离散化构造为sparse特征

参考资料

  1. 【推荐系统】Factorization Machine
  2. 推荐系统遇上深度学习(三)–DeepFM模型理论和实践
  3. datawhale深度推荐系统
  4. 深度推荐模型之DeepFM

0 条评论

发表评论