资讯详情

基于协同过滤的电影推荐系统:从相似度计算到Top-N推荐实战

📅 2026/10/31 13:34:20 | 华诺云谱 👁 阅读
基于协同过滤的电影推荐系统:从相似度计算到Top-N推荐实战
简介一份面向计算机专业毕业设计、课程设计与期末大作业的电影推荐系统完整项目基于Python协同过滤推荐算法构建适合正在准备毕设的学生及需要推荐系统项目实战的入门学习者。压缩包内共包含1197个文件其中22个Python源码文件为核心算法与业务逻辑20个pyc文件为编译缓存另有html、css、js、xml等前端展示资源以及csv、sqlite3格式的电影与用户数据集覆盖数据导入、相似度计算、推荐结果展示等环节。项目已经过严格调试下载解压即可运行可直接作为毕业设计使用也可在此基础上进行二次开发与论文扩展。整体包大小为76.75MB目录结构清晰便于按模块查阅和学习。目前已有147人学习下载适合希望快速搭建推荐系统并理解协同过滤原理的高校学生及开发者。1. 基于协同过滤的电影推荐系统毕设答辩需要的一整套数据闭环选毕设题目时很多人的第一反应是追深度学习、知识图谱结果数据、环境、训练时间反复劝退。基于协同过滤推荐算法的电影推荐系统走的是另一条路算法理论清晰、代码量适中、数据是现成的随源码一起提供的 movie_info.csv 和 users_resulttable.csv 可以直接加载构建“用户-电影”评分矩阵通过相似度计算输出 Top-N 推荐列表再配上一套 Bootstrap 前端页面做演示。整套过程是“数据读取 → 矩阵构建 → 相似度计算 → 结果排序 → 页面展示”的完整闭环既能跑通又能写进论文。这套源码最适合做毕设、课设、期末大作业的计算机专业学生也适合想用最短时间跑通协同过滤全流程的初学者。它的价值不在于算法多新而在于边界清晰、步骤完整答辩现场能演示、能改参数、能回答提问。2. 协同过滤原理与选型UserCF 和 ItemCF 该选谁相似度怎么算才不翻车2.1 两种主流协同过滤UserCF 与 ItemCF毕设场景下怎么选协同过滤的核心假设并不复杂如果两个用户在历史行为上高度一致那么其中一个用户喜欢的内容另一个也大概率会喜欢如果两部电影总是被同一批人高分评价那么喜欢其中一部的人很可能也喜欢另一部。前者叫 UserCF基于用户的协同过滤后者叫 ItemCF基于物品的协同过滤两者都要从一个“用户-电影”评分矩阵出发。UserCF 的完整计算路径是先计算目标用户和其他所有用户的相似度挑出相似度最高的 K 个用户作为邻居再把邻居看过且目标用户没看过的电影按相似度加权汇总排序取前 N 个结果。这套逻辑在用户规模不大、物品更新频繁的场景里很常见。它有一个天生的弱点用户冷启动。新注册用户没有任何评分记录时相似邻居根本凑不出来推荐列表要么为空要么退化成全站热门榜论文的“实验结果”部分容易暴露这个问题。ItemCF 的思路正好反过来先算电影和电影之间的相似度再以目标用户历史打高分或看过的电影为种子去匹配与这些种子相似的电影最后按加权得分汇总出候选列表。电影之间的相似关系相对稳定不会因为新用户加入而频繁变化所以 ItemCF 可以把相似度矩阵离线算好、缓存起来线上只做查表和加权聚合。放到电影推荐这个场景里ItemCF 还有一个不可替代的优势可解释性。“因为你给《盗梦空间》打了高分所以推荐《星际穿越》”这句话在答辩现场比“与你相似的用户也看过”更有说服力。对于这套源码的数据量几百到几千条评分记录两种算法都能在几十毫秒内出结果。如果论文需要做对比实验我的建议是主推 ItemCF 做最终效果演示UserCF 作为对照。要注意的是UserCF 在用户评分记录很少时邻居数量根本凑不齐推荐质量会明显下降这一条很容易被答辩老师当场质疑所以实现时一定要打印“最终参与投票的邻居数”别让这个数字变成黑匣子。2.2 相似度计算余弦相似度与皮尔逊相关系数稀疏数据下差别很大相似度是整个协同过滤的地基公式选错了后面密密麻麻的推荐结果全是噪音。代码里最常用的两种相似度是余弦相似度和皮尔逊相关系数。余弦相似度把两个用户的评分向量看成高维空间里的方向向量计算两个向量夹角的余弦值。公式实现简单numpy 几行就能算完但它只比较方向不比较长度。问题在于不同用户的评分习惯差异很大用户 A 打分普遍在 3 分上下用户 B 打分普遍在 5 分上下即便两人口味完全一致原始余弦相似度依然可能只有 0.7 左右。这是余弦公式天然存在的“评分基准偏移”问题。皮尔逊相关系数则先让每个用户减去自己的评分均值再做类似余弦的计算等于把评分基准漂移给抹平了。对电影评分这种显式反馈数据我一般更推荐皮尔逊因为“有人只给 3 分有人 4 分都嫌低”是真实行为去均值恰好能抵消这一层偏差。皮尔逊实现时要特别注意一个边界条件当某个用户对所有电影的评分完全相同比如只给一部电影打了 5 分他的评分标准差为 0皮尔逊公式的分母是 0计算结果会变成 nan。这种 nan 一旦进入相似度矩阵后续所有加权计算都会被污染所以代码里要显式处理方差为 0 的用户相关系数直接置 0不参与邻居选择。2.3 推荐列表生成Top-N 与评分预测答辩时更好讲的是哪一条相似度算完之后从候选池里挑电影有两种常见方案一种是评分预测先预测目标用户对每部没看过的电影会打多少分再按预测分排序输出另一种是 Top-N 推荐只统计邻居或相似电影中出现频次最高的候选不去猜具体分数。对毕设来说Top-N 推荐更容易演示也贴近真实推荐系统的交互习惯。页面基本上只展示“为你推荐 10 部电影”不需要把每个预测分都亮出来。评分预测则需要多做一步加权平均把候选电影在邻居那里的评分乘以相似度再除以相似度总和。公式不小复杂但有个隐蔽的坑如果不对候选集做过滤所有邻居的零散打分都参与计算稀疏数据下大量电影的预测分会被拉向同一个均值最终 Top-N 和“全站人气榜”几乎没区别个性化就消失了。我一般会给候选集加一个“最少覆盖数”的门槛只有至少被 M 个邻居打过分、或与种子电影有 M 个共同评分用户的电影才进入排序候选M 通常取 2 到 5。经过这一步冷门但口碑好的电影才有机会浮上来推荐结果的区分度会有肉眼可见的改善。答辩时把这一条作为“算法改进点”写进论文评委基本挑不出毛病。3. 数据与前端的工程边界从 CSV 字段清洗到 Bootstrap 样式的角色分工3.1 先摸清两个数据文件CSV 字段结构比算法更难缠拿到源码的第一件事不是跑主程序而是先确认两个数据文件的字段结构。movie_info.csv 是电影信息表一般包含电影 ID、标题、上映年份和类型users_resulttable.csv 是用户评分表常见字段是用户 ID、电影 ID、评分分数和时间戳。直接用 pandas 读进来以后先用 head 和 info 做一次体检。import pandas as pd movie_info pd.read_csv(movie_info.csv, encodingutf-8) ratings pd.read_csv(users_resulttable.csv, encodingutf-8) print(movie_info.head()) print(ratings.head()) print(movie_info.info()) print(ratings.info())encoding 参数值得单独说。从网上下载或从数据库导出的 CSV经常是 utf-8、gbk、gb2312 三种编码混着的先用 utf-8 读报 UnicodeDecodeError 就换 gbk别一上来就猜编码。info() 输出里每一列的非空数量很关键如果某一列空值比例超过 30%后续按电影 ID 做合并时这一列大概率会拖垮整个推荐结果。电影标题字段如果缺失太多最后的推荐列表根本没法给人看因为输出的全是 ID 数字。3.2 数据清洗与矩阵构建空值、重复评分与用户 ID 类型不一致CSV 里的脏数据比想象中多。用户可能重复提交评分同一部电影出现多条记录或者用户 ID 在两张表里类型不一致一张表是 int、另一张表是字符串这些都会让后续的相似度计算翻车。常见做法是先做一轮标准化ratings ratings.drop_duplicates(subset[uid, mid]) ratings[uid] ratings[uid].astype(str) ratings[mid] ratings[mid].astype(str) movie_info[mid] movie_info[mid].astype(str) ratings ratings.dropna(subset[uid, mid, score]) ratings[score] pd.to_numeric(ratings[score], errorscoerce) ratings ratings.dropna(subset[score])整个清洗顺序是有讲究的。先把 uid 和 mid 统一转成字符串避免出现“1”和“1.0”是不是同一个用户这种灵异事件再用 drop_duplicates 去掉完全重复的评分行保留第一条然后单独处理 score 字段读不进数字的值先转成 NaN 再丢弃。记住不要在清洗之前就急着构造矩阵否则后面调算法时经常会出现“明明有 300 个用户矩阵里却只有 298 行”这种对不上账的情况查起来非常头疼。3.3 Bootstrap 前端只负责展示CSS 样式不会参与推荐算法源码目录里放了 bootstrap.css、bootstrap.min.css、Test.css、firstPage.css、main.css、star.css、headstyle.css、demo.css 这一堆样式文件第一次打开项目时很容易被吓到以为要看懂每一行 CSS 才能运行。实际上这些文件只负责页面长什么样推荐算法的全部逻辑都在后端 Python 脚本里。Bootstrap 是现成的 CSS 框架star.css 大概率只控制星星评分组件demo.css 和 firstPage.css 管的是首页和示例页它们跟协同过滤公式没有任何关系。我的建议是把项目分成两条独立的调试线。第一条线是不开网页直接在命令行跑“读取数据 → 构建矩阵 → 算相似度 → 输出推荐电影标题”确认推荐链路本身是通的。第二条线才是启动 Web 服务把后端返回的推荐结果渲染到页面。这样出问题时很容易定位推荐结果不对是算法的锅页面打不开或样式错乱才是前端的问题。很多同学花了大量时间调一个跟自己毫无关系的 CSS 浮动布局纯属浪费时间。4. 核心代码复现构建用户-电影矩阵、计算相似度、生成 Top-N 的完整链路4.1 构建用户-电影评分矩阵pivot_table 的正确打开方式有了干净的 DataFrame第一步是把长表转成宽表也就是把评分表变成“行是用户 ID、列是电影 ID、单元格是对应评分”的矩阵这是协同过滤最标准的输入格式。score_matrix ratings.pivot_table( indexuid, columnsmid, valuesscore ) print(score_matrix.shape)pivot_table 是构建评分矩阵的常用做法index 参数指定行索引是用户 IDcolumns 参数指定列索引是电影 IDvalues 参数指定要被填充的评分列。注意这里我没写 fill_value0这是有意为之。fill_value0 会把“用户没看过这部电影”填充成 0 分但 0 和真实打出的低分在语义上完全不同。如果后续直接拿这个矩阵去算余弦相似度0 值会被当成“用户很讨厌这部电影”从而严重拉低用户之间的相似度。为了避免语义污染缺失评分要保持 NaN 状态。等真正需要稠密矩阵做矩阵乘法时再按场景决定补 0 还是补均值不要在数据准备阶段就过早填充。这一步是很多相似度计算结果“看起来哪里不对”的源头。4.2 计算电影之间的相似度余弦公式与共同评分下限ItemCF 的核心是电影相似度矩阵。对每一对电影取出这两部电影在全部用户上的评分向量只保留两个向量同时非 NaN 的维度然后算余弦相似度。如果共同评分的用户太少相似度没有统计意义直接置 0。import numpy as np def cosine_sim(a, b): mask ~(np.isnan(a) | np.isnan(b)) if mask.sum() 2: return 0.0 a_vec, b_vec a[mask], b[mask] return np.dot(a_vec, b_vec) / (np.linalg.norm(a_vec) * np.linalg.norm(b_vec)) movie_ids list(score_matrix.columns) sim_matrix pd.DataFrame(0.0, indexmovie_ids, columnsmovie_ids) for i, mid1 in enumerate(movie_ids): for mid2 in movie_ids[i 1:]: score cosine_sim(score_matrix[mid1].values, score_matrix[mid2].values) sim_matrix.loc[mid1, mid2] score sim_matrix.loc[mid2, mid1] score双层循环是 O(n²) 复杂度电影数量不多时完全没问题如果电影数量超过 2000建议先把评分矩阵转成 scipy.sparse.csr_matrix再用 sklearn.metrics.pairwise.cosine_similarity 一次性算完整张矩阵。mask.sum() 2 这个下限值得注意两个用户或两部电影之间共同评分的维度少于 2 时计算结果偶然性太大直接置 0可以避免很多噪声相似度。4.3 生成 Top-N 推荐加权聚合候选电影并排序得到相似度矩阵后给一个目标用户生成推荐的步骤很固定取出该用户打过分的电影集合作为种子把与这些种子有相似度的电影做加权聚合按得分排序取前 N 个。def recommend(uid, score_matrix, sim_matrix, top_n10, k20): user_ratings score_matrix.loc[uid].dropna() if len(user_ratings) 0: return [] seed_ids user_ratings.index.tolist() scores {} for seed_id in seed_ids: sims sim_matrix.loc[seed_id].sort_values(ascendingFalse) for candidate_id, sim_value in sims.head(k).items(): if candidate_id in seed_ids: continue if sim_value 0: continue scores[candidate_id] ( scores.get(candidate_id, 0.0) sim_value * user_ratings[seed_id] ) sorted_candidates sorted(scores.items(), keylambda x: x[1], reverseTrue) return [mid for mid, _ in sorted_candidates[:top_n]]代码里有三个关键点。第一sims.head(k) 只取每部种子电影相似度最高的前 k 部候选避免把相似度极低的电影也纳入投票。第二continue 条件先排掉用户已经看过的电影这一步是硬约束少了它推荐列表里全是用户看过的东西。第三加权公式用的是“相似度 × 用户原评分”用户打过分且与候选电影相似度高的种子贡献的权重自然更大。返回结果是电影 ID 列表最后拼上 movie_info 里的标题和类型就可以展示。5. 避坑与排查把源码跑通前必须看的 5 个翻车点5.1 数据读取与矩阵构建阶段的三个翻车点现象一read_csv 直接抛 UnicodeDecodeError或者电影标题读出来全是乱码。 原因movie_info.csv 或 users_resulttable.csv 的编码是 gbk 或 gb2312不是 Python 默认的 utf-8。 解决优先用 chardet 探测文件编码再把探测结果填进 read_csv 的 encoding 参数。别手动在编辑器里改文件编码另存改坏了没有后悔药。with open(movie_info.csv, rb) as f: raw f.read(4096) import chardet print(chardet.detect(raw))现象二pivot_table 生成矩阵后算出来的相似度全是 0推荐列表为空。 原因构建矩阵时用了 fill_value0把大量没看过的电影填充成了 0 分。余弦相似度计算时两个评分向量里的有效项太少或者非零部分没有交集结果全部趋近于 0。 解决缺失评分用 NaN 表示不要在数据准备阶段过早补 0。构建完矩阵后先打印一行非空比例非空率低于 5% 基本可以断定数据清洗阶段出了问题。现象三皮尔逊相关系数计算结果出现 nan推荐函数直接报 ValueError。 原因存在评分方差为 0 的用户比如只给一部电影打了 5 分。皮尔逊公式的分母是标准差方差为 0 时除数为 0结果就是 nan。 解决在计算相似度之前先按用户评分的标准差做过滤方差为 0 的用户相关系数直接置 0不进邻居候选。std score_matrix.std(axis1) zero_std_users std[std 0].index.tolist() for u in zero_std_users: score_matrix.loc[u] np.nan这段代码把这些用户的所有评分置为 NaN本质是让他们不参与相似度计算但保留历史评分记录。逻辑简单答辩时一句话能讲清楚。5.2 算法运行与页面展示阶段的两个长尾问题现象四相似度矩阵计算太慢几百部电影跑了十几分钟出不来CPU 占满。 原因双层循环往一个 n×n 矩阵里填相似度复杂度是 O(n²)而且每对向量都在做 numpy 数组拷贝。 解决电影数量多时改用 scipy.sparse.csr_matrix 加 sklearn 的 cosine_similarity 批量计算。更简单的做法是只对出现次数排名靠前的电影算相似度冷门电影直接走热门兜底对毕设演示效果影响很小。现象五推荐结果里有一半是用户自己看过的电影。 原因聚合候选集时没有排除种子电影自己投了自己一票热门电影频繁出现在种子列表里自我投票导致得分虚高。 解决在候选遍历的 continue 条件里必须预先过滤掉 seed_ids。同时建议在推荐函数里打印一次“种子数”和“候选数”两个数字相差太大说明评分记录被清洗删过头了要回数据清洗阶段检查而不是在算法里硬调参数。6. 进阶玩法给推荐结果加上推荐理由用 Flask 把系统端上 Web 页面6.1 用 Flask 把命令行结果搬进网页薄封装就够命令行里打印推荐电影 ID 只能证明“算法能跑”答辩现场说服力不够。把推荐函数包一层 Web 接口后页面上输入用户 ID 就能看到带电影标题的推荐列表效果完全不同。集成方式保持精简不碰算法核心from flask import Flask, request app Flask(__name__) app.route(/) def recommend_page(): uid request.args.get(uid, defaultU001) rec_list recommend(uid, score_matrix, sim_matrix, top_n10, k20) titles movie_info.set_index(mid).loc[rec_list][title].tolist() return {uid: uid, recommendations: titles}request.args 负责从 URL 参数取 uidrecommend 直接复用第 4 章的函数返回的电影 ID 再通过 movie_info 映射成可读标题。如果页面加载缓慢优先检查相似度矩阵是否每次请求都在重新计算正确做法是服务启动时只算一次之后全局复用。6.2 可解释推荐返回一条“为什么推荐这部电影”的理由ItemCF 天然适合带解释。在聚合候选时记下每个候选得分最高的种子电影返回时拼成一句话“因为你给《盗梦空间》打了 5 分所以推荐《星际穿越》。”这类推荐叫可解释推荐近几年的推荐系统论文里都在讨论。答辩时主动讲出这一句评委的追问方向基本会落到相似度公式上正好呼应第 2 章的皮尔逊相关系数形成完整逻辑闭环。best_seed max(seeds, keylambda seed: sim_matrix.loc[seed, candidate_id]) seed_title movie_info.set_index(mid).loc[best_seed][title] candidate_title movie_info.set_index(mid).loc[candidate_id][title] print(f因为你给《{seed_title}》打了高分所以推荐《{candidate_title}》)选得分最高的种子做解释比罗列所有种子更接近真实推荐产品的口径。从那以后我每次拿新数据都强制走一遍“命令行跑通 → 打印矩阵非空比例 → 启动 Flask 页面刷新核对”先在数据阶段拦住脏数据再谈调参。这些细节希望能帮你在答辩前把系统打磨得更稳一点。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