基于矩阵分解的推荐算法入门
推荐系统入门
协同过滤推荐算法:原理、实现与分析
基于内容的推荐算法入门
一、引言
在个性化推荐系统中,协同过滤(Collaborative Filtering)是一种经典且有效的技术。它通过用户与物品之间的历史交互数据(如评分、点击等)挖掘“集体智慧”,实现用户兴趣建模。其中,基于矩阵分解(Matrix Factorization, MF)的方法,因其兼具建模能力强、可扩展性好、效果稳健等优势,已成为工业界应用最广泛的协同过滤算法之一。
自 Netflix Prize 竞赛以来,以 SVD(Singular Value Decomposition) 及其变体(如 FunkSVD / LFM、BiasSVD)为代表的矩阵分解方法,推动了推荐系统从“基于邻域的浅层方法”迈向“深层结构建模”的时代。这类方法能够通过分解评分矩阵,挖掘出用户和物品在低维空间中的“隐语义特征(Latent Factors)”,实现更加个性化且可泛化的推荐。
矩阵分解的基本思想是:将原始的稀疏评分矩阵近似分解为两个低维矩阵的乘积,其中一个矩阵表示用户在各隐因子上的偏好程度,另一个矩阵表示物品在各隐因子上的属性表达。这些隐因子本质上是一些不可观测但潜在存在的变量,如电影的类型、题材、风格,或用户的偏好偏向等,虽然它们没有明确的语义标签,但其分布能有效捕捉用户 - 物品之间的匹配关系。
从建模视角来看,矩阵分解是一种融合了“协同过滤的集体智慧”“隐语义建模能力”与“机器学习目标函数驱动”的方法,其本质是一个带监督学习过程的矩阵补全任务:通过最小化已知评分位置的预测误差,学习出最优的用户 - 物品隐向量,从而推断出缺失评分并用于推荐排序。
这类模型也被称为隐语义模型(Latent Factor Model, LFM),是推荐系统中建模能力最强、落地场景最广的模型类型之一。
本文面向有经验的技术人员,系统介绍基于矩阵分解的核心推荐算法,包括 SVD、FunkSVD(LFM)与 BiasSVD,从理论出发剖析其建模原理,并通过 Python 示例(基于 Surprise 库)演示评分预测与 Top-N 推荐的完整流程,帮助读者理解并掌握这类模型在实际系统中的应用方式。
二、SVD:奇异值分解简介
2.1 基本定义
奇异值分解(SVD, Singular Value Decomposition)是线性代数中的一种基本矩阵分解技术,能够将任意实矩阵分解为三个矩阵的乘积:
其中:
是原始的评分矩阵( 个用户, 个物品); 是用户的左奇异向量矩阵,可看作是用户在 维潜在空间中的表示; 是奇异值对角矩阵,奇异值表示该方向上的重要性; 是物品的右奇异向量矩阵,可视为物品在同一潜在空间中的表示; 通常 ,我们只保留前 个最大的奇异值和对应的向量,从而获得一个低秩近似。
这是一个经典的矩阵降维方法,本质上是将原始高维空间中的用户和物品投影到一个低维潜在空间,使得在该空间中的点积可以尽可能还原原始评分。
2.2 通俗理解:SVD 是“看懂评分背后隐藏规律”的方法
假设你有一个巨大的表格,里面记录了上千个用户对上千部电影的打分(评分矩阵 )。这张表格看起来很复杂——每个人的口味都不同,每部电影被打的分数也五花八门。但你有没有想过:
是不是可以把这些“打分”的差异,归结为一些“潜在的原因”?
比如:
有些用户喜欢动作片; 有些电影浪漫风十足; 有些人偏好老电影……
这些“看不见但影响很大”的因素,我们称之为潜在因子(latent factors)。
SVD 做了什么?
SVD 就像是一台“X 光机”:
把这个复杂的评分表格()扫描一遍,拆成三个部分(、 和 ),试图找出用户和电影之间隐藏的偏好与特征匹配模式。
:每个用户的“兴趣指纹”,告诉我们他/她在几个潜在维度(比如:喜欢动作、文艺、冷门等)上的倾向; :每个维度的重要程度,类似于“权重”; :每部电影的“风格特征”,也是在相同的潜在空间里的表示。
最终,我们可以通过用户兴趣和电影风格在这个潜在空间里的“点积”,来预测一个打分值。
一句话总结:
SVD 是一种帮我们从混乱的打分数据中,提取出“用户偏好”和“物品特征”之间潜在规律的方法,从而实现压缩、理解和预测。
2.3 推荐系统的建模意义
在推荐系统中,评分矩阵 是一个稀疏矩阵,其中 表示用户 对物品 的评分,而大多数 是未知的。
通过 SVD 对 进行低秩近似,可以得到重构矩阵:
该近似矩阵中, 的值可用作对用户 对物品 的预测评分。由于我们保留了主要的潜在因子(如用户的兴趣、物品的主题),这类预测具有较强的语义表达能力。
这种方法的核心思想是:
用户和物品可以通过一些低维的“隐语义因子”来刻画,而评分可以看作是这些因子的内积。
举个例子,在电影推荐场景中,这些隐因子可能代表风格(喜剧、科幻)、年代、演员偏好等,SVD 的目标就是发现这些因子,并将用户和电影表示为该空间中的向量。
2.4 理论的限制与实际挑战
尽管 SVD 理论简洁优美,但其在实际推荐系统中的直接应用存在如下挑战:
完整性假设不成立:标准 SVD 需要对完整矩阵进行分解,但推荐系统中的评分矩阵极为稀疏,直接对其做 SVD 会产生误导性的重构。 对缺失值处理不当:SVD 会默认缺失项为 0,这不符合推荐系统中“缺失即未知”的假设。 可扩展性差:SVD 计算复杂度较高(特别是在用户数和物品数都很大时),不适用于大规模工业推荐。
2.5 实际中的演化与实现
为解决上述问题,推荐系统中常使用如下几种做法:
截断 SVD(Truncated SVD):仅在预处理后的稠密子矩阵上做降维,用于特征工程。 矩阵补全与优化方法:例如 FunkSVD、BiasSVD、ALS 等,它们本质上是假设 ,通过梯度下降等方法直接学习用户矩阵 和物品矩阵 。 现代库实现:如 Surprise 库中提供的 SVD类,虽然名为“SVD”,但其实现实质是带偏置项的隐语义模型(BiasSVD),并非真正数学意义上的 SVD。
✅ 小结:理论上的 SVD 为推荐系统提供了低秩建模的思路,但在实践中需要结合稀疏性与可扩展性问题进行相应改进,形成实用的矩阵分解推荐算法。
三、FunkSVD(LFM):用 SGD 训练的潜在因子模型
3.1 FunkSVD 简介
在 Netflix Prize 大赛中,Simon Funk 提出了著名的 FunkSVD 算法,这是最早使用随机梯度下降(SGD)对矩阵分解进行优化的推荐方法之一。传统的 SVD 依赖线性代数中的奇异值分解技术,但这在实际的推荐系统中并不适用,因为评分矩阵通常非常稀疏且缺失严重。
FunkSVD 的核心思想是将用户-物品评分矩阵近似为两个低秩矩阵的乘积,并用优化方法(如 SGD)从已有评分中学习这两个矩阵:
其中, 是用户 的隐向量, 是物品 的隐向量, 是潜在因子的维度。
优化目标是最小化所有已知评分上的平方损失函数,并加入 正则项以防止过拟合:
其中:
潜在因子的语义意义
FunkSVD 代表了一种典型的隐语义模型(Latent Factor Model)。它假设每个用户和物品都可以映射到一个共享的低维“兴趣空间”中。在这个空间中,两个向量的相似度(例如内积)能够反映出用户对物品的兴趣程度。
例如:
中的某一维可能表示“喜剧片喜好”,而 中的对应维则代表某部电影的“喜剧成分”; 二者在该维度上的乘积越大,表明该用户更可能喜欢该电影。
3.2 Python 实现(不含偏置)
下面是一个简洁的 FunkSVD 实现,不包含全局平均评分、用户偏置和物品偏置,适合初学者理解核心机制:
import numpy as npclass FunkSVD:def __init__(self, n_users, n_items, k=20, lr=0.01, reg=0.02, epochs=20):self.k = k # 潜在因子维度self.lr = lr # 学习率self.reg = reg # 正则化参数self.epochs = epochs # 训练轮数self.P = np.random.normal(scale=1./k, size=(n_users, k)) # 用户隐向量矩阵self.Q = np.random.normal(scale=1./k, size=(n_items, k)) # 物品隐向量矩阵def train(self, ratings):# ratings: [(user_id, item_id, rating), ...]for epoch in range(self.epochs):for u, i, r in ratings:# 预测评分pred = np.dot(self.P[u], self.Q[i])# 误差err = r - pred# 梯度更新用户和物品向量self.P[u] += self.lr * (err * self.Q[i] - self.reg * self.P[u])self.Q[i] += self.lr * (err * self.P[u] - self.reg * self.Q[i])def predict(self, u, i):return np.dot(self.P[u], self.Q[i])
3.3 训练技巧与常见优化点
在实际工程中,FunkSVD 训练过程中还可以考虑以下优化技巧:
Bias 增强:考虑全局平均评分 、用户偏置 和物品偏置 ,预测评分形式变为:
学习率衰减(Learning Rate Decay):逐步降低学习率有助于收敛稳定。
Shuffle 数据:每轮 epoch 随机打乱训练数据可避免顺序偏差。
早停(Early Stopping):使用验证集监控 RMSE,提前终止过拟合模型训练。
矩阵初始化:小范围高斯分布初始化通常优于全零或均匀分布。
四、BiasSVD:引入用户与物品偏置项
4.1 模型公式
在推荐系统中,评分偏差(Bias)是常见的现象。例如,有些用户通常给出较高的评分,而有些物品可能普遍得到较低的评分。为了更好地拟合这种情况,BiasSVD 对 FunkSVD 进行了扩展,加入了用户偏置项()和物品偏置项(),以捕捉这些评分的系统性偏差。
评分模型
BiasSVD 模型的评分预测公式如下:
其中:
:全局平均评分,表示所有用户对所有物品评分的平均值。 :用户 的偏置项,表示该用户相对于全局平均评分的偏高或偏低的趋势。 :物品 的偏置项,表示该物品相对于全局平均评分的偏高或偏低的趋势。 和 :用户 和物品 的隐向量,表示潜在因子模型中用户和物品的特征。
4.2 偏置的介绍
偏置是推荐系统中不可忽视的因素,它能够捕捉到评分数据中的系统性偏差。在矩阵分解模型中,偏置部分对提升评分预测的准确性至关重要,甚至往往比个性化部分的贡献更大。具体来说,偏置可以分为三个主要部分:
全局平均数():全局平均数表示训练集中所有评分记录的平均值,是衡量全体用户对物品的总体评分偏差。它常常作为预测评分的基准值,用于调整后续的个性化预测。
用户偏置():用户偏置表示某一特定用户的评分习惯,相较于全局平均评分的偏差。例如,有些用户可能倾向于给出较高的评分,而另一些用户可能给出较低的评分。通过引入用户偏置项,模型能够捕捉到这种用户评分的系统性偏差。
物品偏置():物品偏置反映了物品本身的评分特点。某些物品可能因其本身的属性或大众认知得到较高评分(如好片),而其他物品则可能因质量差或不受欢迎而获得较低评分(如烂片)。物品偏置项能够有效捕捉这种系统性偏差。
这些偏置项的引入能帮助模型更好地拟合评分数据中的全局和局部偏差,从而提高评分预测的准确性。在 Netflix Prize 推荐比赛中,Yehuda Koren 表示,单纯使用偏置部分就能够将评分误差降低 32%,而加入个性化部分(即用户和物品的隐向量)后,误差进一步降低至 42%。这也表明偏置部分在推荐系统中发挥的关键作用,远远超过了个性化部分。
4.3 参数更新与梯度推导
通过对损失函数求偏导,可以得到每个参数的更新规则。
假设我们已知误差 ,则每个参数的梯度更新公式如下:
全局平均评分 :在 BiasSVD 中, 是训练集的全局平均评分,通常作为固定常数,不参与梯度更新。 对用户偏置项 : 对物品偏置项 : 对用户隐向量 : 对物品隐向量 :
4.4 Python 实现(简化版)
以下是 BiasSVD 的简化版 Python 实现,它继承自 FunkSVD 类,并加入了用户与物品的偏置项:
import numpy as npclass BiasSVD(FunkSVD):def __init__(self, n_users, n_items, k=20, lr=0.01, reg=0.02, epochs=20):super().__init__(n_users, n_items, k, lr, reg, epochs)self.bu = np.zeros(n_users) # 用户偏置self.bi = np.zeros(n_items) # 物品偏置self.mu = 0 # 全局平均评分def train(self, ratings):# 计算全局平均评分self.mu = np.mean([r for _, _, r in ratings])for _ in range(self.epochs):for u, i, r in ratings:# 计算含偏置的预测值pred = self.mu + self.bu[u] + self.bi[i] + np.dot(self.P[u], self.Q[i])err = r - pred # 计算误差# 更新偏置项self.bu[u] += self.lr * (err - self.reg * self.bu[u]) # 用户偏置更新self.bi[i] += self.lr * (err - self.reg * self.bi[i]) # 物品偏置更新# 更新隐向量(复用父类方法)self.P[u] += self.lr * (err * self.Q[i] - self.reg * self.P[u]) # 用户隐向量更新self.Q[i] += self.lr * (err * self.P[u] - self.reg * self.Q[i]) # 物品隐向量更新def predict(self, u, i):# 含偏置的预测评分return self.mu + self.bu[u] + self.bi[i] + np.dot(self.P[u], self.Q[i])
4.5 模型性能与扩展
BiasSVD 比 FunkSVD 更为精确,因为它能够捕捉用户和物品的偏差性。然而,它也增加了更多的参数,特别是用户和物品的偏置项。在某些场景中,BiasSVD 的性能显著优于 FunkSVD,特别是在存在明显用户和物品偏差的情况下。
1)性能评估
在训练过程中,可以通过 RMSE(均方根误差) 来评估模型的预测性能。一般来说,随着训练的进行,BiasSVD 会随着更好地拟合偏差项而展现出较低的误差。
2)扩展
对于大规模数据集,BiasSVD 可能需要更多的内存和计算资源。为此,我们可以考虑采用 Mini-batch SGD 或 ALS(交替最小二乘法) 等优化方法,以提高训练效率。
五、使用 Surprise 库实现矩阵分解推荐(SVD 与 SVD++)
Surprise 是一个专为推荐系统设计的 Python 库,支持多种协同过滤算法,特别适合基于评分数据的推荐建模任务。其中,矩阵分解类算法尤为核心:
SVD实现了 带偏置项的矩阵分解模型(BiasSVD),不仅分解用户和物品的潜在因子,还引入了全局平均评分、用户偏置和物品偏置;SVDpp(SVD++)在SVD的基础上进一步引入了 用户的隐式反馈(如用户是否曾浏览、点击或评分过某个物品),从而提升了对用户兴趣的建模能力,特别适用于行为数据丰富的场景。
与传统的“仅用显式评分”模型不同,SVD++ 通过建模“用户行为中隐含的偏好”来增强推荐的个性化效果,具体体现在用户隐式交互的物品也会影响其潜在向量表示。
完整代码请访问:
https://www.kaggle.com/code/user215638/movie-recommendation2
输出结果:
🔍 正在搜索 SVD 最优参数...[Parallel(n_jobs=-1)]: Using backend LokyBackend with 4 concurrent workers.[Parallel(n_jobs=-1)]: Done 48 out of 48 | elapsed: 26.5s finishedRMSE: 0.5061✅ SVD 完成,耗时:29.19 秒🔍 正在搜索 SVD++ 最优参数...[Parallel(n_jobs=-1)]: Using backend LokyBackend with 4 concurrent workers.[Parallel(n_jobs=-1)]: Done 48 out of 48 | elapsed: 68.1min finishedRMSE: 0.6355✅ SVD++ 完成,耗时:4824.01 秒📊 模型对比结果:SVD : RMSE = 0.5061,耗时 = 29.19 秒SVD++ : RMSE = 0.6355,耗时 = 4824.01 秒
5.1 模型对比概览
SVD | |||
SVDpp |
在本文中,我们将使用 Surprise 库基于真实评分数据实现 SVD 和 SVDpp 模型,进行训练、评估和参数调优,并从 RMSE 和训练效率等维度进行对比分析。
5.2 环境准备与数据加载
import pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsfrom surprise import SVD, SVDpp, Reader, Dataset, accuracyfrom surprise.model_selection import GridSearchCVimport timeimport warningswarnings.filterwarnings("ignore", category=FutureWarning)# 1. 读取训练集与测试集train_df = pd.read_csv('/kaggle/input/movie-data/recommendation-ratings-train.txt', sep=',')test_df = pd.read_csv('/kaggle/input/movie-data/recommendation-ratings-test.txt', sep=',')print(train_df.head())
示例数据格式如下:
userId,movieId,rating,timestamp1,1,4,9649827031,3,4,964981247...
5.3 数据分析与清洗
为提升模型训练效果,先对数据进行基本分析与冷启动过滤(过滤评分过少的用户与物品):
# 用户评分数分布user_rating_counts = train_df.groupby('userId')['rating'].count()item_rating_counts = train_df.groupby('movieId')['rating'].count()# 仅保留评分数 ≥ 5 的用户与物品min_ratings = 5valid_users = user_rating_counts[user_rating_counts >= min_ratings].indexvalid_items = item_rating_counts[item_rating_counts >= min_ratings].indexfiltered_train_df = train_df[train_df['userId'].isin(valid_users) & train_df['movieId'].isin(valid_items)].copy()filtered_test_df = test_df[test_df['userId'].isin(valid_users) & test_df['movieId'].isin(valid_items)].copy()
5.4 构建 Surprise 数据格式
# 构建 Surprise 所需格式reader = Reader(rating_scale=(1, 5))train_data = Dataset.load_from_df(filtered_train_df[['userId', 'movieId', 'rating']], reader)trainset = train_data.build_full_trainset()# 测试集转为 testset 格式testset = list(zip(filtered_test_df['userId'], filtered_test_df['movieId'], filtered_test_df['rating']))
5.5 BiasSVD(SVD)模型训练与评估
print("🔍 正在训练 SVD 模型...")start = time.time()svd = SVD()svd.fit(trainset)# 测试集预测与评估predictions = svd.test(testset)rmse_svd = accuracy.rmse(predictions, verbose=True)print(f"✅ SVD 训练完成,耗时 {time.time() - start:.2f} 秒")
5.6 SVD++ 模型训练与评估
print("🔍 正在训练 SVD++ 模型...")start = time.time()svdpp = SVDpp()svdpp.fit(trainset)# 测试集预测与评估predictions = svdpp.test(testset)rmse_svdpp = accuracy.rmse(predictions, verbose=True)print(f"✅ SVD++ 训练完成,耗时 {time.time() - start:.2f} 秒")
5.7 超参数搜索(以 SVD++ 为例)
param_grid = {'n_epochs': [20, 40],'lr_all': [0.005, 0.01],'reg_all': [0.02, 0.1],'n_factors': [50, 100]}print("🔍 正在进行网格搜索...")grid_search = GridSearchCV(SVDpp, param_grid, measures=['rmse'], cv=3, n_jobs=-1)grid_search.fit(train_data)print(f"✅ 最佳 RMSE: {grid_search.best_score['rmse']:.4f}")print(f"📌 最佳参数: {grid_search.best_params['rmse']}")
5.8 可视化调参结果
results_df = pd.DataFrame(grid_search.cv_results)# 可视化 RMSE 随参数变化的趋势plt.figure(figsize=(10, 6))sns.lineplot(x='param_n_epochs', y='mean_test_rmse', data=results_df, marker='o')plt.title("RMSE vs. Epochs (SVD++)")plt.xlabel("Epochs")plt.ylabel("Mean Test RMSE")plt.grid(True)plt.show()
5.9 总结与建议
rmse_svd | time_svd | ||
rmse_svdpp | time_svdpp |
SVD(BiasSVD)适合基于评分的轻量推荐系统;SVD++可处理更多用户行为信息(如点击、浏览等隐式反馈),推荐效果更佳,但训练耗时更长;使用 GridSearchCV可有效优化模型参数,降低 RMSE;RMSE 是衡量推荐系统预测精度的常用指标,越小越好。
六、Top-N 推荐评估:Precision@K、Recall@K 与 NDCG@K
在推荐系统中,Top-N 推荐是一个常见的评估场景,用户会收到一个由系统预测的推荐列表,其中包含前 N 个物品。我们通常希望为用户推荐 N 个物品,并评估推荐列表的质量。常见的评估指标包括:
Precision@K:推荐列表中有多少比例是用户感兴趣的;Recall@K:用户感兴趣的物品中有多少被推荐出来;NDCG@K:综合考虑推荐内容的相关性和顺序,排序越靠前得分越高。
这些指标分别关注推荐系统不同的评估维度,在实际应用中通常需要配合使用。
6.1 构建 Top-N 推荐列表
在 Top-N 推荐场景中,系统为用户输出一个有序推荐列表,而用户通常只会关注列表前 K 项,因此我们需要设计能够反映推荐列表质量的评估指标。
Precision@K 衡量推荐列表中相关物品的比例; Recall@K 衡量所有相关物品中有多少被推荐出来; NDCG@K 在命中相关物品的基础上进一步考虑其排序位置,越靠前越好。
为什么不仅仅看命中?
设想两种推荐列表:
推荐列表 A:将用户感兴趣的物品排在第 1 个; 推荐列表 B:将其排在第 10 个。
尽管它们的 Precision@K 和 Recall@K 可能相同,但用户对 A 的体验显然更好。为此,引入了 NDCG@K 来量化排序质量。
6.2 Precision@K 与 Recall@K:是否命中相关物品
在 Top-N 推荐中,我们关心系统推荐给用户的前 K 个物品中,有多少真正是用户感兴趣的。Precision@K 和 Recall@K 是衡量这种“命中情况”的两个基本指标。
1)Precision@K:推荐列表中相关物品的比例
定义:
即:在推荐的前 K 个物品中,有多少比例是用户真正喜欢的。
示例:
假设系统给用户推荐的前 5 个物品为:
ounter(line推荐列表 = [A, B, C, D, E]
用户真实感兴趣的物品为:
ounter(line相关物品 = [B, C, F]
那么前 5 个推荐中有 2 个(B 和 C)是用户感兴趣的,Precision@5 为:
理解要点:
Precision@K越高,说明推荐更精准;适用于推荐位置资源有限、错误推荐代价高的场景(如搜索首屏、广告推荐位); Precision无法反映是否遗漏了用户真正感兴趣的其他内容。
2)Recall@K:相关物品中被推荐出来的比例
定义:
即:在用户真正喜欢的所有物品中,有多少被系统推荐了出来。
延续上例:
用户感兴趣的物品总共是 3 个(B、C、F),系统命中了其中的 2 个(B 和 C),Recall@5 为:
理解要点:
Recall@K越高,说明系统覆盖了更多用户感兴趣的内容;适用于强调信息全面性、希望让用户看到更多喜好内容的场景(如视频平台的“为你推荐”); Recall忽略了推荐结果中非相关物品的比例,不关心“误推荐”。
3)小结
Precision@K 强调推荐结果的“准确率”; Recall@K 强调覆盖到“用户真正感兴趣物品”的比例;
二者的权衡是评估系统性能时的重要维度。在实际推荐系统设计中,应结合业务目标选择更合适的指标:
强调精准推荐 → Precision@K 优先 强调覆盖全面 → Recall@K 优先 追求平衡 → 可综合使用 F1@K、NDCG@K 等
6.3 NDCG@K:衡量排序质量的关键指标
NDCG(Normalized Discounted Cumulative Gain) 是推荐系统和信息检索领域中常用的排序评价指标,它不仅关注推荐列表中是否有用户感兴趣的项目,还考虑了这些项目出现的位置:相关物品越靠前,评价得分越高。
1)核心思想
推荐系统的目标不仅仅是把用户可能感兴趣的物品推荐出来,更重要的是将更相关的物品排在更前面。
对于用户喜欢的物品,排序越靠前越好; 如果用户喜欢的物品排在第 1 个,比分排在第 5 个高; 使用对数衰减的打分函数量化“靠前”的重要性。
2)数学定义
给定一个推荐列表的前 个结果(即 Top-K),我们有两个步骤:
1. 计算 DCG(Discounted Cumulative Gain):
DCG 衡量的是推荐结果的累积相关性得分,同时引入了“位置惩罚”机制(折损):
其中:
表示推荐列表中第 个位置的物品的相关性得分(通常是 0 或 1,也可以是 0-3 等等级评分); 分母的 表示位置折损函数,越靠后的位置,得分被折损得越多。
2. 归一化:计算 IDCG(Ideal DCG)
为了让指标在不同用户之间具有可比性,我们引入一个理想的 DCG,记为 IDCG,即在相关性得分最优排序情况下的 DCG:
3. 得到最终的 NDCG 值:
这样处理后,NDCG 的值始终在 之间,越接近 1 表示排序越好。
3)举个例子(简单 0/1 相关性)
假设用户的真实感兴趣物品为:
A、B、C(3 个相关物品)
推荐系统推荐的 Top-5 列表如下:
那么:
推荐结果的相关性列表为: [0, 1, 0, 1, 1]
计算 DCG@5:
理想排序(A, B, C 在前三位),对应的相关性列表为 [1, 1, 1, 0, 0],计算 IDCG@5:
因此:
4)小结
NDCG@K 衡量的不只是推荐列表是否命中用户兴趣,更重要的是这些“命中”是否出现在了用户最容易看到的位置:
考虑排序位置:比 Precision或Recall更合理;适用于非二值相关性:可以处理等级评分(如相关性为 0~3); 结果归一化,易于比较:便于跨用户、跨模型评估。
6.4 评估指标实现
1)Precision@K 和 Recall@K
Precision@K(精确率)衡量的是推荐列表中有多少比例的物品对用户来说是相关的。 Recall@K(召回率)衡量的是推荐列表中召回的相关物品占总相关物品的比例。
实现代码如下:
def precision_recall_at_k(top_n, testset, threshold=4.0):hits = defaultdict(int) # 命中的推荐数rec_total = defaultdict(int) # 总推荐数rel_total = defaultdict(int) # 总相关物品数# 统计每个用户的真实相关物品数for uid, iid, r in testset:if r >= threshold:rel_total[uid] += 1# 统计推荐命中的情况for uid in top_n:rec_items = {iid for iid, _ in top_n[uid]}test_items = {iid for u, iid, r in testset if u == uid and r >= threshold}hits[uid] = len(rec_items & test_items)rec_total[uid] = len(rec_items)# 计算均值precisions = []for u in top_n:if rec_total[u] > 0:precisions.append(hits[u] / rec_total[u])else:precisions.append(0.0) # 无推荐时 Precision 置零recalls = []for u in top_n:if rel_total[u] > 0:recalls.append(hits[u] / rel_total[u])else:recalls.append(0.0) # 无相关物品时 Recall 置零return np.mean(precisions), np.mean(recalls)
2)NDCG@K
NDCG(Normalized Discounted Cumulative Gain)是衡量推荐列表排序质量的一种指标,考虑了推荐列表中相关物品的位置,位置越前面的相关物品获得的权重越大。
实现代码如下:
def ndcg_at_k(top_n, testset, k=10, threshold=4.0):ndcg_scores = []# 构建测试集用户-物品映射user_test_items = defaultdict(set)for uid, iid, r in testset:if r >= threshold:user_test_items[uid].add(iid)for uid in top_n:# 获取推荐列表和真实相关物品ranked_list = [iid for iid, _ in top_n[uid][:k]]relevant = user_test_items.get(uid, set())# 计算DCGdcg = 0.0for i, iid in enumerate(ranked_list):if iid in relevant:dcg += 1.0 / np.log2(i + 2) # 索引从0开始,需+2# 计算IDCGidcg = sum(1.0 / np.log2(i + 2) for i in range(min(len(relevant), k)))ndcg = dcg / idcg if idcg > 0 else 0.0ndcg_scores.append(ndcg)return np.mean(ndcg_scores)
6.5 使用示例
在实际应用中,首先需要为每个用户生成 Top-N 推荐列表,然后计算评估指标(如 Precision@K、Recall@K 和 NDCG@K)。以下是如何计算这些指标的示例代码:
# 生成反测试集(用户未交互的物品)anti_testset = trainset.build_anti_testset()predictions = algo_biased.test(anti_testset)top_n = get_top_n(predictions, n=10)# 计算评估指标precision, recall = precision_recall_at_k(top_n, testset)ndcg = ndcg_at_k(top_n, testset, k=10)print(f'Precision@10: {precision:.4f}, Recall@10: {recall:.4f}, NDCG@10: {ndcg:.4f}')
6.6 总结
Precision@K 和 Recall@K 是评估推荐系统效果的两个重要指标,它们分别衡量推荐的准确性和覆盖度。NDCG@K 则进一步考虑了推荐列表中的物品顺序,能更好地反映推荐系统的排名质量。
在构建推荐系统时,建议根据实际业务目标综合使用多个评估指标进行建模与调参。
七、总结
推荐系统中的矩阵分解方法依然是构建高质量模型的基础。根据不同的需求和数据特点,选择合适的算法能够有效提升推荐系统的性能。通过引入偏置(BiasSVD)、隐式反馈(SVD++)与时间动态(TimeSVD++),能够进一步提升模型对用户行为的建模能力。实际应用中,需结合评估指标(如 RMSE、NDCG)和业务场景选择最适合的模型,并通过超参数调优获得最佳效果。
以下是不同算法的总结对比:
在实际应用中,矩阵分解方法提供了一种强有力的推荐模型框架,但需要根据数据特性、业务需求和计算资源做出合理选择。
- END -