矩阵分解

矩阵分解(Matrix Factorization,MF)是推荐系统文献中公认的一种算法。最初版本的矩阵分解模型由Simon Funk发表在一篇非常知名的博文中,他在这篇博文描述了分解矩阵的想法。随后,由于2006年举办的Netflix竞赛,该模型变得广为人知。那时,流媒体和视频租赁公司Netflix为了增进其推荐系统的性能而举办了一项比赛。如果最佳团队(例如Cinematch)能够将Netflix的基线提高10%,那么他们将赢得100万美元的奖励。这一比赛在推荐系统领域引起了广泛的关注。随后,BellKor’s Pragmatic Chaos团队(一个由BellKor、Pragmatic Theory和BigChaos混合组成的团队)赢得了这一大奖。尽管他们的最终评分来自一个集成解决方案,矩阵分解算法仍在其中起到了关键作用。Netflix Grand Prize的技术报告:cite:Toscher.Jahrer.Bell.2009详细解释了该方案所采用的模型。在本节中,我们将深入研究矩阵分解模型的细节和实现过程。

矩阵分解模型

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

使用$\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$行,同时使用$\mathbf{q}_i$表示矩阵$\mathbf{Q}$的第$i$行。对于某一物品$i$,$\mathbf{q}_i$中的数值衡量了特征(例如电影风格和语言等)的大小。对于某一用户$u$,$\mathbf{p}_u$中的数值衡量他对物品相应特征的感兴趣程度。这些潜因子可能代表了之前提到的一些维度,但同时它们也可能是完全无法理解的。 用户对物品的预测评分可以通过下式计算:

\hat{\mathbf{R}} = \mathbf{PQ}^\top

上式中的$\hat{\mathbf{R}}\in \mathbb{R}^{m \times n}$表示预测评分矩阵,它的形状和真实评分矩阵$\mathbf{R}$是一致的。这种预测方式的主要问题是无法建模表示用户和物品的偏置。例如,有一些用户倾向于给出较高的评分,而有一些物品由于质量较差得到的评分普遍较低。这类偏置在实际应用中很常见。为了表示这种偏置,我们在此处引入了用户偏置和物品偏置。具体来说,用户$u$对物品$i$的评分由下式计算得到。

$$ \hat{\mathbf{R}}_{ui} = \mathbf{p}_u\mathbf{q}^\top_i + b_u + b_i $$

然后,我们通过减少预测评分和实际评分的均方误差来训练矩阵分解模型。目标函数如下所示:

$$ \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} |^2F + | \mathbf{Q} |^2_F + b_u^2 + b_i^2 )$被用来规避过拟合问题。已知$\mathbf{R}{ui}$的$(u, i)$对储存在集合$\mathcal{K}={(u, i) \mid \mathbf{R}_{ui} \text{ is known}}$当中。模型参数可以通过优化算法(例如随机梯度下降法和Adam)学习得到。

矩阵分解模型的直观示意图如下所示:

矩阵分解模型的图示

在本节的最后,我们将解释矩阵分解模型的实现过程,并使用MovieLens数据集训练模型。

  1. from d2l import mxnet as d2l
  2. from mxnet import autograd, gluon, np, npx
  3. from mxnet.gluon import nn
  4. import mxnet as mx
  5. npx.set_np()

模型实现

首先,我们按照上述描述实现矩阵分解模型。用户和物品的潜因子可以通过nn.Embedding构造。input_dim为用户和物品的数量,而output_dim为潜因子$k$的维度。将output_dim设置为1后,我们也可以使用nn.Embedding构造用户和物品的偏置量。在forward函数中,用户和物品的id被用于索引嵌入向量。

  1. class MF(nn.Block):
  2. def __init__(self, num_factors, num_users, num_items, **kwargs):
  3. super(MF, self).__init__(**kwargs)
  4. self.P = nn.Embedding(input_dim=num_users, output_dim=num_factors)
  5. self.Q = nn.Embedding(input_dim=num_items, output_dim=num_factors)
  6. self.user_bias = nn.Embedding(num_users, 1)
  7. self.item_bias = nn.Embedding(num_items, 1)
  8. def forward(self, user_id, item_id):
  9. P_u = self.P(user_id)
  10. Q_i = self.Q(item_id)
  11. b_u = self.user_bias(user_id)
  12. b_i = self.item_bias(item_id)
  13. outputs = (P_u * Q_i).sum(axis=1) + np.squeeze(b_u) + np.squeeze(b_i)
  14. return outputs.flatten()

