简介这份资源面向从事时间序列预测的机器学习学习者与工程实践者提供一套基于Python与TensorFlow实现的CNN-GRU混合深度学习算法。它利用卷积神经网络提取局部特征、门控循环单元捕捉时序依赖可处理风电功率、电力负荷等场景的预测任务并支持单输入单步、单输入多步、多输入单步、多输入多步四种预测模式兼顾不同输入输出需求。压缩包共8个文件约4.92MB包含py主程序、xlsx与csv测试数据集、pdf与md使用说明、txt依赖清单等代码中文注释清晰便于理解实现细节。算法内置MSE、RMSE、R2、MAE、MAPE多个评估指标可从不同角度衡量预测精度帮助调参与优化。数据集格式支持CSV和Excel可直接替换为自己的数据快速验证效果。目前已有67人学习下载适合希望掌握CNN-GRU时序建模并落地应用的读者参考。1. 从一段风速数据说起CNN-GRU 到底在时序预测里扮演什么角色手里有一份 15 分钟采样的风速序列领导要你预测未来 24 小时。直接上 LSTMMAE 卡在 0.8 下不去换成 Transformer显存炸了训练一轮要半小时。这时候 CNN-GRU 往往是那个刚刚好的方案——用卷积神经网络CNN先把局部时序模式压出来再交给门控循环神经网络GRU建模长程依赖。它不追新但在工业时序预测里活得很好光伏出力、负荷曲线、设备振动、销量波动凡是有局部突变又有周期惯性的序列它都能吃。这篇讲的是用 Python TensorFlow 把 CNN-GRU 从零搭起来、跑通、调参、避坑的完整路径。适合已经会写 Python、装过 TensorFlow、但一上手时序预测就翻车的同学。读完你能拿到一份可复现的建模流程知道每个参数为什么这么设以及哪几个地方最容易让你白跑一晚上。2. 为什么是 CNN 接 GRU结构选型与数据窗口设计2.1 卷积层在时序里到底卷什么很多人第一次看到CNN 做时序会懵卷积不是处理图像的吗关键在于把一维序列当成高度为 1 的图像。假设输入是(batch, timesteps, features)一维卷积核在 timesteps 这个维度上滑动每次只看一个局部窗口。这个窗口内的模式——比如连续三个点陡升、连续五个点平稳——就是卷积要提取的局部形状。这带来两个实际好处。第一卷积核权重共享参数量远小于同感受野的全连接层小数据集不容易过拟合。第二卷积可以并行计算比 RNN 逐时间步串行快得多。所以常见做法是前面堆一两层 Conv1D 做特征压缩和局部模式提取把序列长度降下来再送进 GRU。选 GRU 而不是 LSTM理由很直接GRU 只有更新门和重置门两个门参数量约为 LSTM 的 3/4在中小规模时序任务上精度几乎持平训练更快、更不容易过拟合。如果你的数据量在几万条以内GRU 通常是更稳的选择。2.2 滑动窗口把序列切成监督学习样本时序预测不能直接把整条序列喂进去得先做窗口化。核心是两个参数input_width回看多少步和label_width预测多少步。下面这段代码是我常用的窗口构造函数支持多特征输入和多步预测。import numpy as np def make_windows(data, input_width, label_width, shift1): data: shape (n_samples, n_features) 的二维数组 input_width: 回看步数 label_width: 预测步数 shift: 预测起点相对输入末尾的偏移通常为 1 返回: X shape (N, input_width, n_features), y shape (N, label_width, n_features) X, y [], [] total len(data) start 0 # 保证窗口不越界 while start input_width shift label_width total: x_slice data[start : start input_width] y_start start input_width shift - 1 y_slice data[y_start : y_start label_width] X.append(x_slice) y.append(y_slice) start 1 return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)逻辑说明shift1表示用第 t 步之前的input_width个点预测从第 t1 步开始的label_width个点。start 1是逐点滑动样本量最大化如果数据量太大可以改成start stride做降采样。参数上input_width一般取 24 到 168对应一天到一周的小时数据label_width按业务需求定多步预测时建议先用 1 步验证模型能学会再逐步加长。注意窗口化之前一定要先做归一化且归一化参数只能用训练集统计量否则会引入未来信息泄漏这是时序预测最隐蔽的翻车点。2.3 数据划分不能随机打乱时序数据的训练/验证/测试集必须按时间顺序切绝不能train_test_split(shuffleTrue)。常见做法是 7:1:2 或 8:1:1 按时间先后切分。如果做多步预测还要在切分边界留出input_width label_width的缓冲避免验证集第一个样本的输入窗口落在训练集里。3. 用 TensorFlow 搭一个能跑的 CNN-GRU逐层配置与训练3.1 模型结构Conv1D 到 GRU 的层序下面是一个我反复用过的基线结构输入(input_width, n_features)输出(label_width, n_features)。用 Keras Functional API 写方便后面改多输入多输出。import tensorflow as tf from tensorflow.keras import layers, Model def build_cnn_gru(input_width, n_features, label_width, conv_filters(64, 64), kernel_size3, gru_units64, dropout0.2, lr1e-3): inputs layers.Input(shape(input_width, n_features), nameseq_input) x inputs # 两层一维卷积paddingsame 保持长度便于堆叠 for i, f in enumerate(conv_filters): x layers.Conv1D(filtersf, kernel_sizekernel_size, paddingsame, activationrelu, namefconv1d_{i})(x) x layers.BatchNormalization(namefbn_{i})(x) # 只在第一层后池化避免过度压缩 if i 0: x layers.MaxPooling1D(pool_size2, namepool_0)(x) x layers.Dropout(dropout, namedrop_conv)(x) x layers.GRU(gru_units, return_sequencesFalse, namegru)(x) x layers.Dropout(dropout, namedrop_gru)(x) x layers.Dense(label_width * n_features, namedense)(x) outputs layers.Reshape((label_width, n_features), nameoutput)(x) model Model(inputs, outputs, namecnn_gru) model.compile(optimizertf.keras.optimizers.Adam(learning_ratelr), lossmse, metrics[mae]) return model逻辑说明paddingsame让卷积输出长度不变这样两层卷积可以无缝堆叠。第一层后加MaxPooling1D(pool_size2)把序列长度减半既降计算量又扩大后续卷积的感受野。return_sequencesFalse表示 GRU 只输出最后一个时间步的隐状态直接接 Dense 做回归。如果要做序列到序列的多步预测且想保留时序信息可以改成return_sequencesTrue再接TimeDistributed(Dense)。参数说明conv_filters从 64 起步数据量小就降到 32kernel_size3是时序任务的默认值周期性强可以试 5 或 7gru_units一般 32 到 128超过 128 在小数据上基本是浪费dropout0.2是安全起点过拟合明显再往上加。3.2 训练配置早停、学习率与 batch sizecallbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6), ] model build_cnn_gru(input_width72, n_features1, label_width24) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size64, callbackscallbacks, verbose1, )EarlyStopping的patience10配合ReduceLROnPlateau的patience5是我在多数时序任务上的默认组合先让学习率降一次再等几个 epoch 确认没救才停。batch_size64是平衡速度和梯度稳定性的常用值序列很长或显存紧张就降到 32。注意restore_best_weightsTrue一定要开否则早停后拿到的是最后一轮而非最优权重这个坑我踩过不止一次。3.3 评估别只看 MSEMSE 对量纲敏感业务方看不懂。训练完至少补三个指标MAE、MAPE、以及分步误差。分步误差尤其重要——多步预测里第 1 步往往很准第 24 步可能已经飘了只看总体 MSE 会掩盖这个问题。def stepwise_mae(y_true, y_pred): # y shape: (N, label_width, n_features) err np.abs(y_true - y_pred).mean(axis(0, 2)) return err # 长度 label_width 的数组 pred model.predict(X_test) print(逐步 MAE:, stepwise_mae(y_test, pred))如果发现误差随步数快速上升说明模型的长程建模能力不足可以考虑加深 GRU、增大input_width或者改用return_sequencesTrue的序列到序列结构。4. 调参与排错CNN-GRU 时序预测的避坑清单4.1 现象验证 loss 比训练 loss 低很多原因最常见的是数据泄漏。归一化用了全量数据统计量或者窗口化时验证集的输入窗口和训练集重叠。另一个可能是 Dropout 在验证时被误开。解决归一化参数严格从训练集计算再 transform 验证和测试集检查窗口切分边界确认model.evaluate和predict时处于推理模式Keras 默认已处理但自定义训练循环要手动设trainingFalse。4.2 现象训练 loss 震荡剧烈迟迟不降原因学习率过大或 batch size 太小导致梯度噪声大。时序数据本身噪声高时也会这样。解决先把学习率降到 1e-4 试batch size 提到 64 或 128对输入做平滑移动平均或加BatchNormalization。如果数据里有明显异常值先做 winsorize 截断别指望模型自己扛。4.3 现象单步预测很准多步预测直接崩原因直接多步输出Dense 一次吐 label_width 个点时各步之间没有依赖约束模型倾向于输出均值。这是均值坍缩。解决两条路。一是改成自回归每次预测一步再喂回去但误差会累积二是用 seq2seq 结构GRU 设return_sequencesTrue解码端逐步输出。实践中我一般先用直接多步做基线再上 seq2seq 对比。4.4 现象换了随机种子结果差很多原因小数据集 初始化敏感。CNN-GRU 参数量虽不大但在几千条样本上仍可能不稳定。解决固定所有随机种子tf.random.set_seed、np.random.seed跑 3 到 5 次取平均。如果方差仍然很大说明数据量不够优先考虑简化模型或做数据增强加噪声、时间扭曲。4.5 现象GPU 利用率低训练慢原因tf.data没配好数据在 CPU 和 GPU 之间来回搬。或者序列太短卷积层并行度上不去。解决用tf.data.Dataset的prefetch和cache把窗口化后的数组缓存起来。示例train_ds (tf.data.Dataset.from_tensor_slices((X_train, y_train)) .shuffle(10000) .batch(64) .prefetch(tf.data.AUTOTUNE))cache()在数据能放进内存时效果显著放不下就跳过。prefetch(AUTOTUNE)让数据准备和 GPU 计算重叠基本是必开项。5. 把 CNN-GRU 用稳的几个进阶技巧5.1 多特征输入时的通道处理实际业务里很少只有单变量。风速预测会带温度、气压、历史功率负荷预测会带星期、节假日标记。多特征输入时n_features直接设成特征数即可CNN 会在通道维度上自动学习特征间交互。但要注意不同特征的量纲差异大时必须逐特征归一化不能整体减一个均值。from sklearn.preprocessing import StandardScaler scalers {} for i, col in enumerate(feature_cols): sc StandardScaler() train_scaled[:, i] sc.fit_transform(train[:, i].reshape(-1, 1)).ravel() scalers[col] sc时间特征小时、星期几建议做 sin/cos 编码而不是直接归一化否则 23 点和 0 点的数值距离会被错误放大。5.2 用残差连接稳住深层结构如果卷积层堆到 3 层以上加个残差跳连能明显改善梯度流动。做法很简单把输入通过一个 1x1 卷积对齐通道数再和卷积输出相加。def residual_block(x, filters, kernel_size3): shortcut layers.Conv1D(filters, 1, paddingsame)(x) y layers.Conv1D(filters, kernel_size, paddingsame, activationrelu)(x) y layers.BatchNormalization()(y) y layers.Conv1D(filters, kernel_size, paddingsame)(y) y layers.BatchNormalization()(y) y layers.Add()([shortcut, y]) return layers.Activation(relu)(y)这个块替换掉原来的两层 Conv1D在序列较长input_width 100时收益明显。5.3 验证模型真的学到了东西置换检验一个常被忽略的验证手段把测试集的输入在时间维度上随机打乱再喂给模型。如果预测误差和正常输入差不多说明模型根本没利用时序结构只是记住了均值。正常情况打乱后误差应该显著上升。X_shuffled X_test.copy() for i in range(len(X_shuffled)): np.random.shuffle(X_shuffled[i]) pred_shuffled model.predict(X_shuffled) print(正常 MAE:, np.abs(y_test - pred).mean()) print(打乱 MAE:, np.abs(y_test - pred_shuffled).mean())两者差距越大说明模型对时序依赖的利用越充分。这个检查花不了几分钟但能帮你判断模型是不是在假装工作。5.4 我自己的习惯每次开新任务我会先跑一个只有 Dense 的线性基线再跑单层 GRU最后才上 CNN-GRU。如果 CNN-GRU 比单层 GRU 提升不到 5%我会回头检查数据里到底有没有值得卷积提取的局部模式——很多时候问题不在模型而在特征工程没做到位。模型不是越复杂越好能解释清楚每一层在干什么比盲目堆结构重要得多。希望帮到你。本文还有配套的精品资源点击获取