基于CNN-LSTM-Attention的Matlab光伏功率多步预测实践
2026/9/17 1:24:52 网站建设 项目流程

简介:这是一套基于Matlab的CNN-LSTM-Attention多变量多步预测光伏功率完整源码与数据集,主要面向计算机、电子信息、数学等专业的大学生课程设计、期末大作业和毕业设计。代码采用参数化编程,参数便于修改,注释清晰,运行环境要求Matlab 2023及以上。压缩包共7个文件,包含2个主程序M文件、2张结果图、1个Mat数据文件、1个Excel光伏数据集和1个说明文本,整体大小约2.46MB,结构简洁,便于快速定位代码与数据。目前已有175人学习浏览。模型融合卷积神经网络、长短期记忆网络与注意力机制,可有效提取时序特征并聚焦关键信息,输出预测对比图、误差图以及R2、MAE、MSE、RMSE等评价指标;需注意因真实发电功率含0值,MAPE指标计算结果为无穷大,不建议参考该指标。读者拿到后可直接运行,适合用于算法对比、实验验证及毕业设计演示。

1. CNN-LSTM-Attention 在超短期光伏功率预测里解决什么问题

电网调度要提前 4 小时知道光伏电站的出力曲线,数值天气预报只给了辐照度、温度、湿度这些气象要素,功率曲线得靠模型推出来。气象变化快、云层遮挡随机性强,单靠 LSTM 或者单靠 CNN 都容易在突变段失效,于是工程上把 CNN 的局部特征提取、LSTM 的时序记忆和 Attention 的关键时刻加权拼在一起用,这就是标题里 CNN-LSTM-Attention 多变量多步预测的实际场景。做这套方案的人通常有两类:一类是电力系统或新能源场站的技术人员,手里有 SCADA 历史数据和气象数据,需要在 Matlab 里快速迭代模型;另一类是研究时间序列预测的学生,想把多变量、多步预测从理论落到可运行的代码。这里有个反直觉的经验:模型结构只占工作量的一半,另一半在滑窗构造、归一化方式和验证集划分上,这三处做错任何一个,网络再深精度也上不去。

2. 多变量多步预测的问题定义与策略选型

2.1 光伏功率预测的输入输出怎么定义才不算错

多变量预测的第一步不是选网络,而是把预测问题格式化为监督学习样本。常见做法是以 15 分钟为采样间隔,用过去 96 个时刻(24 小时)的多变量序列,预测未来 16 个时刻(4 小时)的光伏功率,这是超短期光伏功率预测的典型设定。输入矩阵的每一行是一个时刻的全部变量,变量至少应该包含历史功率、水平总辐照度(GHI)、环境温度、组件温度、湿度、风速,以及小时和星期的时间编码。时间编码经常被忽略,但它本质上是在告诉模型“下午两点的功率天然比凌晨高”,这对光伏预测的帮助非常直接。

在 Matlab 里组织数据时,要特别注意深度学习工具箱对序列数据排列的约定:输入是“特征 × 时间步 × 样本数”的三维数组,而不是表格里那种“样本 × 时间步 × 特征”的排列。这个维度顺序弄反,sequenceInputLayer 会直接报维度错误,即使不报错,训练出的模型也完全没有意义。我一般会在构造数据后立刻检查 size(X),确认第一个维度等于特征数。

多步输出的定义同样需要提前定死:如果用“直接多输出”策略,标签 Y 的形状是“样本数 × 预测步数”,未来 16 个时刻的功率一次性输出。如果用“递归多步”,标签只有未来 1 个时刻,预测时再把输出当作输入回填。这两种策略的差别在下一节展开。

2.1.1 三种多步预测策略的选型对比
策略训练方式误差累积工程复杂度适用场景
递归多步训练单步模型,预测值回填有累积误差,越远越偏预测步数少,对远期精度不敏感
直接多步每个预测步长单独训练模型无累积,但特征利用率低各步长规律差异大
多输出一个模型同时输出多个未来值无累积,输出层参数共享中高预测步长中等,端到端训练

