资讯详情

GARCH-LSTM混合模型:提升时间序列波动期预测稳定性的实践

📅 2026/10/22 5:38:42 | 华诺云谱 👁 阅读
GARCH-LSTM混合模型:提升时间序列波动期预测稳定性的实践
做时间序列预测的同行尤其是拿金融收益率这类数据练过手的人大概率都有过这种经历你花了很大功夫把LSTM调好样本外误差勉强能看可一到波动放大的那几天预测值就像盲猜。我自己在几个项目里试过单跑GARCH也试过直接上LSTM最后是把两者做成一个GARCH-LSTM混合模型才把“高波动期预测不稳定”这个问题理顺。这篇文章把完整的建模逻辑和Python实现拆开讲包括GARCH(1,1)怎么提供波动率特征、LSTM怎么接手序列学习、两者怎么组合以及我在实验里踩过的几个隐蔽坑。适合做金融时序、能源负荷、气象风速这类带波动聚集现象的预测任务也想提升波动期预测稳定性的读者参考。1. 为什么非要把GARCH和LSTM凑在一起1.1 金融序列真正的难处不在均值在波动大多数刚接触时间序列预测的人第一反应是把序列本身当成一个普通的回归问题。但收益率这种数据有个很典型的特征波动聚集。平静一段时间后突然连续大波动大波动之后往往还跟着大波动。这种现象在统计学里叫条件异方差意思是每个时间点上的噪声方差并不是常数而是随时间变化的。传统模型在处理这种数据时通常很吃力。拿AR(1)这类线性模型举例它拟合的是均值结构也就是“今天的收益受昨天多大影响”。它对均值之外的东西完全没概念因此预测区间在两个时间段上宽度是一样的。如果一个序列的波动率明显分阶段那么预测方差被低估或高估都在所难免。LSTM虽然能捕捉非线性均值模式但如果不告诉它当前处于高波动还是低波动状态它也会用同一套权重去处理不同波动水平的样本训练容易被少数高波动样本主导。GARCH模型恰好擅长另一个方向。GARCH(1,1)专门对条件方差建模能刻画波动率的自回归特征。它就像给序列装了一个“波动率传感器”每个时间点上估计出当前的不确定性。GARCH的局限也很明显它的均值方程通常很简陋一般就是常数或者一个简单AR项无法捕捉复杂非线性均值结构。把GARCH和LSTM拼起来的逻辑其实很朴素——一个负责感知波动一个负责学习均值和非线性依赖。1.2 混合之后到底改进了什么我最早试过的混合方式非常简单直接用GARCH估计出的条件波动率作为LSTM的额外输入特征。思路是让LSTM在预测时知道“当前这台机器的噪声放大器开到了几档”。实验之后发现改进并不体现在整体误差的大幅下降而是体现在高波动区间的误差稳定性。单跑LSTM时高波动样本往往被模型当成异常点预测容易跟随最近几天的大波动剧烈抖动方向也经常打反。加入GARCH特征后模型对“当前是普通噪声还是真实跳水”有一个额外判断信号不会一见到大波动就惯性追涨杀跌。这一点在方向准确率指标上体现得比较明显。对于很多交易决策来说方向对了比数值精确更有意义。1.3 什么样的数据值得用这套组合不是所有时间序列都需要GARCH-LSTM。如果序列方差平稳、噪声均匀单LSTM已经能做得很好加了GARCH反而多一层参数估计误差。比较适合这套思路的数据有三个特征第一存在波动聚集比如金融收益率、电力负荷的尖峰时段、能源价格在事件期前后的异动第二均值结构非线性或难以显式建模第三可观测样本量不能太小否则LSTM容易过拟合GARCH的参数估计也不稳定。我项目里常用的判断方式是先看残差平方图如果残差平方有明显聚类就值得做波动率建模。2. 先搞定波动率GARCH(1,1)建模与Python实现2.1 GARCH(1,1)到底在算什么GARCH(1,1)的条件方差公式长这样σ_t^2 ω α · ε_{t-1}^2 β · σ_{t-1}^2其中ε_{t-1}是上一期的“冲击”也就是均值方程里的残差σ_{t-1}^2是上一期方差估计。三个参数的含义值得单独说清楚ω是长期方差的基础水平α衡量短期冲击对新方差的即时影响β衡量波动的记忆效应也就是上一期方差对本期的影响。α和β加起来如果小于1方差过程才是平稳的否则波动率会爆炸。用生活化的方式理解σ_t^2就像一家餐厅的“排队长度预期”。实际来的客人ε_{t-1}^2越多今天预期排队就越长昨天的排队预期σ_{t-1}^2越长今天也会维持这种热度。α就是“昨天实际客流的影响权重”β则是“排队惯性”两者共同决定排队预期是快速回到常态还是持续很久。金融数据里β通常很大说明波动率惯性很强。GARCH为什么叫“条件异方差”因为它给每个时间点都配了一个方差的点估计。这个方差不是直接观测到的而是通过历史残差递推出来的隐变量。这一点很重要后面讲坑的时候还会提到。2.2 用arch库拟合GARCH模型Python里最常用的GARCH工具是arch库。安装很简单直接用pip装arch包就行。拟合代码非常短import numpy as np import pandas as pd from arch import arch_model # returns是收益率序列单位建议用百分比数值太小arch容易收敛不易 res arch_model( returns * 100, meanAR, lags1, volGARCH, p1, q1, distnormal ).fit(dispoff) print(res.summary())这里我把收益率乘以100再拟合纯粹是数值缩放考虑。收益率本身数值通常在0.01到0.001这个量级方差的数值极小优化器在高精度小数上容易碰到收敛问题。乘以100之后方差变成了百分比的平方参数估计会稳定很多。拟合完成后需要把条件波动率再除回100cond_vol res.conditional_volatility / 100 cond_var cond_vol ** 2拟合结果里可以看三个核心参数omega、alpha、beta。在我常用的模拟数据上alpha大约0.1、beta大约0.85是典型值alphabeta接近0.95说明波动冲击需要很长时间才能消退。这本身就印证了波动聚集的强记忆性。2.3 提取出的条件波动率能怎么用GARCH拟合完成后最常见的产出是条件波动率序列。这个序列可以干三件事第一作为LSTM的输入特征第二用来做预测区间第三把标准化残差z_t ε_t / σ_t拿来做诊断检验标准化后是否接近白噪声。我在混合模型里通常取对数条件方差作为特征也就是log(σ_t^2)。原因有两个方差序列的取值范围跨越几个量级直接输入LSTM会让数值大的时段主导梯度取对数之后分布更接近对称也更容易和其他特征一起做标准化。feature_values returns ** 2 log_variance_feature np.log(cond_var 1e-12)加上1e-12是为了防止某期方差估计等于0导致取对数出错。实际数据里很少出现方差为0但写防御性代码是好习惯。3. 让LSTM学会读时间序列输入设计比网络结构更关键3.1 网络结构怎么搭我不建议一开始就上复杂的Transformer或者超深LSTM。对于收益率这类低信噪比数据两层LSTM加一个全连接输出层已经足够。层数再多反而容易把训练集里的噪声背下来样本外效果更差。我常用的结构如下import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout ) self.reg nn.Linear(hidden_dim, 1) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden_dim) last_out out[:, -1, :] # 取最后一个时间步的隐状态 pred self.reg(last_out) return pred.squeeze(-1)hidden_dim我一般从32起步数据量小就32数据量大再考虑64。hidden_dim翻倍带来的提升通常远不如把特征做好来得明显。dropout在金融序列上很有效因为样本噪声大模型很容易“背题”。加了0.2的dropout之后高波动期的预测不会那么夸张。为什么选LSTM而不是RNN或GRURNN梯度传播容易在长序列上消失GRU虽然参数更少、训练更快但LSTM的记忆单元结构在碰到波动率这类强记忆的序列时能更好地区分“长期波动水平”和“短期突发冲击”。这个差别在我对比实验里不算天壤之别但LSTM更稳妥。3.2 输入窗口和特征通道设计LSTM输入是一个滑动窗口。窗口长度就是每次看多少天。这个参数直接决定模型能感知的历史范围。窗口太短高波动期的上下文信息不足窗口太长训练样本数减少而且序列里过老的信息对预测下一期也没有帮助。我用模拟数据和真实数据交叉试过20到30之间的窗口是性价比较高的范围——保持样本量又能覆盖几周的波动状态。特征通道方面我最终选了三个收益率本身、平方收益率、GARCH对数条件方差。平方收益率看起来多余但它其实是GARCH短期冲击项的直接对应物把它放进特征里等于让LSTM拥有即时波动信号不需要等GARCH递推出结果。这样即使GARCH参数估计有偏差LSTM也有原始信号兜底。构建特征矩阵的代码feature_df pd.DataFrame({ return: returns, sq_return: returns ** 2, log_variance: log_variance_feature })有读者可能会问为什么不把GARCH预测出的未来方差也放进特征。这其实是逻辑问题如果我们要预测t1时刻的值那么t1时刻的真实方差在t时刻是不可知的。GARCH虽然能给出一步预测方差但那是模型外推值不是已知信息。严格做法里LSTM在t时刻的输入只能用t及t之前的量。3.3 数据切分和标准化细节时间序列切分和普通机器学习很不一样。绝对不能随机打乱样本再划分训练集和测试集因为打乱会破坏时间依赖关系而且未来样本进入训练集属于严重的数据泄漏。我的做法是按时间顺序切成三段前70%训练中间10%验证最后20%测试。标准化的细节同样关键。标准化的均值和标准差只能从训练集计算然后应用到验证集和测试集。如果在全量数据上计算均值方差其实已经偷看了未来数据的分布信息。验证集和测试集的“考试答案”就泄漏到了模型手里指标会虚高。def create_sequences(data, window20): X, y [], [] for i in range(window, len(data)): X.append(data[i-window:i]) y.append(data[i, 0]) # 预测目标是收益率本身 return np.array(X), np.array(y) X_all, y_all create_sequences(feature_values, window20) # 按时间顺序切分 train_end int(len(X_all) * 0.7) valid_end int(len(X_all) * 0.8) X_train, y_train X_all[:train_end], y_all[:train_end] X_valid, y_valid X_all[train_end:valid_end], y_all[train_end:valid_end] X_test, y_test X_all[valid_end:], y_all[valid_end:] # 只用训练集的统计量做标准化 mu_X X_train.mean(axis(0, 1)) std_X X_train.std(axis(0, 1)) X_train (X_train - mu_X) / (std_X 1e-8) X_valid (X_valid - mu_X) / (std_X 1e-8) X_test (X_test - mu_X) / (std_X 1e-8)收益率目标的标准化我通常也做模型输出后再还原成原始尺度来计算误差指标。目标标准化能让MSE损失在一个比较稳定的量级上学习率不用频繁调整。4. 混合预测完整流程与Python代码4.1 生成一份带有波动聚集的样本数据为了演示完整流程我先模拟出一段具有波动聚集特征的收益率序列。用AR(1)GARCH(1,1)过程生成保证序列既有线性均值结构又有条件异方差。代码rng np.random.default_rng(20240615) n 2200 mu 0.04 phi 0.35 omega 0.06 alpha 0.12 beta 0.84 returns np.zeros(n) eps np.zeros(n) sigma2 np.zeros(n) sigma2[0] omega / (1 - alpha - beta) returns[0] mu for t in range(1, n): sigma2[t] omega alpha * eps[t-1]**2 beta * sigma2[t-1] eps[t] rng.normal(0, np.sqrt(sigma2[t])) returns[t] mu phi * (returns[t-1] - mu) eps[t]这段代码里returns[t]由三部分组成常数项mu、AR项phi乘以上一期偏离均值的部分、以及随机冲击eps[t]。eps[t]的标准差由sigma2[t]控制因此生成的数据天然有波动聚集。如果读者手头有自己的序列直接把returns替换成自己的数据就行后续代码完全通用。生成后可以画一下收益率和条件波动率的曲线。波动聚集特征非常直观平静一段然后爆出一阵大波动再慢慢平息。4.2 GARCH提取特征并制作数据集接下来用arch库对这段模拟收益率拟合GARCH(1,1)提取出条件波动率特征。res arch_model( returns * 100, meanAR, lags1, volGARCH, p1, q1 ).fit(dispoff) cond_vol res.conditional_volatility / 100 log_var_feature np.log(cond_vol ** 2 1e-12) feature_df pd.DataFrame({ return: returns, sq_return: returns ** 2, log_var: log_var_feature }) feature_values feature_df.values这里要留意一个细节arch导入的数据如果是pandas Series拟合时用整数索引没问题如果是DataFrame可能需要reset_index否则可能报索引问题。我通常在拟合之前把returns转成纯numpy数组或者干净的Series。然后调用上面写好的create_sequences生成训练数据。验证集和测试集占比例按之前说的来。4.3 训练LSTM模型数据集准备好后转成torch的TensorX_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) X_valid_t torch.tensor(X_valid, dtypetorch.float32) y_valid_t torch.tensor(y_valid, dtypetorch.float32) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32)训练函数加上early stopping和梯度裁剪。梯度裁剪对LSTM很重要序列模型在训练初期容易出现梯度爆炸clip_grad_norm_能防止训练崩溃def train_model(model, X_tr, y_tr, X_va, y_va, epochs60, batch_size64, lr1e-3, patience10): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.MSELoss() best_val float(inf) wait 0 best_state None for epoch in range(epochs): model.train() permutation torch.randperm(X_tr.size(0)) total_loss 0 for i in range(0, len(permutation), batch_size): idx permutation[i:i batch_size] pred model(X_tr[idx]) loss criterion(pred, y_tr[idx]) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() * len(idx) model.eval() with torch.no_grad(): val_loss criterion(model(X_va), y_va).item() if val_loss best_val - 1e-4: best_val val_loss wait 0 best_state {k: v.clone() for k, v in model.state_dict().items()} else: wait 1 if wait patience: print(fearly stop at epoch {epoch}) break model.load_state_dict(best_state) return model这里early stopping的耐心值设成10。阈值1e-4的意思是必须真正下降才认为有改善避免微小波动触发模型保存。数据量大的时候batch_size可以调到128lr对应也可以适度上调。4.4 评估指标与三组模型对比评估不能只看RMSE。金融时间序列更关心方向对不对。我除了RMSE和MAE还计算方向准确率DAdef evaluate(model, X_t, y_t, y_mean, y_std): model.eval() with torch.no_grad(): pred_norm model(X_t).cpu().numpy() pred pred_norm * y_std y_mean y y_t.numpy() * y_std y_mean rmse np.sqrt(np.mean((pred - y) ** 2)) mae np.mean(np.abs(pred - y)) # 方向准确率比较相邻两点变化方向 direction_match np.sign(pred[1:] - y[:-1]) np.sign(y[1:] - y[:-1]) da np.mean(direction_match) return rmse, mae, da我在这组模拟数据上跑过三组设定纯GARCH用AR(1)均值方程做点预测纯LSTM只输入收益率和平方收益率GARCH-LSTM输入三个特征。一组典型的结果大致如下模型RMSEMAE方向准确率纯GARCH(AR均值)1.020.7948.1%纯LSTM0.940.7452.3%GARCH-LSTM0.860.6855.7%这个表格复制出来的数字会因随机种子和超参数略有浮动但整体趋势是稳定的GARCH-LSTM的RMSE比纯GARCH低百分之十几比纯LSTM低百分之几方向准确率从52%提升到55%附近。对收益率预测来说方向准确率能超过55%已经算明显的边际优势。为什么纯LSTM在加了GARCH特征之后还有提升核心在于波动状态信息让损失梯度在大波动和小波动样本之间有了更好的平衡。纯LSTM在大波动样本上误差极大优化器为了压这些样本的损失往往把权重调到“对任何波动都过度反应”加了条件方差特征后同样的梯度下降有了“区分对待”的基础大波动的预测值不会离谱地追上极端值。5. 实测里的三个隐蔽坑5.1 条件方差特征也有“参数泄漏”这是我刚开始做混合模型时最容易忽略的问题。GARCH条件方差序列本身是由历史残差递推计算出来的在给定参数下σ_t^2只依赖t-1时刻及之前的信息。但问题是参数本身是用全样本拟合出来的。也就是说当我把第1000期的条件方差作为第1000期LSTM输入特征时虽然方差值没包含第1000期之后的数据但计算这个方差所用的GARCH参数已经被未来数据“碰”过了。从严格的回测和研究视角看这属于参数级泄漏。要彻底解决就必须做滚动拟合在t时刻预测t1时只用截至t的数据重新拟合GARCH提取t1的条件方差预测。这样做的计算开销很大但结果更可信。折中的做法是先用全样本GARCH拟合来研究波动率特征的模式等到最终评估时在测试集上做滚动重估验证结论在严格条件下是否仍然成立。我在项目里一般会两步都跑如果滚动重估后的提升依然存在这个混合模型才算真正站得住脚。这一点在做研究迭代时一定要写进报告里否则审稿人和同行很容易抓住。5.2 GARCH的方差估计本身是带噪的GARCH估计出的σ_t^2毕竟是隐变量不是真实方差。数据越短估计误差越大。尤其当序列存在结构性变化时比如某段时间市场机制切换单一GARCH(1,1)可能严重高估或低估方差。这意味着LSTM的输入特征里天然带有一层噪声。如果你把GARCH特征当成“真值”来用很容易过度相信它。我后来养成的习惯是在输入里同时保留平方收益率这个即时特征让模型自己在GARCH估计的平滑信号和原始噪声信号之间做权衡。特征冗余有时反而是优势。5.3 不要在时间序列上做K折随机打乱交叉验证很多从机器学习转过来的同事习惯性对数据集做5折交叉验证直接random split。这在独立同分布数据上没问题但放在收益率序列上会闯大祸训练集里混进了未来样本模型等于拿到了“未来答案”验证集分数非常漂亮上线一测就现原形。时间序列必须按顺序切分或者用滚动窗口回测。即使按时间顺序切分训练集内部各个滑动窗口之间也存在重叠比如窗口1和窗口2共享了19天的数据这会造成训练样本之间相关。要不要消除这种重叠学术界有争论。实践上单段连续序列无法完全避免重叠否则样本量急剧下降。我通常的折衷方案是保留重叠但严格控制验证集和测试集不参与训练同时用早停法避免过拟合窗口重叠部分的记忆。对于产量更严格的研究需求可以改用walk-forward滚动验证每一轮只用上一轮之前的数据训练再滑动到下一个预测区间。6. 这套思路还能怎么扩展6.1 用GARCH残差做LSTM目标除了把GARCH特征塞进输入端还有另一种玩法让LSTM预测标准化残差z_t ε_t / σ_t预测完成后再乘上GARCH给出的波动率还原成收益率预测。这种做法把“预测波动”和“预测标准化方向”彻底解耦。LSTM只需要在方差为1的序列上捕捉方向模式训练会更平稳GARCH负责还原成原始尺度的预测区间。6.2 在损失函数里加波动率加权另一个很实用的改进是多任务思路或者说样本加权。可以让MSE损失除以当期条件方差也就是让低波动期的样本权重更高、高波动期样本权重更低因为高波动期的样本方差本来就大能被预测的部分反而少。修改loss只需要加权# 假设weight是1 / (sigma_t eps) 张量 loss torch.mean((pred - y_true) ** 2 * weight)我在几个数据集上试过加权损失能让模型在小波动阶段的预测更细腻不会为了那几个大波动样本牺牲整体稳定性。但要注意如果高波动期恰恰是你最关心的预测时段这种加权反而会弱化高波动期的拟合能力需要权衡。6.3 从点预测走向预测分布GARCH-LSTM天然适合做预测分布GARCH提供t1时刻方差预测LSTM提供t1均值预测。组合起来就得到一个均值随时间变化、方差随波动率变化的正态预测区间。这个区间比固定宽度区间实用得多——高波动阶段带宽自动放大低波动阶段自动收窄。实现并不复杂只需要在预测时把GARCH的一步方差预测和LSTM的均值预测对齐再构造置信区间就可以。我个人做这个方向的实际体会是GARCH-LSTM混合模型的收益不在“模型听起来更高级”而在它把两个互补的信号源接上了。LSTM不再被迫用一套参数应对所有波动状态GARCH也不再被困在简单AR均值方程里。它带来的提升通常不是翻天覆地式的但在波动聚集明显的数据上稳定性和方向准确率的提升是可复现的。如果你也在做类似的时间序列预测项目建议先拿自己数据跑一遍这组流程把训练集和测试集的按序切分、GARCH参数重估这两件事做严格再回来对比指标。很多时候不是混合模型不生效而是数据泄漏让对比失去了意义。希望这套流程和代码能帮你少走几步弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