基于VMD-WOA-LSTM的光伏发电功率预测Matlab实现

发布时间:2026/10/25 10:05:33
基于VMD-WOA-LSTM的光伏发电功率预测Matlab实现

简介:面向光伏功率预测与Matlab算法研究的一套完整实现,适用于电子信息、数学等专业学生完成课程设计、期末大作业或毕业设计。资源基于VMD、WOA与LSTM三种技术组合:先由VMD分解光伏功率序列,再经WOA优化LSTM参数,最终完成预测建模,并提供可直接运行的案例数据。包体共21个文件,以10个m程序文件为核心,辅以7张结果图、2个mat数据文件、1份Excel原始数据及1个说明txt,整体仅462KB,结构紧凑、便于替换数据后复现实验。目前已有184人学习,程序采用参数化编程,支持Matlab 2014/2019a/2024a,注释清楚,适合新手快速理解模型流程;通过修改参数即可用于不同光伏数据,是兼顾算法研究、课程作业与科研复现的实用工具。输出曲线与误差指标可直观评估模型效果,便于不同参数下的对比分析。

1. 基于VMD-WOA-LSTM的光伏预测Matlab实现:先看清这套代码能解决什么

做光伏功率预测的人,大概率都遇到过同一个尴尬:原始功率序列非平稳、波动大,直接丢给LSTM训练,预测值总比实际值“慢半拍”,误差曲线看起来像延迟了一个采样周期。这套基于VMD-WOA-LSTM的Matlab实现,把变分模态分解、鲸鱼优化算法和长短期记忆网络串成了一条完整链路:先用VMD把光伏功率序列拆成不同频段的模态分量,再用WOA去搜LSTM的隐层神经元数、学习率、正则化系数这几个关键超参数,最后逐模态预测再叠加。项目代码用参数化编程思路组织,附带北半球光伏数据和现成的origin_data.mat,属于下载下来改了参数就能跑的那类资源,适合做课程设计、毕业设计,也适合想快速验证VMD+WOA+LSTM组合效果的入门研究者。

2. 拆解链路原理:VMD为什么适合光伏序列,WOA在优化什么,LSTM又在拟合什么

2.1 VMD分解:把非平稳序列拆成有限带宽的模态,而不是经验模态分解那样递归筛选

光伏功率序列受云层遮挡、温度变化影响,呈现出明显的非平稳性和多时间尺度特征。VMD的核心思路是把原始信号分解为K个具有有限带宽的固有模态函数,每个模态围绕各自的中心频率分布。VMD的分解过程是求解一个变分问题,通过交替方向乘子法迭代更新模态和中心频率。相比EMD的递归筛选方式,VMD对采样噪声的鲁棒性更好,模态混叠问题也相对可控,这对后续逐模态建模很有价值。

在光伏预测场景下,我一般建议先把日功率序列按小时分辨率做分解,观察各模态的频域范围。代码里VMD.m的输入是origin_data.mat加载出的原始序列,输出是一组IMF分量和对应的中心频率。分解前的关键参数是模态数K和惩罚因子alpha,K太小则分解不充分,高频成分和趋势成分混在一起;K太大则会出现虚假模态,后续逐个建模的时间成本也成倍增加。

常见做法是先用K=5、alpha=2000跑一轮,看各模态中心频率是否从低到高有序分布,再根据模态间的频谱重叠程度微调。代码中vmdtest.m就是用来做这个可视化验证的,它会画出各模态的时域波形和频谱,方便你判断分解效果。这比把K当作黑匣子参数直接抄作业要稳妥得多,也是后续LSTM预测精度能立住的前提。

2.2 WOA优化的对象:LSTM的隐层神经元数和学习率才是预测误差的主要贡献项

LSTM网络的预测性能对超参数敏感,隐层神经元数决定网络容量,学习率决定收敛速度和稳定性,L2正则化系数影响过拟合程度。传统做法是网格搜索或随机搜索,但这两个参数组合的搜索空间是非光滑的,且每评估一组参数就要完整训练一次LSTM,计算代价不低。WOA这类群体智能优化算法在这个场景的优势在于:它通过模拟座头鲸的包围捕食和螺旋气泡网攻击策略,在搜索空间里同时维护多个候选解,全局探索和局部开发之间的平衡能力相对更好。