光伏功率预测里我一般选第三种多输出策略。原因是 4 小时内的功率曲线有强连续性,16 个输出节点共享同一个 LSTM 隐状态,比单独训练 16 个模型稳定得多,同时避免了递归回填的误差累积。代价是输出节点之间的相关性没有被显式建模,但对工程预测来说这个损失可以接受。

2.2 为什么是 CNN-LSTM-Attention 而不是单一模型

单一 LSTM 能抓住功率序列的长期趋势,但对辐照度的突变不敏感,云层飘过来那一两分钟内功率骤降,LSTM 的响应往往慢半拍。CNN 的感受野恰好能捕捉这种局部突变模式,一维卷积在时间维上滑动,相当于用若干个局部滤波器去识别“功率陡降”和“缓慢爬升”这类短时形态。所以常见的组合逻辑是 CNN 先做序列的局部特征提取,把突变模式编码成特征图,LSTM 再对特征图序列建模长期依赖,最后用 Attention 在时间维上加权,让模型自己学会“看”历史的哪个时段。

Attention 在光伏预测里有一个很直观的物理解释:晴空条件下,前一天的同一时段功率曲线对当前预测最有参考价值;阴天条件下,最近 30 分钟的功率变化趋势更重要。这两种情况需要的“记忆侧重点”完全不同,固定权重的 LSTM 隐状态做不到自动切换,而注意力权重可以在不同样本上动态变化。这也解释了为什么在天气突变频繁的场景,CNN-LSTM-Attention 比单纯的 CNN-LSTM 能稳定提升 5% 到 10% 的归一化 RMSE。

需要说明的是,Matlab 从 R2024a 开始在 Deep Learning Toolbox 里提供了内置的 selfAttentionLayer,可以直接插入 layerGraph。较早版本没有这个层,需要自定义层,后面的章节会分别给出对应做法。

3. Matlab 里的数据清洗、特征构造与滑窗样本生成

拿到一套完整的源码工程,第一件事不是打开网络结构文件,而是检查数据读取和滑窗这两个环节。光伏数据有三个高频问题:SCADA 上传中断导致的缺失、辐照度跳变导致的异常尖峰、多云天气下的功率锯齿。这三个问题处理不好,模型学到的全是噪声模式。

3.1 原始数据常见的三类脏数据问题

问题类型典型表现处理方式
缺失值功率或辐照度为 NaN线性插值,连续缺失超过 2 小时则丢弃该段时间序列
异常值功率超过装机容量,辐照度夜间非零按物理阈值剔除,用前后时刻均值填充
时间戳不连续采样间隔漂移,同一时间出现重复记录重采样到统一时间网格,按 15 分钟对齐

清洗时有个容易被忽略的细节:夜间光伏功率应该严格为零,但不少电站的逆变器在低功率时会上报一个很小的基值,比如 0.5 kW 左右的空载损耗。这个基值对归一化影响不大,却会让 MAPE 误差指标爆炸,后面第 5 章会专门处理。

3.2 用滑窗函数把多变量序列切成监督学习样本

下面这个函数是我在 Matlab 里常用的滑窗实现,输入是清洗后的多变量矩阵 data(每一行是一个时刻),输出是训练用的 X 和 Y。X 的维度是特征数 × 时间步 × 样本数,Y 是未来 horizon 个时刻的功率值。

function [X, Y] = createSlidingWindows(data, numSteps, horizon) % data: nSamples x numFeatures,第一列必须是历史功率 % numSteps: 输入时间步数,例如 96 % horizon: 预测步数,例如 16 n = size(data, 1); numFeatures = size(data, 2); numSamples = n - numSteps - horizon + 1; X = zeros(numFeatures, numSteps, numSamples); Y = zeros(numSamples, horizon); for i = 1:numSamples % 第 i 个样本的输入窗口 X(:, :, i) = data(i:i+numSteps-1, :)'; % 第 i 个样本的未来 horizon 个功率值 Y(i, :) = data(i+numSteps : i+numSteps+horizon-1, 1)'; end end

