ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

协同过滤算法与用户推荐矩阵:电影推荐系统课程设计核心解析

协同过滤算法与用户推荐矩阵:电影推荐系统课程设计核心解析

简介:这是一份基于协同过滤算法与用户推荐矩阵的电影推荐系统Python源码,适合计算机相关专业学生、教师及企业开发者用于课程设计、毕业设计或期末大作业,也便于初学推荐系统者对照学习协同过滤原理与实现流程。资源共10个文件,压缩包约21.55MB,主要包含经典电影评分与电影信息数据、中间计算结果、3个Python功能脚本(数据预处理、相似度计算、推荐生成)以及说明文档,整体目录结构清晰,可独立运行并输出推荐结果。已有163人浏览学习,项目代码经过测试可直接运行,不仅可用于快速搭建原型,还可在现有推荐矩阵基础上修改参数、换数据集或增加新的推荐策略。对于希望深入理解用户相似度计算、评分矩阵构建与实际推荐链路的学习者,这份资源提供了从数据清洗到生成推荐的完整参考。

1. 电影推荐系统课程设计:协同过滤算法与用户推荐矩阵到底在解决什么问题

课程设计里最容易被低估的项目,就是这种「名字很长、代码很多」的推荐系统。很多同学把协同过滤当成查字典:找几个和我相似的用户,把他们看过而我没看过的电影推给我。真上手才发现,评分矩阵怎么建、相似度算出来一堆 NaN、测试集和训练集混着用,每一步都能让结果静悄悄翻车。这个标题做的事其实很具体:读入用户对电影的历史评分,构建用户-物品评分矩阵,用协同过滤算法计算用户相似度或物品相似度,最终生成一份给指定用户的电影推荐列表。标题里说的用户推荐矩阵,指的就是从评分矩阵、相似度矩阵到预测矩阵这条完整链路。适合正在做课程设计、毕业设计,以及刚入行推荐算法、想找个最小可用项目练手的从业者。

2. 先把原理立住:协同过滤的两条路线与用户推荐矩阵的构造逻辑

2.1 基于用户与基于物品:两条路线怎么选

协同过滤算法按实现方式分两大类:基于记忆的 Memory-based 与基于模型的 Model-based。标题既然没写矩阵分解、没写神经网络,最常见、最符合课程设计预期的就是 Memory-based 协同过滤,而它又能拆成两条路线:User-based Collaborative Filtering 和 Item-based Collaborative Filtering。

User-based 的核心逻辑是「人以群分」:先找出和目标用户口味最接近的 k 个用户,把这 k 个邻居看过、而目标用户没看过的电影,按相似度加权打分,分数高的排前面推荐出去。Item-based 的核心逻辑是「物以类聚」:先算电影和电影之间的相似度,然后拿目标用户已经看过的电影去匹配它们的「近亲电影」,按用户的历史评分加权汇总。两条路线在实现上互为转置:一个沿着评分矩阵的行找邻居,另一个沿着列找邻居。

维度User-basedItem-based
相似度对象用户 vs 用户电影 vs 电影
适合场景用户少、物品多的社区型产品用户多、物品相对稳定的电商、视频站
实时性用户相似度变化快,需频繁重算物品相似度相对稳定,可离线算好缓存
冷启动新用户无行为,无法找邻居新电影无交互,无法算相似度
可解释性“和你口味相似的 A 也看过”“你看过 B,B 和 C 风格接近”

课程设计里用 ml-100k 数据集,943 个用户对 1682 部电影产生 10 万条评分,数据量两边都算得动,不存在性能抉择。我一般会建议两个都实现,答辩时对比展示两条路线的结果差异,比只做一条路线更能体现你对算法的理解。也可以用最简单的一句口诀来判断从哪条路线先入手:想看「推荐给谁」用 User-based 更直观,想看「为什么推荐这部」用 Item-based 更容易解释。

2.2 用户推荐矩阵:从评分矩阵到预测矩阵的三层结构

标题里的「用户推荐矩阵」不是一个黑匣子名词,落到代码里就是三层数据结构,理解了这三层,整个项目的骨架就出来了。

第一层是评分矩阵 Rating Matrix:行是用户,列是电影,值是 1 到 5 的评分,用户没看过的电影位置留空,也就是 NaN,绝不能填 0。第二层是相似度矩阵 Similarity Matrix:User-based 路线下它是一个 user × user 的方阵,第 i 行第 j 列表示用户 i 和用户 j 的相似度;Item-based 路线下则是 item × item 的方阵。第三层是预测矩阵 Prediction Matrix,它也叫做用户推荐矩阵——行是用户,列是候选电影,值是模型预测出的用户可能给出的评分,每一行取 Top-N 就得到推荐列表。

