news 2026/9/19 0:54:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Transformer-LSTM混合模型在股票择时中的对比实验与PyTorch实现

简介:在金融量化交易研究不断深化的背景下,这份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(symbol="sh000300") df = df.rename(columns={"date": "trade_date"}) df["trade_date"] = pd.to_datetime(df["trade_date"]) df = df.sort_values("trade_date").reset_index(drop=True) print(df.tail(3))

这段代码把AKShare返回的原始字段重命名为统一的trade_date,再按日期排序。指数本身没有复权问题,但个股最好用stock_zh_a_hist并指定adjust="qfq",否则送进模型的价格序列会包含除权跳空,模型会把这些跳空当成真实行情来学。AKShare的字段名跟随上游网站变动,所以数据到手后先做标准化列名是必要的习惯。

提示:免费数据API都有频率限制,批量下载多只股票之前先确认接口文档里的每分钟请求次数,避免触发限流导致数据缺失。

2.2 择时标签的构造:预测什么才算“择时”

很多刚入门的人直接把close.shift(-1) > close当作二分类标签,这个逻辑没有大错,但股票日线次日的涨跌接近随机,直接用原始类别标签会让模型学成一个输出概率接近50%的分类器。业界常用的折中方案是N日动量标签:

  • 二分类:未来N日收益大于0记为1,否则为0,降低单日噪声
  • 三分类:未来N日涨幅超过阈值记为1,跌幅超过阈值记为-1,中间为0
  • 回归辅助:同时预测未来N日收益,作为排序信号而不是硬分类

以二分类为例,标签构造代码如下:

def build_label(df, horizon=5, method="binary"): """按交易区间生成择时标签。""" 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对应一周左右的预测视野;method="three"时,阈值0.01指的是1%的涨跌幅,具体数值要根据标的的波动率调整。这里的shift(-horizon)把未来收益挪到当前行,但也意味着最后horizon行会产生空值,所以必须从尾部截断,否则测试集里会出现“用未来数据预测过去”的泄漏。

2.3 特征工程:量价信息与技术指标怎么拼

模型可以直接吃原始OHLCV,但纯价格序列对Transformer来说信息密度偏低,注意力机制会在大量重复的价格形态上浪费参数。常见做法是把三类特征拼进输入矩阵:

  1. 量价特征:过去5/10/20日收益率、成交量变化率
  2. 统计特征:滚动波动率、最大最小值位置
  3. 技术指标:RSI、MACD、布林带位置

特征可以在训练前一次性算好并缓存,下面是典型的特征构造方式:

def make_features(df, windows=(5, 10, 20)): df = df.copy() for w in windows: df[f"ret_{w}"] = df["close"].pct_change(w) df[f"vol_ratio_{w}"] = df["volume"] / df["volume"].rolling(w).mean() df["std_20"] = df["close"].rolling(20).std() / df["close"] df["macd"] = (df["close"].ewm(span=12).mean() - df["close"].ewm(span=26).mean()) return df.dropna()

说明:pct_changerolling会把前若干个窗口置为NaN,dropna()会切掉这段头部数据。如果手里有多只股票,这个函数要按股票分组执行,不同股票之间的滚动统计不能互相混算。后面第3章模型代码里的feature_dim必须与这里实际生成的特征数量一致,否则nn.Linear的输入维度会对不上。

2.4 数据切分与归一化的顺序问题

时序模型最常见的错误是先全局标准化再切分,这会让验证集和测试集的均值、方差提前进入训练过程。正确做法是先切分,再只在训练集上拟合scaler:

from 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]的浮点张量,输出是各类别的logits:

import torch import torch.nn as nn class TransformerLSTM(nn.Module): """Transformer提取全局特征,双向LSTM压缩序列,输出分类结果。""" def __init__(self, feature_dim, d_model=128, nhead=8, num_layers=3, lstm_hidden=64, num_classes=2, dropout=0.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_model=d_model, nhead=nhead, dim_feedforward=d_model * 4, dropout=dropout, activation="gelu", batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) # 双向LSTM,输出维度为 lstm_hidden * 2 self.lstm = nn.LSTM( input_size=d_model, hidden_size=lstm_hidden, num_layers=1, batch_first=True, bidirectional=True) self.head = nn.Sequential( nn.Linear(lstm_hidden * 2, 64), nn.GELU(), nn.Dropout(dropout), nn.Linear(64, num_classes)) def forward(self, x, mask=None): # 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_mask=mask) # [B, T, d_model] _, (h_n, _) = self.lstm(x) # h_n: [2, B, hidden],前向和反向拼接 feature = torch.cat([h_n[0], h_n[1]], dim=1) # [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_model12864~256Token嵌入维度,太小丢信息,太大容易过拟合
nhead84~16注意力头数,必须能被d_model整除
num_layers32~6Transformer编码器层数,金融序列不宜过深
lstm_hidden6432~128单方向LSTM隐层维度
dropout0.10.05~0.3层间丢弃率,过拟合严重时加大
lr1e-43e-5~3e-4学习率,推荐配合warmup使用

