简介:这是一套面向计算机相关专业毕业设计场景的协同过滤电影推荐系统完整资料,适合正在准备毕设、课程设计或期末大作业的学生,以及希望进行项目实战练习的学习者。资源以Python实现为核心,配套论文与项目说明,可直接作为毕设参考使用,项目经过严格调试,确保能够正常运行。压缩包共约2000个文件,整体约28.04MB,其中1159个py文件构成推荐算法与业务逻辑主体,324个pyc为编译缓存,另有124个html、40个js与16个css支撑前端页面,148个jpg与11个png提供界面素材,并包含csv、json、xml等数据与配置文件,以及pdf论文文档。目录结构清晰,便于按模块查阅源码与文档。目前已有147人学习下载,读者可从中获取协同过滤推荐流程的完整实现思路、论文写作参考、项目部署与调试经验,适合作为毕设模板或二次开发的学习起点。
1. 从零手写协同过滤:电影推荐系统在毕业设计里到底该怎么落地
很多同学做毕业设计选题时,看到“基于协同过滤的电影推荐系统”第一反应是去 GitHub 找一个能跑的仓库,改改界面就交差。但真正答辩时被问到“用户相似度怎么算的”“冷启动怎么处理”“为什么用皮尔逊而不是余弦”,往往答不上来。这篇笔记不讲空泛概念,而是把一套能跑通、能写进论文、能扛住答辩追问的 Python 实现路径完整拆开。核心围绕协同过滤这条主线,覆盖数据准备、相似度计算、评分预测、Top-N 推荐、评估指标和常见翻车点。适合正在做计算机毕业设计、需要一套可复现源码加论文素材的本科生,也适合想快速理解推荐系统底层逻辑的 Python 入门者。读完你应该能自己从零写出一个不依赖第三方推荐库的版本,并且知道每个参数为什么这样设。
2. 协同过滤的两条路线:UserCF 和 ItemCF 到底选哪个
2.1 原理差异与选型判断
协同过滤的核心思想很朴素:如果两个用户过去对电影的评分很相似,那他们未来对同一部电影的喜好也可能相似,这是 UserCF;如果两部电影被同一批用户打出相似的分数,那喜欢其中一部的人大概率也会喜欢另一部,这是 ItemCF。听起来只是换了个维度,但实际工程里差别很大。
UserCF 计算的是“用户之间的相似度”,推荐时找和目标用户最像的 K 个用户,把他们看过但目标用户没看过的电影推过来。它的优势是社交属性强,适合用户量不大、物品更新快的场景。缺点是用户数量一涨,用户相似度矩阵就是 O(n²) 的存储和计算,而且用户兴趣会漂移,今天喜欢文艺片明天可能只看动作片。
ItemCF 计算的是“物品之间的相似度”,推荐时找目标用户历史喜欢的电影,再找和这些电影最相似的其它电影。它的优势是物品相对稳定,相似度矩阵可以离线算好、定期更新,工业界用得更多。缺点是如果物品更新极快,相似度矩阵要频繁重算。
对于电影推荐这个场景,我一般会选 ItemCF 作为主方案。原因很直接:电影库相对稳定,用户评分行为稀疏,ItemCF 的相似度矩阵可以离线预计算,在线推荐时只需要查表加排序,响应快。论文里也可以把 UserCF 作为对比实验,体现你做了选型分析而不是随便挑一个。
提示:毕业设计论文里一定要写清楚为什么选 ItemCF,最好用一张表对比两种方案在复杂度、实时性、可解释性上的差异,答辩老师很吃这一套。
2.2 数据准备与评分矩阵构建
MovieLens 是最常用的公开数据集,常见做法是下载 ml-latest-small 版本,包含约 10 万条评分、600 个用户、9000 部电影。数据文件通常是 ratings.csv 和 movies.csv,前者字段是 userId、movieId、rating、timestamp,后者是 movieId、title、genres。
构建评分矩阵时不要一上来就用稠密矩阵,9000 部电影乘 600 个用户就是 540 万格,大部分是空的,内存浪费严重。用 pandas 的 pivot_table 生成用户-物品矩阵后,底层是稀疏存储更合理。下面是最小可运行的数据加载和矩阵构建代码:
import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 加载评分数据,注意分隔符和编码 ratings = pd.read_csv('ratings.csv') movies = pd.read_csv('movies.csv') # 构建用户-物品评分矩阵,缺失值填 0 表示未评分 user_item_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) # 转为稀疏矩阵,节省内存 sparse_matrix = csr_matrix(user_item_matrix.values) print(f"用户数: {user_item_matrix.shape[0]}, 电影数: {user_item_matrix.shape[1]}") print(f"稀疏度: {1 - ratings.shape[0] / (user_item_matrix.shape[0] * user_item_matrix.shape[1]):.4f}")这段代码的关键点有三个。第一,pivot_table默认会对重复的 userId-movieId 组合做聚合,如果数据里有重复评分,默认取均值,这通常是合理的。第二,fillna(0)把未评分填成 0,后面计算相似度时要记得把 0 当作缺失而不是真实评分,否则会把“没看过”误判成“打了 0 分”。第三,csr_matrix按行压缩存储,适合按用户切片的操作,如果后面要频繁按列取数,可以换成 csc_matrix。
参数方面,pivot_table的aggfunc默认是 mean,如果数据干净可以不改。稀疏度算出来一般在 0.98 以上,说明评分矩阵非常稀疏,这也是推荐系统要解决的核心难点之一。
2.3 ItemCF 相似度计算的三种实现
物品相似度是 ItemCF 的心脏。常见做法有余弦相似度、皮尔逊相关系数和调整余弦相似度。余弦相似度只看评分向量的方向,不考虑用户打分尺度差异;皮尔逊先去均值再算相关,能抵消一部分用户偏严或偏松的影响;调整余弦则是减去物品均分后再算余弦,对物品热度差异更鲁棒。
下面用余弦相似度做示例,基于稀疏矩阵按列计算物品之间的相似度:
from sklearn.metrics.pairwise import cosine_similarity # 转置后按物品计算相似度,注意稀疏矩阵转置 item_similarity = cosine_similarity(sparse_matrix.T) # 将对角线置零,避免物品和自己相似度为 1 干扰推荐 np.fill_diagonal(item_similarity, 0) # 只保留每个物品最相似的 topK 个,减少噪声 def keep_top_k(sim_matrix, k=20): result = np.zeros_like(sim_matrix) for i in range(sim_matrix.shape[0]): top_k_idx = np.argsort(sim_matrix[i])[-k:] result[i, top_k_idx] = sim_matrix[i, top_k_idx] return result item_similarity_topk = keep_top_k(item_similarity, k=20)逻辑说明:cosine_similarity接收的是物品-用户矩阵,所以要把原始的用户-物品矩阵转置。np.fill_diagonal把物品和自己的相似度设为 0,否则推荐时会把已经看过的电影又推一遍。keep_top_k只保留每个物品最相似的 20 个邻居,这一步在论文里可以解释为“降低噪声和计算量”。
参数 k 的选择有讲究。k 太小,相似度矩阵太稀疏,推荐覆盖率低;k 太大,会引入不相关的物品,降低准确率。常见做法是在验证集上试 10、20、30、50,画一条准确率随 k 变化的曲线,选拐点。我一般先用 20 跑通流程,再调。
如果要用皮尔逊相关系数,可以用numpy.corrcoef,但要注意它把 0 当作真实评分参与计算,需要先做掩码处理。更稳妥的方式是用scipy.stats.pearsonr逐对计算,但速度慢,适合小数据集验证。
3. 评分预测与 Top-N 推荐:从公式到可运行代码
3.1 预测评分的加权公式实现
有了物品相似度矩阵,预测用户 u 对物品 i 的评分,公式是:取用户 u 已评分的物品集合,对其中每个物品 j,用 sim(i, j) 乘以用户对 j 的评分,加权求和后除以相似度绝对值之和。这个公式的本质是“用相似物品的评分来估计目标物品的评分”。
def predict_rating(user_id, item_id, user_item_matrix, item_similarity): # 获取用户已评分的物品和对应分数 user_ratings = user_item_matrix.loc[user_id] rated_items = user_ratings[user_ratings > 0].index.tolist() if not rated_items: return 0 # 冷启动,返回全局均值或默认值 numerator = 0.0 denominator = 0.0 for rated_item in rated_items: # 获取两个物品在矩阵中的列索引 sim = item_similarity[item_id, rated_item] if sim <= 0: continue numerator += sim * user_ratings[rated_item] denominator += abs(sim) if denominator == 0: return user_ratings[rated_items].mean() return numerator / denominator这段代码里有两个容易翻车的地方。第一,item_similarity的索引是矩阵位置而不是 movieId,如果 DataFrame 的列顺序和相似度矩阵不一致,会取错值。稳妥做法是维护一个 movieId 到列索引的映射字典。第二,相似度可能为负,皮尔逊相关系数就会出现负值,这时候分母用绝对值求和,分子保留符号,才能正确表达“负相关物品的评分应该反向参考”。
参数方面,如果用户已评分物品很多,可以只取相似度最高的前 N 个参与加权,减少计算量。N 一般取 20 到 50。
3.2 生成 Top-N 推荐列表
预测评分只是中间步骤,最终要给用户一个排序好的推荐列表。做法是:对目标用户未评分的所有电影,逐个预测评分,然后按分数降序取前 N 个。
def recommend_top_n(user_id, user_item_matrix, item_similarity, n=10): user_ratings = user_item_matrix.loc[user_id] unrated_items = user_ratings[user_ratings == 0].index.tolist() predictions = [] for item_id in unrated_items: pred = predict_rating(user_id, item_id, user_item_matrix, item_similarity) predictions.append((item_id, pred)) # 按预测评分降序排序,取前 n 个 predictions.sort(key=lambda x: x[1], reverse=True) return predictions[:n]逻辑很直白,但性能是瓶颈。如果未评分电影有 8000 部,每部都要遍历用户看过的几百部电影,单用户推荐就要几十万次循环。优化思路有两个:一是把相似度矩阵用 numpy 向量化,一次性算出所有候选物品的分数;二是离线预计算好每个用户的推荐列表,在线只做查询。毕业设计数据量不大,用循环能跑通就行,但论文里可以提一句优化方向。
3.3 评估指标:准确率、召回率和覆盖率
推荐系统不能只看“推出来的电影好不好”,还要看“该推的有没有推出来”。常用指标有准确率(Precision)、召回率(Recall)、F1 和覆盖率(Coverage)。
做法是把每个用户评分最高的若干电影作为测试集,剩下的作为训练集。对测试集里的电影,如果推荐列表命中了,就算一次正确推荐。
def evaluate(train_matrix, test_matrix, item_similarity, n=10, threshold=4.0): precisions, recalls = [], [] for user_id in test_matrix.index: # 测试集中用户真正喜欢的电影 actual = test_matrix.loc[user_id] actual_liked = set(actual[actual >= threshold].index) if not actual_liked: continue # 生成推荐列表 recs = recommend_top_n(user_id, train_matrix, item_similarity, n) rec_items = set([item for item, _ in recs]) hit = len(rec_items & actual_liked) precisions.append(hit / n) recalls.append(hit / len(actual_liked)) return np.mean(precisions), np.mean(recalls)参数 threshold 取 4.0 表示评分 4 分及以上算“喜欢”,这是 MovieLens 上的常见做法。n 取 10 是 Top-10 推荐的标准设定。算出来的准确率一般在 0.1 到 0.3 之间,召回率在 0.05 到 0.2 之间,不同数据集和参数会有波动。如果准确率异常高,要检查是不是训练集和测试集有重叠。
4. 避坑与排查:协同过滤实现中最容易翻车的 5 个点
4.1 现象:推荐结果全是热门电影,冷门好片一个不推
原因:相似度计算时没有对热门物品做惩罚,热门电影被大量用户评分,和任何物品的相似度都偏高,导致推荐列表被头部内容霸占。
解决:在相似度公式里除以物品流行度的对数,或者用 TF-IDF 思想对评分矩阵加权。简单做法是给每个物品的相似度乘以1 / log(1 + 物品被评分次数),降低热门物品权重。
4.2 现象:新用户注册后推荐列表为空或全是默认值
原因:UserCF 和 ItemCF 都依赖历史行为,新用户没有任何评分,相似度无法计算,这就是冷启动。
解决:毕业设计里可以做一个简单的降级策略——新用户先推全局最高分的若干部电影,或者让用户勾选几个喜欢的类型,按类型推热门片。论文里把冷启动单独写一节,体现你考虑到了边界情况。
4.3 现象:预测评分出现大于 5 或小于 1 的异常值
原因:加权公式在相似度之和很小或出现负值时,除法结果会溢出正常评分区间。
解决:在返回预测值前做截断,max(1, min(5, pred))。同时检查相似度矩阵是否有 NaN,cosine_similarity在遇到全零向量时会返回 NaN,需要np.nan_to_num处理。
4.4 现象:训练时准确率很高,换一批数据就崩了
原因:过拟合。物品相似度矩阵在训练集上算得太细,topK 设得太大,把噪声也学进去了。
解决:减小 topK,增加相似度阈值过滤,或者用交叉验证选参数。论文里可以画一张 topK 从 5 到 100 的准确率曲线,说明你做了参数调优而不是拍脑袋。
4.5 现象:代码跑得动但内存爆了
原因:用稠密矩阵存储 9000 乘 600 的评分表,再加上相似度矩阵又是 9000 乘 9000,内存直接起飞。
解决:评分矩阵用scipy.sparse,相似度矩阵只保留 topK 个非零值,用稀疏格式存储。如果还不行,就分块计算相似度,每次只算一批物品。
5. 进阶技巧:用矩阵分解给协同过滤加一层保险
5.1 为什么要在 ItemCF 之外补一个矩阵分解
ItemCF 的可解释性强,但面对稀疏矩阵时相似度计算不稳定。矩阵分解(如 SVD、ALS)把用户和物品映射到低维隐向量空间,用向量内积逼近评分,对稀疏数据的鲁棒性更好。毕业设计里可以把矩阵分解作为改进方案,和 ItemCF 做对比实验,论文的“算法改进”章节就有了实打实的内容。
用surprise库可以快速跑通 SVD:
from surprise import SVD, Dataset, Reader from surprise.model_selection import cross_validate # 指定评分范围 reader = Reader(rating_scale=(0.5, 5.0)) data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader) # 使用 SVD 算法,n_factors 是隐向量维度 algo = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02) results = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)参数说明:n_factors是隐向量维度,50 是常用起点,太大容易过拟合,太小欠拟合;n_epochs是迭代轮数,20 到 50 之间;lr_all是学习率,0.005 比较稳;reg_all是正则化系数,防止参数过大。跑完看 RMSE,一般在 0.85 到 0.95 之间。
5.2 混合推荐的落地思路
把 ItemCF 和 SVD 的预测分数做加权融合,比如final_score = 0.4 * itemcf_score + 0.6 * svd_score,权重在验证集上调。这样既保留了 ItemCF 的可解释性,又借助矩阵分解提升了泛化能力。论文里可以画一张融合前后准确率对比柱状图,结论自然就有了。
5.3 一个我踩过的坑
最早做毕业设计时,我把所有代码写在一个 Jupyter Notebook 里,数据加载、相似度计算、推荐、评估全混在一起,改一个参数要重跑整个文件,调试一次等三分钟。后来拆成data_loader.py、similarity.py、recommender.py、evaluate.py四个模块,每个模块单独测试,效率翻倍。另外,相似度矩阵一定要缓存到本地,用numpy.save存成 npy 文件,下次直接加载,别每次都重算。这个习惯让我在答辩前一周改算法时省了大量时间。希望帮到你。
本文还有配套的精品资源,点击获取