21.3. 矩阵分解
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

矩阵分解 (Matrix Factorization) (Koren et al., 2009) 是推荐系统文献中一种成熟的算法。矩阵分解模型的最初版本由 Simon Funk 在一篇著名的博客文章中提出,他在文章中描述了分解交互矩阵的想法。后来,由于2006年举办的 Netflix 竞赛,这个想法变得广为人知。当时,流媒体和视频租赁公司 Netflix 宣布举办一场竞赛,以提升其推荐系统的性能。能够将 Netflix 基线(即 Cinematch)的性能提升10%的最佳团队将赢得一百万美元的奖金。因此,这场竞赛吸引了大量对推荐系统研究领域的关注。最终,大奖被 BellKor's Pragmatic Chaos 团队赢得,这是一个由 BellKor、Pragmatic Theory 和 BigChaos 组成的联合团队(你现在不必担心这些算法)。尽管最终的得分是集成解决方案(即多种算法的组合)的结果,但矩阵分解算法在最终的融合中扮演了关键角色。Netflix 大奖赛解决方案的技术报告 (Töscher et al., 2009) 详细介绍了所采用的模型。在本节中,我们将深入探讨矩阵分解模型的细节及其实现。

21.3.1. 矩阵分解模型

矩阵分解是一类协同过滤模型。具体来说,该模型将用户-物品交互矩阵(例如,评分矩阵)分解为两个低秩矩阵的乘积,从而捕捉用户-物品交互的低秩结构。

\(\mathbf{R} \in \mathbb{R}^{m \times n}\) 表示交互矩阵,其中有 \(m\) 个用户和 \(n\) 个物品,\(\mathbf{R}\) 的值表示显式评分。用户-物品交互将被分解为一个用户潜在一个矩阵 \(\mathbf{P} \in \mathbb{R}^{m \times k}\) 和一个物品潜在矩阵 \(\mathbf{Q} \in \mathbb{R}^{n \times k}\),其中 \(k \ll m, n\) 是潜在因子的大小。设 \(\mathbf{p}_u\) 表示 \(\mathbf{P}\) 的第 \(u^\textrm{th}\) 行,\(\mathbf{q}_i\) 表示 \(\mathbf{Q}\) 的第 \(i^\textrm{th}\) 行。对于一个给定的物品 \(i\)\(\mathbf{q}_i\) 的元素衡量该物品拥有这些特征(例如电影的类型和语言)的程度。对于一个给定的用户 \(u\)\(\mathbf{p}_u\) 的元素衡量该用户对物品相应特征的兴趣程度。这些潜在因子可能衡量了如这些例子中提到的明显维度,也可能是完全无法解释的。预测的评分可以通过以下方式估算:

(21.3.1)\[\hat{\mathbf{R}} = \mathbf{PQ}^\top\]

其中 \(\hat{\mathbf{R}}\in \mathbb{R}^{m \times n}\) 是预测评分矩阵,其形状与 \(\mathbf{R}\) 相同。此预测规则的一个主要问题是无法对用户/物品的偏置进行建模。例如,有些用户倾向于给出更高的评分,或者有些物品由于质量较差而总是得到较低的评分。这些偏置在现实世界的应用中很常见。为了捕捉这些偏置,我们引入了用户特定和物品特定的偏置项。具体来说,用户 \(u\) 对物品 \(i\) 的预测评分计算如下:

(21.3.2)\[\hat{\mathbf{R}}_{ui} = \mathbf{p}_u\mathbf{q}^\top_i + b_u + b_i\]

然后,我们通过最小化预测评分与真实评分之间的均方误差来训练矩阵分解模型。目标函数定义如下:

(21.3.3)\[\underset{\mathbf{P}, \mathbf{Q}, b}{\mathrm{argmin}} \sum_{(u, i) \in \mathcal{K}} \| \mathbf{R}_{ui} - \hat{\mathbf{R}}_{ui} \|^2 + \lambda (\| \mathbf{P} \|^2_F + \| \mathbf{Q} \|^2_F + b_u^2 + b_i^2 )\]

其中 \(\lambda\) 表示正则化率。正则化项 \(\lambda (\| \mathbf{P} \|^2_F + \| \mathbf{Q} \|^2_F + b_u^2 + b_i^2 )\) 用于通过惩罚参数的大小来避免过拟合。\(\mathbf{R}_{ui}\) 已知的 \((u, i)\) 对存储在集合 \(\mathcal{K}=\{(u, i) \mid \mathbf{R}_{ui} \textrm{ is known}\}\) 中。模型参数可以使用优化算法(如随机梯度下降和 Adam)进行学习。

矩阵分解模型的一个直观图示如下:

../_images/rec-mf.svg

图 21.3.1 矩阵分解模型图示

在本节的其余部分,我们将解释矩阵分解的实现,并在 MovieLens 数据集上训练该模型。

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.3.2. 模型实现

首先,我们实现上面描述的矩阵分解模型。用户和物品的潜在因子可以使用 nn.Embedding 来创建。input_dim 是物品/用户的数量,而 output_dim 是潜在因子 \(k\) 的维度。我们也可以使用 nn.Embedding 来创建用户/物品的偏置,只需将 output_dim 设置为 1。在 forward 函数中,用户和物品的 ID 用于查找相应的嵌入。

