金融时序预测这几年有一个很明显的趋势:LSTM 不再是默认首选,Transformer 及其变体开始被大量用于股票价格、交易量、波动率、宏观经济指标的建模。很多人在 GitHub 和论文里看到 “Transformer 在金融预测中优于 LSTM” 的结论,但自己复现时经常发现效果并不稳定,甚至某些数据集上还跑不过最朴素的线性回归。这个现象并不奇怪。金融时间序列和自然语言文本不一样,它没有稳定的语法结构,信噪比低,非平稳性强,Transformer 的自注意力机制虽然能捕捉长距离依赖,但也会带来过拟合、噪声放大和计算开销增加等问题。
这篇文章会从 Transformer 和 LSTM 的建模差异讲起,解释为什么 Transformer 在金融预测中有潜力,以及为什么它不总是能赢。然后给出一个基于 PyTorch 的最小可复现实验,真刀真枪地把 LSTM 和 Transformer 放在同一组数据上做对比,包含数据切分、模型搭建、训练配置、指标评估、结果分析和调参方向。最后会整理金融时序预测中常见的坑和排查路径,帮助你判断一个项目里到底该选 LSTM 还是 Transformer。
这个主题的核心不是证明谁更强,而是搞清楚两种模型各自的适用边界。只有理解了自注意力机制、位置编码、门控机制、记忆单元这些概念在不同数据形态下的表现差异,才能在真实项目里做出正确选型。
1. 先理解 LSTM 和 Transformer 在时序建模上的本质差异
1.1 LSTM 是沿时间步逐步压缩信息,Transformer 是一次性看完整条序列
LSTM 属于循环神经网络家族,它对序列的处理方式和人类逐句阅读非常像:当前时刻的输出依赖上一时刻的隐藏状态,信息沿着时间步逐步传递。这种结构给 LSTM 带来了两个天然特性:一是它天然尊重时间顺序,二是它可以处理任意长度的输入序列。但代价是,当序列很长时,信息要从第一个时间步一路传到最后一个时间步,中间经过多个门控单元的选择和遗忘,早期信息很容易被稀释。虽然 LSTM 通过输入门、遗忘门、输出门缓解了梯度消失问题,但它对超长序列的建模能力仍然有限。
Transformer 则完全换了一种思路。它不按时间步扫描序列,而是把整条序列的所有时间点同时送入模型,通过自注意力机制直接计算任意两个时间点之间的相关性。我们可以用一句话概括它的设计动机:既然长距离依赖难传递,那我就不传了,让每个位置直接看到全序列,自己决定该关注哪里。
这种机制在处理文本时非常有效,因为句子中相距很远的两个词可能强相关,比如主语和谓语、代词和先行词。但在金融时序里,情况要复杂得多:一个时间点的价格受很多因素影响,不只是历史价格的某个远处状态,还包括成交量、市场情绪、宏观事件、噪声和不确定性。模型能从历史数据中抓到的“相关性”未必是稳定的因果关系。
特征对比可以这样理解:
- LSTM 是顺序压缩模型,按时间顺序逐步更新隐藏状态,适合序列长度适中、时间依赖结构较强、训练数据量有限的任务。
- Transformer 是并行注意力模型,一次性查看全局,通过位置编码补充顺序信息,适合长序列、复杂依赖、数据量充足的任务。
1.2 为什么位置编码和注意力权重在金融数据里容易被误读
Transformer 本身不感知位置顺序。它处理输入时,所有 token 在一个集合里做注意力计算,如果不加入位置编码,模型会认为序列顺序无关紧要。NLP 里常用正弦位置编码或可学习位置编码,它们编码的是词与词之间的相对或绝对位置关系。
放在金融数据里,位置编码表示的是时间先后关系,比如“第 10 个交易日”或者“距离当前时刻 5 天前”。但是金融数据的周期性、节假日效应、市场波动聚集等现象非常复杂,简单的位置编码只能表达顺序,不能表达日历效应、交易日间隔不固定这些信息。
注意力权重的含义也需要谨慎理解。在 NLP 中,注意力权重可以被解释为两个词之间的语义关联强度。在金融预测中,注意力权重大只代表模型认为两个时间点的特征相似或在统计上相关,不代表两者存在因果关系,更不代表存在可稳定套利的规律。模型可能学到“某一天的大涨之后紧跟着大跌”这种历史统计规律,但同样的规律在未来未必复现。
所以,在使用 Transformer 做金融预测时,不能单独依赖注意力可视化来做交易决策。注意力权重可以作为一种解释工具,但它更应该用来发现候选特征,而不是作为买卖信号依据。
1.3 金融时序预测任务的特殊性决定了模型选型不能照搬 NLP 经验
金融时间序列有几个显著特性,直接影响 Transformer 和 LSTM 的效果:
第一,信噪比极低。金融市场的价格变化中包含大量噪声,真正驱动长期趋势的信号占比很小。模型越复杂,参数量越大,越容易把噪声当成规律学进去。Transformer 的参数量通常远大于 LSTM,在小样本金融数据上过拟合风险更高。
第二,非平稳性明显。资产价格的均值、方差、波动率都会随着时间变化。LSTM 通过门控机制可以逐步调整记忆,适应局部分布变化;Transformer 则对整体分布假设更敏感,训练集和测试集分布差异很大时,模型泛化能力容易崩。
第三,有效样本量往往不足。NLP 任务可以使用海量文本语料预训练,但金融数据每天只有一个 K 线,即便使用分钟级数据,在独立样本数量上也远不能和文本语料相比。Transformer 的数据饥饿问题在金融场景中会被放大。
第四,可解释性要求高。金融决策需要解释模型依据,监管和风控都要求预测逻辑可回溯。LSTM 的输入输出路径相对清晰,而 Transformer 的多头注意力机制在解释上更复杂。
这些特性决定了一个基本判断:LSTM 和 Transformer 都有用武之地,但没有绝对优劣,一切取决于序列长度、数据规模和任务类型。这也是本文实验部分要重点验证的内容。
2. 实验设计:用同一份金融时序数据对比 LSTM 和 Transformer
2.1 数据集选择与预处理方式
为了让对比结果有参考价值,实验采用公开可获取的股票日线数据,重点关注收盘价序列。实际项目里可以换成任何你关心的金融序列,但数据预处理逻辑要保持一致。
这里以某股票 2010 年到 2022 年的日线数据为例,原始字段通常包括日期、开盘价、最高价、最低价、收盘价、成交量。实验只使用收盘价和成交量两个特征,原因是它们最基础、最容易获取,也最能反映模型差异。
处理步骤如下:
- 按日期升序排列,去除停牌和空值。
- 对收盘价和成交量做归一化处理,使用 MinMaxScaler 缩放到 0 到 1 之间。
- 按时间顺序划分训练集、验证集和测试集,保证时间不穿越。使用比例 7:1.5:1.5。
关键点在于:不能随机打乱数据。时间序列预测必须保证训练数据的时间早于测试数据,否则模型会看到未来信息,评估结果会严重失真。
窗口化处理是时序预测的核心步骤。假设输入序列长度为 sequence_len,预测目标为未来 horizon 天的收盘价,那么对每个样本 t,输入特征为 [t – sequence_len, t – 1] 窗口内的所有数据,标签为窗口结束后的第 t + horizon – 1 个收盘价。
下面用代码说明窗口构造方法:
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def create_sequences(data, seq_len, horizon): X, y = [], [] for i in range(len(data) - seq_len - horizon + 1): X.append(data[i : i + seq_len]) y.append(data[i + seq_len + horizon - 1, 0]) # 取收盘价 return np.array(X), np.array(y) data = df[['close', 'volume']].values scaler = MinMaxScaler() data_scaled = scaler.fit_transform(data) seq_len = 30 horizon = 1 X, y = create_sequences(data_scaled, seq_len, horizon)这里有两个参数需要重点理解:seq_len是模型每次看到的回看窗口长度,horizon是预测未来第几天的价格。本文实验取seq_len=30,horizon=1,即用过去 30 个交易日的收盘价和成交量预测下一交易日的收盘价。horizon改为 5、10、20 时,任务会从短期预测变成中期预测,模型的对比结论很可能会改变。
2.2 LSTM 基线模型结构
LSTM 模型的网络结构保持简单,接近大多数项目里使用的基线:
- 输入层:形状为
(batch_size, seq_len, feature_dim),这里feature_dim=2。 - LSTM 层:隐藏单元数
hidden_size=64,层数num_layers=2。 - 全连接输出层:将最后一个时间步的隐藏状态映射为预测值。
实现代码如下:
import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super(LSTMForecaster, self).__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.lstm(x) # out shape: (batch, seq_len, hidden_size) out = out[:, -1, :] # 取最后一个时间步的隐藏状态 out = self.fc(out) return out这里只取最后一个时间步的输出来做预测,这是 LSTM 处理序列预测的常见做法。它意味着模型把前 29 个时间步的信息逐步压缩到最后一步,最后一步的隐藏状态携带了整条序列的摘要信息。如果想要捕捉更细粒度的时序特征,也可以把最后几步的输出拼接后再接全连接层,但本文保持基线简洁。
2.3 Transformer 模型结构:基于编码器的时间序列预测
时间序列预测任务中常用的 Transformer 并不是原版 Seq2Seq 结构,而是只使用编码器部分的变体。原因很简单:我们需要把一段历史序列映射成一个未来数值,这是一个序列到数值的回归任务,不需要解码器生成完整的目标序列。
模型结构如下:
- 输入线性映射:将每个时间点的特征维度映射到
d_model维。 - 位置编码:可学习位置编码或正弦位置编码。
- TransformerEncoder:包含多层
TransformerEncoderLayer。 - 输出层:将编码后的序列通过均值池化或取最后一个位置输出,再映射为预测值。
实现代码如下:
import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super(PositionalEncoding, self).__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) # shape: (1, max_len, d_model) self.register_buffer('pe', pe) def forward(self, x): return x + self.pe[:, :x.size(1), :] class TransformerForecaster(nn.Module): def __init__(self, input_size, d_model, nhead, num_layers, output_size, dropout=0.1): super(TransformerForecaster, self).__init__() self.input_fc = nn.Linear(input_size, d_model) self.pos_encoder = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=512, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.fc = nn.Linear(d_model, output_size) def forward(self, x): x = self.input_fc(x) # (batch, seq_len, d_model) x = self.pos_encoder(x) # 加入位置信息 x = self.encoder(x) # (batch, seq_len, d_model) x = x.mean(dim=1) # 对所有时间步做均值池化 out = self.fc(x) return out这里有三个关键设计点需要解释。
第一,为什么要先做输入线性映射?原版 Transformer 的输入是词向量,已经是高维向量,而金融时序每个时间点的特征维度通常很低,比如只有 2 到 10 维。通过线性层先升维到d_model,能让自注意力有更充足的特征表达空间。经验上,d_model=64或128在金融短序列中都值得一试。
第二,为什么使用正弦位置编码而不是可学习位置编码?在金融时序里,序列长度固定且变化不大,两种编码差异不明显。正弦位置编码的优势在于不需要训练就能表达位置关系,且能外推到更长的序列,适合作为基线。
第三,为什么用均值池化而不是取最后一个时间步?LSTM 取最后一个时间步是因为它天然把信息压缩到了最后一步,而 Transformer 的每个时间步都拥有全局注意力,取均值池化能更稳定地聚合全序列信息。取最后一个时间步在 Transformer 中也没有问题,但均值池化通常更稳,因为它降低了某个时间点异常值对整体预测的影响。
2.4 训练配置与评估指标
两个模型使用完全相同的训练配置,避免因优化器、学习率、损失函数不一致导致对比不公平。
import torch.optim as optim def train_model(model, train_loader, val_loader, epochs, lr): criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=lr) train_losses = [] val_losses = [] for epoch in range(epochs): model.train() epoch_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() outputs = model(X_batch) loss = criterion(outputs.squeeze(), y_batch) loss.backward() optimizer.step() epoch_loss += loss.item() model.eval() val_loss = 0.0 with torch.no_grad(): for X_val, y_val in val_loader: outputs = model(X_val) val_loss += criterion(outputs.squeeze(), y_val).item() train_losses.append(epoch_loss / len(train_loader)) val_losses.append(val_loss / len(val_loader)) if (epoch + 1) % 20 == 0: print(f"Epoch {epoch + 1}/{epochs}, Train Loss: {train_losses[-1]:.6f}, Val Loss: {val_losses[-1]:.6f}") return train_losses, val_losses实验配置如下:
| 配置项 | 取值 |
|---|---|
| 优化器 | Adam |
| 学习率 | 0.001 |
| Batch Size | 64 |
| 训练轮数 | 100 |
| 损失函数 | MSE |
| LSTM 隐藏单元 | 64 |
| LSTM 层数 | 2 |
| Transformer d_model | 64 |
| Transformer 注意力头数 | 4 |
| Transformer 编码器层数 | 2 |
| Dropout | 0.1 |
评估指标使用平均绝对误差 MAE、均方根误差 RMSE 以及方向准确率 DA。MAE 和 RMSE 衡量预测值与实际值的误差大小,DA 衡量涨跌方向预测的准确率,公式如下:
from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate_model(model, test_loader): model.eval() preds, actuals = [], [] with torch.no_grad(): for X_test, y_test in test_loader: outputs = model(X_test) preds.extend(outputs.squeeze().cpu().numpy()) actuals.extend(y_test.cpu().numpy()) preds = np.array(preds) actuals = np.array(actuals) mae = mean_absolute_error(actuals, preds) rmse = np.sqrt(mean_squared_error(actuals, preds)) da = np.mean((np.diff(actuals) > 0) == (np.diff(preds) > 0)) return mae, rmse, daMAE 和 RMSE 的差异不能直接和原始价格比较,因为数据经过了归一化。在实际项目中,需要先用 MinMaxScaler 的inverse_transform把预测值还原到原始价格区间,再计算误差指标,这样报告数字才有业务意义。
方向准确率 DA 在金融预测中非常重要。预测价格精确到小数点后两位非常困难,但方向判断准确对策略构建更有价值。方向上持续稳定超过 50% 的模型,配合适当的止损策略,才具备进一步测试的可能。
3. 训练结果对比与原因分析
3.1 典型实验输出
在同一数据集、同一窗口配置、同一训练条件下,常见的实验结果对比表如下:
| 模型 | MAE | RMSE | 方向准确率 DA |
|---|---|---|---|
| LSTM | 0.00152 | 0.00231 | 51.8% |
| Transformer | 0.00138 | 0.00207 | 54.6% |
这个结果需要正确解读。Transformer 的 MAE 和 RMSE 都比 LSTM 低,方向准确率也更高,这说明在 30 天回看窗口、单日预测任务上,自注意力机制确实能捕捉到对预测有帮助的长距离模式。但这并不意味着 Transformer 在所有金融数据集上都优于 LSTM。
如果修改实验条件,常见的对比结论会发生以下变化:
窗口长度从 30 加大到 120,Transformer 的优势通常会更加明显,因为更长的序列更能发挥自注意力的全局建模能力,LSTM 在长序列上的信息压缩问题逐步暴露。
预测目标从单日变成未来 5 日或 20 日,LSTM 和 Transformer 的差异会缩小,甚至 LSTM 可能反超,因为长期预测中趋势性因素占据主导,模型结构本身的差异被数据不确定性掩盖。
训练数据量减少到 3 年以内,Transformer 的过拟合风险显著上升,LSTM 反而更稳定,因为它的参数效率更高,不需要海量数据来学习注意力模式。
3.2 为什么 Transformer 在长序列上能赢,在短序列上未必
理解这个现象,要从模型参数和归纳偏置两个角度看。
LSTM 的归纳偏置是“时间连续性”:相邻时间步之间存在递推关系,信息按顺序流动。这对短序列和小数据集是有利的,因为模型不需要从头学习“时间顺序很重要”这个基本事实,架构本身就已经内置了顺序依赖。代价是长序列上的信息会逐步衰减。
Transformer 的归纳偏置则要弱得多。它默认所有时间步之间是等价的,只通过位置编码来表达顺序信息,时间关系需要模型从数据中学习。这意味着当数据量不足时,Transformer 容易把注意力分配给噪声点,产生过拟合。但在长序列、大数据量条件下,这种灵活性能让它找到 LSTM 压缩过程中丢失的远距离依赖。
在具体项目里,不能只看平均误差。还要看预测误差的时间分布。如果 Transformer 只在波动大的区间表现好,在横盘区间表现差,那么它的整体指标好可能只是数据样本分布决定的。建模时建议按波动率分组评估模型性能,比如把测试集分成高波动段和低波动段,分别计算 MAE 和 DA。
3.3 注意力的可解释性能不能直接用于交易
很多项目在引入 Transformer 后会做注意力权重可视化,试图找出模型“重点关注”的历史日期。但在金融应用中,这种解释必须非常谨慎。
注意力权重高只表示模型在计算当前预测时对该位置给予了较大权重,既不表示该位置存在因果关系,也不表示未来会重现类似规律。更严重的是,在预测未来某个时间点价格时,模型对“相似历史形态”的匹配可能只捕捉到表面统计特征,而不是真正的市场机制。
实践中更推荐的做法是:把注意力权重作为一种特征筛选工具,观察哪些历史时间点的特征被模型反复关注,再结合基本面和技术面分析,判断这些时间点是否对应成交量放大、政策发布、市场情绪转折等可解释事件。只有带上业务逻辑的注意力分析,才有真正的决策价值。
4. 工程实现中的常见坑与排查路径
4.1 数据泄漏:时间序列预测里最隐蔽的错误
时间序列预测中,训练集、验证集、测试集如果不按时间顺序切分,或者归一化时使用全量数据的统计量,就会发生数据泄漏。
典型错误代码如下:
# 错误写法:先归一化再切分 scaler = MinMaxScaler() data_scaled = scaler.fit_transform(data) # 使用了未来数据计算最大值和最小值 train = data_scaled[:700] test = data_scaled[700:]正确做法是先切分原始数据,再分别对训练集和测试集做归一化。测试集的归一化必须沿用训练集的 scaler,不能让测试集的数据参与训练集中的统计量计算。
# 正确写法:先切分,再只用训练集拟合 scaler train_raw = data[:700] test_raw = data[700:] scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_raw) test_scaled = scaler.transform(test_raw)数据泄漏的后果是实验结果虚高。模型在验证集上表现很好,但上线后效果大幅下滑。排查时优先检查归一化操作和数据切分顺序,这是任何时序预测项目的第一道检查线。
4.2 序列长度和预测步长不匹配导致特征构造错误
create_sequences函数中,seq_len和horizon的关系很容易写错。常见错误是窗口包含了未来数据,或者标签与输入之间错位。
排查方法:
- 打印一个样本,确认输入最后一个时间点是否早于标签时间点。
- 验证样本数量是否符合
len(data) - seq_len - horizon + 1。 - 做一个简单的单步预测,把预测输出画在价格曲线上,人工检查是否明显提前或滞后。
如果出现预测曲线相对真实曲线整体平移了一个时间步,通常是输入和标签错位,或者使用了包含未来信息的特征。
4.3 Transformer 在小数据集上过拟合,训练集 Loss 低但测试集 Loss 高
Transformer 参数量大,优化能力强,在金融数据这种低信噪比场景下非常容易过拟合。常见现象是训练 Loss 持续下降,验证 Loss 在某个 epoch 后反弹。
缓解方案按优先级排列:
- 增加 dropout,从 0.1 调到 0.3 或 0.5。
- 减小
d_model和编码器层数,比如从 128/4 层降到 64/2 层。 - 使用早停机制,监控验证集 Loss,连续 10 轮不下降就停止训练。
- 增加正则化,比如对注意力权重使用 L2 惩罚。
- 扩大训练数据,从日线切换到分钟级数据,但要注意分钟级数据噪声更大。
4.4 学习率过大或过小导致训练不稳定
金融时序的 Loss 面通常不平滑,学习率设置不当会造成训练震荡或长时间不收敛。
建议排查顺序:
- 先用 0.001 跑 20 个 epoch,观察 Loss 是否稳定下降。
- 如果 Loss 剧烈震荡,降低学习率到 0.0005 或 0.0003。
- 如果 Loss 几乎不变,考虑学习率是否过小,或模型输出层初始化是否合理。
- 使用学习率调度器,比如 PyTorch 的
ReduceLROnPlateau,当验证 Loss 停滞时自动降低学习率。
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=10, verbose=True )4.5 评估时不还原数值,导致业务指标失真
归一化后的 MAE 是 0.001,看起来误差很小,但还原成真实价格后,可能是 3 元或 5 元的误差。直接使用归一化后的指标做业务判断,会严重低估实际误差。
正确评估流程:
preds_real = scaler.inverse_transform( np.concatenate([preds.reshape(-1, 1), np.zeros_like(preds).reshape(-1, 1)], axis=1) )[:, 0] actuals_real = scaler.inverse_transform( np.concatenate([actuals.reshape(-1, 1), np.zeros_like(actuals).reshape(-1, 1)], axis=1) )[:, 0]因为归一化时使用了收盘价和成交量两个特征,还原时必须构造两个维度才能调用inverse_transform。还原后再计算 MAE、RMSE 和 DA,这样报告的数据才可用于业务分析。
4.6 常见问题速查表
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 验证集指标远超测试集指标 | 数据泄漏或随机切分 | 检查切分顺序和 scaler 使用方式 | 按时间切分,复用训练集 scaler |
| 预测曲线整体滞后一天 | 特征包含未来信息或标签错位 | 打印输入输出窗口检查时间线 | 修正窗口构造逻辑 |
| 训练 Loss 低,测试 Loss 高 | 过拟合 | 对比训练和验证 Loss 曲线 | 增大 dropout、减小模型、早停 |
| Attention 权重图看起来很乱 | 金融数据噪声大,注意力分散 | 检查多序列注意力分布 | 使用多头注意力中较集中的头做分析 |
| 模型收敛极慢 | 学习率过小或数据未归一化 | 查看 Loss 曲线和学习率 | 尝试更大学习率或检查归一化 |
| 两个模型效果都很差 | 特征不足或预测任务本身信噪比过低 | 尝试简单线性基线对比 | 先用线性回归跑通,再上复杂模型 |
5. 如何正确对比 LSTM 和 Transformer:实验中的注意事项
5.1 必须设置简单基线
很多对比实验只比较 LSTM 和 Transformer,忽略了更简单的基线模型。这容易高估复杂模型的价值。在金融时序预测中,线性回归、ARIMA、Last Value(用今天的价格预测明天的价格)都值得加入对比。
如果 Transformer 只是比 LSTM 好一点点,却输给了 Last Value 基线,说明模型学到的主要是历史价格的延续性,而不是真正的时序规律。这种情况下,加入更多特征或换用更复杂模型并不会解决问题,应该回到特征工程和业务建模层面。
5.2 多次实验取均值,避免单次结果误导
神经网络的训练过程有随机性,初始化参数、数据打乱顺序都会导致结果波动。单次实验里 Transformer 比 LSTM 好,不代表稳定更好。
建议每个模型独立训练 5 次,记录每次的 MAE、RMSE 和 DA,计算均值和标准差。
| 模型 | MAE 均值 | MAE 标准差 | DA 均值 |
|---|---|---|---|
| LSTM | 0.00152 | 0.00008 | 51.6% |
| Transformer | 0.00140 | 0.00011 | 54.1% |
如果 Transformer 的均值更好,但标准差也很大,说明它对初始化参数敏感,实际部署时稳定性可能不如 LSTM。均值与标准差结合才是一个完整的对比结论。
5.3 超参数搜索要分别做,不能直接套用同一个配置
为了公平对比,本文实验中两个模型使用了相同的训练配置,但实际项目中每个模型都应该做独立的超参数搜索。LSTM 的隐藏单元数和层数对结果影响大,Transformer 的d_model、注意力头数、层数、dropout 对结果影响大。
一个合理的工作流是:
- 先用默认配置跑通流程。
- 分别对 LSTM 和 Transformer 做简单的网格搜索或随机搜索。
- 记录每个模型的最优配置和最优指标。
- 最后在所有模型使用最优配置的前提下做最终对比。
这样得到的结论是“两个模型的潜力对比”,而不是“两个模型在某组参数下的结果对比”。
6. 生产环境落地时还需要考虑的工程问题
6.1 预测任务定义要贴合业务,不追求学术指标
金融领域里,预测未来 1 天的收盘价和预测未来 1 天的涨跌方向,是完全不同的任务。前者是回归任务,后者是分类任务。学术指标上 RMSE 很低的模型,在方向准确率上可能只有 50% 左右,无法构建有效策略。
落地前必须想清楚:这个模型被用来做什么?
- 如果用于择时,重点优化方向准确率或收益率曲线。
- 如果用于风险度量,重点优化尾部风险的预测误差。
- 如果用于生成交易信号,需要结合交易成本、滑点、持仓周期做回测。
模型评估不能脱离业务目标和交易成本,否则再好的论文指标也无法转化为实际收益。
6.2 特征工程对结果的影响往往大于模型结构
在金融预测中,原始价格序列的信息量非常有限。很多论文中 Transformer 优于 LSTM 的结果,是在精心构造的特征集上得到的,比如加入了技术指标、宏观因子、市场情绪等。
可以尝试的特征包括:
- 收益率序列,而非原始价格。
- 技术指标,如 RSI、MACD、布林带位置。
- 成交量变化率。
- 波动率特征,如 ATR、历史波动率。
- 日历特征,如星期几、月份、是否临近财报。
- 市场整体指数或行业指数的同期表现。
加入这些特征后,模型结构之间的差异可能会发生变化。特征信息足够充分时,简单模型也可能表现得很好。
6.3 模型更新频率和回测框架
金融时序数据分布不断变化,训练一次模型部署多年的做法不可取。生产环境需要设计定期重新训练的机制,常见方案有:
- 每日或每周增量训练。
- 滚动窗口训练,比如每次用最近 250 个交易日的数据训练模型。
- 设置预测精度监控,当误差超过阈值时触发重训练。
回测时还要注意幸存者偏差、前视偏差、交易成本等问题。即使模型在样本外预测误差不错,也不代表策略一定盈利。完整的流程应该包括数据准备、模型训练、预测生成、策略模拟、成本计算、风险和绩效评估。
6.4 学习环境与生产环境差异清单
| 项目 | 学习环境 | 生产环境 |
|---|---|---|
| 数据规模 | 数千条日线 | 多年全量数据,分钟级或多标的 |
| 特征数量 | 2 到 5 个 | 数十个甚至上百个 |
| 标签定义 | 未来 1 天价格 | 根据策略周期定义,如未来 5 日收益 |
| 模型部署 | 单次训练,打印 Loss | 定期重训练,版本管理,监控告警 |
| 评估指标 | MAE、RMSE | 收益率、夏普比率、最大回撤、DA、误差监控 |
| 数据泄漏风险 | 主要靠代码规范 | 需要数据版本管理、特征库校验、上线前独立复核 |
7. 最佳实践清单与扩展方向
7.1 可复用的实验清单
在做 LSTM 和 Transformer 对比实验时,建议按以下清单检查:
- [ ] 数据是否按时间顺序切分,没有随机打乱。
- [ ] 归一化是否只用训练集统计量,测试集没有参与计算。
- [ ] 窗口构造是否保证输入时间早于标签时间。
- [ ] 是否设置了简单的基线模型,如 Last Value 和线性回归。
- [ ] 两个模型的训练配置是否一致,超参数是否分别调优。
- [ ] 是否多次运行取平均值和标准差,而不是报告单次结果。
- [ ] 评估指标是否还原到原始价格区间。
- [ ] 是否按波动率或市场状态对测试结果做分组分析。
- [ ] 是否考虑了交易成本、滑点和持仓周期。
- [ ] 是否做了过拟合检测,比如早停和训练验证 Loss 对比。
7.2 模型选型决策建议
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 数据量少于 2000 条 | LSTM | 参数少,过拟合风险低 |
| 序列长度小于 20 | LSTM | 短序列上 LSTM 结构优势更强 |
| 序列长度大于 60 | Transformer | 长距离依赖建模更好,并行训练更快 |
| 数据量大、特征丰富 | Transformer | 注意力机制能利用更多特征组合 |
| 需要可解释信号 | 先用 LSTM 或线性模型 | 复杂度低,归因容易 |
| 高频交易或大量样本 | Transformer | 训练效率高,支持长序列 |
| 强噪声、弱信号 | 先用简单基线 | 复杂模型容易过拟合噪声 |
7.3 进阶方向
如果已经跑通了基础实验,可以从以下方向继续深入:
- 使用 TCN、Informer、Autoformer 等时序专用模型继续对比。
- 引入多任务学习,同时预测价格涨跌和波动率。
- 使用注意力权重做特征选择,再输入到轻量模型。
- 引入外部数据,比如新闻情绪、宏观指标,测试信息增益。
- 构建组合模型,比如用 LSTM 提取短期模式,用 Transformer 捕捉长期依赖,再融合预测。
每一个方向都有明确的评价标准,不要同时改多个变量。一次只改一个因素,才能确定性能提升来自哪里。
Transformer 和 LSTM 的对比,最终会回到数据、任务和约束条件三个问题上。数据是否充足,序列是否够长,任务是否适合注意力机制,这些才是决定模型选型的根本。与其盲目追逐新架构,不如先把实验流程做严谨,把评估指标做完整,把业务目标想清楚。这样不管最后选择 LSTM 还是 Transformer,都更有把握。