ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Transformer-LSTM混合模型在股票择时中的应用与PyTorch实现

Transformer-LSTM混合模型在股票择时中的应用与PyTorch实现 简介系统阐述金融量化交易中Transformer-LSTM混合模型应用的专题文档面向量化研究员、金融算法工程师及深度学习择时建模进阶学习者。文档以股票择时策略为核心从LSTM与Transformer原理出发逐层讲解两类模型的序列建模优劣势给出混合模型架构、损失函数与优化器设计、超参数调优及注意力可视化等分析还覆盖数据处理与特征工程全流程含多源数据整合、缺失值与异常值处理、标准化归一化与特征构建。实验部分以ARIMA和单一LSTM为对比基线按准确率、精确率、召回率、F1值、夏普比率等指标多维评估并结合牛熊与震荡市分析模型表现。文档为1个42页PDF文件大小约2.04MB目录支持章节跳转与大纲定位。已有61人学习下载适合需系统掌握量化择时建模、模型对比与回测实践的读者。1. Transformer-LSTM混合模型在股票择时策略中的定位股票择时策略本质上是一个序列分类问题给定过去N个交易日的量价数据预测未来一个持有期内上涨或下跌。传统技术指标滞后纯LSTM在长序列上收敛慢Transformer虽然全局注意力强但缺少顺序敏感性。这份42页的对比实验文档围绕量化交易中Transformer-LSTM混合模型与ARIMA、LSTM等基准模型的对比展开完整覆盖了从特征工程、模型搭建到参数调优的复现路径。对正在写策略回测脚本、又想验证注意力机制是否真的能提升择时效果的工程师来说这份材料提供了一个可以直接落地的实验框架。2. LSTM门控机制与Transformer注意力机制在时序预测中的边界金融时间序列同时存在局部动量、全局趋势和噪声先理解LSTM与Transformer的建模差异才能明白混合模型为什么要按“Transformer在前、LSTM在后”的顺序串联。2.1 LSTM的门控机制如何保留长期依赖LSTM在1997年被提出时解决的核心问题是RNN的梯度消失。它通过遗忘门、输入门、输出门和细胞状态来控制信息流动遗忘门决定保留多少历史状态输入门决定当前候选状态写入多少输出门决定隐藏层暴露多少给下一步。这种结构让梯度可以沿着细胞状态跨时间步传播训练长序列时比普通RNN稳定得多但代价是按时间步串行计算在GPU上并行效率偏低。在PyTorch中LSTM的调用方式很直接需要留意的反而是取哪一个时间步的输出。代码如下import torch import torch.nn as nn class LSTMTrendHead(nn.Module): def __init__(self, input_size, hidden_size, num_layers, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) last_hidden out[:, -1, :] return self.fc(last_hidden).squeeze(-1)input_size是每个时间步的特征数hidden_size是隐藏单元数num_layers是堆叠层数。batch_firstTrue让输入张量形状为[batch, seq_len, input_size]这样取最后一个时间步时直接索引out[:, -1, :]。dropout只在层数大于1时生效单层LSTM传dropout不会报错但也不会起作用。LSTM做择时预测时我一般会把窗口长度控制在20到60个交易日。超过80步时即使有门控最后一步的隐藏状态也可能被近期信息淹没这时候就需要Transformer来补充全局依赖。2.2 Transformer的全局注意力与位置编码Transformer放弃了循环结构完全靠自注意力Self-Attention建立任意两个位置之间的直接联系。每个位置都可以看到序列里所有其他位置因此长距离依赖的建模不再依赖一条“传递链”。多头注意力再把输入投影到多组查询、键、值子空间分别计算注意力后再拼接等价于让模型从多个维度同时观察数据关系。注意力权重本身还可以做可视化用来解释模型在看哪些历史片段。金融序列是带顺序的Transformer本身没有顺序概念所以需要加位置编码。常见做法是把正弦基位置编码或可学习位置编码直接加到输入嵌入上。下面是多头注意力的一个最小实现方便拆解维度class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() self.d_model d_model self.num_heads num_heads self.head_dim d_model // num_heads self.qkv nn.Linear(d_model, 3 * d_model) self.out nn.Linear(d_model, d_model) def forward(self, x): B, T, _ x.size() qkv self.qkv(x).chunk(3, dim-1) q, k, v [z.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) for z in qkv] scores torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5) attn torch.softmax(scores, dim-1) out torch.matmul(attn, v) out out.transpose(1, 2).contiguous().view(B, T, self.d_model) return self.out(out)这里把输入一次性投影成Q、K、V再切成num_heads个头。head_dim必须是d_model / num_heads的整数通常取d_model64, num_heads8每个头维度8。scores缩放这一步很关键除以sqrt(head_dim)防止点积过大导致softmax饱和。2.3 两种模型在金融时间序列上的适用边界从上面的实现可以看出LSTM在时间步上串行读取Transformer在序列维度上并行计算两者的计算模式和归纳偏置差异非常明显。把两者放在一张表里看更直观这个对比也直接影响了混合模型的结构设计。对比维度LSTMTransformer顺序建模通过循环逐步隐含编码靠位置编码显式补充并行能力时间步串行GPU利用率低序列维度完全并行长依赖捕捉门控缓解但存在容量上限任意位置直接连接计算复杂度O(T * d^2)O(T^2 * d)对小样本的容忍度相对较高数据不足时容易过拟合从这张表能看出LSTM的短板是并行和超长依赖Transformer的短板是顺序敏感度和小样本下的过拟合风险。股票日线数据通常只有几千根K线特征维度有限直接套用大Transformer很容易过拟合。混合模型的设计思路就是先用Transformer在长窗口上提取全局模式再用LSTM按时间顺序压缩成最终特征既保留全局依赖又强化顺序信号。注意不要把Transformer当作万能特征提取器。金融序列的非平稳性决定了模型容量不能太大参数量一旦超过数据能支撑的范围训练集上的准确率再高样本外都会快速衰减。3. Transformer-LSTM混合模型设计与PyTorch实现混合模型不是简单地把两个模型首尾相接而是要让两种结构各司其职Transformer负责长窗口内的全局特征交互LSTM负责把交互后的序列压缩成带顺序信息的决策向量。3.1 为什么按“Transformer编码器 LSTM层”串联传统单一LSTM在处理超过60步的序列时最后一步隐藏状态会偏重近期信息较早的拐点信号容易衰减。而单独使用Transformer时位置编码只是加性信号对金融数据中“先放量后上涨”这类严格顺序因果逻辑的表达不够直接。把Transformer编码器放在前面可以让注意力机制先对全窗口的任意两日关系做交互再让LSTM按时间顺序读一遍交互后的序列既拿到全局视野又保留了循环网络的顺序归纳偏置。我在复现这份实验时采用的顺序是输入投影 - Transformer编码器堆栈 - LSTM - 线性输出。这个顺序比反过来LSTM - Transformer更容易收敛。反过来时LSTM的输出维度较小Transformer编码器输入信息量受限更重要的是LSTM输出的残差结构不如Transformer直接反向传播时梯度流动性较差。3.2 整体架构与维度变化以60个交易日的日线窗口、12个原始特征为例各层张量形状变化如下模块输入形状输出形状说明Linear输入投影[256, 60, 12][256, 60, 64]12维特征映射到64维TransformerEncoder[256, 60, 64][256, 60, 64]2层编码器8个头LSTM[256, 60, 64][256, 60, 32]取最后时间步Linear输出[256, 32][256]输出一个logit这里的批量大小是256序列长度60d_model为64LSTM隐藏单元32。整个模型的参数量约60万属于轻量级在单个GPU上训练一轮只需要几十秒。3.3 完整模型定义PyTorch的实现很紧凑核心是组合nn.TransformerEncoderLayer和nn.LSTMimport torch import torch.nn as nn class TransformerLSTM(nn.Module): def __init__(self, feature_size, d_model, nhead, num_encoder_layers, lstm_hidden, lstm_layers, dropout0.1): super().__init__() self.input_proj nn.Linear(feature_size, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, batch_firstTrue, dropoutdropout, activationgelu ) self.encoder nn.TransformerEncoder( encoder_layer, num_layersnum_encoder_layers ) self.lstm nn.LSTM( d_model, lstm_hidden, num_layerslstm_layers, batch_firstTrue, dropoutdropout if lstm_layers 1 else 0.0 ) self.head nn.Linear(lstm_hidden, 1) def forward(self, x): x self.input_proj(x) # [B, T, feature_size] - [B, T, d_model] x self.encoder(x) # [B, T, d_model] x, _ self.lstm(x) # [B, T, lstm_hidden] x x[:, -1, :] # 取最后一个时间步的隐藏状态 return self.head(x).squeeze(-1)代码逻辑输入先经过线性投影升维Transformer编码器在60个时间步之间做多头注意力交互LSTM按时间顺序读入编码后的序列输出层只取最后一个时间步。activationgelu在Transformer前馈层中比ReLU平滑对小批量训练更稳定。lstm_layers大于1时dropout会在层间生效只有1层时dropout被置0避免把输入到隐藏的连接也丢弃。3.4 训练流程与损失函数选择择时任务可以建模成二分类未来h日收益为正则1否则0。输出层不带sigmoid训练时用带Logits的BCE损失数值上更稳定。优化器选择AdamW配合余弦退火学习率调度能避免后期在局部振荡。criterion nn.BCEWithLogitsLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) for epoch in range(20): model.train() for xb, yb in train_loader: optimizer.zero_grad() logits model(xb) loss criterion(logits, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()weight_decay1e-5是L2正则对抗金融数据的噪声。clip_grad_norm_(max_norm1.0)把梯度范数限制在1.0以内防止LSTM在反向传播中出现梯度爆炸。T_max20与训练轮数一致让学习率在最后一轮降到接近0。batch size取256或128都行但不要超过序列样本数的十分之一否则验证集波动会很大。提示训练早期最好监控验证集loss而不是准确率。金融数据标签分布不平衡时准确率会虚高而loss能更快反映出模型是否在拟合噪声。4. 数据处理与特征工程择时标签怎么打才靠谱模型结构再合理也扛不住脏数据。金融量化交易里的特征工程直接决定混合模型的上限尤其是日线数据里的复权缺口、极端值和未来信息都会让对比实验得出完全错误的结论。数据清洗、特征构建、样本切分这三步直接影响后面所有模型的训练结果。4.1 数据清洗去极值、缺失值与复权日线数据最常见的坑是前复权价格在除权日出现跳空直接算收益率会得到假信号。通常做法是使用前复权或后复权价格优先选前复权因为它保持当前价格不变历史价格做调整。缺失值处理上简单的ffill即可但要注意如果连续缺失超过5天宁可删除这段样本也不要填充。去极值用MAD法比Z-score更稳健。Z-score对极端值敏感一个异常涨停会把正常数据压缩到很窄的范围。MAD中位数绝对偏差的鲁棒性更好def mad_winsorize(series, n5): median series.median() mad (series - median).abs().median() scale 1.4826 * mad upper median n * scale lower median - n * scale return series.clip(lower, upper)n5表示把偏离中位数5倍MAD的数据截断到边界。1.4826是正态假设下MAD与标准差的换算系数。在应用前先对每个特征单独做不要跨特征统一处理因为不同因子的量纲和分布差异很大。4.2 标准化与归一化的选择Min-Max归一化把数据压到[0,1]适合线性层和注意力打分Z-score标准化把数据变成零均值单位方差适合LSTM的门控输入。实际中我通常用Z-score因为金融特征里存在极端值Min-Max缩放后离群点仍然会主导注意力权重。标准化必须先在训练集上拟合再用同一套均值和方差去变换验证集和测试集。一个常见的错误是直接对整个数据集做标准化这会引入未来信息导致回测结果虚高。代码如下from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 只在训练集上fit测试集只能用transform train_X scaler.fit_transform(train_X.reshape(-1, n_features)).reshape(train_X.shape) test_X scaler.transform(test_X.reshape(-1, n_features)).reshape(test_X.shape)注意这里把二维的[样本数, 时间步*特征数]压平后做拟合再reshape回来。fit_transform只用于训练集测试集只能用transform。4.3 特征构建动量、波动率与量价因子基础量价数据包括开盘、最高、最低、收盘、成交量、成交额。直接喂给模型效果一般需要衍生出具有经济含义的特征。我在实验中常用下面这组特征名称计算方式说明收益率close.pct_change()日度收益动量close / close.shift(n) - 1n日累计动量波动率ret.rolling(n).std()历史波动RSI14日相对强弱指标超买超卖成交量变化率volume.pct_change()量能突变量价相关性close与volume的滚动相关系数量价配合构建时注意所有特征都要对齐到T日收盘不能使用T日之后的信息。滚动窗口的rolling默认右对齐天然避免未来函数。def build_features(df, window20): data df.copy() data[ret] data[close].pct_change() data[momentum] data[close] / data[close].shift(window) - 1 data[volatility] data[ret].rolling(window).std() data[volume_change] data[volume].pct_change() data[ret_vol_corr] data[ret].rolling(window).corr(data[volume].pct_change()) return data.dropna()ret_vol_corr衡量上涨与放量是否同步这个因子在择时里很有辨识度。特征总数控制在10到20个之间过多特征会让Transformer的注意力头数不够分配模型反而学不到稳定模式。4.4 滑窗样本生成与标签定义样本生成采用滑窗每个样本取过去60个交易日的特征作为输入预测未来5日收益的符号。标签不能只用T1的收益太短容易噪声过大T5到T10的收益更适合捕捉趋势。注意在拼接样本时窗口之间只滑动1天会让相邻样本高度重叠训练集和验证集之间必须按时间顺序切分不能随机打乱否则会造成数据泄漏。def make_dataset(data, seq_len60, horizon5): X, y [], [] feature_cols [c for c in data.columns if c ! close] for i in range(len(data) - seq_len - horizon): X.append(data[feature_cols].iloc[i:i seq_len].values) future_ret data[close].iloc[i seq_len horizon] / data[close].iloc[i seq_len] - 1 y.append(int(future_ret 0)) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)horizon5表示看未来5天的累计收益seq_len60是输入窗口。标签定义成二元分类后LSTM和Transformer都能直接训练。如果希望更精细也可以把标签扩成三分类但对比实验里二分类最直接。注意预测目标close.iloc[i seq_len horizon]的位置是从窗口结束位置再往后推horizon天。这样样本最后一个可用交易日是i seq_len未来收益从i seq_len 1开始累计与特征无重叠。5. 对比实验设计、评价指标与超参数调优实验设计的目标不是单纯证明Transformer-LSTM混合模型比别的模型分数高而是要了解不同市场环境下各模型的行为差异。只有实验流程干净结论才有迁移价值。5.1 对比模型选型与实验配置实验至少需要四组对照ARIMA作为线性基线单层LSTM作为序列基线单Transformer作为注意力基线以及Transformer-LSTM混合模型。ARIMA只能处理一维序列所以在ARIMA模型上一般使用单一收益序列做预测不参与特征矩阵训练。LSTM与Transformer使用相同的输入特征、相同的滑窗和标签保证对比公平。5.2 评价指标从准确率到夏普比率分类指标之外量化交易更要关注策略收益。下表把指标分为模型层和策略层指标计算方式关注点Accuracy预测正确样本 / 总样本整体正确率PrecisionTP / (TP FP)预测上涨的可靠性RecallTP / (TP FN)捕捉上涨的能力F1-score2 * P * R / (P R)精确率与召回率的调和Sharpe Ratio(策略收益均值 - 无风险利率) / 收益标准差 * sqrt(252)单位风险超额收益在择时场景下Precision和Recall的含义不同。Precision高意味着模型说“涨”的时候涨的概率大有助于减少错误买入Recall高意味着模型能抓住更多上涨行情但会伴随更多假信号。F1把两者合在一起适合作为模型选择的依据。Sharpe Ratio在回测阶段做最终决策。5.3 超参数搜索网格搜索与贝叶斯优化混合模型的超参数包括d_model、nhead、编码器层数、LSTM隐藏单元、学习率、序列长度和dropout。网格搜索会指数级增长通常先用随机搜索找大范围再在最优区域做精细搜索。更推荐Optuna的贝叶斯优化用TPE采样逐步聚焦到高潜力区域。下面是一个最小样例import optuna import torch def objective(trial): params { d_model: trial.suggest_categorical(d_model, [32, 64, 128]), nhead: trial.suggest_categorical(nhead, [4, 8]), num_encoder_layers: trial.suggest_int(num_encoder_layers, 1, 3), lstm_hidden: trial.suggest_int(lstm_hidden, 16, 64, step16), lr: trial.suggest_float(lr, 1e-4, 1e-2, logTrue), dropout: trial.suggest_float(dropout, 0.0, 0.4) } # 每次实验重新构建模型使用时间顺序前向验证 model build_model(params) val_f1 train_and_evaluate(model, params) return val_f1 study optuna.create_study(directionmaximize) study.optimize(objective, n_trials50)其中n_trials50对2层Transformer和单层LSTM的结构来说足够收敛。每个trial内部使用3折前向验证按时间顺序切分而不是K折交叉验证。时间序列的K折随机切分会打乱序列依赖导致过拟合被误判为泛化。5.4 实验结果分析框架分析实验结果时不能只比较总准确率。我一般会分三段看把测试期按收益分成牛市、熊市、震荡市分别统计每个模型的F1和Sharpe。混合模型通常在牛市和震荡市中表现更稳定因为注意力机制能捕捉到板块轮动和量价配合信号熊市中所有模型都会失效相对跌幅小的模型更值得保留。还要画出模型预测置信度的直方图如果大量样本的预测概率集中在0.5附近说明模型对噪声区间没有判断力这时候调高分类阈值能减少无效交易。参数调优的结论比具体数值更重要。我复现时发现影响最大的参数是d_model和dropout其次是编码器层数。金融序列不需要很高的d_model64维已经足够dropout在0.2到0.3之间最佳太高会让LSTM输出退化。6. 策略回测与实盘前必须处理的几个细节模型给出信号后还要经过回测和模拟盘验证才能进入实战。回测中容易被忽略的几个细节比如交易成本、滑点、信号抖动往往决定了策略在实盘里是否能盈利而这些恰恰是很多回测脚本不会暴露的问题。6.1 交易成本与滑点建模择时策略的调仓频率直接决定成本敏感性。按A股双边费率估算佣金、印花税和过户费合计约0.15%如果模型每次信号都全仓切换一个月交易10次光成本就会吃掉年化收益。回测时至少按双边0.2%建模def backtest_signals(price, signals, fee_rate0.002, slippage0.0002): equity 1.0 position 0 for i, sig in enumerate(signals): if sig ! position: # position1表示持仓sig1表示买入信号 trade_price price[i] * (1 slippage if sig 1 else 1 - slippage) equity * (1 - fee_rate) position sig if position 1: equity * price[i 1] / price[i] if i 1 len(price) else 1 return equity代码中slippage按信号方向影响成交价格买入时成交价上浮卖出时下浮。真实的滑点与流动性相关小盘股流动性差需要额外留出0.1%以上的缓冲否则回测中的盈利在实盘会被滑点吃光。6.2 滚动窗口回测避免未来函数参数调优阶段已经用了前向验证回测阶段仍然推荐walk-forward先用前3年数据训练再用接下来6个月数据测试然后窗口滚动前移而不是一次性在所有历史数据上训练。这样能观察模型参数在变化市场中的衰退速度。如果混合模型每滚动一次就大改超参数说明模型稳定性不足宁可牺牲训练集准确率也不要追逐过拟合的最佳点。6.3 信号平滑与仓位管理模型输出的预测概率直接作为信号时频繁翻转会产生大量无效交易。我一般会做两步处理一是对预测概率做3日移动平均降低抖动二是只在概率超过0.6时开仓低于0.4时平仓中间区间维持原仓位。仓位管理上单信号的最大仓位建议不超过总资金的30%剩余部分配置给低相关性资产避免择时模型的系统性误判在单只股票上造成过大回撤。回测稳定之后还要在模拟盘中至少运行一个完整趋势周期再考虑接入真实现金账户。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表