ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django结合TensorFlow的股票预测系统设计与实现

Django结合TensorFlow的股票预测系统设计与实现

1. 项目整体设计与架构拆解

毕业设计做股票预测系统,是我见过最多人选择的方向之一。每年都有大量计算机相关专业的学生把“基于深度学习的股票走势预测”作为课题,但绝大多数人卡在同一个地方:模型训出来了,却不知道怎么变成一个能演示、能交差、能跑起来的完整系统。标题里给出的“Django + TensorFlow”组合,其实就是解决这个问题的经典方案——Django负责搭建Web应用,TensorFlow负责深度学习预测,两者一结合,就是一个既有技术深度又有完整性的毕业设计项目。

1.1 核心需求解析

在动手之前,先要把这个课题的需求拆开来看。表面上,这个系统要做的事情很简单:用户输入一只股票代码,系统返回未来一段时间走势的预测结果。但真正落实到毕业设计答辩时,评委看的是三个层面的东西。

第一层是深度学习应用能力。这部分主要体现在TensorFlow的建模过程中,包括数据的预处理、LSTM等时序模型的构建、超参数的调整、训练与评估。评委希望看到你对神经网络有基本理解,而不是机械套用现成代码。

第二层是Web系统开发能力。Django作为后端框架,需要实现用户交互、股票查询、结果展示等功能。有些同学会用Flask,但Django自带Admin后台、ORM数据库操作、表单处理等组件,开发效率更高,写进论文里的“系统架构”也更完整,所以毕业设计选Django是明智的。

第三层是系统整合与工程化能力。模型训练好之后,如何保存、如何被Web后端调用、如何把预测结果实时传回前端展示,这部分恰恰是教学课程里很少涉及、但工作中最常用的技能。

这个项目的技术路线非常清晰,就是把一个“离线训练模型”的任务和一个“在线提供服务”的任务拼成一个闭环。我来拆解一下这个闭环具体长什么样。

1.2 总体架构与技术选型

整个系统的典型架构分为四层:

层级技术选型职责说明
前端展示层HTML + CSS + JavaScript + ECharts股票历史K线展示、预测结果图表、用户交互界面
Web服务层Django + Django REST Framework接收请求、调用模型、返回JSON数据、用户管理
模型服务层TensorFlow(LSTM模型)加载训练好的模型,对输入序列执行预测
数据层SQLite/MySQL + Pandas存储用户信息、历史股票数据、预测记录

选型背后是有讲究的。Django自带ORM,操作数据库基本不用写SQL,能省下大量开发时间。TensorFlow作为深度学习框架,生态成熟,Keras API对新手友好,LSTM模型几十行代码就能搭出来。而ECharts在前端绘制K线和走势图,效果比matplotlib生成的静态图好太多,演示时视觉冲击力强。

整个系统的数据流向是这样的:用户在前端页面输入股票代码,Django视图接收请求,调用TensorFlow模型服务,模型从数据源(本地数据库或实时接口)提取最近N个交易日的股票数据,经预处理后交给LSTM网络做预测,再把预测结果返回给前端渲染。这个流程里最核心的一句话是——模型只在训练阶段使用“历史数据+标签”的形式学习规律,在推理阶段只做前向传播计算,这两者必须明确区分,很多人在答辩被问倒就是没搞清这个基本概念。

2. 核心功能模块详解与建模实操

既然整体架构定了,接下来要做的就是逐个模块实现。我以自己实际开发过的模拟项目X为参照(一套完整的股票预测演示系统),把每个核心模块的实现细节和关键代码思路展开讲。

2.1 Django后端:从项目骨架到接口设计

Django后端是整个系统的中枢。首先是创建项目和应用:

django-admin startproject stock_project cd stock_project python manage.py startapp stock_app

在settings.py中,有几处需要特别注意。一是把stock_app注册到INSTALLED_APPS,二是配置数据库(开发环境直接用默认的SQLite即可,省去环境配置成本),三是设置静态文件和模板路径。

这里我习惯先设计好数据模型,再反向梳理接口。核心数据表有两张:一张是StockHistory,保存股票每日的交易数据(开高低收、成交量等);另一张是PredictionRecord,保存每次预测的请求参数与结果,既方便演示时展示历史预测记录,也能在论文里体现“系统具备数据持久化能力”。

