TCN-LSTM光伏功率预测实战:Matlab实现因果卷积与多步预测
2026/9/12 10:08:33 网站建设 项目流程

简介:面向光伏功率预测场景,这套资源提供TCN-LSTM组合模型的多变量多步预测完整Matlab方案。代码需Matlab2023及以上环境,可自动输出预测图、误差图及R2、MAE、MSE、RMSE等评价指标,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业与毕业设计。压缩包共10个文件、2.47MB,包含4个m脚本、2个mat数据、1个xlsx光伏数据集、2张结果展示图及1份txt说明文档,整体采用参数化编程风格,参数易改、注释明确、思路清晰,适合二次开发。已有69人学习浏览。资源附带真实光伏数据,预处理、训练、评估与可视化模块齐备,说明文档特别提示MAPE指标因真实值含零而不可用,可帮助快速复现实验、理解TCN与LSTM融合机制,也可作为论文实验或项目验证的基础框架。

1. TCN-LSTM做光伏功率预测,先守住因果边界

光伏功率预测做久了你会发现,模型精度上不去的头号原因往往不是网络不够深,而是数据边界没守住:训练时用到的一些信息,到推理那一刻其实根本拿不到。TCN-LSTM这种混合结构,把时间卷积网络TCN的长程感受野和长短期记忆网络LSTM的门控记忆拼在一起,专门处理多变量多步预测这类带强周期和突发波动的序列问题。我按平时在Matlab里搭这套模型的顺序来讲:先说清TCN-LSTM为什么适合光伏功率预测,再给一套能从CSV直接跑起来的数据管道和网络前向实现,接着对比多步输出的几种做法和关键超参数,最后落在滚动验证与滞后排查上。适合正在做新能源功率预测、负荷预测的工程师,以及想把基线RNN模型换成混合结构的算法岗。

2. TCN-LSTM原理:从膨胀感受野到多变量多步预测建模

2.1 TCN为什么能覆盖光伏的日内周期和云层突变

光伏序列有天然的日内双峰,早上和晚上接近零,中午随太阳高度角爬升,这是确定性很强的周期成分;真正难预测的是云层遮挡导致的分钟级波动,辐照度可以在几十秒内掉一半,功率跟着出现锯齿状跌落。TCN在这个场景里有两个别的方法替代不了的性质:因果卷积和膨胀感受野。

因果卷积的意思是t时刻的输出只由t时刻及更早的输入计算,不会把未来的样本点混进来,这一点在滚动预测里直接关系到训练和推理的一致性。膨胀卷积做的事情是让每个卷积核的采样点之间隔开d个位置,d就是膨胀因子。TCN感受野的计算公式是:

RF = 1 + Σ (k - 1) * d_i

其中k是卷积核大小,d_i是第i层膨胀因子。假设采样间隔是15分钟,一天96个点,用核大小3、膨胀因子[1,2,4,8]的四层TCN,代入公式得到RF = 1 + 2 × (1+2+4+8) = 31,只覆盖不到8小时。要让模型看见至少一个完整的日周期,我一般会把膨胀因子加到[1,2,4,8,16,32],此时RF = 127,对应约31小时历史信息,日内曲线能完整覆盖。先算一遍RF再定层数,比拍脑袋堆残差块可靠得多。

2.2 LSTM在TCN输出上的角色与两种组合方式

TCN把每个时间步都编码成同样维度的高级特征,但它没有天然的“选择性记忆”机制,残差块对每个时间步做的是同一种非线性映射。光伏序列里,云层突变前的几个点可能比一天前的同一时刻更重要,这种“动态决定依赖谁”的能力恰好是LSTM门控部分提供的。

