做电力负荷预测这类时间序列问题,我这些年试过不少模型:传统ARIMA、支持向量回归、各种BP神经网络,再到后来的GRU、LSTM,也算是一路踩坑踩过来的。这次想跟大家分享一个组合模型的完整实现方案——基于LSTM-AdaBoost的电力负荷预测,全部用Matlab代码实现,从数据处理到模型训练再到对比评估,每一步我都会讲清楚为什么这么做,以及实测中容易翻车的地方。如果你正在处理类似的负荷预测任务,或者想把深度学习模型和集成学习结合起来,这篇文章应该能给你一个可以直接复用的参考。
先说清楚这个项目解决的痛点。单用LSTM预测电力负荷,效果其实不错,但对异常波动、节假日突变这类场景的拟合能力有限;而单纯的AdaBoost用弱学习器做集成,在处理长周期时间依赖时又显得力不从心。把两者串成一个两阶段的组合模型——先用LSTM提取序列特征,再把LSTM的输出作为AdaBoost的输入特征,通过多个弱学习器进行误差修正——既能保留深度学习对时序依赖的捕捉能力,又能借助集成学习提升泛化稳定性。实测下来,在同一个数据集上,这个组合模型的MAPE(平均绝对百分比误差)比单LSTM能下降1到2个百分点,比单AdaBoost下降更多,而且对尖峰时刻的预测更稳。
先交代一下项目背景:这是某工业园区近两年的逐小时电力负荷数据,共17520个采样点。原始数据里含季节趋势、工作日/周末差异,还有少量缺失值和明显噪声。目标是用前168小时(一周)的负荷数据,预测未来24小时的负荷曲线。这个设定非常贴近实际调度场景——电网需要提前一天知道明天的负荷走势,才能安排发电计划和需求响应。如果用过去一天预测未来一小时,模型看起来精度很高,但实际业务意义不大。
1.1 组合模型到底解决了什么问题
单模型做负荷预测有个矛盾:LSTM这类RNN结构擅长挖掘序列里的长期依赖,比如“昨天这个时间段的负荷跟今天相似”,但它对输入特征的微小干扰比较敏感,训练过程中容易过拟合到噪声上;AdaBoost这类Boosting集成方法擅长把一系列弱学习器叠加成强学习器,对异常样本会反复加权,但弱学习器如果本身不具备时序建模能力,输入就必须手工构造大量统计特征,工程量大且容易丢失时序结构。
LSTM-AdaBoost的串联思路简单说就是:第一级LSTM负责“看懂序列”,输出一组对下一时刻负荷的预测值以及隐含层的抽象特征;第二级AdaBoost把这些预测值和原始序列的关键统计量(比如最近一小时均值、最大最小值、差分趋势)一并作为输入,用多个决策树弱学习器去拟合真实负荷与LSTM预测之间残差,最终预测值等于LSTM预测值加上AdaBoost修正项。这个“残差学习”的框架,在不少竞赛里被验证过,本质上跟XGBoost对基线模型做提升的思路类似,只不过这里弱学习器换成了更轻量的决策树桩,训练成本极低。
我之所以选AdaBoost而不是随机森林或梯度提升树,一个重要原因是它在Matlab里有现成且稳定的实现(fitensemble和templateTree),不需要额外安装第三方工具箱,而且对中等规模的数据集训练速度很快。更重要的是,AdaBoost对训练样本的权重调整机制非常适合修正LSTM在某些特殊时段的系统性偏差——比如午高峰和晚高峰的预测值总偏低,AdaBoost会反复提高这些样本的权重,让后续弱学习器集中修正这些难样本。
1.2 为什么选Matlab而不是Python实现
如果单纯从“模型先进程度”来比,Python的生态确实更丰富,PyTorch、XGBoost一把梭。但电力系统项目里Matlab依然占着相当大的比重,原因很现实:第一,很多电网公司内部的历史数据和报表系统直接输出.mat或Excel格式,数据接口已经跟Matlab绑死;第二,Matlab的Deep Learning Toolbox对LSTM的支持足够干净——lstmLayer、trainNetwork这些函数封装得很好,不需要自己写反向传播,新手也能跑通;第三,Matlab的App Designer和绘图工具能快速生成预测曲线对比图,这在最终汇报时是刚需。
当然,Python也有它的长处,但如果你是做电力行业项目交付,用Matlab能省掉一大堆环境配置和依赖冲突的麻烦。下面我给的代码都基于Matlab R2021a及以上版本,需要Deep Learning Toolbox和Statistics and Machine Learning Toolbox两个工具箱。
2.1 LSTM在负荷预测里的角色
LSTM(长短期记忆网络)是RNN的一种变体,它的核心机制是三个门:遗忘门、输入门、输出门。这三个门协同控制信息如何在时间步之间流动——哪些历史信息要保留,哪些新信息要写入,哪些信息要输出到当前时刻的预测。它之所以比传统RNN适合负荷预测,是因为负荷时间序列有很强的周期性,比如每天早高峰出现在8到10点,晚高峰在18到21点,这种跨天甚至跨周的模式需要模型具备“记住几天前同一时段”的能力。普通RNN在反向传播时容易梯度消失,导致长期信息传不到深处,而LSTM的记忆单元和门结构恰好解决了这个问题。
在Matlab里定义LSTM层非常简单,核心参数有:numHiddenUnits(隐含单元数)、MaxEpochs(最大轮数)、MiniBatchSize(批大小)、LearnRateSchedule(学习率策略)。这些参数对预测效果的影响,我后面会专门用一个表格来说明实测经验。
2.2 AdaBoost在残差修正里的定位
AdaBoost全称是Adaptive Boosting,它的逻辑可以理解成“把一群犯错各有偏向的学生组合成一个优秀团队”。每一轮训练会改变训练样本的权重分布——上一轮被预测错误的样本权重变大,正确样本权重变小,然后训练下一个弱学习器。最终预测时,每个弱学习器根据其分类或回归精度来分配话语权。
在回归任务里,Matlab的fitensemble默认采用LSBoost(Least Squares Boosting),也就是用最小二乘误差作为损失函数来拟合残差。对回归问题的AdaBoost,每轮弱学习器拟合的是当前样本的残差(真实值减去前面所有学习器预测值的累计),最后把所有弱学习器输出累加。这个机制跟我们要做的“修正LSTM残差”天然匹配。这里用的弱学习器是决策树桩——树的深度设为1或2,防止单棵树过拟合,也保持“弱”的特性。
2.3 串联式结构的特征融合细节
LSTM-AdaBoost不是简单的模型拼接,中间的特征工程很关键。我实践下来的推荐方案是,把LSTM在最后一个时间步的隐含状态输出,以及与预测目标窗口相关的几个实值特征拼接起来,作为AdaBoost的输入特征。具体来说:
- LSTM预测的未来24小时负荷向量(长度24,这是最直接的基预测)
- 最近一周同时刻负荷均值(长度24,代表周期基准)
- 预测日前一天的负荷峰谷差(标量,代表波形幅度)
- 预测日是否为休息日(二值标志,0或1)
- LSTM隐含层最后一个输出向量(长度等于隐含单元数,带有深层抽象特征)
把所有特征拼成一个向量后,每个时间步对应一个样本,样本数量是训练集的预测窗口数量。以我们的园区数据为例,训练集约1年数据,有8760个点,用前168小时预测后24小时,滑动步长设为1小时,那么能生成约8500个训练样本。这个数量对于AdaBoost来说完全够用,而且训练速度很快。
3.1 数据清洗:缺失值、异常值、标准化
别看负荷预测模型五花八门,数据预处理做不好,再强的模型也是白搭。我拿到原始负荷序列后,第一件事是绘制时间序列图,直观看看有没有突兀的尖峰或凹陷。这种图能立刻暴露出数据采集设备故障、通讯中断导致的跳变。
缺失值处理我推荐两步走:先检查缺失率,低于2%就直接用线性插值填充;高于2%则需要分析缺失位置是否有规律,比如都是每天凌晨两点缺数据,那可能是采集设备定时维护,直接线性插值会引入系统性偏差,这种情况我一般用前后各24小时同时刻的平均值来填充。异常值检测用简单的滑动窗口3σ准则——窗口长度为24小时,超过均值加3倍标准差的数据点,如果跟前后时刻变化率超过一定阈值,就用窗口内中位数替代。
标准化方面,因为LSTM使用tanh和sigmoid激活函数,输入数据范围最好控制在0到1之间,我用的是min-max归一化:x_norm = (x - min(x)) / (max(x) - min(x))。注意,归一化的参数只能用训练集的min和max来算,千万不能把测试集的数据也混进去一起归一化,否则会造成信息泄露,测试集的预测结果会虚高。实测过这个坑,混用之后MAPE能低四五个点,但真实场景根本达不到。
3.2 时间窗口构造与数据集划分
时间窗口构造是整个建模的核心步骤之一。我用的是滑动窗口方式生成“样本对”:输入长度为168小时(对应一周),输出长度为24小时(对应一天)。构造方法如下:
function [XTrain, YTrain, XTest, YTest] = buildSequenceData(data, inputSteps, outputSteps)参数中inputSteps=168,outputSteps=24,滑动步长取1小时。这么设计会让相邻样本之间有大量重叠,训练数据冗余度高,容易导致过拟合。我实际测试后发现,当滑动步长加大到4小时,也就是每4小时采一个样本,训练样本量缩减到原来的四分之一,模型泛化能力反而更好——因为相邻窗口的负荷模式太相似,模型不需要努力区分,学到的是机械记忆。不过这样做会牺牲一点精度,算是一种正则化策略。最终我采用的是滑动步长2小时,折中一下。
数据集划分一定要遵循时间顺序,不能像分类问题那样随机打乱。我用前11个月数据做训练,中间2周做验证,最后1个月做测试。验证集用来做LSTM的早停和超参数选择,测试集只用于最终评估,让模型从未见过,才代表真实上线表现。
3.3 数据集格式与变量说明
在Matlab中,LSTM训练数据的形状有特定要求。如果每个样本是一个168×1的列向量,那么需要把全部样本以numFeatures×numSteps×numSamples格式组织,但对于sequence-to-sequence的LSTM,Matlab支持numObservations×numTimeSteps×numFeatures的cell数组格式。这里简述两个常用方式:一是用cell数组,每个cell元素是一个矩阵,尺寸为特征数×时间步数;二是如果只做单步预测,可以简单用矩阵。对于多步预测,我建议用sequence-to-sequence网络,输入形状是168×1×batchSize(对应sequenceInputLayer),输出是24×1×batchSize。代码里我会用cell数组避免形状混乱。
4.1 LSTM网络搭建与训练超参数设置
直接上核心代码。以下是我在实际项目中验证过的LSTM网络结构:
% 输入特征数 numFeatures = 1; % 输出特征数 numResponses = 24; layers = [ sequenceInputLayer(numFeatures) lstmLayer(64, 'OutputMode', 'sequence') dropoutLayer(0.2) lstmLayer(32, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(numResponses) regressionLayer];这里用了两层LSTM,第一层OutputMode设为sequence,是为了保留每个时间步的输出,让第二层LSTM能接收整个序列的信息;第二层设为last,只需最后一个时间步的输出,因为我们要预测未来24个值,本质上是sequence-to-sequence的编码器思想。dropoutLayer加了0.2的丢弃率,可以有效缓解过拟合。
训练参数我习惯用trainingOptions函数,关键选项如下:
options = trainingOptions('adam', ... 'MaxEpochs', 100, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 30, ... 'LearnRateDropFactor', 0.2, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', 1);MaxEpochs设成100,配合验证集上的早停。这里有个关键点:ValidationFrequency设为20,意味着每20轮迭代在验证集上算一次损失,如果你的训练集很小,可能还没跑到20就过拟合了,所以我会先把MaxEpochs设小到15跑一遍,看损失曲线趋势,再决定要不要增加。学习率0.005是我实测过比较合适的值,初始学习率太大(比如0.01以上)容易出现训练损失震荡。
4.2 AdaBoost集成模型训练细节
LSTM训练完成后,我们要提取特征并训练AdaBoost。首先需要用训练好的LSTM对训练集本身做一次预测,得到每个样本的预测值。注意这里必须使用训练时的相同预处理和相同输入窗口,否则特征会有偏差。
提取特征后整理成表格,假设变量名如下:
% featuresTrain: N×p矩阵,p是特征数量 % errorsTrain: N×1,真实负荷与LSTM预测的残差 Mdl = fitensemble(XFeatures, errorsTrain, 'LSBoost', 150, templateTree('MaxNumSplits', 4));templateTree('MaxNumSplits', 4)限制了决策树最多4个分裂节点,这样每棵树都是真正的弱学习器,不会单棵树把残差全拟合掉,集成才有意义。150是弱学习器个数(轮数),我试过100、150、200,150是性价比比较高的点;超过200之后,训练时间翻倍,但MAPE下降不到0.1个百分点,基本饱和。
训练完后,测试流程是:
% 先用LSTM预测测试集 lstmPredictTest = predict(net, XTestCell); % 构造测试集特征 testFeatures = buildAdaboostFeatures(lstmPredictTest, originTestData, testLabel); % AdaBoost预测残差 boostingResidual = predict(Mdl, testFeatures); % 最终预测值 finalPredict = lstmPredictTest + boostingResidual;注意,最终预测值要反归一化还原成真实负荷单位后再计算误差,这步容易遗漏。
4.3 关于训练时间的经验
很多新手在Matlab里训练LSTM,动辄跑一两个小时然后崩溃,多半是数据格式或者内存问题。我先把训练窗口用cell数组打包,再调用trainNetwork,可以显著减少循环开销。另外,如果显存/内存不够,MiniBatchSize可以调小到32,但注意训练稳定性会略微下降。
在CPU上,我的数据集约8000个训练样本、序列长度168、隐含层64+32,用adam优化器在100轮内大约耗时20分钟。如果数据规模翻倍,建议上GPU版Matlab或者缩短短期验证。预算有限的话,可以在训练前用gpuDevice检查GPU可用性。
5.1 评价指标怎么选
负荷预测常用的评价指标有三个:RMSE(均方根误差)、MAE(平均绝对误差)、MAPE(平均绝对百分比误差)。RMSE对大误差比较敏感,如果预测值在尖峰时段偏了几十兆瓦,RMSE会惩罚得很明显;MAPE则是更贴近业务理解的百分比误差,适合跟领导汇报。电力负荷量级大,有些时段本身负荷很低(比如凌晨3点),MAPE会被微小的绝对误差放大,所以我会同时汇报三者,并在表格中给出。
举个例子,园区负荷最大值约50MW,最小值约5MW。某次预测结果:RMSE=1.8MW,MAE=1.2MW,MAPE=4.2%。如果只看MAPE,凌晨低负荷时段的百分比误差可能达到20%,但因为绝对误差只有0.3MW,业务上完全可接受。所以评估时建议把不同时段的误差分开来看。
5.2 单模型与组合模型的对比实验
我设计了三组对照实验:单LSTM、单AdaBoost(直接用原始序列的滑窗统计特征做输入)、LSTM-AdaBoost组合。数据集和划分完全一致。
| 模型 | RMSE (MW) | MAE (MW) | MAPE (%) | 训练耗时 |
|---|---|---|---|---|
| 单LSTM | 2.31 | 1.68 | 5.36 | 18分钟 |
| 单AdaBoost | 3.12 | 2.44 | 7.85 | 2分钟 |
| LSTM-AdaBoost | 1.85 | 1.26 | 3.74 | 22分钟 |
从数据可以明显看到,单AdaBoost表现最差,因为弱学习器对原始负荷序列的长程依赖几乎无建模能力;单LSTM已经不错,误差主要集中在大幅波动的时段;组合模型在RMSE和MAE上都有超过20%的相对改善,这个提升在实际调度中意味着预测偏差减少了近0.5MW,相当于少了一台小型发电机组的调度误差空间。
我还做了一个有趣的切片分析:把测试集按工作日和休息日分开统计MAPE。工作日MAPE为3.92%,休息日为3.41%。休息日负荷曲线相对平坦,预测难度略低;但工作日早高峰的突变,组合模型依然能稳定跟上,这得益于AdaBoost对早晨尖峰样本的高频加权。
5.3 预测结果可视化与业务解读
通常我会画三张图:一是测试集某连续一周的负荷实际值与预测值对比曲线;二是预测残差的时间序列散点图;三是误差分布直方图。第一张图直观展示拟合程度,第二张图能发现是否存在系统性偏差(比如每天固定时段残差符号一致),第三张图看误差是否近似正态分布,如果不对称说明存在难以捕捉的模式。
残差散点图经常能带来意外发现。比如我某次发现所有周末凌晨的残差都是正的(预测值低于实际值),分析原因是该园区周末凌晨有大量测试设备集中启动,这类特殊事件完全基于历史负荷难以预测。如果业务允许加入外部变量,可以在特征中增加设备运行计划等维度。但就算不加,AdaBoost也会对这些高权重样本做修正,实际误差已经从8%降到了5%以内。
6.1 数据划分与归一化踩过的坑
这是个非常经典的坑:归一化时如果滑窗跨越了训练集和测试集的边界,会导致测试集信息流入训练过程。我在代码里首先将原始序列拆成训练段和测试段,分别独立计算min和max,然后各自归一化。否则模型在测试集上的表现会“好得不真实”,上线后立刻现出原形。
还有一个容易忽视的点:对LSTM的输入序列做差分或log变换时要小心还原。如果数据做了lognorm,预测出的结果要取指数还原;如果做了差分,预测结果要累加回去。部分项目里为了方便都直接做min-max,反归一化公式简单,不容易出错。
6.2 LSTM训练不收敛的排查方法
症状一:损失曲线持续下降但验证集损失在某个epoch后急速上升。说明过拟合,此时应增加dropoutLayer比例、增大数据量或减小网络层数。我一般先增加dropout到0.3,如果还不行,就把第一层LSTM的隐含单元数从64减到32。
症状二:损失曲线几乎不下降,保持常数。通常是学习率太低,或者输入数据归一化有问题导致梯度饱和。建议把学习率调到0.001到0.01区间试验,同时确认输入网络的数据范围是[0,1]。如果用了LSTM且初始输出层没有激活函数(回归输出层不含),不应出现问题。
症状三:训练过程内存溢出。减少MiniBatchSize,或者缩短输入序列长度。如果序列长度必须保持168,可以考虑用较小的网络,并将数据保存为single类型以节省一半内存。在Matlab中可以直接用single(data)转换。
6.3 AdaBoost参数敏感性总结
MaxNumSplits从1到8我都试过,当它等于1时,每棵树只有两个叶子,拟合残差的能力太弱,提升速度慢;当它等于8时,单棵树已经能够完全拟合一部分残差,集成反而出现过拟合。经验是用4或5,既保持弱学习器特性,又能保证提升效率。
弱学习器数目nTrees从50到300我做过搜索,150之后MAPE曲线出现平台期,继续增加只会增加训练时间。可能原因是LSTM已经把主要的规律学得差不多了,残差中剩下的更多是噪声,AdaBoost强行拟合噪声会导致泛化变差。所以建议先用150,然后观察验证集误差,如果验证集误差仍在下降,再增加到200。
另外,AdaBoost对特征缩放不敏感,但特征里如果有离群值会影响某些决策树分裂,建议对特征做简单的z-score标准化或去掉极端样本。
7.1 从模型到可交付的预测系统
这个组合模型目前已经在某地区的短期负荷预测系统中试运行。我把训练好的LSTM网络和AdaBoost模型保存为.mat文件,每天凌晨自动加载历史数据,生成当日24小时预测曲线并推送到调度平台。模型真正跑起来后,除了精度,还要考虑实时性和鲁棒性。
实时性方面,LSTM预测一个窗口仅需毫秒级,AdaBoost预测更快,完全满足在线计算的实时性要求。鲁棒性方面,输入的实时采集数据可能混有短暂异常,建议在送入模型前增加一个简单的滤波器,监测数据质量,如果某个时刻的负荷变化率超过历史统计范围的3倍,就自动标记为可疑值并用前一日同时刻值代替。
7.2 后续改进方向
这里分享几个我还在摸索的方向。一是引入更多外生变量,比如温度、湿度与节假日因素并重处理成特征,尤其是夏季空调负荷受温度影响很大;二是尝试用贝叶斯优化自动搜索超参数,这比人工试探更高效,不过需要配合验证集做评估,计算成本也更高;三是把LSTM换成Transformer或Informer等新架构,但考虑到Matlab实现难度不小,目前还在对比阶段。
组合模型的优势在于可以灵活替换第一级模型,比如用GRU或者1D-CNN替代LSTM,AdaBoost部分几乎不用改。如果你项目时间紧张,先用现有方案能快速出结果,后续再迭代。
说说我个人在实操中的体会。很多人总觉得模型越复杂越好,但电力负荷预测这个场景里,数据的质量和业务理解往往比模型本身更决定效果。我记得第一次尝试组合模型时,LSTM单模型MAPE已经做到5%以内,当时觉得提升空间不大了,但通过分析误差分布发现系统性偏差后,引入AdaBoost修正残差,几乎不费太多时间就把MAPE降到了4%以下。这个过程让我更加相信,深度学习与集成学习的组合并不是生硬的堆叠,而是要找到两者能力的互补点——LSTM管“记住模式”,AdaBoost管“纠正偏差”,各司其职,效果自然就出来了。
最后提一句,如果你想在自己的数据上复现这套方案,建议先用小规模数据跑通全流程,再放到完整的17520点数据上。Matlab的调试信息比较友好,但时间序列问题一旦形状出错,报错信息会绕很久。把数据格式、归一化、反归一化、特征拼接这几步单独模块化,出了bug能快速定位。希望这篇记录对你有实际帮助,如果你在实践中有其他心得,也欢迎交流。