稀疏数据如何建模-场感知因子分解机

2023-08-28 17:43:29 浏览数 (2)

一、前言

在许多机器学习算法中,都假设各个特征之间无关,比如逻辑回归和SVM各个特征对应一个特定的权重。基于这一假设,模型可以非常简单,而且参数量也不会过多。但是实际场景中,特征之间关联是非常大的,尤其是经过one-hot编码后的类别特征。

为了建立特征之间的关联,可以组合各个特征并赋予一个权重。比如数据有x1、x2、...、xn这n个特征,在逻辑回归中会对应w1、w2、...、wn这n个权重。为了建立特征之间的关联,加入组合特征x1x1、x1x2、...、x2x1、x2x2、...,并给组合特征赋予一个权重w11、w12...

此时权重从n个变成了n n^2个,虽然达到了建立特征关联的效果,但是参数量过多。而因子分解机可以解决这一问题。

二、因子分解机

2.1 因子分解机原理

因子分解机也叫Factorization Machines,简称FM。其思想就是在模型中加入组合特征和组合特征权重,并使用因子分解减少模型的参数。

在组合特征后,可以用W矩阵来表示组合特征的权重,比如x1x2的权重为W12。当W矩阵低秩时,可以将W分解成长度为n的向量即:

这样权重就从n^2变成了n了,原本W12可以用v1v2替代,此时参数量在容许范围内。虽然W矩阵并不一定是低秩的,但是我们可以假设W低秩,基于这一假设,因子分解机的表达式可以写成:

如果要完成分类任务,那么需要对结果直线softmax或sigmoid函数。相比简单的逻辑回归,因子分解机考虑了特征之间的关系,并对这种关系进行了简化。比如vivj表示特征xi和xj联合起来对结果的影响权重。

2.2 代码实现

在因子分解机中,存在有两组权重和一个偏置,分别为W、V、b,其中W和V都是长度为n_features的向量。我们把公式分为两个部分,第一部分就是线性回归的计算:

用PyTorch实现如下:

代码语言:javascript复制
a = torch.matmul(X, W)   b

其中X形状为“样本数×1×特征数”,W的形状为“特征数×1”。第二部分是组合特征的计算:

用PyTorch实现如下:

代码语言:javascript复制
xx = torch.matmul(X.transpose(1, 2), X)
vv = torch.matmul(V, V.T)
b = torch.sum(xx * vv, dim=[1, 2])

其中X的形状与前面是一样的,V的形状与W一样。由此可以知道xx的形状为“样本数×特征数×特征数”,vv的形状为“特征数×特征数”。知道了这些,FM就很好实现了,下面是FM的代码:

代码语言:javascript复制
import torch
from torch import nn


class FM:
    def __init__(self, n_features, lr=0.001, max_iter=500):
        # 学习率
        self.lr = lr
        # 最大迭代次数
        self.max_iter = max_iter
        # 特征数量 
        self.n_features = n_features
        # 初始化权重
        self.W = torch.randn(n_features, 1, requires_grad=True)
        self.V = torch.randn(n_features, 1, requires_grad=True)
        self.b = torch.zeros(1, requires_grad=True)

    def fit(self, X, y):
        for iter in range(self.max_iter):
            # 计算结果
            pred = self.predict(X)
            # 计算loss
            loss = self.loss(pred, y)
            # 求梯度
            self.gradient_step(loss)
            if (iter   1) % 100 == 1:
                print(f"iter: {iter   1}/{self.max_iter}, loss: {loss.item()}")

    def predict(self, X):
        n_samples = X.size(0)
        if X.ndim == 2:
            X = X.view(n_samples, 1, -1)
        a = torch.matmul(X, self.W)   self.b
        xx = torch.matmul(X.transpose(1, 2), X)
        vv = torch.matmul(self.V, self.V.T)
        b = torch.sum(xx * vv, dim=[1, 2])
        return a.view(n_samples, 1)   b.view(n_samples, 1)

    def loss(self, pred, y):
        return torch.pow(pred - y, 2).mean()

    def gradient_step(self, loss):
        # 计算梯度
        gW, gV, gb = torch.autograd.grad(loss, [self.W, self.V, self.b])
        # 更新权重
        self.W = torch.subtract(self.W, self.lr * gW)
        self.V = torch.subtract(self.V, self.lr * gV)
        self.b = torch.subtract(self.b, self.lr * gb)


