
简介这份资源面向电力负荷预测方向的学习者与研究人员提供一套基于CNN-GRU-Attention混合神经网络的完整实现方案用于处理时间序列预测中的复杂非线性与长期依赖问题。模型将CNN的局部特征提取、GRU的长期记忆能力与注意力机制的权重分配相结合可应用于电力负荷、风电功率等场景。压缩包共3个文件包含1个py脚本、1个csv数据集和1个txt依赖说明整体约1.32MB分别对应模型代码、历史负荷数据与运行环境配置。目前已有445人学习下载适合具备一定深度学习基础、希望快速复现或二次开发混合预测模型的读者。通过该资源读者可获取数据预处理、模型构建、训练与评估的完整代码框架理解注意力机制在时序预测中的具体落地方式并借助依赖版本说明快速搭建一致环境减少调试成本。1. 电力负荷预测的混合网络从 load1.csv 到可复现的 CNN-GRU-Attention电力负荷预测这个场景做过的都知道难点不在模型有多深而在数据里的周期、趋势和突发扰动搅在一起单一模型很难同时吃下。这个资源包给了一个很直接的思路用 CNN 提局部时序特征用 GRU 记长期依赖再用 Attention 给关键历史时刻加权三件套拼成一个混合神经网络。包里就三样东西——load1.csv历史负荷数据、依赖包版本_2.txt环境清单、CNN-GRU-Attention.py主程序结构干净没有多余的脚手架。它适合两类人一类是刚接触时序预测、想找一个能跑通的完整例子练手另一类是已经在做负荷预测、想对比混合模型和单 GRU 差距的从业者。下面我按「数据长什么样 → 模型怎么搭 → 怎么跑起来 → 坑在哪」的顺序拆一遍。2. 数据与模型结构CNN 提特征、GRU 记依赖、Attention 加权2.1 load1.csv 的读取与预处理拿到load1.csv第一件事不是急着喂模型而是先看清楚它的列结构和时间粒度。电力负荷数据常见的是「时间戳 负荷值」两列也可能是多列有功、无功、温度等。用 Pandas 读进来先做一次体检import pandas as pd import numpy as np # 读取负荷数据parse_dates 把时间列直接转成 datetime df pd.read_csv(load1.csv, parse_dates[timestamp]) print(df.head()) print(df.describe()) print(df.isnull().sum()) # 先看缺失值分布 # 缺失值用前向填充负荷数据一般不会突变到 0 df[load] df[load].fillna(methodffill) # 归一化到 [0,1]MinMax 对负荷这种有界序列比 Z-score 更稳 from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() data scaler.fit_transform(df[[load]].values)这里几个参数值得说清楚。parse_dates不指定的话时间列就是字符串后面做滑窗会出错。缺失值处理上负荷数据我一般用前向填充而不是均值填充因为负荷有强连续性均值填充会人为制造一个不存在的「平均时刻」。归一化选 MinMax 而不是 StandardScaler是因为负荷值有明确上下界MinMax 能保留这个边界信息而且反归一化时不会因为方差估计偏差导致系统性偏移。滑窗构造监督样本是时序预测的核心步骤。假设用过去 24 个点预测下一个点def create_sequences(data, seq_len): X, y [], [] for i in range(len(data) - seq_len): X.append(data[i:iseq_len]) y.append(data[iseq_len]) return np.array(X), np.array(y) SEQ_LEN 24 X, y create_sequences(data, SEQ_LEN) # 按 8:2 切训练测试注意不能 shuffle时序数据打乱就泄露了 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:]SEQ_LEN这个参数不是拍脑袋定的。如果你的数据是小时级24 对应一天如果是 15 分钟级96 才是一天。窗口太短模型看不到日周期太长则训练慢且容易过拟合。我一般会先画一张自相关图ACF确认周期长度再定SEQ_LEN。2.2 CNN-GRU-Attention 三层结构的设计理由CNN-GRU-Attention.py里的模型主体是三层串联。先说为什么这么排而不是随便堆。CNN 这一层用的是一维卷积Conv1D不是图像里的二维卷积。一维卷积核在时间轴上滑动每个核负责捕捉一种局部模式比如「连续三个点上升」这种短时趋势。多个核并行就得到多组局部特征图。它的价值在于把原始序列里噪声和有效模式做一次初步分离相当于给后面的 GRU 减负。GRU 接在 CNN 后面接收的是已经被卷积提纯过的特征序列。GRU 相比 LSTM 少了一个门参数更少在负荷预测这种数据量不算特别大的场景下训练更快、更不容易过拟合。它的更新门和重置门决定了「记住多久以前的信息」和「忘掉多少当前输入」这正是负荷预测需要的长期依赖建模能力。Attention 放在 GRU 输出之后做的是加权求和。GRU 每个时间步都会输出一个隐状态但不是每个时刻对预测都同等重要——比如预测明天早高峰昨天早高峰的数据显然比凌晨三点的数据更关键。Attention 就是让模型自己学出这组权重import tensorflow as tf from tensorflow.keras import layers, Model def build_model(seq_len): inputs layers.Input(shape(seq_len, 1)) # CNN 提局部特征64 个卷积核核大小 3 x layers.Conv1D(filters64, kernel_size3, paddingsame, activationrelu)(inputs) x layers.MaxPooling1D(pool_size2)(x) # GRU 记长期依赖return_sequencesTrue 才能接 Attention x layers.GRU(64, return_sequencesTrue)(x) # Attention对 GRU 每个时间步输出打分再加权 attn layers.Dense(1, activationtanh)(x) attn layers.Softmax(axis1)(attn) x layers.Multiply()([x, attn]) x layers.Lambda(lambda t: tf.reduce_sum(t, axis1))(x) outputs layers.Dense(1)(x) model Model(inputs, outputs) model.compile(optimizeradam, lossmse) return modelfilters64和kernel_size3是常见起点核大小 3 意味着每次看连续三个时间点对小时级负荷够用。return_sequencesTrue是必须的否则 GRU 只输出最后一个时间步Attention 就没有序列可加权。Attention 这里用了一个 DenseSoftmax 的简化实现先给每个时间步算一个分数再归一化成权重最后乘回原特征求和。Lambda层做的是沿时间轴求和把加权后的序列压成一个向量送进全连接输出。提示如果你的 TensorFlow 版本较新tf.reduce_sum在 Lambda 里可能遇到图执行问题可以换成layers.Lambda(lambda t: tf.reduce_sum(t, axis1, keepdimsFalse))或直接用自定义层。3. 环境搭建与训练依赖包版本_2.txt 怎么用才不翻车3.1 依赖安装的版本对齐依赖包版本_2.txt这个文件的价值在于锁定版本。深度学习项目最怕的就是「代码没错但跑不起来」十有八九是版本漂移。常见做法是先建虚拟环境再按清单装python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r 依赖包版本_2.txt如果清单里写的是tensorflow2.x.x这种精确版本不要自作主张升级。TensorFlow 2.6 到 2.10 之间 API 变动不大但 2.16 之后 Keras 3 的引入会让很多旧写法失效比如layers.Lambda里用tf.reduce_sum的行为就有差异。我一般会先pip install -r装完再pip check看有没有依赖冲突。如果清单里没有写死版本比如只写了tensorflow那就要小心了。这种情况我建议手动指定一个稳定版本比如tensorflow2.10.0这个版本对 Keras 2 的兼容性最好网上大部分 CNN-GRU-Attention 的代码都是基于这个版本写的。3.2 训练循环与关键超参数模型搭好、数据备好训练本身不复杂但几个超参数直接决定能不能收敛model build_model(SEQ_LEN) # batch_size 32 是时序预测的常用起点 history model.fit( X_train, y_train, epochs50, batch_size32, validation_split0.1, callbacks[ tf.keras.callbacks.EarlyStopping(patience8, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience4) ] )batch_size32不是硬性规定但太小比如 8会让梯度噪声大、训练震荡太大比如 256则可能陷入局部最优。EarlyStopping的patience8意思是验证损失连续 8 轮不降就停restore_best_weightsTrue保证拿回的是最优轮次的权重而不是最后一轮的。ReduceLROnPlateau在损失停滞时把学习率砍半这是防止在局部最优附近来回跳的后悔药。训练完别忘了反归一化再算误差pred model.predict(X_test) # 反归一化scaler 是在原始数据上 fit 的 pred_inv scaler.inverse_transform(pred) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1)) from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_test_inv, pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, pred_inv)) print(fMAE: {mae:.2f}, RMSE: {rmse:.2f})这里有个容易翻车的点scaler必须是在训练集上 fit 的不能在全量数据上 fit 再切分否则测试集的信息泄露到了归一化参数里评估结果会虚高。正确做法是先切分再 fit或者至少保证 fit 只用了训练部分。4. 避坑与排查跑不通时先看这几处4.1 现象Loss 不降或直接变 NaN原因通常有三个。一是学习率太大Adam 默认 0.001 在负荷数据上一般没问题但如果你的数据没归一化梯度会爆炸。二是数据里有 NaN 或 Inffillna没处理干净或者归一化时出现了除零。三是SEQ_LEN设得比数据长度还大导致create_sequences返回空数组。解决先print(df.isnull().sum())确认无缺失再print(data.min(), data.max())确认归一化范围正常最后检查len(X_train) 0。如果都正常把学习率降到 0.0005 试一轮。4.2 现象验证集 Loss 远高于训练集 Loss这是典型过拟合。负荷预测数据量通常不大模型参数一多就容易记住训练集。表现是训练 Loss 一路降验证 Loss 先降后升。解决先加 Dropout在 GRU 后面加layers.Dropout(0.2)再把 CNN 的filters从 64 降到 32如果还不行减小 GRU 的units。另外EarlyStopping一定要开不要手动跑满 epochs。4.3 现象预测曲线整体平移形状对但数值偏这种「形状对、幅值偏」的情况十有八九是反归一化出了问题。要么scaler是在归一化后的数据上又 fit 了一次要么inverse_transform的输入形状不对比如忘了reshape(-1, 1)。解决确认scaler.fit_transform只调用一次且是在原始df[[load]]上。预测时scaler.inverse_transform(pred)的pred形状必须是(n, 1)如果是(n,)要先 reshape。4.4 现象Attention 权重全一样没有区分度Attention 层如果初始化不好或者训练不充分Softmax 输出会接近均匀分布等于没加权。表现是打印attn权重时每个时间步都差不多。解决检查 Attention 前的 Dense 有没有加激活函数tanh比relu更适合打分场景。另外训练轮次太少也会导致 Attention 没学出来可以先把epochs加到 100 观察。如果还是均匀考虑在 Attention 前加一层LayerNormalization稳定数值。4.5 现象换一台机器跑结果差很多这是随机种子没固定。TensorFlow 的权重初始化、Dropout、数据切分都带随机性。不固定种子的话两次运行 MAE 可能差 10% 以上对比实验就没意义了。解决在脚本开头统一设种子import random, numpy as np, tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)注意tf.random.set_seed对 GPU 上的某些操作仍然不能完全复现如果要做严格对比可以在tf.config里关掉 GPU 或设置TF_DETERMINISTIC_OPS1。5. 进阶技巧用滑动预测和误差分解验证模型真实能力跑通单步预测只是起点。实际负荷预测场景里更关心的是未来 24 小时甚至 72 小时的连续预测这就需要用滑动预测把模型上一步的输出当作下一步的输入滚动向前推。def recursive_forecast(model, last_window, steps): preds [] window last_window.copy() for _ in range(steps): p model.predict(window.reshape(1, SEQ_LEN, 1), verbose0) preds.append(p[0, 0]) # 把预测值滚入窗口丢掉最老的点 window np.append(window[1:], p, axis0) return np.array(preds) last_window X_test[0] # 取测试集第一个窗口 future recursive_forecast(model, last_window, 24) future_inv scaler.inverse_transform(future.reshape(-1, 1))滑动预测的误差会累积步数越多越明显。这时候可以做一个误差分解把总误差拆成「单步误差」和「累积误差」两部分。单步误差反映模型本身的拟合能力累积误差反映模型对自身预测的信任程度。如果单步 MAE 很小但 24 步后 MAE 爆炸说明模型过度依赖真实历史值泛化到自回归场景会崩。我一般会画三张图来验证第一张是测试集单步预测对比第二张是 24 步滑动预测对比第三张是 Attention 权重随时间的分布。第三张图特别有用——如果 Attention 权重集中在每天同一时段比如早高峰说明模型学到了日周期如果权重散乱说明它还没抓住规律这时候加SEQ_LEN或加 CNN 层数比调学习率更有效。还有一个容易被忽略的点负荷数据的周期性不只日周期还有周周期。如果你的load1.csv覆盖了数周可以在特征里加一个「星期几」的 one-hot 编码或者直接把SEQ_LEN设成 168一周小时数。后者对 GRU 的压力比较大常见做法是先用 CNN 降采样再送 GRU。从那以后我每次拿到一个新的时序预测包都强制先跑一遍「单步 → 滑动 → 误差分解」三步验证不看到滑动预测的衰减曲线就不下结论。希望帮到你。本文还有配套的精品资源点击获取