简介:面向光伏功率预测场景,这份Matlab完整源码包基于CNN-GRU卷积神经网络与门控循环单元实现多变量多步预测。作者为博客专家“机器学习之心”,代码采用参数化编程,注释清晰,便于修改网络结构和超参数,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计。
包体共7个文件,含2个.m主程序与误差计算脚本、1个.mat模型文件、1个xlsx光伏数据、2个png预测结果图和1个txt说明,整体2.46MB,轻量易用。运行环境为Matlab2023及以上,输出预测图、误差图及R2、MAE、MSE、RMSE等指标,其中因真实功率含0导致MAPE无穷大的情况已在说明中提示。压缩包目录清晰,主程序、数据、结果图分门别类存放,便于直接运行和二次开发。
当前已有101人学习该资源。通过完整源码可快速复现CNN-GRU光伏功率预测流程,同时获得数据预处理、多步预测建模与误差评估的实践思路,非常适合入门深度学习时序预测并希望动手验证效果的学习者。
1. 光伏功率预测为什么需要CNN-GRU?——先破除“模型越深越准”的错觉
光伏功率预测这件事,电网侧考核和电站侧运维的需求差异很大。超短期预测覆盖未来15分钟到4小时的功率曲线,调度用它做有功控制,储能策略也依赖它。晴天条件下,一个带辐照度外推的线性模型就能把RMSE压得不错;真正让传统方法失效的是云团遮挡导致的辐照度骤变——数值天气预报的时间分辨率跟不上,物理模型只能给出趋势性估计。数据驱动的深度学习方案不写显式公式,它把历史辐照、温度、湿度、风速、功率直接喂给网络,让模型自己学出“云来了功率会怎么掉”这类非线性映射。
CNN-GRU在这个任务里的价值是分工明确:CNN用一维卷积提取多变量时间窗口内的局部变化特征,GRU用门控机制把特征序列按时间顺序串起来。相比纯CNN,GRU弥补了卷积感受野对长程时间依赖刻画不足的问题;相比纯LSTM,GRU参数少、收敛快,在电站级几千到几万条样本上不容易过拟合。对正在用Matlab做数据驱动功率预测的工程师来说,下面这条从理论到评估的完整路径——模型原理、样本构造、训练配置、多步预测验证——每一步的参数选择逻辑我都会掰开讲,新手能照着跑,老手也能在边界条件和坑点上找到对照。
2. CNN与GRU各司其职:一维卷积做特征提取,门控单元学时序规律
2.1 CNN一维卷积如何处理多变量时间序列
多变量预测的输入本质是一个时间窗矩阵:行是变量(辐照度、温度、湿度、历史功率),列是时间步。Matlab的Deep Learning Toolbox里,输入张量按(特征数, 时间步, 样本数)组织,即(C, T, N),跟Python习惯的(样本数, 时间步, 特征数)顺序不同。sequenceInputLayer的第一个参数就是C,不是时间步。很多从Python切到Matlab的人第一次跑维度错误就出在这里。
卷积层采用convolution1dLayer,它沿时间轴滑动卷积核,对局部时段做加权求和。卷积核宽度设为3,表示每次看相邻3个时间点的联合变化;16个卷积核会生成16张独立特征图,分别对应云层遮挡、温度爬升等不同局部形态。Matlab中网络结构的前半段是这么写的:
numFeatures = 6; % 辐照度、温度、湿度、风速、历史功率等6个变量 layers = [ sequenceInputLayer(numFeatures, 'Normalization', 'none') convolution1dLayer(3, 16, 'Padding', 'same') reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 32, 'Padding', 'same') reluLayer gruLayer(64, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer ];两层卷积之间用了maxPooling1dLayer(2, 'Stride', 2),时间维度下采样一半。15分钟采样、窗口96点时,池化后剩48点,GRU计算量明显下降;但极端天气下的短时突变也可能被池化抹平。我的做法是第一层卷积后不急着池化,第二层卷积后再池化,保留更多原始时间细节。Padding, 'same' 让卷积输出保持和输入相同的时间长度,避免时间轴收缩导致信息丢失。
GRU层的OutputMode参数要单独说。'last'只取最后一个时间步的隐状态,适合回归任务只输出一个值的情况;'sequence'会保留每个时间步的隐状态,适合后面接全连接层做多步输出。两种写法的参数量差别不大,但'last'训练时收敛更快,内存占用也更小。
2.2 GRU门控单元:更新门和重置门各管什么事
GRU是LSTM的简化变体,去掉单独的细胞状态,只保留两个门。更新门决定上一时刻的隐状态以多大比例带入当前时刻,重置门决定当前候选状态受过去信息的控制程度。更新门偏向1时,网络学习的是长程依赖;重置门偏向0时,网络倾向于只看当前输入。光伏功率信号既有强日周期(昨天同一时刻的出力对今天有参考价值),又有突发扰动(云层遮挡导致分钟级跌落),两个门配合能同时兼顾周期性和突变性。
与LSTM相比,GRU少了一组门,参数量约减少四分之一。电站级数据集通常只有几千到几万条样本,LSTM的三门结构在这个量级上更容易过拟合,GRU的参数规模更匹配。两个模型在Matlab里的对比如下:
| 模型 | 核心结构 | 参数量(64隐单元) | 小样本表现 | 训练速度 |
|---|---|---|---|---|
| LSTM | 遗忘门、输入门、输出门 | 约 3.3 万 | 容易过拟合 | 较慢 |
| GRU | 更新门、重置门 | 约 2.5 万 | 更稳 | 较快 |
我还会在GRU层后加dropoutLayer(0.2),这是深度学习时序模型防过拟合的标准做法。需要注意的是,dropout在训练时生效、验证和预测时不生效,所以训练曲线的损失通常明显低于验证曲线,这是正常现象,不代表模型实现有问题。
2.3 多步预测的三种思路在搭网络前就要定下来
多步预测的实现策略直接影响训练标签的构造,必须在训练前定下来。递归法把下一步预测值当成已知输入再预测下下步,实现简单,但误差会逐步累积;直接法为每一步单独训练一个模型,精度可控但训练成本成倍增加;多输出法让输出层直接有horizon个神经元,一次前向传播得到未来若干步,Matlab里实现最方便,也是本文第5章展开的方式。光伏预测任务里,多输出法还有额外的好处:horizon个输出共享底层的CNN-GRU特征提取器,参数效率高,且每一步的预测都在同一特征空间上,不会出现递归法中误差越滚越大的问题。
3. 样本集构造:把气象记录和功率序列变成Matlab能吃的训练数据
3.1 滞后窗口与预测步长:96点窗口还是48点窗口
数据采样性质决定窗口和步长的选择。并网光伏电站的采集系统大多数是15分钟一个点,4小时预测对应horizon=16步;也有5分钟采样、1小时预测对应12步的数据。horizon不是越大越好,而是取决于考核要求——超短期光伏功率预测的标准区间是未来0到4小时,short-term预测才看未来1到3天。
滞后窗口的选择要看光伏时序的自相关结构。光伏出力有强日周期,昨天同一时刻的功率和今天同一时刻高度相关,窗口至少要覆盖一个完整日周期,即96个15分钟点。如果数据量有限,48点也能跑,但早晚功率陡升陡降时段的误差会明显增大。一个可参考的判断方法:画出功率序列的自相关图,找到自相关系数降到0.1以下的时间滞后,窗口至少覆盖这个滞后长度。
训练集多变量输入矩阵的典型列布局如下:
| 列 | 变量 | 单位 | 典型范围 | 说明 |
|---|---|---|---|---|
| 1 | 水平面辐照度 | W/m² | 0~1200 | 云层影响最大的变量 |
| 2 | 环境温度 | ℃ | -10~45 | 影响组件的温度特性 |
| 3 | 湿度 | %RH | 0~100 | 云层与降水的间接指标 |
| 4 | 风速 | m/s | 0~25 | 影响组件散热与云团移动 |
| 5 | 历史功率 | kW | 0~额定功率 | 模型主要的记忆来源 |
3.2 用Matlab切窗的代码与数据结构
切窗函数把连续时序切成(特征数, 时间步)的样本集合。Matlab中trainNetwork接受cell array形式的序列数据,每个cell是一个样本。下面这个函数是完整可复用的:
function [X, Y] = buildSamples(data, winSize, horizon) % 输入: % data: 数值矩阵, 每行为一个采样时刻, 最后一列为功率 % winSize: 滞后窗口长度, 例如96表示过去24小时(15min采样) % horizon: 预测步长, 例如4表示预测未来1小时 % 输出: % X: cell数组, 每个元素是 (特征数×winSize) 的矩阵 % Y: 列向量, 每个元素是未来horizon步的功率 numSamples = size(data,1) - winSize - horizon + 1; X = cell(numSamples, 1); Y = zeros(numSamples, 1); for i = 1:numSamples X{i} = data(i:i+winSize-1, :).'; % 转置为 特征数×时间步 Y(i) = data(i+winSize+horizon-1, end); end end切窗索引是这段代码最容易错的地方。i: i+winSize-1取的是输入窗口,i+winSize+horizon-1取的是预测目标时刻。如果写成i+winSize+horizon,会把目标整体往后错一位,样本数和真实对应关系全乱。调用buildSamples之后,样本总数为numSamples,按时间顺序前70%~80%划为训练段,后20%~30%划为测试段。测试段必须是最末尾的时间区间,不能随机抽样。
3.3 归一化与反归一化:训练集统计量不能重新计算
多变量的数值范围差异很大:辐照度是0到1200,湿度是0到100,功率是0到额定值。GRU内部用sigmoid和tanh激活,不归一化时梯度在深层传播中容易消失或爆炸。光伏预测最常用的归一化是mapminmax,把数据放缩到[0,1]区间,关键是归一化必须在切窗之前完成,并且验证集和测试集必须复用训练集的统计量:
% 按特征列归一化, 保存每一列的ps结构 dataN = zeros(size(data)); for c = 1:size(data,2) [dataN(:,c), psX(c)] = mapminmax(data(:,c)', 0, 1); dataN(:,c) = dataN(:,c)'; end % 切窗 [Xall, Yall] = buildSamples(dataN, winSize, horizon); % 按时间顺序划分, 不能用randperm! numTrain = floor(length(Xall) * 0.7); Xtrain = Xall(1:numTrain); Ytrain = Yall(1:numTrain); Xtest = Xall(numTrain+1:end); Ytest = Yall(numTrain+1:end);注意:验证集/测试集的归一化直接复用训练集的
psX进行mapminmax('apply', ...),而不是对整段序列重新计算最大最小值。后者会把测试集的分布信息泄漏到训练过程里,导致评估结果虚高。这是时间序列预测最常见的隐藏错误。
预测完成后,模型输出是归一化域的结果。反归一化时调用mapminmax('reverse', Ypred, psX(end)),其中psX(end)保存的是功率列的统计量。很多人在画预测曲线时发现预测值整体偏离真实功率数倍,八成就是漏了这一步。
3.4 验证集与序列划分:随机打乱是时间序列的大忌
分类任务里随机打乱样本是标配,但时间序列预测绝不能这么做。打乱后训练集里混进了未来信息,模型相当于提前看到了答案,测试集上的RMSE会异常好看,部署到线上立刻原形毕露。正确划分是:原始数据按时间顺序,前70%做训练,训练段末尾10%劈出来做验证,最后30%做测试。trainingOptions里的'ValidationData'指定验证集,训练过程中模型会持续监测验证损失,配合早停返回最优权重。
4. 训练CNN-GRU:网络搭建、训练选项与收敛判断
4.1 完整可运行的训练流程
把前面3章的产物串起来,一份最小可运行的Matlab训练脚本如下:
% 假设已经通过buildSamples和归一化得到: % Xtrain, Ytrain, Xval, Yval, Xtest, Ytest numFeatures = size(Xtrain{1}, 1); layers = [ sequenceInputLayer(numFeatures) convolution1dLayer(3, 16, 'Padding', 'same') reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 32, 'Padding', 'same') reluLayer gruLayer(64, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(1) regressionLayer ]; options = trainingOptions('adam', ... 'MaxEpochs', 80, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, ... 'ValidationData', {Xval, Yval}, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'OutputNetwork', 'best-validation', ... 'Plots', 'training-progress', ... 'Verbose', false); net = trainNetwork(Xtrain, Ytrain, layers, options);这段脚本里每个选项都有实际影响。'Shuffle', 'every-epoch' 让每个epoch训练样本顺序重排,避免样本先后顺序引入额外偏差;'OutputNetwork', 'best-validation' 让训练返回验证损失最小时的网络,而不是最后一个epoch的网络,这对光伏这类波动较强的数据很关键——最后一个epoch往往已经过拟合;'ValidationFrequency', 20 表示每20次迭代计算一次验证损失,太频繁会拖慢训练,太少则无法及时发现过拟合。
4.2 超参数选不好,模型再新也白搭
CNN-GRU在光伏预测上的超参数,实践中存在一组可迁移的经验区间。下表是我在多个电站数据集上测试过的配置范围:
| 超参数 | 常用范围 | 推荐起点 | 调整逻辑 |
|---|---|---|---|
| 卷积核尺寸 | 3~5 | 3 | 越大感受野越长,但参数和过拟合风险同步上升 |
| 第一层卷积核数 | 8~32 | 16 | 过少特征提取不足,过多容易学习到噪声 |
| 第二层卷积核数 | 16~64 | 32 | 一般按第一层的2倍递增 |
| GRU隐单元数 | 32~128 | 64 | 序列越长、变量越多,取值越靠上 |
| 初始学习率 | 0.01~0.0005 | 0.001 | Adam配合0.001通常是最稳的起点 |
| MiniBatchSize | 16~64 | 32 | 样本少时偏小,样本多时调大 |
| Dropout | 0.1~0.4 | 0.2 | 验证损失偏离训练损失越大,越要调大 |
逻辑上要记住一个反直觉点:光伏预测里,隐藏单元数和模型精度不是单调正相关。GRU隐单元从64加到128,训练损失确实下降,但验证集上的表现往往持平甚至更差——小样本深度模型在特征维度上率先触达瓶颈,再加参数只是让模型记住训练集的天气形态。
4.3 判断收敛的三个信号和两个坑
打开'Plots', 'training-progress' 后,左侧是损失曲线,右侧是RMSE曲线。收敛正常的信号有三个:训练损失与验证损失同步下降且最后进入平台期;验证损失没有在某个epoch后明显反弹;RMSE曲线进入波动区间后不再创新低。如果验证损失连续十几个epoch稳步上升,训练损失还在降,模型已经过拟合,优先调大dropout,而不是加数据增强。
两个常见的坑,分别对应训练崩溃和性能假象:
坑一是损失曲线直接出现NaN。光伏数据里混入了传感器异常值,比如夜间辐照度出现负数、功率值在部分时段跳零,这些脏数据会在归一化后变成离群点,拉爆梯度。排查方法:对原始数据绘制每列的最小值/最大值直方图,把异常采样时刻整行剔除,再用线性插值填补缺测时段。
坑二是GPU内存不足报错。Matlab默认数据是double,深度学习训练用single就够,把输入转成single(Xtrain)能省一半显存;再不行就把MiniBatchSize从32降到16。了解这些默认行为,比盲目堆显存换显卡更实际。
4.4 验证损失和测试损失的关系要提前讲清
验证损失是训练过程中用于早停的指标,测试损失是训练结束后在完全没见过的时间段上评估的指标。电网考核关注的是测试段表现,所以训练过程中如果发现验证损失和测试损失差距很大,优先回查第3章的归一化是否用了全序列统计量——这个数据泄漏问题会在测试集上制造出“模型很准”的假象。
5. 多步预测实践:从一步预测到未来四小时,递归滚动的误差拆解
5.1 三种多步预测在Matlab里的落地差异
训练完成后,predict(net, X)只能输出一步预测。要做多步,递归法把当前样本窗口右移、用预测值填充功率列,得到下一样本;直接法为每个horizon单独训练模型,比如horizon=16就训练16个不同的CNN-GRU;多输出法在输出层直接用horizon个神经元,一次前向传播输出整个序列。工程上最推荐多输出法,因为共享特征提取器、训练一次就行,但Matlab的多输出需要把fullyConnectedLayer(1)改为fullyConnectedLayer(horizon),训练标签Y变成numSamples×horizon的矩阵,回归层要求每个样本输出一个向量。
5.2 递归滚动预测:Matlab实现与误差累积检验
如果模型已经按单步输出训练好,可以用递归法做验证。以下代码模拟未来horizon步的滚动预测,并计算每一步的RMSE,用来检视误差随预测步长的累积速度:
numTest = length(Xtest); horizon = 4; % 预测未来4个点, 15min采样即1小时 predMat = zeros(numTest, horizon); trueMat = zeros(numTest, horizon); for i = 1:numTest xw = Xtest{i}; % 特征数×winSize for h = 1:horizon yhat = predict(net, {single(xw)}); predMat(i, h) = yhat; % 窗口右移: 去掉最老一列, 新列由已知气象变量最后值和预测功率拼接 newCol = [xw(1:end-1, end); yhat]; xw = [xw(:, 2:end), newCol]; end % 真实值从yTest对应位置取, 构造trueMat end这段代码里xw(1:end-1, end)取的是当前窗口最后一列的前 numFeatures-1 行,也就是气象变量在当前时刻的值;yhat是刚预测出的功率,拼在最后一行作为功率的新值。注意:这个递归做法做了一个近似假设——未来时刻的气象变量沿用当前已知时刻的值。模型在晴空平稳时,这个近似误差很小;在云团快速移动时段,误差会明显放大。因此递归预测的逐点RMSE曲线如果显示第3、4步误差陡增,说明模型主要误差来自气象输入的不确定性,而不是网络结构本身。
5.3 评价指标:整体RMSE会掩盖突变时段的真实水平
整体RMSE是电网考核的底线,但它只给一个数字。光伏功率在两个时段有特殊的误差特征:夜间功率为0,样本落在回归线的0值附近,大量接近0的误差会稀释白天的真实误差;早晚的功率陡升陡降时段,误差往往是全天最大的。建议按小时分桶计算RMSE,看误差分布而不是只看总体:
rmsePerHour = zeros(24, 1); for hour = 0:23 idx = (testHour == hour); rmsePerHour(hour+1) = sqrt(mean((predMat(idx) - trueMat(idx)).^2)); end bar(0:23, rmsePerHour);这个分时误差柱状图比单一RMSE更有诊断价值。如果曲线呈“U型”分布,早晚时段误差明显高,常见应对是提高该时段的采样权重,或者在训练时对非零功率时段单独计算损失,而不是让夜间零功率主导整个优化方向。
5.4 用analyzeNetwork检查网络结构,替代盲调参
验证模型设计的最后一步,是查看每一层的输出尺寸和参数量。Matlab的analyzeNetwork(net)会列出每一层的激活尺寸、可学习参数量和层连接关系。检查的重点是:池化层前后的时间维度是否按预期减半;GRU层输出维度是否匹配后面的全连接层输入;整个网络的可学习参数量是否和数据集规模匹配。如果参数量超过样本量的10倍,训练几乎必然会过拟合,这时优先减少GRU隐单元或删掉第二层卷积,而不是加正则化硬扛。
本文还有配套的精品资源,点击获取