项目里的WOA.m实现了完整的鲸鱼优化流程。目标函数不是直接取预测误差,而是把LSTM的验证集均方根误差作为适应度值——也就是WOA每提议一组超参数,就训练一次LSTM并返回验证集误差,迭代若干轮后收敛到一组较优参数。这个设计是合理的,但要注意:WOA的群体规模和迭代次数直接决定总训练时长。我在拆代码时看到的默认参数是种群规模30、迭代次数50,如果是30分钟粒度的光伏数据,这个规模跑完大约需要一到两小时,具体取决于机器配置和LSTM的训练轮数。

这里给出一个WOA核心迭代逻辑的Matlab示意,方便你理解参数是怎么被寻优的:

% WOA核心循环示意(节选) for t = 1:Max_iteration % 迭代主循环 for i = 1:SearchAgents_no % 遍历每个候选解 a = 2 - t * (2 / Max_iteration); % a从2线性递减到0 r1 = rand(); r2 = rand(); A = 2 * a * r1 - a; % 包围步长系数 C = 2 * r2; p = rand(); if p < 0.5 if abs(A) < 1 % 包围捕食:向当前最优解收缩 Positions(i,:) = Best_pos - A * abs(C * Best_pos - Positions(i,:)); else % 随机搜索:探索远离当前最优的区域 rand_idx = randi(SearchAgents_no); Positions(i,:) = Positions(rand_idx,:) - A * abs(C * Positions(rand_idx,:)); end else % 螺旋气泡网:沿对数螺旋路径逼近最优解 dist = abs(Best_pos - Positions(i,:)); Positions(i,:) = dist * exp(b * l) * cos(2 * pi * l) + Best_pos; end end end

这段代码里A是收敛因子,它大于1时算法倾向于全局探索,小于1时倾向于局部开发;p是随机概率,用来切换包围捕食和螺旋更新两种策略。WOA的关键参数就是SearchAgents_no(种群数)和Max_iteration(迭代次数),如果你想缩短总运行时间,优先降迭代次数而不是降种群规模,因为种群太小容易让搜索陷入局部最优。

2.3 LSTM的输入构造:模态分解后每个分量单独建模,再叠加得到最终预测

LSTM在Matlab中的使用有两种路径——2019a之前用lstm函数配合trainNetwork,2024a可以用lstmLayer搭建更灵活的网络结构。项目兼容2014/2019a/2024a三个版本,说明代码作者在层定义上做了版本兼容处理,这一点对新手很友好。我在实际查看main.m时发现,核心流程是:加载数据→data_process.m构造训练集和测试集→VMD.m分解→对每个IMF分量分别训练LSTM→叠加预测值→calc_error.m计算误差指标。

每个IMF分量在进入LSTM前需要构造输入输出对。常见做法是用滑窗方式,前若干个时刻的模态值预测下一时刻的值。窗口长度这个参数在代码里是变量num_time_steps,我通常建议设为12到24小时对应的采样点数。如果数据是每小时一个点,窗口设为24,模型能学到日周期性;如果数据是15分钟一个点,窗口设为96。VMD分解后的低频分量相对平缓,小窗口也能拟合;高频分量波动剧烈,窗口太短则信息不足,误差会明显偏大。

WOSA优化后的LSTM参数会写回预测脚本,再重新训练一次完整模型做测试集评估。这里有个值得注意的设计:WOA寻优过程中用的是验证集误差,最终评估用的是测试集误差,两者必须分开,否则相当于提前把测试集信息泄漏给超参数搜索过程,评估结果会偏乐观。

3. 从零跑通main.m:数据流、函数调用链和三个必须检查的输出文件

3.1 打开工程先做三件事:确认路径、确认数据、确认版本

解压后不要直接双击main.m就点运行。我拆过的Matlab工程里,十有七八的“一运行就报错”是路径问题。项目根目录里虽然有北半球光伏数据.xlsx和origin_data.mat两份数据,但main.m默认加载的是origin_data.mat。你需要在运行前用cd命令切换到工程目录,或者在Matlab左侧当前文件夹窗口定位到解压目录,确保工作路径正确。

版本检查也值得提前做。如果你用的是2019a,代码里如果出现trainNetwork相关的层定义不存在,大概率是因为用了2024a的层语法。项目声称兼容三个版本,但我实际测试时发现:2014版需要额外确认深度学习工具箱版本,因为LSTM在2014版还属于较新的功能。如果你只有2014版且没有Deep Learning Toolbox,建议直接把代码拿到2019a或2024a上跑。

数据格式的检查点是缺失值和归一化。data_process.m里会做归一化处理,把功率值缩放到0到1区间,这个步骤对LSTM训练非常关键。如果原始数据里有NaN,归一化后NaN不会消失,LSTM训练时会产生无效梯度。代码里没有显式的数据清洗函数,我一般会在data_process.m开头手动加两行:

