【推荐算法】因子分解机(Factorization Machines,FM)
因子分解机(Factorization Machines,FM)主要解决了LR的以下几个痛点:
- 实现自动特征交叉。LR只能只能手工设计特征之间的交叉,依赖大量人力与业务知识,并且无法挖掘业务构建特征的盲点;
- 在稀疏特征上的效果更好。对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