为什么非要用矩阵形式而不是直接查表?因为评分矩阵的每一行天然就是用户的向量表示,可以用 numpy 直接做向量点积;pandas 的 pivot_table 能把原始三元组(user, movie, rating)一次转成矩阵;后续如果要换 scipy.sparse 稀疏矩阵、要接矩阵分解模型,接口也不需要大改。矩阵还有一个隐藏好处:稀疏度可以直接算出来,数据稀疏到什么程度,决定了你该不该用杰卡德相似度、该不该调大最小共同评分下限。

注意一个细节:pivot_table 转出来的矩阵里,缺失值默认就是 NaN,这一步看似不起眼,却是后面所有相似度计算不出错的前提。很多翻车现场都是在这里用 fillna(0) 把缺失补成了 0,结果余弦相似度分母算错、预测评分全偏向低分,整个推荐列表变成热门电影榜。

2.3 相似度度量:余弦、皮尔逊、杰卡德的取舍与最小共同评分

相似度怎么算,直接决定邻居找得准不准。课程设计最常见的是余弦相似度,公式是向量点积除以模长乘积。但余弦有一个毛病:它对用户的评分习惯敏感。有人习惯给分 2 到 3,有人习惯给分 4 到 5,两个人品味一致,余弦算出来却不高。皮尔逊相关系数本质上是中心化后的余弦——先减掉各自的均值再算余弦,能抵消「手松手紧」的影响。杰卡德相似度只看交集比例,适合 0/1 的看过/没看过隐式反馈,但它丢掉了评分强度信息。

import numpy as np def cosine_sim(vec_a, vec_b, min_common=2): # 只取两边都有评分的维度,其余位置忽略 mask = ~np.isnan(vec_a) & ~np.isnan(vec_b) if mask.sum() < min_common: return 0.0 a, b = vec_a[mask], vec_b[mask] denom = np.linalg.norm(a) * np.linalg.norm(b) if denom == 0.0: return 0.0 return np.dot(a, b) / denom

这段代码里有三个关键点。mask 求的是两个向量共同评过分的维度,这是协同过滤「只比较重叠部分」的核心;min_common 是相似度计算的下限,默认 2,意思是两个用户至少共同看过 2 部电影才认为相似度有意义,否则直接返回 0,避免用一条共同评分就硬算相似度;分母为 0 时返回 0,防止除零得到 NaN。实际课程设计里,皮尔逊就是在这个基础上先减均值再调用 cosine_sim,代码可以复用同一套 mask 逻辑。至于用余弦还是皮尔逊,没有绝对优劣,建议把两个都实现,最后看测试集 RMSE 谁更低就用谁,这是血泪经验里最实用的判断方式。

3. 把数据灌进矩阵:MovieLens 读取、清洗与 user-item 评分矩阵构建

3.1 数据源选型:为什么课程设计首选 MovieLens 100K

电影推荐系统的标准数据集是 GroupLens 的 MovieLens 系列。常用版本有 ml-100k、ml-1m 和 ml-latest-small,课程设计我首选 ml-100k,理由很实际:它正好 10 万条评分、943 个用户、1682 部电影,文件只有几 MB,pandas 秒级加载,不需要分布式、不需要采样,而且自带按时间切分的 ua.base 和 ua.test 划分文件。ml-1m 数据量是它十倍,跑 User-based 的两两相似度会明显变慢;ml-latest-small 虽然量小,但电影 ID 更新到较新版本,老教程的字段处理方式不一定对得上。

ml-100k 里的 u.data 是一个 Tab 分隔的文本文件,四列分别是 user_id、movie_id、rating(1 到 5)、timestamp。u.item 存电影元数据,第一列是 movie_id,第二列是标题,后面是类型标签。下面这段代码把评分读进来,顺便看数据规模和缺失情况:

import pandas as pd # 列名一定要显式指定,u.data 文件里没有表头 df = pd.read_csv('ml-100k/u.data', sep='\t', names=['user_id', 'movie_id', 'rating', 'timestamp']) print(f"评分总数: {len(df)}") print(f"用户数: {df['user_id'].nunique()}") print(f"电影数: {df['movie_id'].nunique()}") print(f"评分分布:\n{df['rating'].value_counts().sort_index()}")

