简介:基于协同过滤的电影推荐系统Python实现源码和配套论文,是一套98分的本科毕业设计实战项目,面向计算机相关专业正在准备毕设的学生,也适合作为课程设计或期末大作业。资源包内含完整的推荐系统代码、项目说明文档与论文材料,重点解决从数据预处理、相似度计算到电影推荐展示的完整流程。压缩包共2000个文件,大小约28MB,以Python源码与编译文件为主,并包含HTML页面、图片素材、CSS样式、CSV数据文件、多语言配置等类型,覆盖前端展示、后端逻辑、数据存储与国际化支持,目录结构较完整,便于按模块阅读和部署。已有147人学习下载。对于毕业设计使用者而言,可直接运行调试,并参考其中的说明文档理解协同过滤算法在电影推荐场景中的落地方式;通过前端页面可观察推荐结果展示,借助CSV数据文件可快速替换测试数据,为后续功能扩展和论文撰写提供基础。
1. 协同过滤电影推荐系统:Python毕设源码包拆解
先说结论:这套基于协同过滤的电影推荐系统源码包,是毕设题里少有的「数据、算法、系统、论文」四个维度都能铺开的组合。内容包含完整可运行的Python项目源码和配套论文说明,数据层基于MovieLens公开电影评分数据,算法层覆盖了UserCF和ItemCF两条主流协同过滤路线,展示层用Flask/Django配合Bootstrap做了可交互的Web界面,读者既能跑出真实推荐结果,也有材料支撑论文里的系统设计、算法对比、实验评估等章节。适合正在准备Python方向毕业设计、课程设计、期末大作业的计算机专业学生,也适合想从零搭一个推荐系统练手的学习者。我先后拆过十几套同类毕设项目,最典型的翻车点其实不在算法,而在评分数据预处理和评估指标的统计口径——这两块恰恰是答辩时老师最爱追问的地方。
2. 协同过滤的两个流派:选型之前先想清楚
2.1 User-Based CF:找人,再找 ta 看过的
基于用户的协同过滤,核心思路是「物以类聚、人以群分」。系统先计算用户与用户之间的相似度,对一个待推荐用户,找到和他口味最接近的一群邻居,再把这些邻居看过、而目标用户没看过的电影按相似度加权汇总,生成推荐列表。它的优点是足够直观,论文里容易画图表述,缺点也明显:用户规模一大,两两相似度计算量按用户数的平方增长,在线推荐响应会越来越慢。
python
用户相似度计算(皮尔逊相关系数)
import pandas as pd import numpy as np
def user_similarity(ratings_matrix): """计算用户间皮尔逊相关系数,返回用户相似度矩阵
ratings_matrix: 行是用户索引, 列是电影, 值是评分 返回值的 shape 是 (用户数, 用户数) """ # 中心化:每个用户减去自己的平均评分 # 这样能抵消"给分普遍偏高"和"给分普遍偏低"两类用户的尺度差异 rated_means = ratings_matrix.mean(axis=1) centered = ratings_matrix.sub(rated_means, axis=0) # 中心化后的矩阵做内积,得到协方差矩阵 cov_matrix = centered.dot(centered.T) # 计算每个用户的模长(即分数波动幅度) user_norm = np.sqrt(np.sum(centered ** 2, axis=1)) denom = user_norm.values.reshape(-1, 1) * user_norm.values.reshape(1, -1) # 用协方差除以模长乘积,得到皮尔逊相关系数 similarity = cov_matrix / denom # 两个用户没有共同评分时,分母为0,直接置0 similarity[denom == 0] = 0 return similarity这段代码里最容易阴沟翻船的是中心化那一步。原始评分数据存在明显的个人偏好:有人习惯打分偏低、有人常年给高分,如果直接用原始分数算余弦相似度,两个口味完全一致的用户可能因为打分尺度不同而相似度很低。减去自身均值后再算,比较的是评分「走势」而不是「绝对值」,皮尔逊系数才能真正反映口味接近程度。开发环境里建议先验证:随机抽两个用户,手动算一遍结果,再和矩阵计算结果对比,确认数字一致后再往下走。
2.2 Item-Based CF:找电影,再找相似的电影
基于物品的协同过滤思路反过来,计算电影与电影之间的相似度,再给用户推荐「和他看过的电影相似」的片子。这么做有个天然的工程优势:电影数量通常远小于用户数量,几千部电影两两算相似度,比几万用户两两算要便宜得多;且电影相似度可以离线算好、定期更新,用户产生新评分时直接查表,在线计算压力小一个量级。
python def item_similarity(ratings_matrix): """计算电影间相似度,返回物品相似度矩阵
转置后行是电影、列是用户,每个单元格代表某用户对该电影的评分 返回值的 shape 是 (电影数, 电影数) """ item_matrix = ratings_matrix.T # 计算每部电影的评分向量模长 norm = np.sqrt(np.sum(item_matrix ** 2, axis=1)) denom = norm.values.reshape(-1, 1) * norm.values.reshape(1, -1) # 电影间内积除以模长乘积,即余弦相似度 similarity = item_matrix.dot(item_matrix.T) / denom similarity[denom == 0] = 0 return similarityItemCF 在电影推荐场景的实战表现通常优于 UserCF,原因不只是性能。用户的兴趣会随时间漂移——昨天爱看科幻片,今天可能转投悬疑剧;而电影本身的「属性」变化很慢,纪录片永远比战争片更接近其他纪录片。所以基于电影的相似度推荐,结果更稳定,也更容易解释:系统告诉用户「因为你喜欢《盗梦空间》,所以推荐《星际穿越》」,这个逻辑用户能看懂,答辩时也讲得清晰。不过 ItemCF 的弱点同样要记住:它擅长发现「相似」的东西,却很难发现「不同但相关」的东西,完全依赖它做推荐,容易出现推荐列表同质化。
2.3 相似度度量选型:余弦、皮尔逊、调整余弦
相似度度量是协同过滤里最容易被低估的决策点。不同的度量方式对同一份数据产出的相似度矩阵差异很大,直接决定了推荐质量。我在拆过的项目里见过三种主流方案,放在一起对比会看得更清楚:
| 度量方法 | 计算思路 | 推荐使用场景 | 主要风险 |
|---|---|---|---|
| 余弦相似度 | 向量夹角余弦 | 评分数据稀疏、只关心方向 | 未消除用户评分尺度差异 |
| 皮尔逊相关系数 | 中心化后余弦 | 用户评分习惯差异明显 | 共同评分过少时数值不稳定 |
| 调整余弦 | 按用户均值中心化 | ItemCF 的相似度计算 | 不理解负评分的含义会算错 |
毕设里我建议 UserCF 用皮尔逊、ItemCF 用调整余弦。调整余弦的做法是:算电影相似度前,先把每个用户的所有评分减去该用户自己的平均分,得到的矩阵再去算电影间余弦。注意处理后会出现负值,这个词义是「该用户对这部电影的喜爱低于他自己的平均水平」,在后续加权汇总时负值是有意义的,逻辑上必须正确处理,不能顺手取绝对值。
2.4 UserCF 与 ItemCF 的工程取舍:复杂度、时效与冷启动
选哪条路线,不能只看论文里哪个写起来好讲,要看你的部署条件和数据形态。用户数几千、电影数几百,UserCF 跑得动,结果也还不错;用户数上万、电影数几千,UserCF 每次推荐实时算相似度基本扛不住,ItemCF 的「离线算物品相似度 + 在线查表」才是合理方案。冷启动场景两者各有应对思路:新用户刚注册没有评分,任何协同过滤都失效,这时需要系统提供一个「热门推荐」兜底接口,先吸引用户产生评分,再逐步切到个性化推荐;新电影上线没有评分,UserCF 还能靠用户行为曲线兜一兜,ItemCF 就彻底无能为力。项目源码里如果这两个兜底逻辑都有,说明作者考虑过完整链路,论文里写系统设计时可以直接引用。
3. 源码包里的三层结构:从数据到推荐结果
3.1 打开源码包先看什么
源码包拿到手,先别急着跑主程序,花十分钟把目录结构摸一遍,能省下后面大量的调试时间。我拆过的项目呈现出来的结构一般是:数据预处理独立成一个模块或脚本,算法模型单独成文件,Web 接口与前端页面再拆一层。源码包里出现了 bootstrap.css、base.css、widgets.css 这一组样式文件,说明系统是有前端页面的,不是一个黑乎乎的终端输出——通常是 Flask 或 Django 工程,静态文件放在 static 目录下。
优先确认三件事:第一,有没有 requirements.txt 或 Pipfile,这是依赖清单,直接决定你能不能一次性把环境搭起来;第二,数据集文件是否完整,ratings.csv、movies.csv 这类原始数据缺了后面全是白费;第三,入口文件是 manage.py(Django 风格)还是 app.py(Flask 风格),找对入口才能启动。源码包描述里写了「经过严格调试,确保可以运行」,我实际拆包时会先按这三步快速验证,因为「能跑」和「在你的机器上能跑」之间,隔着一个依赖版本地狱。
3.2 数据预处理:评分矩阵才是算法的命根子
协同过滤的算法吃的是矩阵,不是 CSV。原始数据长这样:userId、movieId、rating、timestamp 四列。预处理阶段要做的事是过滤异常用户、把长表转成宽表、处理缺失值。
python import pandas as pd
读取原始评分数据
ratings = pd.read_csv('data/ml-latest-small/ratings.csv')
只保留算法需要的三个字段,timestamp 在纯协同过滤里用不到
ratings = ratings[['userId', 'movieId', 'rating']]
过滤掉评分总数少于20条的用户
评分过少的用户本身就是"冷启动用户",放进训练集只会增加噪声
user_counts = ratings['userId'].value_counts() valid_users = user_counts[user_counts >= 20].index ratings = ratings[ratings['userId'].isin(valid_users)]
把长表转成宽表:行=用户,列=电影,值=评分
没有评分的单元格是 NaN,算法里统一用 0 补位
rating_matrix = ratings.pivot(index='userId', columns='movieId', values='rating') rating_matrix = rating_matrix.fillna(0)
确认矩阵形状,心里有个底
print('评分矩阵形状:', rating_matrix.shape)
这个预处理的坑在于 fillna(0) 的语义。MovieLens 原始数据里不存在 0 分,最低评分是 0.5 分,所以 0 在这里只代表「缺失」,不代表「用户打了零分」。如果后面用了自己采集的数据,且数据里真的有 0 分,那就必须重新设计缺失值策略,否则「没看过」和「看过且打了 0 分」会被混为一谈。论文里建议专门用一小节描述这个处理,写上「0 代表缺失评分」,这句话在答辩时经常能挡掉一个刁钻问题。
3.3 UserCF 推荐函数:邻居选取与分数聚合
相似度矩阵算完后,推荐逻辑分三步:挑邻居、按权重聚合分数、排序取 TopN。新手最容易倒在第二步——直接把邻居的评分加起来排序,完全不用相似度加权,结果推荐列表里全是「大众高分片」,个性化等于零。
python def recommend_for_user(user_id, rating_matrix, user_sim, k=10, top_n=20): """给指定用户做 TopN 推荐
user_id: 目标用户 ID rating_matrix: 用户-电影评分矩阵 user_sim: 用户相似度矩阵 k: 只取相似度最高的前 k 个邻居 top_n: 最终返回的电影数量 """ user_ratings = rating_matrix.loc[user_id] # 找出当前用户没看过的电影(评分为0的) unseen_mask = user_ratings == 0 unseen_items = user_ratings[unseen_mask].index # 按相似度排序,排除自己,取前 k 个邻居 sim_scores = user_sim[user_id].sort_values(ascending=False) neighbors = sim_scores[sim_scores.index != user_id].head(k) neighbors = neighbors[neighbors > 0.05] # 丢弃相似度低于阈值的 # 所有没看过的电影,按邻居评分 × 相似度加权求和 scores = {} for movie in unseen_items: score = 0 for neighbor_id, sim in neighbors.items(): rating = rating_matrix.loc[neighbor_id, movie] if rating > 0: score += sim * rating scores[movie] = score # 按分数降序取 TopN recommendations = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return recommendations这段代码里我特别加了相似度阈值过滤:邻居的相似度如果只有 0.02,它对推荐的贡献趋近于零,拉进来只会增加计算量、稀释结果。阈值的取值需要按实际数据调节,后面调优章节细说。另一个参数 k 也很关键,k 太小视野太窄,k 太大把差异很大的用户也当成邻居,推荐品味被拉平,一般从 5 开始按 5 的间隔试。
3.4 ItemCF 推荐函数:离线算相似度,线上直接查
ItemCF 的推荐逻辑比 UserCF 轻快得多,因为电影相似度表是提前算好并持久化的,用户发起请求时只做「按已评电影查相似电影」这件事。
python def recommend_based_on_item(user_id, rating_matrix, item_sim, top_n=20): """基于物品相似度的推荐
item_sim: 提前算好的电影间相似度矩阵 逻辑: 取用户最近评过分的电影,找它们的相似电影并加权累加 """ user_ratings = rating_matrix.loc[user_id] # 只保留用户真正评过分的电影(评分>0) rated_items = user_ratings[user_ratings > 0] scores = {} for item_id, rating in rated_items.items(): # 取与当前电影最相似的10部电影 sim_scores = item_sim[item_id].sort_values(ascending=False).head(10) for similar_item, sim in sim_scores.items(): if similar_item in rated_items: continue # 跳过用户已经看过的电影 if similar_item not in scores: scores[similar_item] = 0 # 权重 = 用户对原电影的评分 × 电影间相似度 scores[similar_item] += rating * sim recommendations = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return recommendations这个函数里隐藏着一个细节:用户评过的所有电影都会贡献「候选电影」,一部电影可能从多部已看过的电影的相似列表里被多次命中,分数累加,所以最终排序依据的是「累计加权分」而不仅是单次相似度。这个设计让推荐结果更稳,也更容易在论文里解释清楚。线上运行时,item_sim 应该以 npz 或 parquet 格式持久化到本地,而不是每次启动服务都重新算一遍。
3.5 前端展示层:Bootstrap 模板与评分交互
选一个合适的展示方案,推荐系统的「完成度」直接上一个档次。源码包里那组 CSS 文件(bootstrap.css、widgets.css、forms.css 等)说明作者用了 Bootstrap 作为前端基础框架,页面结构一般是:首页展示热门电影,用户点击某部电影进入详情页,详情页提供 1-5 分的评分入口,评分提交后立即触发推荐刷新,在一侧展示「因为你看过 X,所以推荐 Y」的推荐列表。完整的前后端拉通逻辑,是答辩现场最有说服力的演示素材——只跑命令行输出推荐结果,和打开浏览器给老师看可交互的页面,观感天差地别。
4. 数据集与参数调优:从能跑到跑得好看
4.1 MovieLens 数据集选哪个版本
这套源码包配套使用的绝大多数是 MovieLens 数据集。ml-latest-small 是 600 个用户、约 9000 部电影、10 万条评分的轻量级数据,单机跑起来非常快,适合快速验证算法流程;ml-20m 是 2000 万条评分的完整数据集,适合做论文里的性能对比实验,但对内存和计算时间的要求明显提升,选之前先看看自己的机器——评分矩阵在 Python 里用 float64 存储,2000 万条评分转成稀疏矩阵后内存占用依然可观,处理时要考虑分批或使用 SciPy 的稀疏矩阵类型。
一个常见的低级错误是拿错文件:有人直接把 movies.csv 当成评分数据去做 pivot,结果出来的矩阵全是 NaN。拿到任何数据,先 head 一眼列名再动手,30 秒的检查能省下一个小时的 debug。数据集的完整路径结构通常是 data/ 目录下按数据源分文件夹,源码包里如果已经内置了一份 ml-latest-small,可以直接用;没有的话去官方源下载即可,文件不大,解压后就是标准的 CSV。
4.2 邻居数 k 与相似度阈值的调法
k 是协同过滤里最核心的超参数,决定推荐结果的「视野宽度」。k 太小时,只参考几个铁杆邻居,推荐结果个性化强但覆盖面窄,容易翻来覆去推同一批片子;k 太大时,大量相似度偏低的用户也被拉进邻居群,推荐品味被稀释,整体效果反而下降。调参的目标是找到这两者之间的拐点,常见做法是在训练集和验证集上做小规模网格搜索。
python from sklearn.model_selection import train_test_split
按用户分层切分,避免同一用户评分被拆到训练集和测试集两边
train_data, test_data = train_test_split( ratings, test_size=0.2, stratify=ratings['userId'] )
k_values = [5, 10, 15, 20, 30, 50] results = {} for k in k_values: # 每次都用同一份训练集重建相似度矩阵 train_matrix = build_matrix(train_data) item_sim = item_similarity(train_matrix) # 在验证集上评估 TopN 推荐命中率 hit_ratio = evaluate_topn(item_sim, train_matrix, test_data, k=k) results[k] = hit_ratio print(f'k={k}: hit@10 = {hit_ratio:.4f}')
打印全部结果,选指标最高对应的 k
print(results)
调参时的严肃红线是信息泄漏:训练集、验证集必须在用户粒度上完全隔离。如果随机切分一行行评分,同一个用户的评分会同时出现在两边,模型相当于提前见过答案,评估指标虚高得非常离谱,答辩时被问一句「你的验证集怎么划分的」就当场暴露。stratify 按用户分层只是第一步,更稳妥的是用 GroupKFold,后面评估章节会给出完整写法。
4.3 稀疏度、冷启动与热门兜底
MovieLens 的评分矩阵稀疏度通常超过 95%,意味着矩阵里绝大多数格子是空的。稀疏度越高,两个用户共同评过的电影越少,皮尔逊系数的分母越不稳定,算出来的相似度噪声越大。这不是调参能根治的问题,得靠策略配合,常见做法是两条腿走路:一是冷启动兜底,新用户没有评分时直接返回热门榜单,用「近期评分人数最多的电影」做初版推荐;二是混合策略,系统同时算协同过滤结果和热门结果,当协同过滤的可信度不足(比如相似邻居太少)时,降级到热门。
下面这张参数表是我在一套电影推荐毕设项目里实际跑过的配置,可以作为调试起点,不代表最优:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 用户最少评分条数 | 20 | 过滤掉评分极少的冷启动噪声用户 |
| 邻居数 k | 10 ~ 15 | 超过 20 后 F1 明显下降 |
| 相似度阈值 | 0.05 | 低于此值的邻居直接丢弃 |
| TopN | 10 ~ 20 | 演示和评估都常用 |
| 测试集比例 | 0.2 | 按用户分层切分 |
5. 避坑与常见问题排查:这些坑我帮你踩过了
5.1 现象:推荐结果永远是热门大片,个性化等于零
原因:评分矩阵过于稀疏,大量电影只有个位数用户评过,算相似度时找不够邻居,算法兜底逻辑只能回到热门榜。另一个可能原因是相似度阈值设得过高,电影间相似度普遍偏低,阈值一卡,能进候选的电影全没了。
解决:先过滤掉评分人数过少的电影,把「长尾电影」从训练集里剔除,降低矩阵稀疏度。同时把相似度阈值从 0.1 降到 0.05 左右,重新跑推荐看结果分布。如果还是热门一片,检查一下是不是相似度计算时用了未中心化的原始分数——这种情况下「高分大片」天然占优。
5.2 现象:UserCF 在用户量增加后,每次推荐要等好几秒
原因:用户相似度矩阵是用户数的平方级,几千用户每对都要算,推荐时每次都要实时做邻居聚合,在线计算量完全失控。
解决:这类场景就应该切到 ItemCF,把电影相似度离线算好、定期重建,用户请求只做查表加排序,响应时间可以压到毫秒级。毕设论文里可以补一个「复杂度对比」小节,用数据说话——用户量 5000 时 UserCF 单次推荐耗时和 ItemCF 的差异,拉个折线图会很有说服力。另外可以用 SciPy 的稀疏矩阵代替 pandas DataFrame 来存相似度矩阵,空间和时间都能再省一个量级。
5.3 现象:推荐列表里混进了用户已经看过的电影
原因:推荐函数里的已看过滤条件写错,最常见的是把 rating > 0 写成 rating >= 0,而缺失值填充的 0 恰好满足了 >= 条件,把用户没评分但被填充为 0 的电影也当成了已看;或者是相似电影追加上来时,只在累加时过滤,最后排序输出前没有再统一去重。
解决:在推荐函数最后强制加一道差集操作,推荐结果必须和用户已评电影集合做差。我习惯在函数返回前加一个断言:推荐结果与已看列表交集必须为空,非空就报错终止。这道防护能拦住这一类低级错误,答辩演示时推荐列表一旦出现已看过的片,是非常显眼的硬伤。
5.4 现象:离线评估指标虚高,答辩被追问就露馅
原因:划分训练测试集时直接对整个评分表做随机切分,同一个用户的评分被拆到两边。协同过滤的本质是「从用户历史行为里学偏好」,测试集里残留了同用户的其他评分,模型等于开卷考试,指标自然好看到不真实。
解决:必须按用户粒度做隔离。用 sklearn 的 GroupKFold,或者先用 userId 分组再切分。画交叉验证结果时也要按分组方式说明,论文里写清楚「评测协议」——怎么划分训练与测试集、是否按用户隔离、TopN 取多少。这部分写扎实了,审阅老师挑不出毛病,反而是加分项。
5.5 现象:论文里的精确率和召回率,和代码跑出来的对不上
原因:统计口径不统一。最常见的是精确率的计算公式里,分子用了「推荐列表 ∩ 测试集观看记录」的数量,分母却写成了测试集的观看总数——两个数字一个来自推荐侧、一个来自真实侧,口径完全错位,指标当然对不上。
解决:统一成下面这套口径,代码和论文一一对应:精确率 = 推荐列表中用户真实观看过的数量 ÷ 推荐列表长度(TopN 的 N);召回率 = 推荐列表中用户真实观看过的数量 ÷ 测试集中该用户的观看总数。为了确保对得上,建议直接复用同一份统计函数来生成论文里画图用的数据,不要手工重新算一遍——两套算法之间差一个笔误,就够你对着两张表发愁半天的。
6. 进阶:离线评估跑通,答辩才有底气
评估环节是最容易被忽视、又最能拉开分数差距的部分。早些年我帮人排项目踩过坑:推荐跑通了,演示也流畅,被问到「推荐效果到底怎么样」,答一句「看起来还行」就直接冷场。后来我养成了习惯——任何推荐系统源码包到手,先写一套离线评估脚本,再碰推荐逻辑本身。评估脚本的意义不只是给论文凑一组数据,它能让你的调参从拍脑袋变成有依据的迭代。
python from sklearn.model_selection import GroupKFold
GroupKFold 按用户分组切分,保证同一用户的评分不会出现在训练和测试两侧
gkf = GroupKFold(n_splits=5) for train_idx, test_idx in gkf.split(ratings, groups=ratings['userId']): train = ratings.iloc[train_idx] test = ratings.iloc[test_idx]
train_matrix = build_matrix(train) item_sim = item_similarity(train_matrix) precision_list, recall_list = [], [] for user_id in test['userId'].unique(): recs = recommend_based_on_item(user_id, train_matrix, item_sim, top_n=10) # 命中数:推荐列表里的电影在测试集里真实出现了多少部 hit = len(set(recs) & set(test[test['userId'] == user_id]['movieId'])) precision_list.append(hit / 10) recall_list.append(hit / max(1, len(test[test['userId'] == user_id]))) print(f'Precision: {np.mean(precision_list):.4f}, Recall: {np.mean(recall_list):.4f}')这段脚本跑完,你能拿到每一折的精确率和召回率,取平均就是论文里的核心实验数据。更进一步,把 k 值、相似度阈值、TopN 三个参数各设几档,画出指标随参数变化的曲线,这部分内容放进论文的「实验与分析」章节,比任何空泛的系统描述都有分量。从那以后,我每拿到一套推荐系统源码,都会强制先跑一遍这个评估流程,确认基线指标正常再继续改代码——这个习惯帮我躲过了不少答辩现场「你的指标怎么算的」式的追问。希望这份拆解能帮你在毕设路上少走几步弯路。
本文还有配套的精品资源,点击获取