# 创建数据
X = torch.randn(2000, 5)
a = torch.sum(torch.matmul(X ** 2, torch.randn(5, 1)), dim=1)
b = torch.sum(torch.matmul(X, torch.randn(5, 1)), dim=1)
y = a   b   torch.randn(2000)
# 构建模型
fm = FM(5, lr=0.01, max_iter=2000)
# 训练
fm.fit(X, y)

三、场感知因子分解机

因子分解机可以解决逻辑回归中无法学习组合特征的问题,同时也解决了参数量过大的问题。但是在因子分解机中,组合特征的权重矩阵秩为1,这限制了模型的能力。并且在因子分解机中,默认所有特征两两之间都是有关系的,这一假设也不符合常理。为了解决上述两个问题,在因子分解机中引入场(特征组)的概念。

3.1 场感知因子分解机原理

在场感知因子分解机中,n个特征会被分为f个场,每个场中的特征有较强的相关性。比如一个类别变量进行one-hot编码后得到k个特征,这k个特征就可以被划分到一个场。引入场后的模型被称为场感知因子分解机(Field-aware Factorization Machine FFM)。

场感知因子分解机可以看作有f个组合特征的因子分解机,其表达如下:

此时模型参数量为fn,当f为1时,模型简化为因子分解机;当f为n时,和最开始的策略一致。

3.2 代码实现

场感知因子分解机的代码与因子分解机类似,这里只需要修改V的初始化和predict方法即可。代码如下:

代码语言:javascript复制
import torch
from torch import nn


class FFM:
    def __init__(self, n_features, n_field=2, lr=0.001, max_iter=500):
        self.lr = lr
        self.max_iter = max_iter
        # 场的数量
        self.n_field = n_field
        self.n_features = n_features
        self.W = torch.randn(n_features, 1, requires_grad=True)
        # 形状为“场数×特征数×1”
        self.V = torch.randn(n_field, n_features, 1, requires_grad=True)
        self.b = torch.zeros(1, requires_grad=True)
        self.criterion = nn.MSELoss()

    def fit(self, X, y):
        pass

    def predict(self, X):
        n_samples = X.size(0)
        if X.ndim == 2:
            X = X.view(n_samples, 1, -1)
        a = torch.matmul(X, self.W)   self.b
        xx = torch.matmul(X.transpose(1, 2), X).view(n_samples, -1)
        vv = torch.matmul(self.V, self.V.transpose(1, 2)).view(self.n_field, -1).transpose(0, 1)
        b = torch.sum(torch.matmul(xx, vv), dim=[1])
        return a.view(n_samples, 1)   b.view(n_samples, 1)

    def loss(self, pred, y):
        pass

    def gradient_step(self, loss):
        pass

这里使用的是比较简单的实现,即把原本的向量V改成一个f*n的矩阵,其余部分可以复用FM的代码。在上述代码中,场没有刻意去设计。

相比逻辑回归,场因子分解机有很多优点。当面临稀疏数据时,逻辑回归的效果会非常差。在数据挖掘中,类别数据大量存在,结果one-hot编码后,数据会特别稀疏而使用场感知因子分解机可以很好解决该问题。

四、总结

本文讨论了对回归模型的改进,在文章开始提出线性回归的缺陷,即特征之间没有关联。基于这一问题,提出了几种解决方案,分别是加入组合特征、因子分解机、场感知因子分解机。三种方案各有优缺点,因子分解机的模型表达能力会偏弱,而加入全部组合特征模型的复杂度非常高。场感知因子分解机则是在两者之间取的一个平衡,利用场的概念简化让模型即有较强的表达能力,又有相当较少的参数。

最后,关于FM和FFM的实现可以参考libfm模块。

0 人点赞