class MF(nn.Block):
    def __init__(self, num_factors, num_users, num_items, **kwargs):
        super(MF, self).__init__(**kwargs)
        self.P = nn.Embedding(input_dim=num_users, output_dim=num_factors)
        self.Q = nn.Embedding(input_dim=num_items, output_dim=num_factors)
        self.user_bias = nn.Embedding(num_users, 1)
        self.item_bias = nn.Embedding(num_items, 1)

    def forward(self, user_id, item_id):
        P_u = self.P(user_id)
        Q_i = self.Q(item_id)
        b_u = self.user_bias(user_id)
        b_i = self.item_bias(item_id)
        outputs = (P_u * Q_i).sum(axis=1) + np.squeeze(b_u) + np.squeeze(b_i)
        return outputs.flatten()

21.3.3. 评估指标

然后我们实现均方根误差(RMSE)指标,它通常用于衡量模型预测的评分与实际观察到的评分(真实值)之间的差异 (Gunawardana and Shani, 2015)。RMSE 定义为:

(21.3.4)\[\textrm{RMSE} = \sqrt{\frac{1}{|\mathcal{T}|}\sum_{(u, i) \in \mathcal{T}}(\mathbf{R}_{ui} -\hat{\mathbf{R}}_{ui})^2}\]

其中 \(\mathcal{T}\) 是你想要评估的用户和物品对的集合。\(|\mathcal{T}|\) 是这个集合的大小。我们可以使用 mx.metric 提供的 RMSE 函数。

def evaluator(net, test_iter, devices):
    rmse = mx.metric.RMSE()  # Get the RMSE
    rmse_list = []
    for idx, (users, items, ratings) in enumerate(test_iter):
        u = gluon.utils.split_and_load(users, devices, even_split=False)
        i = gluon.utils.split_and_load(items, devices, even_split=False)
        r_ui = gluon.utils.split_and_load(ratings, devices, even_split=False)
        r_hat = [net(u, i) for u, i in zip(u, i)]
        rmse.update(labels=r_ui, preds=r_hat)
        rmse_list.append(rmse.get()[1])
    return float(np.mean(np.array(rmse_list)))

21.3.4. 训练和评估模型

在训练函数中,我们采用带权重衰减的 \(\ell_2\) 损失。权重衰减机制与 \(\ell_2\) 正则化具有相同的效果。

#@save
def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,
                        devices=d2l.try_all_gpus(), evaluator=None,
                        **kwargs):
    timer = d2l.Timer()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 2],
                            legend=['train loss', 'test RMSE'])
    for epoch in range(num_epochs):
        metric, l = d2l.Accumulator(3), 0.
        for i, values in enumerate(train_iter):
            timer.start()
            input_data = []
            values = values if isinstance(values, list) else [values]
            for v in values:
                input_data.append(gluon.utils.split_and_load(v, devices))
            train_feat = input_data[:-1] if len(values) > 1 else input_data
            train_label = input_data[-1]
            with autograd.record():
                preds = [net(*t) for t in zip(*train_feat)]
                ls = [loss(p, s) for p, s in zip(preds, train_label)]
            [l.backward() for l in ls]
            l += sum([l.asnumpy() for l in ls]).mean() / len(devices)
            trainer.step(values[0].shape[0])
            metric.add(l, values[0].shape[0], values[0].size)
            timer.stop()
        if len(kwargs) > 0:  # It will be used in section AutoRec
            test_rmse = evaluator(net, test_iter, kwargs['inter_mat'],
                                  devices)
        else:
            test_rmse = evaluator(net, test_iter, devices)
        train_l = l / (i + 1)
        animator.add(epoch + 1, (train_l, test_rmse))
    print(f'train loss {metric[0] / metric[1]:.3f}, '
          f'test RMSE {test_rmse:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(devices)}')

最后,我们将所有部分整合起来并训练模型。这里,我们将潜在因子维度设置为30。

devices = d2l.try_all_gpus()
num_users, num_items, train_iter, test_iter = d2l.split_and_load_ml100k(
    test_ratio=0.1, batch_size=512)
net = MF(30, num_users, num_items)
net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))
lr, num_epochs, wd, optimizer = 0.002, 20, 1e-5, 'adam'
loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), optimizer,
                        {"learning_rate": lr, 'wd': wd})
train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,
                    devices, evaluator)
train loss 0.065, test RMSE 1.066
56722.8 examples/sec on [gpu(0), gpu(1)]
../_images/output_mf_922250_9_1.svg

下面,我们使用训练好的模型来预测一个用户(ID 20)可能给一个物品(ID 30)的评分。

scores = net(np.array([20], dtype='int', ctx=devices[0]),
             np.array([30], dtype='int', ctx=devices[0]))
scores
array([3.1718655], ctx=gpu(0))

21.3.5. 小结

  • 矩阵分解模型广泛应用于推荐系统中。它可以用来预测用户可能给物品的评分。

  • 我们可以实现并训练用于推荐系统的矩阵分解模型。

21.3.6. 练习

  • 改变潜在因子的大小。潜在因子的大小如何影响模型性能?

  • 尝试不同的优化器、学习率和权重衰减率。

  • 查看其他用户对特定电影的预测评分。

讨论