拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python协同过滤电影推荐系统:从零实现ItemCF与UserCF毕业设计

Python协同过滤电影推荐系统:从零实现ItemCF与UserCF毕业设计

简介:这是一套面向计算机相关专业毕业设计与课程设计场景的完整项目资料,核心为基于协同过滤算法的电影推荐系统Python实现,附带论文与项目说明,适合正在准备毕设、期末大作业或需要推荐算法实战练习的学习者直接参考使用。资源包共约2000个文件,压缩后约28.04MB,其中以1159个py源码文件为主体,配合324个pyc编译文件、124个html页面、40个js脚本与16个css样式文件构成Web交互界面,另有148张jpg与11张png图片资源、7个csv数据集、5个json与5个xml配置,以及63个po与63个mo多语言文件,整体结构完整、层次清晰。项目经过严格调试,可正常运行,并配有论文文档与说明材料,便于理解协同过滤推荐流程、系统模块划分与前后端组织方式。目前已有147人学习关注,可作为毕设选题落地的可靠参考。

1. 从零手写协同过滤:电影推荐系统毕业设计到底在做什么

很多同学拿到「基于协同过滤的电影推荐系统Python实现源码+论文」这个题目时,第一反应是去搜一份现成源码,改改界面就交差。但真正答辩时被问一句「你的相似度矩阵怎么算的、冷启动怎么处理」,往往就答不上来。这个题目的核心其实只有两件事:一是用 Python 把用户对电影的评分数据变成一张可计算的矩阵,二是用协同过滤算法从这张矩阵里预测出「你可能喜欢的电影」。它适合计算机、软件工程、大数据方向的本科毕业生,也适合想入门推荐算法的 Python 学习者。整套系统通常包含数据加载、相似度计算、评分预测、Top-N 推荐、可视化展示和论文撰写六个环节。下面我按自己带过几届毕设的经验,把这条链路拆开讲清楚,让你既能跑通代码,也能在论文里写出有说服力的算法分析。

2. 协同过滤的两条路线:UserCF 和 ItemCF 怎么选

协同过滤(Collaborative Filtering)的本质是「物以类聚,人以群分」。它不关心电影本身是什么类型、导演是谁,只关心「谁和谁的口味像」「哪些电影被同一批人喜欢」。这个思路决定了它有两个分支:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。选哪条路线,直接决定你后面代码怎么写、论文怎么论证。

2.1 UserCF 与 ItemCF 的数学差异

UserCF 的核心假设是:如果用户 A 和用户 B 对很多电影的打分都接近,那 A 喜欢但 B 没看过的电影,就可以推荐给 B。它的计算对象是「用户-用户」相似度矩阵。假设有 M 个用户、N 部电影,评分矩阵 R 是 M×N 的稀疏矩阵,UserCF 要算的是 M×M 的用户相似度矩阵。

ItemCF 反过来:如果电影 X 和电影 Y 被很多同一批用户喜欢,那喜欢 X 的人大概率也会喜欢 Y。它算的是 N×N 的物品相似度矩阵。

两者的数学公式都以余弦相似度或皮尔逊相关系数为基础。以余弦相似度为例,用户 u 和用户 v 的相似度:

sim(u,v) = Σ(r_ui · r_vi) / (√Σr_ui² · √Σr_vi²)

其中 r_ui 是用户 u 对电影 i 的评分。ItemCF 把用户换成物品即可。

实际选型时有个经验法则:用户数远小于物品数时用 UserCF,物品数远小于用户数时用 ItemCF。电影推荐场景里,MovieLens 数据集通常有几千用户、几千电影,两者都能跑。但 UserCF 有个明显问题——用户口味变化快,相似度矩阵需要频繁更新;而电影的内容属性相对稳定,ItemCF 的相似度矩阵可以离线算好、定期更新。所以工业界(比如早期的亚马逊)更偏向 ItemCF。毕业设计里我一般建议主做 ItemCF,论文里对比 UserCF 作为改进论证,这样既有工作量,又能体现你对两种算法的理解。

2.2 用 Python 加载 MovieLens 并构建评分矩阵