这段代码的逻辑是:从第 i 行开始取 numSteps 行作为输入窗口,转置后填入 X 的第 i 页;窗口结束后的 horizon 行取第一列功率作为标签。转置操作是把“时间 × 特征”变成“特征 × 时间”,这正好对应 2.1 节提到的那条维度约定。调用方式如下,数据划分时只按时间顺序切,绝不打乱样本顺序。

data = readtable('pv_data.csv'); % 假设第三列是功率?不对,我一般会先重排矩阵,第一列固定为功率 raw = [data.power, data.ghi, data.temp, data.humidity, data.wind, data.hour_sin, data.hour_cos]; numSteps = 96; horizon = 16; [X, Y] = createSlidingWindows(raw, numSteps, horizon); % 按时间顺序切分,前 80% 训练,后 20% 验证 trainEnd = round(size(X, 3) * 0.8); XTrain = X(:, :, 1:trainEnd); YTrain = Y(1:trainEnd, :); XVal = X(:, :, trainEnd+1:end); YVal = Y(trainEnd+1:end, :);

这里有一个光伏场景特有的问题:样本里存在大量“整段都在夜间”的窗口,此时输入和标签全是零或接近零。这类样本会拉低模型对白天功率曲线的拟合能力,因为梯度在大量零值上被稀释了。我一般的做法是统计每个样本窗口的功率总和,把功率总和低于阈值的样本过滤掉,或者干脆按时段分开建模。过滤阈值的经验值是装机容量的 5%:装机 100 kW 的电站,窗口功率总和低于 5 kW·h 就丢弃。

3.3 归一化的正确时机和 mapminmax 的隐藏坑

归一化必须放在滑窗之后、切分之后,在训练集上计算归一化参数,再把这组参数原样应用于验证集和测试集。在实际项目中经常有人对全部数据统一归一化,这引入了未来信息,验证集误差会偏小,等到真上线预测时立刻现出原形。Matlab 的 mapminmax 函数保存了归一化参数,可以这样用:

% PS 保存了训练集每列的最小值和最大值 [XTrainN, PS] = mapminmax(XTrain, 0, 1); % 验证集复用训练集的参数,而不是重新计算 XValN = mapminmax('apply', XVal, PS); % 标签单独归一化,同样只基于训练集 [YTrainN, PSY] = mapminmax(YTrain, 0, 1); YValN = mapminmax('apply', YVal, PSY);

这里有个大多数教程不会提的坑:mapminmax 对矩阵默认按行处理,而我们的 X 是三维数组,mapminmax 对三维数组的处理是按页循环、对每个特征行独立归一化的,这符合预期。但如果你把 X 展成二维再归一化再还原,就很容易把时间步维度搞乱。我建议直接用这个写法,不要再 reshape 两次。归一化范围选 0 到 1 还是 -1 到 1 对最终结果影响很小,真正影响大的是是否在训练集上单独计算参数。

4. 在 Matlab 深度学习工具箱里搭建 CNN-LSTM-Attention

4.1 网络各层的数据流和参数如何衔接

整个网络的数据流是这样的:sequenceInputLayer 接收“特征 × 时间 × 样本”的序列输入,convolution1dLayer 沿时间维抽取局部突变特征,ReLU 激活后进入 LSTM 层,LSTM 输出每个时间步的隐状态序列,Attention 对时间步加权,最后全连接层直接输出未来 16 个时刻的功率值。这里的关键是 LSTM 的 OutputMode 必须设置为 'sequence' 而不是 'last',因为 Attention 需要的是每个时间步的隐状态序列,而不是最后一个时间步的单一向量。这个细节搞错,程序不会报错,但 Attention 层拿到的输入长度永远是 1,等同于没有注意力机制。

CNN 层的参数选择遵循“小卷积核、适度通道数”的原则:卷积核大小选 3,因为光伏功率曲线的突变通常持续 1 到 2 个采样点,核太大反而把突变平滑掉了;滤波器数量选 32 到 64 之间,光伏数据的信息量不如图像,64 个滤波器往上提升有限。卷积层的 Padding 设置为 'same',保证时间步数不变,让 LSTM 能顺利接收与输入等长的时间序列。