# models.py from django.db import models class StockHistory(models.Model): code = models.CharField(max_length=10, db_index=True) date = models.CharField(max_length=20) open = models.FloatField() high = models.FloatField() low = models.FloatField() close = models.FloatField() volume = models.FloatField() class Meta: unique_together = ('code', 'date') class PredictionRecord(models.Model): code = models.CharField(max_length=10) predict_date = models.CharField(max_length=20) predicted_price = models.FloatField() actual_price = models.FloatField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True)

这种设计思路叫作“先定数据模型,再写业务逻辑”。模型字段定义清楚了,数据库表结构就固定了,后续视图和模板的编写都会顺畅很多。

接口设计方面,我建议用前后端分离的思路,即使模板渲染也能做,但返回JSON接口的方式更方便前端做动态更新。核心接口有三个:

  • GET /api/stock/<code>/history/:返回历史行情数据
  • GET /api/stock/<code>/predict/:返回预测结果
  • POST /api/user/register/和POST /api/user/login/:用户认证

视图部分有一个细节值得注意:加载TensorFlow模型不能在每次请求时执行,因为模型加载耗时可达几秒,会拖垮接口响应。正确做法是在Django启动时加载一次,放在模块级全局变量里。例如在models.py同级新建ml_service.py:

# ml_service.py import tensorflow as tf from tensorflow.keras.models import load_model _model = None def get_model(): global _model if _model is None: _model = load_model('model/stock_lstm.h5') return _model

这个设计在答辩时很容易成为加分项,因为它体现了对“模型生命周期管理”的理解。顺便说一句,模型加载路径建议用绝对路径或os.path.join(BASE_DIR, 'model', 'stock_lstm.h5'),不要用相对路径,避免运行时找不到文件。

2.2 TensorFlow模型构建:用LSTM建模时间序列

股票数据本质上是时间序列,所以深度学习模型的选择上,LSTM(长短期记忆网络)是最自然、也是论文里最好写的方案。LSTM能通过门控机制记住长期依赖关系,比普通全连接网络在时序预测上表现更好。