评估方法

接下来,我们使用均方根误差(root-mean-square error,RMSE)作为度量。该度量方式常用于测量模型的预测评分和实际评分(真值)之间的差异:cite:Gunawardana.Shani.2015。RMSE定义如下:

$$ \mathrm{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函数。

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

训练和评估模型

在训练函数中,我们采用了$L_2$损失作为权重衰减函数。该权重衰减机制和$L_2$正则化具有相同的效果。

  1. #@save
  2. def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,
  3. ctx_list=d2l.try_all_gpus(), evaluator=None,
  4. **kwargs):
  5. timer = d2l.Timer()
  6. animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 2],
  7. legend=['train loss', 'test RMSE'])
  8. for epoch in range(num_epochs):
  9. metric, l = d2l.Accumulator(3), 0.
  10. for i, values in enumerate(train_iter):
  11. timer.start()
  12. input_data = []
  13. values = values if isinstance(values, list) else [values]
  14. for v in values:
  15. input_data.append(gluon.utils.split_and_load(v, ctx_list))
  16. train_feat = input_data[0:-1] if len(values) > 1 else input_data
  17. train_label = input_data[-1]
  18. with autograd.record():
  19. preds = [net(*t) for t in zip(*train_feat)]
  20. ls = [loss(p, s) for p, s in zip(preds, train_label)]
  21. [l.backward() for l in ls]
  22. l += sum([l.asnumpy() for l in ls]).mean() / len(ctx_list)
  23. trainer.step(values[0].shape[0])
  24. metric.add(l, values[0].shape[0], values[0].size)
  25. timer.stop()
  26. if len(kwargs) > 0: # It will be used in section AutoRec
  27. test_rmse = evaluator(net, test_iter, kwargs['inter_mat'],
  28. ctx_list)
  29. else:
  30. test_rmse = evaluator(net, test_iter, ctx_list)
  31. train_l = l / (i + 1)
  32. animator.add(epoch + 1, (train_l, test_rmse))
  33. print(f'train loss {metric[0] / metric[1]:.3f}, '
  34. f'test RMSE {test_rmse:.3f}')
  35. print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
  36. f'on {str(ctx_list)}')

最后,把所有的东西全都结合起来然后开始训练模型。此处,我们将潜因子的维度设置为30。

  1. ctx = d2l.try_all_gpus()
  2. num_users, num_items, train_iter, test_iter = d2l.split_and_load_ml100k(
  3. test_ratio=0.1, batch_size=512)
  4. net = MF(30, num_users, num_items)
  5. net.initialize(ctx=ctx, force_reinit=True, init=mx.init.Normal(0.01))
  6. lr, num_epochs, wd, optimizer = 0.002, 20, 1e-5, 'adam'
  7. loss = gluon.loss.L2Loss()
  8. trainer = gluon.Trainer(net.collect_params(), optimizer,
  9. {"learning_rate": lr, 'wd': wd})
  10. train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,
  11. ctx, evaluator)

下面,我们使用训练过的模型来预测ID为20的用户对ID为30的物品的评分。

  1. scores = net(np.array([20], dtype='int', ctx=d2l.try_gpu()),
  2. np.array([30], dtype='int', ctx=d2l.try_gpu()))
  3. scores

小结

  • 矩阵分解模型在推荐系统中有着广泛的应用。它可以用于预测用户对物品的评分。
  • 我们可以为推荐系统实现和训练一个矩阵分解模型。

练习

  • 修改潜因子的维度。潜因子的维度将怎样影响模型的性能呢?
  • 尝试不同的优化器、学习率和权重衰减率。
  • 检查其他用户对某一电影的评分。

Discussions