最近好几个准备做毕设的同学都在问同一个问题:股票预测相关的项目,网上单讲LSTM训练的教程一抓一大把,单讲Django做网站的教程也多得是,可一旦要把“深度学习模型跑在 Django 网站里”这条链路整体串起来,能直接参考的资料就零星了。我这次做的“基于深度学习的 Django+TensorFlow 股票走势预测系统”,正好就是这样一个把数据采集、特征工程、LSTM 模型训练、Web 系统集成、预测结果可视化串成完整闭环的项目。这篇文章就把这个项目的设计思路、核心代码逻辑、训练细节、以及我在实际运维中踩过的坑全部摊开讲。想拿来做毕业设计、或者想通过一个完整全栈项目把深度学习落地到 Web 系统的同学,可以直接照着这套方案走。
先声明一句:这套系统的定位是学术研究与技术学习,不是炒股工具。市场预测本身具有高度的不确定性,任何模型都不能作为真实投资决策依据。把预期摆正了,后面每一步都会做得更踏实。
1. 为什么选这个题目:深度学习加 Web 全栈的技术选型逻辑
1.1 这个选题在毕业设计里的“性价比”在哪
每年都有大量同学做“管理系统”或“算法实验”,前者容易做成重复劳动的增删改查,后者容易被老师质疑“只是跑了个 notebook”。股票走势预测系统恰好把两边都占了:算法层面有深度学习的训练与评估,工程层面有数据落库、Web 展示、接口交互、用户管理。无论老师更看重算法创新还是系统完整度,你都有东西可以拿出来讲。
从工作量分配看,数据清洗和特征工程占三成,模型训练与调参占三成,Django 集成和前端展示占三成,论文与测试占一成。这个比例很健康,不会出现“算法写完只剩一个光杆模型”或者“网页做完了但和深度学习毫无关系”的局面。更重要的是,这类选题在网上有大量公开参考,遇到报错基本都能搜到解决方案,对本科生来说非常友好。
1.2 Django 而不是 Flask:系统完整度优先
我见过不少同学为了图轻量选 Flask,结果做着做着就开始自己实现登录、后台管理、分页这些 DJango 自带的功能。Flask 本身没有错,但毕业设计要呈现的是“一个完整的系统”,而不是“一组接口”。Django 自带 ORM、Admin 后台、用户认证、CSRF 防护,这些不是摆设,它们可以直接写进论文的功能模块里,还能少写几百行代码。
另一个非常实际的好处是:Django 的 ORM 让数据库切换几乎没有成本。项目阶段我用 SQLite 就够了,如果答辩环境要求 MySQL,改一下配置文件里的数据库连接即可。数据表结构在 models.py 里定义好后,迁移命令会帮你把表建好,不需要手写 SQL。对于最终交付要包含“完整源码”的项目来说,SQLite 也能保证评审老师拿到代码后直接跑起来,省去配置数据库的麻烦。
1.3 TensorFlow 选型的真实理由
选 TensorFlow 而不是 PyTorch,我得先说明:PyTorch 在科研和动态图调试上确实很香,但这个项目的核心诉求是“快速写出可部署的模型并集成到服务器”,TensorFlow 的 Keras 高层 API 在这种场景下更顺手。Keras 的 Sequential 模型接口让 LSTM 原型的搭建非常短,model.save() 导出的模型文件也可以被 Django 端直接加载。
从资料获取角度看,股票时序预测相关的开源项目里,TensorFlow + Keras 的参考代码占比非常高,这意味着你在训练报错、维度不匹配、版本兼容这类问题时,能搜到大量现成问答。唯一要提醒的是 TensorFlow 2.x 各小版本之间的 API 变动比较大,比如 2.9 之后模型的保存格式从 .h5 逐步转向 .keras,一不留神就会在加载时报协议错误。解决办法就是锁定版本,后面我会专门写环境配置。
2. 系统整体架构与数据流设计
2.1 模块划分与各自职责
整个系统我拆成了五个模块:数据采集模块、数据处理模块、模型训练模块、预测服务模块、Web 展示模块。模块之间通过数据库和模型文件衔接,耦合度很低,任何一块单独抽出来都可以写进论文做详细设计。
| 模块 | 主要职责 | 关键技术点 |
|---|---|---|
| 数据采集 | 从公开数据接口获取日K数据并落库 | 增量更新、去重、行情完整性校验 |
| 数据处理 | 清洗、复权、特征衍生、归一化、生成样本 | pandas、MinMaxScaler、滑动窗口 |
| 模型训练 | 构建LSTM、训练、评估、导出模型文件 | TensorFlow Keras、EarlyStopping |
| 预测服务 | 接收前端请求,返回预测结果 | Django视图、模型常驻内存、线程加锁 |
| Web展示 | 行情K线、预测曲线、股票切换、历史结果 | ECharts、Ajax、Bootstrap |
这五个模块不一定全要自己写,但每一步的输入输出都要清晰。比如数据采集模块输出的必须是“已经去重、按日期排序”的行情表;数据处理模块输出的必须是“已经归一化并切片好的数组”。能明确说出模块间接口的人,答辩时给人的感觉完全不一样。
2.2 数据流与关键路径
整套系统的数据流可以概括成一条单行道:外部数据源把日K数据写入数据库,数据处理模块从库里读数据并加工成模型可用的张量,模型训练模块产出模型文件,Django 在启动时加载模型并对外提供预测接口,浏览器端通过 ECharts 把真实走势和预测走势画在同一张图上。
其中最容易出问题的是“数据处理逻辑不一致”。训练的时候你可能写了一套脚本做归一化和窗口切分,到了 Django 端重新实现一遍时,缩放器的参数、窗口长度、特征顺序只要有一处对不上,预测结果就会完全错误。我的做法是:把数据处理函数独立成一个公共模块,训练脚本和 Django 视图都从同一个模块导入,确保训练和预测走完全相同的代码路径。这个设计建议每个做类似项目的同学从一开始就采用。
2.3 数据库设计细节
数据库不需要复杂的表结构,核心是两张表。一张是 stock_daily,字段包括 stock_code、trade_date、open、high、low、close、volume;另一张是 prediction_record,字段包括 stock_code、model_version、pred_date、pred_value。用户功能直接用 Django 内置的 User 表,不需要自己建。
stock_daily 表的唯一索引要设置在 (stock_code, trade_date) 上,避免重复导入。预测记录表其实是可选的,但加上它之后能多做一个“历史预测对比”的功能,论文里也能多放一张功能截图,属于低成本高收益的设计。
3. 数据获取与预处理:决定模型上限的隐形战场
3.1 数据源选择与完整性校验
很多教程会直接给你一段“下载数据”的代码,好像数据是免费的空气一样。实际做的时候你会发现,不同数据源的字段命名、复权规则、停牌处理都不一样。我用的方案是直接调用国内免费开源数据接口,它不需要复杂的权限申请,拿某只股票近五年日K线就够用。
数据拿回来之后先别急着训练,先做三件事:检查是否有空值、检查日期是否连续、检查是否存在价格异常跳变。停牌导致的成交量骤减和价格缺失,在行情数据里非常常见,如果不处理,LSTM 会把这些异常当成正常规律去学习。我的处理策略是把停牌日的行直接用前一日数据补齐,并在特征里增加一列“是否停牌次日”的标记。这一列能显著降低模型在某些跳水行情上的偏差。
3.2 前复权处理:最容易被忽略的关键细节
股票除权除息后,价格会出现人为的跳空缺口,这一天前后 K 线的“连续感”被破坏了。LSTM 的核心假设就是历史模式会延续,跳空缺口会让模型误以为发生了剧烈的真实波动。解决方式是使用前复权价格训练。
前复权可以理解为:以当前价格为准,把历史价格按分红送股比例做统一换算,让整条价格曲线保持连续。代码上,常见的数据接口一般提供复权因子,或者直接提供复权后的价格字段。要注意的是,复权后的历史价格会随最新行情动态变化,所以“固定某一天的复权数据”是更稳妥的做法,否则同一只股票不同日期下载的数据特征会不一致。
3.3 特征工程与归一化的正确姿势
我的输入特征并不复杂:开盘价、收盘价、最高价、最低价、成交量,再加上几个常见技术指标,比如 MA5、MA10、MA20、RSI。这些特征在 pandas 里几十行就能算完,但有两个原则必须遵守:其一,所有特征序列必须对齐,不能有的按自然日、有的按交易日;其二,技术指标计算只用历史数据,不能把未来数据揉进指标里。
归一化这里有一个很多教程都处理错的点:直接对整个数据集做 fit_transform。这个操作看着没问题,实际上是数据泄漏,等于是让缩放器提前看到了测试集的统计信息。正确做法是先对训练集调用 fit 生成缩放器,再用同一个缩放器分别 transform 训练集和测试集。用一个类比解释:训练集是考前刷题,测试集是正式考试,你肯定不能在刷题时就拿到考卷答案去调整自己的答题尺度。
3.4 滑动窗口样本构建
模型输入需要固定长度的连续时间窗口。我选定的窗口是 60 个交易日,也就是大约一个季度。每生成一个样本,X 是连续 60 天的全部特征构成的矩阵,形状是 (60, feature_count);y 是第 61 天的收盘价。窗口滑动一步就生成一个新样本,一只股票五年数据大约能生成一千多个样本。
import numpy as np def create_samples(features, target, window=60): X, y = [], [] for i in range(len(features) - window): X.append(features[i:i + window]) y.append(target[i + window]) return np.array(X), np.array(y)窗口选 60 的理由很简单:太短看不出短期趋势,太长训练样本数量会明显下降,而且更早的数据对“明天”的预测价值不大。我也对比过 20 天和 120 天的效果,60 天在多数股票上略好,但这本身就是一个可以写进论文的对比实验。
4. LSTM 模型设计与训练:把预测问题变成可学习的模式
4.1 为什么是 LSTM:序列建模的基本逻辑
股票行情是一组有序的时间序列,今天的收盘价受过去几十个交易日影响,这种时间依赖关系是本质特征。如果用普通全连接网络,输入只被当成一组独立的数字,时间顺序被完全丢弃。RNN 虽然能处理序列,但梯度消失严重,很难学到长期依赖。LSTM 通过输入门、遗忘门、输出门三个门控机制,有选择地保留和遗忘历史信息,从而在较长序列上保持稳定的梯度传递。
通俗一点说:LSTM 就像一个懂得记笔记的员工,每天上班都会看一眼之前的记录,但对哪些信息值得保留、哪些可以丢弃有自己的判断。股票数据噪声极大,这种“选择性记忆”能力非常重要。GRU 也能达到类似效果且参数更少,但 LSTM 在公开资料、模型解释性、论文写作素材方面更丰富,所以我最终选了 LSTM。
4.2 模型结构与参数设置
我采用的是两层 LSTM 加 Dropout 加全连接输出层的结构:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model = Sequential([ LSTM(64, return_sequences=True, input_shape=(60, feature_count)), Dropout(0.2), LSTM(32), Dropout(0.2), Dense(1) ]) model.compile(optimizer=Adam(learning_rate=0.001), loss='mse')第一层 LSTM 返回完整序列,给第二层提供更丰富的中间表示;第二层只返回最后一个时间步的输出,再接一个 Dense 层输出预测的收盘价。中间夹 Dropout 是为了抑制过拟合。只输出一个神经元是因为我把任务定义成了回归任务,输出的就是一个连续价格;如果你想预测涨跌方向,可以把 Dense 改成两个神经元加 softmax,那就是一个分类任务,评价指标也要相应改成准确率。
4.3 训练策略与评价指标
训练时我会设置 EarlyStopping,监控验证集损失,连续 10 个 epoch 不下降就停止,并且恢复最优权重。同时配合 ReduceLROnPlateau,当验证损失进入平台期时自动把学习率降低一半,这样既不用手动调参,又能让收敛过程更平滑。
评价指标我同时看三组:MAE、RMSE、MAPE。其中 MAPE 是百分比误差,报告给非专业听众时更直观。但是这里有个大坑:当真实价格非常接近 0 时,MAPE 会爆炸。股票价格一般不容易碰到这种情况,但如果你的特征里放了成交量等指标,要小心归一化后的数据范围问题。
还有一个必须做的对照实验:用“今天的收盘价预测明天的收盘价”作为朴素基线。如果 LSTM 在测试集上的误差连这个基线都跑不赢,说明模型没有学到有效信息。这不是丢人的事,金融时序本身的信噪比极低,很多学术研究也承认纯价格序列的预测上限就在基线附近。论文里如实呈现这个对比,反而能让老师相信你的结果是严谨的。
5. Django 集成 TensorFlow 模型:从训练脚本到 Web 服务的完整链路
5.1 模型保存与加载:别在请求里反复读模型
训练完成后,把模型导出成 SavedModel 格式,这是 TensorFlow 官方推荐的生产部署格式。相比 .h5,SavedModel 是一整个目录,包含了模型结构、权重和推理所需的其他资源,跨环境加载的容错性更好。如果你用的是 Keras 自带的 .h5 或 .keras,在加载时务必保证训练环境和 Django 环境的 TensorFlow 版本一致,否则很容易出现自定义层无法反序列化的报错。
加载时机非常关键。直接把 load_model 写在视图函数里会带来两个问题:每次请求都重新加载模型,响应速度慢得让人无法接受;多次加载同一模型还会造成内存浪费。我的做法是在 Django 的 AppConfig.ready() 方法里加载模型,并把它挂到应用实例上,这样服务启动时只加载一次。predict_service.py 里可以写一个简单的单例封装。
# predictor.py import threading import tensorflow as tf _model = None _lock = threading.Lock() def load_model_once(model_path): global _model with _lock: if _model is None: _model = tf.keras.models.load_model(model_path) return _model def predict(input_array): model = load_model_once("models/stock_model") return model.predict(input_array, verbose=0)5.2 预测接口设计与并发处理
Django 端我设计了一个简单的 REST 风格接口。前端请求 /api/predict/?code=600519,视图函数拿到股票代码后,查询最近 60 个交易日的数据,调用公共数据处理模块做归一化和窗口拼装,再交给模型推理,最终返回 JSON 格式的预测结果。真实行情曲线也可以由同一个接口返回,或者单独做一个行情查询接口。
这里要特别提醒并发问题。TensorFlow 模型在多线程环境下调用 predict 并不是绝对线程安全的,同一个 Session 或 Graph 被多个线程同时使用可能抛出莫名其妙的问题。我的做法是为推理加了一个线程锁。由于单次推理只有毫秒级,加锁的吞吐损失在毕设演示场景下完全感受不到。如果你的系统要支撑更高并发,更合理的方案是把模型推理拆成一个独立服务,Django 通过 HTTP 或消息队列发起调用。毕设阶段不需要过度设计,但论文里可以提一句未来优化方向,这属于加分项。
5.3 前端可视化:ECharts 叠加真实曲线与预测曲线
前端我用 Bootstrap 搭页面,用 ECharts 画图。ECharts 的 k线图组件对股票场景支持非常好,关键是把真实收盘价曲线和预测曲线画在一起,做一个直观的视觉对比。预测曲线通常从最后一个真实数据点开始,往后延伸未来 5 到 10 个交易日。
$.getJSON('/api/predict/', {code: stockCode}, function (res) { myChart.setOption({ xAxis: { type: 'category', data: res.date_list }, series: [ { type: 'line', name: '真实收盘价', data: res.real_list }, { type: 'line', name: '预测收盘价', data: res.pred_list } ] }); });页面交互逻辑很简单:一个下拉框切换股票代码,点击预测按钮后 Ajax 请求接口,拿到数据后刷新图表。整个过程中用户无感知的地方是后端要完成查询数据库、数据对齐、归一化、窗口拼接这一整套动作。所以这个接口在编写时一定要把“代码换成输入”的逻辑和训练脚本保持一致,我建议直接把训练脚本里的数据处理函数做成公共模块 import 进来,不要复制粘贴代码,否则改了一处忘了另一处,结果就会非常难排查。
6. 实测效果复盘:那些教程里不会写的真实问题
6.1 预测曲线为什么总是“慢半拍”
第一次在测试集上画图时,我明显看到预测曲线比真实曲线在时间上“滞后”了一截,看起来就像把真实曲线整体向右平移了一天。这不是过拟合,而是 MSE 损失下的典型现象:在随机游走型时间序列上,对明天最安全的预测就是今天的价格。模型只要学会“输出接近最后一个输入值”,MSE 就能降得比较低。
这个现象我当时很不甘心,甚至试过加更深的网络、更多特征,结果都没有本质改善。后来在整理论文时我把它作为一个“模型局限性分析”章节好好写了出来,反而成了答辩时老师最认可的分析片段。做时序预测项目,一定要提前做好“纯价格序列预测难以大幅超越基线”的心理准备,这并不代表项目失败,而是对问题本质的准确认识。你可以尝试加入新闻情绪、宏观因子等外部数据来提升上限,这是论文展望部分的天然素材。
6.2 训练与测试的样本重叠问题
滑动窗口生成的样本之间存在高度重叠,相邻样本的时间点几乎一样。如果在构建数据集时不小心做了随机打乱,再切分训练集和测试集,那么测试集里的很多样本和训练集样本只差一两天,模型相当于提前见过了答案,测试指标看起来会很好,但完全没有泛化意义。
正确做法是按时间顺序硬切分:先取前面 70% 的交易日生成样本作为训练集,后面 15% 作为验证集,最后 15% 作为测试集,整个过程中不 shuffle。我在代码里严格按日期排序后再切分,训练时只在训练集内部做 batch 的 shuffle,验证集和测试集原样传入模型。这个细节我建议每个同学都写进论文的数据处理章节,能显著提升评审老师对你专业度的评价。
6.3 环境依赖与版本锁定
这个项目最烦人的坑集中在环境依赖上。TensorFlow 2.10 之后在 Windows 上的 GPU 支持策略有调整,如果没有 NVIDIA 显卡或 CUDA 环境,直接用 CPU 版即可:预测任务数据量小,CPU 推理完全够用。Django 建议用 4.2 的稳定版本,不要追最新主版本,某些第三方插件可能还没有跟上新版本。
我将 requirements.txt 贴出来供参考:
tensorflow==2.10.0 Django==4.2 pandas==2.0.3 numpy==1.24.3 scikit-learn==1.3.0 akshare==1.10.0这一组版本在我反复搭建和测试过程中没有出现兼容问题。特别提醒 numpy,TensorFlow 较旧版本对 numpy 2.x 的兼容性很差,如果直接 pip install 安装了最新 numpy,启动时经常会报找不到某个内部符号的错误。先把 numpy 版本锁住,能省掉大量排查时间。
6.4 性能优化与体验打磨
模型推理本身毫秒级,真正拖慢响应的是数据查询和预处理。我做了两个优化:第一,给 stock_daily 表的 stock_code 和 trade_date 加上联合索引,历史数据查询从几百毫秒降到几十毫秒;第二,把最近一次预测结果按股票代码缓存到内存里,相同股票短时间内重复请求直接返回缓存。这两个优化都很简单,但能让演示现场流畅很多,体验感差距非常明显。
7. 论文写作与答辩准备:让项目“讲得出、问不倒”
7.1 论文结构怎么安排
论文的章节安排我建议遵循经典的“背景—技术—设计—实现—测试—总结”六段式:绪论部分交代研究背景、意义、国内外研究现状;相关技术部分介绍深度学习、LSTM、Django、前后端技术栈;需求分析部分从功能和非功能两个维度展开;系统设计部分画架构图、功能模块图、数据库 ER 图、时序图;系统实现部分按模块展示核心代码和运行截图;测试部分同时包含功能测试用例和模型实验结果对比表。
一个非常有效的写作技巧:把每个模块的“输入—处理—输出”用一个小节描述清楚,配合一张截图,这段基本不会缺字数。模型实验最好放一张对比表,包括窗口长度、层数、学习率、MAE、RMSE、MAPE 这些指标,表中呈现 2 到 3 组对比实验即可。论文不需要写出惊天动地的创新点,但需要让老师看到你做了完整的实验闭环。
7.2 答辩高频问题与应对思路
答辩时老师大概率会问几个方向:你为什么要选 LSTM?数据泄漏怎么避免?MAPE 和 MSE 的区别?模型预测不准怎么办?系统如何更新数据和模型?这些问题其实都在前面的内容里覆盖了。我的建议是准备一张 A4 纸,写下“技术难点—解决方案—实验结果”的对照提纲,每一个问题都用“问题现象—原因分析—我的处理方式—效果如何”的结构回答。
演示环节要提前准备一只走势有代表性的股票,既有明显的上升和下跌段,预测曲线也和真实曲线有可讲述的差异点。不要在答辩现场临时换股票或重新训练模型,所有演示数据要提前跑一遍并截图保存。如果老师问到模型上线后的数据更新问题,可以如实回答当前是离线训练、定期更新,这属于合理的阶段性结论。
7.3 给后面做同类项目的同学三条建议
第一,优先保证链路通畅,再去调效果。先把“数据接入—模型训练—Django 加载—前端展示”跑通,哪怕模型效果一般,至少是一个完整系统;先花两周调参而不搭系统,最后很容易崩溃。第二,所有预处理逻辑尽量收拢到一个模块,保证训练和预测共用一套代码,这是减少“跑通训练却集不成”的最有效手段。第三,论文配图不要用手机拍照,统一用截图工具导出高清图,字体大小保持一致,这属于第一印象分。
最后再分享一个实际操作中的体会:做完这个项目后,我最大的收获不是“会了 LSTM”或者“会了 Django”,而是建立了一个完整的产品思维闭环——从数据到模型再到用户交互每一层都有取舍,每一层的结果都会传导到最终体验上。股票预测系统的精度上限摆在那里,但系统设计与实现的工程价值不会因为模型效果打折。希望这篇拆解能帮你少走几个大弯,把时间花在真正有产出的事情上。