简介:推荐系统作为信息过滤的重要手段,在视频、电商等场景中广泛应用。协同过滤算法通过分析用户历史行为,计算相似度并预测偏好,是构建个性化推荐的核心方法之一。Python凭借丰富的数据处理库和成熟的Web框架Django,成为快速实现推荐系统原型的理想技术栈。本文从协同过滤原理出发,讲解如何基于Python+Django搭建一个完整的电影推荐视频网站,涵盖评分矩阵构建、用户相似度计算、预测评分生成、Django工程落地以及数据库脚本设计等关键环节,并总结了开发过程中的常见问题与答辩要点,适合毕业设计及推荐系统入门实践参考。 毕业设计如果用自己熟悉的领域当切入口,确实比凭空选方向靠谱得多。我第一次接触这个基于Python+Django的协同过滤电影推荐视频网站时,最大的感受是:这题再适合毕设不过,但前提是你没有把它做成一坨拼凑的Demo。多数同学在选这个题目的时候,心里想的是"推荐算法听起来有深度,Django又能出网站,刚好还能配数据库脚本",真正动手才发现,算法讲得浅了显得没工作量,网站做得糙了又容易被抽检到。这篇文章我会把从数据准备、协同过滤算法实现、Django工程落地到答辩演示的完整链路拆开来讲,顺便把我在实际开发中踩过的坑一并交代清楚。内容适合正在准备毕设、或者想快速搭出一个"有点东西"的推荐系统原型的同学参考,不需要你有多深的算法基础,但希望你愿意自己动手敲代码。
1. 选题动机与系统全景:这个毕设题目到底在考核什么
1.1 为什么"电影推荐"是简历和答辩两头通吃的选题
选毕设题目有个隐蔽的评分逻辑:导师和评阅老师看的不是你的功能多花哨,而是"你在这套系统里做了什么有门槛的事"。普通的增删改查网站早就没有区分度了,但加入推荐算法之后,整个系统就从一个"管理系统"升级成了"有智能决策能力的应用平台",这在毕业设计的评分维度里属于算法设计、应用创新和工程实现都有涉及的类型。
电影推荐又比商品推荐、新闻推荐更适合学生做,原因有三个。第一,电影数据容易获得,MovieLens数据集提供了大量匿名的用户评分记录,字段清晰、格式统一,不用自己去清洗那些乱七八糟的电商数据。第二,电影内容本身好解释,推荐结果的评估可以直接看是不是同一类型、同一年代、同一个导演的作品,答辩时展示效果直观。第三,视频网站是大家日常都有使用经验的场景,界面做出来像一个"能用的产品"的门槛很低,不用额外解释业务规则。
一个容易被忽视的点是,推荐算法在整个系统里的占比不需要太高,但必须能被清楚地讲出来。我见过太多同学把协同过滤写成了一个"相似用户看过的电影取交集"的玩具代码,这样答辩被追问到公式推导时很容易露馅。正确的做法是:算法真正跑起来,并且你能够说清楚相似度怎么算、预测评分怎么算、为什么要这样做。这也是我这篇文章想重点解决的事。
1.2 技术选型:Python、Django、协同过滤分别扮演什么角色
整个系统的分工可以用一句话概括:Python负责数据清洗和算法实现,Django负责把算法结果变成一个可视化的网站,协同过滤算法负责生成"推荐列表"这个核心卖点,数据库脚本负责让系统在任何一台机器上都能快速复现。
选Python不选Java的原因很简单,Python的数据处理生态太成熟了。pandas能快速搞定评分矩阵的构建,numpy能直接做向量化的相似度计算,scikit-learn虽然自带了一些推荐相关的工具,但毕设场景下我建议你自己写协同过滤的核心逻辑,因为这个过程本身就是工作量的一部分。Django则是Python Web框架里"开箱即用"程度最高的选择,自带Admin后台、ORM数据库映射、模板引擎和用户认证,这几个组件刚好覆盖电影网站所需要的用户管理、影片管理和页面渲染,省去了大量重复造轮子的时间。
关于数据库,我推荐的组合是MySQL或SQLite二选一,具体看你们实验室的习惯。既然题目明确要求提供数据库脚本,那就说明你最终要能导出一份结构完整、可复现的建表和初始化数据脚本。我的建议是开发阶段用SQLite跑通功能,因为Django对SQLite的支持是零配置的,不用安装数据库服务;但是毕业设计提交的时候,最好把数据迁到MySQL,然后导出一份完整的.sql文件。这个迁移过程本身不复杂,但能体现你对数据库操作的基本功。
1.3 项目交付物的完整清单
当你说"内含Python完整源代码和数据库脚本"的时候,交付物要有明确的边界。不要只在论文里提一句"系统包含推荐功能",交上去的东西至少要包含以下几类:
- 完整的Django项目源码,包括settings配置、app目录、模板文件、静态文件和依赖清单requirements.txt
- 数据库脚本,既要包含建库建表的DDL语句,也要包含可直接导入的初始数据INSERT语句
- 推荐算法的核心模块,最好独立成一个Python文件或者一个Django app中的service模块,让评阅老师能快速找到算法位置
- 一个简短的部署说明文档,写清楚Python版本、依赖安装命令、数据导入命令和启动命令
这部分经验是我吃了亏才总结出来的。第一次交中期材料的时候,我只交了一个项目压缩包,里面没有requirements.txt,也没有数据导入脚本,老师在自己的电脑上根本跑不起来,当场就被打回整改。后来我把环境依赖、数据库脚本、README全部补齐,再提交就顺利多了。毕业设计本质上是一次工程交付,能不能在你的环境之外复现,是很重要的一条线。
2. 协同过滤算法的最小可用实现:从原理到能跑的代码
2.1 基于用户的协同过滤为什么更适合毕设场景
协同过滤算法主要有两个流派:基于用户的(User-based)和基于物品的(Item-based)。基于用户的思路是"找到和你口味相似的人,把他们喜欢的而你没看过的东西推荐给你";基于物品的思路是"找到和你看过的电影相似的电影推荐给你"。在毕设场景里,我强烈建议优先实现基于用户的协同过滤,原因有三个。
第一,原理更容易讲清楚。"人以群分"这个概念不需要任何数学基础也能理解,答辩时你从使用场景切入,导师很容易跟上你的思路;而基于物品的推荐虽然实际应用更广,但在讲"物品相似度"的时候,容易被追问"相似度到底怎么定义",解释成本更高。第二,代码实现直接对应评分矩阵的运算逻辑,方便在论文里画出清晰的流程图和数据流图。第三,基于用户的协同过滤天然需要一个"当前用户"作为输入,这和网站的登录体系、用户行为记录能够自然结合,不至于让算法部分和Web部分像两个孤立模块。
当然,基于用户的协同过滤也有一个众所周知的短板:冷启动。新用户没有任何评分行为时,系统无法算相似度。解决方法是给它一个默认的"热门榜"兜底,让冷启动用户看到的是全网评分最高的电影,之后随着用户评分行为增多,再逐步切换成个性化推荐。这个"冷启动+个性化"的组合策略,是答辩时的一个加分项,因为它说明你考虑到了实际工程问题。
2.2 评分矩阵、相似度计算与预测打分的核心代码
这一节我给出一个可以直接跑的最小实现,不依赖任何大型框架,只用pandas和numpy就能完成。整个流程分三步:构建用户-电影评分矩阵,计算用户之间的相似度,为目标用户生成推荐。
先看数据形态。假设我们有三张表:用户表、电影表、评分表。评分表里至少要有user_id、movie_id、rating三个字段。构建评分矩阵的代码如下:
import pandas as pd import numpy as np # ratings是评分表DataFrame,列名为user_id, movie_id, rating # 构建用户-电影评分矩阵,行是用户,列是电影,空值填0 rating_matrix = ratings.pivot_table( index='user_id', columns='movie_id', values='rating', fill_value=0 ) # 转成numpy数组方便计算 matrix = rating_matrix.values接下来是用户相似度计算。常用的相似度指标有余弦相似度和皮尔逊相关系数。在评分数据场景下,皮尔逊相关系数比余弦相似度更合适,因为它对每个用户的评分习惯做了归一化——有人习惯打4分以上,有人习惯把好片和烂片拉开差距,皮尔逊系数能消除这些尺度差异。
from sklearn.metrics.pairwise import cosine_similarity # 计算用户间的皮尔逊相关系数矩阵 # 为了处理0值和少量评分的情况,这里用numpy手写实现 def pearson_similarity(matrix): n_users = matrix.shape[0] sim_matrix = np.zeros((n_users, n_users)) # 每一行是一个用户在这个矩阵中的索引 for i in range(n_users): for j in range(n_users): if i == j: sim_matrix[i][j] = 1.0 continue # 取出两个用户都评过分的电影 mask = (matrix[i] > 0) & (matrix[j] > 0) if np.sum(mask) == 0: sim_matrix[i][j] = 0 continue vec_i = matrix[i][mask] vec_j = matrix[j][mask] if np.std(vec_i) == 0 or np.std(vec_j) == 0: sim_matrix[i][j] = 0 continue sim_matrix[i][j] = np.corrcoef(vec_i, vec_j)[0][1] return sim_matrix最后一步是生成推荐。对目标用户尚未看过的每一部电影,把所有对该电影有评分的用户按相似度加权求和,得到预测评分,然后排序取TopN即可。
def recommend(user_id, rating_matrix, sim_matrix, top_n=10): user_idx = list(rating_matrix.index).index(user_id) # 找到当前用户没有评分的电影 unseen = rating_matrix.columns[rating_matrix.loc[user_id] == 0] scores = [] for movie in unseen: movie_idx = list(rating_matrix.columns).index(movie) # 取出所有对该电影有评分的用户的评分向量 rated_users = rating_matrix[movie] > 0 # 只保留相似度大于0的用户 valid = rated_users & (sim_matrix[user_idx] > 0) if np.sum(valid) == 0: scores.append((movie, 0)) continue # 加权平均:相似度 * 评分 / 相似度之和 sim_values = sim_matrix[user_idx][valid] movie_ratings = rating_matrix[movie][valid].values pred = np.dot(sim_values, movie_ratings) / np.sum(sim_values) scores.append((movie, pred)) scores.sort(key=lambda x: x[1], reverse=True) return scores[:top_n]这套代码在数据量几千条时性能还不错,但如果评分记录到了几十万条,双层循环计算用户相似度就会很慢。我在毕设里用的解法是,把相似度计算封装成一个后台任务,每天凌晨跑一次,结果存到Redis或者数据库的推荐结果表里,用户访问推荐页时直接读结果,而不是实时计算。这个优化在毕设里是可选项,但如果你能写进论文里,进度感和工程深度都会提升一个档次。
2.3 冷启动和“新电影无人评分”问题的毕业设计版处理
推荐系统最怕的是新用户和新物品。新用户的问题上面已经说了,用热门榜兜底。新电影的问题更隐蔽:一部电影刚入库,评分人数为0,协同过滤永远不会推荐它,但如果它是刚上线的高热度电影,不推荐反而显得系统不智能。我在这个项目里采取了一个很朴素的混合策略:
- 评分人数大于等于5的电影,才纳入协同过滤推荐候选池
- 评分人数不足5的新电影,靠标签相似度做补充——从电影表里取出类型、导演、演员等属性,和用户最近看过的电影比较,给出基于内容的简单推荐
- 最终推荐页面上,协同过滤结果放前10个,内容推荐结果放前3个,两版合并
这种混合推荐方案算法含量不高,但胜在靠谱,而且答辩的时候你可以明确说出来:这是为了解决冷启动问题做的工程妥协。导师会认为你想过实际应用,而不是只会调包。
3. Django工程落地的关键模块设计:你不能只写一个算法Demo
3.1 数据模型设计:用户、电影、评分、推荐结果表
算法再好,最终要落到数据库里。我设计的Django模型有四个核心部分,这里直接给出模型结构思路。
用户模型直接复用Django内置的auth.User,在此基础上通过OneToOne扩展一个Profile,存放用户的头像、个性签名等信息,方便后续扩展。电影模型Movie是另一张核心表,字段包括title、poster_url、description、release_date、director、actors、genres、rating_average、rating_count。其中genres我推荐用逗号分隔的字符串存,虽然不那么符合关系数据库范式,但在毕设场景里查询起来最省事,不用为了多对多关系引入额外的关联表。
评分模型Rating是算法输入的核心,字段为user、movie、score、comment、created_at。这是一个多对多的中间表,需要设置unique_together约束,确保同一个用户对同一部电影只能有一条评分记录。推荐结果表Recommendation则是一个缓存表,字段包括user、movie、score、rank、created_at。算法离线计算产生的推荐结果直接写进这张表,Django的推荐视图只查它,这是性能和逻辑解耦的关键。
3.2 推荐服务的接口封装与后台任务设计
推荐逻辑不能散落在视图函数里,单独放在一个services/recommend.py模块中是更清晰的工程组织方式。我习惯的做法是:views.py只负责HTTP请求和响应,推荐相关的计算全部封装成一个服务类,例如:
# services/recommend.py class Recommender: def __init__(self): self.rating_matrix = self._build_matrix() self.sim_matrix = self._calc_sim() def _build_matrix(self): ratings = Rating.objects.all().values('user_id', 'movie_id', 'score') # 转DataFrame后生成矩阵 return matrix def recommend_for_user(self, user_id, top_n=10): cached = Recommendation.objects.filter(user_id=user_id).order_by('rank') if cached: return cached results = compute_online(user_id) for rank, (movie, score) in enumerate(results): Recommendation.objects.update_or_create(...) return results在线推荐和离线预计算的取舍上面已经提到,实际开发时我建议走混合路线:普通用户直接读缓存推荐表,评分行为刚发生变化后的首次刷新触发一次在线计算,但这个在线计算只对当前用户跑,避免全矩阵重新算一遍。
3.3 视频播放页与前端模板的组织
视频网站不能只有列表页,播放页才是用户停留的核心。这个项目的播放页需要处理三件事:引入视频播放器、展示电影详情和推荐理由、商机推荐列表和评论区。前端我用的是Django模板+少量JavaScript,没有引入复杂的前端框架。视频播放器推荐用video.js或者Django默认支持的HTML5 video标签,本地开发阶段放几个准备好的MP4测试片段就行,不需要真正的流媒体服务器。
播放页有一个容易被忽略的细节:推荐理由的展示。推荐系统不能只告诉用户"我们推荐你看《霸王别姬》",还要在推荐理由里写"因为你看过《活着》,和你品味相似的用户也喜欢《霸王别姬》"。我实现了一个简单的方法,在生成推荐结果时把相似用户的信息一并存储。展示推荐理由不仅让页面看起来更专业,也方便你在系统演示时解释推荐逻辑,一举两得。
4. 数据库脚本与初始数据准备:做不好这一步很容易翻车
4.1 数据库脚本里该放什么、不该放什么
题目强调"内含数据库脚本",说明这不仅是项目的一个附属品,还是一份重要的交付物。很多同学的数据库脚本只写了建表语句,没有初始数据,老师拿过来一运行,页面全是空的,体验非常差。我的建议是脚本至少包含三层:
- DDL层:建库、建表、字段注释、索引、唯一约束
- DML层:初始数据,包括电影、分类、模拟用户、模拟评分
- 可选存储过程或视图层:如果你在系统里用了复杂的统计查询,可以写成视图或存储过程,也能体现数据库功底
不需要放进去的是大段的测试数据和无意义的垃圾数据。数据库脚本的长度不是重点,重点是导入之后系统能正常运行,而且数据量和数据形态能支撑算法演示。我最终提交的脚本里包含了大约500部电影和1000条用户评分记录,这个量级不算大,但足够演示协同过滤的效果,也不会让老师在导入时等太久。
4.2 数据来源的三条路:爬虫、公开数据集和手工造数
电影数据哪里来?这个问题看似简单,实际是很多同学停滞不前的地方。我总结出一条经验:不要想着去爬那些大型视频网站,一是反爬策略复杂,二是版权和数据安全问题麻烦,三是爬下来的数据质量参差不齐。毕设场景下,三条路是可靠的。
第一条路是直接用公开数据集MovieLens,它有不同规模的版本,最小的是100K评分数据,足够用了。这个数据集包含用户ID、电影ID、评分、时间戳,还有一些电影标题和类型的文件,格式非常干净。第二条路是找一个电影信息API,比如一些公开的开放接口,获取电影名称、导演、演员、海报地址等信息,存进自己的数据库。第三条路是手工造小规模数据,适合你只需要演示算法效果的场景,造几十个用户、几百条评分就够,但要保证评分分布合理,不能太稀疏。
我用的是第一条路和第二条路的结合:从MovieLens拿评分数据,再通过脚本批量更新电影详情字段。这样评分数据真实、电影信息也完整,演示效果会好很多。
4.3 从MovieLens到本地库:完整导入流程
导入流程我写成了一个独立的Python脚本import_data.py,放在项目根目录下。核心步骤是:读CSV文件、清洗字段、写入Django模型对应的表。
import csv from movies.models import Movie, Rating from django.contrib.auth.models import User def import_movies(csv_path): with open(csv_path, encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: Movie.objects.update_or_create( movie_id=row['movieId'], defaults={ 'title': row['title'], 'genres': row['genres'], } ) def import_ratings(csv_path): # 从评分文件读入,写入Rating表 ...有一个特殊的坑要提醒你:MovieLens里的用户ID是自己的一套编号,而Django自带的User表主键是自增的,两者不能直接对应。我当时的处理方式是单独建一张映射表,或者在MovieLens的用户ID基础上加一个偏移量,保证不冲突。这块在答辩的时候也很容易被问到,因为评阅老师会好奇你的用户数据是怎么产生的。
5. 开发过程中踩坑的记录:帮你省下至少一周的无头排查
5.1 Django版本与Python版本的匹配陷阱
这个坑几乎每个用Django的毕设党都会遇到。Django版本和Python版本是强绑定的,Django 2.x支持Python 3.5以上,Django 3.x支持Python 3.6以上,Django 4.x要求Python 3.8以上。很多同学的电脑上装的是Python 3.10甚至3.12,如果直接按网上的旧教程装一个Django 2.2,运行时会报各种不兼容错误,而且报错信息还不那么直观。
我建议直接用Python 3.10或者3.11搭配Django 4.2,这是一个长期维护版本,文档多、示例多、坑也被人踩得差不多了。requirements.txt一定要锁定版本号,不要写那种没有版本号的裸依赖,否则换一台机器运行时,所有依赖都会被装成最新版,系统行为可能变化。
5.2 协同过滤计算慢:数据量不大也一样卡
按理说几百条评分数据算相似度矩阵应该瞬间完成,但我在第一次实现时还是卡了好几秒,原因是我在每次请求推荐页面时都重新算了一遍全量相似度矩阵。问题不在算法复杂度,而在重复计算。解决方法是上面提到的推荐结果缓存表,同时把相似度计算改成了异步任务。Django里可以用Celery来实现异步,如果不想引入额外依赖,也可以用一个简单方案:把相似度计算的结果存成pickle文件,每天定时重新生成,用户请求直接读文件。
这个方法虽然不如Celery优雅,但胜在简单,而且对于毕设的数据量完全够用。答辩时你可以说"使用定时任务预计算推荐结果,并采用缓存策略提升系统响应速度",没有任何问题。
5.3 视频文件的存储与播放兼容性
视频文件是视频网站的灵魂,但也是坑最密集的地方。开发时要注意两点。第一,本地开发时,视频文件放在Django的media目录下,通过Django配置的MEDIA_URL进行访问。但如果视频文件很大,Django自带的开发服务器在处理并发请求时会非常吃力,所以演示时最好用几个小体积的MP4文件,不要放几个GB的1080p原片。第二,视频编码格式要注意浏览器兼容性。MP4的H.264编码是兼容性最好的,别的编码格式经常出现页面有播放器但画面打不开的情况。
我吃过一次亏:下载了一个高品质的MKV测试视频,用video.js播放时一直黑屏,查了半天才发现浏览器根本不支持MKV格式。后来统一转成H.264编码的MP4文件,一切正常。做毕设一定要记住,不要在产品形态上追求极致的格式支持,稳定、能演示、不翻车才是第一位。
5.4 编码问题:数据库里的中文乱码
数据库脚本导入时中文乱码是很常见的问题。要避开这个坑,有几个固定操作:MySQL建库时指定utf8mb4字符集,Django的数据库配置里加上OPTIONS,脚本文件保存为UTF-8无BOM格式。如果你在Windows下开发,用记事本或者某些默认编码的工具保存文件,很容易存入带BOM的UTF-8,导入MySQL时就会乱码。我的经验是,涉及中文数据的脚本文件,一律用VS Code重新保存为UTF-8格式。
另外,数据库脚本里的INSERT语句如果包含大量中文文本,建议在脚本开头加一句SET NAMES utf8mb4;,这样命令行导入时就不会出现乱码。
6. 答辩演示与材料提交:让系统在你的电脑之外也能复现
6.1 演示时的“预置数据”设计:什么场景最加分
答辩演示是最容易紧张、也最容易出问题的环节。我的一条核心经验是:在演示系统前,先在数据库里预置好一组“表演数据”,让推荐结果呈现出明显的个性化差异。什么意思呢?你可以准备两个测试账号,账号A看过的电影集中在悬疑片和犯罪片,账号B看过的电影集中在爱情片和喜剧片,这样两个账号登录后的推荐页就有截然不同的风格。演示时一登录,推荐列表明显不同,评委一眼就能看出推荐算法确实在工作。
如果演示时所有账号看到的都是同样的热门电影,评委就会怀疑你的算法是不是摆设。所以预置数据的核心目标就是让推荐结果有区分度。我的做法是建立一个seed 数据脚本,专门负责生成这些演示账号及其评分行为,且不会污染正式数据。
6.2 讲算法时最容易被追问的三个问题
答辩时只要你的题目里带“推荐”二字,评委提问的重点基本集中在算法部分。我梳理了三个高频问题,提前准备好答案就能稳定发挥。
第一个问题是:“你的相似度是怎么算的,为什么选皮尔逊相关系数而不是余弦相似度?”这个问题我们前面已经解释过,重点在于评分偏差的归一化。你要能用自己的话说清楚,不要背教材定义。
第二个问题是:“冷启动问题怎么解决?”一定要回答用户冷启动和物品冷启动两个方面,如果只回答用户冷启动,会被追问“新电影怎么办”。
第三个问题是:“你的系统是实时推荐还是离线推荐?”你要讲清楚自己用的混合策略:普通场景读缓存,评分行为变化后触发单用户在线重算。这个回答同时展示了算法能力和工程意识,是很加分的。
6.3 代码目录整洁度、注释规范与论文素材的对应关系
在最终提交材料时,我强烈建议把代码目录按照“数据导入脚本、算法模块、Web应用、文档”四个维度组织,而不是让所有文件堆在根目录里。评阅老师可能不会逐行看代码,但一定会先看目录结构,一个清晰的项目结构本身就是第一印象。
注释方面,我建议在核心算法代码里添加关键注释,讲清楚每一步的数学意义,这样论文里写算法章节时可以直接对照着写。不要追求满屏注释,而是要在相似度计算、预测评分、TopN排序这几个核心逻辑处写清注释。这些注释放在论文里、“实验过程”里都会成为你的素材。
另外一个小技巧:在Git仓库里维护一个CHANGELOG或者README,记录每一步开发过程和决策理由。答辩准备PPT时你会发现,这份文档比你的记忆可靠得多。
最后分享一个我个人的做法
如果你现在还在纠结“代码从哪里开始”,我建议第一步不要写Django,而是先把数据准备和算法跑通。用MovieLens的数据集,在Jupyter Notebook里先做出一个能生成推荐列表的脚本,然后再迁移到Django工程里。这个顺序能帮你把“算法”和“Web”两个相对独立的部分分别调通,避免在Django里同时排算法和框架的错,排查难度直接减半。
在我实际做这个项目的过程中,最花时间的其实不是算法本身,而是数据导入和工程组织。你只要把数据脚本、Django模型、推荐服务这三块理清楚,整个系统就已经成型了。剩下那些花哨的页面效果和额外的功能,都是锦上添花。把核心链路搞稳、搞透,比堆砌功能有用得多。
本文还有配套的精品资源,点击获取