data = data(~isnan(data)); % 剔除NaN data = data(data > 0); % 光伏功率不应为负

这两行的作用是保证后续分解和训练的数据都是物理上合理的正值。光伏功率为负要么是测量噪声要么是设备异常,直接剔除比重采样更安全。

3.2 main.m 的主流程:每个函数调用在做什么

main.m是整个工程的调度中心,它的调用顺序基本就是预测流程的顺序。我按自己跑通后的理解整理出调用链如下:

%% 1. 加载原始数据 load('origin_data.mat'); % 得到原始光伏功率序列 %% 2. 数据预处理:归一化 + 划分训练/测试集 [data_normalized, train_data, test_data, train_label, test_label] = ... data_process(origin_data); %% 3. VMD分解,把归一化后的序列拆成K个模态 IMF = VMD(data_normalized, alpha, tau, K, DC, init, tol); % alpha: 惩罚因子,tau: 噪声容忍度,K: 模态数 %% 4. 对每个模态训练LSTM并预测 % 循环内调用WOA搜索每个模态的最优隐层数和学习率 for k = 1:K best_params = WOA(@(params) lstm_obj_func(params, IMF(k,:)), dim, lb, ub); pred_imf{k} = train_and_predict_lstm(IMF(k,:), best_params); end %% 5. 叠加模态预测值 + 反归一化 pred_final = sum(cell2mat(pred_imf), 1); pred_actual = pred_final * (max(origin_data) - min(origin_data)) + min(origin_data); %% 6. 误差计算输出 rmse_val = calc_error(pred_actual, test_actual);

origin_data.mat里存的是原始功率序列,data_process负责归一化和划分训练集测试集,VMD输出各模态分量,WOA在LSTM目标函数上做超参数搜索,最后叠加预测并反归一化。这个流程里最容易被忽略的是第3步和第4步之间的衔接:VMD是在归一化后的数据上做的,而不是原始数据上。因为归一化到0到1区间后,各模态的幅值也在这个区间内,LSTM的激活函数天然适合这种输入范围。如果在原始数据上做VMD再归一化,虽然数学上等价,但数值稳定性差一些,尤其在alpha较大时容易出现边界振荡。

参数配置区域一般在main.m开头,包含了K、alpha、tau、种群数、迭代次数、LSTM训练轮数等。我建议第一轮跑通时全部用默认参数,先验证环境没问题,再逐步改成自己的数据和参数。

3.3 三种不同层数的LSTM实现差异

很多读者会困惑:为什么同一个项目里LSTM相关的代码看起来有多个版本?这是因为Matlab版本差异导致的。2019a的trainNetwork支持通过sequenceInputLayer、lstmLayer、fullyConnectedLayer和regressionLayer组合搭建网络,代码结构清晰;2024a则额外支持了trainedNetwork等新的接口。项目里的fun.m和Get_Functions_details.m主要是WOA的辅助函数,前者定义了目标函数的包装方式,后者提供了基准测试函数方便验证WOA实现是否正确。

如果你在2019a上运行遇到lstmLayer相关的错误,常见原因是函数名在新版本中改成了lstmLayers(复数形式),或参数输入格式从cell数组改成了矩阵。这种版本兼容性问题没有捷径,只能根据报错信息逐一调整。我的习惯是先单独跑vmdtest.m确认VMD模块正常,再用Get_Functions_details.m里的测试函数验证WOA模块正常,最后才跑完整的main.m。这样排查问题时的范围能缩小到LSTM模块,而不是整个链路混在一起翻车。

4. WOA与LSTM的接口设计:目标函数怎么写,参数化编程改哪里

4.1 目标函数的结构:把LSTM的训练封装成黑匣子返回误差

WOA要搜索LSTM的超参数组合,必须在两者之间建立一个目标函数接口。项目里的lstm_obj_func接受一组参数向量,然后完成“构建LSTM→训练→预测→返回误差”的完整流程。这个函数本质上把LSTM训练包装成了一个可被优化的黑匣子,WOA只关心输入参数和输出误差之间的映射关系,不关心LSTM内部的梯度如何传播。

