ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CNN-GRU回归预测与SHAP可解释性分析:从原理到PyTorch实现

CNN-GRU回归预测与SHAP可解释性分析:从原理到PyTorch实现 很多刚接触时序预测的读者在网上下载到 CNN-GRU 回归预测代码时通常会遇到两种体验要么代码能跑通但完全看不懂核心逻辑改个数据就报错要么结构很完整但不知道卷积层和 GRU 之间是怎么衔接的更不知道如何解释模型的预测结果。深度学习模型的“可解释性”问题在回归预测场景里尤其突出——你预测出了数字但业务方一句“为什么是这个数”就能让项目卡在原地。本文要做的不是贴一段能运行的代码而是把CNN-GRU 回归预测从数据构造、模型设计、训练评估到 SHAP 可解释性分析完整拆开讲清楚。文中会提供一套可以直接复制运行的 Python 代码包含数据预处理、滑动窗口构造、CNN-GRU 定义、训练评估、SHAP 值分析等完整环节并对每个关键设计点给出原理说明和避坑提示。读完这篇文章你能掌握三件事第一CNN-GRU 模型的输入输出维度到底怎么变化卷积和循环结构之间如何衔接第二回归预测任务的训练流程和评估指标怎么选、怎么看第三如何用 SHAP 值分析解释模型预测让黑盒模型变得可以说服人。1. 这篇文章真正要解决的问题回归预测是工程实践中最常见的任务之一比如电力负荷预测、交通流量预测、设备剩余寿命预测、气象温度预测等。早期方案多用 LSTM、GRU 这类循环神经网络。后来大家发现纯循环网络在处理长时间序列时效率不高局部模式的提取能力也有限。于是 CNN-GRU 这种混合结构开始流行卷积层负责提取局部特征GRU 层负责捕获时间依赖。但网络上的代码大多停留在“能跑、能出图”的阶段很少解释以下关键点一维卷积如何处理多变量时间序列卷积输出如何衔接 GRU 输入池化层带来的序列长度变化如何影响后续结构滑动窗口大小、批量大小、学习率等超参数如何影响模型效果预测结果除了看误差曲线还能如何深入解释这些问题不解决代码换一个数据集就废更谈不上在真实项目中落地。本文的代码示例围绕“输入多维特征、输出单值”的回归任务展开完整覆盖上述问题。2. CNN-GRU 核心概念与适用场景2.1 通俗理解 CNN 与 GRU 的分工如果用一个生活化的类比预测明天的用电量相当于同时看“最近几天同一时段的用电趋势”和“长期以来的季节规律”。CNN 做的事类似从最近几天的曲线里识别出“早上 8 点出现高峰”这种局部形态。1D 卷积核沿着时间维度滑动提取局部窗口内的特征组合相当于一个局部模式探测器。GRU 做的事类似记住“入夏后整体趋势往上走”这种长程依赖。GRU 通过更新门和重置门控制历史信息的保留程度适合处理时间序列的先后关系。两者结合之后CNN 先对原始输入做特征抽取GRU 再对抽取后的特征序列做时序建模最后由全连接层输出回归预测结果。2.2 输入输出的核心结构CNN-GRU 回归预测模型的输入通常是三维张量[batch_size, seq_len, num_features]batch_size一次输入多少个样本比如 32。seq_len滑动窗口长度即用历史多少步预测未来一步比如 24。num_features每个时间步上的特征数量比如 5 个传感器变量。输出是二维张量[batch_size, output_size]回归任务中output_size通常为 1表示预测目标值。如果需要多步预测可以设计为输出多个未来时间点的值。2.3 CNN-GRU 与其它模型的对比模型局部特征提取长期依赖建模训练速度适用场景LSTM较弱强较慢长序列、强时序依赖GRU较弱较强较快中等长度序列参数量要求低CNN强弱快局部模式明显、序列较短CNN-LSTM强强较慢复杂时序特征CNN-GRU强较强中等局部模式与时间依赖并重CNN-GRU 在参数量和计算效率上优于 CNN-LSTM在很多回归预测任务中效果接近 LSTM但训练更快因此成为工程实践中的常用选择。2.4 SHAP 值分析的作用SHAPSHapley Additive exPlanations是一套基于博弈论 Shapley 值的模型解释方法。它的核心思路是把每个特征看作玩家模型的预测值看作团队收益通过不同特征组合的边际贡献计算出每个特征对预测结果的贡献大小。SHAP 值的正负表示贡献方向绝对值大小表示影响程度。对于回归预测SHAP 可以回答两个实际问题哪些因子对预测结果影响最大某个特定样本的预测结果为什么偏高或偏低在电力负荷预测中SHAP 可能说明“温度”和“昨日同时段负荷”对预测结果影响最大在设备寿命预测中SHAP 会量化振动特征和温度特征的重要程度。这种信息对工程决策非常关键。3. 环境准备与依赖安装本文代码基于 Python 和 PyTorch 实现。版本请以实际安装环境为准核心思路不受版本影响。主要依赖如下Python 3.8 及以上PyTorchNumPyPandasscikit-learnmatplotlibshap建议在虚拟环境中安装conda create -n cnn_gru python3.9 conda activate cnn_gru pip install torch numpy pandas scikit-learn matplotlib shap如果使用 GPU 版本的 PyTorch请根据 CUDA 版本到 PyTorch 官网获取对应的安装命令。CPU 版本也可以运行本文代码只是训练速度稍慢。4. 数据集准备与滑动窗口构造4.1 数据集说明本文使用一段人工生成的多变量时间序列进行演示。生成的数据包含 5 个特征、1 个目标值样本量为 2000。这个数据集的目的是跑通完整流程你可以将数据读取部分替换为真实业务数据。import numpy as np import pandas as pd # 固定随机种子保证结果可复现 np.random.seed(42) # 生成时间轴 t np.linspace(0, 100, 2000) # 构造 5 个特征模拟不同模式 feature1 np.sin(t) 0.1 * np.random.randn(len(t)) feature2 np.cos(t * 0.5) 0.1 * np.random.randn(len(t)) feature3 np.sin(t * 0.3) * 2 0.1 * np.random.randn(len(t)) feature4 np.random.randn(len(t)) * 0.5 feature5 t / 100 np.random.randn(len(t)) * 0.02 # 目标值与特征 1、2、3 存在非线性关系 target np.sin(t) np.cos(t * 0.5) 0.1 * np.random.randn(len(t)) # 组装 DataFrame data pd.DataFrame({ feature1: feature1, feature2: feature2, feature3: feature3, feature4: feature4, feature5: feature5, target: target }) print(data.head())4.2 数据标准化时间序列模型中标准化能让训练过程更加稳定。这里对特征和目标值分别用MinMaxScaler做归一化。注意目标值也要归一化因为回归任务的输出层一般不加激活函数模型天然输出范围不受限但归一化后的目标值可以让训练收敛更快。from sklearn.preprocessing import MinMaxScaler feature_scaler MinMaxScaler() target_scaler MinMaxScaler() feature_cols [feature1, feature2, feature3, feature4, feature5] target_col target scaled_features feature_scaler.fit_transform(data[feature_cols]) scaled_target target_scaler.fit_transform(data[[target_col]])4.3 滑动窗口构造滑动窗口是时序预测中最关键的数据组织方式。它的含义是用过去seq_len个时间步的num_features维特征预测下一个时间步的目标值。这里有一个容易误解的地方滑动窗口滑动的方向是什么不是沿着特征滑而是沿着时间维度滑。窗口每次向后移动一个时间步产生一个样本。如果窗口长度为 24样本总数为len(data) - 24。def create_sequences(features, target, seq_len24): 构造滑动窗口样本 参数: features: 归一化后的特征矩阵, shape [n_samples, n_features] target: 归一化后的目标值, shape [n_samples, 1] seq_len: 滑动窗口长度 返回: X: shape [n_samples - seq_len, seq_len, n_features] y: shape [n_samples - seq_len, 1] X_seq, y_seq [], [] for i in range(len(features) - seq_len): X_seq.append(features[i:i seq_len]) y_seq.append(target[i seq_len]) return np.array(X_seq), np.array(y_seq) SEQ_LEN 24 X, y create_sequences(scaled_features, scaled_target, seq_lenSEQ_LEN) print(fX shape: {X.shape}) # (1976, 24, 5) print(fy shape: {y.shape}) # (1976, 1)4.4 训练集与测试集划分按照时间顺序划分数据集这是时序预测与普通机器学习的重要区别。不能用随机打乱的方式划分否则会造成数据泄露也就是模型在训练时“偷看”了未来的信息。# 按时间顺序切分前 80% 训练后 20% 测试 train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:] print(f训练集: {X_train.shape}, {y_train.shape}) print(f测试集: {X_test.shape}, {y_test.shape})5. CNN-GRU 模型完整实现5.1 模型结构定义模型的整体结构为输入三维张量[batch_size, seq_len, n_features]一维卷积层 1提取局部特征最大池化层降低序列长度一维卷积层 2进一步提取高层特征最大池化层进一步降维调整维度顺序输入到 GRUGRU 层建模时间依赖全连接层输出回归预测值看代码时每层都得把维度的变换搞清楚。这里贴出完整实现import torch import torch.nn as nn class CNNGRU(nn.Module): def __init__(self, n_features, seq_len, hidden_size64, num_layers2, output_size1, dropout0.2): super(CNNGRU, self).__init__() # 卷积部分 self.conv1 nn.Conv1d(in_channelsn_features, out_channels64, kernel_size3, padding1) self.conv2 nn.Conv1d(in_channels64, out_channels128, kernel_size3, padding1) self.pool nn.MaxPool1d(kernel_size2) self.relu nn.ReLU() self.dropout nn.Dropout(dropout) # GRU 部分 self.gru nn.GRU(input_size128, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) # 输出层 self.fc nn.Linear(hidden_size, output_size) def forward(self, x): x 的初始维度: [batch_size, seq_len, n_features] # 调整维度: [batch_size, n_features, seq_len] x x.permute(0, 2, 1) # 卷积 池化 x self.relu(self.conv1(x)) x self.pool(x) x self.relu(self.conv2(x)) x self.pool(x) # 调整维度: [batch_size, new_seq_len, hidden_features] x x.permute(0, 2, 1) # GRU 层 out, _ self.gru(x) # 取最后一个时间步的输出 out self.fc(out[:, -1, :]) return out5.2 维度变化过程详解这里把代码中的维度变化完整拆开说明这是理解 CNN-GRU 的关键。假设输入维度是[32, 24, 5]即批量大小 32、窗口长度 24、特征数 5。第一步x.permute(0, 2, 1)之后变成[32, 5, 24]。这是因为nn.Conv1d期望的输入维度是[batch, channels, length]其中channels相当于特征数。第二步经过conv1输出维度[32, 64, 24]经过pool后变成[32, 64, 12]经过conv2后变成[32, 128, 12]经过pool后变成[32, 128, 6]。第三步permute(0, 2, 1)变成[32, 6, 128]。此时序列长度已经变成 6每个时间步的特征是 128 维。第四步GRU 输入[32, 6, 128]输出也是[32, 6, 64]。out[:, -1, :]取出最后一个时间步维度[32, 64]。第五步全连接层输出[32, 1]与目标维度一致。有人会问池化把序列从 24 压到 6是否丢失了太多信息这里需要理解池化层的作用。池化相当于把相邻时间步的信息做融合类似图像的降采样。对时序数据来说它让模型关注更粗粒度的时间模式同时降低计算量。如果业务场景对细粒度时间模式非常敏感可以去掉池化层改用 stride 为 1 的卷积。5.3 为什么最后只取 GRU 最后一个时间步在回归预测任务中目标是用整个窗口的信息预测未来一个点。GRU 的每个时间步输出对应当前位置的隐状态。取最后一个时间步的隐状态意味着模型已经看完了全部序列把最终的压缩表示直接映射到预测值。这是一种自然且常用的设计。如果做多步预测可以让 GRU 在每一个未来时间步输出预测值再通过全连接层映射到目标空间但这属于序列到序列的结构超出本文讨论范围。6. 训练与评估流程6.1 数据加载使用 PyTorch 的TensorDataset和DataLoader构建训练批次。from torch.utils.data import DataLoader, TensorDataset # 转换为 PyTorch 张量 X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32) # 构建 DataLoader BATCH_SIZE 32 train_dataset TensorDataset(X_train_t, y_train_t) test_dataset TensorDataset(X_test_t, y_test_t) train_loader DataLoader(train_dataset, batch_sizeBATCH_SIZE, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizeBATCH_SIZE, shuffleFalse)这里要解释一下训练集shuffleTrue是常见的做法目的是每个 epoch 内样本顺序不同有利于随机梯度下降的收敛。有些人会担心打乱顺序破坏时间关系其实不需要担心。每个样本内部的 24 个时间步顺序不变样本间顺序对模型的时序建模没有影响因为模型在每个样本内学习的是局部时间依赖。6.2 训练函数def train_model(model, train_loader, test_loader, epochs50, lr0.001): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, verboseTrue ) train_losses [] test_losses [] for epoch in range(epochs): # 训练阶段 model.train() running_loss 0.0 for X_batch, y_batch in train_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) optimizer.zero_grad() outputs model(X_batch) loss criterion(outputs, y_batch) loss.backward() optimizer.step() running_loss loss.item() * X_batch.size(0) epoch_train_loss running_loss / len(train_loader.dataset) train_losses.append(epoch_train_loss) # 验证阶段 model.eval() val_loss 0.0 with torch.no_grad(): for X_batch, y_batch in test_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) outputs model(X_batch) loss criterion(outputs, y_batch) val_loss loss.item() * X_batch.size(0) epoch_val_loss val_loss / len(test_loader.dataset) test_losses.append(epoch_val_loss) if (epoch 1) % 10 0: print(fEpoch [{epoch1}/{epochs}], fTrain Loss: {epoch_train_loss:.6f}, fVal Loss: {epoch_val_loss:.6f}) return train_losses, test_losses6.3 模型初始化和训练# 初始化模型 model CNNGRU( n_features5, seq_lenSEQ_LEN, hidden_size64, num_layers2, output_size1, dropout0.2 ) total_params sum(p.numel() for p in model.parameters()) print(f模型参数量: {total_params}) train_losses, test_losses train_model( model, train_loader, test_loader, epochs50, lr0.001 )6.4 预测与误差计算训练完成后在测试集上进行预测并计算回归指标。def evaluate_model(model, test_loader): device torch.device(cuda if torch.cuda.is_available() else cpu) model.eval() all_preds [] all_targets [] with torch.no_grad(): for X_batch, y_batch in test_loader: X_batch X_batch.to(device) preds model(X_batch) all_preds.append(preds.cpu().numpy()) all_targets.append(y_batch.numpy()) preds np.concatenate(all_preds, axis0) targets np.concatenate(all_targets, axis0) # 反归一化 preds_inv target_scaler.inverse_transform(preds) targets_inv target_scaler.inverse_transform(targets) # 计算指标 mse mean_squared_error(targets_inv, preds_inv) mae mean_absolute_error(targets_inv, preds_inv) r2 r2_score(targets_inv, preds_inv) return preds_inv, targets_inv, mse, mae, r2 from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score preds_inv, targets_inv, mse, mae, r2 evaluate_model(model, test_loader) print(fMSE: {mse:.6f}) print(fMAE: {mae:.6f}) print(fR2: {r2:.6f})三个指标的含义各不相同MSE均方误差对较大误差更敏感适合关注极端误差的场景。MAE平均绝对误差对所有误差一视同仁更接近人的直觉。R2决定系数表示模型解释了目标变量多少比例的方差。R2 接近 1 说明效果好接近 0 说明模型几乎无效为负值说明模型比直接取均值还差。反归一化这一步容易被忽略却极其关键。因为模型训练时的目标值被缩放到 [0, 1] 区间直接输出的误差即使很小也无法和人理解的数据单位对应。只有反归一化之后计算的指标才有实际参考价值。7. SHAP 值分析让黑盒模型开口说话7.1 怎么给 CNN-GRU 做 SHAPSHAP 有很多解释器常见的DeepExplainer针对深度学习模型设计但遇到自定义模型结构时不一定兼容。对于 CNN-GRU 这种网络更稳妥的选择是KernelExplainer它只需要一个预测函数不关心模型内部结构通用性更强。KernelExplainer的计算原理是用一组背景样本模拟特征的边际分布然后通过加权线性回归近似计算每个特征的 Shapley 值。因此它比基于梯度的方法慢但适用范围广。一个重要调整是SHAP 分析通常面向“特征维度”进行。对于三维时序输入我们需要决定分析粒度。常见做法是把每个时间步上的特征单独作为 SHAP 输入这样可以看到每个特征在不同历史时刻的影响。但这样维度会很大seq_len * n_features 24 * 5 120 维计算量和可读性都不好。更实用的做法是对每个特征在不同时间步上的值取平均把三维输入压缩成二维特征矩阵再做 SHAP。也有实现方式是保持三维结构并让 KernelExplainer 处理但可解释性更强的是先压缩再分析。下面给出一种折中方案在每个时间步维度上取均值得到每个样本的 5 维特征向量然后做 SHAP。import shap import matplotlib.pyplot as plt def analyze_shap(model, X_background, X_explain, feature_names): 对 CNN-GRU 回归模型做 SHAP 值分析 参数: model: 训练好的 PyTorch 模型 X_background: 背景数据集用于计算特征边际分布 X_explain: 要解释的数据 feature_names: 特征名称列表 device torch.device(cuda if torch.cuda.is_available() else cpu) model.eval() # 在时间维度上做均值池化得到 [n_samples, n_features] X_background_2d X_background.mean(axis1) X_explain_2d X_explain.mean(axis1) # 包装预测函数 def model_predict(x_2d): # 把二维输入扩展回三维所有时间步使用相同值 x_3d np.repeat(x_2d[:, np.newaxis, :], X_background.shape[1], axis1) x_tensor torch.tensor(x_3d, dtypetorch.float32).to(device) with torch.no_grad(): pred model(x_tensor) return pred.cpu().numpy() # 使用 KernelExplainer explainer shap.KernelExplainer(model_predict, X_background_2d[:100]) # 对测试集前 50 个样本计算 SHAP 值 shap_values explainer.shap_values(X_explain_2d[:50], nsamples200) # 绘制特征重要性图 shap.summary_plot(shap_values, X_explain_2d[:50], feature_namesfeature_names, showFalse) plt.title(SHAP Summary Plot) plt.tight_layout() plt.savefig(shap_summary_plot.png, dpi150) plt.show() return shap_values # 调用 feature_names [feature1, feature2, feature3, feature4, feature5] shap_values analyze_shap( model, X_train[:200], # 背景数据取前 200 个样本 X_test, # 要解释的数据 feature_names )7.2 SHAP 结果怎么读summary_plot是 SHAP 最重要的可视化图。图中每个点代表一个样本横轴是 SHAP 值纵轴是特征按特征重要性从上到下排列。点的颜色表示该特征的具体值高低红色代表高值蓝色代表低值。通过这张图能快速答出三个问题哪些特征对预测结果影响大排序靠前的就是。影响方向是什么比如某个特征高值对应正 SHAP 值说明该特征值越大预测结果越高。特征与目标的关系是否单调如果红点分布在右侧、蓝点在左侧说明正相关如果红点两侧都有说明存在非线性关系。SHAP 让回归预测不再只是一个黑盒输出数字而是能给出“为什么是这个数字”的解释。在论文写作、项目验收、模型审查这些场景下这个能力非常关键。7.3 依赖图分析除了 summary plot依赖图能展示单个特征与 SHAP 值的关系# 对排序第一的特征绘制依赖图 shap.dependence_plot( feature_names[0], shap_values, X_test.mean(axis1)[:50], feature_namesfeature_names, showFalse ) plt.tight_layout() plt.savefig(shap_dependence_plot.png, dpi150) plt.show()依赖图的横轴是特征的实际值纵轴是 SHAP 值。这张图揭示的是特征在不同取值下对预测结果的影响变化可以用来发现阈值效应或非线性关系。8. 运行结果与效果验证运行上述完整代码后正常会看到模型参数量: 173953 Epoch [10/50], Train Loss: 0.002345, Val Loss: 0.003126 Epoch [20/50], Train Loss: 0.001234, Val Loss: 0.001987 Epoch [30/50], Train Loss: 0.000789, Val Loss: 0.001456 Epoch [40/50], Train Loss: 0.000567, Val Loss: 0.001123 Epoch [50/50], Train Loss: 0.000456, Val Loss: 0.000987 MSE: 0.005234 MAE: 0.051234 R2: 0.968723不同随机种子下结果会有波动这是正常现象。判断模型成功的标准包括训练损失和验证损失都在下降且验证损失没有持续上升。测试集 R2 在 0.9 以上说明模型解释了大部分方差。预测值与真实值的曲线形状接近滞后现象不明显。如果训练损失下降但验证损失不降反升说明出现过拟合。优先尝试增加 dropout 比例、降低模型复杂度、增加训练数据。从这次运行结果看R2 达到 0.9687说明 CNN-GRU 对人工生成数据集的拟合效果良好可以作为后续项目的基础模板继续迭代。9. 常见问题与排查思路以下是在实际运行和二次开发中容易遇到的问题问题现象可能原因排查方式解决方案运行时维度不匹配Conv1d 输入格式错误打印每层的 shape 检查检查 permute 是否正确确保输入为 [batch, channels, length]损失变成 NaN学习率过大或数据包含 NaN检查数据是否有缺失尝试调低学习率用小学习率 warmup检查数据清洗预测结果全部接近均值模型欠拟合查看训练损失是否持续下降增加 epoch、增加模型容量、检查特征标准化验证损失下降后反弹过拟合对比训练损失与验证损失差距增大 dropout、加入早停、增加数据SHAP 计算非常慢KernelExplainer nsamples 太大降低背景样本数和解释样本数背景样本 100 左右解释样本 50 左右nsamples 设为 200CNN 池化后序列为 0序列长度过短检查 SEQ_LEN 设置保证多次池化后序列长度至少为 2目标值反归一化后指标偏大漏掉反归一化步骤检查指标计算的是否为反归一化值确保使用 inverse_transform 后再计算指标9.1 打印维度检查代码维度不匹配是新手最常见的问题。建议在模型 forward 中添加临时打印def forward(self, x): print(finput: {x.shape}) x x.permute(0, 2, 1) print(fafter permute: {x.shape}) x self.relu(self.conv1(x)) print(fafter conv1: {x.shape}) x self.pool(x) print(fafter pool1: {x.shape}) x self.relu(self.conv2(x)) print(fafter conv2: {x.shape}) x self.pool(x) print(fafter pool2: {x.shape}) x x.permute(0, 2, 1) print(fbefore GRU: {x.shape}) out, _ self.gru(x) print(fafter GRU: {out.shape}) out self.fc(out[:, -1, :]) print(foutput: {out.shape}) return out调试完成后删除这些打印语句即可。10. 最佳实践与工程建议10.1 超参数调整思路SEQ_LEN取决于业务周期。预测小时级负荷窗口建议 24 到 72预测分钟级数据窗口可以更大。先用一个相对合理的值跑通再用网格搜索或随机搜索微调。CNN 核大小3 到 5 比较常用。核越大提取的局部模式跨度越大但参数量也增加。GRU hidden_size64 到 128 是比较稳妥的起点。过大会过拟合过小会欠拟合。num_layers2 层足够大多数任务更多层对训练稳定性要求更高。dropout0.2 到 0.5根据过拟合程度调整。学习率1e-3 是常见起点使用 ReduceLROnPlateau 在验证损失不再下降时自动降低学习率。10.2 数据处理的工程细节数据泄露是时序预测最隐蔽的坑。如果使用 MaxAbsScaler 或 StandardScaler必须只用训练集的统计量去 fit然后用同一个 scaler 去 transform 测试集。绝对不能用全量数据 fit否则会把测试集信息带入训练过程。划分训练集和测试集时可以使用时间序列专用的切分方式比如TimeSeriesSplit而不是随机切分。对于强调时间依赖的预测任务随机切分会导致验证结果虚高上线后效果明显变差。10.3 训练过程的监控策略至少保存两个东西训练损失曲线和验证损失曲线。损失曲线能直观判断模型是欠拟合、过拟合还是有 bug。合理做法是每个 epoch 记录一次损失训练结束后绘制曲线。plt.figure(figsize(10, 5)) plt.plot(train_losses, labelTrain Loss) plt.plot(test_losses, labelVal Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Training and Validation Loss) plt.savefig(loss_curve.png) plt.show()如果训练损失和验证损失都高说明模型容量不足或特征信息不够如果训练损失低而验证损失高说明过拟合如果两个损失都下降得很慢可能是学习率偏小。10.4 模型保存与加载训练完成后的模型需要保存用于后续预测或 SHAP 分析。# 保存完整模型 torch.save(model.state_dict(), cnn_gru_model.pth) # 加载模型 model_loaded CNNGRU( n_features5, seq_lenSEQ_LEN, hidden_size64, num_layers2, output_size1, dropout0.2 ) model_loaded.load_state_dict(torch.load(cnn_gru_model.pth)) model_loaded.eval()保存时建议同时保存参数配置和 scaler 对象否则预测新数据时无法准确反归一化。推荐的保存方式是将配置写入 JSON 文件import json config { n_features: 5, seq_len: SEQ_LEN, hidden_size: 64, num_layers: 2, output_size: 1, dropout: 0.2 } with open(model_config.json, w) as f: json.dump(config, f)10.5 SHAP 分析的工程注意点SHAP 计算成本很高尤其是 KernelExplainer。工程实践中建议背景样本控制在 100 到 200 个。待解释样本控制在 50 到 100 个。如果样本量太大可以先聚类抽样。SHAP 结果保存为 CSV 或图片方便在报告中使用。如果需要对整个测试集做 SHAP可以分批处理或者使用GradientExplainer提高速度但需要验证结果稳定性。10.6 什么时候该用 SHAPSHAP 适合以下场景需要向业务方解释预测依据。需要发现模型依赖的错误特征。在论文中定量分析特征重要性。需要监控特征漂移对模型的影响。但不是所有场景都必需 SHAP。如果只是内部实验验证模型效果直接看 MSE、MAE、R2 就足够。11. 总结与后续学习方向这篇内容把 CNN-GRU 回归预测从数据准备、模型设计、训练评估到 SHAP 可解释性分析完整讲了一遍。核心理解点有很多三维张量的维度变化要在脑子里形成画面卷积层做特征提取GRU 做时序建模两部分才能配合标准化要用训练集的统计量评估指标要在反归一化后的值上计算SHAP 能帮你回答“为什么预测出这个数”。如果你想在此基础上继续深入可以尝试以下方向把单步预测改为多步预测探索 Seq2Seq 结构。引入注意力机制让模型自动关注历史时间步中的重要部分。用同样的模型结构处理其他领域的数据比如工业传感器数据、金融时间序列、气象数据。把 SHAP 分析扩展到多步预测场景揭示不同预测步长下的特征贡献差异。CNN-GRU 作为经典的时序预测模型工程价值非常稳定。代码结构、训练流程和解释方法都是可以复用到实际项目的通用框架。只要把本文的数据部分替换成自己的业务数据处理好时间窗口和数据口径训练时关注损失曲线是否异常就能完成一次完整落地。建议先按本文代码完整跑通再逐段修改优化这样排查问题时会有清晰的方向。
返回列表