21.4. AutoRec:使用自动编码器进行评分预测
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

虽然矩阵分解模型在评分预测任务上取得了不错的性能,但它本质上是一个线性模型。因此,这类模型无法捕捉可能预测用户偏好的复杂非线性和错综复杂的关系。在本节中,我们介绍一个非线性的神经网络协同过滤模型——AutoRec Sedhain et al., 2015。它将协同过滤(CF)与自动编码器架构相结合,旨在将非线性变换融入到基于显式反馈的协同过滤中。神经网络已被证明能够逼近任何连续函数,这使其非常适合解决矩阵分解的局限性并丰富矩阵分解的表达能力。

一方面,AutoRec的结构与自动编码器相同,由输入层、隐藏层和重构(输出)层组成。自动编码器是一种神经网络,它学习将其输入复制到其输出,以便将输入编码到隐藏(通常是低维)的表示中。在AutoRec中,它不是将用户/物品显式地嵌入到低维空间,而是使用交互矩阵的列/行作为输入,然后在输出层重构交互矩阵。

另一方面,AutoRec与传统的自动编码器不同:AutoRec关注的不是学习隐藏表示,而是学习/重构输出层。它使用部分观察到的交互矩阵作为输入,旨在重构一个完整的评分矩阵。同时,输入的缺失条目通过重构在输出层中被填充,以用于推荐。

AutoRec 有两种变体:基于用户的和基于物品的。为简洁起见,这里我们只介绍基于物品的 AutoRec。基于用户的 AutoRec 可以相应地推导出来。

21.4.1. 模型

\(\mathbf{R}_{*i}\) 表示评分矩阵的第 \(i^\textrm{th}\) 列,其中未知评分默认设置为零。神经网络架构定义为

(21.4.1)\[h(\mathbf{R}_{*i}) = f(\mathbf{W} \cdot g(\mathbf{V} \mathbf{R}_{*i} + \mu) + b)\]

其中 \(f(\cdot)\)\(g(\cdot)\) 表示激活函数,\(\mathbf{W}\)\(\mathbf{V}\) 是权重矩阵,\(\mu\)\(b\) 是偏置。令 \(h( \cdot )\) 表示 AutoRec 的整个网络。输出 \(h(\mathbf{R}_{*i})\) 是评分矩阵第 \(i^\textrm{th}\) 列的重构。

以下目标函数旨在最小化重构误差

(21.4.2)\[\underset{\mathbf{W},\mathbf{V},\mu, b}{\mathrm{argmin}} \sum_{i=1}^M{\parallel \mathbf{R}_{*i} - h(\mathbf{R}_{*i})\parallel_{\mathcal{O}}^2} +\lambda(\| \mathbf{W} \|_F^2 + \| \mathbf{V}\|_F^2)\]

其中 \(\| \cdot \|_{\mathcal{O}}\) 表示只考虑已观察到的评分的贡献,也就是说,在反向传播过程中,只有与已观察到的输入相关联的权重会被更新。

import mxnet as mx
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l

npx.set_np()

21.4.2. 实现模型

一个典型的自动编码器由编码器和解码器组成。编码器将输入投影到隐藏表示,解码器将隐藏层映射到重构层。我们遵循这种做法,并使用全连接层创建编码器和解码器。编码器的激活函数默认设置为 sigmoid,而解码器不应用任何激活函数。在编码转换之后加入 Dropout 以减少过拟合。未观察到的输入的梯度被掩蔽掉,以确保只有观察到的评分对模型学习过程有贡献。

class AutoRec(nn.Block):
    def __init__(self, num_hidden, num_users, dropout=0.05):
        super(AutoRec, self).__init__()
        self.encoder = nn.Dense(num_hidden, activation='sigmoid',
                                use_bias=True)
        self.decoder = nn.Dense(num_users, use_bias=True)
        self.dropout = nn.Dropout(dropout)

    def forward(self, input):
        hidden = self.dropout(self.encoder(input))
        pred = self.decoder(hidden)
        if autograd.is_training():  # Mask the gradient during training
            return pred * np.sign(input)
        else:
            return pred

21.4.3. 重新实现评估器

由于输入和输出已经改变,我们需要重新实现评估函数,但我们仍然使用 RMSE 作为准确度度量。

def evaluator(network, inter_matrix, test_data, devices):
    scores = []
    for values in inter_matrix:
        feat = gluon.utils.split_and_load(values, devices, even_split=False)
        scores.extend([network(i).asnumpy() for i in feat])
    recons = np.array([item for sublist in scores for item in sublist])
    # Calculate the test RMSE
    rmse = np.sqrt(np.sum(np.square(test_data - np.sign(test_data) * recons))
                   / np.sum(np.sign(test_data)))
    return float(rmse)

21.4.4. 训练和评估模型

现在,让我们在 MovieLens 数据集上训练和评估 AutoRec。我们可以清楚地看到,测试 RMSE 低于矩阵分解模型,这证实了神经网络在评分预测任务中的有效性。

devices = d2l.try_all_gpus()
# Load the MovieLens 100K dataset
df, num_users, num_items = d2l.read_data_ml100k()
train_data, test_data = d2l.split_data_ml100k(df, num_users, num_items)
_, _, _, train_inter_mat = d2l.load_data_ml100k(train_data, num_users,
                                                num_items)
_, _, _, test_inter_mat = d2l.load_data_ml100k(test_data, num_users,
                                               num_items)
train_iter = gluon.data.DataLoader(train_inter_mat, shuffle=True,
                                   last_batch="rollover", batch_size=256,
                                   num_workers=d2l.get_dataloader_workers())
test_iter = gluon.data.DataLoader(np.array(train_inter_mat), shuffle=False,
                                  last_batch="keep", batch_size=1024,
                                  num_workers=d2l.get_dataloader_workers())
# Model initialization, training, and evaluation
net = AutoRec(500, num_users)
net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))
lr, num_epochs, wd, optimizer = 0.002, 25, 1e-5, 'adam'
loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), optimizer,
                        {"learning_rate": lr, 'wd': wd})
d2l.train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,
                        devices, evaluator, inter_mat=test_inter_mat)
train loss 0.000, test RMSE 0.900
10593393.7 examples/sec on [gpu(0), gpu(1)]
../_images/output_autorec_4e5735_7_1.svg

21.4.5. 小结

  • 我们可以用自动编码器来构建矩阵分解算法,同时集成非线性层和 dropout 正则化。

  • 在 MovieLens 100K 数据集上的实验表明,AutoRec 的性能优于矩阵分解。

21.4.6. 练习

  • 改变 AutoRec 的隐藏维度,观察其对模型性能的影响。

  • 尝试添加更多隐藏层。这有助于提高模型性能吗?

  • 你能找到更好的解码器和编码器激活函数组合吗?

讨论