这段代码的作用是把原始评分文件变成标准的三元组 DataFrame。参数 sep='\t' 指定分隔符,names 手动给列起名,因为原始文件没有表头,不指定的话第一行数据会被当成列名导致解析错位。打印评分分布是为了检查数据质量:正常 100K 评分中 3、4 分的占比远高于 1、2 分,如果分布异常说明文件解析出错或数据源有问题。做这一步的时候顺便确认一下 Python 环境配好了没,pandas 和 numpy 装了没有,很多人卡在第一步不是代码问题,而是 python 安装 时没把 pip 路径加进环境变量。

3.2 用 pivot_table 构建评分矩阵:一行代码但有三个隐藏细节

评分矩阵的构建核心就一行 pivot_table,但这一行后面藏着三个课程设计高频踩坑点。先看代码:

import numpy as np import pandas as pd # 确保评分是数值类型,对同一对 user-movie 重复评分取均值 df['rating'] = pd.to_numeric(df['rating'], errors='coerce') matrix = df.pivot_table(index='user_id', columns='movie_id', values='rating', aggfunc='mean') # 打印矩阵尺寸和稀疏度 n_user, n_item = matrix.shape sparsity = 1 - matrix.notna().sum().sum() / (n_user * n_item) print(f"矩阵形状: {matrix.shape}") print(f"稀疏度: {sparsity:.2%}") print(f"内存占用: {matrix.memory_usage(deep=True).sum() / 1024:.1f} KB")

三个细节逐个说。第一,pivot_table 默认对重复索引取均值,如果原始数据里同一个用户对同一部电影有多条评分,aggfunc='mean' 会合并成一条,而 error='coerce' 能兜底非数值脏数据。第二,生成出来的矩阵行索引是 user_id,列索引是 movie_id,缺失值自动变成 NaN,这一步不要画蛇添足去 fillna(0),0 在推荐系统里代表「用户打了 0 分」而不是「没看过」,填进去会让后续余弦相似度把大量无效维度当成共同评分为 0。第三,稀疏度算出来应该在 93% 左右,这是 100K 数据集的正常水平——943 个用户乘 1682 部电影全满需要 158 万条评分,实际只有 10 万条。了解稀疏度不是为了炫技,而是为了决定后续相似度计算策略:这种稀疏度下,不可能存在两个用户把 1682 部电影全看过,所以 min_common 必须设得足够小,否则几乎找不到邻居。

3.3 切分训练集和测试集:先切分再建矩阵,顺序错了就是数据泄漏

推荐系统的评估和普通机器学习一样,需要把数据切成训练集和测试集。但这里有一个极为隐蔽的坑:很多人先把全量数据做成评分矩阵,再随机抽矩阵里的一些格子当测试集。这样训练矩阵里已经包含了测试格子对应的用户和物品信息,相似度计算阶段模型就已经「看过」测试评分了,测试指标虚高,答辩时老师一问就露馅。

正确做法是先切分原始三元组,再分别构建训练矩阵和测试矩阵:

from sklearn.model_selection import train_test_split # 按 80/20 随机切分,固定随机种子保证结果可复现 train_df, test_df = train_test_split(df, test_size=0.2, random_state=42) # 只用训练数据构建评分矩阵 train_matrix = train_df.pivot_table( index='user_id', columns='movie_id', values='rating', aggfunc='mean') test_matrix = test_df.pivot_table( index='user_id', columns='movie_id', values='rating', aggfunc='mean') print("训练集评分条数:", len(train_df)) print("测试集评分条数:", len(test_df))

train_test_split 里的 test_size=0.2 表示拿 20% 的评分当测试集,random_state=42 固定随机数种子,保证每次运行切分结果一致,这是课程设计报告里必须写清楚的参数。测试矩阵只用于评估,它的作用是:取出在测试集里出现、但在训练集里没有被预测过的 (user, movie) 对,把这些真实评分和模型预测值做对比。这样的评估流程才是可信的。另外有一种更贴近真实场景的切分是按时间:对 timestamp 排序,取前 80% 当训练、后 20% 当测试,模拟「用过去预测未来」的推荐场景。课程设计用随机切分就够,如果想在报告里多写一笔,可以两种都做对比。

4. 核心代码实现:基于用户的协同过滤与基于物品的协同过滤

4.1 基于用户的协同过滤:找邻居、加权评分、排序推荐

User-based 协同过滤的完整实现可以拆成三步:对每个候选电影找目标用户的 k 个相似邻居、用邻居评分做相似度加权平均、对所有候选电影分数排序取 Top-N。下面这段代码是可独立运行的完整版,建议直接存成 cf_user.py:

import numpy as np import pandas as pd def cosine_sim(vec_a, vec_b, min_common=2): mask = ~np.isnan(vec_a) & ~np.isnan(vec_b) if mask.sum() < min_common: return 0.0 a, b = vec_a[mask], vec_b[mask] denom = np.linalg.norm(a) * np.linalg.norm(b) if denom == 0.0: return 0.0 return np.dot(a, b) / denom def find_similar_users(user_id, matrix, k=10, min_common=2): target = matrix.loc[user_id].values sims = [] for other_id in matrix.index: if other_id == user_id: continue other = matrix.loc[other_id].values sim = cosine_sim(target, other, min_common) sims.append((other_id, sim)) # 相似度从高到低排序,只保留正相似邻居 sims.sort(key=lambda x: x[1], reverse=True) return [item for item in sims if item[1] > 0][:k] def predict_user_based(user_id, movie_id, matrix, k=10, min_common=2): if user_id not in matrix.index or movie_id not in matrix.columns: return np.nan watched = matrix.loc[user_id, movie_id] if not np.isnan(watched): return watched neighbors = find_similar_users(user_id, matrix, k, min_common) numerator, denominator = 0.0, 0.0 for neighbor_id, sim in neighbors: rating = matrix.loc[neighbor_id, movie_id] if np.isnan(rating): continue # 邻居没看过这部电影就不参与加权 numerator += sim * rating denominator += sim # 邻居全没看过时回退到该电影全局平均分 if denominator == 0.0: return np.nanmean(matrix[movie_id]) return numerator / denominator def recommend_for_user(user_id, matrix, k=10, top_n=10): if user_id not in matrix.index: return None watched = matrix.loc[user_id].dropna().index candidates = [mid for mid in matrix.columns if mid not in watched] scores = {} for movie_id in candidates: pred = predict_user_based(user_id, movie_id, matrix, k) if not np.isnan(pred): scores[movie_id] = pred ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return ranked

代码逻辑的关键在于:find_similar_users 里对每个用户做一次余弦相似度,只保留相似度大于 0 的邻居,再取前 k 个,避免负相似度邻居把预测分数拉低。predict_user_based 里先判断目标用户是否已经看过这部电影,看过就直接返回真实评分而不是再预测一次。加权平均部分,denominator 累计的是邻居相似度之和,等于给相似度高的邻居更大话语权,这是 User-based 最简单的评分聚合方式。最后回退到全局平均分是为了兜底——候选电影的邻居可能恰好全都没看过,这时候返回 NaN 会导致推荐列表缺项,回退后至少能保住召回。

参数方面,k 是邻居数,课程设计常用 10 到 20,k 太小只受少数几个用户影响、方差大,k 太大把相似度低的人拉进来、精度下降;min_common 是最小共同评分数量,100K 数据下 2 到 5 均可。运行一次完整预测需要遍历所有候选电影,100K 数据下大概几十秒,可接受。

4.2 基于物品的协同过滤:换个方向做相似度计算

Item-based 与 User-based 的差别在于:相似度对象从用户行变成电影列,邻居集合从相似用户变成目标用户已经看过电影的各路相似电影。还有一点细节,Item-based 推荐通常用「调整后的余弦相似度」——计算电影相似度时先把每个用户的评分减去该用户自己的平均分,消除用户评分习惯偏差。核心实现如下:

def adjusted_cosine_sim(col_a, col_b, user_means, min_common=2): # 减去用户均值后再算余弦,消除用户打分习惯差异 a = col_a - user_means b = col_b - user_means mask = ~np.isnan(a) & ~np.isnan(b) if mask.sum() < min_common: return 0.0 a, b = a[mask], b[mask] denom = np.linalg.norm(a) * np.linalg.norm(b) if denom == 0.0: return 0.0 return np.dot(a, b) / denom def predict_item_based(user_id, movie_id, matrix, k=10, min_common=2): if user_id not in matrix.index or movie_id not in matrix.columns: return np.nan if not np.isnan(matrix.loc[user_id, movie_id]): return matrix.loc[user_id, movie_id] # 用户已看过的电影及评分,是 Item-based 的“历史素材” user_ratings = matrix.loc[user_id].dropna() if len(user_ratings) == 0: return np.nan user_means = matrix.loc[user_id].mean(skipna=True) target_col = matrix[movie_id].values scores = [] for other_id, rating in user_ratings.items(): if other_id == movie_id: continue other_col = matrix[other_id].values sim = adjusted_cosine_sim(target_col, other_col, matrix.mean(axis=1, skipna=True).values, min_common) scores.append((sim, rating)) scores.sort(key=lambda x: x[0], reverse=True) neighbors = [s for s in scores if s[0] > 0][:k] if not neighbors: return np.nanmean(matrix[movie_id]) numerator = sum(sim * rating for sim, rating in neighbors) denominator = sum(sim for sim, _ in neighbors) if denominator == 0: return np.nanmean(matrix[movie_id]) return numerator / denominator

