ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

电影推荐与票房预测系统:Python+Flask+机器学习实战

电影推荐与票房预测系统:Python+Flask+机器学习实战 临近毕业那阵子我身边好几个同学都在为选题发愁。做纯网页没技术含量做算法又怕撑不起工作量做APP又来不及学新框架。我最后定下来的是“电影推荐与票房预测系统”——一个基于PythonFlask机器学习算法的多维度应用推荐和预测两条业务线放在同一个Web系统里既能展示算法能力又能体现工程落地水平。这个题目后来帮我顺利通过了答辩还被两个学弟要走了源码。如果你也在准备类似的毕业设计或者想做一个能写进简历的完整项目这篇东西应该能帮你少走不少弯路。先把这套系统能干什么说清楚用户注册登录后系统会基于他的历史评分记录从电影库中捞出一批他没看过但大概率会喜欢的影片管理员另外还有一个票房预测后台输入一部电影的属性特征比如类型、预算、上映档期、主演热度模型会给出一个预测票房区间。整个系统涉及数据采集与预处理、推荐算法、回归预测模型、Flask后端接口、前端可视化展示这几个完整的模块每一个都是面试官爱问、答辩老师爱听的东西。1. 为什么把推荐和票房预测放在同一个系统里很多人看到这个题目第一反应是两个功能扯在一起会不会很牵强其实恰恰相反把它们放在一起是有内在逻辑的。电影推荐解决的是“用户想看什么”的问题票房预测解决的是“这部片子能卖多少”的问题前者是消费侧后者是供给侧放到真实业务里这就是一个完整的电影产业数据闭环。1.1 选题价值一个系统覆盖算法和工程两大考核点毕业设计最怕的是什么是工作量看起来不够或者技术点太单一。推荐系统属于典型的“经典机器学习应用”可以写协同过滤、矩阵分解、内容相似度计算也可以上深度学习模型算法部分有充分的展示空间。票房预测则是标准的回归任务涉及特征工程、模型选择、评估指标和推荐系统的“排序/召回”逻辑正好形成互补。更关键的是Flask把这两个看起来偏算法的模块串联成了一个真正能跑起来的Web应用。答辩的时候老师想看的不只是你训练了一个模型而是数据怎么流转、模型怎么被调用、结果怎么展示。推荐和预测共用一个后端服务共用一套用户体系共用同一个数据库这比做两个毫不相干的小功能要完整得多。1.2 技术栈选型为什么是PythonFlask而不是其他组合选Python没有悬念机器学习生态就摆在那里pandas做数据处理、scikit-learn跑模型、Matplotlib画分布图一套下来非常顺。真正需要纠结的是Web框架我当时在Flask和Django之间犹豫了一阵。Django自带Admin后台、ORM、认证体系开发效率很高但问题在于它“太重”了很多东西是框架帮你做好的写在论文里不好展开答辨的时候老师一问“你这个用户认证是怎么实现的”你说“Django自带的”这个深度就不够。Flask足够轻量核心只有路由和请求处理ORM要自己接SQLAlchemy用户认证要自己写逻辑登录态要自己维护Session——听起来麻烦但每一个环节都能写进设计文档都是工作量。另外还有一层考虑Flask的接口写法非常直观一个app.route(/api/recommend)装饰器下面挂一个函数这个函数的内部逻辑就是调用算法模块前后端完全解耦。对于单人开发的毕设项目这种自由度比Django的“全家桶”模式更友好。2. 推荐系统核心模块三种算法从原理到代码落地推荐模块是整个系统里我花时间最多的地方。不是因为代码难写而是“效果”这件事很难量化。系统做的不是算法竞赛而是要让非专业的用户包括答辩老师直观感觉到“推荐结果好像还挺合理”这就需要在算法选型和结果呈现上花心思。2.1 数据基础MovieLens数据集与冷启动问题的处理思路推荐系统没有数据就是空中楼阁我用的是MovieLens 1M数据集包含约100万条评分记录、4000部电影和6000个用户。这个数据集的好处是格式干净、字段完整每个用户至少有过20条评分可以直接用来算用户相似度缺点是它只到2019年左右电影列表偏老新片很少。数据预处理有几个要点。原始ratings.dat文件的字段分隔符是::需要切成标准的三元组用户ID、电影ID、评分然后做一个映射表把原始ID换成连续整数这是为了后面建评分矩阵方便。还有一个细节是异常值过滤数据集里偶尔有重复评分记录pandas里drop_duplicates(subset[userId, movieId], keeplast)就能解决。冷启动问题我必须单独说一下因为这是毕业设计答辩时出现频率最高的问题。一个从来没有评分记录的新用户协同过滤算法是没办法给他做推荐的因为找不到相似用户。我的处理方式是在注册时强制用户先勾选喜欢和不喜欢的影片标签至少选择5个基于这些标签在内容推荐模块里给他初始化推荐列表等他有了评分行为之后再逐步切换到协同过滤的逻辑。这样既解决了冷启动又体现了一个“混合推荐”的设计思路论文里也多了可以展开的章节。2.2 基于用户的协同过滤相似度计算与评分预测基于用户的协同过滤UserCF的核心思想用一句话说就是“物以类聚、人以群分”找到和你口味最相似的一批用户看他们喜欢什么再把这些内容推荐给你。算法分成三步第一步构建用户-物品评分矩阵。用pandas的pivot_table或者scipy的稀疏矩阵都可以但数据量到了百万级之后直接用pandas操作效率会很差。我最后用的是scipy.sparse.csr_matrix用一个自定义的映射函数把(userId, movieId, rating)转成稀疏矩阵的行列索引。稀疏矩阵在内存和计算效率上都比普通的二维数组好得多。第二步计算用户相似度。常见的选择有皮尔逊相关系数、余弦相似度和调整余弦相似度。这里有个容易被忽略的细节如果直接用原始评分做余弦相似度等于默认所有用户的打分尺度一致但有些用户天生手松喜欢的打5分不喜欢的也打3分另一些用户手紧喜欢的才打3分这就导致相似度计算失真。标准做法是先把每个用户的评分做中心化处理减去他自己的平均分再算相似度这就是调整余弦相似度也是代码里实际采用的方案。第三步预测目标用户对未评分物品的评分。找到相似度最高的K个用户对这K个用户评分过的、目标用户没有评分的电影按相似度加权计算预测评分然后排序取Top-N。K值我经过对比测试选了30K太小推荐结果波动大K太大又会被“泛口味用户”稀释掉个性化。核心代码大致是这样一个结构def user_cf_recommend(user_id, top_n10, k_similar30): # 1. 取目标用户评分向量 target_vec rating_matrix.getrow(user_id).toarray().ravel() # 2. 计算与所有其他用户的调整余弦相似度 sims {} for other in range(n_users): if other user_id: continue vec rating_matrix.getrow(other).toarray().ravel() # 只取双方都有评分的电影计算避免稀疏区域干扰 mask (target_vec 0) (vec 0) if mask.sum() 5: continue norm_target target_vec[mask] - target_vec[mask].mean() norm_other vec[mask] - vec[mask].mean() denom np.linalg.norm(norm_target) * np.linalg.norm(norm_other) if denom 0: continue sims[other] np.dot(norm_target, norm_other) / denom # 3. 按相似度排序取前 k_similar 个用户 neighbors sorted(sims.items(), keylambda x: x[1], reverseTrue)[:k_similar] # 4. 候选物品 邻居评分过但目标用户未评分的电影 candidate_scores {} for other, sim in neighbors: vec rating_matrix.getrow(other).toarray().ravel() for movie_idx in np.where((vec 0) (target_vec 0))[0]: candidate_scores.setdefault(movie_idx, 0) candidate_scores[movie_idx] sim * vec[movie_idx] # 5. 按加权分排序返回片名列表 ranked sorted(candidate_scores.items(), keylambda x: x[1], reverseTrue) return [id2title[movie_id] for movie_id, _ in ranked[:top_n]]2.3 基于内容的推荐TF-IDF与余弦相似度的配合协同过滤有个天然的短板新电影没有任何用户评分永远不会被推荐出去这就是物品冷启动。解决思路是走内容推荐——不依赖用户行为而是直接分析电影本身的属性找到和用户历史喜欢电影“长得像”的影片。我的做法是把每部电影的标题、类型、导演、主演、简介拼成一个长文本用jieba库做中文分词然后通过TfidfVectorizer转成TF-IDF特征向量再计算电影之间的余弦相似度矩阵。用户对某部电影打过高分就把这部电影最相似的5部影片捞出来去掉已经看过的作为内容推荐结果。这里有很多细节值得展开。TF-IDF的含义是“词频-逆文档频率”如果一个词在某一部电影的简介里出现次数多但在所有电影简介里出现得少这个词就具备很强的区分度特征权重就高。Python里做这一步非常简洁from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity tfidf TfidfVectorizer(tokenizerjieba.lcut, max_features5000) tfidf_matrix tfidf.fit_transform(all_movie_texts) sim_matrix cosine_similarity(tfidf_matrix)计算得到的是一个4000×4000的相似度矩阵直接存成.npy文件系统启动时一次性加载避免每次请求都重新计算。这个预计算的思想很重要答辩的时候也很加分因为它体现了你考虑了“算法实时性和工程性能的权衡”。2.4 混合推荐策略把两个算法的结果融合起来最终线上跑的不是单一算法而是一个简单的混合策略如果用户评分记录大于等于10条推荐结果里协同过滤占7成、内容推荐占3成按位置交替排列如果评分记录少于10条则完全用内容推荐因为数据量太小算相似用户不靠谱。这个混合策略的实现比我预想的简单不需要复杂的加权排序逻辑直接按比例从两个算法结果里取数再合并就行def hybrid_recommend(user_id, top_n12): cf_list user_cf_recommend(user_id, top_ntop_n) cb_list content_based_recommend(user_id, top_ntop_n) # 7:3 交替融合 merged [] cf_idx, cb_idx 0, 0 for i in range(top_n): if i % 3 2 and cb_idx len(cb_list): merged.append(cb_list[cb_idx]); cb_idx 1 else: merged.append(cf_list[cf_idx]); cf_idx 1 return deduplicate(merged)[:top_n]这种策略不是最优解但对于毕业设计级别的项目已经足够。在实际的推荐系统里还有LGBM排序模型、DSSM双塔模型等更复杂的方案但那些属于加分项不属于必选项。把基础算法吃透、把混合策略讲清楚已经能体现出完整的算法思维。3. 票房预测模块从数据清洗到回归模型的全过程拆解票房预测和电影推荐在技术上有本质的区别推荐是排序问题票房是回归问题。回归问题的核心在于“特征决定上限”模型本身反而是相对标准化的部分。这个模块我花了很多时间在特征处理上也因为这个模块的严谨性答辩时老师追问了好几个问题都没有被难住。3.1 预测目标与特征体系设计首先要想清楚一个问题票房预测的输入和输出分别是什么输出可以直接定为“首周票房”或“总票房”但这个定义要慎重——如果预测的是总票房需要等电影下映才能拿到真实值做训练数据时效性会差如果预测首周票房那电影上映第一周结束后就能拿到标签更有利于扩充训练集。我最后选择的是首周票房以万元为单位做对数变换这样标签近似正态分布模型更容易收敛。特征体系我分了四类特征类别具体特征特征说明电影属性类型(one-hot)、时长、是否为续集、是否改编直接反映影片本身的吸引力制作信息预算(取对数)、制片国家/地区、出品公司热度大投资未必高回报但预算和宣发规模高度相关主创信息导演历史最高票房、主演历史平均票房、导演与主演合作次数主创的号召力是票房的重要解释变量档期与竞争上映月份、是否为黄金档、同档期竞争影片数量、同档期竞品平均票房票房受档期环境影响极大这四类特征合在一起大概是40维左右经过标准化之后喂给回归模型。特征设计的时候有一个很常见的坑有些特征含有未来信息比如“上映首日的口碑评分”这不叫特征叫作弊训练集里看起来效果很好实际预测时根本拿不到这个数。我当时犯过这个错后来花了不少时间才把这类特征从训练集里清干净。3.2 数据来源与清洗过程票房数据在国内没有官方开放接口我用的是一份从公开渠道整理的国产电影票房数据集涵盖2012年到2022年的1000多部电影包含片名、上映日期、类型、预算、导演、主演等字段。拿到手之后清洗工作占了很大比重常见问题有三类第一类是缺失值。导演和主演信息相对完整缺失最多的是预算字段大部分国产电影不公开制作成本。我最后用“同类型同时长影片预算的中位数”做了填充然后在特征里加了一个“预算是否缺失”的0/1标志位让模型自己学习缺失这个信息本身可能隐含的意义。第二类是异常值。某些中小成本文艺片的票房趋近于0还有个别票房特别高的现象级影片比如50亿级别的直接把标签分布拉得很偏。处理方式是给标签做np.log1p对数变换让分布变得集中。第三类是文本字段的编码。类型字段是多值的比如“剧情/爱情/历史”用MultiLabelBinarizer做多标签编码。主演字段的取值空间太大直接one-hot会生成几百维稀疏特征我改成只保留主演中“累计票房最高的一位”的历史票房中位数用这个数字作为主演号召力的代理变量。3.3 模型选型线性回归、树模型还是深度学习我在这个模块里对比了三种模型线性回归、随机森林、XGBoost最后选用了随机森林作为主要模型XGBoost作为对比模型写进了论文。对比测试的结果是这样的模型RMSE(万元)R²训练时间可解释性线性回归48210.3761s高随机森林32760.684~15s中等XGBoost31080.702~85s低随机森林和XGBoost差距不大但随机森林对特征缺省值更友好调参难度也更低对于我的数据规模和特征质量来说已经足够了。线性回归效果差是可以预料的因为票房和特征之间远不是线性关系比如黄金档期这个特征是0/1变量它对票房的影响在小成本影片和大制作影片身上完全不一样这种交互效应树模型天然能捕捉到。调参上我主要关注了三个超参数n_estimators树的数量设为300再高提升不明显反而增加计算量max_depth设为8防止过拟合min_samples_leaf设为5确保叶子节点有足够样本量。这里有个判断方法如果训练集R²远高于测试集R²说明过拟合应该降低深度或增大叶子节点最少样本数。模型训练完成后我用feature_importances_看了特征重要性排序排在前几位的是预算对数、导演历史最高票房、是否黄金档、主演历史票房中位数、是否续集这个排序很符合行业直觉写论文的时候也把这个结果放进去做了分析老师反馈不错。3.4 票房区间的可视化呈现与误差分析预测模块不只是输出一个数字我把它设计成“预测区间 置信度”这样显得更专业。随机森林每棵树的预测结果其实是一个分布我取所有树预测值的5%和95%分位数作为区间上下界如果区间跨度太大说明模型对这部片子信心不足前端页面上会显示“预测区间较宽参考价值有限”的提示。误差分析里我发现一个有意思的现象模型对高成本大制作电影的预测误差明显小于低成本电影。原因很好理解大制作电影的票房受档期和宣发影响相对可控而低成本电影经常出现口碑逆袭或完全无人问津的极端情况这部分随机性很大谁来预测都很难做好。这个分析在答辩时被老师追问过因为这说明我真的理解了模型的能力边界而不只是跑了一组数字出来。4. Flask后端架构API设计、数据库表结构与模型调用Flask这部分是整个系统的骨架它把推荐算法和票房预测模型包装成可以被浏览器调用的服务。很多做算法的同学会在这里翻车因为习惯在Jupyter Notebook里写代码完全不熟悉Web服务的部署和调用方式。我在这里分享一套完整的Flask后端组织方法。4.1 蓝图的模块划分与项目目录结构Flask项目最忌讳的就是把一堆路由堆在app.py里。我一开始也这么干结果代码写到600行之后自己都快找不着北了。后来参考了一些开源项目的做法按蓝图Blueprint拆分目录结构如下movie_project/ ├── app.py # 应用入口注册蓝图 ├── config.py # 全局配置 ├── requirements.txt ├── models/ # 数据库ORM模型 │ ├── __init__.py │ ├── user.py │ ├── movie.py │ └── rating.py ├── algorithms/ # 算法模块纯Python不依赖Flask │ ├── __init__.py │ ├── recommender.py # 混合推荐主逻辑 │ ├── user_cf.py # 协同过滤 │ ├── content_based.py # 内容推荐 │ └── box_office.py # 票房预测模型封装 ├── api/ # 蓝图路由 │ ├── __init__.py │ ├── auth.py # 登录注册接口 │ ├── movies.py # 电影列表、详情接口 │ ├── recommend.py # 推荐接口 │ └── forecast.py # 票房预测接口 ├── templates/ # Jinja2 模板 ├── static/ # CSS、JS、图片 └── data/ # 预处理好的数据文件把算法从Flask代码里剥离出来是这条设计链路上最重要的一步。algorithms目录下的所有代码都是纯Python类只依赖pandas、numpy、sklearn这些库完全不import Flask这样算法模块可以脱离Web单独测试在终端里跑一跑就知道结果正不正确不用每次起服务再调试。4.2 核心API设计RESTful风格的接口规范整个系统的接口设计遵循简单的RESTful风格下面几个是核心接口POST /api/register 注册 POST /api/login 登录写入session GET /api/movies 分页获取电影列表支持类型筛选 GET /api/movies/id 获取电影详情 POST /api/rating 提交评分 GET /api/recommend/uid 获取推荐列表可带type参数切换策略 POST /api/forecast 票房预测body传入电影特征JSON票房预测接口的实现比较有代表性前端拿到的是一个JSONapp.route(/api/forecast, methods[POST]) def forecast(): data request.get_json() # 把前端传过来的原始字段转成模型特征向量 feature_vector box_office_engine.feature_transform(data) # 加载预训练模型输出对数票房后还原为万元 log_pred model.predict([feature_vector])[0] pred_value round(np.expm1(log_pred), 2) # 从决策树分位数计算区间 low, high model.estimators_interval(feature_vector) return jsonify({ predicted: pred_value, interval: [round(np.expm1(low), 2), round(np.expm1(high), 2)], unit: 万元 })有个关键的性能问题需要处理模型文件如果每次请求都从磁盘加载耗时会是几百毫秒级别性能很差。我的做法是创建一个全局的模型单例在Flask应用启动时加载一次后续请求直接复用。同时因为GIL的限制多个请求并发时模型的predict调用实际上是串行执行的但一次推理只有几毫秒完全不影响使用。4.3 数据库设计用户、电影、评分三张核心表数据库用SQLite就够了毕设项目完全没有必要上MySQLSQLite是文件型数据库不需要单独安装服务整个数据库就是项目里的一个文件方便移动和备份。三张核心表的设计如下用户表CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );电影表CREATE TABLE movie ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, genres TEXT NOT NULL, release_year INTEGER, rating FLOAT, rating_count INTEGER, description TEXT );评分表CREATE TABLE rating ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, movie_id INTEGER NOT NULL, score FLOAT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, movie_id) );这里有个很关键的设置rating表的UNIQUE(user_id, movie_id)约束确保同一个用户对同一部电影只能有一条评分记录。业务上用户修改评分时用INSERT OR REPLACE或者先查询再更新的方式不会产生重复评分。推荐算法读数据的时候直接SELECT user_id, movie_id, score FROM rating。密码安全这个点我提一下绝对不要明文存密码用werkzeug.security的generate_password_hash和check_password_hash这是Flask自带的密码哈希工具加盐存储比自己写一个MD5强得多。答辩时这个细节很加分因为这说明你有基本的安全意识。4.4 前端页面与Flask模板渲染的配合我采用了Jinja2模板加原生JavaScript的方式没有用前后端分离架构。原因很现实毕设项目如果引入Vue或React就需要处理跨域、打包、构建等一系列问题工作量会增加不少但在答辩展示层面收益有限。页面逻辑其实也不复杂。首页展示推荐结果用户登录后调用/api/recommend/uid拿数据再渲染卡片。电影列表页有类型筛选、搜索、分页功能。电影详情页展示电影信息用户可以打分打分之后立即刷新推荐结果。票房预测页是一个表单用户填写类型、预算、上映档期、导演等信息点击预测后端返回预测票房和区间前端用Chart.js画一个对比条形图展示“实际值 vs 预测值”的对照。有一点经验值得分享列表页的分页一定在SQL层做用LIMIT和OFFSET不要在Python里对全部数据做切片。我刚开始就是把5000条电影全部load出来再在内存里切片页面加载慢得不行改成分页查询之后流畅很多。推荐结果展示时封面图不能直接加载网络图片因为网络图片经常失效导致页面破图我把图片处理成两种方案有图片就显示没有就用带渐变背景的色块加片名首字效果反而很清爽。5. 我在开发过程中踩过的坑和完整排查链路这一节是最想分享给你的内容因为这些坑每一个都花了我至少半天时间去排查。如果当时有人告诉我哪里出了问题我至少能省出一个星期。5.1 场景一评分矩阵稀疏导致的推荐结果全是热门电影第一次跑通UserCF推荐输入一个喜欢文艺片的用户ID返回的结果全是《变形金刚》《复仇者联盟》这类高票房大片文艺片一部都没有。当时第一反应是算法写错了花了很长时间在相似度计算上找问题。后来排查到问题根源在于评分矩阵的稀疏性。MovieLens数据集中最热门的几百部电影占据了绝大部分评分大多数长尾电影只有零星几条评分。当计算用户相似度时只有那些电影有共同评分即使这个文艺片用户和另一个用户都看了某部冷门文艺片但因为共同的评分数量太少比如只有2部相似度权重被稀释了。而两个都喜欢看热门大片的用户共同评分数量很多相似度自然更高结果就是推荐列表被热门电影霸榜。解决办法是在计算相似度时加了一个最小共同评分数的过滤条件共同评分数少于10的用户对直接把相似度置为0不参与邻居筛选。同时如果推荐结果里某个类型占比超过70%就对热门结果做一次降权处理让长尾电影有机会浮上来。这个坑让我真正理解了数据分布对算法效果的影响远比模型调参的影响大。5.2 场景二Flask Debug模式下模型被重复加载导致内存暴涨系统开发过程中我习惯用app.run(debugTrue)启动服务方便改代码自动重启。结果发现每次向票房预测接口发一个请求内存占用就涨几十MB多请求几次之后电脑越来越卡最后直接卡死。通过任务管理器查看进程发现Python进程的内存占用持续增长没有释放迹象。一开始以为是内存泄漏往模型代码里加了各种gc.collect()都没用。后来无意中看到一条终端输出“Reloader detected change, restarting server”才反应过来问题出在Debug模式。Flask的Debug模式会启动一个独立的文件监视进程代码文件一旦发生变化就自动重启整个服务。而我的票房模型是在模块级别加载的每次服务重启都会重新加载一遍模型文件旧进程还没来得及完全释放新进程又起来了就这样不断叠加。定位到原因之后思路就清晰了把模型加载改成懒加载模式_model None def get_model(): global _model if _model is None: _model joblib.load(./models/box_forest.pkl) return _model同时在验收演示时关闭Debug模式用正式模式启动服务。这个坑给我的教训是开发模式和正式运行模式下资源管理逻辑完全不同可能是两套代码行为。5.3 场景三票房预测特征泄漏导致测试指标虚高第一次训练票房预测模型时测试集的R²达到了0.85我一度以为自己的模型已经超过了大多数行业水平直到提交给导师看老师问了一句“你的测试集里是不是包含了上映之后的特征”我才意识到自己踩了特征泄漏的坑。我当时的训练数据里包含了一个“猫眼想看人数”的特征这个特征在电影上映前确实可以获取但我用的数据集里这个字段记录的是电影上映后某一天的累积值而不是上映前的值。这等于模型在预测时已经偷看到了“结果变量”的一部分信息。测试指标虚高的原因。排查方法是用时间序列的方式重新划分训练集和测试集按上映日期排序前80%作为训练集后20%作为测试集保证测试集里的电影都晚于训练集里的电影。重新训练之后R²掉到了0.68左右虽然数字变低了但这个数字才是可信的。这个经历让我深刻理解了一个道理评估指标的可靠性比数值本身更重要。论文里我把特征泄漏的排查过程也写进去了作为数据预处理严谨性的例证。6. 系统测试与部署上线从本地跑通到答辩演示全流程测试和部署看起来是最“不算法”的部分但往往是决定项目完成度的关键。一个能稳定演示的系统比一个在开发环境跑得飞起但换个机器就挂掉的系统给人的整体印象完全不一样。6.1 我设计的核心测试用例测试阶段不能只靠手动点页面我写了一批接口层级的测试脚本用requests库直接调用后端接口自动校验返回结果。核心测试用例有下面几类用户模块测试注册一个全新用户名预期返回成功重复注册同一用户名预期返回“用户名已存在”正确密码登录预期session创建成功错误密码登录预期返回401推荐模块测试新注册用户无评分记录调用推荐接口预期返回内容推荐结果已有评分记录用户调用推荐接口预期返回混合推荐结果推荐结果不包含用户已评过分的电影票房预测模块测试输入完整特征预期返回预测值和置信区间缺少必填字段预期返回参数错误提示请求方法改为GET预期返回405这些测试脚本我放在tests/目录下每跑一次大约一分钟能快速发现接口是否正常。答辩前一天我把整个测试脚本跑了一遍发现推荐接口在无评分用户场景下会报错就是因为测试脚本暴露的边界条件没处理当场修复避免了演示现场翻车。6.2 部署方案为什么我选Gunicorn而不是自带的开发服务器Flask自带的服务器Werkzeug只适合开发调试它默认是单进程单线程性能很弱而且调试模式下会暴露大量内部错误信息不适合正式部署。最终我用了Gunicorn来启动服务一个非常成熟的Python WSGI服务器配置非常简单pip install gunicorn gunicorn -w 3 -b 0.0.0.0:8000 app:app这个命令的含义是启动3个worker进程监听8000端口。-w 3的配置要和你服务器CPU核心数匹配不是越大越好——worker之间是互相独立的进程如果你的算法模型内存占用很大worker开太多会把服务器内存占满。一个2核4G的云服务器2到3个worker就够用了。部署时还需要注意一个容易忽略的问题算法初始化的耗时。如果模型加载要5秒那第一个请求进来要等5秒才能返回如果前端没有设置超时时间用户会以为页面卡死了。解决方案是在系统启动前预加载模型比如在应用入口处调用一次get_model()让初始化耗时发生在服务启动阶段而不是用户请求阶段。6.3 答辩演示的几个实用技巧答辩演示和平时开发完全不一样追求的不是功能多而是可控和稳定。我的经验是准备两条演示路径一条是顺畅的“完美路径”按规划好的步骤展示核心功能另一条是“应急预案”如果现场网络不好或数据加载慢就直接切换到一个预先录制好的录屏视频。演示顺序也有讲究先展示推荐功能因为推荐结果直观可视化页面好看容易让老师产生兴趣再展示票房预测输入参数点击预测输出结果区间图表展示模型的“智能感”最后展示电影列表、评分等常规功能。把算法和工程的部分穿插在功能演示中说明而不是生硬地把算法讲完再演示功能。答辩时经常会遇到的问题是“你这个结果为什么可信”“指标为什么是这个数”。这里有个话术经验数据集的评分可以打印出来让老师直观看到冷门电影的推荐命中情况票房预测的误差可以用具体电影举例比如“《XX》预测6000万实际7200万误差约16%”有一个具体可见的案例比念R²数字有说服力得多。7. 关于扩展方向和我的几点真心建议做完整个项目之后我对“毕业设计源码”这件事有了新的理解。一套真正能拿得出手的毕业设计不在于堆砌了多少个算法而在于每一个功能点背后都有完整的设计逻辑和数据支撑。如果你想在这个项目基础上继续扩展这里有三个方向参考第一个方向是给推荐系统加一个“猜你喜欢”的实时刷新机制。当前的设计是用户评分后手动刷新你可以增加一个事件监听用户评分之后通过AJAX异步调用推荐接口新推荐结果直接替换页面内容交互体验会有明显提升。第二个方向是给票房预测加更多的数据源。比如微博热度指数、预告片播放量、灯塔想看人数这些都是行业里真实使用的预测特征。但要注意数据的获取方式和时间粒度保持特征和标签在时间线上的一致性是核心。第三个方向是把Flask后端替换成FastAPI。FastAPI原生支持异步、自动生成Swagger文档性能也更好。如果之后想找工作时展示这个项目用FastAPI重构后写成接口文档会比Flask更容易打动面试官。最后再聊一点务实的个人感受。毕设项目不在于大而全而在于“完整”二字。一个功能简单但完整落地的系统远比一个算法复杂但只存在于Notebook里的方案更有价值。你把这个系统跑起来的那一刻从数据库表结构、到算法模块、到前端页面、到部署上线全链路都想通了那才是做毕业设计真正的收获。项目源码里的注释和文档我写得很完整拿到源码的同学基本都能自己跑起来在这个基础上做自己的修改把里面每个模块的逻辑换成自己的理解哪怕只改了一个算法、加了一个功能它就已经是你自己的作品了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表