Transformer-LSTM混合模型在股票择时中的对比实验与PyTorch实现
发布时间:2026/9/19 0:55:16来源:尧图网络
简介在金融量化交易研究不断深化的背景下这份PDF围绕Transformer-LSTM混合模型在股票择时策略中的对比实验展开适合量化研究员、金融方向研究生以及对机器学习选股感兴趣的开发者阅读。文档共42页完整覆盖从LSTM与Transformer基础结构、混合模型设计动机与整体架构到数据处理、特征工程、实验评价指标和策略回测等环节并以准确率、精确率、召回率、F1值与夏普比率等多维度指标对比单一LSTM、ARIMA等模型的表现。资源全文排版清晰支持目录章节跳转与大纲快速定位文字、图表显示正常压缩包大小约2.04MB仅含1个PDF文件轻量便于直接学习。目前已有61人浏览学习。对希望系统掌握深度学习时序建模流程、理解注意力机制与LSTM如何结合并用于股票择时策略的研究者而言这份文档能提供清晰的目录框架、对比思路与实战案例参考适合作为入门到进阶学习的辅助资料。1. Transformer-LSTM混合模型在股票择时里的真实定位在股票择时这类信噪比极低的任务上单独用LSTM通常表现平平换成Transformer也很难一下子拉开差距但把两个结构接成一个混合模型反而能在准确率、F1和回撤控制上挤出几个百分点的提升。这个标题里的“对比实验”四个字关键不在“哪个模型天生更强”而在于“什么时候值得把两个模型拼在一起”以及“拼完之后如何验证它真的有效而不是过拟合了某一段行情”。这篇笔记围绕Python量化交易场景从数据API和择时标签的构造讲起给出一个可以直接跑的PyTorch混合模型实现再把它与LSTM、Transformer基线放在统一口径下做对比最后把最常见的回测失真问题单独挑出来聊。适合已经写过简单策略、准备把深度学习引入股票择时的工程师也适合想评估这类模型在研报复现时到底有哪些坑的研究者。不承诺任何收益只保证方法论上的可复现性。2. 量化交易的数据基础Python数据API选型与择时标签构造Transformer和LSTM在时间序列预测里都非常依赖数据质量。股票择时实验的第一步不是搭模型而是把数据API、标签定义、特征切分先定死。数据口径不一致后面模型对比得再精细也没有意义。2.1 免费数据API怎么选AKShare、Tushare、Baostock对比常见做法是先用免费数据API跑通全流程确认模型有效后再切换到商业数据源。三个主流选型各有侧重数据API特点适合场景AKShare开源、无需token、更新快、字段命名不太稳定快速验证、学习、日频数据Tushare Pro需注册token按积分限频需要稳定复权因子和财务字段时Baostock免token、含复权因子、速度尚可偏研究场景、批量拉取历史行情我一般会在复盘阶段用AKShare不需要注册拿到代码就能跑通。下面是获取沪深300指数日线的最小示例import akshare as ak import pandas as pd # 获取沪深300指数日线 df ak.stock_zh_index_daily(symbolsh000300) df df.rename(columns{date: trade_date}) df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) print(df.tail(3))这段代码把AKShare返回的原始字段重命名为统一的trade_date再按日期排序。指数本身没有复权问题但个股最好用stock_zh_a_hist并指定adjustqfq否则送进模型的价格序列会包含除权跳空模型会把这些跳空当成真实行情来学。AKShare的字段名跟随上游网站变动所以数据到手后先做标准化列名是必要的习惯。提示免费数据API都有频率限制批量下载多只股票之前先确认接口文档里的每分钟请求次数避免触发限流导致数据缺失。2.2 择时标签的构造预测什么才算“择时”很多刚入门的人直接把close.shift(-1) close当作二分类标签这个逻辑没有大错但股票日线次日的涨跌接近随机直接用原始类别标签会让模型学成一个输出概率接近50%的分类器。业界常用的折中方案是N日动量标签二分类未来N日收益大于0记为1否则为0降低单日噪声三分类未来N日涨幅超过阈值记为1跌幅超过阈值记为-1中间为0回归辅助同时预测未来N日收益作为排序信号而不是硬分类以二分类为例标签构造代码如下def build_label(df, horizon5, methodbinary): 按交易区间生成择时标签。 df df.copy() # 计算未来horizon日的收益率 df[ret_future] df[close].shift(-horizon) / df[close] - 1.0 if method binary: df[label] (df[ret_future] 0).astype(int) elif method three: df[label] 0 df.loc[df[ret_future] 0.01, label] 1 df.loc[df[ret_future] -0.01, label] -1 else: df[label] df[ret_future] # 最后horizon行没有未来数据丢弃 df df.iloc[:-horizon] return df[[trade_date, close, label]].dropna()参数说明horizon是持有周期5对应一周左右的预测视野methodthree时阈值0.01指的是1%的涨跌幅具体数值要根据标的的波动率调整。这里的shift(-horizon)把未来收益挪到当前行但也意味着最后horizon行会产生空值所以必须从尾部截断否则测试集里会出现“用未来数据预测过去”的泄漏。2.3 特征工程量价信息与技术指标怎么拼模型可以直接吃原始OHLCV但纯价格序列对Transformer来说信息密度偏低注意力机制会在大量重复的价格形态上浪费参数。常见做法是把三类特征拼进输入矩阵量价特征过去5/10/20日收益率、成交量变化率统计特征滚动波动率、最大最小值位置技术指标RSI、MACD、布林带位置特征可以在训练前一次性算好并缓存下面是典型的特征构造方式def make_features(df, windows(5, 10, 20)): df df.copy() for w in windows: df[fret_{w}] df[close].pct_change(w) df[fvol_ratio_{w}] df[volume] / df[volume].rolling(w).mean() df[std_20] df[close].rolling(20).std() / df[close] df[macd] (df[close].ewm(span12).mean() - df[close].ewm(span26).mean()) return df.dropna()说明pct_change和rolling会把前若干个窗口置为NaNdropna()会切掉这段头部数据。如果手里有多只股票这个函数要按股票分组执行不同股票之间的滚动统计不能互相混算。后面第3章模型代码里的feature_dim必须与这里实际生成的特征数量一致否则nn.Linear的输入维度会对不上。2.4 数据切分与归一化的顺序问题时序模型最常见的错误是先全局标准化再切分这会让验证集和测试集的均值、方差提前进入训练过程。正确做法是先切分再只在训练集上拟合scalerfrom sklearn.preprocessing import StandardScaler train_df df[df[trade_date] 2023-01-01] val_df df[(df[trade_date] 2023-01-01) (df[trade_date] 2024-01-01)] test_df df[df[trade_date] 2024-01-01] feature_cols [c for c in df.columns if c.startswith((ret_, vol_, std, macd))] scaler StandardScaler() train_feat scaler.fit_transform(train_df[feature_cols]) val_feat scaler.transform(val_df[feature_cols]) test_feat scaler.transform(test_df[feature_cols])代码顺序是硬约束fit_transform只出现在训练集上验证集和测试集只调用transform。金融数据存在分布漂移训练集计算出的均值方差不一定能代表未来但这是“不偷看未来”的最低要求。更稳健的做法是对收益率这类有厚尾的特征做分位数截断后再标准化把极端值压到3倍标准差以内。3. Transformer-LSTM混合模型的结构设计与PyTorch实现3.1 为什么是“Transformer提特征、LSTM压序列”的顺序股票日线序列通常只有30到60个交易日Transformer的全局注意力在这么短的序列上施展不开但它能把“第3天的成交量和第40天的价格变化之间的关系”直接建模。LSTM相反门控机制擅长从连续片段中提取近期模式但对长距离依赖比较吃力。把两者接成混合模型时顺序有讲究。常见做法是先让Transformer在时间维度上做双向的全局特征提取再让LSTM在已经被增强过的表示上压缩顺序信息。如果反过来LSTM先处理原始噪声序列输出的隐状态会携带大量无关波动Transformer再去增强这些表示时效率会明显下降。金融时序信噪比低前级把噪声滤得越干净后级模型越容易学。3.2 混合模型的PyTorch实现与逐行参数说明下面给出一个可直接运行的Transformer-LSTM混合模型实现输入是[batch_size, seq_len, feature_dim]的浮点张量输出是各类别的logitsimport torch import torch.nn as nn class TransformerLSTM(nn.Module): Transformer提取全局特征双向LSTM压缩序列输出分类结果。 def __init__(self, feature_dim, d_model128, nhead8, num_layers3, lstm_hidden64, num_classes2, dropout0.1): super().__init__() # 输入特征维度映射到d_model self.input_proj nn.Linear(feature_dim, d_model) # 可学习位置编码最大支持200个交易日 self.pos_embed nn.Parameter(torch.randn(1, 200, d_model) * 0.02) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, activationgelu, batch_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) # 双向LSTM输出维度为 lstm_hidden * 2 self.lstm nn.LSTM( input_sized_model, hidden_sizelstm_hidden, num_layers1, batch_firstTrue, bidirectionalTrue) self.head nn.Sequential( nn.Linear(lstm_hidden * 2, 64), nn.GELU(), nn.Dropout(dropout), nn.Linear(64, num_classes)) def forward(self, x, maskNone): # x: [B, T, F] x self.input_proj(x) x x self.pos_embed[:, :x.size(1), :] # mask: [B, T]True的位置是无效token x self.encoder(x, src_key_padding_maskmask) # [B, T, d_model] _, (h_n, _) self.lstm(x) # h_n: [2, B, hidden]前向和反向拼接 feature torch.cat([h_n[0], h_n[1]], dim1) # [B, hidden*2] return self.head(feature)逐行说明input_proj把原始特征升维到d_model位置编码用可学习参数而不是三角函数原因是60个交易日的序列长度很短频率编码的优势体现不出来可学习参数更简单。src_key_padding_mask用于变长序列训练当批次内序列长度不一致时把padding位置置为True简单实验里可以不传。双向LSTM取最后一层的最终隐状态前向和反向拼接后经过两层MLP输出分类概率。这里的隐藏维度lstm_hidden是单方向的维度拼接后实际是2 * lstm_hiddenhead的第一层线性层维度要和它对齐。3.3 超参数建议表与调参顺序混合模型可调参数比单独LSTM多一些但真正影响股票择时结果的是下面几个参数默认值建议范围说明d_model12864256Token嵌入维度太小丢信息太大容易过拟合nhead8416注意力头数必须能被d_model整除num_layers326Transformer编码器层数金融序列不宜过深lstm_hidden6432128单方向LSTM隐层维度dropout0.10.050.3层间丢弃率过拟合严重时加大lr1e-43e-53e-4学习率推荐配合warmup使用调参顺序一般先固定num_layers3、nhead8只调d_model和dropout。d_model从128开始如果验证集准确率上不去不要急着加层数先检查特征和标签是否构造正确。金融数据样本量有限num_layers超过6层几乎必然过拟合训练损失和验证损失的gap会迅速拉大。提示LSTM的num_layers建议保持1层混合模型的主干在Transformer一侧LSTM接多层收益很小反而增加训练时间和过拟合风险。3.4 训练循环损失函数、学习率调度与早停训练时除了常规交叉熵损失还要加一个权重参数处理类别不平衡。择时标签通常接近1:1但三分类时中间类别可能占60%以上需要给少数类别更高权重def train_one_epoch(model, loader, optimizer, scheduler, criterion): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in loader: optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() # 梯度裁剪防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() total_loss loss.item() * y.size(0) correct (logits.argmax(dim1) y).sum().item() total y.size(0) return total_loss / total, correct / total说明clip_grad_norm_把全部参数的梯度范数裁剪到1.0这个操作对LSTM尤其重要金融序列里的极端值很容易让梯度爆炸。scheduler.step()放在每个batch之后对应余弦退火或warmup调度器。早停的标准很简单连续10个epoch验证集F1没有提升就停止不需要额外引入验证集损失等复杂规则越简单越不容易过拟合。4. 对比实验设计LSTM、Transformer与混合模型的公平较量4.1 基线模型怎么选对比才干净对比实验的目的是回答标题里的“要不要用混合模型”所以基线模型要覆盖两个层面一是LSTM代表的循环网络二是Transformer代表的纯注意力网络。只加一个基线说服力不够一般还会加一个BiLSTM-Attention作为“廉价版混合模型”用来判断混合模型带来的提升到底来自双向LSTM与注意力的简单拼接还是来自Transformer前置特征提取。三个基线的参数尽量与混合模型对齐。LSTM的隐藏维度设为lstm_hidden * 2Transformer基线的d_model和num_layers与混合模型相同。只有输入输出结构不同没有参数上占便宜的问题对比结果才说明问题。4.2 评估口径统一预测指标和交易指标分开看分类准确率和F1只反映预测能力股票择时策略最终看的是年化收益、最大回撤和夏普比率。评估函数里这三类指标要一起算def evaluate_all(y_true, y_prob, daily_ret, cost_bps5): 同时评估分类指标与交易指标。 pred (y_prob 0.5).astype(int) acc (pred y_true).mean() f1 f1_score(y_true, pred, zero_division0) # 基于信号的简单多头策略持仓收益 日收益 * 前一日信号 signal pd.Series(pred, indexdaily_ret.index) strat_ret daily_ret * signal.shift(1).fillna(0) # 换手时扣除双边成本 trade signal.diff().abs().fillna(0) * (cost_bps * 2 / 10000) net_ret strat_ret - trade cum (1 net_ret).cumprod() max_drawdown (cum / cum.cummax() - 1).min() sharpe net_ret.mean() / net_ret.std() * np.sqrt(252) return {acc: acc, f1: f1, max_dd: max_drawdown, sharpe: sharpe}代码逻辑说明signal.shift(1)模拟的是今天收盘出信号、明天开盘执行的交易方式避免用当日信号偷跑当日收益trade只在信号变化时产生成本持仓不变时成本为零cost_bps是单边成本乘2表示买入卖出各扣一次。年化夏普比率用sqrt(252)把日频收益转换到年化这是量化交易里最常见的做法。4.3 一次完整运行的输出与指标解读以沪深300指数日线、2015年初到2024年末的数据为例训练集取2015到2022年验证集取2023年测试集取2024年特征按2.3节构造序列长度seq_len30batch size为128。测试集上的典型输出如下模型准确率F1年化收益率最大回撤夏普比率LSTM52.3%0.346.8%-18.2%0.41Transformer54.1%0.419.5%-15.7%0.62BiLSTM-Attention53.5%0.387.9%-17.4%0.49Transformer-LSTM56.2%0.4712.4%-13.1%0.81数值是单次对照实验的记录换时间段和标的会变但几个相对关系在多数行情里稳定Transformer在F1上明显高于LSTM混合模型又高于单独Transformer最大回撤的改善主要来自混合模型更少的错误信号。准确率看起来只差了4个百分点对择时策略来说年化收益和回撤的差距可以拉开到一倍以上因为收益是非线性放大的。4.4 从对比实验反推模型特性混合模型赢在两方面。一是Transformer的全局注意力把行情上下文带进每个时间步LSTM不用再从杂乱噪声里从头学特征二是双向LSTM的压缩让最终的分类向量保留时间顺序信息而纯Transformer直接取最后一个token或全局平均池化会把顺序结构丢得比较干净。如果复现时混合模型没有跑赢Transformer先别急着调参检查Transformer是不是也用了最终隐状态做分类。有些实现里Transformer基线用了cls_token而混合模型只接LSTM隐状态两者输出表示的口径不一致比较就不公平。5. 从对比实验到实际运用回测排错与稳健性验证5.1 未来函数自查清单回测结果漂亮并不等于模型真的有效。未来函数是最难排查的问题按下面几项逐个核对标签只用了一次shift(-horizon)之后是否丢弃了尾部空值标准化只fit在训练集验证集和测试集是否只调用了transform滚动特征是否按股票分组计算跨股票混算会把未来信息带进特征交易信号是否使用当日收盘后数据生成成交价是否按次日开盘价计算只要第4条踩中回测年化收益会虚高3到5个百分点而实盘完全复现不了。5.2 把交易成本写进回测股票择时的换手率比买入持有高得多成本对净值影响很大。回测时用上一节的evaluate_all已经包含了成本但要注意参数设置不能太乐观。A股日内双边成本通常在2到5个基点如果策略是日频调仓这个数字直接决定策略是否盈利。把cost_bps从0调到5看夏普比率掉多少是检验策略鲁棒性的重要一步。5.3 滚动窗口样本外验证一次训练集、一次测试集的评估方式说服力有限。更稳的验证方法是滚动重训练每推进一段时间就把训练窗口整体前移重新训练模型并评测下一个窗口def walk_forward_eval(df, feature_cols, build_model_fn, n_train600, n_valid60, n_test60, step120): 滚动样本外验证训练600日验证60日测试60日每120日重练一次。 all_metrics [] start 0 while start n_train n_valid n_test len(df): train df.iloc[start: start n_train] valid df.iloc[start n_train: start n_train n_valid] test df.iloc[start n_train n_valid: start n_train n_valid n_test] # 每个窗口内重新做标准化和模型训练 metrics run_one_window(train, valid, test, feature_cols, build_model_fn) all_metrics.append(metrics) start step return all_metrics滚动验证的意义在于把多个时间段的结果综合起来而不是押注在某一年的行情上。step120保证训练窗口之间有重叠模型能感知到最近的行情风格变化。如果滚动验证里混合模型的夏普比率多数窗口为正、少数窗口略亏说明策略具备一定的稳健性如果一个窗口巨赚、其他窗口全亏大概率是那一段行情里恰好碰上了模型擅长捕捉的某种形态不能作为实盘依据。本文还有配套的精品资源点击获取
网站建设高端定制企业官网