简介这是一套基于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(dropTrue) 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(window5).mean() df[ma20] df[close].rolling(window20).mean() df[high_5] df[high].rolling(window5).max() df[low_5] df[low].rolling(window5).min() df[volatility] df[return_1].rolling(window20).std() df df.dropna().reset_index(dropTrue)# 参数解释 # pct_change()计算相邻两日收益率消除价格绝对水平的影响 # rolling(5).mean()5 日均线捕捉短期趋势 # rolling(20).max() / min()20 日价格上下边界反映波动区间 # volatility20 日收益率标准差衡量风险水平我想强调一点特征不在多而在和目标的因果关联。像high_5这种滚动最高价如果未来数据窗口里包含了当天标签就会泄露。你写特征脚本时必须确保每个特征只用到当前时刻之前的数据。2.4 构造滑动窗口把时间序列变成监督学习样本get_data.py里还有一个核心函数就是把时序转成“特征窗口 标签”。窗口大小lookback一般选 5、10 或 20 个交易日我这边为了平衡训练速度和记忆长度选的是 10。import numpy as np def create_sequences(data, lookback10, label_index3): 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, lookback10, label_index3) X_valid, y_valid create_sequences(valid_scaled, lookback10, label_index3) X_test, y_test create_sequences(test_scaled, lookback10, label_index3)label_index3对应的是 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, activationrelu, input_shape(lookback, feature_dim))) model.add(Dropout(0.2)) model.add(Dense(32, activationrelu)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(16, activationrelu)) model.add(Dense(1, activationlinear)) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()# 参数说明 # input_shape(lookback, feature_dim)lookback 是时间步数feature_dim 是每个时间步的特征数量 # Dense(64) 在 LSTM 之前把时序数据展平到 64 维再还原序列Keras 会自动广播 # return_sequencesFalseLSTM 只输出最后一个时间步的隐状态 # 回归任务输出层用 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_sequencesFalse)) model_alt.add(Dense(32, activationrelu)) model_alt.add(Dense(1, activationlinear)) model_alt.compile(optimizeradam, lossmse, metrics[mae])这种做法的优点是训练速度更快参数更少LSTM 可以直接处理原始特征序列缺点是当特征数量少于 10 个时LSTM 的信息瓶颈会很明显模型容易出现欠拟合。两套代码我都跑过在只给 5 个价格-量特征的条件下DNN 前置的验证损失通常低 10% 左右。3.3 训练与早停别等到过拟合了才想到回调函数model.fit里光设 epochs 是不够的。股票数据噪声大验证损失经常在第 20 轮到达最低点然后开始反弹。用 EarlyStopping 是基本操作from keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) reduce_lr ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6) history model.fit( X_train, y_train, validation_data(X_valid, y_valid), epochs100, batch_size32, callbacks[early_stop, reduce_lr], verbose1 )patience10的意思是连续 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)], axis1) )[:, -1] y_test_actual scaler.inverse_transform( np.concatenate([np.zeros((len(y_test), 4)), y_test.reshape(-1, 1)], axis1) )[:, -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 坑三shuffleTrue 把时间顺序打乱验证集失去时序意义现象训练集和验证集的 loss 都很好看但每次跑结果波动很大重复训练同一份数据结果不稳定。原因model.fit默认shuffleTrue它会打乱时间顺序。股票数据一旦失去时间顺序验证集的样本可能来自训练集的“未来”等于提前交卷。解决shuffleFalse或者在构造数据集时按时间戳分组确保验证集所有样本的时间都晚于训练集。4.4 坑四LSTM 输入形状设错训练直接报错现象报ValueError: Input 0 is incompatible with layer lstm_1: expected ndim3, found ndim2原因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 日收盘后产生t1 日才执行 # actual_diff 是 t1 日相对 t 日的涨跌和信号正好对齐 # 注意加 1e-8 防止除零以及真实交易还有手续费这个结果要扣成本再看5.3 参数滚动训练让模型跟着市场风格变化股票市场不是平稳过程市场风格每隔几个月就可能切换一次。最优做法是滚动窗口训练用过去 500 天的数据训练预测未来 5 天然后窗口向前推 5 天重新训练。这样模型始终在学习最新的市场状态而不是抱着三年前的规律不放。def rolling_train_predict(df, lookback10, train_days500, pred_days5): 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, start2018-01-01, end2024-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()还有一个习惯我从那次踩坑之后一直保留每次跑完预测我都会把当日的预测结果、实际涨跌、模型输入的尾部窗口数据存成一条日志连续记三个月。这样做不是为了验证准确率而是为了复盘——当模型连续多日预测失败时去回看那段时间的特征分布往往会发现市场进入了模型没见过的状态。从那以后我每次动手前都强制走一遍“先看特征分布 → 再跑模型 → 最后看残差”的流程与其盲目调参不如先确认输入数据没有异常。希望这份拆解能帮你把课程设计跑通也让你少走我当时绕过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?