TCN和LSTM的组合常见有两种。串行结构是先让TCN对原始多变量序列做多尺度卷积,把输出序列送进LSTM,再接全连接输出;并行结构是TCN与LSTM各读一份输入,最后拼接特征。光伏功率预测的输入都是同一组气象和功率序列,没有模态差异,串行结构参数更少、训练更稳,是绝大多数论文和工程实现默认的选择。做对比实验时,很多人会把BiLSTM版本也跑一组当基线,这也正常,但基线模型感受野不足的问题在BiLSTM里同样存在,该暴露的滞后依然会暴露。TCN在前端先把时间依赖压缩过,LSTM处理的序列变短、噪声变少,门控记忆的压力才真正降下来。

2.3 多变量多步预测的问题定义与滑窗样本生成

多变量指的是输入不只功率本身,还有辐照度、温度、湿度、风速等;多步指的是输出未来H个时刻的功率,而不是只预测下一个点。形式化地说,给定历史窗口[X_t, X_{t-1}, …, X_{t-L+1}],其中每个X是包含功率和气象变量的特征向量,模型输出[P_{t+1}, P_{t+2}, …, P_{t+H}]。下一步就是把原始表数据切成这种“输入窗口+输出窗口”的有监督样本。

function [X, Y] = makeSlidingWindows(data, featCols, powerCol, seqLen, horizon) % data: 按时间升序的表格或矩阵 % featCols: 特征列索引,包含历史功率、辐照度、温度等 % powerCol: 功率列的列号,用于构造输出 % seqLen: 历史窗口长度,horizon: 预测步长 if istable(data) data = table2array(data); end N = size(data, 1); numSamples = N - seqLen - horizon + 1; X = zeros(seqLen, numel(featCols), numSamples); Y = zeros(horizon, numSamples); for i = 1:numSamples X(:, :, i) = data(i:i+seqLen-1, featCols); Y(:, i) = data(i+seqLen:i+seqLen+horizon-1, powerCol)'; end end

参数说明:seqLen取48在15分钟采样下等于12小时历史,能覆盖上午到下午的趋势;horizon取6对应未来1.5小时,适合超短期滚动发布。功率列可以同时出现在featCols和powerCol里,历史功率本身就是强特征。判断seqLen是否合理,看训练样本数numSamples是否还够,如果窗口和预测步长把数据吃掉一半以上,就要考虑减小seqLen。切样本时严格按时间顺序逐窗滑动,不要随机抽样,后面验证阶段用的也是同一套滑窗逻辑,训练和推理的窗口重叠方式保持一致。

2.4 超短期和短期光伏功率预测在建模上的差别

超短期光伏功率预测通常指未来15分钟到4小时的功率曲线,对滚动预测的实时性和误差衰减速度要求很高;短期预测则指次日或未来三天,更依赖数值天气预报NWP提供的辐照度预报。同一个TCN-LSTM网络两种任务都能做,但输入特征差别很大:超短期以历史功率和地基辐照度仪为主,短期必须把NWP辐照度预报列进特征。如果只给历史序列预测第二天中午,那么无论膨胀因子怎么调,模型都没有信息来源,这一点要在选特征时先想清楚,网络结构再先进也补不了缺失的输入。

3. Matlab数据管道与因果TCN前向实现

3.1 CSV导入、时间戳特征与缺失值处理

Matlab读光伏数据最常见的就是readtable直接进CSV,然后按时间排序。读取后第一步处理物理上不可能的值:光伏功率不会为负,辐照度为0时功率在夜间也应当是0附近的小值。

opts = detectImportOptions('pv_plant.csv'); data = readtable('pv_plant.csv', opts); data.Properties.VariableNames = {'Timestamp','Power','GHI','Temp','Humidity','WindSpeed'}; data.Power(data.Power < 0) = 0; % 功率不可能为负 data.Power = fillmissing(data.Power, 'linear'); t = data.Timestamp; data.Hour = hour(t) + minute(t)/60; % 10:30 -> 10.5 data.DaySin = sin(2*pi*data.Hour/24); % 周期编码,避免0点跳变 data.DayCos = cos(2*pi*data.Hour/24);