调参顺序一般先固定num_layers=3nhead=8,只调d_modeldropoutd_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(dim=1) == 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 * 2,Transformer基线的d_modelnum_layers与混合模型相同。只有输入输出结构不同,没有参数上占便宜的问题,对比结果才说明问题。

4.2 评估口径统一:预测指标和交易指标分开看

分类准确率和F1只反映预测能力,股票择时策略最终看的是年化收益、最大回撤和夏普比率。评估函数里这三类指标要一起算:

def evaluate_all(y_true, y_prob, daily_ret, cost_bps=5): """同时评估分类指标与交易指标。""" pred = (y_prob > 0.5).astype(int) acc = (pred == y_true).mean() f1 = f1_score(y_true, pred, zero_division=0) # 基于信号的简单多头策略:持仓收益 = 日收益 * 前一日信号 signal = pd.Series(pred, index=daily_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_len=30,batch size为128。测试集上的典型输出如下:

模型准确率F1年化收益率最大回撤夏普比率
LSTM52.3%0.346.8%-18.2%0.41
Transformer54.1%0.419.5%-15.7%0.62
BiLSTM-Attention53.5%0.387.9%-17.4%0.49
Transformer-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 未来函数自查清单

回测结果漂亮并不等于模型真的有效。未来函数是最难排查的问题,按下面几项逐个核对:

  1. 标签只用了一次shift(-horizon),之后是否丢弃了尾部空值
  2. 标准化只fit在训练集,验证集和测试集是否只调用了transform
  3. 滚动特征是否按股票分组计算,跨股票混算会把未来信息带进特征
  4. 交易信号是否使用当日收盘后数据生成,成交价是否按次日开盘价计算

只要第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_train=600, n_valid=60, n_test=60, step=120): """滚动样本外验证:训练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

滚动验证的意义在于把多个时间段的结果综合起来,而不是押注在某一年的行情上。step=120保证训练窗口之间有重叠,模型能感知到最近的行情风格变化。如果滚动验证里混合模型的夏普比率多数窗口为正、少数窗口略亏,说明策略具备一定的稳健性;如果一个窗口巨赚、其他窗口全亏,大概率是那一段行情里恰好碰上了模型擅长捕捉的某种形态,不能作为实盘依据。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 0:52:00

2026年北京断桥铝开平窗源头厂家综合实力推荐:恒熠门窗行业全景分析

断桥铝门窗作为北方家装与工程场景的核心建材&#xff0c;其核心性能直接关系到居住舒适度与能耗控制。在华北地区&#xff0c;夏季高温多雨、冬季严寒多风&#xff0c;普通塑钢窗或劣质铝合金门窗往往难以兼顾隔音、保温与耐用性&#xff0c;而断桥铝门窗通过隔热条切断金属导…

作者头像 李华
网站建设 2026/9/19 0:51:05

手写K-means与FCM聚类算法:从原理到工程实现

简介&#xff1a;本资源是面向高校人工智能课程学习者与初学者的聚类算法实验教学材料&#xff0c;聚焦无监督学习核心内容&#xff0c;助力理解K-means与FCM两种经典算法的原理差异、实现逻辑与实际应用。文档完整呈现广州大学《人工智能导论》实验四的规范报告结构&#xff0…

作者头像 李华
网站建设 2026/9/19 0:50:49

2026年锂电行业破拱装置行业发展现状与市场占有率及排名研究分析报告

锂电行业作为国内新能源赛道的核心支撑产业&#xff0c;近年来产能规模持续扩张&#xff0c;上游原材料加工、中游正极前驱体制备、下游成品存储转运环节&#xff0c;对料仓存储的稳定性要求不断提升。对于锂电粉体企业来说&#xff0c;超细粉体团聚架拱、高含水率物料粘壁堵仓…

作者头像 李华
网站建设 2026/9/19 0:49:10

Prompt 版本管理散乱,TaoToken 在 LLM Space 里收口

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 0:49:09

齿轮-轴-轴承系统含间隙非线性动力学建模与Matlab仿真实践

搞机械传动的同行应该都有体会&#xff1a;齿轮-轴-轴承系统这东西&#xff0c;理论上看着是标准转子动力学&#xff0c;一放到实际工况里就全是"意外"。齿侧间隙、轴承游隙、制造误差、安装偏心、动载荷突变……任何一个环节都会让系统从教科书里那个光滑的线性模型…

作者头像 李华
网站建设 2026/9/19 0:47:03

STM32驱动WS2812B:PWM+DMA方案详解与避坑指南

1. 为什么WS2812B值得用DMA来驱动如果你玩过WS2812B&#xff0c;大概率经历过这样的场景&#xff1a;用GPIO翻转模拟时序&#xff0c;主循环里塞一个for循环逐位输出&#xff0c;灯带一长&#xff0c;CPU就被彻底绑死&#xff0c;稍微来个串口中断&#xff0c;灯珠就开始随机闪…

作者头像 李华