简介:Matlab实现基于长短期记忆神经网络的电力负荷预测模型,面向电气、计算机、数学等专业学生的课程设计、期末大作业或毕业设计场景,提供单变量时间序列预测的完整源码与数据。资源共5个文件,包含1个m源码文件、1个csv数据表以及3张结果图,压缩包大小约1002KB,代码在Matlab 2020及以上环境可直接运行,适合快速上手。已有559人学习下载,可作为负荷预测方向的算法实验与对照模板。源码覆盖数据加载、网络构建、训练预测、多指标评价(R2、MAE、MBE)等环节,数据文件为csv格式,方便用Excel打开并替换成自己的负荷序列。代码由资深算法工程师编写,注释清晰,对数据预处理、网络参数设置和误差分析均有完整实现,同时附带的3张结果图便于直观对比真实值与预测值,能帮助理解时间序列建模全流程,也是电力系统短期负荷预测、毕业设计或竞赛项目的高质量参考。
1. 电力负荷预测为什么绕不开LSTM,Matlab里又该怎么落地
电网调度室每天都要回答一个问题:下一个小时的负荷是多少。传统ARIMA在平稳序列上够用,一旦碰到节假日、极端天气和早晚尖峰,残差就明显变大。十年前大家用BP神经网络做负荷预测,效果有了,但BP把时间当普通特征丢进去,历史信息全凭窗口硬扛。LSTM不一样,它把"记住多久"变成网络自己学的东西,天然适合按时间顺序流入的数据。Matlab的Deep Learning Toolbox提供sequenceInputLayer和lstmLayer,不需要自己写反向传播,也不需要装Python环境,一份完整源码和一个CSV数据文件就能把模型跑起来。但能跑通只是第一步,真正决定预测精度的往往在代码之外:序列窗口怎么切、归一化用哪段数据、验证集怎么留、多步预测怎么做。这篇从LSTM门控原理讲到Matlab训练闭环,再落实到调参和滚动预测,适合刚接触深度学习的工程师,也适合想把手头ARIMA换成深度学习方案的运维人员。
2. 先理解LSTM:门控机制、梯度问题与序列建模的边界
2.1 从RNN梯度消失说起:为什么简单循环网络不够用
电力负荷序列有一个特点:今天上午10点的负荷,可能和七天前上午10点的负荷更接近,反而和昨天下午2点差距较大。RNN通过隐藏状态循环传递信息,理论上可以保留长期依赖,但训练时误差沿时间反向传播,每经过一步就乘一次状态矩阵的雅可比。当序列长度到24、48甚至168,连乘结果不是指数爆炸就是指数衰减,前者让损失变成NaN,后者让网络学不到遥远时刻的特征。
这就是困扰RNN多年的梯度消失问题。Hochreiter和Schmidhuber在1997年提出长短期记忆网络LSTM,核心思路不是消除连乘,而是给记忆加上可控的"读写门"。门控制在0到1之间,决定了上一时刻的信息有多少能留下来。矩阵连乘变成逐元素的记忆叠加,梯度路径也变成直线,长期依赖才真正可训。
2.2 遗忘门、输入门和输出门:LSTM在负荷序列里记住什么
LSTM在每个时间步维护两个状态:隐藏状态h和单元状态c。单元状态是信息高速公路,门控决定怎么改写它。三个门的简化计算如下:
i_t = sigmoid(W_i * [h_{t-1}, x_t] + b_i) f_t = sigmoid(W_f * [h_{t-1}, x_t] + b_f) o_t = sigmoid(W_o * [h_{t-1}, x_t] + b_o) c_t = f_t * c_{t-1} + i_t * tanh(W_c * [h_{t-1}, x_t] + b_c) h_t = o_t * tanh(c_t)遗忘门f_t决定昨天同一时刻的负荷记忆保留多少,输入门i_t决定当前观测值以多大权重写入单元状态,输出门o_t决定当前时刻输出什么。放到电力负荷场景里,晚上8点的尖峰负荷会被输入门记进单元状态;凌晨低负荷时段,遗忘门会把白天积累的历史模式适当弱化,让模型跟着日周期走。
这正好解释为什么LSTM比分段线性回归和BP更稳:BP把"昨天20点"当成编号特征,跨天规律要靠特征工程去拼凑;LSTM则是在时间轴上递归更新,同周期性由单元状态自己发现。但要注意,LSTM只是擅长学习"可记忆"的规律,不代表它能预知"从未出现过"的事件,比如临时停电检修造成的大幅甩负荷。
2.3 什么时候LSTM会失效:脉冲型负荷与输入输出的关系
LSTM对输入数据的口径非常敏感。如果训练数据里包含节假日,模型会学习节假日模式;但如果模型只喂负荷本身,没有一个变量告诉它"今天是春节",它就只能把春节负荷当成异常值。遇到这种情况,标准做法是把日期特征转成独热编码后和负荷向量拼接,作为多变量序列输入。
常见失效场景可以归纳为下表:
| 场景 | 表现 | 改进方向 |
|---|---|---|
| 节假日负荷骤降 | 预测值偏高,误差集中在节假日 | 增加日历特征、节假日掩码 |
| 时间步长过短 | 预测结果滞后一小时 | 将numSteps从24提高到48或72 |
| 训练集和验证集划分串了 | 验证损失很低但上线后崩 | 按时间顺序划分,不能随机打乱 |
| 负荷序列有缺失值 | 训练Loss抖动,预测出现尖刺 | 先插值,且只用历史段统计量归一化 |
换句话说,LSTM不是万能钥匙,它解决的是"序列记忆"这一环。数据质量和问题定义不当,再好的门控也救不回来。下一章开始进入Matlab实现,把上述原则落到具体代码里。
3. Matlab实现基于LSTM的电力负荷预测:数据、切片、训练与预测
3.1 数据准备:CSV读取、清洗与归一化
我一般用readtable直接读CSV,列名按实际文件调整。假设文件里有一列是时间戳,一列是负荷值Load。
raw = readtable('load_data.csv'); y = raw.Load; % 负荷序列,单位MW y = fillmissing(y, 'linear'); % 线性插值处理缺失点 trainLen = floor(0.7 * numel(y)); mu = mean(y(1:trainLen)); % 只用训练段计算均值 sig = std(y(1:trainLen)); % 只用训练段计算标准差 yn = (y - mu) / sig; % Z-score归一化代码里最关键的是mu和sig只能从训练段算。如果先用全样本归一化再做训练测试划分,测试集信息会通过均值方差泄漏到训练过程里,线上部署时数据分布一变,预测就失真。fillmissing的linear插值适合短时间缺失,连续多个点缺失时建议用前一天同时刻值填充,这是电力数据处理的常见做法。
归一化之后,做一次可视化确认数据没有异常尖刺:
plot(raw.Time, yn); xlabel('时间'); ylabel('归一化负荷'); title('负荷序列检查');这一步不是可选的。如果序列尾部有大幅度跳变,后面训练出来的模型会把跳变当作常规模式,导致测试集误差被严重高估。
3.2 构造训练样本:滑动窗口与train/test划分
单步预测任务可以定义为:用过去numSteps个小时的负荷,预测下个小时的负荷。滑动窗口切出来的每个样本是一个连续子序列。
function [X, Y] = createSequenceData(yn, numSteps) n = numel(yn); X = zeros(n - numSteps, numSteps); Y = zeros(n - numSteps, 1); for i = 1:n - numSteps X(i,:) = yn(i:i + numSteps - 1)'; Y(i,:) = yn(i + numSteps); end end numSteps = 24; [X, Y] = createSequenceData(yn, numSteps);numSteps取24,代表用过去一天预测下个小时;取168则代表用过去一周。取多少取决于业务节奏:日周期明显的地区24够用,周周期明显的行业负荷建议168。循环构造在数据量大时会慢,但电力负荷一天96点、一年约35000点,三层循环也只要几秒,不用刻意向量化。
接下来按时间顺序切出训练集、验证集和测试集,比例取70%、15%、15%。
n = size(X,1); idxTrain = 1:floor(0.7*n); idxVal = floor(0.7*n)+1:floor(0.85*n); idxTest = floor(0.85*n)+1:n; % LSTM层要求单个序列用cell数组承载 XTrain = cell(numel(idxTrain),1); for i = 1:numel(idxTrain) XTrain{i} = X(idxTrain(i),:)'; % 1 × numSteps 转成 numSteps × 1 end YTrain = Y(idxTrain); XVal = cell(numel(idxVal),1); for i = 1:numel(idxVal) XVal{i} = X(idxVal(i),:)'; end YVal = Y(idxVal);这里容易踩坑的地方是数据格式:trainNetwork对序列输入接受cell数组,每个cell存放一条sequence,维度是特征数×时间步数。因为本例是单个特征,所以每个cell是1×24的列向量。如果不转成cell,直接用矩阵输入,lstmLayer会把每一行当成一个独立观测,序列长度就只剩1,模型退化成普通MLP。
3.3 用Deep Learning Toolbox定义LSTM网络
Matlab里定义网络用layer数组,一行一个层。针对电力负荷回归问题,我用如下结构:
numFeatures = 1; numHiddenUnits = 64; layers = [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, 'OutputMode', 'last') fullyConnectedLayer(32) dropoutLayer(0.2) fullyConnectedLayer(1) regressionLayer ];sequenceInputLayer声明输入特征维度为1。lstmLayer的OutputMode设成last,因为我们只关心最后一个时刻的输出,不需要输出每个时间步的隐藏状态。fullyConnectedLayer(32)让网络在LSTM输出后再做一层非线性映射,dropoutLayer(0.2)在训练时随机丢弃20%神经元,防止过拟合,最后的regressionLayer对应回归损失。
如果你的机器显存不大,可以去掉中间的fullyConnectedLayer(32),直接让LSTM输出到回归层,参数规模会小很多,训练也更快。网络不是越深越好,电力负荷单序列预测,一层LSTM加一个全连接通常就够。
3.4 训练选项的设置思路与模型保存
训练选项直接决定模型是收敛到合理误差,还是陷入震荡。这里给一组我常用的初始值:
options = trainingOptions('adam', ... 'MaxEpochs', 150, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 40, ... 'LearnRateDropFactor', 0.5, ... 'Shuffle', 'every-epoch', ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', true);InitialLearnRate是深度学习中最重要的超参数之一。0.005适合大多数负荷序列,太小收敛慢,太大会在收敛点附近震荡。LearnRateDropPeriod设为40,表示每40个epoch学习率乘0.5,让训练后期步长变小。Shuffle在每轮训练前打乱样本顺序,避免同一天数据扎堆影响梯度估计。ValidationData传的是验证集,Matlab会在训练过程中实时计算验证损失并画出来。
训练一行命令:
net = trainNetwork(XTrain, YTrain, layers, options); save('lstm_load_net.mat', 'net', 'mu', 'sig');训练完成后把网络、均值、标准差一起保存。反归一化的时候必须用到mu和sig,漏存任何一个,预测值都在错误尺度上。
4. 评估与调参:让电力负荷预测模型的误差落在合理区间
4.1 反归一化与RMSE、MAPE、R2的计算
测试集预测前,要先把测试输入做成cell数组,和训练数据格式一致。模型输出是归一化后的值,需要逆变换回原始量纲再算指标。
XTest = cell(numel(idxTest),1); for i = 1:numel(idxTest) XTest{i} = X(idxTest(i),:)'; end YTestTrue = Y(idxTest) * sig + mu; YPredNorm = predict(net, XTest); YPred = YPredNorm * sig + mu; err = YTestTrue - YPred; rmse = sqrt(mean(err.^2)); mape = mean(abs(err ./ YTestTrue)) * 100; ssRes = sum(err.^2); ssTot = sum((YTestTrue - mean(YTestTrue)).^2); r2 = 1 - ssRes / ssTot; fprintf('RMSE: %.4f MW\nMAPE: %.2f%%\nR2: %.4f\n', rmse, mape, r2);RMSE给出绝对误差水平,单位是MW;MAPE是相对误差百分比,业务上最直观;R2反映模型对负荷波动的解释程度,接近1说明预测曲线与真实曲线形态一致。电力负荷的MAPE在3%到8%之间都很常见,具体看所在地区的负荷波动性。如果MAPE超过15%,先别急着加Inception结构,大概率是归一化泄漏、时间步长或者数据对齐的问题。
还要注意MAPE在负荷接近零的时刻会爆炸。凌晨低负荷时段,真实值只有几十MW,分子一个微小偏差就能让百分比抬升几个点。遇到这种情况,我一般再算一个MAE来判断实际偏差是否不可接受,不要只盯MAPE。
4.2 单步预测与多步预测差别
上面所有代码都是单步预测:输入过去24小时,预测下1小时。实际业务经常要求预测未来24小时甚至72小时,这时候有两种做法。
一种做法是用预测出的结果当输入,继续预测下个时刻,形成递归多步预测。这个方案简单但误差会逐步累积,预测时间越远越平滑,尖峰消失。另一种做法是直接训练多步输出,把网络最后的全连接层改成24个神经元,回归层输出24个值,对应未来24小时。后者训练数据构造不同,Y要变成未来24小时的向量。
选择哪种取决于调度需求。如果只看下个15分钟的短窗口,递归预测足够;如果要做日计划,最好用多步输出结构,但训练时间更长。两种方案在Matlab里都能实现,区别只在数据切分和最后一层全连接尺寸。
4.3 四个必调参数:隐藏单元、学习率、BatchSize、时间步长
刚跑通的模型不建议大面积铺网格搜索,优先调下面四个参数,性价比最高。
| 参数 | 常用范围 | 调整方向 |
|---|---|---|
| numHiddenUnits | 16 / 32 / 64 / 128 | 验证Loss不降就增大,过拟合就减小 |
| InitialLearnRate | 0.001 / 0.005 / 0.01 | 训练曲线震荡就调小,Loss下降太慢就调大 |
| MiniBatchSize | 16 / 32 / 64 | 显存足够时影响小,验证Loss波动大时减小 |
| numSteps | 24 / 48 / 72 / 168 | 滞后明显就加大,验证Loss升高可能是步长过长 |
调参顺序上,我一般先固定numSteps为24,调隐藏单元和学习率,把网络调到不震荡不欠拟合;再回来试不同时间步长,做一轮对比。每次只改一个变量,记录RMSE变化,否则很难判断是哪个改动起了作用。
Matlab的training-progress图会同时显示训练Loss和验证Loss。验证Loss先降后升就是过拟合信号,这时候提高dropout比例或增大L2正则更有效。如果训练Loss一直不降,优先检查归一化和数据格式,而不是模型结构。
5. 滚动预测与多变量扩展:把模型真正部署到下一个时段
单步模型上线时,最简单也最稳的用法是滚动预测:最新实际负荷每到一个时刻,就把窗口往后推一格,让模型预测下一个时刻。下面这个函数接收训练好的网络、初始窗口、预测步数和归一化参数,递归生成一条完整预测曲线:
function yPredSeq = recursivePredict(net, x0, steps, mu, sig) yPredSeq = zeros(steps,1); curWindow = x0; % 1 × numSteps 的归一化窗口 for k = 1:steps predNorm = predict(net, {curWindow(:)}); % 当前窗口预测下一点 pred = predNorm * sig + mu; yPredSeq(k) = pred; % 将预测值归一化后追加进窗口,并丢掉最旧的一个点 curWindow = [curWindow(2:end), predNorm]; end end注意predict返回的是归一化数值,写回窗口时也要用归一化值。如果直接塞反归一化后的MW值,窗口尺度混乱,第二步预测就会漂移。滚动预测适合短期值班场景,每15分钟执行一次,不需要重训练。
如果要支持节假日和温度,把sequenceInputLayer的numFeatures改成特征数量,窗口X从n×numSteps扩展成n×numFeatures×numSteps,每个时间步的输入向量包含负荷、温度、星期几、是否节假日。构造时不再用单列切片,而是先拼特征矩阵,再按时间步抽连续片段。这种多变量LSTM的代码结构和单变量没有本质区别,主要额外工作集中在特征对齐和时间戳匹配上。
部署到生产环境前,我还会固定随机种子:训练开头加一行rng(0),保证复现。否则GPU上的随机数在不同次运行之间会有细微差异,差分对比时很难判断模型改动还是随机性导致的结果波动。当天的调度系统如果要求零点生成96点曲线,建议每天只加载一次模型文件,用滚动预测循环生成全天数据,不要每个点重新load网络,IO开销和格式转换都容易引入延迟。
本文还有配套的精品资源,点击获取