尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

Transformer-LSTM混合模型在股票择时中的实践与PyTorch实现 简介一份42页的PDF技术文档聚焦金融量化交易中Transformer-LSTM混合模型在股票择时策略中的对比实验。文档面向金融量化分析师、深度学习研究者和量化策略开发者系统覆盖量化交易与择时策略概述、Transformer与LSTM模型基础、混合模型架构设计、数据清洗与特征工程、实验设计与参数调优、不同市场环境下的模型表现、策略优化与实战应用。整个压缩包仅1个PDF文件大小约2.04MB支持目录章节跳转和阅读器大纲定位文字、图表均可正常显示。目前已有61人学习下载。文档结合准确率、精确率、召回率、F1值与夏普比率等评价指标对比了传统时序模型、LSTM和混合模型的优劣并给出网格搜索、随机搜索与贝叶斯优化等调参方法可帮助读者理解Transformer注意力机制在长序列依赖上的优势获得可复用的股票择时实验思路与回测分析框架。1. 行情序列既不是文本也不是语义Transformer-LSTM混合才适合做股票择时把 Transformer 直接搬到日频 K 线上十有八九打不过一个调参充分的 LSTM但把两者按合适的结构拼起来往往能在量化交易里的股票择时策略中获得更一致的超额表现。这不是“模型越先进越好”而是行情序列和自然语言的底层假设完全不同K 线序列短、信噪比低、分布漂移快注意力机制擅长抓相距较远的关联却天然缺少对时间顺序的归纳偏置LSTM 靠门控逐步记忆善于跟踪局部趋势却又难回溯相距很远的结构。Transformer-LSTM 混合模型的价值就是用 LSTM 维持短时依赖用 Transformer 捕捉需要更长视野的调整两套结构各管一段。这篇博文把混合结构的接法、数据切片方式、PyTorch 实现和对比实验设计完整走一遍适合想在自己数据集上落地的策略工程师和算法研究员。2. 混合模型的结构设计LSTM保留短时依赖Transformer补充长程调整2.1 从LSTM原理到Transformer注意力择时任务真正缺的是什么LSTM原理并不复杂输入门、遗忘门、输出门控制信息在记忆单元中的写入、保留和读取因此它对“过去几天发生了什么”有天然的序列偏好。这个属性非常贴合日频择时里的动量效应和反转效应——连续上涨后的回调、放量后的缩量都是短时间窗口内可以观察到的状态转移。Transformer把序列建模成两两之间的注意力权重理论上能看到几百步以外的结构但代价是放弃了对位置关系的先验表达。虽然有位置编码但金融时序中位置编码更多是“第几天”而不是语言里那种句法位置。更重要的是股票日频数据的信噪比极低真实信号可能只占序列方差的一小部分注意力层在这种数据上容易把噪声当成重要关联去放大。所以择时任务真正缺的不是更强的拟合能力而是“什么时候该沿时间方向累积什么时候该跨时间比较”。LSTM沿时间累积Transformer跨时间比较混合结构恰恰把这两种归纳偏置放在同一模型里让数据决定每一时刻更依赖哪一边。2.2 三种混合接法串行、并行、门控加权常见做法是三种接法先理解结构再决定用哪种。混合方式特征流向优点风险串行ALSTM→Transformer原始序列先进LSTM输出隐状态序列再进TransformerEncoderTransformer能看到LSTM压缩后的上下文训练相对平稳LSTM的遗忘门可能提前丢弃Transformer需要的远程信息串行BTransformer→LSTM先做全局注意力增强再按时间顺序过LSTM远程结构先参与表达LSTM再做时序决策注意力层直接作用于原始噪声特征容易过拟合并行融合LSTM和Transformer分别处理原始序列输出拼接后过预测头两分支互不干扰梯度独立需要额外设计融合层参数翻倍并行门控两分支输出经学习的门向量加权求和模型能自己控制每个时刻的信息偏好门控可能塌缩到某一分支需要观察门值的分布我一般先跑并行拼接因为它实现最简单、归因清晰适合作为对比实验里的“混合模型”基准。如果拼接后AUC明显高于单结构再试门控加权如果门控学习到的权重长期偏向某一侧说明另一条分支对当前数据集基本无用。# 串行A的构造顺序先用伪代码说明结构关系 # x: (B, T, F) - lstm_out: (B, T, H) - attn_out: (B, T, H) lstm_out, _ lstm(x) attn_out transformer(lstm_out) feat attn_out[:, -1, :] # 只取最后时刻做当前决策 logits head(feat)这段代码的关键点是attn_out[:, -1, :]择时策略输出的是当前这一天的多空结论所以只取序列末尾的状态。如果改成对整条序列做池化等于把过去N天的信息平均进当前决策会稀释掉“最新状态”的影响力。2.3 输入特征序列构造从OHLCV到归一化滑窗Transformer-LSTM混合模型输入的不是单日截面数据而是形状为(B, T, F)的时间序列批。T是回溯窗口长度F是每日特征数。特征不能只放原始OHLCV因为价格绝对水平会被归一化过程抹掉模型真正能学的是形态。我常用的特征分成三组原始价格变换对数收益率、最高最低价振幅、成交量对数相对强弱类收盘价相对MA5/MA20的偏离度、RSI、ATR真实波幅均值截面滚动类过去5/10/20日收益的滚动均值与滚动标准差import pandas as pd import numpy as np def build_features(df: pd.DataFrame) - pd.DataFrame: out pd.DataFrame(indexdf.index) close df[close] out[ret_1] close.pct_change(1) out[ret_5] close.pct_change(5) out[ret_20] close.pct_change(20) out[ma_bias_5] close / close.rolling(5).mean() - 1.0 out[ma_bias_20] close / close.rolling(20).mean() - 1.0 out[vol_20] out[ret_1].rolling(20).std() out[amplitude] (df[high] - df[low]) / close out[volume_log] np.log1p(df[volume]) return out.replace([np.inf, -np.inf], np.nan).dropna()这里把收益率、乖离率和滚动波动率都做成了时间序列而不是截面数值目的是让模型同时看到“当前值”和“历史走势”。replace([np.inf, -np.inf], np.nan).dropna()是必须的除数为零或价格未变时会产生无穷值直接喂给LSTM会让梯度爆炸。3. 数据API选型与样本构造把行情切成“T-N到T、T5”的监督样本3.1 行情数据API怎么选覆盖度、频率与接入成本量化交易使用的数据API大体分两类本地下载型直接落库网络请求型按需拉取。选型标准不是某个接口性能多好而是数据连续性、复权方式和字段完整度。数据接口接入形式覆盖范围适合场景akshare开源、直接调函数A股/期货/宏观数据接口多快速验证策略、研究原型baostock开源、返回DataFrameA股日/周/月K线行情字段全长时间序列回测、免注册tushare pro需要token积分制A股高频数据、财务因子需要高质量财务和每日指标时jqdatasdk付费量化平台SDK国内股票/基金/指数平台内研究行情质量有服务保障注意区分前复权和后复权。做择时策略必须用后复权价计算收益率否则除权除息造成的跳空会被模型误认为真实涨跌。用前复权数据做长回测历史价格会被不断修正标签的真实性打折。网络请求型接口要考虑调用频率限制批量下载历史数据时我一般分段请求中间加time.sleep(0.5)。日频策略不追求毫秒级响应把数据落成parquet文件后离线读取避免训练过程中反复请求接口。3.2 滑窗样本构造与滚动归一化代码模型学习的是“过去N天特征序列 → 未来H天方向”。窗口长度T取60预测周期horizon取5是日频择时里比较常见的起点。T太短捕捉不到月级别节奏T太长样本量急剧下降且早期行情对当前策略的参考意义有限。def make_samples(df: pd.DataFrame, feat_cols: list, seq_len: int 60, horizon: int 5): xs, ys [], [] data build_features(df) close df[close].values feats data[feat_cols].values for i in range(seq_len, len(data) - horizon): x feats[i - seq_len:i] # (seq_len, F) y 1.0 if close[i horizon] close[i] else 0.0 xs.append(x) ys.append(y) return np.array(xs, dtypenp.float32), np.array(ys, dtypenp.float32)标签y是“T5收盘价高于T日收盘价”没有加任何缓冲。真实行情里涨跌幅小于0.1%的情况很多这类样本本质上是噪声所以后面需要专门的样本筛选策略。归一化必须用滚动统计量且统计量只能来自当前样本及其之前的训练数据不能把验证集和测试集的均值方差算进训练集。特征列数固定后我按训练段逐列计算均值和标准差验证集和测试集复用这组参数。mu train_x.reshape(-1, train_x.shape[-1]).mean(axis0) std train_x.reshape(-1, train_x.shape[-1]).std(axis0) train_x (train_x - mu) / (std 1e-8) val_x (val_x - mu) / (std 1e-8) # 只用训练段的mu/stdstd 1e-8防止某些技术指标在横盘期标准差接近0时产生除零。如果某个特征的标准差为0说明它在这一段行情里没有区分度保留在模型里只会增加参数负担可以直接在特征选择阶段剔除。3.3 标签设计未来N日收益符号和类别不平衡close[ihorizon] close[i]这种二元标签在日频数据上通常产生55:45到52:48的不平衡。看似不严重但A股的上涨日不一定能转化为策略收益因为交易成本、滑点和涨跌幅分布会影响最终结果。更好的做法是设置中性带未来5日收益大于某个阈值设为1小于负阈值设为0落在中间的直接丢弃。future_ret close[i horizon] / close[i] - 1.0 if future_ret 0.015: y 1.0 elif future_ret -0.015: y 0.0 else: continue1.5%的阈值对应日频5日的平均波动水平样本量大约减少20%-30%但标签的确定性大幅提升。阈值设太大会把样本量压到几千条混合模型在这种数据量下非常容易过拟合所以阈值要结合标的的年化波动率来回调整。4. 用PyTorch实现混合模型最小可跑通的代码与训练参数4.1 混合模型结构实现LSTM输出接TransformerEncoder核心结构采用串行ALSTM先按时间顺序压缩行情Transformer再在LSTM产生的隐状态序列上做全局注意力。选择串行A不是因为它一定最优而是它训练噪声最小——Transformer不直接接触原始行情特征而是在LSTM已经规整过的语义上工作不容易被单日异常值带偏。import torch import torch.nn as nn class TransformerLSTM(nn.Module): def __init__(self, input_dim: int, hidden_dim: int 64, n_layers: int 2, nhead: int 4, dropout: float 0.2): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, num_layersn_layers, batch_firstTrue, dropoutdropout) encoder_layer nn.TransformerEncoderLayer( d_modelhidden_dim, nheadnhead, dim_feedforwardhidden_dim * 2, dropoutdropout, batch_firstTrue, norm_firstTrue) self.transformer nn.TransformerEncoder( encoder_layer, num_layers2) self.head nn.Sequential( nn.Linear(hidden_dim, 32), nn.GELU(), nn.Dropout(dropout), nn.Linear(32, 1)) def forward(self, x): lstm_out, _ self.lstm(x) # (B, T, hidden_dim) attn_out self.transformer(lstm_out) feat attn_out[:, -1, :] # 当前决策时刻 return self.head(feat).squeeze(-1)batch_firstTrue让输入形状符合(batch, seq_len, features)和 NumPy 数组的滑动窗口一一对应省去转置操作。norm_firstTrue代表先做 LayerNorm 再进注意力层Transformer 训练会更平稳收敛也更快。dim_feedforwardhidden_dim * 2比默认的hidden_dim * 4小因为金融序列的容量需求远小于文本扩大FFN只会让模型记住更多噪声。4.2 训练循环、AdamW参数、早停与梯度裁剪训练时最容易被忽略的是梯度裁剪。LSTM经过多步时间展开梯度范数可能突然变为正常值的几十倍这通常是某个交易日出现极端涨跌导致的。clip_grad_norm_只在梯度范数超过阈值时才起作用不影响正常更新。import torch.optim as optim from sklearn.metrics import roc_auc_score model TransformerLSTM(input_dimtrain_x.shape[-1]) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) pos_weight torch.tensor([(train_y 0).sum() / (train_y 1).sum()]) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) best_auc, wait, patience 0.0, 0, 10 for epoch in range(200): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() model.eval() with torch.no_grad(): proba torch.sigmoid(model(val_x)).numpy() auc roc_auc_score(val_y, proba) if auc best_auc: best_auc, wait auc, 0 torch.save(model.state_dict(), best.pt) else: wait 1 if wait patience: breakAdamW 的weight_decay1e-5不要省它约束权重的 L2 范数在样本量不大的择时任务里是主要的抗过拟合手段。BCEWithLogitsLoss需要配合模型最后不加 sigmoid 的输出数值上比先sigmoid再BCELoss更稳。pos_weight按负样本/正样本比例计算涨跌样本不平衡越严重这个权重的影响越明显。4.3 基线模型怎么设纯LSTM、纯Transformer与LightGBM对比实验的价值在于回答“混合模型到底带来了什么”。基线要分两层序列模型层和处理时间序列的经典机器学习层。基线模型配置方式回答的问题纯LSTM去掉Transformer分支feat lstm_out[:, -1]短时依赖本身够不够用纯TransformerLSTM替换为线性投影直接对原始seq做TransformerEncoder注意力机制独自能否处理行情噪声LightGBM把(T, F)拉平成T*F向量用原始时间顺序感知较弱的树模型序列建模相对普通特征工程的增量LightGBM 是极其重要的基线如果 LightGBM 拉平特征就能达到和混合模型相近的AUC说明整个序列建模是在过度拟合只有当混合模型的验证集AUC显著高于LightGBM时才说明时间结构和远程注意力的引入是值得的。纯Transformer基线的学习率要降到5e-5因为它直接接触高频噪声特征更容易收敛到过拟合区域。5. 对比实验别只报AUC择时信号验证的指标与滚动回测技巧5.1 用RankIC和分层收益替代Acc/AUC分类准确率在涨跌52:48的数据集上毫无意义AUC也只能说明排序能力。择时策略真正关心的是信号和未来收益之间的相关性预测概率高时未来收益是否真的更高。RankIC就是预测值与未来收益的Spearman秩相关按日计算后取均值和标准差。如果模型输出的概率均值在0.5附近挤成一团AUC可能依旧好看但按0.5阈值交易会产生大量无效信号。我一般把预测概率按每日分位切五层统计每层未来5日平均收益。最上层平均收益显著高于最下层才说明预测值有单调的择时信息。5.2 滚动训练回测与阈值过滤的多空信号时间序列不能随机打乱做K折交叉验证行情分布随时间漂移模型在2018年学到的模式可能完全不适配2024年。常见做法是滚动训练每20个交易日向前滚动一次用过去约5年的数据训练预测接下来20天然后合并所有预测片段。proba_seq, y_seq [], [] for start in range(0, len(data) - train_len - seq_len, step): end start train_len train_data data[start:end] test_data data[end:end step] X_tr, y_tr make_samples(train_data) X_te, y_te make_samples(test_data) model fit(X_tr, y_tr) proba_seq.append(predict(model, X_te)) y_seq.append(y_te)信号过滤用分位数比固定阈值更稳健。把全体预测概率的历史分位数作为动态阈值超过75%分位数且低于25%分位数的才进入多空信号集回避了0.5这种没有业务含义的阈值导致的频繁换仓。同时过滤掉预测概率连续多日不变的情况这种信号通常是模型对横盘走势的“不知道”强行交易只会贡献手续费。5.3 稳健性检查清单最后对照检查这四项换一个回溯起点前三年和后三年的RankIC是否方向一致对特征加少量高斯噪声预测排序是否剧烈变化改变horizon从5天到10天结论是否依赖特定周期多头信号样本至少保持一定数量避免统计显著性不足。混合模型若在这四项检查中表现一致才值得进入实盘模拟。本文还有配套的精品资源点击获取
返回列表