
简介这份资源面向电力系统负荷预测方向的学习者与工程实践者提供一套基于TCN时间卷积神经网络的完整预测方案。TCN通过增大扩张因子与加深网络层数扩大感受野可接收更长历史时序信息并借助残差连接避免深层网络精度退化从而降低预测误差、提升准确率适合作为时序预测入门与进阶的实战参考。压缩包共27个文件约64.21MB包含4个Python源码文件、2个Jupyter Notebook、7个Shell脚本以及jpg、png图表和txt、md说明文档覆盖模型定义、数据预处理、训练与结果可视化等环节并附有requirements依赖清单便于快速复现。目前已有381人学习下载。读者可据此掌握扩张因果卷积与残差连接的实现细节理解多窗口长度实验的配置方式并借助现成脚本与数据完成从训练到评估的完整流程为电力负荷预测项目提供可直接参考的代码基础。1. 电力负荷预测遇上 TCN为什么时序卷积能替代 LSTM 成为新选择做过电力负荷预测的工程师大多踩过同一个坑用 LSTM 或 GRU 跑出来的曲线在平稳段看着还行一到早高峰爬坡、晚高峰回落、周末负荷骤降这些拐点位置预测值就开始“糊”——要么滞后半天要么把尖峰削平。问题不在数据而在循环网络本身的串行结构它必须一步步递推长序列里早期信息被反复稀释梯度传播路径又长拐点这种“突变特征”最容易被平滑掉。TCNTemporal Convolutional Network时间卷积神经网络换了个思路用一维因果卷积加膨胀因子让感受野随层数指数级扩张同时靠残差连接保住梯度训练还能并行。这篇就围绕 Python 实现基于 TCN 的电力负荷预测模型把网络结构、数据窗口、训练参数、完整源码怎么落地讲透适合已经会 Python、想找一个比 LSTM 更稳的时序方案的从业者也适合刚入门想直接跑通一套完整源码的新手。2. TCN 的核心结构因果卷积、膨胀因子和残差块怎么搭2.1 因果卷积解决的是“不能偷看未来”电力负荷预测是典型的自回归任务预测 t 时刻的负荷只能用 t 时刻及之前的数据。普通一维卷积的卷积核会同时看到左右两侧直接用在预测上就是数据泄漏。因果卷积Causal Convolution的做法是在左侧做 padding右侧不补保证输出第 t 个位置只依赖输入的前 t 个位置。实现上通常用padding(kernel_size-1)*dilation再裁掉右侧多余部分或者用paddingcausal的封装。这一步是 TCN 能用于预测的前提漏了它验证集指标会好得离谱上线就崩。2.2 膨胀卷积把感受野指数级撑开单靠因果卷积要覆盖 96 个点15 分钟采样一天的输入kernel_size3 时需要堆很多层。膨胀卷积Dilated Convolution在卷积核元素之间插入空洞膨胀因子 d 逐层按 2 的幂增长1,2,4,8…感受野按1 2*(k-1)*(2^L - 1)扩张。层数 L6、k3 时感受野已经到 253足够覆盖多天周期。这也是 TCN 相比 LSTM 的硬优势并行计算 可控感受野长依赖不用靠记忆门硬扛。2.3 残差块是训练不崩的保险TCN 的基本单元是残差块每个块里两层膨胀因果卷积配 WeightNorm、Dropout、ReLU再加一条 1x1 卷积的 shortcut 把输入直接加到输出上。残差连接让梯度能绕过卷积层回传深层网络才训得动。下面是一个可直接用的残差块实现import torch import torch.nn as nn from torch.nn.utils import weight_norm class Chomp1d(nn.Module): # 裁掉右侧多余 padding保证因果性 def __init__(self, chomp_size): super().__init__() self.chomp_size chomp_size def forward(self, x): return x[:, :, :-self.chomp_size].contiguous() class TemporalBlock(nn.Module): def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout0.2): super().__init__() self.conv1 weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size, stridestride, paddingpadding, dilationdilation)) self.chomp1 Chomp1d(padding) self.relu1 nn.ReLU() self.dropout1 nn.Dropout(dropout) self.conv2 weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size, stridestride, paddingpadding, dilationdilation)) self.chomp2 Chomp1d(padding) self.relu2 nn.ReLU() self.dropout2 nn.Dropout(dropout) self.net nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1, self.conv2, self.chomp2, self.relu2, self.dropout2) # 输入输出通道不一致时用 1x1 卷积对齐 self.downsample nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs ! n_outputs else None self.relu nn.ReLU() self.init_weights() def init_weights(self): self.conv1.weight.data.normal_(0, 0.01) self.conv2.weight.data.normal_(0, 0.01) if self.downsample is not None: self.downsample.weight.data.normal_(0, 0.01) def forward(self, x): out self.net(x) res x if self.downsample is None else self.downsample(x) return self.relu(out res)逻辑说明padding(kernel_size-1)*dilation是因果卷积的标准配法Chomp1d把右侧多出来的部分切掉保证输出长度和输入一致且不看未来。weight_norm对卷积核做重参数化训练更稳。downsample处理通道数变化残差相加要求形状一致。参数上kernel_size一般取 2 或 3dilation按2**i递增dropout在负荷数据上 0.1~0.2 比较合适太大欠拟合太小过拟合。2.4 堆叠成完整 TCN 网络把多个残差块按膨胀因子递增堆起来就是 TCN 主体最后接一层 1x1 卷积把通道压到预测维度class TCN(nn.Module): def __init__(self, input_size, output_size, num_channels, kernel_size3, dropout0.2): super().__init__() layers [] num_levels len(num_channels) for i in range(num_levels): dilation_size 2 ** i in_channels input_size if i 0 else num_channels[i-1] out_channels num_channels[i] layers.append(TemporalBlock(in_channels, out_channels, kernel_size, stride1, dilationdilation_size, padding(kernel_size-1)*dilation_size, dropoutdropout)) self.network nn.Sequential(*layers) self.linear nn.Linear(num_channels[-1], output_size) def forward(self, x): # x: (batch, input_size, seq_len) y self.network(x) y y[:, :, -1] # 取最后一个时间步 return self.linear(y)num_channels是每层输出通道列表常见配置[64, 64, 128, 128]或[32, 32, 64, 64]层数越多感受野越大但参数也涨。forward里取y[:, :, -1]是因为因果卷积最后一个位置聚合了整段历史信息直接拿它做预测头输入最省事。3. 数据准备与特征工程负荷序列怎么切窗口、怎么归一化3.1 电力负荷数据的典型形态公开负荷数据集一般是「时间戳 负荷值」两列采样间隔 15 分钟、30 分钟或 1 小时。真实项目里还会带温度、湿度、节假日标记、星期几这些外生变量。TCN 的输入是(batch, channels, seq_len)所以要把多特征按通道维堆叠。如果只有单变量负荷input_size1加了温度、星期几 one-hotinput_size相应增加。别小看外生变量温度对空调负荷的影响在夏季能占到 20% 以上波动只喂负荷值模型学不到这层关系。3.2 滑动窗口切样本时序预测的标准做法是滑动窗口用过去seq_len个点预测未来pred_len个点。下面这段把 DataFrame 切成 TCN 需要的三维张量import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def make_windows(data, seq_len96, pred_len1): data: np.array, shape (T, F) F为特征数 返回 X: (N, F, seq_len), y: (N, pred_len) X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:iseq_len].T) # 转成 (F, seq_len) y.append(data[iseq_len:iseq_lenpred_len, 0]) # 只预测负荷列 return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) # 读取并归一化 df pd.read_csv(load.csv, parse_dates[timestamp]) feat_cols [load, temperature, is_weekend] scaler MinMaxScaler() df[feat_cols] scaler.fit_transform(df[feat_cols]) X, y make_windows(df[feat_cols].values, seq_len96, pred_len1) print(X.shape, y.shape) # 例如 (8000, 3, 96) (8000, 1)逻辑说明X.append(data[i:iseq_len].T)把(seq_len, F)转成(F, seq_len)正好对上 Conv1d 的通道维。y只取负荷列因为预测目标就是负荷。参数上seq_len96对应 15 分钟采样的一天pred_len1是单步预测要做多步就把pred_len调大但注意误差会累积多步建议用滚动预测而不是一次性输出。3.3 归一化的坑别用全局 scaler血泪经验很多人图省事把整个数据集一起 fit scaler结果验证集和测试集的统计量泄漏进训练。正确做法是只用训练集 fit然后 transform 验证和测试。另外负荷数据有明显的日周期和季节周期MinMaxScaler 对异常值敏感如果数据里有极端尖峰考虑 RobustScaler 或先做异常值处理。归一化后的反变换也要对应预测出来的值记得inverse_transform回原始量纲再算 MAPE。3.4 训练集/验证集/测试集怎么分时序数据不能随机打乱。常见做法是按时间顺序 7:1:2 切分或者用滚动预测的方式做多折验证。切分点最好落在自然日边界避免把同一天的样本拆到训练和测试两边造成信息泄漏。如果数据跨年注意春节、国庆这种长假负荷模式和平常完全不同测试集里最好包含至少一个完整假期否则指标虚高。4. 训练、调参与评估让 TCN 在负荷数据上真正收敛4.1 训练循环与损失函数负荷预测常用 MSE 或 MAE。MSE 对大误差惩罚重适合关注尖峰MAE 对异常值鲁棒适合整体平稳的数据。实际项目里我一般先用 MSE 训看验证曲线如果尖峰总是欠拟合就换 HuberLoss。下面是训练主循环import torch from torch.utils.data import TensorDataset, DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model TCN(input_sizeX.shape[1], output_size1, num_channels[64, 64, 128, 128], kernel_size3, dropout0.2).to(device) # 按时间切分不要 shuffle 训练集内部顺序 split int(len(X) * 0.8) train_ds TensorDataset(torch.from_numpy(X[:split]), torch.from_numpy(y[:split])) val_ds TensorDataset(torch.from_numpy(X[split:]), torch.from_numpy(y[split:])) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) criterion torch.nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5, factor0.5) best_val float(inf) for epoch in range(100): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() # 梯度裁剪TCN 深层偶尔会爆梯度 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) val_loss criterion(model(xb), yb).item() * len(xb) val_loss / len(val_ds) scheduler.step(val_loss) if val_loss best_val: best_val val_loss torch.save(model.state_dict(), best_tcn.pth) print(fepoch {epoch}, val_loss {val_loss:.6f})逻辑说明shuffleTrue只打乱训练集内部样本顺序不破坏时间切分。clip_grad_norm_是 TCN 训练的后悔药深层膨胀卷积偶尔梯度爆炸裁剪到 1.0 能稳住。ReduceLROnPlateau在验证损失停滞时降学习率比固定 lr 更容易收敛到好点。参数上batch_size64是负荷数据的常用值数据量小可以降到 32lr1e-3配 Adam 是稳妥起点。4.2 关键超参数怎么调参数常用范围影响调整建议seq_len48~336输入历史长度至少覆盖一个完整日周期15分钟采样取96num_channels[32,32,64,64] ~ [128,128,256,256]容量与感受野数据量大往大调小数据用浅层防过拟合kernel_size2~3单层感受野3 最常用2 更轻量dropout0.1~0.3正则强度验证损失震荡就加大lr1e-4~1e-3收敛速度先用 1e-3不收敛降到 1e-4调参顺序建议先定seq_len和num_channels这两个决定模型能力上限再调dropout和weight_decay压过拟合最后动lr和batch_size抠收敛细节。别一上来就网格搜所有参数算力烧不起。4.3 评估指标与对比基线负荷预测看三个指标MAE、RMSE、MAPE。MAPE 在负荷接近零时比如某些工业负荷夜间低谷会爆炸建议加一个阈值过滤或者用 sMAPE。评估时一定要和基线比naive 预测直接用昨天同一时刻的值、LSTM、GRU。TCN 如果在你的数据上打不过 naive说明特征工程或窗口设置有问题不是模型不行。我见过太多人只报自己的 MAPE 不说基线这种结果没有说服力。def evaluate(model, loader, scaler, device): model.eval() preds, trues [], [] with torch.no_grad(): for xb, yb in loader: xb xb.to(device) preds.append(model(xb).cpu().numpy()) trues.append(yb.numpy()) preds np.concatenate(preds) trues np.concatenate(trues) # 反归一化只对负荷列做 preds_inv scaler.inverse_transform( np.concatenate([preds, np.zeros_like(preds)], axis1))[:, :1] trues_inv scaler.inverse_transform( np.concatenate([trues, np.zeros_like(trues)], axis1))[:, :1] mae np.mean(np.abs(preds_inv - trues_inv)) rmse np.sqrt(np.mean((preds_inv - trues_inv) ** 2)) mape np.mean(np.abs((preds_inv - trues_inv) / (trues_inv 1e-6))) * 100 return mae, rmse, mape反归一化这里有个细节scaler 是对多列 fit 的inverse_transform 要求输入列数和 fit 时一致所以用np.zeros_like补齐其他列再取第一列。这个操作容易写错写完记得拿几个样本手工核对。5. 避坑与排查TCN 负荷预测最常见的 5 个翻车现场5.1 验证集指标好得离谱上线全崩现象验证集 MAPE 低于 1%测试集或线上直接飙到 15% 以上。原因数据泄漏。要么归一化用了全局 scaler要么滑动窗口切分时训练集和验证集有重叠样本要么因果卷积的 padding 写错让模型看到了未来。解决检查 scaler 是否只在训练集 fit检查窗口切分索引有没有重叠打印一层卷积输出确认第 t 个位置不依赖 t1 的输入。5.2 预测曲线整体滞后一个时间步现象预测值形状对但总是比真实值晚一拍拐点尤其明显。原因模型学到了“用上一时刻预测当前时刻”的捷径本质是输入窗口里最后一个点和预测目标太接近。解决把预测目标改成未来第 k 步而不是下一步或者检查forward里取的是不是最后一个时间步。另外seq_len太短也会加剧滞后试着加长输入窗口。5.3 训练损失不降或震荡现象loss 在某个值附近来回跳降不下去。原因学习率太大、梯度爆炸、或者 WeightNorm 和 BatchNorm 混用冲突。解决先加梯度裁剪再把 lr 降到 1e-4 试检查网络里有没有误加 BatchNormTCN 官方实现用 WeightNorm 就够了两个一起上容易打架。数据里如果有 NaN 或极端异常值也会导致 loss 震荡训练前先做缺失值填充和异常值截断。5.4 多步预测误差累积爆炸现象单步预测很准一次预测未来 24 步时后面几步完全跑偏。原因直接多输出output_size24时模型对远期没有足够约束或者滚动预测时把预测值当真实值喂回去误差滚雪球。解决多步预测建议用 seq2seq 结构或者直接多输出加 teacher forcing滚动预测时限制滚动步数超过 4~6 步就重新用真实值校正。别指望一个模型裸奔预测一整天。5.5 GPU 显存不够或训练太慢现象CUDA out of memory或者一个 epoch 跑十几分钟。原因num_channels太大、batch_size太大、或者seq_len过长导致中间激活占满显存。解决先把 batch_size 减半试再降num_channels比如从[128,128,256,256]降到[64,64,128,128]seq_len如果超过 336 考虑下采样或分段。另外 DataLoader 的num_workers设成 CPU 核数能加快数据加载别让它成为瓶颈。6. 进阶技巧用滚动预测和误差分析把 MAPE 再压两个点模型训完只是起点真正拉开差距的是后处理。第一个技巧是滚动预测加真实值校正预测未来 4 步用模型输出第 5 步开始把最近的真实观测值重新喂回窗口这样既做了多步预测又不会让误差无限累积。实现上维护一个滑动 buffer每次预测后把真实值 append 进去、丢掉最老的。第二个技巧是分时段评估把一天切成峰、平、谷三段分别算 MAPE你会发现模型在谷段往往最差因为夜间负荷低、相对误差大这时候可以针对谷段单独训一个模型或者调权重。第三个技巧是残差分析把预测误差按小时画出来如果误差有明显的周期性说明模型没学到某个周期特征回去检查外生变量里有没有漏掉星期几或节假日标记。def rolling_predict(model, init_window, steps, scaler, device): init_window: (1, F, seq_len) 已归一化 model.eval() window init_window.copy() preds [] for _ in range(steps): with torch.no_grad(): x torch.from_numpy(window).float().to(device) p model(x).cpu().numpy()[0, 0] preds.append(p) # 用预测值填充下一步的负荷通道其他特征保持不变 new_step window[:, :, -1:].copy() new_step[:, 0, 0] p window np.concatenate([window[:, :, 1:], new_step], axis2) # 反归一化 preds np.array(preds).reshape(-1, 1) preds_inv scaler.inverse_transform( np.concatenate([preds, np.zeros_like(preds)], axis1))[:, :1] return preds_inv这段滚动预测的关键在new_step负荷通道用预测值温度、星期几这些外生变量用已知的未来值天气预报和日历是提前知道的这样才符合真实预测场景。如果外生变量也未知那就只能纯自回归误差会更大。最后说个我自己的习惯每次训完模型我一定把预测曲线和真实曲线叠在一起画出来肉眼看拐点对不对齐再看误差分布有没有长尾。指标是给报告用的曲线是给自己看的。很多问题指标上看不出来一眼曲线就露馅。这套 TCN 方案我在几个地区的日负荷数据上跑过配合滚动预测和分时段调参MAPE 能稳定压到 3% 以内比同数据上的 LSTM 基线低 1.5 到 2 个点。值不值得做取决于你对拐点精度的要求——如果只是看趋势LSTM 够用如果要卡峰谷差和调度精度TCN 这套源码值得花两天跑通。希望帮到你。本文还有配套的精品资源点击获取