MovieLens 是这个题目最常用的公开数据集,常见的是 ml-latest-small 版本,包含 ratings.csv、movies.csv 两个核心文件。ratings.csv 的字段是 userId、movieId、rating、timestamp,每行是一条评分记录。下面这段代码完成数据加载和评分矩阵构建:

import pandas as pd import numpy as np # 加载评分数据和电影信息 ratings = pd.read_csv('ml-latest-small/ratings.csv') movies = pd.read_csv('ml-latest-small/movies.csv') # 只保留有评分的用户和电影,过滤掉评分次数过少的用户(冷启动处理) user_counts = ratings['userId'].value_counts() active_users = user_counts[user_counts >= 20].index ratings = ratings[ratings['userId'].isin(active_users)] # 构建用户-电影评分矩阵,缺失值填0表示未评分 rating_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) print(f"评分矩阵形状: {rating_matrix.shape}") print(f"稀疏度: {1 - (ratings.shape[0] / (rating_matrix.shape[0] * rating_matrix.shape[1])):.4f}")

这段代码有三个关键点。第一,pivot_table把长表转成宽表,行是用户、列是电影,这是协同过滤的标准输入格式。第二,fillna(0)把未评分填成 0,但要注意——0 在余弦相似度里表示「没有交互」,不是「打了 0 分」,这个区别在论文里必须写清楚,否则会被答辩老师追问。第三,过滤掉评分少于 20 条的用户,是为了降低矩阵稀疏度、提升相似度计算的信噪比。MovieLens 小数据集的稀疏度通常在 98% 以上,也就是说矩阵里 98% 的位置都是 0,这是协同过滤面临的核心挑战之一。

2.3 ItemCF 相似度矩阵的计算与参数说明

构建好评分矩阵后,下一步是算物品之间的相似度。这里用余弦相似度,把评分矩阵转置后按列计算:

from sklearn.metrics.pairwise import cosine_similarity # 转置矩阵,行变成电影,列变成用户 item_user_matrix = rating_matrix.T.values # 计算电影之间的余弦相似度 item_sim_matrix = cosine_similarity(item_user_matrix) # 将对角线置0,避免电影和自己相似度为1影响推荐 np.fill_diagonal(item_sim_matrix, 0) # 取相似度最高的K部电影作为邻居,K是核心调参项 K = 20 item_sim_df = pd.DataFrame( item_sim_matrix, index=rating_matrix.columns, columns=rating_matrix.columns ) print(f"相似度矩阵形状: {item_sim_df.shape}") print(f"电影1最相似的5部电影: {item_sim_df[1].nlargest(5).index.tolist()}")

这里有几个参数需要重点说明。K 值控制邻居数量,K 太小推荐结果不稳定,K 太大则引入噪声。经验值在 10 到 50 之间,我一般先用 20 跑基线,再在论文里做 K 值敏感性分析。相似度度量方式除了余弦,还可以用皮尔逊相关系数,后者对用户评分偏置更鲁棒,但计算量更大。对角线置 0是必须的,否则推荐结果里会出现「因为你看过这部电影所以推荐这部电影」的荒谬情况。另外要注意,cosine_similarity返回的是 numpy 数组,转成 DataFrame 时索引和列名要对齐,否则后面按 movieId 取相似电影时会取错。

3. 评分预测与 Top-N 推荐:从相似度到可解释的推荐列表

有了相似度矩阵,接下来要解决的是「给定一个用户,怎么生成推荐列表」。这一步分两个子问题:一是预测用户对未看电影的评分,二是按评分排序取前 N 个。很多同学的代码到这里就开始「玄学」了——推荐结果时好时坏,自己也说不清为什么。核心原因通常是评分预测公式没写对,或者没有做去偏处理。

3.1 基于加权平均的评分预测公式

ItemCF 预测用户 u 对电影 i 的评分,思路是:找到和电影 i 最相似的 K 部电影,看用户 u 对这些电影的打分,用相似度加权平均。公式如下:

pred(u,i) = Σ(sim(i,j) · r_uj) / Σ|sim(i,j)|

其中 j 遍历电影 i 的 K 个最近邻,且用户 u 对 j 有过评分。这个公式的直觉是:和你喜欢的电影越像的电影,你越可能喜欢。下面是 Python 实现:

def predict_rating(user_id, movie_id, rating_matrix, item_sim_df, K=20): """预测用户对某部电影的评分""" if movie_id not in item_sim_df.columns: return 0 # 获取该用户已评分的电影 user_ratings = rating_matrix.loc[user_id] rated_movies = user_ratings[user_ratings > 0].index # 取与目标电影最相似的K部,且用户已评分的电影 sim_scores = item_sim_df[movie_id].loc[rated_movies] top_k = sim_scores.nlargest(K) if top_k.sum() == 0: return 0 # 加权平均 numerator = sum(top_k[j] * user_ratings[j] for j in top_k.index) denominator = sum(abs(top_k[j]) for j in top_k.index) return numerator / denominator if denominator != 0 else 0

这段代码里有个容易翻车的点:sim_scores.nlargest(K)取的是相似度最高的 K 部电影,但如果用户对其中某部电影的评分是 0(未评分),加权平均时会被错误地当成「打了 0 分」。所以必须先过滤rated_movies,只保留用户真正评过分的电影。另一个点是分母用了abs(),因为相似度可能为负(余弦相似度在评分向量夹角大于 90 度时为负),取绝对值保证权重为正。如果你的数据集里出现负相似度,说明这两个电影的用户群体几乎不重叠,这种邻居其实应该被剔除,可以在nlargest之前先过滤掉相似度小于 0 的项。

3.2 生成 Top-N 推荐列表并过滤已看

预测完评分后,要对用户没看过的所有电影按预测分排序,取前 N 个。这里的关键是「过滤已看」——推荐系统最忌讳把用户已经看过的电影再推一遍。实现如下:

def recommend_movies(user_id, rating_matrix, item_sim_df, top_n=10, K=20): """为用户生成Top-N电影推荐""" # 用户已评分的电影 user_ratings = rating_matrix.loc[user_id] rated_movies = set(user_ratings[user_ratings > 0].index) # 所有电影 all_movies = set(rating_matrix.columns) # 候选集 = 所有电影 - 已看 candidate_movies = all_movies - rated_movies # 预测评分 predictions = [] for movie_id in candidate_movies: pred = predict_rating(user_id, movie_id, rating_matrix, item_sim_df, K) if pred > 0: predictions.append((movie_id, pred)) # 按预测分降序排序,取前N predictions.sort(key=lambda x: x[1], reverse=True) top_n_movies = predictions[:top_n] # 关联电影标题 result = [] for movie_id, pred in top_n_movies: title = movies[movies['movieId'] == movie_id]['title'].values result.append({ 'movieId': movie_id, 'title': title[0] if len(title) > 0 else 'Unknown', 'predicted_rating': round(pred, 2) }) return result # 测试:给用户1推荐10部电影 recs = recommend_movies(1, rating_matrix, item_sim_df, top_n=10) for r in recs: print(f"{r['title']} 预测评分: {r['predicted_rating']}")

这段代码的性能瓶颈在for movie_id in candidate_movies这个循环。如果候选集有几千部电影,每部都要算一次加权平均,Python 原生循环会非常慢。优化方向有两个:一是用 numpy 向量化,把相似度矩阵和评分向量做矩阵乘法;二是预先算好每部电影的 K 个最近邻,存成字典,预测时直接查表。毕业设计的数据量通常不大,原生循环能跑通,但论文里可以提一句「工程上可用向量化或倒排索引优化」,体现你有工程意识。

3.3 用 RMSE 和 MAE 评估推荐质量

推荐系统不能只看「推出来的电影像不像」,要有量化指标。最常用的是 RMSE(均方根误差)和 MAE(平均绝对误差),衡量预测评分和真实评分的差距。做法是把评分数据按 8:2 切成训练集和测试集,在训练集上算相似度,在测试集上预测并计算误差:

from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error # 划分训练集和测试集 train_data, test_data = train_test_split(ratings, test_size=0.2, random_state=42) # 用训练集重建评分矩阵和相似度矩阵(代码同上,此处省略) # ... # 在测试集上评估 true_ratings = [] pred_ratings = [] for _, row in test_data.iterrows(): pred = predict_rating(row['userId'], row['movieId'], train_matrix, train_item_sim_df, K=20) if pred > 0: true_ratings.append(row['rating']) pred_ratings.append(pred) rmse = np.sqrt(mean_squared_error(true_ratings, pred_ratings)) mae = mean_absolute_error(true_ratings, pred_ratings) print(f"RMSE: {rmse:.4f}, MAE: {mae:.4f}")

MovieLens 小数据集上,ItemCF 的 RMSE 通常在 0.85 到 0.95 之间,MAE 在 0.65 到 0.75 之间。如果你的结果明显偏离这个范围,先检查是不是把测试集的评分泄漏到了训练集里——这是毕设里最常见的「翻车」原因。另外,train_test_split的random_state要固定,否则每次跑出来的指标不一样,论文里的数据就没法复现。

4. 避坑与排查:协同过滤毕设里最容易翻车的 5 个地方

带过几届毕设后,我发现同学们踩的坑高度集中。下面这 5 条按「现象 → 原因 → 解决」整理,每条都是血泪经验。

4.1 推荐结果全是冷门电影

现象:给用户推荐的电影,大部分是评分人数很少的冷门片,用户根本没听过。

原因:余弦相似度对热门电影有天然偏好——热门电影和很多电影都有共同评分用户,相似度容易偏高;而冷门电影因为评分用户少,相似度计算不稳定,偶尔会出现「两个冷门电影因为唯一一个共同用户而相似度极高」的情况。另外,如果没做评分次数过滤,长尾电影会污染相似度矩阵。

解决:在计算相似度之前,过滤掉评分次数少于阈值(比如 50 次)的电影;或者在相似度公式里加入热门度惩罚项,比如用sim(i,j) / log(1 + count(i))降低热门电影的权重。更简单的做法是在推荐结果里做多样性重排,避免同一类型的冷门片扎堆。

4.2 RMSE 低但推荐结果很差

现象:论文里 RMSE 只有 0.82,看起来不错,但实际推荐列表里全是用户不感兴趣的电影。

原因:RMSE 衡量的是评分预测精度,不是推荐排序质量。一个总是预测「平均分 3.5」的模型,RMSE 可能很低,但它没有区分能力。推荐系统的核心是排序,不是回归。

解决:补充排序指标,比如 Precision@K、Recall@K、NDCG@K。做法是:对每个用户,把测试集里评分大于 4 的电影作为「真正相关」,看推荐列表前 K 个里命中了几个。Precision@10 能到 0.15 以上就算及格。论文里同时报告 RMSE 和 Precision@K,论证才完整。

4.3 矩阵太大导致内存溢出

现象:跑相似度计算时程序卡死,或者报MemoryError。

原因:cosine_similarity会生成一个 N×N 的稠密矩阵。如果电影数超过 1 万,这个矩阵就是 1 亿个浮点数,占 800MB 内存;如果用户数也很大,转置后的矩阵更夸张。

解决:用稀疏矩阵(scipy.sparse)存储评分矩阵,相似度计算改用sklearn.metrics.pairwise.cosine_similarity的稀疏版本,或者只计算每个物品的 Top-K 邻居而不是全量矩阵。毕业设计的数据量通常不至于溢出,但如果你用了 ml-20m 这种大数据集,就必须做稀疏化处理。

4.4 用户相似度计算时把「未评分」当成「差评」

现象:UserCF 跑出来的推荐结果和直觉完全相反,喜欢的电影被预测成低分。

原因:评分矩阵里用 0 填充未评分项,但余弦相似度会把 0 当成「评分为 0」,导致两个用户即使都没看过某部电影,也会因为「都打了 0 分」而增加相似度。这是协同过滤最经典的陷阱。

解决:计算相似度时只考虑两个用户共同评过分的电影,用掩码矩阵过滤掉未评分项。或者改用皮尔逊相关系数,它本身会减去用户平均分,对未评分项的处理更鲁棒。代码上可以用np.where(rating_matrix > 0, rating_matrix, np.nan)把 0 变成 NaN,再用np.nanmean之类的函数处理。

4.5 论文里的公式和代码对不上