逻辑说明:fillmissing用线性插值补短时间的通信掉线;超过2小时的大段缺失不建议插值,直接丢弃这段样本。Hour、DaySin、DayCos不需要先把时间戳转成字符串再拼特征,直接拆成数值特征对网络更友好。sin和cos一起用是因为单纯用小时数值的话,0点和24点在数值上相差24,物理上却相邻,正弦余弦编码把一天映射到单位圆上,天然保留循环连续性。如果数据跨年,用一年中的第几天再做一组sin/cos,把季节周期也编码进去。

3.2 归一化与按时间顺序切分

归一化我用zscore,功率、辐照度、温度等每个特征单独算均值和标准差。这里有一个关键要求:验证集和测试集必须复用训练集算出来的均值和标准差,不能各自重新归一化。时间序列尤其不能随机打乱后用randperm切分,光伏数据跨天自相关强,随机打乱会把同一天的数据同时扔进训练和测试,等于提前泄露答案。我一般按时间顺序切成70%训练、15%验证、15%测试,测试集固定用最后那段连续天数。

mu = mean(Xtrain, [1 3]); % 对每个特征列求均值 sg = std(Xtrain, 0, [1 3]); Xtrain = (Xtrain - mu) ./ sg; Xval = (Xval - mu) ./ sg; % 只用训练集的mu/sg Xtest = (Xtest - mu) ./ sg;

这里Xtrain的维度是seqLen×numFeatures×numSamples,mean的[1 3]表示沿时间和样本两个维度求均值,得到每个特征的标量。功率如果事先除以装机容量得到的是0到1之间的标称值,归一化照做,不影响后验。

3.3 用dlarray和dlconv实现因果膨胀卷积块

Matlab里convolution1dLayer没有直接的因果选项,这是很多人在层图里搭不出标准TCN的原因。常见做法是绕开trainNetwork,用dlarray配合dlconv写前向,把样本组织成时间×特征×批次的SCB格式,卷积核在时间维度上滑动,和图像上的二维卷积是同一个函数。

function Xp = causalPadTime(X, dilation, k) % 只在时间维(第一维)左侧补零,保证因果性 padLen = dilation * (k - 1); Xp = padarray(X, [padLen, 0, 0], 'pre'); end function Z = tcnResBlock(X, W1, b1, W2, b2, k, d, Wproj, bproj) % X: seqLen x numFeatures x batchSize,普通数组 % W1, W2: 卷积核,尺寸 k x C x F,C是输入特征数,F是滤波器数 % k: 核大小,d: 膨胀因子 Xp = causalPadTime(X, d, k); Xd = dlarray(Xp, 'SCB'); % S=时间,C=特征,B=batch Z = dlconv(Xd, W1, b1); Z = relu(Z); Zp = dlarray(causalPadTime(extractdata(Z), d, k), 'SCB'); Z = dlconv(Zp, W2, b2); if ~isempty(Wproj) Xr = dlconv(Xd, Wproj, bproj); % 1x1卷积对齐通道 else Xr = Xd; end Z = relu(Z + Xr); end

逻辑说明:causalPadTime补的padLen恰好等于当前层感受野向过去延伸的长度,补完后用无padding的卷积,输出长度和输入一致。第一个残差块输入通道数和滤波器数不一致,需要Wproj做1×1卷积把残差支路对齐,之后每个块输入输出通道一致就不用再投影。膨胀因子d从第一块开始依次取1、2、4、8。用extractdata先取数据再padarray,是因为padarray不接受dlarray,写完前向后用dlfeval自动微分即可,不需要自己写反向传播。

3.4 残差连接与序列长度对齐的细节

TCN块内残差连接有一个容易忽略的点:如果输入序列长度不是卷积核和膨胀因子的整数倍,补零后裁剪会让残差相加维度对不上。最简单的方式是保证所有输入序列固定seqLen不变,每一层按公式算好padLen后卷积输出长度天然等于seqLen,残差相加没有裁剪问题。另一个常见错误是把普通卷积的Padding='same'当成因果,'same'是两侧对称补零,卷积输出每个时间步都会看到未来半个卷积核的信息,在自回归训练里会造成训练推理不一致。自定义训练循环版按上面代码写,就能完全避开这两个问题。

