资讯详情

从零手搓电影推荐系统:协同过滤算法落地与毕业设计实战

📅 2026/10/1 4:57:20 | 华诺云谱 👁 阅读
从零手搓电影推荐系统:协同过滤算法落地与毕业设计实战
简介这份资源是Python基于协同过滤推荐算法的电影推荐系统完整项目面向计算机相关专业正在做毕业设计的学生以及需要项目实战练习的学习者也可用于课程设计或期末大作业。项目包含全部源码与数据集经过严格调试下载后可直接运行帮助读者省去从零搭建推荐系统的时间成本。压缩包共1197个文件约76.75MB其中22个py文件承载协同过滤核心算法与业务逻辑14个html、12个js与8个css文件构成前端交互界面4个csv与1个sqlite3、1个sql文件提供用户评分、电影信息等数据支撑另有大量jpg图片用于页面展示。目前已有147人学习下载。读者可获得一套结构完整的推荐系统实现方案涵盖用户相似度计算、评分预测与电影推荐等关键环节便于理解协同过滤原理并在此基础上进行二次开发或论文撰写适合作为毕设参考与实战练手项目。1. 从零手搓一个能跑的电影推荐系统协同过滤到底怎么落地很多同学做毕业设计时一看到“推荐系统”四个字就头大觉得必须上深度学习、上大模型才够格。其实在电影推荐这个场景里协同过滤推荐算法才是那个最稳、最容易讲清楚、也最容易跑出效果的方案。它不依赖电影的海报、简介、演员表这些内容特征只看一件事谁和谁的口味像谁和谁看过同一批电影。你手头只要有一份用户对电影的评分数据就能把整套系统跑起来。这篇文章面向的是正在做毕业设计、需要一套能演示、能答辩、能写进论文的python电影推荐系统源码的同学。我会从数据长什么样、算法怎么算、代码怎么写、参数怎么调、坑在哪一路讲到怎么验证推荐结果是不是靠谱。整套方案用 pandas numpy 就能实现不需要 GPU一台普通笔记本十分钟内能跑完。看完你至少能得到三样东西一个可复现的协同过滤核心、一套可替换的数据接口、一份能写进论文的实验对比思路。2. 协同过滤的两条路线UserCF 和 ItemCF 到底选哪个在动手写代码之前必须先把算法选型这件事想明白。协同过滤推荐算法主要分两条路线基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。很多同学直接抄网上的代码跑完也不知道自己用的是哪种答辩时被问一句“为什么选这个”就卡住了。这一章把两条路线的原理、适用场景和选型理由讲透再落到具体实现。2.1 UserCF 和 ItemCF 的核心差异UserCF 的思路是找到和你口味相似的一批用户把他们喜欢但你没看过的电影推荐给你。它计算的是“用户之间的相似度”。ItemCF 的思路是找到和你历史喜欢的电影相似的电影推荐给你。它计算的是“物品之间的相似度”。两者的关键差异体现在数据规模和维护成本上。UserCF 适合用户数量远小于物品数量的场景因为用户相似度矩阵是 N×NN 为用户数用户多了矩阵会爆炸。ItemCF 适合物品数量相对稳定的场景电影库一般不会天天暴增但用户会持续增长所以工业界推荐系统更多用 ItemCF。对于毕业设计来说MovieLens 数据集通常有几千个用户、几千部电影两者都能跑但 ItemCF 的推荐结果更稳定解释性也更强——“因为你看了《星球大战》所以推荐《帝国反击战》”这种话术答辩时很好用。我一般会建议如果你的数据集用户数少于物品数用 UserCF反之用 ItemCF。如果拿不准两个都实现用评测指标对比这本身就是论文里的一章。2.2 相似度计算的三种方式和参数含义相似度是协同过滤的心脏。常用的有三种余弦相似度、皮尔逊相关系数和调整余弦相似度。余弦相似度把每个用户的评分向量看成空间中的向量计算夹角的余弦值。它的问题是没有考虑用户评分尺度的差异——有人习惯打高分有人习惯打低分。皮尔逊相关系数通过减去用户平均分来修正这个问题但它要求两个用户必须有共同评分的电影否则无法计算。调整余弦相似度则是在余弦的基础上减去物品平均分适合物品评分偏差大的场景。import numpy as np from sklearn.metrics.pairwise import cosine_similarity def user_similarity_cosine(user_item_matrix): 基于余弦相似度计算用户相似度矩阵 user_item_matrix: 行是用户列是电影值是评分未评分为0 # 余弦相似度对稀疏矩阵友好但未评分填0会引入偏差 sim cosine_similarity(user_item_matrix) np.fill_diagonal(sim, 0) # 自己和自己相似度置0避免推荐自己看过的 return sim def user_similarity_pearson(user_item_matrix): 基于皮尔逊相关系数计算用户相似度 需要先对每个用户减去其平均评分 matrix user_item_matrix.astype(float) # 只对已评分项计算均值未评分不参与 mask matrix 0 user_mean np.true_divide( (matrix * mask).sum(axis1), mask.sum(axis1), wheremask.sum(axis1) ! 0 ) # 中心化已评分减去均值未评分保持0 centered np.where(mask, matrix - user_mean[:, np.newaxis], 0) sim cosine_similarity(centered) np.fill_diagonal(sim, 0) return sim上面两段代码的差别就在“是否中心化”。余弦相似度直接用原始评分皮尔逊先减去用户平均分再算余弦。参数上要注意np.fill_diagonal(sim, 0)这行不能省否则每个用户和自己相似度最高推荐结果会全是自己已经看过的电影。另外如果某个用户评分数量为0user_mean会出现除零代码里用where做了保护实际数据中要先把这类冷启动用户过滤掉。2.3 预测评分与 Top-N 推荐的实现有了相似度矩阵下一步就是预测某个用户对未看电影的评分。公式是预测分 用户平均分 相似用户对该电影的加权偏差之和 / 相似度绝对值之和。这个公式比简单加权平均更合理因为它考虑了每个用户的评分基准线不同。def predict_rating(user_id, item_id, user_item_matrix, sim_matrix, k20): 预测用户对某部电影的评分 k: 取最相似的k个用户参与预测 user_ratings user_item_matrix[user_id] if user_ratings[item_id] 0: return user_ratings[item_id] # 已经看过直接返回 # 找到看过这部电影的其他用户 rated_users np.where(user_item_matrix[:, item_id] 0)[0] if len(rated_users) 0: return 0 # 没人看过无法预测 # 取相似度最高的k个 sims sim_matrix[user_id, rated_users] top_k_idx np.argsort(sims)[-k:] top_users rated_users[top_k_idx] top_sims sims[top_k_idx] # 过滤掉负相似度或零相似度 valid top_sims 0 if not valid.any(): return user_ratings[user_ratings 0].mean() if (user_ratings 0).any() else 0 top_users top_users[valid] top_sims top_sims[valid] # 加权预测 user_mean user_ratings[user_ratings 0].mean() numerator 0.0 denominator 0.0 for u, s in zip(top_users, top_sims): u_mean user_item_matrix[u][user_item_matrix[u] 0].mean() numerator s * (user_item_matrix[u, item_id] - u_mean) denominator abs(s) if denominator 0: return user_mean return user_mean numerator / denominator这段代码里k20是一个关键参数。k 太小预测容易受个别极端用户影响k 太大会引入不相似用户的噪声。MovieLens 数据集上一般取 20 到 40 之间效果比较稳。valid top_sims 0这行过滤掉负相关用户因为口味相反的人不应该参与推荐。最后denominator 0的保护是防止所有相似度都为零时除零崩溃。生成 Top-N 推荐列表时对用户所有未评分的电影调用predict_rating按预测分排序取前 N 个即可。实际跑的时候这一步会比较慢因为要对每个用户遍历所有未评分电影。优化方式是用矩阵运算批量计算或者只对候选集比如相似用户看过的电影做预测不要全量遍历。3. 数据准备与工程结构从 MovieLens 到可运行项目算法讲清楚了接下来要解决“数据从哪来、代码怎么组织”的问题。很多毕业设计源码跑不起来不是算法错而是数据路径、编码格式、依赖版本这些工程细节翻车。这一章把数据加载、预处理和项目结构一次性讲清楚。3.1 MovieLens 数据集的字段含义与加载方式MovieLens 是推荐系统最常用的公开数据集常见的有 ml-latest-small约10万条评分和 ml-1m约100万条评分。毕业设计用 ml-latest-small 就够了跑得快效果也够演示。它包含两个核心文件ratings.csv和movies.csv。ratings.csv的字段是userId、movieId、rating、timestamp。movies.csv的字段是movieId、title、genres。评分是 0.5 到 5.0 的浮点数每 0.5 一档。import pandas as pd import numpy as np def load_movielens(data_dir): 加载 MovieLens 数据集 data_dir: 包含 ratings.csv 和 movies.csv 的目录 ratings pd.read_csv(f{data_dir}/ratings.csv) movies pd.read_csv(f{data_dir}/movies.csv) # 检查缺失值 print(ratings 缺失值, ratings.isnull().sum().sum()) print(movies 缺失值, movies.isnull().sum().sum()) # 过滤评分次数过少的用户和电影缓解冷启动 user_counts ratings[userId].value_counts() movie_counts ratings[movieId].value_counts() active_users user_counts[user_counts 20].index popular_movies movie_counts[movie_counts 20].index ratings ratings[ ratings[userId].isin(active_users) ratings[movieId].isin(popular_movies) ] print(f过滤后评分条数{len(ratings)}) print(f用户数{ratings[userId].nunique()}电影数{ratings[movieId].nunique()}) return ratings, movies这段代码里user_counts 20和movie_counts 20是两个经验阈值。评分少于20条的用户行为模式不稳定参与相似度计算反而引入噪声被评分少于20次的电影推荐出去也没多少人能验证。过滤后数据量会减少但推荐质量会提升。实际跑的时候先打印过滤前后的数量对比写进论文里就是数据预处理章节的内容。3.2 构建用户-物品评分矩阵协同过滤的输入是一个用户×物品的评分矩阵。pandas 的 pivot 可以直接构建但要注意内存。ml-latest-small 大概几千用户、几千电影稠密矩阵也就几十兆没问题。如果是 ml-1m 或更大就要用稀疏矩阵。from scipy.sparse import csr_matrix def build_matrix(ratings): 构建用户-物品评分矩阵 返回稠密矩阵、用户ID映射、电影ID映射 # 重新编码用户和电影ID从0开始连续 user_ids ratings[userId].unique() movie_ids ratings[movieId].unique() user_to_idx {uid: i for i, uid in enumerate(user_ids)} movie_to_idx {mid: i for i, mid in enumerate(movie_ids)} rows ratings[userId].map(user_to_idx).values cols ratings[movieId].map(movie_to_idx).values vals ratings[rating].values.astype(float) n_users len(user_ids) n_movies len(movie_ids) # 稀疏矩阵构建适合大数据集 sparse_matrix csr_matrix((vals, (rows, cols)), shape(n_users, n_movies)) # 转稠密用于后续相似度计算数据大时改用稀疏运算 dense_matrix sparse_matrix.toarray() return dense_matrix, user_to_idx, movie_to_idx, user_ids, movie_ids这里做了 ID 重映射因为原始 userId 和 movieId 可能不连续比如有用户注销后 ID 留空。重映射后矩阵索引从0开始后续所有操作都用索引而不是原始 ID。csr_matrix是稀疏存储toarray()转稠密。如果数据集超过10万用户建议保持稀疏相似度计算用sklearn的稀疏接口否则内存会爆。这个函数返回的映射字典要保存好推荐结果最终要映射回原始 movieId 才能显示电影名。3.3 项目目录结构与依赖清单一个能交付的毕业设计项目目录结构要清晰答辩时老师一眼能看懂。我一般会这样组织movie_recommend/ ├── data/ │ ├── ratings.csv │ └── movies.csv ├── src/ │ ├── data_loader.py # 数据加载与预处理 │ ├── similarity.py # 相似度计算 │ ├── recommender.py # 推荐核心逻辑 │ ├── evaluate.py # 评测指标 │ └── main.py # 入口 ├── requirements.txt └── README.mdrequirements.txt里写清楚版本避免换台电脑就报错pandas2.0.3 numpy1.24.3 scikit-learn1.3.0 scipy1.11.1版本号不是随便写的pandas 2.x 和 1.x 在value_counts和map行为上有差异numpy 1.24 以上对np.true_divide的where参数支持才完整。毕业设计答辩用的电脑不一定和你开发机一样锁版本能省很多解释成本。4. 避坑与排查协同过滤跑不通的五个血泪经验这一章是我带过几届毕业设计后攒下来的踩坑记录。协同过滤的代码看起来简单但实际跑的时候各种玄学问题层出不穷。下面五条按“现象→原因→解决”写遇到问题直接对号入座。4.1 推荐结果全是冷门电影现象跑出来的 Top-N 推荐列表里电影名字都很陌生评分人数个位数。原因相似度矩阵计算时冷门电影因为评分人数少只要有一两个相似用户打高分预测分就会被拉得很高。这是协同过滤的流行度偏差问题。解决在预测分上乘一个流行度惩罚项或者直接在数据预处理阶段过滤掉评分次数少于阈值的电影。我一般用后者简单粗暴。如果不想过滤可以在排序时加一个score * log(1 movie_rating_count)的加权。4.2 相似度矩阵出现 NaN现象cosine_similarity返回的矩阵里有 NaN后续计算全部污染。原因某个用户对所有电影的评分都是0过滤后仍可能存在导致其评分向量模长为0余弦相似度分母为零。解决在计算相似度前检查每行是否全零全零行直接剔除。代码里加一行matrix matrix[matrix.sum(axis1) 0]同时同步更新用户映射。4.3 评测指标 RMSE 高得离谱现象用测试集算 RMSE结果超过2.0而评分范围才0.5到5.0。原因预测时没有做评分截断预测分可能超出评分范围或者训练集和测试集划分时没有按用户分层导致某些用户在测试集里的电影在训练集里完全没出现过。解决预测分做np.clip(pred, 0.5, 5.0)截断。划分数据集时用train_test_split的stratify参数按用户分层保证每个用户在训练集里至少有一条评分。4.4 跑一次要十几分钟现象生成推荐列表时卡住CPU 跑满十几分钟出不来结果。原因对每个用户的每部未评分电影都调用一次predict_rating复杂度是 O(用户数 × 电影数 × k)数据量稍大就爆炸。解决改成矩阵运算。先用相似度矩阵和评分矩阵做矩阵乘法一次性算出所有预测分再对每个用户取 Top-N。核心代码是pred sim_matrix.dot(ratings) / np.abs(sim_matrix).dot(ratings 0)一行顶几千次循环。4.5 换数据集后 ID 对不上现象推荐列表里的电影 ID 在movies.csv里找不到显示为 NaN。原因构建矩阵时做了 ID 重映射但输出推荐结果时忘了映射回原始 movieId。解决保存movie_ids数组推荐结果拿到索引后用movie_ids[idx]取回原始 ID再和movies.csv做 merge。这个映射关系在build_matrix函数里已经返回了关键是别在后续流程里丢掉。5. 评测与调参怎么证明你的推荐系统真的有效代码跑通了推荐列表也出来了但答辩时老师一定会问“你怎么知道推荐得准”这一章讲评测指标和调参方法让你的毕业设计从“能跑”变成“有说服力”。5.1 评分预测指标RMSE 和 MAERMSE均方根误差和 MAE平均绝对误差衡量的是预测评分和真实评分的差距。RMSE 对大误差更敏感MAE 更稳健。两个都算论文里放一起对比。from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_rating_prediction(ratings, test_size0.2): 划分训练测试集评测评分预测误差 train, test train_test_split(ratings, test_sizetest_size, random_state42) # 用训练集构建矩阵和相似度 train_matrix, user_to_idx, movie_to_idx, _, _ build_matrix(train) sim_matrix user_similarity_pearson(train_matrix) # 批量预测测试集 preds, truths [], [] for _, row in test.iterrows(): u user_to_idx.get(row[userId]) m movie_to_idx.get(row[movieId]) if u is None or m is None: continue # 测试集里的冷启动用户/电影跳过 pred predict_rating(u, m, train_matrix, sim_matrix, k20) preds.append(np.clip(pred, 0.5, 5.0)) truths.append(row[rating]) rmse np.sqrt(mean_squared_error(truths, preds)) mae mean_absolute_error(truths, preds) print(fRMSE: {rmse:.4f}, MAE: {mae:.4f}, 测试样本数: {len(preds)}) return rmse, mae这段代码里random_state42保证每次划分一致方便复现。user_to_idx.get()返回 None 时跳过是因为测试集里可能有训练集没出现过的用户或电影这些无法预测强行算会报错。np.clip截断到评分范围。MovieLens 上 ItemCF 的 RMSE 一般在 0.85 到 0.95 之间如果你的结果超过 1.0检查相似度计算和 k 值。5.2 Top-N 推荐指标PrecisionK 和 RecallK评分预测指标衡量的是“打分准不准”但推荐系统实际关心的是“推的东西用户喜不喜欢”。PrecisionK 表示推荐的前 K 个里有多少是用户真正喜欢的RecallK 表示用户喜欢的电影里有多少被推荐出来了。def precision_recall_at_k(ratings, k10, threshold4.0, test_size0.2): 计算 PrecisionK 和 RecallK threshold: 评分大于等于该值视为“喜欢” train, test train_test_split(ratings, test_sizetest_size, random_state42) train_matrix, user_to_idx, movie_to_idx, _, movie_ids build_matrix(train) sim_matrix user_similarity_pearson(train_matrix) precisions, recalls [], [] for uid in test[userId].unique(): u user_to_idx.get(uid) if u is None: continue # 用户喜欢的电影集合测试集 user_test test[(test[userId] uid) (test[rating] threshold)] if len(user_test) 0: continue liked set(user_test[movieId].map(movie_to_idx).dropna().astype(int)) # 预测所有未评分电影的分数 scores [] for m in range(train_matrix.shape[1]): if train_matrix[u, m] 0: scores.append((m, predict_rating(u, m, train_matrix, sim_matrix, k20))) scores.sort(keylambda x: x[1], reverseTrue) top_k set([m for m, _ in scores[:k]]) hit len(top_k liked) precisions.append(hit / k) recalls.append(hit / len(liked) if liked else 0) print(fPrecision{k}: {np.mean(precisions):.4f}) print(fRecall{k}: {np.mean(recalls):.4f}) return np.mean(precisions), np.mean(recalls)threshold4.0是定义“喜欢”的阈值MovieLens 里 4 分以上通常算好评。k10是推荐列表长度论文里可以画一条 K 从 5 到 50 的曲线展示 Precision 和 Recall 的权衡。注意这段代码里对每个用户遍历所有电影算预测分实际跑会比较慢可以用 4.4 节说的矩阵运算优化。5.3 关键参数调优相似度算法、K 值和阈值调参是毕业设计里最容易出彩的部分。把不同参数组合的评测结果做成表格论文里就是现成的实验章节。参数候选值推荐范围影响相似度算法余弦/皮尔逊/调整余弦皮尔逊皮尔逊对评分尺度差异更鲁棒邻居数 K5/10/20/40/8020-40太小欠拟合太大引入噪声喜欢阈值3.0/3.5/4.0/4.54.0阈值越高评测越严格推荐列表长度5/10/20/5010影响 Precision 和 Recall 权衡我一般会固定其他参数单独调一个画出折线图。比如 K 从 5 到 80RMSE 先降后升最低点就是最优 K。这个图放在论文里比单纯说“我用了 K20”有说服力得多。5.4 用矩阵运算把评测速度提上来5.2 节的代码跑一次可能要几分钟调参时反复跑不现实。用矩阵运算重写预测部分def batch_predict(train_matrix, sim_matrix): 批量预测所有用户对所有电影的评分 利用矩阵运算替代循环 # 用户平均分 mask train_matrix 0 user_means np.true_divide( (train_matrix * mask).sum(axis1), mask.sum(axis1), wheremask.sum(axis1) ! 0 ) user_means np.nan_to_num(user_means) # 中心化评分矩阵 centered np.where(mask, train_matrix - user_means[:, np.newaxis], 0) # 分子相似度加权偏差之和 numerator sim_matrix.dot(centered) # 分母相似度绝对值之和只对已评分项 denominator np.abs(sim_matrix).dot(mask.astype(float)) # 避免除零 denominator[denominator 0] 1 pred user_means[:, np.newaxis] numerator / denominator return np.clip(pred, 0.5, 5.0)这个函数一次性算出所有预测分耗时从分钟级降到秒级。np.true_divide的where参数处理除零np.nan_to_num兜底。denominator[denominator 0] 1是防止某些电影没人评分导致分母为零。调参时用这个批量版本效率提升非常明显。6. 从毕业设计到可演示系统一个技巧让答辩加分最后讲一个我压箱底的技巧把推荐结果和电影海报、简介关联起来做一个简单的 Web 界面。毕业设计答辩时老师看代码看多了会疲劳一个能点击、能切换用户、能看到推荐理由的界面比一堆命令行输出有冲击力得多。具体做法是用 Flask 起一个轻量服务前端用最简单的 HTML 表格。核心是把推荐结果和movies.csv做 merge拿到电影名和类型。推荐理由可以这样生成“因为你喜欢《星球大战》和《帝国反击战》为你推荐《绝地归来》”——这需要记录推荐时贡献最大的相似物品或相似用户。def explain_recommendation(user_id, movie_id, train_matrix, sim_matrix, movie_ids, movies_df, top_n3): 生成推荐理由找出贡献最大的几部已看电影 u user_id m movie_id # 找到看过这部电影的用户 rated_users np.where(train_matrix[:, m] 0)[0] if len(rated_users) 0: return 暂无推荐理由 # 按相似度排序 sims sim_matrix[u, rated_users] top_idx np.argsort(sims)[-top_n:][::-1] reasons [] for idx in top_idx: other_user rated_users[idx] # 找这个相似用户看过、且目标用户也看过的电影 common np.where((train_matrix[u] 0) (train_matrix[other_user] 0))[0] if len(common) 0: # 取评分最高的一部 best common[np.argmax(train_matrix[u][common])] title movies_df[movies_df[movieId] movie_ids[best]][title].values if len(title) 0: reasons.append(title[0]) if reasons: return f因为你喜欢《{》《.join(reasons[:2])}》为你推荐 return 根据相似用户推荐这个函数返回的字符串直接拼到推荐列表里答辩演示时效果很好。top_n3控制理由数量太多显得啰嗦。common找的是目标用户和相似用户都看过的电影取目标用户评分最高的那部作为理由逻辑上最自然。界面部分用 Flask 的render_template传一个列表就行不需要前端框架。用户切换用下拉框选完提交表单重新请求。整个 Web 部分不超过100行代码但能让你的毕业设计从“算法演示”变成“系统演示”。我自己的习惯是算法核心和评测部分保持命令行可复现Web 界面只做展示层不掺业务逻辑。这样论文里写算法章节时干净利落答辩演示时又有东西可看。另外推荐理由这个功能一定要做它逼着你回头理解相似度矩阵的物理含义对写论文的“结果分析”章节帮助很大。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