4.2 不依赖新版本的自定义注意力实现

Matlab 的内置 selfAttentionLayer 在 R2024a 之后可用,可以直接放进 layerGraph。但如果你在 R2023b 或更早版本上运行,或者想把注意力权重提取出来做可视化分析,就需要自己实现。下面这段代码展示的就是光伏预测中常用的时间注意力机制:对 LSTM 输出的每个时间步的隐状态序列做加权求和,得到一个固定长度的上下文向量。

function [context, attn] = temporalAttention(H, w) % H: hiddenUnits x numSteps x batchSize,LSTM 的隐状态序列 % w: 1 x hiddenUnits,可学习的打分向量 [~, T, B] = size(H); % 每个时间步打分:score(t) = w * H(:,t,:) scores = pagemtimes(w, H); % 1 x T x B scores = reshape(scores, T, B); % T x B % 沿时间维做 softmax,得到每个时刻的注意力权重 attn = softmax(scores, 1); % T x B % 用权重对隐状态加权求和,得到上下文向量 attn = reshape(attn, 1, T, B); context = sum(H .* attn, 2); % hiddenUnits x 1 x batchSize end

打分函数 w 是需要随网络一起训练的参数,在自定义层里声明为 Learnable 属性即可。softmax 沿第 1 维(时间维)归一化,含义是让模型自己决定“这个时刻对最终预测有多重要”。如果读者使用的 Matlab 版本不支持 softmax 的 dim 参数,可以用 scoreExp = exp(scores); attn = scoreExp ./ sum(scoreExp, 1) 替换。把这段核心逻辑封装进 nnet.layer.Layer 子类时,需要同时实现 predict 和 forward 两个方法,forward 里多返回一个 dLdZ 用于梯度回传,训练逻辑与内置层完全一致。

4.3 用一个 layerGraph 把整条链路串起来

下面是一个完整可运行的网络搭建代码,用 layerGraph 组合各层。这里把自定义注意力层简化为一个参数固定的函数,实际源码工程里是完整的自定义 Layer 类,训练时通过 dlnetwork 调用。

numFeatures = 7; % 输入特征数 numHidden = 64; % LSTM 隐状态维度 numOutputs = 16; % 未来 16 个时刻 lgraph = layerGraph(); lgraph = addLayers(lgraph, [ sequenceInputLayer(numFeatures, 'Name', 'input') convolution1dLayer(3, 32, 'Padding', 'same', 'Name', 'conv1') reluLayer('Name', 'relu1') lstmLayer(numHidden, 'OutputMode', 'sequence', 'Name', 'lstm1') fullyConnectedLayer(numOutputs, 'Name', 'fc_out') regressionLayer('Name', 'reg_out')]); % 自定义注意力层需要继承 nnet.layer.Layer,并在 forward 中 % 完成 4.2 节的 temporalAttention 计算,这里用全连接层占位示意 % 实际源码中这一行替换为 attentionLayer(numHidden, 'attn1') figure; plot(lgraph);

regressionLayer 默认使用均方误差作为损失函数,光伏功率预测场景下不需要改。如果希望模型对功率低谷区间的拟合更好,可以考虑在 trainNetwork 中使用自定义损失层,但工程上先用默认 MSE 跑通基线,再决定是否加复杂损失。

4.4 trainingOptions 里决定收敛效果的参数组合

参数推荐值说明
InitialLearnRate0.001用 Adam 优化器时最稳的起点,学习率过大会在 20 轮内发散
MaxEpochs200配早停使用,单纯固定轮数容易过拟合
MiniBatchSize64光伏样本量通常在数万级,64 在稳定性和速度之间最平衡
ValidationFrequency20每 20 次迭代验证一次,比按 epoch 验证响应更快
OutputNetwork'best-validation'保留验证损失最低的权重,而不是最后一轮的权重
Plots'training-progress'训练时实时看损失曲线