function rmse_val = lstm_obj_func(params, train_x, train_y, val_x, val_y) numHidden = round(params(1)); % 隐层神经元数,WOA搜索的是连续值,需要取整 lr = params(2); % 学习率 l2reg = params(3); % L2正则化系数 % 构建LSTM层 layers = [ sequenceInputLayer(size(train_x, 1)) lstmLayer(numHidden, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer]; options = trainingOptions('adam', ... 'InitialLearnRate', lr, ... 'L2Regularization', l2reg, ... 'MaxEpochs', 100, ... 'Verbose', 0); net = trainNetwork(train_x, train_y, layers, options); % 验证集预测 pred = predict(net, val_x); rmse_val = sqrt(mean((pred - val_y).^2)); end

这个函数里值得注意的细节是round(params(1)):WOA在连续空间内搜索,但LSTM的隐层神经元数必须是整数,所以在传入网络结构前要做取整处理。学习率和L2正则化系数则直接使用连续值。这种混合处理方式是WOA调参中常见的做法。另外,训练选项里MaxEpochs不宜太大,否则WOA每评估一组参数都要花很长时间,我一般设为50到100之间,迭代次数和总时长的平衡点需要自己试。

4.2 边界设置与初始值:lb和ub决定搜索空间的上限

WOA.m的调用需要提供lb(下界)和ub(上界)参数,它们与dim(搜索维度)共同定义了搜索空间的形状。这个项目的dim是3,因为搜索的是隐层神经元数、学习率、L2正则化系数三个参数。常见的边界设置建议:隐层神经元数范围取[10, 100],学习率取[0.001, 0.01],L2系数取[0.0001, 0.01]。

上下界的设定直接影响优化效果。上界太大,LSTM隐层神经元数到几百个,训练耗时长且容易过拟合;上界太小,搜索空间受限,最优参数可能落在边界外而被截断。WOA本身没有处理边界约束的机制,代码里是通过把越界值强制拉回边界实现的。我见过有人把学习率上界设成0.1,导致LSTM在训练初期直接发散,误差不降反升,WOA寻优完全失效——这不是WOA算法的问题,是参数空间定义不合理的问题。

数据划分比例也值得关注。data_process.m里默认把前80%作为训练集,后20%作为测试集。但在光伏预测中,如果数据包含完整的一年,前80%是1月到10月,后20%是11月和12月,这两个时段的光照条件差异很大,测试误差会明显高于随机划分。我一般会先按时间顺序划分,确认模型在跨季节场景下还能保持可接受的误差,再决定是否改用滚动划分方式。

4.3 参数化编程的改法:只碰配置区,不动算法区

项目强调参数化编程,意思是代码把可调参数集中放置。打开main.m你会看到开头的参数配置区,里面有VMD的K、alpha,WOA的种群数和迭代次数,LSTM的训练轮数。修改参数时只改这个区域,不要动算法主体。这个设计对课程设计和毕业设计尤其友好——你可以在论文里写“通过控制变量法调节模态数K,观察预测误差变化”,而不需要深挖VMD内部的交替方向乘子法实现。

但要注意,参数化编程不等于所有参数都无脑暴露。VMD内部还有tau(噪声容忍度)、DC(直流分量保留开关)、init(初始中心频率策略)、tol(迭代容差)这几个参数,它们在main.m里可能没有显式配置,而是使用VMD.m内部的默认值。如果论文要求做敏感性分析,需要把这些参数也提升到配置区,否则只能改VMD.m源码。我习惯的做法是拷贝一份VMD.m到自己的工程目录再改,避免破坏原始文件。

你可视化验证参数效果时,vmdtest.m和func_plot.m是两个辅助脚本:前者输出分解后的波形图,后者画出WOA的收敛曲线。收敛曲线是判断WOA是否正常工作的重要依据——如果迭代到一半误差就停止下降且没有波动,说明算法陷入局部最优,需要增大种群规模或重新初始化。

5. 避坑与常见问题排查:跑不通、误差大、参数失效的三个根源

5.1 现象:main.m一运行就报“未定义函数或变量VMD”

原因:工作路径没有切换到工程目录。Matlab找不到VMD.m时会把错误定位到main.m调用VMD的位置,新手很容易误以为是VMD代码本身有bug。

解决:在命令行窗口执行cd '你的解压路径',然后用which VMD确认能返回VMD.m的完整路径。如果which返回空,说明文件不在当前路径也不在搜索路径中,用addpath(genpath(pwd))把当前目录及子目录全部加入搜索路径。

5.2 现象:WOA每轮迭代耗时极长,跑了一晚上还在寻优

原因:WOA内部每评估一组参数就要完整训练一次LSTM,默认的种群规模和迭代次数相乘,再加上LSTM的MaxEpochs设得过大,总计算量是三者乘积。

解决:先降MaxEpochs到30,再降迭代次数到20,跑通流程后再逐步加大。如果用的是2019a且没有GPU,LSTM训练在CPU上很慢,建议把数据长度降到1000个点左右做一次快速验证。另外检查LSTM是否用了'OutputMode','last'——如果误设为'sequence',输出维度会和标签不匹配,训练会额外消耗大量内存。

5.3 现象:预测曲线整体滞后,像实际值向右平移了一个时间步

原因:这是单步预测的经典问题。模型输入是过去N个时刻的功率值,输出是下一个时刻的预测值,当序列自相关性很强时,模型倾向于认为下一个值约等于当前值,导致预测曲线滞后于实际变化。

解决:把滑窗步长加大,或者改用多步预测结构,即输入N个时刻预测未来M个时刻。另一个有效手段是加大VMD的模态数K,把高频分量分解得更细,LSTM对高频分量的拟合能力会提升,但代价是训练时间显著增加。这个滞后问题在测试集上表现为RMSE不算太大但相关系数很高,论文里可以单独讨论。

5.4 现象:替换成自己的数据后,预测误差比示例数据大一个量级

原因:自己的数据量太少,或数据的时间分辨率与代码默认配置不匹配。示例数据是北半球逐小时光伏功率,如果你的数据是15分钟分辨率,序列长度是示例的4倍,但LSTM的num_time_steps还是24,输入窗口只能覆盖6小时而非24小时,模型学不到日周期。

解决:把num_time_steps按比例调整。15分钟数据建议设96,覆盖完整一天;如果你的数据是分钟级光伏功率,滑窗还应包含前一天同时刻的功率值作为额外特征。data_process.m的数据划分比例也要检查,测试集比例不要超过30%,否则训练不充分,替换数据后误差放大是必然结果。

5.5 现象:vmdtest.m画出的模态波形明显中断,或某个模态几乎全为负值

原因:VMD对数据边界敏感,分解时在序列两端容易出现边界效应,表现为模态的幅值在边缘异常波动。如果原始序列本身有趋势项且没有做去趋势处理,VMD会把趋势项拆到某个模态中,导致该模态均值不为零。

解决:在VMD前先对序列做去趋势处理,用detrend(data_normalized)去掉线性趋势。边界效应的更彻底处理是对序列两端做对称延拓,分解后再截断,但代码里没有内置这个功能,需要你自己在VMD.m的调用前加延拓逻辑。如果只是为了课程设计,去趋势通常已经足够让模态波形恢复正常。

6. 误差回传与最终可视化:验证误差指标、看图和收敛曲线的三个习惯

训练结束后,calc_error.m会输出RMSE、MAE等误差指标,func_plot.m会画出预测值对比曲线和WOA收敛曲线。这三类输出各有各的验证价值。我先看WOA收敛曲线是否平滑下降到稳定值,如果曲线呈锯齿状且抖动剧烈,说明目标函数噪声太大,可能是LSTM训练本身的随机性造成的。遇到这种情况,在目标函数里固定随机种子就能让收敛曲线变得平滑。

预测对比图需要重点检查两个区域:一是测试集的早晚光伏功率为零的时段,模型是否输出了负值;二是多云天气下功率急剧波动时,预测是否跟得上趋势。光伏站在夜间输出功率接近零,如果模型预测出明显负值,说明反归一化或者LSTM的激活函数设置有偏差。我一般在calc_error.m后追加几行:

pred_actual(pred_actual < 0) = 0; % 功率不可能为负 % 归一化误差指标,便于与不同量纲的数据对比 nrmse = rmse_val / (max(test_actual) - min(test_actual)); fprintf('NRMSE = %.4f\n', nrmse);

NRMSE比RMSE更适合在不同数据集之间横向对比,因为RMSE的绝对数值依赖功率量纲。论文里如果比较不同模型的预测性能,直接写RMSE会被审稿人追问数据量纲问题,但NRMSE可以部分规避这一质疑。这是我拆这个项目后养成的一个习惯:任何预测类任务,最后都补一个归一化误差指标。

另外,在完成替换数据、重跑参数之后,我建议把每次实验的WOA收敛曲线截图存档。同一个项目用不同K值跑出来的最优超参数往往差异很大,比如K=5时WOA找到的隐层神经元数是45,K=8时变成78,这个变化本身就是论文里可以分析的现象。如果没有存档习惯,事后想回溯某个参数组合对应的误差,就只能重跑一遍,浪费时间。从那以后我每次跑这套VMD-WOA-LSTM流程,都会强制走一遍:先开存档记录参数配置和误差指标,再看收敛曲线判断寻优是否正常,最后检查预测图末尾的功率归零段。希望这套检查习惯对你有用。

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