4. 多步输出策略与TCN-LSTM训练参数设置

4.1 三种多步预测策略对比

策略做法误差累积训练代价推荐场景
直接多步输出层直接给H个神经元超短期光伏滚动预测,H较小时首选
递归多步预测值当输入再预测下一步有,随步长放大长周期粗略趋势
多模型每个步长训练一个模型H倍步长间相关性弱的场景

直接多步训练时一次算出所有步长误差,超短期4小时内完全够用;递归多步的误差累积在云层突变时会被放大,一步错后面全错;多模型代价高,一般不必要。下面的层图和训练选项都按直接多步来写。

4.2 trainNetwork快速基线:层图与维度坑

先给一个能直接在Matlab里跑的基线:非因果卷积加LSTM加全连接。注意sequenceInputLayer期望每个样本是features×time,而自定义训练循环里我们用的是time×features,数据入口方向是反的,这里很容易报维度错误。

numFeatures = size(Xtr, 2); horizon = 6; seqLen = 48; XTrain = cell(size(Xtr, 3), 1); for i = 1:numel(XTrain) XTrain{i} = Xtr(:, :, i)'; % 转成 features x seqLen end YTrain = Ytr'; % samples x horizon layers = [ sequenceInputLayer(numFeatures) convolution1dLayer(5, 32, 'Padding', 'same') reluLayer lstmLayer(64, 'OutputMode', 'last') fullyConnectedLayer(horizon) reluLayer % 输出非负功率 regressionLayer ]; options = trainingOptions('adam', ... 'MaxEpochs', 60, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.001, ... 'GradientThreshold', 2, ... 'ValidationData', {XVal, YVal}, ... 'Plots', 'training-progress'); net = trainNetwork(XTrain, YTrain, layers, options);

这里输出层故意加了一个reluLayer,保证预测功率不会出现负值;如果要限制上限,用functionLayer包一个min(max(x,0),cap),cap设为装机容量。卷积层用'Padding','same'在这条路径下不会导致严格意义上的未来泄漏,因为滑窗截止在t时刻,卷积输出取最后一个位置时用不到t之后的数据;但它的感受野结构和真正的因果TCN不同,换短窗口时建议还是回退到dlarray版本。老版本Matlab对sequenceInputLayer接convolution1dLayer支持不完整,遇到不支持时把每个样本转成二维矩阵,用featureInputLayer接收即可。

4.3 TCN-LSTM超参数表与损失函数选择

参数常见取值调整方向
TCN膨胀因子1, 2, 4, 8数据波动快就加一档
卷积核大小3 ~ 7大核覆盖长周期,小核抓细节
每层滤波器数32 / 64特征少时32够
TCN块数2 ~ 3超过3收益递减
LSTM层数1 ~ 22层注意加dropout
LSTM单元数64 ~ 128看样本量
dropout0.1 ~ 0.3防止过拟合训练集
初始学习率0.001训练不稳降一半
梯度裁剪1 ~ 2防NaN必开

损失函数默认用MSE,它对大幅误差的惩罚重,符合并网考核对坏点的关注;如果数据里毛刺多,Huber损失更稳。想在损失中加一阶差分惩罚让预测曲线不抖,需要走自定义训练循环,trainNetwork固定了loss形式。

4.4 自定义训练循环骨架与梯度裁剪

完整训练代码可以放进一个trainTCNLSTM.m,前向部分用3.3的tcnResBlock串上dlstm和全连接,核心训练循环如下:

for epoch = 1:maxEpochs for i = 1:numIter [Xb, Yb] = nextBatch(trainData, i, batchSize); [loss, grads] = dlfeval(@modelLoss, Xb, Yb, params, horizon); % 梯度裁剪:L2范数超过阈值就等比缩放 gNorm = 0; f = fieldnames(grads); for j = 1:numel(f) gNorm = gNorm + sum(grads.(f{j})(:).^2); end gNorm = sqrt(gNorm); if gNorm > gradThreshold scale = gradThreshold / gNorm; for j = 1:numel(f) grads.(f{j}) = grads.(f{j}) * scale; end end [params, avgGrad, avgSqGrad] = adamupdate(params, grads, ... avgGrad, avgSqGrad, iter, learnRate); end end

参数说明:modelLoss内部用dlfeval自动微分,返回loss和grads结构体;avgGrad和avgSqGrad初始化为与params同结构的零值,adamupdate会维护这两个动量项。梯度裁剪对LSTM尤其重要,光伏数据里偶发的辐照度尖峰会让梯度过大,不裁剪时训练中期容易出现NaN。注意dlstm的输入格式是CBT(特征×批次×时间),前面causalPadTime输出的是SCB,进入dlstm前要做一次permute并重新指定dlarray格式,这一步是新手最容易报错的地方。版本不同dlstm的参数名可能有差异,写代码前用doc dlstm确认当前版本的签名。

5. 滚动验证与nRMSE:光伏功率预测的收尾技巧

5.1 用滚动验证看误差随提前量的变化

多步预测不能只看平均RMSE,我一般按预测步长分别统计误差。常见做法是把测试集窗口逐点滚动,每个点都输出horizon步预测,然后按步长分组算nRMSE(RMSE除以装机容量)。nRMSE的好处是去掉电站规模影响,不同装机容量的电站能横向对比。

function rmseStep = rollingValidate(net, Xtest, Ytest, horizon) nTest = size(Xtest, 3); predAll = zeros(horizon, nTest); for t = 1:nTest % 输入仍然是历史窗口,输出未来horizon步 predAll(:, t) = predict(net, Xtest(:, :, t)); end err = predAll - Ytest; rmseStep = sqrt(mean(err.^2, 2)) / installedCapacity * 100; end

参数说明:installedCapacity是电站装机容量,比如5 MW,乘以100的意思是nRMSE按百分比输出。预测步长1到horizon的误差曲线如果前两三步涨得特别快,通常是模型过度依赖最近值;如果整体平移偏高,检查归一化时是不是把未来均值算进去了。

5.2 预测滞后与NaN的排查清单

光伏预测最常见的失败模式是预测曲线比实测晚一个采样点。优先级最高的三个排查点:

  1. 特征里是否混入了目标自身未做shift。把P(t)同时当特征又当P(t+1)的目标,等于让模型抄近路,t时刻的功率已经包含未来信息。
  2. TCN感受野是否覆盖到主周期。按2.1的公式算,如果RF换算成时间不到12小时,模型大概率退化成“记住最近值”。
  3. 损失函数是否过度惩罚突变点。纯MSE在云层快速遮挡时会输出条件均值,曲线被拉平,看起来就像延迟;换Huber或在loss里加一阶差分惩罚项可以改善。

提示:判断感受野是否够用,把各层膨胀因子代入RF公式,再乘采样间隔换算成小时,看是否覆盖到至少一个完整的日周期。

5.3 三个能立刻用上的落地技巧

超参数搜索别手调,用bayesopt配optimizableVariable定义膨胀因子、核大小、LSTM单元数,目标函数设成验证集nRMSE,跑100轮比手工试快得多。夜间样本建议降采样或加权,否则一天里功率为0的时段主导了梯度方向,白天的尖峰学不尖。至于用大模型代码助手生成Matlab训练循环,现在确实常见,但Matlab各版本API差异很大,dlarray、dlstm这类函数在不同版本签名改过好几次,生成完先doc确认,不要直接跑。保存模型用save,导出图用exportgraphics而不是saveas。把测试集最后一天画成预测vs实测对比图,按提前1、3、6步分三条子图,同时打印各步nRMSE,十分钟就能判断这次调参值不值得留。

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

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

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

立即咨询