ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于LSTM与注意力机制的金融时序预测实战:从模型原理到代码复现

基于LSTM与注意力机制的金融时序预测实战:从模型原理到代码复现 简介这份PDF文献面向金融数据分析、量化投资与深度学习方向的研究者及学生聚焦金融时间序列预测这一核心问题。内容以LSTM与注意力机制融合的AM-LSTM模型为主线对比传统ARIMA等统计方法系统梳理了深度学习在股价、基金、市场指数波动预测中的应用并给出RMSE、MAE等精度评价指标适合作为课程论文、毕业设计或课题研究的参考文献。资源包共1个PDF文件大小约1.55MB内容为完整期刊论文含模型结构图、公式推导与实证对比便于直接引用与复现思路。目前已有391人学习下载可作为深度学习与金融交叉领域入门及方法选型的专业指导材料。1. 一份把 LSTM 和注意力机制讲透的金融时序预测实战文档金融时间序列预测这个方向很多人卡在同一个地方论文看了一堆公式推导也能跟着走但真到动手复现 AM-LSTM 的时候数据怎么切、序列长度设多少、归一化在哪一步做全是模糊的。这份《基于深度学习方法的金融时间序列预测研究》正好补上了这个断层——它不是纯理论综述而是拿上证指数 2010 到 2019 年共 2432 天真实数据把 LSTM 和引入注意力机制的 AM-LSTM 两个模型从头跑了一遍给出了 RMSE 和 MAE 的逐项对比。适合两类人一是正在做金融时序方向论文、需要一份可参照的实验设计二是已经会用 Python 搭 LSTM、但想搞清楚注意力机制到底怎么嵌进时序预测流程的工程师。下面我按自己拆解这份文档的思路把能复现的部分一条条拆开讲。2. 从 LSTM 到 AM-LSTM两个模型的结构差异与选型逻辑2.1 LSTM 的三个门到底在干什么文档里对 LSTM 的描述没有停留在“解决了梯度消失”这种一句话结论上而是把输入门、遗忘门、输出门的计算式逐个列了出来。我按自己的理解重新捋一遍输入门 ( i_t ) 决定当前时刻的新信息有多少要写进单元状态遗忘门 ( f_t ) 决定上一时刻的单元状态有多少要保留输出门 ( o_t ) 决定当前单元状态有多少要暴露给隐藏层。三个门都用 sigmoid 函数把值压到 0 到 1 之间本质上是在做“信息通过率”的软控制。候选值向量 ( \tilde{c}t ) 用的是 tanh 激活输出范围 -1 到 1负责生成新的候选信息。单元状态的更新是 ( c_t f_t \odot c{t-1} i_t \odot \tilde{c}_t )这里 ( \odot ) 是哈达玛积也就是逐元素相乘。隐藏层状态 ( h_t o_t \odot \tanh(c_t) )。这套机制的核心在于LSTM 不是把所有历史信息一视同仁地往下传而是通过门控做选择性记忆。为什么金融时序特别吃这套因为股价数据里噪声占比很高某一天的异常波动可能只是短期扰动不代表趋势。LSTM 的遗忘门可以在训练中学会“忽略”这类信息把真正有预测价值的长期模式保留在单元状态里。这也是文档里提到的“适应非平稳序列”和“对非线性系统建模”两个优点的底层支撑。2.2 注意力机制在时序预测里加在哪一层AM-LSTM 的关键改动在 LSTM 输出之后、最终预测之前。文档里的做法是LSTM 跑完整个输入序列后得到每个时刻的隐藏层状态 ( h_1, h_2, \ldots, h_t )把这些隐藏层状态组成矩阵 ( h )然后送进注意力机制。注意力机制先算每个时刻的注意力得分 ( e_t V^T \tanh(W h_{t-1} U h_t) )再用 softmax 归一化成权重因子 ( \alpha_t )。最终用于预测的不是最后一个时刻的隐藏状态而是所有时刻隐藏状态的加权和 ( E_j \sum_{t1}^{n} \alpha_{j,t} h_t )。这个设计的直觉是预测某一天的指数时不是只有最近几天的数据重要可能一个月前甚至更早的某个时间点包含了关键信息。LSTM 虽然能记住长期依赖但它对所有时间步的隐藏状态是“平权”地往下传的到了最后一步早期信息已经被稀释了。注意力机制相当于给每个时间步的隐藏状态打了一个“重要性分数”让模型自己学出来哪些时间点对当前预测更关键。文档里没有展开说的一点是注意力权重的可解释性。在实际项目中你可以把 ( \alpha_t ) 序列画出来看看模型在预测某个时间段时注意力集中在哪些历史日期上。如果发现权重集中在财报发布日、政策公告日附近说明模型确实学到了有意义的时间模式而不是在拟合噪声。这个验证手段在论文里没提但我在实际做时序项目时经常用对判断模型是否真的“学到东西”很有帮助。2.3 为什么选上证指数而不是个股数据文档用的是上证指数 2010/01/04 到 2019/12/31 的数据剔除节假日和无效数据后共 2432 天。选指数而不是单只股票有几个实际考虑指数由多只股票加权而成单只股票的异常波动比如某公司突发利空会被平滑掉数据噪声相对低指数不会退市、不会停牌数据连续性有保障上证指数覆盖上海证券交易所全部上市股票样本量大模型学到的模式更有泛化性。输入特征选了五个开盘价、收盘价、最高价、最低价、成交量。这五个指标是股票市场最基础的数据获取成本低不需要额外计算技术指标。文档没有用 MACD、RSI 这类衍生指标说明作者想验证的是在原始数据上AM-LSTM 能否自动学到有效的特征表示。这个思路对工程实践有参考价值——很多时候我们花大量时间做特征工程但深度学习模型本身就有特征提取能力先把原始数据喂进去看看效果再决定要不要加人工特征。3. 数据预处理与模型搭建可复现的代码路径3.1 线性插补和归一化的顺序不能反文档明确写了处理顺序先线性插补再归一化。这个顺序有讲究。如果先归一化再插补缺失值在归一化时会被当作 0 或者 NaN 处理插补出来的值会偏离真实分布。先插补的话缺失位置用前后有效值的线性插值填充保持数据在原始尺度上的连续性然后再统一做 min-max 归一化。归一化公式是 ( X \frac{X - X_{\min}}{X_{\max} - X_{\min}} )把每个特征缩放到 [0, 1] 区间。注意这里的 ( X_{\max} ) 和 ( X_{\min} ) 是训练集上的极值不是全量数据的极值。如果用全量数据的极值做归一化测试集的信息会泄露到训练过程中导致评估结果虚高。文档没有明确说这一点但这是时序预测里必须注意的细节。import numpy as np import pandas as pd # 假设 df 是原始数据包含 open, close, high, low, volume 五列 # 第一步线性插补 df df.interpolate(methodlinear, limit_directionboth) # 第二步按 3:1 划分训练集和测试集 train_size int(len(df) * 0.75) # 1824 天 train_df df.iloc[:train_size] test_df df.iloc[train_size:] # 第三步用训练集的极值做归一化避免信息泄露 train_min train_df.min() train_max train_df.max() train_norm (train_df - train_min) / (train_max - train_min) test_norm (test_df - train_min) / (train_max - train_min) # 第四步构造滑动窗口序列 def create_sequences(data, seq_len): xs, ys [], [] for i in range(len(data) - seq_len): x data[i:(i seq_len)] y data[i seq_len, 1] # 预测收盘价索引 1 对应 close xs.append(x) ys.append(y) return np.array(xs), np.array(ys) SEQ_LEN 70 # 文档中效果最好的序列长度 X_train, y_train create_sequences(train_norm.values, SEQ_LEN) X_test, y_test create_sequences(test_norm.values, SEQ_LEN)这段代码里几个关键参数interpolate的methodlinear是线性插补limit_directionboth保证首尾缺失也能填充train_size按 3:1 切分和文档一致SEQ_LEN70是文档中 RMSE 最低的序列长度。滑动窗口的构造逻辑是用前 70 天的五个指标预测第 71 天的收盘价。注意y取的是data[i seq_len, 1]索引 1 对应 close 列这个索引取决于你的 DataFrame 列顺序实际跑的时候要确认。3.2 LSTM 和 AM-LSTM 的 TensorFlow 实现文档用的是 Python 3.7 TensorFlow 1.1.5优化器选 Adam损失函数用均方差迭代 6 次。这个配置放到现在看有些老但核心逻辑不变。下面用 TensorFlow 2.x 的 Keras API 重写一版结构上保持和文档一致。import tensorflow as tf from tensorflow.keras import layers, Model class AttentionLayer(layers.Layer): def __init__(self, units): super(AttentionLayer, self).__init__() self.W layers.Dense(units) self.U layers.Dense(units) self.V layers.Dense(1) def call(self, hidden_states): # hidden_states shape: (batch, seq_len, units) # 计算注意力得分 score self.V(tf.nn.tanh(self.W(hidden_states))) # score shape: (batch, seq_len, 1) attention_weights tf.nn.softmax(score, axis1) # 加权求和 context attention_weights * hidden_states context tf.reduce_sum(context, axis1) return context, attention_weights def build_lstm_model(seq_len, n_features): inputs layers.Input(shape(seq_len, n_features)) x layers.LSTM(64, return_sequencesFalse)(inputs) x layers.Dense(1)(x) model Model(inputs, x) model.compile(optimizeradam, lossmse) return model def build_am_lstm_model(seq_len, n_features): inputs layers.Input(shape(seq_len, n_features)) x layers.LSTM(64, return_sequencesTrue)(inputs) context, attn_weights AttentionLayer(64)(x) outputs layers.Dense(1)(context) model Model(inputs, outputs) model.compile(optimizeradam, lossmse) return model # 训练 lstm_model build_lstm_model(SEQ_LEN, 5) lstm_model.fit(X_train, y_train, epochs6, batch_size32, verbose1) am_lstm_model build_am_lstm_model(SEQ_LEN, 5) am_lstm_model.fit(X_train, y_train, epochs6, batch_size32, verbose1)LSTM 模型里return_sequencesFalse只取最后一个时间步的隐藏状态做预测。AM-LSTM 模型里return_sequencesTrue保留所有时间步的隐藏状态送进自定义的AttentionLayer。注意力层的三个 Dense 分别对应文档公式里的 ( W )、( U )、( V^T )tf.nn.softmax(score, axis1)在时间维度上做归一化保证所有时间步的权重和为 1。context是加权求和后的向量再经过一个 Dense 输出预测值。迭代次数设 6 次是文档里的配置实际跑的时候可以观察验证集 loss 曲线如果还在下降可以适当增加。batch_size 文档没提我一般设 32 或 64显存够的话可以调大。LSTM 单元数文档也没写这里设 64 是一个常见起点可以按数据量调整。3.3 评估指标的计算与对比文档用了 RMSE 和 MAE 两个指标公式分别是 ( \text{RMSE} \sqrt{\frac{1}{T}\sum_{i1}^{T}(y_i - \hat{y}i)^2} ) 和 ( \text{MAE} \frac{1}{T}\sum{i1}^{T}|y_i - \hat{y}_i| )。两个指标都是值越小越好。RMSE 对大误差更敏感因为误差被平方了MAE 对所有误差一视同仁更能反映平均偏差水平。from sklearn.metrics import mean_squared_error, mean_absolute_error # 预测 y_pred_lstm lstm_model.predict(X_test) y_pred_am am_lstm_model.predict(X_test) # 反归一化还原到原始价格尺度 y_test_orig y_test * (train_max[close] - train_min[close]) train_min[close] y_pred_lstm_orig y_pred_lstm.flatten() * (train_max[close] - train_min[close]) train_min[close] y_pred_am_orig y_pred_am.flatten() * (train_max[close] - train_min[close]) train_min[close] # 计算指标 rmse_lstm np.sqrt(mean_squared_error(y_test_orig, y_pred_lstm_orig)) mae_lstm mean_absolute_error(y_test_orig, y_pred_lstm_orig) rmse_am np.sqrt(mean_squared_error(y_test_orig, y_pred_am_orig)) mae_am mean_absolute_error(y_test_orig, y_pred_am_orig) print(fLSTM RMSE: {rmse_lstm:.4f}, MAE: {mae_lstm:.4f}) print(fAM-LSTM RMSE: {rmse_am:.4f}, MAE: {mae_am:.4f})反归一化这一步容易被忽略。模型训练和预测都是在归一化后的数据上进行的算出来的 RMSE 和 MAE 也是归一化尺度上的值。如果要和文档里的数值对比需要确认是否在同一个尺度上。文档里的 RMSE 在 0.16 到 0.51 之间这个量级看起来是归一化后的结果因为上证指数原始值在 2000 到 5000 点之间RMSE 不可能只有零点几。文档给出的对比结果序列长度 70 时LSTM 的 RMSE 是 0.27305AM-LSTM 是 0.16102MAE 方面 LSTM 是 0.22105AM-LSTM 是 0.11932。序列长度 50 和 30 时两个模型的误差都上升但 AM-LSTM 始终优于 LSTM。这个结论和注意力机制的设计直觉一致序列越长LSTM 的隐藏状态被稀释得越严重注意力机制的加权筛选作用越明显。4. 避坑与排查复现 AM-LSTM 时最容易翻车的五个地方4.1 序列长度设太大导致训练不收敛现象把 SEQ_LEN 设到 100 以上训练 loss 震荡不下降验证集误差反而比短序列更大。原因序列越长LSTM 需要记忆的步数越多梯度在时间维度上传播的路径越长即使有门控机制训练难度也会显著上升。文档里最长只试到 70再往上没有给出数据说明作者可能也遇到了类似问题。解决从 30 开始试逐步增加到 50、70观察验证集 RMSE 的变化趋势。如果增加序列长度后误差不再下降甚至上升说明当前数据量和模型容量不支持更长的依赖。也可以考虑增大 LSTM 单元数或增加层数来提升模型容量但要注意过拟合。4.2 归一化用了全量数据的极值现象测试集上的 RMSE 异常低但把模型部署到新数据上预测时误差巨大。原因归一化时用了全量数据的 min 和 max测试集的极值信息泄露到了训练过程中。模型在训练时“见过”测试集的边界评估结果虚高。解决严格用训练集的 min 和 max 做归一化测试集用同一组参数变换。如果后续有新数据进来也沿用训练集的归一化参数不要重新计算。这个坑在时序预测里非常常见血泪经验是任何涉及全局统计量的预处理步骤都要检查是否用了未来信息。4.3 注意力权重全为均匀分布现象训练完 AM-LSTM 后把注意力权重 ( \alpha_t ) 画出来发现所有时间步的权重几乎一样没有明显的集中趋势。原因注意力机制没有学到有效的时间模式。可能是序列长度太短注意力层没有足够的区分空间也可能是 LSTM 输出的隐藏状态本身区分度不够注意力层无法从中提取有效信号。解决先检查 LSTM 的隐藏状态是否有区分度可以把 ( h_t ) 做 PCA 降维后可视化看看不同时间步的隐藏状态是否分散。如果隐藏状态本身就很相似说明 LSTM 没有学到有效的时间特征需要调整 LSTM 的结构或训练配置。如果隐藏状态有区分度但注意力权重均匀可以尝试增大注意力层 Dense 的单元数或者给注意力得分加温度系数来放大差异。4.4 训练集和测试集划分时打乱了时间顺序现象模型在测试集上表现很好但实际预测时完全不可用。原因用了随机划分或者 shuffle 操作把未来数据混入了训练集。时序预测的核心假设是“用过去预测未来”一旦时间顺序被打乱模型学到的模式在实际场景中不存在。解决严格按时间顺序划分前 75% 做训练后 25% 做测试。训练过程中的 batch 也不要 shuffle保持时间顺序。如果数据量足够大还可以做滚动窗口验证用前 N 天训练预测第 N1 天然后窗口后移重复这个过程。4.5 损失函数选 MSE 但评估用 RMSE 时忘记开方现象训练日志里的 loss 一直在下降但算出来的 RMSE 和 loss 对不上。原因MSE 是均方误差RMSE 是均方根误差两者差一个开方。如果训练时用 MSE 做损失评估时直接拿 loss 当 RMSE 用数值会偏小。解决训练 loss 用 MSE 没问题梯度计算更方便。评估时单独算 RMSE记得开方。文档里 RMSE 和 MAE 是分开算的没有直接用 loss 代替这个做法是对的。5. 注意力权重的可视化验证与滚动预测技巧注意力权重 ( \alpha_t ) 是 AM-LSTM 相比 LSTM 多出来的一个输出很多人训练完模型就结束了没有把这个中间结果利用起来。我的习惯是每次跑完 AM-LSTM都把测试集上每个样本的注意力权重画出来按时间顺序排列看看模型在预测不同时间段时注意力集中在哪些历史日期上。具体做法是在AttentionLayer的call方法里把attention_weights一并返回预测时收集所有测试样本的权重矩阵。然后取最后一个测试样本的权重序列画成折线图横轴是输入序列的 70 个时间步纵轴是权重值。如果发现权重在某些时间点上有明显峰值去查一下那些日期对应什么事件——财报季、政策发布、重大公告如果对得上说明模型确实学到了有意义的时间模式。# 修改 AttentionLayer 的 call 方法返回注意力权重 # 预测时收集权重 _, attn_weights am_lstm_model.predict(X_test) # 假设模型输出包含权重 # 取最后一个测试样本的权重 last_weights attn_weights[-1].flatten() # shape: (70,) # 画图 import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.plot(range(SEQ_LEN), last_weights, markero, markersize3) plt.xlabel(Time Step (within input sequence)) plt.ylabel(Attention Weight) plt.title(Attention Weight Distribution for Last Test Sample) plt.grid(True, alpha0.3) plt.show()这个验证手段在论文里通常不会写但在实际项目中很有用。如果注意力权重完全均匀说明模型没有学到有效的时间选择性可能需要调整模型结构或训练配置。如果权重集中在少数几个时间步上可以进一步分析这些时间步对应的原始数据有什么特征——是波动率高的时期还是成交量放大的时期这些分析能帮你判断模型是否真的在按你期望的方式工作。另一个实用技巧是滚动预测。文档里的做法是一次性预测整个测试集的 608 天但实际应用中更常见的是滚动预测用前 70 天预测第 71 天然后把第 71 天的真实值加入输入序列再预测第 72 天如此滚动。滚动预测更接近实际使用场景因为每天收盘后你都能拿到最新的真实数据。实现上只需要把create_sequences改成逐步滑动的方式每次预测完把真实值 append 到输入序列末尾。从那以后我每次跑时序预测模型都强制走一遍注意力权重可视化确认模型不是在“假装学习”。这个习惯帮我省了很多次重新调参的时间——有时候 loss 曲线很好看但注意力权重一画出来就发现模型根本没学到东西早点发现比等到部署后翻车强。希望帮到你。本文还有配套的精品资源点击获取
返回列表