实现里的核心差异点要重点解释。adjusted_cosine_sim 接收三个参数:两列电影评分向量和每个用户的平均分,它先把评分数值减去对应用户的平均分再做余弦,这一步是 Item-based 精度比朴素余弦高的关键。predict_item_based 里对目标用户只遍历 ta 已经看过的电影,每一部电影根据相似度加权贡献一个预测分数,权重是电影相似度,值是该用户给那部电影的评分。这等价于在说:你很喜欢 A 电影,而 B 电影和 A 很像,那预测你也会喜欢 B。

从工程角度看,Item-based 有一个天然优势:电影相似度矩阵可以离线一次性算好存起来,线上推荐时直接查表,而 User-based 的用户相似度会因为新评分不断变化需要频繁重算。课程设计可以提前算好物品相似度矩阵,再写一个函数直接取相似度,体验一下两种架构的效率差别。

4.3 参数设置:k 值、min_common、相似度阈值到底怎么调

参数建议范围100K 课设推荐值调小的影响调大的影响
k(邻居数)5 到 5010 或 20方差大,结果依赖个别用户混入低相似度噪声,精度下降
min_common2 到 102 或 5相似度计算脆弱,易出错覆盖率下降,很多用户找不到邻居
相似度阈值0 到 0.20(保留全部正相似)邻居太少覆盖率下降
top_n5 到 2010推荐列表太短推荐列表尾部精度下降

调参的方法是固定其他参数、单变量扫一遍。比如 k 分别取 5、10、15、20、30,看测试集 RMSE 的变化曲线,课程设计报告里画一条折线图,比空谈「k 取 10 效果最好」有说服力得多。min_common 的调法类似:用 1 到 10 扫一遍,观察相似度有效率和覆盖率的变化,min_common=1 会让两个用户只共同看过一部电影就强算相似度,结果极不可靠;min_common 过大又会让新用户、冷门用户彻底没有邻居。相似度阈值我这里建议直接设 0,原因很简单:负相似度邻居的评分即使加进去也是反向拉低预测值,直接剔除比调阈值简单,而且省掉了「阈值取 0.05 还是 0.1」的玄学争论。

5. 协同过滤避坑指南:五处让推荐结果悄悄翻车的细节

5.1 测试集评分被 fillna(0) 污染,RMSE 虚低还查不出来

现象:预测函数跑完,RMSE 只有 0.7 左右,看起来效果极好,但推荐列表全是热门电影,答辩时一问细节就心虚。

原因:构建矩阵时用了 fillna(0),把「没评分」和「评了 0 分」混为一谈。余弦相似度计算时,大量空白位置被当成 0 参与点积,等于人为制造了稀疏维度上的伪相似度;更致命的是预测函数里把没评过的格子也当成「评了 0 分」的真值,测试集评估误差被大幅稀释。

解决:矩阵里保留 NaN,相似度计算用 mask 跳过无评分维度;预测评分时只对真正存在评分的测试条目计算误差。

5.2 两个用户没有共同看过的电影,余弦相似度直接除零

现象:某些用户之间相似度算出 NaN,邻居列表为空,推荐结果回退成全局热门榜,冷门用户的结果完全没个性。

原因:余弦相似度公式里分母是两个向量的模长乘积,而两个用户没有任何共同评分时,mask 过滤后两个向量全是 NaN,np.linalg.norm 对全 NaN 向量返回 NaN,继续运算把 NaN 传染给整个相似度矩阵。

解决:在余弦函数开头判断mask.sum() < min_common就直接返回 0.0,从源头阻断 NaN 的产生;同时分母判断是否为 0。这两个保护缺一不可,前者保证「没有足够重叠就不认邻居」,后者防止模长恰好为 0 的边界。

5.3 训练集和测试集没分开,数据泄漏让评估分数成摆设

现象:测试集 RMSE 特别漂亮,但随机抽几部电影看推荐原因,发现推荐的电影和目标用户已经在测试集里评过 1 分的电影高度重合。

