Transformer 与 LSTM 在金融时序预测中的对比与实战方案
这次我们来看一个在量化研究里被反复讨论的问题:金融时序预测中,Transformer 模型能否真正超越 LSTM。
这个问题的答案并不只是“Transformer 更强”这么简单。金融数据本身噪声大、非平稳、样本量有限,LSTM 作为循环网络在这类数据上统治了很多年。而 Transformer 靠全局注意力机制,天然适合捕捉长距离依赖和多变量之间的交叉影响。近几年的大量实验表明,在合适的特征工程和训练策略下,Transformer 在金融预测任务上的表现确实可以超过 LSTM,尤其在中长期趋势预测和多标的联合建模场景中。
这篇文章会从模型原理对比讲起,然后给出一套完整的 Transformer 金融时序预测方案的搭建思路,包括数据预处理、模型实现、训练验证、批量预测和 API 服务封装。如果你正在做股票价格预测、收益率预测、宏观经济指标预测,或者想给自己的量化策略加一个更现代的基线模型,这篇文章可以直接参考。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 模型类型 | Transformer Encoder 时序回归模型,可扩展为多变量输入 |
| 对比基准 | LSTM、BiLSTM、ARIMA 等传统时序模型 |
| 核心优势 | 全局注意力机制、并行计算、长序列依赖建模、多变量交叉特征提取 |
| 支持任务 | 单变量时序预测、多变量时序预测、多步预测、分类/回归 |
| 硬件要求 | 训练端建议 NVIDIA GPU,显存 6G 以上;推理端 CPU 即可 |
| 开发框架 | PyTorch 2.x + pandas + numpy + scikit-learn |
| 数据要求 | 标准化后的数值型时序数据,建议样本量在数千条以上 |
| 启动方式 | 命令行训练脚本 + Python 推理脚本 + FastAPI 接口服务 |
| 是否支持批量任务 | 支持,可通过批量预测脚本或 API 批量提交 |
| 是否支持 API | 支持,提供 POST /predict 接口 |
| 适合场景 | 股票/基金/商品价格预测、收益率预测、波动率建模、宏观经济指标预测 |
这里要提前说明:Transformer 并不是一个“开箱即用就能赚钱”的工具。它的优势在于特征提取能力,但金融预测的最终效果高度依赖数据质量、特征工程、训练策略和评估方式。后面每个环节都会提到。
2. 为什么金融预测需要 Transformer 而不是继续用 LSTM
2.1 LSTM 的核心瓶颈
LSTM 通过门控机制解决长序列训练中的梯度消失问题,但它本质上还是逐步递推的结构。当前时间步的计算依赖前一步的隐状态,这意味着:
- 训练无法完全并行,序列越长训练越慢。
- 长期依赖信息在经过多步传递后仍会被稀释,虽然比 RNN 好,但依然有上限。
- 对多变量之间复杂的交叉依赖关系,LSTM 需要靠堆叠层数和增加隐单元来隐式建模,效率不高。
在金融数据中,不同资产、不同因子之间往往存在滞后相关性。比如美债收益率变化可能滞后影响 A 股成长股估值,这个滞后期可能是不固定的。LSTM 对这种“不定长滞后交叉影响”的建模能力有限。
2.2 Transformer 的注意力机制解决了什么
Transformer 的核心是自注意力机制。给定输入序列,模型会计算序列中任意两个位置之间的注意力权重。这带来几个直接优势:
- 任意距离的依赖都能直接建模,不需要逐层传递。
- 所有位置可以并行计算,训练速度明显快于 LSTM。
- 可以同时关注多个维度的特征,比如开盘价、成交量、技术指标和外盘数据之间的交互关系。
- 位置编码为模型提供了时间顺序信息,不依赖递归结构。
以金融预测为例,假设我们要用过去 60 个交易日的数据预测未来 5 天的走势。对于 LSTM,第 60 天的信息需要经过 60 步更新才能影响输出,虽然门控机制让信息衰减没那么严重,但终究有损耗。而 Transformer 直接计算第 1 天和第 60 天之间的注意力权重,可以精准捕捉“60 天前的某个宏观事件仍然在影响当前走势”这种长周期规律。
2.3 实验对比中的常见结论
从材料中可以看到,Transformer 在时间序列预测任务上的表现并不是绝对优于所有模型,但在以下情况下通常能体现出优势:
- 序列长度较长(一般超过 50 个时间步)。
- 多变量之间存在复杂的交叉相关性。
- 需要同时预测多个标的或多个目标字段。
- 训练数据量相对充足(数千条以上)。
在短序列、小样本、强噪声场景下,LSTM 甚至 ARIMA 仍然可能是更稳的选择。这提醒我们:选模型不是追新,而是看数据和任务形态。更稳妥的判断是,将 Transformer 作为一个更强的基线,与 LSTM、XGBoost 等模型做对比实验,而不是默认它一定更好。
3. 金融数据预处理与特征工程
金融时序预测中,数据预处理的质量往往比模型结构更重要。下面给出一个通用处理流程。
3.1 数据来源与基础清洗
建议使用 pandas 加载数据,基础字段至少包括日期、开盘价、最高价、最低价、收盘价、成交量。数据清洗包括:
- 去除重复日期的记录。
- 处理停牌导致的缺失值,可以选择前向填充或删除。
- 剔除明显异常的交易数据(比如成交量骤变为 0 且非停牌日)。
- 统一时区和交易日历。
import pandas as pd
df = pd.read_csv("stock_data.csv", parse_dates=["date"])
df = df.drop_duplicates(subset="date")
df = df.sort_values("date").reset_index(drop=True)
df = df.fillna(method="ffill")
3.2 特征构造
原始 OHLCV 数据直接输入模型往往效果一般,建议构造以下特征:
-
收益率序列:
close.pct_change() -
对数收益率:
np.log(close / close.shift(1)) - 技术指标:移动平均线、布林带、RSI、MACD
- 市场情绪特征:成交额变化率、换手率
- 外部特征:指数涨跌、行业板块涨跌、宏观利率变化
需要特别注意的是:所有特征必须在时间序列上“向前看”以避免未来函数。也就是说,构造第 t 天的特征时,只能用第 t 天及之前的数据。
3.3 数据标准化与序列化
金融数据数值范围差异很大,建议对每个特征分别做标准化。标准化器必须只在训练集上拟合,然后用同一套参数转换验证集和测试集,否则会造成数据泄漏。
from sklearn.preprocessing import StandardScaler
feature_cols = ["open", "high", "low", "close", "volume", "pct_change", "ma5", "ma20"]
scaler = StandardScaler()
scaled = scaler.fit_transform(df[feature_cols])
然后构造滑窗样本。常用配置是:用过去 60 天的数据预测未来 5 天的收益率方向或数值。为了让代码可复现,需要固定随机种子。
import numpy as np
def create_sequences(data, seq_len=60, pred_len=5):
X, y = [], []
for i in range(len(data) - seq_len - pred_len + 1):
X.append(data[i : i + seq_len])
y.append(data[i + seq_len : i + seq_len + pred_len, target_idx])
return np.array(X), np.array(y)
4. 环境准备与依赖安装
4.1 推荐环境
- 操作系统:Windows 10/11、Ubuntu 20.04 及以上
- Python 版本:3.9 到 3.11
- 深度学习框架:PyTorch 2.x
- GPU:建议 NVIDIA 显卡,显存 6G 以上
- CPU 推理:支持,但批量预测时速度会有明显差距
- 磁盘空间:数据量不大时,预留 10G 以上足够
4.2 创建虚拟环境
conda create -n fin_transformer python=3.10
conda activate fin_transformer
4.3 安装依赖
pip install torch --index-url https://download.pytorch.org/whl/cu118
pip install pandas numpy scikit-learn matplotlib tqdm fastapi uvicorn
如果机器没有 NVIDIA GPU,可以安装 CPU 版 PyTorch:
pip install torch --index-url https://download.pytorch.org/whl/cpu
注意:实际安装命令需要根据本机 CUDA 版本和 PyTorch 官方索引调整,建议先访问官方页面确认最新的安装命令。
5. Transformer 金融预测模型设计与实现
5.1 模型结构
整个模型由以下几部分组成:
- 输入嵌入层:将每个时间步的特征向量映射到 d_model 维。
- 位置编码:为输入添加时间顺序信息。
- TransformerEncoder:堆叠多层编码器,每层包含多头自注意力机制和前馈网络。
- 输出层:将最终编码结果映射为预测目标。
代码实现如下:
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=512):
super().__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)
self.register_buffer("pe", pe.unsqueeze(0))
def forward(self, x):
return x + self.pe[:, : x.size(1)]
class TimeSeriesTransformer(nn.Module):
def __init__(self, input_dim, d_model=128, nhead=8, num_layers=4, pred_len=5, dropout=0.1):
super().__init__()
self.input_proj = nn.Linear(input_dim, 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.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
self.decoder = nn.Sequential(
nn.Linear(d_model, 64),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(64, pred_len),
)
def forward(self, x):
# x: [batch, seq_len, input_dim]
x = self.input_proj(x)
x = self.pos_encoder(x)
x = self.transformer_encoder(x)
# 取最后一个时间步的编码结果
out = self.decoder(x[:, -1, :])
return out
5.2 训练配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = TimeSeriesTransformer(
input_dim=feature_cols_num,
d_model=128,
nhead=8,
num_layers=4,
pred_len=5,
dropout=0.1,
).to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
训练时建议使用早停机制,防止过拟合。金融数据噪声大,模型很容易在训练集上拟合得很好但在测试集上失效。
from tqdm import tqdm
for epoch in range(100):
model.train()
train_loss = 0.0
for X_batch, y_batch in train_loader:
X_batch = X_batch.float().to(device)
y_batch = y_batch.float().to(device)
optimizer.zero_grad()
pred = model(X_batch)
loss = criterion(pred, y_batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
train_loss += loss.item() * len(X_batch)
val_loss = evaluate(model, val_loader, criterion, device)
scheduler.step()
print(f"Epoch {epoch + 1}: train_loss={train_loss / len(train_loader.dataset):.4f}, val_loss={val_loss:.4f}")
6. 模型训练与效果验证
6.1 训练集 / 验证集 / 测试集划分
金融时序数据不能随机打乱划分。必须按时间顺序切分,例如前 70% 训练,中间 15% 验证,最后 15% 测试。这是金融预测与图像分类任务最大的不同点。
train_size = int(len(sequences) * 0.7)
val_size = int(len(sequences) * 0.15)
train_sequences = sequences[:train_size]
val_sequences = sequences[train_size : train_size + val_size]
test_sequences = sequences[train_size + val_size:]
6.2 评价指标
常用的评价指标包括:
- MSE / RMSE:衡量预测值与真实值之间的平均误差,数值越小越好。
- MAE:平均绝对误差,对异常值更稳健。
- 方向准确率:预测涨跌方向与真实涨跌方向一致的比例,对交易策略更直观。
- IC / Rank IC:信息系数,衡量预测值与未来收益的相关性。
from sklearn.metrics import mean_squared_error, mean_absolute_error
def evaluate_direction(y_true, y_pred):
true_direction = np.diff(y_true, axis=0)
pred_direction = np.diff(y_pred, axis=0)
return np.mean(np.sign(true_direction) == np.sign(pred_direction))
6.3 Transformer vs LSTM 对比实验设计
要验证“Transformer 超越 LSTM”,必须保证两组实验使用完全相同的数据、特征和评估方式。推荐做法是把训练脚本抽成同一个框架,只替换模型类。
models = {
"transformer": TimeSeriesTransformer(...),
"lstm": LSTMModel(...),
}
results = {}
for name, model in models.items():
print(f"Training {name}...")
history = train_model(model, train_loader, val_loader, device)
test_pred = predict(model, test_loader, device)
results[name] = {
"mse": mean_squared_error(test_y, test_pred),
"mae": mean_absolute_error(test_y, test_pred),
"direction_acc": evaluate_direction(test_y, test_pred),
}
for name, metrics in results.items():
print(f"{name}: {metrics}")
实际实验时建议记录以下信息:
- 训练轮数和最终损失
- 验证集最优 epoch
- 测试集 RMSE / MAE / 方向准确率
- 单次训练耗时和显存占用
- 推理耗时
6.4 判断训练是否成功的标准
- 训练损失和验证损失都持续下降,没有出现明显 divergence。
- 验证集损失不再下降时触发早停,模型进入收敛状态。
- 测试集方向准确率显著高于 0.5,如果是回归任务则 RMSE 低于基线模型(如直接用历史均值预测)。
- 如果训练损失下降但验证损失大幅回升,说明过拟合,需要增加 dropout、减小模型容量或增加数据量。
7. 批量预测与 API 接口封装
训练完成后的模型可以用于两个主要场景:离线批量预测和在线接口服务。
7.1 保存模型
torch.save({
"model_state_dict": model.state_dict(),
"feature_cols": feature_cols,
"scaler": scaler,
"config": {
"input_dim": feature_cols_num,
"d_model": 128,
"nhead": 8,
"num_layers": 4,
"pred_len": 5,
},
}, "transformer_finance.pt")
7.2 批量预测脚本
批量预测适合处理多只股票、多个时间窗口的预测任务。输入目录下放多只股票的数据文件,输出目录保存各自的预测结果。
python batch_predict.py --input-dir ./data/stocks --output-dir ./output/predictions --model ./checkpoints/transformer_finance.pt
batch_predict.py 核心逻辑:
import os
import glob
import pandas as pd
model, scaler, config = load_model("checkpoints/transformer_finance.pt")
input_files = glob.glob(os.path.join(args.input_dir, "*.csv"))
for file in input_files:
df = pd.read_csv(file)
processed = preprocess(df, scaler, config["feature_cols"])
pred = model.predict(processed)
output_path = os.path.join(args.output_dir, os.path.basename(file).replace(".csv", "_pred.csv"))
pd.DataFrame(pred, columns=[f"pred_{i+1}" for i in range(config["pred_len"])]).to_csv(output_path, index=False)
批量任务需要注意:
- 每只股票单独标准化,不能混用其他股票的统计量,除非有明确依据。
- 输出文件建议带上生成时间戳,便于后续回测和复盘。
- 增加日志记录,逐文件打印成功或失败原因,避免中途卡住无法定位。
7.3 FastAPI 接口服务
接口调用是生产环境最常见的集成方式。使用 FastAPI 封装预测服务:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class PredictRequest(BaseModel):
features: list # 标准化后的特征序列
@app.post("/predict")
def predict(data: PredictRequest):
if len(data.features) < 60:
raise HTTPException(status_code=400, detail="Feature sequence too short")
X = torch.tensor([data.features], dtype=torch.float32).to(device)
with torch.no_grad():
pred = model(X)
return {"prediction": pred.cpu().tolist()}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
启动接口服务:
python api_server.py
调用接口:
import requests
payload = {
"features": [
[1.2, 0.5, 10.3, 20.1],
[1.3, 0.6, 10.5, 20.3],
]
}
response = requests.post("http://127.0.0.1:8000/predict", json=payload, timeout=30)
print(response.json())
接口服务上线时要加访问限制,最好只绑定内网地址,并通过网关鉴权。如果担心并发压力,可以把模型加载放到全局变量中,避免每个请求都重新加载模型。
8. 资源占用与性能观察
8.1 显存占用观察方法
训练时可通过以下方式观察显存占用:
nvidia-smi -l 2
如果使用 PyTorch,也可以在训练脚本中加入显存日志:
if torch.cuda.is_available():
print(f"CUDA memory allocated: {torch.cuda.memory_reserved() / 1024**3:.2f} GB")
显存占用取决于模型维度、层数、批次大小和序列长度。实际占用需以本机测试为准,无法给出固定数字。
8.2 影响资源消耗的关键因素
- 序列长度:序列越长,注意力矩阵越大,显存和计算量呈平方增长。
- 批次大小:批次越大,梯度估计越稳定,但显存占用同步上升。
- d_model 和 num_layers:模型容量越大,参数量越多,训练越慢。
- 预测长度:影响输出层和损失计算,但相对影响较小。
8.3 降低资源消耗的方法
- 减少序列长度,例如从 120 天降到 60 天。
- 减小 d_model,从 128 降到 64。
- 使用梯度累积模拟更大的批次。
- 推理时开启 torch.no_grad() 并切换到 eval 模式。
- CPU 推理时限制线程数,避免资源被全部占满。
torch.set_num_threads(4)
model.eval()
with torch.no_grad():
pred = model(X)
8.4 长时间训练时的稳定性
金融数据训练通常需要多次实验调参。建议:
- 固定随机种子,保证实验可复现。
- 定期保存 checkpoint 和最优模型。
- 记录每次实验的配置和指标,用 CSV 或 MLflow 保存。
- 如果训练崩溃,优先检查数据中是否存在 NaN 或 Inf。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练损失不下降 | 学习率过大或过小 | 打印每轮损失变化 | 调整学习率,改用 warmup 策略 |
| 验证集损失大幅上升 | 过拟合 | 对比训练集和验证集损失 | 增大 dropout、降低模型容量、早停 |
| 预测结果全部接近同一个值 | 数据标准化不当或模型容量不足 | 查看预测值分布 | 检查 scaler 拟合方式,增加模型层数 |
| 注意力矩阵显存溢出 | 序列过长或批次过大 | 查看报错信息和显存占用 | 减小批次或序列长度,开启梯度累积 |
| 方向准确率接近 0.5 | 数据噪声大或特征信息不足 | 检查训练集方向准确率 | 增加特征、改用分类任务、尝试多模型集成 |
| API 请求超时 | 推理耗时过长或并发过高 | 查看服务日志和耗时 | 使用异步接口、增加批处理、模型量化 |
| 数据中含有 NaN | 除零、缺失值未处理 | 检查数据摘要 | 填充缺失值或删除对应样本 |
| 不同股票预测效果差异大 | 各标的波动特征不同 | 分组评估指标 | 对不同标的分开训练或加入标的特征 |
10. 最佳实践与合规使用建议
10.1 工程化建议
- 第一次训练先跑小参数,确认数据管线和训练流程没问题再放大模型。
- 保留一套最小可运行配置,方便快速复现问题。
- 模型文件、训练日志、预测结果分目录管理,文件名带时间戳。
- 批量任务必须加日志和失败重试,单文件失败不应中断整个任务。
- 模型服务要限制访问范围,绑定内网地址并加鉴权。
10.2 金融预测与合规边界
金融预测模型的结果不能作为投资决策的唯一依据。以下几点需要特别强调:
- 本文所有模型和代码仅用于技术研究和教育目的,不构成任何投资建议。
- 如果使用真实股票数据,确保数据来源合法,并且符合数据使用协议。
- 模型预测存在很大的不确定性,特别是在市场极端行情下,预测结果可能完全失效。
- 如果模型用于实盘策略,必须经过充分的回测、样本外验证和模拟交易测试。
- 涉及他人数据或非公开数据时,必须确认获得了合法授权。
10.3 模型之外的关键环节
Transformer 在金融预测中能否超越 LSTM,取决于很多模型之外的因素:
- 训练数据的质量和长度。
- 特征工程的有效性。
- 预测目标的定义是回归还是分类。
- 评估指标是否真实反映策略价值。
- 是否做了样本外测试和稳健性检验。
模型结构只是整个预测系统中的一个环节。即便是最先进的 Transformer,面对强噪声、低信噪比的金融数据,也需要配合严格的数据处理、测试和风控流程才能落地上线。
11. 总结与下一步
Transformer 在金融时序预测中的价值,主要体现在长序列建模、多变量交叉特征捕捉和并行训练效率上。和 LSTM 相比,它在合适的条件下确实能取得更好的预测效果,但这种优势不是无条件的。
如果现在要开始实践,建议按以下顺序推进:
- 先准备一份干净的日线数据,做好缺失值处理和特征工程。
- 分别实现 Transformer 和 LSTM 基线模型,固定相同的训练配置。
- 在测试集上对比 RMSE、MAE 和方向准确率。
- 确认 Transformer 有优势后,再考虑调参、加入更多特征或拓展到多标的预测。
- 最后再考虑封装 API 服务和批量预测流程。
最容易踩的坑有两个:
- 没有按时间顺序划分数据集,导致数据泄漏,测试结果虚高。
- 忽略金融数据的强噪声特性,模型过拟合到训练集噪声上,实盘完全失效。
后续可以继续扩展的方向包括:
- 用 Transformer 做多步预测时添加误差修正机制。
- 在注意力机制中加入交易日期和宏观经济事件的先验信息。
- 对比 Transformer 与 XGBoost、LightGBM 等树模型在不同时间尺度上的表现。
- 将预测结果接入回测框架,评估真实策略收益和风险指标。
建议把整套训练、评估和部署流程沉淀成模板,后续换数据、换标的时可以快速复用。先用一种资产验证流程,再横向扩展到更多资产,这是最稳妥的推进方式。
更多推荐
所有评论(0)