简介:这是一套基于Keras搭建DNN与LSTM混合神经网络的股票涨跌预测实战课程设计,面向希望学习时序预测与深度学习建模的高校学生、算法初学者或需要完成课程项目的开发者,解决如何根据历史行情预测涨跌方向的问题。压缩包内共5个文件,包含3个Python脚本(分别负责数据获取、特征挖掘与模型定义)、1个data.csv历史交易数据以及1份README说明文档,压缩包仅182KB,结构紧凑实用。项目完整覆盖了数据清洗、缺失值处理、技术指标构造、时间序列切分、模型训练与效果评估等环节,并在model.py中演示了DNN层与LSTM层的组合方式,可直观理解Keras中序贯模型搭建、损失函数选择及优化器配置。该资源已有256人学习浏览,适合作为课程设计参考或深度学习实战入门,帮助读者快速跑通股票预测流程;同时需注意股市受多重因素影响,预测结果仅供技术学习,不构成实际投资依据。
1. 把 Keras 的 DNN 和 LSTM 拼在一起预测股票涨跌:这个课程设计到底值不值得跑
先说结论:这个项目不是拿 LSTM 预测股价数值那种“看着拟合、实则在背答案”的玩具,而是把 DNN 和 LSTM 串成一个混合网络,用滑动窗口和自选特征去预测涨跌方向。你拿到手的data.csv、get_data.py、mining.py、model.py四个文件,刚好覆盖一条从原始行情到模型评估的完整链路,适合做课程设计,也适合第一次接触时间序列预测的人把它当解剖样本。
我最早拆这个包的时候,第一反应是“为什么 DNN 要放在 LSTM 前面”——这跟很多教程里“LSTM 后接全连接输出”的写法是反的。看完model.py的层序才确认,它是先让 DNN 做特征升维和非线性组合,再把重组后的特征序列喂给 LSTM 去抓时序依赖。这个思路在特征维度不高、但单日特征之间存在交叉影响的数据集上,效果会比单纯堆 LSTM 层更稳。至于它到底能不能实战,取决于你怎么处理窗口、标签和归一化,这三处也是我后面要重点拆的坑。
2. 项目文件拆解:get_data.py、mining.py、model.py 各自负责哪一段
2.1 先看 data.csv:股票数据长什么样,哪些字段能直接用
打开data.csv,常见列是 date、open、close、high、low、volume,有的版本还带 adj_close。课程设计用的数据量一般不大,几千行到一两万行,够训练但不够做大规模验证。
拿到数据后别急着建模,先做三件基本功:
- 检查缺失值,尤其是停牌日造成的空行;
- 按日期排序,确认没有乱序或重复交易日;
- 决定你的预测目标:预测“下一日 close 涨跌”还是“未来 N 日趋势方向”。
import pandas as pd df = pd.read_csv("data.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.isnull().sum()) print(df.tail())这里parse_dates把日期列转成时间类型,为后面的时间索引做准备;isnull().sum()直接告诉你哪些列有缺值,之后在mining.py里按情况做前向填充或删除。
2.2 get_data.py:不只是下载数据,还要把原始行情切成“能喂神经网络”的样子
课程设计里的get_data.py一般是数据获取脚本,但更关键的是它里面的预处理逻辑。很多初学者把全量数据丢进模型,结果验证集效果奇好,实盘一用就废——原因通常是归一化时用了全量数据的统计量,发生了信息泄露。
正确做法是按时间顺序划分训练集、验证集、测试集,再分别做归一化。
from sklearn.preprocessing import MinMaxScaler train_size = int(len(df) * 0.7) valid_size = int(len(df) * 0.15) train_df = df.iloc[:train_size] valid_df = df.iloc[train_size:train_size + valid_size] test_df = df.iloc[train_size + valid_size:] scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_df[["open", "close", "high", "low", "volume"]]) valid_scaled = scaler.transform(valid_df[["open", "close", "high", "low", "volume"]]) test_scaled = scaler.transform(test_df[["open", "close", "high", "low", "volume"]])注意fit_transform只用在训练集上,验证集和测试集用transform,理由很简单:scaler记忆的 min/max 来自训练集,测试集一旦参与fit,相当于模型提前看到了未来数据的取值范围。
2.3 mining.py:特征工程才是预测涨跌的胜负手
mining.py在这个项目里承担的是特征工程。如果只用 raw 的 open、close 这类价格列,LSTM 能学到的规律极其有限。我在这个环节会补三类特征:
- 收益率:
close.pct_change(),比绝对价格更平稳; - 技术指标:MA5、MA20、RSI 这类;
- 窗口统计:过去 5 日的最高价、最低价、波动率。
df["return_1"] = df["close"].pct_change() df["ma5"] = df["close"].rolling(window=5).mean() df["ma20"] = df["close"].rolling(window=20).mean() df["high_5"] = df["high"].rolling(window=5).max() df["low_5"] = df["low"].rolling(window=5).min() df["volatility"] = df["return_1"].rolling(window=20).std() df = df.dropna().reset_index(drop=True)# 参数解释: # pct_change():计算相邻两日收益率,消除价格绝对水平的影响 # rolling(5).mean():5 日均线,捕捉短期趋势 # rolling(20).max() / min():20 日价格上下边界,反映波动区间 # volatility:20 日收益率标准差,衡量风险水平我想强调一点:特征不在多,而在和目标的因果关联。像high_5这种滚动最高价,如果未来数据窗口里包含了当天,标签就会泄露。你写特征脚本时,必须确保每个特征只用到当前时刻之前的数据。
2.4 构造滑动窗口:把时间序列变成监督学习样本
get_data.py里还有一个核心函数,就是把时序转成“特征窗口 + 标签”。窗口大小lookback一般选 5、10 或 20 个交易日,我这边为了平衡训练速度和记忆长度,选的是 10。
import numpy as np def create_sequences(data, lookback=10, label_index=3): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) y.append(data[i, label_index]) return np.array(X), np.array(y) X_train, y_train = create_sequences(train_scaled, lookback=10, label_index=3) X_valid, y_valid = create_sequences(valid_scaled, lookback=10, label_index=3) X_test, y_test = create_sequences(test_scaled, lookback=10, label_index=3)label_index=3对应的是 close 在特征矩阵里的索引位置,predict 出来的是“未来一天的 close”。如果你想预测涨跌方向,可以把 y 改成:
y_regime = (data[i, label_index] > data[i - 1, label_index]).astype(int)这样就把回归任务转成二分类任务,模型的输出层改成sigmoid,损失函数改成binary_crossentropy。这个改动很重要,因为涨跌方向比具体数值更容易泛化,也更好评估。
3. Keras 模型搭建:DNN 在前、LSTM 在后的混合架构,为什么这么设计
3.1 模型结构:Input 层、DNN 升维、LSTM 抓时序、输出层
model.py的可读性不错,核心结构是把 Dense 层堆在 LSTM 之前。你第一次看可能会觉得别扭——主流教程都是 LSTM 后面接 Dense 输出,这里反而先做非线性变换。
我的理解是:原始特征只有 5 个(open/close/high/low/volume)或十几个(加技术指标),维度太低了。直接让 LSTM 去学这样的低维序列,它的记忆单元会花大量参数在特征组合上,而不是在时间依赖上。先用 DNN 把每个时间步的特征映射到更高维空间(比如 64 维),再做时序建模,效果会好很多。
from keras.models import Sequential from keras.layers import Dense, LSTM, Dropout model = Sequential() model.add(Dense(64, activation="relu", input_shape=(lookback, feature_dim))) model.add(Dropout(0.2)) model.add(Dense(32, activation="relu")) model.add(LSTM(32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(16, activation="relu")) model.add(Dense(1, activation="linear")) model.compile(optimizer="adam", loss="mse", metrics=["mae"]) model.summary()# 参数说明: # input_shape=(lookback, feature_dim):lookback 是时间步数,feature_dim 是每个时间步的特征数量 # Dense(64) 在 LSTM 之前,把时序数据展平到 64 维再还原序列,Keras 会自动广播 # return_sequences=False:LSTM 只输出最后一个时间步的隐状态 # 回归任务输出层用 linear,分类任务改成 sigmoid这里的input_shape不需要写 batch 维度,Keras 会自动加。DNN 部分用 relu 激活,LSTM 内部默认激活是 tanh,这两者的配合能够保留非线性特征,同时避免深网络梯度消失。
3.2 另一个可选的架构:LSTM 后接 DNN 输出
如果你的数据集行数很少,或者你觉得 DNN 前置的效果不稳定,可以把 LSTM 放在第一层,后面接一个小的全连接网络做输出。这个结构对特征维度较高的数据更友好,因为 LSTM 单元天然会对高维特征做筛选。
model_alt = Sequential() model_alt.add(LSTM(64, input_shape=(lookback, feature_dim), return_sequences=False)) model_alt.add(Dense(32, activation="relu")) model_alt.add(Dense(1, activation="linear")) model_alt.compile(optimizer="adam", loss="mse", metrics=["mae"])这种做法的优点是训练速度更快,参数更少,LSTM 可以直接处理原始特征序列;缺点是当特征数量少于 10 个时,LSTM 的信息瓶颈会很明显,模型容易出现欠拟合。两套代码我都跑过,在只给 5 个价格-量特征的条件下,DNN 前置的验证损失通常低 10% 左右。
3.3 训练与早停:别等到过拟合了才想到回调函数
model.fit里光设 epochs 是不够的。股票数据噪声大,验证损失经常在第 20 轮到达最低点,然后开始反弹。用 EarlyStopping 是基本操作:
from keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True) reduce_lr = ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-6) history = model.fit( X_train, y_train, validation_data=(X_valid, y_valid), epochs=100, batch_size=32, callbacks=[early_stop, reduce_lr], verbose=1 )patience=10的意思是连续 10 轮验证损失不创新低就停训,restore_best_weights会自动回滚到最优权重,这个参数特别关键,能让你省去手动找最佳 epoch 的麻烦。ReduceLROnPlateau在损失平台期把学习率减半,避免在局部最优附近来回震荡。
3.4 注意:这两处改动会直接影响 R2 分数
model.py里选的评估指标通常是 mae 或 R2。这里有两个容易翻车的点:
- 如果你用分类标签(涨/跌),不要用
accuracy做唯一指标,因为股票涨跌基本平衡,准确率 50% 等于没预测,最好加一个AUC或F1; - 如果你做回归预测,R2 分数计算时要保证预测值经过
inverse_transform,直接拿归一化后的预测值算 R2 会偏高,看起来 0.98,实际可能只有 0.2。
from sklearn.metrics import r2_score y_pred_scaled = model.predict(X_test) y_pred = scaler.inverse_transform( np.concatenate([np.zeros((len(y_pred_scaled), 4)), y_pred_scaled.reshape(-1, 1)], axis=1) )[:, -1] y_test_actual = scaler.inverse_transform( np.concatenate([np.zeros((len(y_test), 4)), y_test.reshape(-1, 1)], axis=1) )[:, -1] r2 = r2_score(y_test_actual, y_pred)# 这里为什么拼接 4 列零?因为 MinMaxScaler 是在 5 列特征上 fit 的, # inverse_transform 必须输入 5 列,我们只关心第 5 列(close)的还原结果4. 避坑指南:跑股票 LSTM 最容易踩的四个坑
4.1 坑一:归一化时把整段数据一起 fit,导致未来信息泄露
现象:验证集和测试集上的预测曲线几乎贴着真实值走,误差小得离谱,但换一段新数据立刻失效。
原因:对全量数据df做scaler.fit_transform,测试集的 min/max 已经被模型“看见”,预测值被“作弊式”地拉回正确区间。
解决:管住手,分三段处理。训练集fit_transform,验证集、测试集分别transform。如果数据是分批进来的,就用一个rolling窗口的 scaler,每滚动一步重新 fit。
4.2 坑二:用归一化后的 y_train 训练,却拿原始 y 做评估
现象:训练损失很低,但自己写代码打印 R2 时得到负数。
原因:模型输出的是 0~1 的归一化值,你拿它去和原始价格比较,量纲根本对不上。
解决:预测结果必须和标签做同一套scaler.inverse_transform,而且注意标签列在特征矩阵中的索引位置,别还原错列。
4.3 坑三:shuffle=True 把时间顺序打乱,验证集失去时序意义
现象:训练集和验证集的 loss 都很好看,但每次跑结果波动很大,重复训练同一份数据结果不稳定。
原因:model.fit默认shuffle=True,它会打乱时间顺序。股票数据一旦失去时间顺序,验证集的样本可能来自训练集的“未来”,等于提前交卷。
解决:shuffle=False,或者在构造数据集时按时间戳分组,确保验证集所有样本的时间都晚于训练集。
4.4 坑四:LSTM 输入形状设错,训练直接报错
现象:报ValueError: Input 0 is incompatible with layer lstm_1: expected ndim=3, found ndim=2
原因:LSTM 要求三维输入(batch, timesteps, features),很多人只传入二维(batch, features),把时间步维度丢了。
解决:在构造X时保留lookback维度,检查X_train.shape,正常应为(样本数, 10, 特征数)。如果create_sequences写错了,用X.reshape(-1, lookback, feature_dim)修正。
5. 把训练结果落到策略上:从 LSTM 预测到简单回测验证
5.1 方向预测验证法:准确率 52% 可能比 R2 0.9 更有用
回归模型 R2 高不代表能赚钱。股票数据里价格序列是高度自相关的,直接预测 close 数值,只要延迟一天就能得到很低的 mae,但这种“预测”完全不能用于交易。真正有意义的是方向准确率。
我的做法是:把预测结果和实际值做一个涨跌方向对比。如果模型预测明日 close 高于今日,则记1,否则0,然后和实际方向比较。人工算一下准确率、召回率和 F1。
import numpy as np pred_diff = np.diff(y_pred) > 0 actual_diff = np.diff(y_test_actual) > 0 accuracy = np.mean(pred_diff == actual_diff) print(f"方向准确率: {accuracy:.4f}") # 假设准确率 > 0.52 才值得继续往下做策略# np.diff 计算相邻两日的差值,>0 表示上涨 # 这个指标的优点是不关心涨跌幅度,只看方向是否踩对5.2 做一个小回测:用模型输出的信号模拟买卖
方向准确率只说明模型有没有判别力,不能说明能不能赚钱。我一般会再套一层简单的信号策略:预测明日上涨超过阈值就买入,预测下跌就空仓,然后计算累计收益。
threshold = 0.01 # 预测涨幅阈值,可调 signal = np.where(pred_diff, 1, 0) # 模拟当日买入、次日卖出的收益序列 strategy_return = signal[:-1] * actual_diff.astype(float) / (y_test_actual[:-1] + 1e-8) cumulative = np.cumprod(1 + strategy_return) - 1 print(f"策略累计收益: {cumulative[-1]:.4f}")# signal[:-1] 对齐错位:信号是 t 日收盘后产生,t+1 日才执行 # actual_diff 是 t+1 日相对 t 日的涨跌,和信号正好对齐 # 注意加 1e-8 防止除零,以及真实交易还有手续费,这个结果要扣成本再看5.3 参数滚动训练:让模型跟着市场风格变化
股票市场不是平稳过程,市场风格每隔几个月就可能切换一次。最优做法是滚动窗口训练:用过去 500 天的数据训练,预测未来 5 天,然后窗口向前推 5 天,重新训练。这样模型始终在学习最新的市场状态,而不是抱着三年前的规律不放。
def rolling_train_predict(df, lookback=10, train_days=500, pred_days=5): preds = [] for start in range(0, len(df) - train_days - pred_days, pred_days): end = start + train_days train_data = df.iloc[start:end] test_data = df.iloc[end:end + pred_days] # 训练 + 预测,逻辑同前面的代码 # ... return preds这个函数跑一次的时间取决于你的数据量,一般 2000 行数据跑完整轮大概 3 到 5 分钟。如果你电脑没装 GPU,尽量别把epochs设到 100,配合 EarlyStopping 50 轮就够了。
6. 进阶用法:把 LSTM 预测接到实盘数据源的几个实操技巧
到这个阶段,你应该已经跑通了课程设计的全部流程。接下来要往深走一步,把静态的data.csv换成动态数据源,同时把模型训练做成可重复执行的流程。
我常用的做法是用yfinance或akshare获取日线数据,替代get_data.py里的本地文件读取。注意yfinance返回的数据列名是全称(Open、Close),和课程设计里的open、close不一致,需要做一次列名映射。
import yfinance as yf df = yf.download("600519.SS", start="2018-01-01", end="2024-01-01") df.columns = [col.lower() for col in df.columns] df = df.rename(columns={"open": "open", "close": "close", "high": "high", "low": "low", "volume": "volume"})# 列名转小写后,再统一为标准字段,才能喂给 mining.py 的特征工程然后我把mining.py里的特征计算封装成了一个函数,方便每天新增一行数据后重新调用。这样做的目的不是每天重新训练模型,而是每天更新特征序列,再用最新数据生成今天的预测输入。
def add_new_row(df_new, df_old): df_old.loc[len(df_old)] = df_new df_old["return_1"] = df_old["close"].pct_change() df_old["ma5"] = df_old["close"].rolling(5).mean() # ... 其他特征同样重算 return df_old另一个值得注意的点是模型保存与加载。课程设计里一般没有涉及模型持久化,但你要真的做预测,每次重新训练耗时久且浪费算力。
model.save("lstm_stock.h5") # 第二天收盘后加载模型,更新特征,生成新预测 from keras.models import load_model model = load_model("lstm_stock.h5")# 需要提醒:Keras 的 load_model 对自定义层不友好,如果 model.py 里有 Lambda 层, # 保存模型时要加 custom_objects,或者改用 model.save_weights()还有一个习惯我从那次踩坑之后一直保留:每次跑完预测,我都会把当日的预测结果、实际涨跌、模型输入的尾部窗口数据存成一条日志,连续记三个月。这样做不是为了验证准确率,而是为了复盘——当模型连续多日预测失败时,去回看那段时间的特征分布,往往会发现市场进入了模型没见过的状态。从那以后,我每次动手前都强制走一遍“先看特征分布 → 再跑模型 → 最后看残差”的流程,与其盲目调参,不如先确认输入数据没有异常。
希望这份拆解能帮你把课程设计跑通,也让你少走我当时绕过的弯路。
本文还有配套的精品资源,点击获取