FEATURED · 精选文章

CNN-GRU多变量时序回归预测与SHAP解释实践

发布时间 / 2026/9/2 4:00:18
来源 / 创域科博编辑部
栏目 / 资讯中心
CNN-GRU多变量时序回归预测与SHAP解释实践 多变量时序回归预测里CNN-GRU 是一种非常常见的混合网络结构。CNN 通过一维卷积提取局部特征GRU 负责建模时间上的长期依赖两者组合后往往比单独使用 CNN 或单独使用 GRU 更稳定。真正让模型落地变得困难的是另一件事当预测结果出来后业务方会问为什么某一天预测值被拉高了为什么某个输入特征会改变预测方向。这时候就需要 SHAP 值分析来回答。本文围绕“CNN-GRU 回归预测代码”展开从模型原理、环境准备、数据构造、PyTorch 实现、训练评估到 SHAP 值分析提供一个可复现的完整流程并说明常见报错和排查路径。1. 先把模型原理讲清楚后面代码才容易理解1.1 回归预测任务里的 CNN 和 GRU 各自承担什么角色回归预测的输入通常是连续的历史观测值。比如用过去 16 个时间步、每个时间步的 3 个传感器指标预测未来一个时间步的目标值。输入可以看作一个三维张量形状是(batch, seq_len, n_features)。在这个结构下CNN 和 GRU 各司其职。CNN 在时间序列里通常使用一维卷积Conv1d。它的作用是把相邻时间步之间的局部关系提取出来。比如某个特征在连续 3 个时间步内出现先升后降的形态这种局部模式可以被卷积核捕捉到。一维卷积的特点是共享权重、感受野受卷积核大小控制因此参数量小不容易过拟合。GRU门控循环单元则负责对序列信息进行递归处理。它通过更新门和重置门决定哪些历史信息要保留哪些信息要遗忘。与 LSTM 相比GRU 参数更少训练更快在中等规模数据上表现通常不差。GRU 输出的最后一个隐藏状态可以看作模型对整段序列信息的高层总结。在 CNN-GRU 结构中CNN 相当于“特征预处理器”先把原始时间序列转成更高层的局部特征序列再交给 GRU 做时序建模。如果直接把原始多变量序列交给 GRUGRU 也能工作但它对局部交叉特征和非线性局部模式的提取能力较弱。CNN 在前置阶段承担这部分工作后GRU 可以专注于时间依赖。1.2 为什么单独用 CNN 或单独用 GRU 不够单独使用 CNN 时模型会在每个局部窗口内做特征提取但窗口之外的信息需要通过多层卷积扩大感受野来间接获得。对于依赖开始时刻信息、很久之后才产生影响的序列CNN 要么加深层数要么增大卷积核否则难以建模长期依赖。单独使用 GRU 时模型虽然能记住历史状态但输入中的每个时间步是逐位进入网络的。如果输入维度很高、特征之间存在复杂的局部交叉关系GRU 需要自己学习这些局部模式这会增加拟合难度也更容易在数据量不足时过拟合。CNN-GRU 的思路是把两个问题分开处理CNN 解决“局部特征怎么提取”GRU 解决“长时间依赖怎么建模”。这样设计后模型的归纳偏置更符合时间序列数据的内在结构训练也更容易收敛。1.3 SHAP 值在深度学习回归中解释什么SHAPSHapley Additive exPlanations来源于合作博弈论中的 Shapley 值。它的核心思想是对于一次预测把每个特征视为一个参与合作的成员计算每个成员对预测结果的边际贡献。把所有特征组合下的贡献加权平均就得到该特征的 SHAP 值。在 CNN-GRU 回归模型中SHAP 值可以回答三类问题哪些特征对本次预测影响最大在时间维度上哪几个历史时间步对预测结果贡献更明显某特征是将预测值拉高还是拉低需要特别说明的是SHAP 值不等于特征的原始取值大小。它描述的是“特征值相对基线水平的变化对模型输出造成的影响”。因此即使某个特征数值很大如果它对输出贡献很小SHAP 值也会很小。在模型开发阶段SHAP 能帮助发现无效特征和潜在数据泄漏。在业务落地阶段它能成为向业务方解释模型输出的依据。把 CNN-GRU 与 SHAP 配合使用模型就不是完全的黑盒。2. 环境准备与一份可复现的多变量回归数据2.1 安装依赖本文的代码基于 PyTorch 实现SHAP 用于可解释性分析。建议先创建独立的 Python 环境然后安装以下依赖pip install numpy pandas scikit-learn matplotlib torch shap版本方面不需要刻意追求最新。PyTorch 1.13 之后、2.x 版本都可以运行本文代码。SHAP 建议使用 0.40 以上的版本。如果已经安装过 TensorFlow要注意环境里是否出现cudart动态库冲突这种情况容易导致导入shap时报错。最简单的做法是使用独立的虚拟环境。安装完成后可以执行下面这个命令验证关键包能否正常导入python -c import torch, shap, sklearn, numpy; print(torch.__version__, shap.__version__)如果输出两个版本号说明基础环境没问题。2.2 构造多变量时间序列回归数据为了不依赖外部数据文件同时也为了让整个流程可以完整复现这里用人工生成的方式构造一组多变量时间序列。生成 1200 个时间步每个时间步有 3 个特征。三个特征分别由不同周期的正弦波、余弦波和带噪声的信号组成。目标值y由三个特征按一定系数组合再加上少量噪声。import numpy as np np.random.seed(42) T 1200 t np.arange(T) f1 np.sin(2 * np.pi * t / 50) f2 np.cos(2 * np.pi * t / 30) f3 np.sin(2 * np.pi * t / 100) 0.3 * np.random.randn(T) X np.stack([f1, f2, f3], axis1) y 0.5 * f1 0.8 * f2 - 0.4 * f3 0.2 * np.random.randn(T) print(X.shape, y.shape)输出为(1200, 3)和(1200,)。这里的特征名可以改成实际业务中的含义例如温度、压力、转速等但本文为了通用性使用feature_1、feature_2、feature_3称呼。回归预测模型不能直接使用(T, n_features)作为输入因为每个预测样本需要包含一段历史窗口。通常使用滑动窗口构造样本SEQ_LEN 16 def create_sequences(features, target, seq_len): Xs, ys [], [] for i in range(seq_len, len(target)): Xs.append(features[i - seq_len:i]) ys.append(target[i]) return np.array(Xs, dtypenp.float32), np.array(ys, dtypenp.float32) X_seq, y_seq create_sequences(X, y, SEQ_LEN) print(X_seq.shape, y_seq.shape)输出为(1184, 16, 3)和(1184,)。每个样本表示“过去 16 个时间步的 3 个特征”目标值是第 17 个时间步的y。2.3 按时间顺序切分训练集和测试集这里必须注意一个原则时间序列预测场景中测试集不能从全体样本中随机抽取否则会让模型“偷看”到未来信息。本文按时间顺序前 80% 的样本作为训练集后 20% 作为测试集。train_size int(X_seq.shape[0] * 0.8) train_x X_seq[:train_size] test_x X_seq[train_size:] train_y y_seq[:train_size] test_y y_seq[train_size:] print(train_x.shape, test_x.shape)2.4 标准化时只能使用训练集统计量神经网络对输入数值范围很敏感。如果两个特征不在同一量纲下梯度更新会不稳定。因此需要对特征和目标值做标准化。常见做法是使用StandardScaler把数据转换为均值为 0、标准差为 1 的分布。这里有一个关键约束scaler只能通过训练集拟合再用这个已经拟合好的scaler同时转换训练集和测试集。如果先对整个数据集做fit测试集的信息会泄漏到训练过程中导致验证结果虚高。from sklearn.preprocessing import StandardScaler scaler_x StandardScaler() train_x_2d train_x.reshape(-1, train_x.shape[-1]) scaler_x.fit(train_x_2d) train_x_scale scaler_x.transform(train_x_2d).reshape(train_x.shape) test_x_2d test_x.reshape(-1, test_x.shape[-1]) test_x_scale scaler_x.transform(test_x_2d).reshape(test_x.shape) scaler_y StandardScaler() train_y_scale scaler_y.fit_transform(train_y.reshape(-1, 1)).reshape(-1) test_y_scale scaler_y.transform(test_y.reshape(-1, 1)).reshape(-1)标准化之后训练和验证时的损失计算都在“标准化空间”进行最终评估指标需要还原到原始数值范围才能解释实际误差含义。数据形状是否标准化使用方式原始特征 X(T, 3)是转换时使用训练集拟合的 scaler窗口样本 X_seq(样本数, 16, 3)是模型输入目标值 y(T,)是模型输出目标最终预测值(样本数, 1)否用 scaler_y 反变换后评估3. 从零实现 CNN-GRU 回归预测模型3.1 网络结构设计模型分为四个部分Conv1d从n_features个原始特征中提取局部特征卷积核大小为 3输出 32 个通道。ReLU引入非线性。AdaptiveAvgPool1d把序列长度压缩为SEQ_LEN // 2减少 GRU 的计算量。GRU对压缩后的特征序列建模输出最后一个隐藏状态。Linear把隐藏状态映射为 1 个数值得到回归预测结果。各模块输入输出关系如下模块输入形状输出形状Conv1d ReLU Pool(B, 3, 16)(B, 32, 8)维度重排(B, 32, 8)(B, 8, 32)GRU(B, 8, 32)(B, 8, 32)取最后隐状态 (B, 32)Linear(B, 32)(B, 1)这里的B是 batch size。输出层使用线性层不加 Sigmoid 或 ReLU因为回归任务要求输出连续数值需要保留正负方向。3.2 模型代码实现import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class CNNGRURegressor(nn.Module): def __init__(self, n_features, seq_len16, hidden_size32, num_layers1): super().__init__() self.conv1 nn.Conv1d(n_features, 32, kernel_size3, padding1) self.relu nn.ReLU() self.pool nn.AdaptiveAvgPool1d(seq_len // 2) self.gru nn.GRU( input_size32, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (B, L, F) x x.permute(0, 2, 1) # (B, F, L) x self.relu(self.conv1(x)) x self.pool(x) # (B, 32, L/2) x x.permute(0, 2, 1) # (B, L/2, 32) _, h_n self.gru(x) # h_n: (num_layers, B, hidden_size) out self.fc(h_n[-1]) # (B, 1) return out前向过程中有两处关键维度转换。第一处是permute(0, 2, 1)。模型默认输入是(B, L, F)而Conv1d要求输入是(B, C, L)其中C是输入通道数。这里把特征维度F放到通道位置把时间步长度放到最后一维。第二处是 GRU 的batch_firstTrue。此时 GRU 的输入格式是(B, L, hidden_input)。所以池化后需要再次permute把维度从(B, 32, L/2)调整为(B, L/2, 32)。h_n是 GRU 每层最后的隐藏状态形状为(num_layers, B, hidden_size)。本文使用单层 GRU因此h_n[-1]等价于最后一层最后一个时间步的隐藏状态形状是(B, hidden_size)。3.3 损失函数、优化器与训练参数回归任务通常使用均方误差MSELoss。优化器使用Adam学习率从1e-3开始。学习率过大会导致损失在初期就出现 NaN过小则收敛缓慢。device torch.device(cuda if torch.cuda.is_available() else cpu) model CNNGRURegressor(n_features3, seq_lenSEQ_LEN).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)训练数据通过DataLoader分 batch 加载。这里使用shuffleTrue因为每个样本本身已经包含一段历史窗口样本之间不要求顺序进入模型。如果任务本身强调“状态连续”比如在线学习场景则要考虑关闭 shuffle。train_dataset TensorDataset( torch.from_numpy(train_x_scale), torch.from_numpy(train_y_scale) ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue)4. 完整训练流程与回归预测评估4.1 训练与验证循环训练循环中需要把输入和目标都放到device上。yb的形状是(B,)需要转换成(B, 1)与模型输出一致。epochs 25 for epoch in range(epochs): model.train() total_loss 0.0 for xb, yb in train_loader: xb xb.to(device) yb yb.view(-1, 1).to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_loss loss.item() * xb.size(0) avg_loss total_loss / len(train_dataset) if (epoch 1) % 5 0: print(fepoch {epoch 1:3d}, train_mse {avg_loss:.6f})这里只输出了训练集 MSE没有输出验证集。实际项目中建议在训练过程中每隔几个 epoch在验证集上计算一次损失并保存验证损失最低的模型。验证和预测时必须切换到model.eval()模式同时使用with torch.no_grad()关闭梯度计算减少显存占用并提升速度。model.eval() with torch.no_grad(): x_test_tensor torch.from_numpy(test_x_scale).float().to(device) pred_scaled model(x_test_tensor).cpu().numpy() true scaler_y.inverse_transform(test_y_scale.reshape(-1, 1)) preds scaler_y.inverse_transform(pred_scaled)4.2 回归评估指标RMSE、MAE、R2回归预测常用三个指标RMSE均方根误差对大误差敏感。MAE平均绝对误差对大误差的惩罚更平滑。R2决定系数越接近 1 表示模型解释能力越强。rmse np.sqrt(np.mean((preds - true) ** 2)) mae np.mean(np.abs(preds - true)) ss_res np.sum((true - preds) ** 2) ss_tot np.sum((true - np.mean(true)) ** 2) r2 1 - ss_res / ss_tot print(fRMSE: {rmse:.4f}, MAE: {mae:.4f}, R2: {r2:.4f})在人工生成的数据上这个简单模型通常可以得到比较高的 R2。真实业务数据中R2 低于 0.5 也很常见此时需要先检查特征是否足够、序列长度是否合适、是否存在数据泄漏。4.3 预测曲线可视化把测试集中前 200 个样本的真实值和预测值画出来可以直观看到预测是否跟上了真实曲线。import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.plot(true[:200], labeltrue) plt.plot(preds[:200], labelpred) plt.legend() plt.title(CNN-GRU regression prediction) plt.show()如果预测曲线整体滞后于真实曲线常见原因是窗口长度太短、模型容量不足、或者序列本身存在强趋势但网络结构没有处理趋势的机制。4.4 效果不理想时先检查什么现象检查点训练损失不下降学习率是否过大/过小特征是否标准化测试集远差于训练集是否过拟合模型容量是否偏大样本是否太少预测曲线滞后窗口长度是否太短特征是否包含足够的先导信息R2 非常低数据是否存在强噪声特征与目标是否真的相关损失出现 NaN学习率过大、梯度爆炸、数据中存在异常值5. 用 SHAP 解释 CNN-GRU 的回归预测结果5.1 选择适合深度模型的 SHAP 方法shap库提供了多种解释器。对于深度学习模型常见的有两种DeepExplainer基于 DeepLIFT 和 Shapley 值的近似适合大多数神经网络但对部分自定义算子兼容性有限。GradientExplainer基于期望梯度Expected Gradients对卷积网络和循环网络都有较好的兼容性实现简单不容易因为某个算子不支持而报错。在 CNN-GRU 这类包含Conv1d、GRU、Linear的混合模型中推荐优先使用GradientExplainer。DeepExplainer在部分 PyTorch 版本中对 GRU 支持不稳定如果换成GradientExplainer后能稳定运行就说明问题不在特征而在解释器。5.2 为 CNN-GRU 计算 SHAP 值使用 SHAP 时需要从训练集中选一部分样本作为背景数据。背景数据用于估计模型在没有特征贡献时的基准输出。背景数据越多结果越稳定但计算会明显变慢。通常取 64 到 256 条即可。import shap model.eval() background torch.from_numpy(train_x_scale[:128]).float().to(device) test_sample torch.from_numpy(test_x_scale[:32]).float().to(device) explainer shap.GradientExplainer(model, background) shap_values explainer.shap_values(test_sample)explainer.shap_values的返回类型可能与预期不完全一致。有些版本会返回 list有些版本返回 ndarray有些情况下输出形状中还可能多出最后一维。保险起见统一做一次兼容处理if isinstance(shap_values, list): sv shap_values[0] else: sv shap_values sv np.array(sv) if sv.ndim 4: sv sv.squeeze(-1) elif sv.ndim 3 and sv.shape[-1] 1: sv sv.squeeze(-1) print(sv.shape)正常情况下sv的形状是(32, 16, 3)含义是32 个测试样本每个样本 16 个时间步每个时间步 3 个特征的 SHAP 值。5.3 SHAP 结果的三种解读方式第一种是全局特征重要性。把sv在样本维度和时间步维度上取绝对值均值得到每个特征的平均贡献强度。feature_names [feature_1, feature_2, feature_3] feat_imp np.abs(sv).mean(axis(0, 1)) plt.figure(figsize(8, 3)) plt.bar(feature_names, feat_imp) plt.title(SHAP global feature importance) plt.show()第二种是时间步重要性。把sv在样本维度和特征维度上取绝对值均值可以得到每个历史时间步对预测的整体影响程度。time_imp np.abs(sv).mean(axis(0, 2)) plt.figure(figsize(8, 3)) plt.plot(range(1, SEQ_LEN 1), time_imp, markero) plt.xlabel(time step) plt.ylabel(mean |SHAP|) plt.title(SHAP importance across time) plt.show()如果序列最后的几个时间步贡献明显高于早期时间步说明模型主要依赖近期信息如果所有时间步都很重要说明长期依赖对当前预测不可忽略。第三种是单个样本的特征贡献方向。以测试集第 2 个样本为例将每个特征的 SHAP 值在时间步上取均值得到这个样本中每个特征对预测值的拉高或拉低效果。sample_idx 1 effects sv[sample_idx].mean(axis0) plt.figure(figsize(8, 3)) plt.bar(feature_names, effects) plt.title(fsingle sample feature effects, sample {sample_idx}) plt.show()SHAP 值为正表示该特征将预测值推向更高的方向SHAP 值为负表示将预测值推向更低的方向绝对值越大贡献越强。5.4 用 SHAP 结果指导模型调优SHAP 不只是用来画图的它应该反过来参与模型迭代。如果某个特征在所有样本上的 SHAP 绝对值都很小说明该特征对预测几乎没有帮助可以考虑删除减少模型输入维度降低过拟合风险。如果某个特征在测试集上的 SHAP 方向与业务常识相反需要排查特征是否在数据采集阶段出现了延迟或错误。比如传感器故障导致某段时间数据异常模型学到的可能是错误的局部规律。如果时间步重要性显示模型过度依赖最近 1 到 2 个时间步而业务上真正需要的是更早期的状态那么可以考虑增大窗口长度、加强 CNN 对局部特征的提取或者引入注意力机制。6. 常见问题排查6.1 Conv1d 输入维度报错现象Expected 3D input (batch, channels, length), got (batch, length, features)原因是 PyTorch 的Conv1d要求输入维度是(B, C, L)而数据构造阶段生成的是(B, L, F)。解决方式是在进入卷积层之前执行permute(0, 2, 1)。在 GRU 之前还要再执行一次反向permute(0, 2, 1)。检查维度是否正确的最快方式是在forward里临时加一行print(x.shape)观察每一层前后的形状变化。6.2 GRU 隐状态使用错误现象模型训练时输出形状不是(B, 1)或者预测结果没有变化。h_n的形状是(num_layers, B, hidden_size)。取最后一层时使用h_n[-1]而不是h_n。如果写成self.fc(h_n)线性层输入的维度会变成(num_layers, B, hidden_size)与期望的(B, hidden_size)不一致。6.3 SHAP 计算慢或内存不足现象执行explainer.shap_values时耗时很长或者 CPU/GPU 内存被占满。原因通常是背景数据太多或者测试样本一次传入太多。解决方式是把背景数据控制在 128 条以内测试样本也可以分批计算最后拼接结果。batch_size_shap 16 shap_parts [] for i in range(0, test_sample.shape[0], batch_size_shap
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