搭建模型的核心代码如下:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_model(input_shape): model = Sequential([ LSTM(units=64, return_sequences=True, input_shape=input_shape), Dropout(0.2), LSTM(units=64, return_sequences=False), Dropout(0.2), Dense(units=32, activation='relu'), Dense(units=1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model

这个结构包含两层LSTM和两个全连接层,参数选择基于常见经验:第一层LSTM返回完整序列给第二层继续提取特征,第二层仅返回最后一个时间步的输出,接上全连接层输出预测价格。Dropout设置为0.2是为了防止过拟合,因为股票数据噪声很大,模型如果过强很容易死记硬背训练集。

输入形状input_shape是一个二元组(time_steps, n_features)。time_steps表示用过去多少个交易日的特征序列来预测未来一天的价格,我一般取60(相当于3个月左右的交易日)。n_features取5,对应开盘价、最高价、最低价、收盘价、成交量这五个基本维度。

这里要给准备答辩的人提个醒:一定要能说清楚为什么取60、为什么用MSE损失函数。不能只会说“参考网上代码”。常见的回答逻辑是:股票短中期趋势一般参考20日、60日均线,60个交易日能让模型看到足够长的趋势信息;MSE(均方误差)对大误差施加更大惩罚,和价格预测这类回归任务天然契合;Adam优化器自适应学习率,在大多数场景下无需手动调节学习率,对新手最友好。

2.3 前端可视化:K线图与预测曲线的实现

前端部分,我采用的是Bootstrap布局加ECharts图表。整个页面分左右结构:左侧是股票代码输入框和查询按钮,右侧是图表区域。图表区域放置两个图表,一个显示历史K线叠加预测曲线,另一个显示最近30日收盘价与未来7日预测值对比。

ECharts的使用方式不复杂,核心是引入JavaScript文件,在初始化时配置图表类型和数据源:

const chart = echarts.init(document.getElementById('chart')); $.get(`/api/stock/${code}/history/`, function(res) { // 构造K线数据: [open, close, lowest, highest] const klineData = res.data.map(item => [item.open, item.close, item.low, item.high]); const dates = res.data.map(item => item.date); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates }, yAxis: { scale: true }, series: [{ type: 'candlestick', data: klineData }] }); });

预测结果则用一条单独的折线叠加在K线尾部,用不同颜色区分真实数据和预测数据。这个视觉效果非常直接,演示时一句话就能讲清楚:“红色虚线是模型对接下来5个交易日的预测收盘价”。

需要提醒的是,前端请求路径中,一定要处理好Django的CSRF防护。对于使用jQuery发起POST请求的场景,最简单的方式是在模板中加入{% csrf_token %},并在Ajax请求头中带上令牌。我在第一次做的时候就踩过这个坑,所有POST请求全部403,排查了半天才想起来是CSRF的问题。

3. 数据获取、预处理与特征工程

数据是深度学习的粮食,这个项目里也是最容易被忽略、却最影响最终效果的一环。很多同学拿到一份现成的CSV文件,直接把数据丢给模型训练,结果模型损失值低得离谱,实际上是因为数据处理不当造成了数据泄露。

3.1 股票数据获取方案

数据获取有两条路径:离线下载和实时接口。离线下载适合训练阶段,一次获取多年历史数据,存成CSV或写入数据库;实时接口适合系统运行阶段,预测时拉取最新行情。国内常用的免费数据源有akshare和tushare,我建议用akshare,因为它无需注册令牌,开箱即用。

import akshare as ak # 获取A股日线行情 df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20180101", end_date="20231231", adjust="qfq")

参数adjust="qfq"表示前复权。这个参数很多人会忽略,但它对模型训练极其重要。股票分红送股会导致价格跳空,不复权的话模型会把这种非交易因素造成的价格变动当成规律来学习,直接拉低预测准确率。

获取到的数据包含日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅等字段,我们建模只用前五个核心字段加上成交量,共六个维度。多余的字段删掉,减少数据冗余对训练速度的影响。

3.2 数据清洗与序列化

清洗环节做三件事:处理缺失值、去除异常值、统一数据类型。缺失值一般用前向填充法,也就是用上一个交易日的数据填补空缺,因为停牌等场景会导致个别日期数据缺失,直接用当日行情延续是最合理的近似。异常值处理方面,如果出现涨幅超过20%或跌幅超过20%的数据,多半是数据源错误或特殊事件,直接删除更安全。

然后是构建监督学习所需的数据集。核心思路是把时间序列转换成“滑动窗口”的数据结构:

def create_dataset(data, time_steps=60): X, y = [], [] for i in range(time_steps, len(data)): X.append(data[i - time_steps:i]) y.append(data[i, 3]) # 第3列是收盘价,预测目标 return np.array(X), np.array(y)

这里每一行样本都由过去60个交易日的六维数据组成,标签是第61天的收盘价。用大白话讲就是:给模型看过去两个多月的数据,让它猜下一天收盘价是多少。

数据标准化是必不可少的步骤。股票价格的绝对数值可能从几元到几百元不等,成交量差异更大,如果不归一化,模型训练很容易不收敛。我用的是MinMaxScaler,把所有特征压缩到[0, 1]区间:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) scaled_data = scaler.fit_transform(data)

这里有一个必须记住的细节:fit_transform只用在训练集上,验证集和测试集只能用transform。如果整体数据一起做标准化,测试集的信息就提前泄露给了模型,训练出来的模型在真实场景中效果会大打折扣。这个细节在毕业设计论文里写出来,是很加分的设计意识。

3.3 训练集与测试集划分

时间序列数据不能像普通分类数据一样随机打乱再划分,因为顺序本身即信息。正确做法是按时间顺序切分。我习惯用前80%的数据做训练,后20%做测试。

train_size = int(len(scaled_data) * 0.8) train_data = scaled_data[:train_size] test_data = scaled_data[train_size:]

再进一步,从训练集中再切出10%做验证集,用于观察训练过程中的过拟合情况。完整比例为:训练集72%、验证集8%、测试集20%。这个比例是经验值,既保证了训练数据量充足,又让验证集有足够的样本量来反映训练是否过拟合。

4. 模型训练、指标评估与调优实战

模型训练是这个项目里耗时最长、最磨人的阶段。我在模拟项目X的训练过程中跑了将近50组对比实验,最终确定了比较稳定的参数组合。下面把整个过程拆开讲。

4.1 训练流程与关键参数解析

完整的训练代码大概长这样:

history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, callbacks=[ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5) ] )

两个回调函数是训练质量的保障。EarlyStopping监控验证集损失,如果连续10轮不下降就停止训练并回滚到最优权重,可以防止训练到后期模型开始过拟合;ReduceLROnPlateau在损失陷入平台期时将学习率减半,帮助模型跳出局部最优。

关于batch_size,我做过不同取值的对比实验。32的效果相对均衡,64的训练速度更快但损失略高,16则训练震荡明显。epochs设定为100,但配合早停机制,实际训练通常到30~40轮就停止了。这也说明了回调函数的意义——不用手动盯着训练曲线,模型自己会在最合适的位置停下来。

4.2 模型评估:不只是看损失值

训练完成后,在测试集上评估模型:

test_loss, test_mae = model.evaluate(X_test, y_test) # 预测并还原真实价格 predicted = model.predict(X_test) predicted_prices = scaler.inverse_transform( np.concatenate([np.zeros((len(predicted), 5)), predicted], axis=1) )[:, 5]

这里有一个反向标准化的技巧:因为标准化时把六维特征一起归一化,预测结果只是一维收盘价,还原时需要补上其他五维的占位数据,再取最后一列。很多人就在这里报错,因为数组维度对不上。

评估指标方面,均方误差(MSE)、平均绝对误差(MAE)和平均绝对百分比误差(MAPE)是三个最常用的。对股票预测而言,MAE和MAPE更直观,因为价格单位是“元”,误差多少元一眼就能看懂。

指标模拟项目X测试集结果含义说明
MSE0.0025(标准化后)均方误差,对离群点敏感
MAE0.031(标准化后)平均绝对误差
MAPE约2.3%(还原后)平均误差占真实价格的比例

MAPE在2%~3%左右看起来不错,但要泼一盆冷水:这只能说明模型在“整体趋势跟随”上有效,不代表它能精准预测未来涨跌。股票预测的本质是概率性估计,任何声称能精确预测价格的系统,在学术上都是站不住脚的。毕业设计论文里如果明确写到这一点,反而更显严谨客观。

4.3 模型持久化与集成

训练完成后,模型需要导出为文件,供Django调用:

model.save('stock_lstm.h5')

TensorFlow 2.x推荐使用SavedModel格式(保存为整个文件夹),但h5格式在毕业设计场景下更简单,一个文件即可管理。加载时使用之前提到的load_model即可。

除了模型本身,scaler也需要持久化。推理阶段对新数据进行预测时,必须使用训练时拟合好的scaler做转换,而不能重新计算。这个可以用joblib保存:

import joblib joblib.dump(scaler, 'scaler.pkl')

模型和scaler的加载目录建议统一放在model/文件夹下,与Django的manage.py同级,路径清晰且不容易混淆。

5. 系统集成、测试与部署

模型训练好、Web后端写好,两个模块就要联调了。这个阶段看似只是“拼起来”,实际踩坑非常多。

5.1 后端调用模型的完整链路

前端发起预测请求后,后端视图函数的逻辑大致如下:

def predict(request, code): # 1. 从数据库获取最近60个交易日的历史数据 history = StockHistory.objects.filter(code=code).order_by('-date')[:60] history = list(reversed(history)) # 2. 转换为模型输入格式 df = pd.DataFrame([{ 'open': h.open, 'high': h.high, 'low': h.low, 'close': h.close, 'volume': h.volume } for h in history]) # 3. 标准化 scaled = scaler.transform(df[['open', 'high', 'low', 'close', 'volume']]) X_input = np.expand_dims(scaled, axis=0) # 形状 (1, 60, 5) # 4. 预测并还原 pred = model.predict(X_input)[0][0] pred_price = scaler.inverse_transform( np.concatenate([np.zeros((1, 4)), [[pred]]], axis=1) )[0, 4] return JsonResponse({'code': code, 'predicted_price': float(pred_price)})

第4步的还原逻辑和训练时的处理方式一致,这是最容易出错的地方。我在第一次实现时,直接在单个数值上调用inverse_transform,结果报错说需要二维数组。后来才意识到scaler是根据六列数据拟合的,必须有完整的特征矩阵才能转换。

5.2 端到端测试要点

联调完成之后,需要走一遍完整的功能测试。测试用例至少要覆盖以下几个方面:

  • 正常流程:输入有效的股票代码,返回历史数据和预测结果,前端图表渲染正常。
  • 异常输入:输入不存在的股票代码,系统不能崩溃,要返回友好的错误提示。
  • 数据不足:新上市股票历史交易日不足60天,系统应提示“数据不足,无法预测”。
  • 并发请求:多个用户同时查询,Django开发服务器在单线程下可能阻塞,但毕业设计场景一般不做高并发要求,能正常处理即可。

这些测试用例的截图放进毕业论文里,就能展示完整的“系统测试”章节,比空泛地写“系统运行稳定”要有说服力得多。

5.3 本地部署注意事项

毕业设计通常是在答辩现场演示,部署环境一般是自己的笔记本电脑。这里有几个经验点:

TensorFlow是CPU版本就够用。训练时如果只用CPU,100轮LSTM训练在小数据集上大约需要3~5分钟,可以接受。如果装了GPU版反而容易因为显卡驱动版本不匹配出现CUDA错误,浪费时间。

Django开发服务器的运行命令保持不变:

python manage.py runserver

启动后浏览器访问http://127.0.0.1:8000即可。在实际演示之前,我强烈建议先完整跑一遍流程,确认TensorFlow和Django的版本兼容性没问题。

6. 常见问题与排查技巧实录

最后这节,我把在模拟项目X开发过程中遇到最典型的几类问题记录在这里,这些问题在答辩前没解决的话,现场演示翻车概率极高。

6.1 版本兼容性问题

TensorFlow与Python、NumPy之间的版本兼容是新手最容易卡住的环节。比如TensorFlow 2.10之后的版本在Windows上不再支持原生GPU训练;NumPy版本过高会导致加载模型时报错module 'numpy' has no attribute 'float'。

这个问题我刚遇到时头疼了半天,后来总结出最稳妥的环境配置组合:

pip install tensorflow==2.10.0 pip install numpy==1.23.5 pip install django==4.1.7 pip install pandas==1.5.3 pip install scikit-learn==1.2.2

这个组合在2024年的Windows和macOS上都能正常跑,各库之间的依赖兼容性经过大量项目验证。不要盲目安装最新版本,尤其是TensorFlow,新版本对依赖项的约束经常让人措手不及。

6.2 预测结果严重偏高的数据泄露陷阱

我最初训练出来的模型在测试集上MAPE只有0.8%,但把模型接入系统后,对真实新数据的预测值却明显偏离正常范围。排查了很久,发现是数据预处理时,我用全部数据(包含测试集)做了fit_transform,再切分训练集和测试集。

这就相当于模型在训练时“偷看”了测试集的价格区间,测试表现自然虚高。而真实推理时,新数据的分布和训练时的标准化范围不一致,预测值就崩了。这个问题的修复方案很简单:先用训练集拟合scaler,然后用训练好的scaler去转换所有数据。

这是股票预测毕业设计中最容易被忽视、也最具迷惑性的错误,写论文时如果能把“避免数据泄露”单独作为一个设计要点展开,会是非常亮眼的内容。

6.3 LSTM训练震荡不收敛

有同学会遇到训练loss曲线剧烈震荡、迟迟不降的情况。排查方向主要有三个:

第一,数据有没有做标准化。没有标准化的原始价格数据和成交量数据量级差异巨大,梯度更新方向被量级大的特征主导,模型自然训练不好。

第二,学习率是否过大。Adam默认学习率是0.001,一般不用改,但如果数据量很小,可以降到0.0001试试。

第三,时间步长是否过大。有些同学把time_steps设成240(一年的交易日),对LSTM来说输入过长且样本急剧减少,反而让训练更困难。60~90是合理区间。

6.4 模型加载后首次请求响应慢

首次请求时Django进程需要加载h5模型文件,这个动作可能要2~3秒。这在演示现场是非常尴尬的等待。解决思路是:在Django启动时预加载模型。可以在apps.py中重写ready()方法,或在视图模块被导入时触发加载。

如果不想改代码架构,还有一个土办法:在运行系统前,先手动执行一个小脚本,把模型加载到内存里,然后再启动Django。虽然不太优雅,但在答辩演示场景中很实用。

最后的经验分享

做这个项目最大的体会是:毕业设计不是模型效果越炫越好,而是要把“数据→训练→部署→展示”整条链路打通。很多同学花大量时间调模型、刷指标,却在系统演示环节翻车——不是Django起不来,就是前端图表不显示,反而得不偿失。

如果想在原课题基础上做得更有亮点,我建议可以增加一个“多股票对比预测”功能,让用户同时输入两只股票代码,前端并排展示预测走势曲线。这个功能实现难度不大,但答辩时视觉冲击力更强,也能引出“模型是否具有泛化能力”的讨论。

另外一个实用的建议是:把项目文件组织好,模型、源码、论文、答辩PPT分目录存放,README写清楚运行步骤。答辩现场老师大概率会要求现场操作,一个条例清晰的工程目录本身就是加分项。最后再说一句,所有预测结果仅供学习研究使用,论文里记得加上这一句说明。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进