
做电力预测有一段时间了手里正好有个“某区域电网负荷与发电数据”的分析项目需要提前几小时预测用电量和发电量用于调度和削峰填谷。试过ARIMA、Prophet也跑过XGBoost最后在实际落地时选了CNN来做时间序列预测。很多人一听到CNN第一反应是图像处理觉得时序数据用不上其实这是一个很大的误解。这篇就以“CNN实现用电量/发电量预测”为例子把从数据准备、模型设计到训练评估、踩坑排查的完整过程整理出来希望对做能耗预测、负荷预测、新能源出力预测的朋友有帮助。这个项目目标很直接给定过去168小时一周的用电量、发电量、气温、风速等历史数据预测未来24小时每个小时的用电量和发电量。纯回归问题输出24个值。之所以选CNN而不是LSTM、Transformer是因为数据规模不算大模型要轻量、稳定、能快速迭代一维卷积网络在局部时序特征提取上完全不虚而且训练速度快、不易过拟合工程部署也简单。不管你是刚入门时间序列预测还是想给现有预测方案换一个更稳的底层模型这篇都可以作为参考。下面我会按一个完整项目的推进顺序来写包含每个环节的“为什么这么做”以及我在现场调参、排查问题时遇到的真实情况。1. CNN用于时间序列预测的整体思路1.1 为什么选择CNN处理时序数据这个项目的输入是“过去N个时刻的多种变量”输出是“未来M个时刻的目标变量”。本质上是把一个滑动窗口映射成未来窗口核心是挖掘历史序列中的局部依赖模式比如每天早晚两个用电高峰、午间光伏发电峰值、温度升高导致空调负荷上升等。CNN恰好擅长提取局部特征这个特性放在时间序列上同样成立。一维卷积核在序列上滑动时相当于在时间维度上做加权求和。比如一个核大小3的卷积每次看的是连续3个时刻的数据多个卷积核并行就能同时捕捉不同尺度的时间模式。堆叠两层卷积之后感受野扩大模型可以“看到”更长的上下文。这个逻辑很像图像CNN从边缘到纹理再到高级语义的过程只不过在时序里“边缘”变成“突变”“纹理”变成“周期性波动”。和LSTM相比CNN的优势是计算快、并行度高。LSTM必须按时间步逐步展开训练一个168输入、24输出的双层LSTM可能要十几分钟而一个4层1D-CNN在相同任务上两三分钟就能跑完。梯度回传路径也短调参压力小。缺点是CNN对全局长依赖的建模能力弱于注意力机制但短期负荷预测这种场景7天的窗口已经足够覆盖主要周期CNN完全够用。1.2 核心流程与框架整个项目流程我分成七步下面会逐一展开。先用表格给出整体结构后面分节详细说。阶段核心工作关键输出数据获取采集某区域负荷、发电、气象数据小时级DataFrame清洗与预处理缺失值、异常值、时间对齐干净序列窗口构建构造输入输出对训练/验证/测试张量模型设计1D卷积池化全连接CNN模型结构训练评估MSE/MAE、训练曲线分析模型权重、日志滚动预测递归多步/直接多步未来24h预测值部署落地模型导出、接口调用定时预测服务因为预测目标是未来24小时而输入包含负荷、发电量、温度等特征输入形状就是[样本数, 168, 特征数]。模型可以设计成多输出结构同时预测用电量和发电量也可以拆成两个单输出模型。我实测下来共享底层特征的多输出模型效果更好因为用电和发电在一个区域内存在强耦合比如午间光伏出力高时净负荷会降低共享卷积层能自动学到这种相关性。2. 数据准备与预处理2.1 数据集说明与序列构建先说数据来源。项目用的是某区域公开的电力负荷预测数据集包含从某年1月到同年12月的小时级用电量部分月份同时包含该区域的风速、温度、湿度和光伏发电量字段。原始数据大约8760个小时点。做时间序列预测第一步不是建模而是把数据“对齐”好。数据对齐要注意两个问题。第一时区。所有时间戳必须统一跨夏令时区域还要处理时区偏移我这次数据本身已经是东八区标准时间省去了转换。第二采样间隔。最好统一为小时点即每个整点一条记录。原始数据偶尔会延迟上传导致部分小时记录缺失。处理缺失值时我采用“前后均值临近日同小时均值”的组合填充法若连续缺失不超过3个点用前后两个有效值的线性插值填充若超过3个点则用最近7天同一小时的平均值填充如果是每天固定特征比如节假日单独打标处理这样做的原因是负荷曲线具有明显的“小时模式”和“周模式”用周期同位置的均值去补比单纯线性插值更符合实际运行规律。异常值方面我用了一个比较实用的规则某个时刻值偏离前后三小时均值超过3倍标准差且变化幅度超过去年同日的30%就判定为异常替换为该时刻前7天中位值。现场数据里经常出现“某站通信故障导致归零”这种明显错误如果不处理会让卷积核学到虚假的跳变特征。2.2 归一化与滑动窗口时序预测必须做归一化。负荷数据均值在几百到几千之间而温度是几十风速是个位数。如果不归一化卷积计算时梯度会被大数值特征主导模型训练极度不稳定。我用的是Min-Max归一化把每个特征压缩到0到1之间。归一化的关键是不能先在整个数据集上计算全局最大值最小值否则会引入未来信息造成数据泄露。正确做法是在训练集上计算每个特征的 min 和 max保存下来再分别应用到验证集和测试集。推理阶段也一样用保存好的缩放器对新数据做变换预测完再逆变换回真实单位。这一步看着简单但我见过不少新人在Kaggle上拿满分、一到线上就崩十有八九都是归一化统计量计算范围出了问题。窗口构建是一个标准的滑窗过程。假设输入窗口长度是168小时输出窗口长度是24小时那么在第t时刻构造的样本为输入[t-167, t-166, ..., t]共168个时刻的所有特征输出[t1, ..., t24]共24个小时的用电量或发电量窗口的步长通常选1也就是每小时生成一个样本。这么做的缺点是样本高度重叠训练集数量会膨胀但CNN训练快这个问题可以接受。重叠窗口带来的另一个风险是验证集与训练集之间不能有交叉我会在2.3节详细说划分策略。2.3 数据划分与标签构造时间序列不能像普通分类数据集那样随机打乱后按比例划分否则模型会看到未来的样本看起来是“死记硬背”。我采用的做法是按时间顺序切分前70%的数据作为训练集中间15%作为验证集最后15%作为测试集。同时保证切分点落在整点边界避免一个样本横跨训练和验证两个区间。具体窗口构造在整个数据张量上统一进行然后按索引切分。由于滑窗会产生大量重叠样本直接按时间顺序划分就可以避免信息泄漏。如果还想再做更严谨的验证可以按“块状划分”或“滚动起源验证”但日常项目里先按时间顺序切分足够用。标签构造这部分容易出错。我设计模型时目标有两种选择单输出只预测未来24小时用电量输出维度24多输出同时预测未来24小时用电量和发电量输出维度48我的做法是把标签做成二维数组[样本数, 24, 2]然后直接将模型最后一层改为输出[24, 2]用MSE损失同时优化两个目标。这里有一点值得注意用电量和发电量的量纲不完全一致发电量数据里可能包含新能源出力数值范围不同必须对每个输出特征分别做归一化模型才能稳定收敛。3. CNN模型构建3.1 输入形状设计模型输入形状直接决定了Conv1D层怎么配置。假设窗口长度是168小时特征字段包括用电量、发电量、温度、湿度、风速、是否节假日共6个特征。输入张量形状为(None, 168, 6)。如果你用PyTorch卷积层要求的输入是(batch, channels, length)所以需要用permute把最后一层特征维度转到通道维变成(None, 6, 168)。这个转换很多人第一次写就会踩坑报错信息显示维度不匹配并不是因为模型错了而是通道维度的约定不同。我这里写的是TensorFlow/Keras风格代码因为在实际项目里Keras的API更直观方便快速做实验。PyTorch版本逻辑一样只是注意维度顺序。先给出最简单的模型结构from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout, Input model Sequential([ Input(shape(168, 6)), Conv1D(filters64, kernel_size7, activationrelu, paddingsame), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size5, activationrelu, paddingsame), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, activationrelu, paddingsame), Flatten(), Dense(128, activationrelu), Dropout(0.3), Dense(24, activationlinear) ])3.2 卷积层与池化层配置卷积核大小我建议从7、5、3这样递减。第一层用大核7意味着初始感受野是7个小时能覆盖一个工作班次的长度可以捕捉到日周期内的局部趋势。第二层核大小5第三层核大小3逐层缩小核尺寸、增加通道数是图像CNN常见的“从局部到抽象”的设计手法。放到时序里第一层学会的是“近3小时趋势”、第二层学会“半天形态”、第三层学会“日周期和早晚高峰的模式”。kernel_size的选择不一定是越大越好。曾经试验过直接使用kernel_size24希望每个卷积核只看一整天结果模型严重过拟合因为参数量太大而且局部关联被破坏。经验法则是第一层核大小不要超过输入窗口长度的1/107在天级别的能耗预测里是合理的。如果数据是分钟级可以适当加大到15或30因为分钟级数据的局部模式持续时间更长。池化层用MaxPooling1D会把时间维度压缩比如窗口168经过两次池化变成42这样后面的全连接层参数量会小很多训练速度更快。但要注意过度池化会让模型失去对精确时间点的把握。比如你希望模型能准确预测明天下午两点是峰值如果序列被压缩太狠位置信息可能丢失。我一般最多做两层池化。如果还想降低分辨率优先使用步长为2的卷积代替池化这样模型还能在训练中自动学习怎么降采样。3.3 全连接与输出层卷积和池化输出的是一组特征图Flatten之后接全连接层。对于时序预测全连接层不需要太多128或256就够了。多了反而容易过拟合。Dense层后加Dropout比例我常用0.3太大模型欠拟合太小防不住过拟合。这个0.3是根据验证集损失反复试出来的。输出层激活函数必须用线性因为预测的负荷值是一个连续实数。如果误用relu有两个问题一是relu输出不能小于0虽然负荷一般不为负但当预测净负荷用电量减去光伏发电量时可能出现负值二是relu在大数值区间的梯度特性不稳定不利于拟合正态分布的回归目标。因此回归任务的输出层几乎总是linear。输出维度是24对应未来24小时的用电量预测。若是多输出模型则输出维度是48或者用Keras函数式API定义两个输出分支。3.4 模型编译与训练编译模型时优化器选择Adam学习率从0.001开始。损失函数用均方误差MSE因为我更关心误差大的点是否被惩罚得足够强而且MSE是可微的训练稳定。指标上同时监控MAE这样评估时更直观。代码如下model.compile(optimizeradam, lossmse, metrics[mae]) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size128, callbacks[ EarlyStopping(patience10, restore_best_weightsTrue), ReduceLROnPlateau(patience3, factor0.5) ] )EarlyStopping的 patience 设为10意思是连续10个epoch验证损失不下降就停。为什么要10而不设5因为时序数据的验证损失往往有周期性波动设太短容易在局部低点过早停止设太长又浪费时间。ReduceLROnPlateau在验证损失停滞3个epoch后学习率减半这样可以在训练后期用更小的步长找到更好的最优解。这两个回调是标配别省。训练时还有一个细节样本顺序不能打乱得太厉害。Keras的shuffleTrue默认会在每个epoch重新打乱样本但对于强周期的时间序列打乱可以让模型避免按时间顺序产生偏差因此我还是保留。如果数据集非常小可以设shuffleFalse并改用循环智能体策略但这里用不到。4. 模型评估与结果分析4.1 评价指标回归预测我用四个指标评估MAE平均绝对误差、RMSE均方根误差、MAPE平均绝对百分比误差和R²。下面给出计算公式MAEmean(|y_true - y_pred|)RMSEsqrt(mean((y_true - y_pred)^2))MAPEmean(|y_true - y_pred| / y_true) * 100%R²1 - sum((y_true - y_pred)^2) / sum((y_true - mean_true)^2)MAE最直观直接反映平均偏差多少MW。RMSE会放大较大误差适合评估系统是否会出现极端偏差。MAPE在负荷预测里非常常用但因为负荷有昼夜差异晚上基数小MAPE可能会很敏感需要结合前两个指标一起看。R²则衡量模型相对“直接用历史均值”的改进程度。这个项目中最后在测试集上的结果用电量预测MAE约为23MWMAPE约2.1%发电量预测MAE约为41MWMAPE约2.8%。对于一个区域级数据集这个误差在可接受范围内。如果对单个小时预测白天的误差通常会低于夜间因为白天负荷基数大相对误差小。4.2 训练过程与过拟合处理训练过程中要盯住两个曲线的拐点。如果训练损失持续下降而验证损失先降后升就是过拟合。我这次训练时发现第一版模型两层卷积512全连接在第12个epoch验证损失开始反弹但训练损失还在降。这说明模型开始记忆训练集里的随机波动。处理措施有几个增加Dropout比例从0.2提高到0.3减少全连接层神经元从512降到128增加L2正则化对Dense层设置kernel_regularizerl2增加数据量通过集成更多区域的数据或做数据增强其中数据增强对时间序列有一招比较有效把整个窗口随机平移几个时刻相当于给模型看更多起始相位。但数据增强在严格时间序列里要谨慎使用因为不能破坏时间因果性。我最终没有用平移而是通过调Dropout和减少参数量解决了过拟合。最终训练过程稳定在38个epoch左右训练MAE约16MW验证MAE约21MW测试MAE约23MW验证集和测试集差距不大说明泛化能力尚可。如果验证集和测试集差距过1.5倍以上往往说明验证集划分仍然和训练集有重叠需要检查滑窗是否跨越了划分点。4.3 多步预测实现模型一次输出24个值这是“直接多步预测”。这个方法实现简单不会累积误差比较适合短中期负荷预测。但存在一个问题模型训练时的输入始终是历史观测值如果未来真到了要预测的时候也没有“真实观测值”来滚动更新所以“直接多步”是合理选择。有些场景需要更长的预测窗口比如未来72小时。这时候有几种做法直接增加输出维度到72让模型一次输出72个值。缺点是输出维度越大训练难度越高训练一个“滚动预测器”模型先预测下一小时然后把预测值拼到输入末尾不断递归预测72小时。这是“递归多步”但会累积误差混合策略每24小时用最新观测值重置一次中间用预测值滚动我在项目里使用的是“直接多步预测值反馈修正”的方案模型输出未来24小时到下一小时获取真实值后更新输入窗口再生成新的24小时预测。这样始终保持了预测新鲜度误差不会累积超过24小时。如果你使用的是递归多步强烈建议在训练时就加入“teacher forcing”和“scheduled sampling”否则预测值的小误差会在递归中放大越往后越离谱。5. 常见问题与排查技巧5.1 数据泄露问题这是时间序列预测里最隐蔽的坑。除了归一化统计量只能从训练集计算这一个点还有两个容易犯的错误在窗口构造时如果使用整个数据集统一计算统计特征比如过去24小时均值、过去7天均值这些统计量在验证集样本里可能包含了未来数据。比如你计算每个时刻的“过去24小时均值”这个特征是历史信息没问题但如果你计算“未来24小时均值”做特征工程那就泄露了数据增强时如果用验证集样本来生成训练集样本也会泄露为了排查我会在拿到一个数据样本后人工检查“输入中是否包含输出时刻附近的信息”。比如预测目标t1但输入窗口如果错误地包含t时刻之后的某个值就是泄露。排查方法很简单随机打印几条样本看时间戳是否严格按过去到未来的顺序排列。5.2 卷积核大小与感受野计算感受野决定了模型能“看到”多长的历史信息。如果你有三层卷积kernel_size分别是7、5、3stride都是1padding都是same那么感受野大小是1 (7-1) (5-1) (3-1) 13。也就是说第三层卷积输出的每个特征点实际上覆盖了输入序列的13个连续时刻。如果你需要模型感知一天24小时的变化那么感受野至少要到24这时候需要调整卷积核大小或增加层数。我经常见到有人堆了五层卷积模型反而变差了原因不是模型深度不够而是感受野已经足够覆盖整个窗口再加深只会增加参数边际收益为负。计算公式可以帮你做一个预判设第i层核大小Ki则总感受野为1 Σ(Ki - 1)。如果这个值远超输入窗口长度就说明模型结构偏深了。我的项目输入窗口是168三层卷积感受野只到13看起来不大但池化层的存在扩大了有效感受野而且网络可以逐层组合局部特征所以最终效果足够好。如果感觉信息不足可以加深一层或把第一层kernel_size改为12。5.3 长序列训练时的显存与内存问题窗口长度为168、特征数为6时单个样本很小不会有内存问题。但如果窗口长度变成72030天分钟级特征数增加到20样本数几百万训练时会发现CPU内存先爆。解决办法是使用tf.data.Dataset做流式读取不要一次性把全部样本加载到内存用from_tensor_slices构造输入管道并设置prefetch用生成器按批次读取原始文件实时构造窗口TensorFlow里我推荐用tf.data里的window方法或直接预生成样本张量。预生成的好处是简单、调试方便缺点是内存开销大。后来我写了一个采样器不再从原始数据中滑窗生成所有样本而是每次生成一个batch的随机窗口。这样内存占用骤降而且由于窗口重叠随机采样并不会损失太多信息。这个方法适合窗口较长、数据量较大的场景。以下是使用tf.data构建输入管道的一个示例代码避免一次性加载过多数据def make_window_dataset(data, input_len, output_len, batch_size): ds tf.data.Dataset.from_tensor_slices(data) ds ds.window(input_len output_len, shift1, drop_remainderTrue) ds ds.flat_map(lambda w: w.batch(input_len output_len)) ds ds.map(lambda w: (w[:-output_len], w[-output_len:])) ds ds.batch(batch_size).prefetch(tf.data.AUTOTUNE) return ds这个写法比较干净shift1表示每小时生成一个窗口。如果你的数据量很大没必要每个小时都取可以设shift4即每四小时取一个窗口训练样本量能大幅减少而性能损失通常很小。我用这个技巧把原本200万样本的项目降到50万训练时间缩短四分之一验证集损失几乎没有变化。5.4 输出值异常震荡的处理第一次训练出的模型预测曲线在若干时间点出现不合理的跳变比如相邻两小时用电量从1000突降到300再回升。排查后定位到两个原因。一个原因是样本标签构造错误训练标签没有按时间对齐导致模型学到了错误映射。修改标签索引后恢复正常。另一个原因是归一化逆变换时误用了全局min/max。因为不同时间段的负荷范围不同如果你用全年的min和max缩放夏季高峰会被压缩得过小冬季低谷会被放大模型在小数值区间内的微小误差逆变换后就会变成大波动。后来我改用滚动分位数归一化每个样本用它自身窗口内的min/max进行缩放相当于让模型学习相对形态而不是绝对幅度预测稳定性提高不少。如果你也遇到预测曲线震荡建议先画出验证集里真实值和预测值的前两天曲线看是否为个别点异常。如果是个别点十有八九是数据问题如果是整段高频抖动则是模型结构问题可以尝试加一层1D卷积或调整权重初始化方式。还有一种简单有效的技巧对预测结果做指数平滑比如y_smooth[t] 0.7 * y_pred[t] 0.3 * y_pred[t-1]能明显消除单点毛刺但要小心不要过度平滑否则峰值会被削掉。5.5 特征重要性分析与多模型对比CNN不像树模型那样天然给出特征重要性但可以通过“置换重要性”来评估。做法是将测试集中某个特征随机打乱再输入模型观察误差增大多少。如果打乱温度特征后误差显著上升说明模型高度依赖温度。在用电量预测里温度通常是最重要的外部特征其次是是否为工作日。发电量预测则受日照和风速影响。这个分析能帮你决定是否需要继续采集更细的天气数据还是说现有特征已经足够。除了CNN我还用同一个数据集跑了LSTM和MLP基线模型。LSTM收敛慢效果略优于CNN但训练时间是CNN的三倍。MLP只把历史值展平输入表现差不少因为它无法在时间维度上共享权重导致参数量巨大、局部模式提取能力弱。这也再次说明对于中等规模、特征丰富的时序数据CNN是一个性价比很高的选择。我并不是说LSTM不好而是在工程资源有限时先用简单模型拿到稳定结果再考虑升级这才是务实的做法。5.6 冷启动与节假日问题负荷预测的一大痛点是节假日。模型从历史序列里学会了“周一早高峰”“周末低负荷”但遇到春节、国庆这类长假期历史模式完全被打乱。我处理的办法是引入“节假日距离”特征当前时刻距离最近节假日还有多少天以及是否在节假日期间。这个特征虽然只是一个数字但能显著提升节假日前后的预测精度。另外在训练时把节假日样本的损失权重提高让模型更关注这类少见但重要的模式。冷启动问题则更多出现在新能源出力预测。如果一个地区新装了光伏电站只有三个月数据CNN难以学到完整的年度周期。这种场景下建议先做简单的线性模型或Holt-Winters再逐步加入CNN。不要妄图用三个月历史预测一年后的发电量模型再强也敌不过数据不足。6. 关于CNN时序预测的一点经验总结CNN做时间序列预测并不是什么黑科技核心是把一维卷积当成一种“局部模式模板”通过堆叠扩大感受野从而建模时间依赖。它的强项在于训练快、稳定性好、部署容易特别适合负荷预测、用电量预测、交通流预测这类有强周期性的场景。我个人在实际操作中体会最深的一点是时间序列项目的成败往往不在模型结构而在数据处理和评估方式。窗口怎么构建、归一化统计量在哪里计算、验证集怎么切分这些环节任何一个出错都会让后续工作白费。模型精度卡住的时候先不要急着加层、加注意力回到数据侧去检查往往能找到真正的瓶颈。最后再分享一个小技巧如果你要上线一个用电量预测服务建议模型预测结果不要直接使用原始值而是加入一个“人工规则兜底”比如预测值不能低于历史同期最小值的50%也不能高于历史同期最大值的1.5倍。这种规则看起来粗糙却能拦住一些模型偶发的不合理输出。先用CNN把精度做到90分再用规则把安全做到100分才是工程落地的正常思路。