首页 > 技术文章 > 【推荐算法】因子分解机(Factorization Machines,FM)

tmpUser 2021-06-30 14:13 原文

因子分解机(Factorization Machines,FM)主要解决了LR的以下几个痛点:

  1. 实现自动特征交叉。LR只能只能手工设计特征之间的交叉,依赖大量人力与业务知识,并且无法挖掘业务构建特征的盲点;
  2. 在稀疏特征上的效果更好。对LR进行暴力二阶特征交叉也能实现特征自动交叉的效果(如POLY_v2),但是这样的模型只能更新这个样本对应的特征pair。例如,某一个样本拥有特征A与B,该样本只能更新A-B这个pair的权重,这个样本对特征A-C是没有任何作用的。而FM对每一个特征建立了一个隐向量(可以看做embedding),交叉特征的权重等于两个隐向量的内积。这样,样本对A-B可以同时更新特征A与特征B的隐向量,缓解了特征稀疏的问题。

算法

FM使用矩阵分解的方法,为每个特征学习了一个隐权重向量。特征交叉时,将两个特征对应的隐向量相乘,得到该交叉特征的权重。与LR相比,多了个二阶项。

\[\text{FM}(\mathbf{w, x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{j}\right\rangle x_{i} x_{j} \]

二次项可以化简,训练和推理的时间复杂度可以从\(O(kn^2)\)降到\(O(kn)\)\(n\)为向量维度:

\[\begin{aligned} & \sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{j}\right\rangle x_{i} x_{j} \\ =& \frac{1}{2} \sum_{i=1}^{n} \sum_{j=1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{j}\right\rangle x_{i} x_{j}-\frac{1}{2} \sum_{i=1}^{n}\left\langle\mathbf{v}_{i}, \mathbf{v}_{i}\right\rangle x_{i} x_{i} \\ =& \frac{1}{2}\left(\sum_{i=1}^{n} \sum_{j=1}^{n} \sum_{f=1}^{k} v_{i, f} v_{j, f} x_{i} x_{j}-\sum_{i=1}^{n} \sum_{f=1}^{k} v_{i, f} v_{i, f} x_{i} x_{i}\right) \\ =& \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{i=1}^{n} v_{i, f} x_{i}\right)\left(\sum_{j=1}^{n} v_{j, f} x_{j}\right)-\sum_{i=1}^{n} v_{i, f}^{2} x_{i}^{2}\right) \\ =& \frac{1}{2} \sum_{f=1}^{k}\left(\left(\sum_{i=1}^{n} v_{i, f} x_{i}\right)^{2}-\sum_{i=1}^{n} v_{i, f}^{2} x_{i}^{2}\right) \end{aligned} \]

模型输入

FM的一阶部分可以直接复用LR,只需要额外实现特征的二阶交叉。首先,我们需要表示每个特征的embedding向量:

class FeaturesEmbedding(torch.nn.Module):
    def __init__(self, field_dims, embed_dim):
        super().__init__()
        self.embedding = torch.nn.Embedding(sum(field_dims), embed_dim)
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
        torch.nn.init.xavier_uniform_(self.embedding.weight.data)

    def forward(self, x):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        x = x + x.new_tensor(self.offsets).unsqueeze(0)
        return self.embedding(x)

根据上一节化简后的公式,我们可以通过下面的代码计算二阶交叉特征:

class FactorizationMachine(torch.nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        """
        :param x: Float tensor of size ``(batch_size, num_fields, embed_dim)``
        """
        square_of_sum = torch.sum(x, dim=1) ** 2
        sum_of_square = torch.sum(x ** 2, dim=1)
        ix = square_of_sum - sum_of_square
        ix = torch.sum(ix, dim=1, keepdim=True)
        return 0.5 * ix

最后,构建完整的FM前向传播链路:

class FactorizationMachineModel(torch.nn.Module):
    def __init__(self, field_dims, embed_dim=10):
        super().__init__()
        self.embedding = FeaturesEmbedding(field_dims, embed_dim)
        self.linear = FeaturesLinear(field_dims)
        self.fm = FactorizationMachine(reduce_sum=True)

    def forward(self, x):
        x = self.linear(x) + self.fm(self.embedding(x))
        return torch.sigmoid(x.squeeze(1))



模型效果

设置:
数据集:ml-100k
优化方法:Adam
学习率:0.003

效果:
收敛epoch:10
train logloss: 0.51644
val auc: 0.77954
test auc: 0.78550

推荐阅读