简介:本资源是一套基于协同过滤算法的电影推荐系统Python实现,包含完整项目源码与配套论文,面向计算机相关专业正在做毕业设计的学生,也可用于课程设计、期末大作业或项目实战练习。系统采用协同过滤核心思路完成用户与物品的相似度计算和推荐生成,代码经过严格调试,可直接运行并作为毕设参考。压缩包共约2000个文件,以1159个py源码文件为主体,辅以324个pyc编译文件、124个html页面模板、148张jpg图片及40个js、16个css等前端资源,另有csv、json、xml等数据与配置文件,整体约28.04MB,目录结构清晰,便于按模块查阅。目前已有147人学习下载。读者可获得完整可运行的推荐系统实现、论文文档与项目说明,理解协同过滤的算法落地过程,并参考其前后端组织方式与数据处理流程,快速搭建自己的毕设项目。
1. 基于协同过滤的电影推荐系统:从零到跑通的完整路径
很多同学做毕业设计时,第一反应是找个现成模板改改界面就交差。但如果你选的是「基于协同过滤的电影推荐系统Python实现源码+论文」这个方向,光改界面是过不了答辩的——老师一定会问你:推荐结果怎么来的?相似度怎么算的?冷启动怎么处理的?这些问题答不上来,源码再漂亮也没用。
这个方案的核心其实就三件事:用MovieLens数据集做基础、用协同过滤算法算出推荐列表、用Python把整条链路串起来。它适合计算机相关专业的本科毕业生,也适合想入门推荐系统的Python学习者。你不需要深度学习框架,不需要GPU,一台普通笔记本就能跑通全流程。接下来我会把数据准备、算法实现、参数调优、论文写作要点全部拆开讲清楚,让你不仅能跑出结果,还能讲明白每一步为什么这么做。
2. 数据准备与相似度计算:推荐系统的地基怎么打
2.1 MovieLens数据集的选择与加载
做电影推荐,数据源基本绕不开MovieLens。这个数据集由GroupLens实验室维护,有多个规模版本。毕业设计我一般推荐用ml-latest-small,包含约10万条评分、600多个用户、9000多部电影。数据量适中,跑一次完整实验几分钟就能出结果,不会让你在调试阶段等得心焦。
数据集核心文件是两个CSV:ratings.csv存用户对电影的评分(userId, movieId, rating, timestamp),movies.csv存电影元信息(movieId, title, genres)。加载时用pandas最直接:
import pandas as pd # 加载评分数据,指定数据类型可以加快读取速度 ratings = pd.read_csv('ml-latest-small/ratings.csv', dtype={'userId': 'int32', 'movieId': 'int32', 'rating': 'float32'}) # 加载电影信息 movies = pd.read_csv('ml-latest-small/movies.csv', dtype={'movieId': 'int32', 'title': 'str', 'genres': 'str'}) # 快速看一眼数据规模和分布 print(f"评分总数: {len(ratings)}") print(f"用户数: {ratings['userId'].nunique()}") print(f"电影数: {ratings['movieId'].nunique()}") print(f"评分均值: {ratings['rating'].mean():.2f}") print(f"评分标准差: {ratings['rating'].std():.2f}")这段代码做了三件事:读取两个核心文件、指定dtype减少内存占用、输出基本统计量。参数上注意userId和movieId用int32就够了,rating用float32,这样10万条数据占内存不到5MB。如果你用默认的int64和float64,内存会翻倍,虽然10万条无所谓,但养成习惯对以后处理大数据集有好处。
跑完你应该看到评分均值在3.5左右,标准差约1.04。这个分布说明用户整体偏好评,但方差不大,意味着大部分评分集中在3到4分之间。这对后续计算相似度有影响——评分区分度不高时,余弦相似度和皮尔逊相似度的差异会缩小。
2.2 用户-物品评分矩阵的构建
协同过滤的核心数据结构是用户-物品评分矩阵。行是用户,列是电影,格子里是评分。但这个矩阵极其稀疏——10万条评分放在600×9000的矩阵里,填充率不到2%。构建时用pivot_table:
# 构建用户-物品评分矩阵,缺失值填0 user_item_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) print(f"矩阵形状: {user_item_matrix.shape}") print(f"稀疏度: {1 - (ratings.shape[0] / (user_item_matrix.shape[0] * user_item_matrix.shape[1])):.4f}")这里有个关键决策:缺失值填0还是填均值?填0的逻辑是“用户没看过这部电影,评分为0”,这在计算余弦相似度时会把未评分当作低分处理,可能引入偏差。更合理的做法是只对有共同评分的物品计算相似度,或者用均值填充。我一般建议在论文里两种都跑一下做对比,这本身就是很好的实验章节素材。
稀疏度算出来大概在0.98左右,也就是说98%的格子是空的。这个数字要写进论文,因为它直接解释了为什么需要推荐系统——用户面对9000部电影根本无从选起。
2.3 相似度计算方法与选型对比
相似度计算是协同过滤的灵魂。常用的有三种:余弦相似度、皮尔逊相关系数、调整余弦相似度。它们各有适用场景,不是随便选一个就行。
余弦相似度把每个用户的评分向量看作高维空间中的向量,计算夹角的余弦值。公式简单,但对未评分项填0敏感。皮尔逊相关系数先减去用户均值再算相关,能消除用户评分尺度差异——有人习惯打高分有人习惯打低分,皮尔逊能修正这个偏差。调整余弦相似度则减去物品均值,适合物品评分偏差大的场景。
from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 基于用户的余弦相似度 user_similarity = cosine_similarity(user_item_matrix) print(f"用户相似度矩阵形状: {user_similarity.shape}") # 基于物品的余弦相似度(转置后计算) item_similarity = cosine_similarity(user_item_matrix.T) print(f"物品相似度矩阵形状: {item_similarity.shape}") # 皮尔逊相似度需要手动实现,因为要处理共同评分 def pearson_similarity(matrix): """计算用户间的皮尔逊相似度,只考虑共同评分项""" n_users = matrix.shape[0] sim = np.zeros((n_users, n_users)) for i in range(n_users): for j in range(i+1, n_users): # 找出两个用户都评过分的电影 mask = (matrix[i] > 0) & (matrix[j] > 0) if mask.sum() < 2: # 共同评分少于2个无法计算相关 continue r_i = matrix[i][mask] r_j = matrix[j][mask] sim[i][j] = np.corrcoef(r_i, r_j)[0, 1] sim[j][i] = sim[i][j] return sim余弦相似度用sklearn一行搞定,但皮尔逊需要手动处理共同评分掩码。注意mask.sum() < 2这个判断——如果两个用户只共同评过一部电影,相关系数无法计算(分母为0),必须跳过。这个细节很多开源代码没处理,导致结果里出现NaN。
选型建议:如果你的论文要做对比实验,三种都算一遍,用RMSE和MAE评估。根据我的经验,在MovieLens上调整余弦相似度通常略优于普通余弦,皮尔逊在用户评分尺度差异大时表现更好。但差距不会特别大,所以论文里重点不是“哪个最好”,而是“为什么在不同场景下表现不同”。
3. 协同过滤算法实现:UserCF与ItemCF的代码落地
3.1 基于用户的协同过滤(UserCF)完整实现
UserCF的逻辑是:找到和目标用户口味相似的一群人,把他们喜欢但目标用户没看过的电影推荐过来。核心步骤有四步:算用户相似度、找K个最近邻、加权预测评分、按预测分排序推荐。
import numpy as np from sklearn.metrics.pairwise import cosine_similarity class UserCF: def __init__(self, k_neighbors=20, n_recommendations=10): self.k = k_neighbors # 最近邻数量 self.n = n_recommendations # 推荐电影数量 self.user_sim = None self.matrix = None def fit(self, user_item_matrix): """训练:计算用户相似度矩阵""" self.matrix = user_item_matrix.values self.user_sim = cosine_similarity(self.matrix) # 对角线设为0,避免自己和自己最相似 np.fill_diagonal(self.user_sim, 0) return self def recommend(self, user_id, user_index): """给指定用户生成推荐列表""" # 获取该用户的相似度向量 sim_scores = self.user_sim[user_index] # 找到K个最相似的用户(排除自己) top_k_users = np.argsort(sim_scores)[::-1][:self.k] # 加权预测评分 user_ratings = self.matrix[user_index] predicted = np.zeros(self.matrix.shape[1]) sim_sum = np.zeros(self.matrix.shape[1]) for neighbor in top_k_users: neighbor_ratings = self.matrix[neighbor] # 只考虑邻居评过分的电影 mask = neighbor_ratings > 0 predicted[mask] += sim_scores[neighbor] * neighbor_ratings[mask] sim_sum[mask] += abs(sim_scores[neighbor]) # 避免除零 sim_sum[sim_sum == 0] = 1 predicted = predicted / sim_sum # 只推荐目标用户没看过的电影 unseen_mask = user_ratings == 0 predicted[~unseen_mask] = -1 # 取Top-N top_items = np.argsort(predicted)[::-1][:self.n] return top_items, predicted[top_items]k_neighbors这个参数控制推荐多样性——K越大,推荐越保守但可能更准;K越小,推荐越个性化但可能不稳定。我一般从10开始试,逐步加到50,看RMSE的变化曲线。n_recommendations就是最终给用户看的列表长度,毕业设计里设10比较合适,论文里可以展示Top-10的推荐结果。
注意np.fill_diagonal(self.user_sim, 0)这行——如果不把对角线置零,每个用户和自己相似度是1,找最近邻时永远第一个是自己,推荐结果就废了。这是新手最容易翻车的地方之一。
3.2 基于物品的协同过滤(ItemCF)实现与差异
ItemCF的逻辑反过来:先算电影之间的相似度,然后看用户看过哪些电影,推荐和这些电影相似的。ItemCF通常比UserCF更稳定,因为电影之间的相似度不会因为用户数量变化而剧烈波动。
class ItemCF: def __init__(self, k_neighbors=20, n_recommendations=10): self.k = k_neighbors self.n = n_recommendations self.item_sim = None self.matrix = None def fit(self, user_item_matrix): """训练:计算物品相似度矩阵""" self.matrix = user_item_matrix.values # 转置后计算物品间相似度 self.item_sim = cosine_similarity(self.matrix.T) np.fill_diagonal(self.item_sim, 0) return self def recommend(self, user_index): """基于用户历史评分生成推荐""" user_ratings = self.matrix[user_index] rated_items = np.where(user_ratings > 0)[0] if len(rated_items) == 0: return np.array([]), np.array([]) # 对每个未评分的物品,计算它与用户已评分物品的加权相似度 scores = np.zeros(self.matrix.shape[1]) for item in rated_items: # 该物品与其他所有物品的相似度 sim_vector = self.item_sim[item] # 加权:用户评分 × 物品相似度 scores += user_ratings[item] * sim_vector # 排除已评分的物品 scores[rated_items] = -1 top_items = np.argsort(scores)[::-1][:self.n] return top_items, scores[top_items]ItemCF和UserCF最大的区别在推荐解释性上。UserCF可以说“和你相似的用户也喜欢这部电影”,ItemCF可以说“因为你喜欢《星球大战》,所以推荐《星际迷航》”。论文里做用户调研时,ItemCF的解释通常更容易被接受。
两个算法在MovieLens上的表现:UserCF的RMSE大约在0.95-1.0之间,ItemCF在0.90-0.95之间。ItemCF略好,但差距不大。实际选型还要看场景——用户数远大于物品数时用ItemCF更划算,因为物品相似度矩阵更小。
3.3 推荐结果生成与评估指标计算
跑出推荐列表只是第一步,毕业设计必须要有量化评估。常用的指标是RMSE(均方根误差)和MAE(平均绝对误差),它们衡量预测评分和真实评分的偏差。
from sklearn.model_selection import train_test_split def evaluate_recommendation(ratings, test_size=0.2): """划分训练测试集并评估""" # 按时间戳排序后划分,模拟真实场景 ratings_sorted = ratings.sort_values('timestamp') train, test = train_test_split(ratings_sorted, test_size=test_size, random_state=42) # 构建训练集矩阵 train_matrix = train.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) # 训练UserCF cf = UserCF(k_neighbors=20) cf.fit(train_matrix) # 在测试集上评估 predictions, actuals = [], [] user_id_to_index = {uid: idx for idx, uid in enumerate(train_matrix.index)} movie_id_to_index = {mid: idx for idx, mid in enumerate(train_matrix.columns)} for _, row in test.iterrows(): uid, mid, true_rating = row['userId'], row['movieId'], row['rating'] if uid not in user_id_to_index or mid not in movie_id_to_index: continue u_idx = user_id_to_index[uid] m_idx = movie_id_to_index[mid] # 预测该用户对该电影的评分 sim_scores = cf.user_sim[u_idx] top_k = np.argsort(sim_scores)[::-1][:cf.k] neighbor_ratings = cf.matrix[top_k, m_idx] neighbor_sims = sim_scores[top_k] mask = neighbor_ratings > 0 if mask.sum() == 0: continue pred = np.sum(neighbor_sims[mask] * neighbor_ratings[mask]) / np.sum(np.abs(neighbor_sims[mask])) predictions.append(pred) actuals.append(true_rating) rmse = np.sqrt(np.mean((np.array(predictions) - np.array(actuals)) ** 2)) mae = np.mean(np.abs(np.array(predictions) - np.array(actuals))) return rmse, mae rmse, mae = evaluate_recommendation(ratings) print(f"RMSE: {rmse:.4f}, MAE: {mae:.4f}")这段评估代码有几个关键点:按时间戳排序再划分,避免用未来数据预测过去;只评估训练集中出现过的用户和电影;预测时只考虑有共同评分的邻居。RMSE在0.95左右、MAE在0.75左右算是正常水平。如果RMSE超过1.2,说明参数或数据处理有问题。
论文里除了RMSE和MAE,还可以加准确率、召回率、F1值。但注意推荐系统的准确率和分类问题不一样——推荐10部电影,用户实际看了3部,准确率就是30%。这些指标的计算方式要在论文里写清楚,答辩老师很可能会问。
4. 避坑与排查:协同过滤落地时最容易翻车的五个地方
4.1 冷启动问题导致新用户推荐为空
现象:新注册用户没有任何评分记录,推荐列表返回空数组或随机结果。
原因:协同过滤完全依赖历史评分,没有评分就无法计算相似度。这是算法的固有缺陷,不是代码bug。
解决:在推荐流程最前面加一个兜底策略——检测到用户评分记录少于5条时,直接返回热门电影Top-N。热门电影按评分人数和平均分加权排序,公式是score = avg_rating * log(rating_count)。这个兜底逻辑要写进论文的“冷启动处理”章节,是加分项。
4.2 相似度矩阵对角线未置零导致推荐自己
现象:推荐结果里出现用户已经看过的电影,或者推荐列表第一位永远是用户自己。
原因:cosine_similarity计算时,用户和自己的相似度是1,如果不把对角线置零,找最近邻时第一个永远是自己。
解决:np.fill_diagonal(sim_matrix, 0)这一行不能省。另外在生成推荐时,要把用户已评分的电影从候选集中排除,用predicted[user_ratings > 0] = -1实现。
4.3 数据稀疏导致相似度计算出现NaN
现象:相似度矩阵里出现NaN,后续推荐结果全部异常。
原因:两个用户没有共同评分电影时,皮尔逊相关系数的分母为0,numpy会返回NaN或报warning。
解决:计算前检查共同评分数量,少于2个直接跳过,相似度保持为0。代码里加if mask.sum() < 2: continue。另外可以用np.nan_to_num()做后处理,把NaN替换为0。
4.4 训练测试集划分不当导致评估结果虚高
现象:RMSE低到0.5以下,看起来效果极好,但实际推荐质量很差。
原因:随机划分训练测试集时,同一个用户的评分可能同时出现在训练集和测试集中,模型“见过”测试数据,评估结果失真。
解决:按时间戳排序后划分,用前80%时间的数据做训练,后20%做测试。或者用留一法——每个用户最后一个评分做测试,其余做训练。这样评估结果才可信。
4.5 推荐列表多样性不足
现象:推荐给用户的10部电影全是同一个系列或同一个类型,用户觉得没新意。
原因:余弦相似度倾向于推荐热门物品,导致推荐结果集中在头部电影。
解决:在排序阶段引入多样性惩罚——对同一类型的电影做降权,或者用MMR(最大边际相关性)算法平衡相关性和多样性。简单做法是:推荐列表里同一类型的电影不超过3部。这个改进可以写在论文的“算法优化”部分。
5. 论文写作与答辩准备:让源码和文档互相支撑
5.1 论文结构怎么对应代码模块
毕业设计论文和源码要能互相印证。我建议论文按这个结构写:第一章绪论讲推荐系统背景和协同过滤的研究现状;第二章相关技术介绍Python、pandas、sklearn和协同过滤原理;第三章需求分析写功能需求(用户管理、评分、推荐)和非功能需求(响应时间、准确率);第四章系统设计画架构图和数据库ER图;第五章详细实现对应UserCF和ItemCF的代码逻辑;第六章实验与评估放RMSE、MAE对比表和参数调优曲线;第七章总结与展望。
关键技巧:论文里的每个公式都要能在代码里找到对应实现。比如余弦相似度公式旁边标注“对应代码第X行”,答辩老师看到这个会觉得你确实自己动手了。
5.2 答辩时老师最可能追问的五个问题
根据我见过的答辩现场,老师最爱问这几个:为什么选协同过滤而不是深度学习?K值怎么确定的?冷启动怎么处理的?RMSE多少算好?推荐结果怎么展示给用户?前四个前面都讲过了,第五个建议做一个简单的Web界面,用Flask或Streamlit都行,能输入用户ID、显示推荐列表和海报就够了。不需要多漂亮,能跑通就行。
5.3 从毕业设计到可展示项目的最后一公里
如果你的目标不只是过答辩,还想把这个项目写进简历,那需要多做三件事:第一,把代码整理成模块化的包,有清晰的README和requirements.txt;第二,加一个简单的Web演示界面,截图放简历里;第三,把实验对比结果整理成表格,面试时能说清楚“我对比了三种相似度、两种算法、五个K值,最终选了ItemCF+K=30”。这比只写“实现了推荐系统”有说服力得多。
提示:论文查重时注意,协同过滤的公式和原理描述容易重复,建议用自己的话重新组织,代码注释也要改写成个人风格。
我自己做第一个推荐系统时,光顾着调算法参数,忘了处理冷启动,结果演示时新用户登录后推荐列表一片空白,当场翻车。后来加了热门兜底才救回来。希望你不用经历这个尴尬。把兜底逻辑和边界情况处理好,比追求算法精度更能体现工程能力。希望帮到你。
本文还有配套的精品资源,点击获取