现象:答辩老师对照论文公式和源码,发现符号定义不一致、下标范围不对、甚至公式写错了。

原因:论文里的公式是直接从参考文献抄的,代码是按自己理解写的,两者没有对齐。比如论文里写「sim(u,v) 表示用户 u 和 v 的相似度」,代码里却用item_sim变量名,老师一看就懵。

解决:写论文时,每个公式下面用一段话解释「对应代码里的哪个函数、哪个变量」,符号表单独列一张表。代码里的变量命名尽量和论文公式一致,比如论文用sim(i,j),代码里就用sim_ij而不是score。这个细节能大幅提升答辩通过率。

5. 从能跑到能写进论文:三个让毕设加分的小技巧

跑通代码只是及格线,要让毕设拿到好成绩,还得在「可解释性」和「对比实验」上做文章。下面三个技巧是我带学生时反复验证有效的,不需要复杂工程,但能让论文的算法章节厚实很多。

5.1 用相似电影解释推荐理由

推荐系统最被诟病的是「黑匣子」——用户不知道为什么被推荐了这部电影。ItemCF 天然有可解释性:推荐电影 i 是因为你喜欢电影 j,而 i 和 j 相似。把这个理由展示出来,既提升用户体验,又能在论文里作为「可解释推荐」的亮点。实现很简单,在推荐结果里加上「因为你看过 XX」:

def explain_recommendation(user_id, movie_id, rating_matrix, item_sim_df, K=5): """解释为什么推荐这部电影""" user_ratings = rating_matrix.loc[user_id] rated_movies = user_ratings[user_ratings > 0].index sim_scores = item_sim_df[movie_id].loc[rated_movies] top_k = sim_scores.nlargest(K) explanations = [] for mid, sim in top_k.items(): title = movies[movies['movieId'] == mid]['title'].values explanations.append({ 'because_you_watched': title[0] if len(title) > 0 else 'Unknown', 'similarity': round(sim, 3), 'your_rating': user_ratings[mid] }) return explanations

这段代码返回的是「因为你看了 A(你打了 4 分),而 A 和 B 的相似度是 0.85,所以推荐 B」。论文里可以把这个作为「推荐解释模块」单独写一节,配上界面截图,工作量就上来了。

5.2 对比 UserCF 和 ItemCF 的实验设计

论文里只写一种算法会显得单薄。建议做一组对比实验:同样的数据集、同样的训练测试划分,分别跑 UserCF 和 ItemCF,对比 RMSE、MAE、Precision@10 和运行时间。下面是一个对比表格的模板:

指标UserCFItemCF
RMSE0.920.88
MAE0.710.68
Precision@100.120.16
相似度矩阵大小M×MN×N
运行时间(秒)4532

表格里的数据要自己跑出来,不能编。实验设计部分写清楚:数据集版本、训练测试比例、K 值、相似度度量方式、随机种子。这样老师问「你的实验可复现吗」,你可以直接说「固定了 random_state=42,代码在附录」。

5.3 冷启动问题的低成本处理方案

冷启动是推荐系统的经典难题:新用户没有评分历史,新电影没有用户评分。毕业设计里不需要做复杂的深度学习方案,两个低成本处理就能写进论文。第一,新用户引导:注册时让用户勾选几部看过的电影并打分,用这几条数据算相似度,虽然稀疏但比没有强。第二,热门兜底:如果用户评分记录少于 5 条,直接推荐全站评分最高的 10 部电影(按贝叶斯平均分排序,避免只有 1 个 5 分的新电影排第一)。贝叶斯平均分公式:

bayesian_score = (v / (v + m)) · R + (m / (v + m)) · C

其中 v 是电影评分人数,R 是电影平均分,m 是最小评分人数阈值(比如 50),C 是全站平均分。这个公式在论文里写出来,比单纯说「推荐热门电影」专业得多。

最后说个我自己的习惯:每次跑完实验,把参数、指标、随机种子记在一个experiment_log.md里,论文写到哪一步都能回溯。带过的学生里,凡是坚持记日志的,答辩时被问细节都不慌;凡是跑完就忘的,最后都在「你这个 0.88 是怎么来的」上卡壳。希望帮到你。

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

返回列表