2.1.4. 矩阵分解¶
在推荐系统中,我们经常观察到这样的规律:喜欢《理智与情感》的用户往往也会给《公主日记》好评,而钟爱《致命武器》的观众通常也喜欢《独立日》。这反映了用户偏好的内在结构——一些隐含的“品味因子”在起作用。比如有些用户偏好面向女性的影片,有些则更喜欢面向男性的内容;有些人倾向于严肃深刻的作品如《阿马迪斯》,有些人则更享受轻松娱乐的片子如《阿呆与阿瓜》。
在前面的章节中,我们了解了UserCF和ItemCF这些基于邻域的协同过滤方法。它们的思路很直观:通过寻找相似的用户或物品来进行推荐,就像 图2.1.6 展示的那样。
图2.1.6 基于用户行为统计的方法。张三喜欢左边的三部电影。为了对他进行预测,系统会找到也喜欢这些电影的相似用户,然后确定他们还喜欢哪些其他电影。在这种情况下,所有三位用户都喜欢《拯救大兵瑞恩》,因此这是首个推荐。接着,其中两位用户喜欢《沙丘》,所以它排在第二位,依此类推。¶
但这种方法有个致命弱点:当评分数据非常稀疏时,很难找到足够的相似用户或物品。想想看,在一个有百万用户和十万电影的系统中,大部分用户只看过其中几十部电影,传统方法很可能因为共同评分太少而失效。
这时候,矩阵分解登场了。它不再直接寻找相似性,而是换了个思路:假设用户的偏好和电影的特征都可以用几个关键因子来描述。比如,我们可以用“面向男性vs面向女性”和“严肃vs逃避现实”这两个维度来刻画电影,同时用用户对这两类特征的偏好程度来描述用户。这样,预测一个用户对某部电影的评分就变成了计算这两个向量的相似度。
图2.1.7 隐语义模型意图,该方法通过两个维度来刻画用户和电影:一个是面向男性与面向女性,另一个是严肃与逃避现实。¶
矩阵分解的核心想法建立在两个关键假设上:
第一个是低秩假设:虽然评分矩阵看起来很复杂,但实际上可能只受少数几个隐含因素影响,比如“面向男性vs面向女性”、“严肃vs逃避现实”等维度。
第二个是隐向量假设:每个用户和每部电影都能用一个包含这些隐含因子的向量来表示,用户向量反映了其对各种因子的偏好程度,而电影向量则描述了该电影在各个因子上的特征强度。
这种做法的好处是显而易见的:即使两个用户没有看过相同的电影,只要他们在隐含因子上表现相似,我们就能为他们推荐相似的内容。这大大提高了模型处理稀疏数据的能力。
接下来我们看看如何把这个想法变成具体的算法。我们将介绍两种矩阵分解模型:简单直接的基础模型(FunkSVD)和考虑评分偏差的改进模型(BiasSVD)。
2.1.4.1. FunkSVD: 基础模型¶
FunkSVD 由 Simon Funk 在2006年提出 (Funk, 2006),是矩阵分解家族中最容易理解的一个。它的想法非常直接:把复杂的用户-物品评分矩阵分解成两个简单的矩阵——用户特征矩阵和物品特征矩阵。
假设我们有\(m\)个用户和\(n\)个物品,想要用\(K\)个隐含因子来描述它们。那么用户\(u\)可以用一个\(K\)维向量\(p_u\)来表示,物品\(i\)也可以用一个\(K\)维向量\(q_i\)来表示。预测用户\(u\)对物品\(i\)的评分就是这两个向量的内积:
这里\(p_{u,k}\)表示用户\(u\)在第\(k\)个隐含因子上的偏好程度,\(q_{i,k}\)表示物品\(i\)在第\(k\)个隐含因子上的特征强度。
现在问题变成了:如何找到这些隐含因子?我们采用一个很自然的思路——让预测评分尽可能接近真实评分。具体来说,我们要最小化所有已知评分的预测误差:
这里\(\mathcal{K}\)表示所有已知评分的用户-物品对,\(r_{ui}\)是用户\(u\)对物品\(i\)的真实评分。
要解决这个优化问题,我们使用梯度下降法。对于每个观测到的评分,我们先计算预测误差\(e_{ui} = r_{ui} - p_u^T q_i\),然后沿着误差减小的方向更新参数:
其中\(\eta\)是学习率,控制每次更新的步长。这个更新规则的直觉很简单:如果预测评分偏低了(\(e_{ui} > 0\)),我们就增大相关的参数值;如果预测偏高了,就减小参数值。
不过在实际应用中,我们通常还会加入L2正则化来防止过拟合:
这样可以避免模型过度拟合训练数据,提高在新数据上的表现。
2.1.4.2. BiasSVD: 改进模型¶
基础模型虽然简洁,但在实际使用中我们发现了一个问题:不同用户的评分习惯差异很大。有些用户天生就是“好人”,很少给低分;有些用户则比较严格,平均分都不高。同样,有些电影因为制作精良或者明星云集,普遍得到较高评分;而有些冷门或质量一般的电影则评分偏低。
这些系统性的偏差如果不处理,会影响推荐的准确性。BiasSVD (Koren et al., 2009) 正是为了解决这个问题而提出的。它在基础模型的基础上引入了偏置项,让预测公式变成:
这里新增了三个项:\(\mu\)是所有评分的全局平均值,反映了整个系统的评分水平;\(b_u\)是用户\(u\)的个人偏置,反映了该用户相对于平均水平是倾向于给高分还是低分;\(b_i\)是物品\(i\)的偏置,反映了该物品相对于平均水平是受欢迎还是不受欢迎。
相应地,优化目标也要调整:
在参数更新时,除了用户和物品的隐向量,我们还需要更新偏置项:
这种改进看似简单,但效果显著。通过分离出系统性偏差,模型能够更准确地捕捉用户和物品之间的真实交互模式,从而提供更精准的推荐。
2.1.4.3. 代码实践¶
FunkSVD 在 MovieLens 数据集上的应用
import os
import sys
import funrec
from funrec.utils import build_metrics_table
# 加载配置
config = funrec.load_config('funksvd')
# 加载数据
train_data, test_data = funrec.load_data(config.data)
# 准备特征
feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data)
# 训练模型
models = funrec.train_model(config.training, feature_columns, processed_data)
# 评估模型
metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns)
print(build_metrics_table(metrics))
+---------------+--------------+-----------+----------+----------------+---------------+
| hit_rate@10 | hit_rate@5 | ndcg@10 | ndcg@5 | precision@10 | precision@5 |
+===============+==============+===========+==========+================+===============+
| 0.0033 | 0.0018 | 0.0016 | 0.0011 | 0.0003 | 0.0004 |
+---------------+--------------+-----------+----------+----------------+---------------+
BiasSVD 在 MovieLens 数据集上的应用
config = funrec.load_config('biassvd')
# 加载数据
train_data, test_data = funrec.load_data(config.data)
# 准备特征
feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data)
# 训练模型
models = funrec.train_model(config.training, feature_columns, processed_data)
# 评估模型
metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns)
print(build_metrics_table(metrics))
+---------------+--------------+-----------+----------+----------------+---------------+
| hit_rate@10 | hit_rate@5 | ndcg@10 | ndcg@5 | precision@10 | precision@5 |
+===============+==============+===========+==========+================+===============+
| 0.001 | 0.0007 | 0.0008 | 0.0007 | 0.0001 | 0.0001 |
+---------------+--------------+-----------+----------+----------------+---------------+