训练和早停的配置如下,注意同时设置 Plots 为 training-progress,可以实时观察训练损失和验证损失是否出现背离,这是判断过拟合最直观的方式。

options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 200, ... 'MiniBatchSize', 64, ... 'ValidationData', {XValN, YValN}, ... 'ValidationFrequency', 20, ... 'OutputNetwork', 'best-validation', ... 'Plots', 'training-progress', ... 'Verbose', true); net = trainNetwork(XTrainN, YTrainN, lgraph, options);

需要留意的是,如果训练集和验证集存在分布差异,比如训练集以晴天为主,验证集以阴天为主,模型在验证集上的把戏就会很惨。所以验证集划分一定要按时间顺序取最后一段,而不是随机抽样,这也是 3.2 节强调的按时间切分的原因。

5. 多步预测的反归一化与光伏场景误差评估

5.1 预测结果如何还原成真实功率值

模型输出的是 0 到 1 区间内的归一化功率,评估之前必须先用训练时的 PSY 参数反归一化。误用测试集重新归一化是新手高频错误,会导致预测曲线整体平移。

YPredN = predict(net, XValN); % mapminmax 的 reverse 操作还原功率值 YPred = mapminmax('reverse', YPredN, PSY); YTrue = mapminmax('reverse', YValN, PSY); % 计算逐时刻的平均绝对误差 maePerStep = mean(abs(YPred - YTrue), 1); rmsePerStep = sqrt(mean((YPred - YTrue).^2, 1));

这段代码里 maePerStep 是一个长度为 16 的向量,对应未来 1 到 16 个时刻的误差。画图时把这 16 个值连成曲线,可以很直观地看到误差随预测步长增加而放大的趋势。如果曲线在第 4 步之后急剧上升,说明模型对远期预测的泛化能力弱,此时增加输入窗口长度通常比加深网络更有效。

5.2 光伏功率预测的误差指标要分时段看

MAPE 在光伏场景里几乎不可用,因为夜间真实功率接近 0,任何微小偏差都会把 MAPE 放大到几千个百分点。工程上习惯用归一化均方根误差(nRMSE)和分时段评估两个指标。分时段评估是把白天和夜间、晴天和阴天的误差分别统计,这样才能定位模型的真实短板。下面这段代码演示了如何按样本时刻分离误差。

% 假设 dataTime 是验证集对应的实际时间戳 hourOfDay = hour(dataTime); dayMask = (hourOfDay >= 6) & (hourOfDay <= 18); nightMask = ~dayMask; rmseDay = sqrt(mean((YPred(dayMask, :) - YTrue(dayMask, :)).^2, 'all')); rmseNight = sqrt(mean((YPred(nightMask, :) - YTrue(nightMask, :)).^2, 'all'));

如果夜间误差显著高于白天,通常是数据清洗时没有过滤掉逆变器空载基值,或者样本过滤阈值设置不合理;如果白天阴雨天的误差高,说明辐照度的特征表达不够,考虑加入云量或降水概率作为额外输入。

5.3 提升精度的三个工程技巧

第一个技巧是引入预测时段的数值天气预报(NWP)数据。超短期预测最有效的手段不是换网络结构,而是把未来时段的辐照度预报值当成输入特征,但注意不要让输入包含未来时刻的历史功率,否则会导致数据泄漏。第二个技巧是按晴雨分模型训练,晴天模型和阴天模型各自训练,预测时先用辐照度阈值判断天气类型再路由到对应模型,工程上比单个大模型稳定得多。第三个技巧是预测结果后处理,对 16 个输出步做滑动平均滤波,窗口大小选 3,能有效去除高频抖动,代价是峰值功率会被轻微压低,需要根据调度要求权衡。

要验证注意力机制是否真的在工作,可以把注意力权重导出并与天气对应起来观察:晴天样本的注意力权重通常集中在历史同刻附近,阴天样本的权重则偏向最近几个时刻,这种物理解释能帮助判断模型是否学到合理规律。与其纠结网络层数,不如先把验证集做成最近两周的滚动窗口,配合逐时刻误差分布图去排查模型退化问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询