原因:先做完整矩阵再切分,训练阶段计算用户相似度时已经看到了测试集评分,模型相当于带着答案进考场。

解决:严格按「先切分原始三元组 → 训练矩阵和测试矩阵各自 pivot → 用训练矩阵做预测 → 和测试矩阵比对」的顺序执行。用 sklearn 的 train_test_split 或按时间戳排序切分都行,关键是 pivot 动作必须在切分之后。

5.4 新用户和新电影在矩阵里没有位置,predict 直接 KeyError

现象:把推荐函数接到 Web 后端或新数据上时,传入一个刚注册的用户 ID,程序直接抛 KeyError,接口崩溃。

原因:训练矩阵是固定的,不在矩阵行索引里的新用户、不在列索引里的新电影,用 .loc 取值时自然报错。

解决:预测函数开头加守卫if user_id not in matrix.index or movie_id not in matrix.columns: return np.nan,上层推荐函数对返回 NaN 的候选电影做兜底——回退到该电影全局平均分,或者直接推荐热门电影。冷启动在工业界也是难题,课程设计阶段能用回退策略处理就算合格,不要硬套相似度算法。

5.5 评分矩阵内存爆炸,稀疏存储能救回一个数量级

现象:换用更大数据集后,pivot_table 构建矩阵时进程直接被系统杀死,或者把相似度矩阵存成 CSV 后文件大得打不开。

原因:pivot_table 默认生成稠密 DataFrame,ml-100k 是 943×1682 才十几 MB,扩大到 ml-1m 就是几千乘几千,稠密存储开销随用户数和电影数乘积增长,而实际评分占比通常不到 5%。

解决:用 scipy.sparse 的 csr_matrix 保存评分矩阵,只存非零评分。稀疏矩阵有两个好处,一是内存占用从 O(m×n) 降到 O(非零评分数量);二是矩阵乘法可以直接调用稀疏矩阵的 dot 方法,一次算完全部用户相似度,比 for 循环两两计算快几十倍。课程设计用好这个,报告能多写一节「工程化优化」。

6. 离线评估与进阶调优:把课设从能跑变成能用

课程设计交一个能跑的代码只是及格线,真正拉开差距的是评估方法和进阶优化。先看评估指标怎么落到代码里。评分预测类任务用 RMSE,推荐列表任务用 Precision@K 和 Recall@K,两个维度都要覆盖:

from sklearn.metrics import mean_squared_error def evaluate_rmse(test_df, train_matrix, predict_func, k=10): actuals, preds = [], [] for row in test_df.itertuples(): pred = predict_func(row.user_id, row.movie_id, train_matrix, k) if not np.isnan(pred): actuals.append(row.rating) preds.append(pred) return np.sqrt(mean_squared_error(actuals, preds))

这段代码只对测试集里能成功预测出的条目统计误差,预测为 NaN 的样本直接跳过,因为那是覆盖不到的死角。RMSE 大概在 0.9 到 1.1 之间属于正常范围,用随机猜测模拟的基线是 1.5 以上,低于 0.9 要警惕是不是发生了数据泄漏。

再进一步,用留一法验证推荐列表:对每个用户隐藏一条真实评分,用剩下数据训练,看 Top-K 推荐列表里有没有把隐藏那条电影推回来,命中比例就是 Hit Rate。这个方法比 RMSE 更贴近真实使用感受——用户关心的不是预测分准不准,而是推荐列表里有没有想看的片子。课程设计里做一张 k 从 5 到 30 的 Hit Rate 折线图,配合 RMSE 曲线一起分析,就能看出 k 太大虽然覆盖率上去了,但列表精度会掉。

进阶方向有两个值得写进报告:一是引入矩阵分解做 baseline 对比,用 surprise 库的 SVD 跑一遍同样数据,和协同过滤的 RMSE 对比,这是答辩时展示「我知道两种流派差异」的最好证据;二是把物品相似度矩阵提前算好、序列化缓存到磁盘,线上推荐时直接查表,这一步能让推荐速度从秒级降到毫秒级,属于工程上性价比最高的优化。我自己当年做课设时也犯过先建全量矩阵再切分的错,测试分数好看得离谱,答辩老师一句话就问穿了;后来老老实实按先切分再建矩阵的流程重跑,分数真实了,但报告里多了一份「对数据泄漏的分析」,反而成了加分项。希望这份实现思路和踩坑记录能帮你把课设做得既跑得通、也讲得清。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进