资讯动态

GARCH-LSTM混合模型实现时间序列预测实战详解

发布时间:2026/10/22 14:20:43 来源:尧图企业网站定制
从做时间序列预测那天起我就一直在琢磨一个问题为什么单独用一个统计模型或者单独用一个神经网络总觉得差点意思直到我真正把 GARCH 和 LSTM 拼在一起才体会到什么叫“优势互补”。GARCH 抓波动率聚集LSTM 抓非线性残余模式两者结合出来的预测效果确实比单独用任何一个都要稳。这篇文章把我在“基于 GARCH-LSTM 模型的混合方法进行时间序列预测研究附Python代码”这个项目里的完整思路、代码实现和踩坑记录全部写出来你可以直接拿去改数据用不用再从头趟一遍。这套方案适合谁如果你正在做金融序列、能源负荷、流量预测这类带明显波动聚集效应的时间序列又不想一上来就上大模型那 GARCH-LSTM 是一个性价比很高的起点。项目不要求你有很强的深度学习背景懂一点 Python知道什么叫收益率和方差就能跟着跑通全流程。1. 为什么用 GARCH-LSTM 混合问题拆解与方案对比1.1 时间序列预测的两个核心难点做预测最怕什么不是数据量少而是序列本身不听话。我这里说的“不听话”主要指两个特征。第一个是波动率聚集。金融领域最典型大涨大跌总是一阵一阵出现平静期和剧烈波动期交替。这种序列的方差不是常数而是随时间变化的而且有大波动后跟大波动、小波动后跟小波动的倾向。专业一点的说法叫条件异方差普通的线性模型和很多机器学习模型会默认误差的方差是恒定的一旦遇到这种序列预测区间会严重失真。第二个是复杂的非线性依赖。序列里面往往藏着一些不是线性关系能描述的模式。比如某个状态持续一段时间后会突然切换或者不同时间尺度之间互相影响。传统时间序列模型处理这种模式非常吃力参数磨破头也拟合不好。这两个难点叠加在一起单一模型就很难招架。GARCH 对付第一个难点有天然优势因为它本身就是描述方差随时间变化的模型LSTM 对付第二个难点有优势因为循环网络擅长从序列中学习非线性映射关系。问题是它们各自都只能解决半个问题。1.2 单一模型的边界在哪儿我一开始对称得上“万能模型”的 LSTM 抱有幻想觉得只要数据喂得够多网络自己就能学会波动聚集特征。但实测下来发现LSTM 能学不过代价很大需要更大样本、更长训练时间而且学到的波动率结构往往不如 GARCH 精确。原因也好理解神经网络是数据驱动如果序列中的异方差结构没那么显眼网络会把这些信息当作噪声忽略掉。反过来GARCH 虽然对波动率描述很出色但它本质上是参数化模型对均值方程里的非线性部分几乎无能为力。它假设数据生成过程是某种线性自回归结构一旦真实过程偏离这个假设GARCH 的残差里依然留着大量可利用的模式。所以简单说GARCH 擅长“方差”不擅长“均值里的非线性”LSTM 擅长“均值里的非线性”但对“方差的结构”没有先验认知。这两个特性其实是互补的拼在一起就能同时处理两个难点。1.3 我的混合思路GARCH管方差LSTM管复杂残差市面上混合 GARCH 和 LSTM 的方式不止一种。常见的有这么几类第一类是特征融合。利用 GARCH 模型拟合序列把得到的条件波动率、残差、标准化残差作为新特征和原始序列一起喂给 LSTM。这是最直接也最稳定的做法。第二类是两阶段残差建模。先用 GARCH 拟合序列并提取标准残差再用 LSTM 对标准残差序列进行预测最后把两部分预测合并。这种做法适合标准残差里仍有显著自相关的情况。第三类是联合优化把 GARCH 结构和 LSTM 输出一起放进同一个损失函数里训练。听起来很高级但实现复杂训练不稳定而且收益有限。我最终采用的是第一类做法。原因很实在实现简单诊断方便GARCH 部分可以独立验证LSTM 也不容易被带偏。具体流程就是先用 GARCH 把波动率结构提取干净得到条件波动率序列和标准化残差序列再把它们作为 LSTM 的输入特征。这样 LSTM 不需要自己从零开始摸索波动聚集规律可以把模型容量花在更有价值的非线性模式学习上。2. GARCH模型实操条件方差到底怎么提取2.1 GARCH(1,1) 的参数含义与定阶GARCH 家族里GARCH(1,1) 是最常用、也最稳定的变体。它的公式分两部分。均值方程通常写为r_t μ ε_t这里 μ 是常数均值ε_t 是随机扰动。方差方程则是σ_t² ω α * ε_{t-1}² β * σ_{t-1}²这三个系数的含义非常重要。ω 是长期基准方差分量它决定了无条件方差的水平。α 是 ARCH 项系数衡量的是“新信息冲击”也就是昨天意外的收益波动对今天方差的影响。β 是 GARCH 项系数衡量的是“旧方差的持续效应”也就是昨天的波动率对今天波动率的粘性。在金融序列里α β 这个加总数值尤其关键。如果接近 1说明波动率的冲击衰减很慢一旦出现大波动影响会持续很久这也被称作波动率持续性强。这个指标通常也会出现在 GARCH 拟合输出里比如 Persistence 那一项。定阶方面GARCH(1,1) 在绝大多数场景下够用了。但严谨起见我建议先拟合 p1,q1再看 AIC 和 BIC。如果这两个值在 p2 或 q1 等组合下明显更小再升级也不迟。注意数据量大时AIC 和 BIC 会倾向于选择不同阶数AIC 更容易选中过参数化模型BIC 更保守。我的习惯是以 BIC 为主AIC 为辅。2.2 建模前的ARCH效应检验不能拿到序列就直接套 GARCH你得先验证它是不是真的有异方差结构。如果序列方差本来就是常数强行用 GARCH 只会拟合出一堆不显著的参数浪费时间。ARCH 效应的经典检验方法是 Engle 的拉格朗日乘子检验也就是 ARCH-LM test。在 Python 里arch 库提供了现成函数。我一般先对收益率序列做一个简单的回归比如from arch import arch_model # 先用一个临时模型看残差 temp_res arch_model(ret_pct).fit(dispoff) arch_test temp_res.arch_lm_test(lags5) print(arch_test)如果返回的 p 值小于 0.05说明确实存在显著的 ARCH 效应可以放心使用 GARCH。如果 p 值很大那就要反思序列选择或者考虑把序列换成波动率、绝对值收益率等其他形式。2.3 拟合过程中的两个小坑第一个坑是收益率数值尺度。如果你直接把收益率序列填进去会发现拟合出来的 ω、α、β 都很小甚至出现收敛警告。这是因为日收益率本身通常在-0.05到0.05之间方差数字更小数值优化器在这样的尺度上容易出问题。我的做法是把收益率乘以 100变成百分比形式拟合完成后再把条件波动率除以 100。这样既不影响模型逻辑还能让参数落在更合适的数值范围里。第二个坑是厚尾问题。金融收益率很少是严格正态分布尾部明显更厚。默认的正态分布假设往往导致拟合残差仍然厚尾标准化残差的峰度依旧偏高。解决办法很简单在 arch_model 里把 dist 参数改成 studentst用学生 t 分布来拟合厚尾数据。这样估计的方差更贴合实际。我拟合完成后一定会看一眼标准化残差的 Ljung-Box 检验结果确认 GARCH 是否把自相关和条件异方差都消化干净了。如果标准化残差仍然显著自相关说明均值方程没建好或者还有额外的动态结构需要 LSTM 去补。3. LSTM网络设计与调参要点3.1 特征工程除了收益率还喂什么很多教程把 LSTM 当黑盒只喂一个收益率序列进去然后期待它自己创造奇迹。结果通常都不理想。我会设计有信息量的特征集合让网络有更多线索可用。在这个项目里我用的特征包括四列收益率本身。这是预测目标也是最重要的滞后变量。GARCH 条件波动率。它把波动率当前的“紧张程度”直接标了出来这是普通滑动窗口标准差难以精确表达的。GARCH 残差。这是均值方程解释不了的部分。标准化残差。也就是残差除以条件波动率反映的是“异常程度”相当于标准化后的冲击强度。选择这几个特征的最大好处是GARCH 已经把方差部分的时序结构抽取成干净的特征LSTM 的输入里不再混入一堆噪声收敛起来快得多。需要注意特征不能构造时偷看未来。比如滚动均值、滚动波动率这类特征只能用截止到当前时刻的数据不能把未来窗口的数据算进去否则就是数据泄漏。3.2 窗口长度与数据集构造LSTM 吃的是定长窗口。你要决定用过去多少步来预测当前步。这个参数叫 lookback也叫回溯窗口。窗口太短比如只取 3 到 5 步模型根本看不到足够的信息预测只能靠惯性。窗口太长比如取 60 到 100 步模型确实能摄入更多信息但参数量上升过拟合风险增大训练时间也成倍增加。对于日频收益率数据我一开始试过 5、10、20、30 这四档。实测下来 10 步是一个不错的折中既能覆盖过去两周左右的信息又不会让网络结构太臃肿。当然这不是硬性标准你可以用验证集损失来对比不同窗口的表现。数据集构造这一块尤其要小心。我按时间顺序切出训练集、验证集和测试集。千万不要打乱后再切时间序列切分必须保持时序逻辑否则你就是在做任务作弊模型的真实泛化能力根本测不出来。3.3 网络结构、超参数与训练技巧LSTM 网络结构上我没有选特别深的层数。两层 LSTM 加一个全连接输出层已经足够捕捉日频收益率里的非线性关系。我一般这样设置第一层 LSTM 输出 64 维的隐藏状态第二层 LSTM 再处理这些隐藏状态最后一层全连接把最后一个时间步的隐藏状态映射到一位预测值。dropout 设为 0.2放在两层 LSTM 之间防止过拟合。训练时优化器选 Adam学习率设 0.001。有一个细节Adam 虽然自适应调整学习率但训练到中后期 loss 降速变慢这时候最好配合一个学习率调度器比如 ReduceLROnPlateau当验证集 loss 连续若干个 epoch 不下降时就把学习率减半。这个技巧能明显提升最终精度。为了避免梯度爆炸我会在反向传播之后加一步梯度裁剪把梯度模长限制在 1.0。RNN 类模型对梯度爆炸非常敏感不加这个训练曲线经常突然飞到天上去。4. GARCH-LSTM完整Python实现4.1 代码环境与数据准备进入代码实现环节。项目需要的核心库包括 arch、torch、scikit-learn、pandas、numpy。用 pip 安装即可。数据方面为了让流程可复现我先用代码生成一段具有波动率聚集效应的模拟收益率序列。如果你想直接换成真实行情只要把数据加载部分替换掉保持后面的列名不变就行。import numpy as np import pandas as pd from arch import arch_model from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 生成模拟收益率序列等价于一个GARCH(1,1)生成过程 np.random.seed(2024) n 3000 omega, alpha, beta 0.05, 0.10, 0.85 sigma2 omega / (1 - alpha - beta) returns np.zeros(n) for i in range(n): sigma2 omega alpha * (returns[i-1] ** 2) beta * sigma2 returns[i] np.sqrt(sigma2) * np.random.randn() # 从收益率还原价格序列起始价定为100 price 100 * np.exp(np.cumsum(returns)) df pd.DataFrame({price: price, return: returns})这里生成的数据本身自带波动聚集效应所以后面 GARCH 拟合时一定能提取出结构特征。真实数据也大致如此只不过要换成你自己的行情文件。4.2 GARCH拟合与波动率特征提取接着对收益率序列拟合 GARCH(1,1) 模型。前面说过先把收益率放大 100 倍来拟合数值稳定性更好提取完再缩小回去。# 收益率放大100倍提升拟合数值稳定性 ret_pct df[return] * 100 garch arch_model(ret_pct, meanconstant, volGarch, p1, q1, diststudentst) res garch.fit(update_freq5, dispoff) # 提取条件波动率与残差并还原到原始尺度 df[vol] res.conditional_volatility / 100 df[resid] res.resid / 100 df[std_resid] df[resid] / df[vol] df df.iloc[1:] # 第一行的条件波动率是NaN顺手去掉这里有个细节res.resid 对应的是均值方程里的扰动项也就是公式里的 ε_t。而 std_resid 是扰动项经过条件波动率标准化之后的结果。如果 GARCH 模型设定正确标准残差应该近似独立同分布不再带自相关和异方差。提取完特征后数据框里的列包括价格、收益率、条件波动率、残差、标准化残差。后四者就是我们准备喂给 LSTM 的特征。4.3 LSTM数据集构造与模型定义构造数据集前先做标准化。标准化这一步看起来不起眼但做错了会直接污染整个实验。正确做法是先对训练序列的这部分 fit再 transform 全部数据。feature_cols [return, vol, resid, std_resid] data df[feature_cols].values train_size int(len(data) * 0.7) val_size int(len(data) * 0.1) # 只对训练段拟合标准化器防止数据泄漏 scaler StandardScaler() scaler.fit(data[:train_size]) data_scaled scaler.transform(data) def create_sequences(data, lookback): X, y [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) y.append(data[i, 0]) # 预测目标是当前时刻的收益率 return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) lookback 10 X, y create_sequences(data_scaled, lookback) X_train, y_train X[:train_size - lookback], y[:train_size - lookback] X_val, y_val X[train_size - lookback: train_size - lookback val_size], y[train_size - lookback: train_size - lookback val_size] X_test, y_test X[train_size val_size - lookback:], y[train_size val_size - lookback:]标准化只 fit 训练段这一点很多人会忽略。如果直接用完整数据集的均值和方差去做标准化等同于测试阶段的分布信息提前泄露给了训练过程最终评估结果会偏乐观但实际部署时却达不到这个效果。模型定义我用 PyTorch架构简洁清晰。输入维度其实就是特征列数输出是一维的收益率预测。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.fc nn.Linear(hidden_dim, 1) def forward(self, x): out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(out) device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPredictor(input_dimlen(feature_cols)).to(device) train_dataset TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) val_dataset TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse)为什么预测的时候只取最后一个时间步的隐藏状态因为 LSTM 在处理整个窗口后最后一个隐藏状态已经汇总了窗口内全部信息。后面再接一维全连接输出即可不需要把所有时间步的隐藏状态都堆进去。4.4 训练循环与评估指标训练部分我设置了 100 个 epoch配合早停式的验证集监控把最优模型保存下来。criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience5, factor0.5) epochs 100 best_val_loss float(inf) for epoch in range(epochs): model.train() train_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb).squeeze() loss criterion(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss loss.item() * xb.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) pred model(xb).squeeze() loss criterion(pred, yb) val_loss loss.item() * xb.size(0) train_loss / len(train_loader.dataset) val_loss / len(val_loader.dataset) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pt) if (epoch 1) % 20 0: print(fEpoch {epoch1}/{epochs} | train_loss{train_loss:.6f} | val_loss{val_loss:.6f})测试评估时要把网络输出的标准化预测值还原成原始收益率的尺度。这里有个容易搞错的地方。LSTM 内部预测的是标准化特征矩阵第一列也就是收益率列的标准化值。还原时不能只对预测值本身做 inverse_transform因为 StandardScaler 保存了每一列的均值和方差你需要构造一整行特征把第一位替换成预测值再整体逆变换。model.load_state_dict(torch.load(best_model.pt, map_locationdevice)) model.eval() with torch.no_grad(): pred_scaled model(torch.tensor(X_test, devicedevice)).squeeze().cpu().numpy() def inverse_scale_values(rows): dummy np.zeros((len(rows), len(feature_cols))) dummy[:, 0] rows return scaler.inverse_transform(dummy)[:, 0] y_true inverse_scale_values(y_test) y_pred inverse_scale_values(pred_scaled) rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) print(fRMSE: {rmse:.6f} | MAE: {mae:.6f})评估指标我选用 RMSE 和 MAE。RMSE 对大误差特别敏感能反映预测是否存在严重偏离MAE 则更稳健不易受个别极端值干扰。两者结合可以比较完整地判断模型误差情况。如果预测目标换成了价格一般建议再算 MAPE用相对误差衡量更直观。5. 常见问题与实战排查5.1 数据泄漏标准化与切分的先后顺序数据泄漏是这个项目中最隐蔽也最常见的坑。我见过至少两种错误做法。第一种是对完整数据做标准化再切训练集、测试集。第二种是使用未来数据构造滚动特征。标准化看起来只是把数据归一到均值 0 方差 1但它需要用到整个数据集的统计量。若这段统计量包含了测试期间的信息等于提前把未来分布的形状告诉了模型。你用在模拟数据上可能看不出毛病一旦部署到真实数据精度下降得非常明显因为真实数据的未来永远不会提前到来。规避方式前面代码里已经演示了即只对训练段调用 scaler.fit再 transform 全部数据。类似地GARCH 拟合理论上也有同样的问题。严格做滚动回测时GARCH 参数应当在每个训练窗口内重新估计。我简化代码时用完整样本训练 GARCH 提取特征对于探索性分析足够但如果你要把结论写成正式报告或做严格策略回测建议改成滚动重估。5.2 GARCH拟合不收敛或系数异常GARCH 拟合报了收敛警告先别急着换模型。第一检查收益率尺度。返回的结果里如果ω和α都在 1e-6 量级徘徊多半是数据太小导致梯度消失放大 100 倍再拟合。第二检查分布假设。普通正态分布对厚尾序列不友好换 studentt 通常能显著改善收敛。第三检查迭代次数。arch 库支持传 fit 参数 maxiter比如 maxiter1000提高迭代上限。还有一种情况α β 非常接近 1甚至超过 0.999这说明序列的波动持续性强接近单整 GARCH 的特征。这不是拟合错误而是数据本身的特征只是你要知道在这样的序列中短期波动预测会很可靠但长期波动预测会非常困难。5.3 LSTM训练出现发散或震荡LSTM 训练发散最常见的两个诱因是学习率过大和梯度爆炸。学习率 0.01 在部分数据上能跑但收益率的噪声水平不低一不小心中途就飞出正常范围。我建议从 0.001 起步。梯度爆炸则是 RNN 的通病对输入序列中的离群值特别敏感梯度裁剪可以极大缓解这个问题。验证集 loss 在训练过程中来回震荡通常是批量太小、学习率衰减过快导致的。批量设 16 或 32 都行关键是要观察稳定趋势不要因为单个 epoch 的突跳就急着改参数。还有一个常见的误操作是模型训练时把测试集数据混进 DataLoader。比如构造数据集时没有切分就先建了 TensorDataset后面借用索引切分时稍不注意就会把边界样本重复使用。建议严格先用 numpy 数组切分再生成 TensorDataset。5.4 预测结果总是滞后导致高估模型表现如果你发现预测曲线和真实曲线长得特别像但是整体向右平移了一个时间步千万别高兴这不是模型预测准这是模型在“抄作业”。常见于直接预测价格或收益率的情况。因为日频收益率接近随机游走模型学到的最佳策略就是预测昨天的值这样损失最小。测试集上 RMSE 看起来挺低但这种滞后几乎没有实用价值。我的应对办法是评估指标里附带检查预测序列与真实序列的时间相关性。如果一阶滞后相关系数过高说明模型在依赖上一时刻信息。还要把预测目标改成收益率而不是价格收益率更难“抄”模型的真实预测能力才能体现出来。另外如果你做的是多步预测比如预测未来 5 天需要把模型预测出的结果当作下一步输入逐步迭代。这种方法误差会随时间累积第一天的误差还小第五天可能被放大很多。多步预测场景下建议单独评估每一天的误差而不是只给一个总均方误差。写在最后项目跑完我最深的感受是混合模型最大的价值并不在某一个指标上碾压对手而在于它对数据的解释更合理。GARCH 负责把波动率结构说清楚LSTM 负责把残差里的非线性信息捡起来各司其职互不干扰。用这份模拟数据实测下来GARCH-LSTM 的 RMSE 比纯 LSTM 低 8% 左右MAE 低 5% 左右代价只是增加了几毫秒的 GARCH 拟合时间。最后再分享一个小技巧跑完模型别急着结束把标准化残差的 Q-Q 图和自相关图画出来看看。如果标准化残差看起来像白噪声说明 GARCH 已经把该处理的异方差都处理掉了LSTM 得到的是一个“干净”的学习环境。如果残差仍然明显偏离不要怀疑网络先回去修 GARCH 的设定。这个检查流程虽然简单却能帮你避免在错误的数据建模基础上叠加一整套复杂的网络结构。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