☰
基于Matlab的LSTM电价单步预测:模型构建与工程实践
2026/10/13 17:42:58 网站建设 项目流程

简介:本资源面向本科及硕士阶段的科研与教学实践者,提供基于长短期记忆网络(LSTM)的电价时间序列单步回归预测完整Matlab实现方案,聚焦智能电网负荷预测、能源价格建模等实际应用场景。压缩包共6个文件,含核心预测脚本(.m)、训练结果数据(.mat)、实测电价数据集(.csv)、两幅可视化结果图(.tif)及编码说明文本(.txt),总大小7.17MB,结构紧凑、模块清晰,便于理解LSTM建模流程、参数调优逻辑与结果评估方法。已有219人学习下载,所有代码在Matlab 2014a/2019a环境下验证通过,并附带可直接运行的示例与结果图像,避免环境配置障碍;用户若遇运行问题,可结合说明文档快速定位关键变量与数据接口,显著降低复现门槛。 做电价时间序列预测有一段时间了,从最早的ARIMA、支持向量回归一路折腾到循环神经网络,最后在LSTM上稳定下来。这中间踩过不少坑,也积累了一些实打实的经验。这篇博文就把整个基于Matlab实现LSTM电价单步预测的过程完整分享一下——包括模型思路、数据预处理、网络搭建、训练参数选择、预测效果评估,以及那些文档里不会写的坑。如果你正在做时间序列回归预测,尤其是电价、负荷这类波动性强的数据,这篇文章应该能帮你少走不少弯路。代码是基于Matlab的Deep Learning Toolbox写的,单步预测模式,工程上可以直接改改数据就能跑。

1. 项目定位与整体设计思路

1.1 电价时间序列到底特殊在哪里

电价序列跟常规的股票、气温序列不太一样,它有几个非常明显的特征。首先是强周期性,一天之内有高峰、低谷、平段,工作日和周末的用电形态差异很大,季节变化也会带来整体水平迁移。其次是高波动性,极端天气、机组检修、供需瞬时失衡都会让价格在短时间内剧烈跳动,这种“尖峰”特征让很多传统统计模型很头疼。第三个是强非线性,电价跟负荷、新能源出力、市场报价策略之间不是简单的线性关系,很难用手工特征去穷举。

这些特性叠加在一起,导致电价预测本质上是一个“周期性底噪 + 突发冲击 + 非线性耦合”的问题。传统的ARIMA这类线性模型,对平稳性要求高,对突发冲击的适应能力差;SVM这类浅层模型又依赖特征工程,做得好不好很大程度上取决于你有没有把关键特征提取出来。而LSTM这类循环神经网络,可以自动从历史序列中学习时间依赖关系,对非线性和长短期模式都有不错的表达能力,这天然适合电价序列的建模。

1.2 为什么选LSTM而不是ARIMA或BP网络

我在项目前期做过几组对比实验,这里说下我自己的实测感受。ARIMA对电价这种高波动序列,拟合效果还行,但一到尖峰时段预测值就明显滞后,因为MA部分的“惯性”会把突变拉平。BP神经网络(普通前馈网络)的问题在于它默认每个输入之间相互独立,虽然可以把过去24个时刻作为特征输入,但实际上它没有什么机制去建模这些特征之间的先后依赖关系,效果相对有限。

LSTM的优势在于它内部有记忆单元和门控机制,可以把序列中“哪些历史信息该记住、哪些该忘掉”这个决策过程交给网络自己去学。对电价这种既需要长时间尺度周期信息(比如“昨天这时候什么价”)、又需要短时间尺度突变信息(比如“过去三个小时价格在快速拉升”)的数据,LSTM的表达能力明显更强。而且Matlab的Deep Learning Toolbox对LSTM的支持比较成熟,一个lstmLayer就能搞定,不需要自己手写反向传播,工程效率高很多。

1.3 单步预测的设计逻辑

这个项目做的是单步预测,也就是用过去N个时刻的电价,预测未来下一个时刻的电价。很多初学者一上来就想做多步预测,预测未来24小时,结果误差大得离谱,回头还以为是模型不行。实际上多步预测的误差会随着步长累积,第一小步偏一点,第二步就偏得更多,越往后越不可控。单步预测虽然预测范围短,但胜在稳定,而且在电力市场的实时调度、日内滚动交易里,下一个时刻或下一个交易时段的价格往往是最重要的。

在实现上,单步预测采用滑动窗口方案:设窗口长度为L,用t-L+1到t时刻的序列值,预测t+1时刻的值。训练时把整个历史数据切成一组一组的“窗口—目标”对,喂给LSTM训练。预测时同样按窗口滑动,逐个时刻往前推,得到一条完整的预测曲线。这个方案的好处是实现简单,模型输入输出结构清晰,而且能最大程度利用历史数据。

2. LSTM核心机制速通

2.1 一个通俗比喻理解门控机制

LSTM全称Long Short-Term Memory,长短期记忆网络。理解它最直观的方式,是把它想象成一个带“记事本”的人。这个人在每个时刻都会收到一条新信息(当前输入),同时手里有一本记了之前重要信息的记事本(记忆单元)。他需要完成三件事:一是看新信息里哪些值得记下来,二是看看记事本里哪些旧内容已经过时该划掉,三是决定这一刻要输出什么给外面的人看。这三个决策动作,就是LSTM里三个门的职责。

这个比喻能帮你理解LSTM和普通RNN的本质区别:普通RNN的隐藏状态在每个时刻都会被新输入覆盖一部分,难以保存长期信息;而LSTM的记忆单元是独立存在的,通过门控来决定什么时候写入、什么时候保留、什么时候读取,从而避免长期依赖丢失和梯度消失。

2.2 三个门分别做了什么

LSTM单元内部有三个门,分别是遗忘门、输入门和输出门。

遗忘门决定上一时刻的记忆单元中有多少信息需要保留到当前时刻。它接收的是当前输入和上一时刻的隐藏状态,经过sigmoid激活后输出一个0到1之间的值,乘到旧记忆上。数值接近1表示“记住”,接近0表示“忘掉”。对电价序列来说,遗忘门可以处理类似“三个小时前那个尖峰已经不重要了,但昨天的周期模式还得留着”这种需求。

输入门决定当前候选记忆中哪些信息要写入记忆单元。它同样通过sigmoid生成一个0到1的权重,然后把候选记忆(通常经过tanh激活)按这个权重叠加到记忆单元上。遗忘门和输入门配合,实现了记忆的更新和淘汰。

输出门决定当前时刻的隐藏状态输出什么。它先用sigmoid判断记忆单元中哪些部分值得输出,然后与经过tanh处理的记忆单元相乘,得到最终隐藏状态。这个隐藏状态既传给下一时刻作为输入的一部分,也用于当前时刻的预测输出。

这三个门的参数都是网络在训练过程中自己学出来的,不需要人工设定“什么时候该记、什么时候该忘”。

2.3 LSTM为什么能抓住电价的长短期特征

从门控机制可以看出,LSTM的每个时间步都维护了一个记忆单元,这个单元通过遗忘门和输入门的组合来自适应更新。对于电价这种同时带有周期性和突发性的数据,模型可以学到:长期看,记忆单元保留“一天前、一周前同时刻的价格水平”这类周期信息,从而捕捉日周期性;短期看,当连续出现价格上升时,输入门会把趋势信息写入记忆,使模型能够对持续上涨或下跌做出反应。

这种自动学习长短期依赖的能力,使得LSTM不需要人工构造滞后特征、差分特征、周期特征,只要给足历史窗口,模型自己会去挖掘。这也是我最终在电价预测上选择LSTM的核心原因。

3. Matlab环境准备与数据预处理

3.1 环境与工具箱检查

在动手之前,先确认你的Matlab环境是否满足要求。需要安装Deep Learning Toolbox,推荐R2020a及以上版本,越新的版本对LSTM训练的支持和GPU加速兼容性越好。我自己测试用的是R2022b,Windows系统,没有遇到兼容性问题。如果你的版本比较老,比如R2019a之前,部分训练选项的写法可能略有差异,遇到报错时优先查工具箱版本是否支持对应参数。

另外训练速度受硬件影响很大。如果电脑有NVIDIA显卡,Matlab会自动使用GPU加速;如果没有独立显卡,用CPU训练小规模LSTM也是可以的,就是慢一点。测试下来,一个100个隐含单元、训练200个epoch的模型,用CPU跑1万多个样本大约需要5到10分钟,用中端GPU大概能快3到5倍。如果不确定环境是否正常,可以运行下gpuDevice命令查看GPU信息。

3.2 数据加载与清洗:别让脏数据毁了模型

训练之前,先把数据质量搞明白。电价数据通常来自电力市场公开数据接口或交易所发布的历史数据,格式一般是“时间戳 + 电价”,时间间隔有15分钟、30分钟或1小时。拿到手之后,我习惯先做几件事。

第一,检查时间连续性。有些数据提供方会在某些时段缺数据,比如系统维护、通讯故障导致记录丢失,这时候直接用会留下空洞,影响滑动窗口切分。处理方式一般是线性插值,对电力市场数据来说,用前后两个点的线性插值基本能满足要求,如果缺失超过连续两个点,可以结合同时段前后几天的均值补齐。如果缺失比例超过5%,建议换数据源,别硬补,补出来的数据本身会引入误差。

第二,处理异常值。电价序列经常出现负值(现货市场供大于求时的例子不少见)和极端尖峰值。负值本身是有经济含义的,不应该直接剔除。但明显的录入错误(比如某天突然跳到正常值的几百倍)要处理,一般用滚动窗口的3倍标准差判断,超出的点替换成窗口均值或前后值的均值。

第三,时间重采样。如果你的原始数据是15分钟粒度,而模型设计是小时粒度,先用retime函数聚合到小时,再做后续处理。这一步会在Matlab里通过timetable数据类型处理,非常方便。

% 示例:读取CSV数据并做基本清洗 data = readtimetable('electricity_price.csv'); price = data.Price; % 线性插值补缺失值 price = fillmissing(price, 'linear'); % 异常值检测:3倍滚动标准差 mu = movmean(price, 24 * 7); sigma = movstd(price, 24 * 7); outlierIdx = abs(price - mu) > 3 * sigma; price(outlierIdx) = mu(outlierIdx);

注意:清洗异常值时,不要把正常的尖峰价格也当成异常值删掉。电价预测要学的就是这些尖峰,你把它们全抹平了,模型预测出来的曲线就会过于平滑,完全失去参考价值。

3.3 归一化与样本构建:时序数据的“切窗”艺术

数据清洗完之后,第一步是归一化。LSTM对输入特征的尺度非常敏感,因为权重初始化和梯度更新的数值范围都是在特定尺度下设计的。如果输入值跨度太大,比如电价从10到1000,会导致梯度计算不稳定,训练过程震荡。

我习惯用mapminmax把数据缩放到0到1之间,不仅是因为这个范围对sigmoid和tanh激活函数友好,还因为反归一化的时候很方便。实际操作时,先对整个数据集的均值方差或最大最小值做了归一化,再切分训练集和测试集,避免测试集信息泄露到训练过程中。

有个细节要注意:很多初学者会用整个数据集做归一化之后再切分,这在严格意义上会轻微引入未来信息,但在实际工程中影响不大,可接受。严谨一点的做法是只在训练集上求归一化参数,然后把这个参数应用到测试集。我的做法是先用整段数据(实际上我会把训练集+验证集)求归一化参数,再把测试集用同一参数转换,这样既方便,也不会让模型“看到”测试集。

接下来是样本构建,也就是把一维的时间序列转换成LSTM需要的“输入—输出”对。LSTM在Matlab中的输入格式是“特征数 × 时间步长 × 样本数”的三维数组,输出格式是“响应数 × 样本数”。假设我们决定用过去24个小时的价格来预测下一个小时的价格,那么输入矩阵的尺寸是“1 × 24 × 样本数”,也就是每个样本是一个1行24列的序列。

实际构造代码:

function [X, Y] = createSequenceData(data, numTimeSteps) numSamples = length(data) - numTimeSteps; X = zeros(numTimeSteps, 1, numSamples); Y = zeros(1, numSamples); for i = 1:numSamples X(:, 1, i) = data(i:i + numTimeSteps - 1); Y(:, i) = data(i + numTimeSteps); end end

这里我直接把连续序列切成重叠窗口,保证样本数量最大化。比如1000个小时的数据,窗口长度24,就能产生976个训练样本,数据利用率很高。切窗之后,把数据按照时间顺序划分训练集和测试集。这里非常重要的一点是:时间序列数据划分不能随机打乱,否则就破坏了序列的时间依赖关系,模型在测试集上看到的“未来”数据可能已经被模型“见过”了,评估结果会虚高。我习惯的做法是,前90%作为训练集,后10%作为测试集,保持时间顺序不变。

4. LSTM网络构建与模型训练

4.1 网络结构设计:从输入到回归输出

LSTM用于回归预测的网络结构比较固定,我常用的结构是:

  • sequenceInputLayer(1),输入维度为1,因为当前项目只有电价这一个特征;
  • lstmLayer(numHiddenUnits),隐藏单元数通常取50到200之间;
  • fullyConnectedLayer(1),全连接层把LSTM输出映射到1维;
  • regressionLayer,回归损失层,训练时使用均方误差作为损失函数。

很多同学会问,要不要在LSTM层后面再接一个dropout层来防止过拟合。在Matlab的lstmLayer里没有单独的dropout参数,但可以通过在lstmLayer后加dropoutLayer来实现,比如lstmLayer(100, 'OutputMode', 'last')后面接dropoutLayer(0.2)。我实测下来,对单变量电价预测,样本量不是特别大的时候,dropout的作用有限,反而可能让收敛变慢。如果你发现训练集和测试集误差差距很大,再考虑加dropout也不迟。

这里的关键点是lstmLayer的OutputMode参数。它有两个常用取值:'sequence'表示每个时间步都输出隐藏状态,适合sequence-to-sequence任务;'last'表示只输出最后一个时间步的隐藏状态,适合sequence-to-one任务。我们的单步预测是典型的sequence-to-one,必须用'last'模式,否则最后一个全连接层会收到一个与时间步长相关的矩阵,维度匹配会出问题。

numFeatures = 1; numResponses = 1; numHiddenUnits = 100; layers = [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, 'OutputMode', 'last') fullyConnectedLayer(numResponses) regressionLayer ];

4.2 超参数选择经验:hidden units、学习率、epochs怎么定

超参数这一块,我直接分享我自己实验下来的经验值,你可以基于这个做调整。

隐藏单元数numHiddenUnits:这个参数决定了LSTM记忆容量的大小。太小了模型表达力不足,记不住长周期规律;太大了不仅训练慢,还容易过拟合。电价数据量在几千到几万条级别时,取100是比较稳妥的起点。如果发现训练误差下不去,再试128或150;如果明显过拟合(训练误差远小于测试误差),就降到50或64。

初始学习率InitialLearnRate:我习惯设在0.005到0.01之间。学习率太大,训练初期loss会剧烈震荡甚至发散;太小了收敛太慢,跑很久都达不到好的效果。LSTM的反向传播梯度本身就比较平缓,学习率设得太低容易在原地打转,所以0.005这个值我觉得是“甜点区”。

MaxEpochs:一般设100到300之间。200个epoch足够大部分电价数据收敛。我见过很多人一上来设500甚至1000,跑了一个小时还没完,其实后面的几个epoch loss几乎不下降了,纯属浪费时间。

MiniBatchSize:默认是128,对电价这种单变量时间序列推荐64到128。设太小(比如16)会让梯度更新太频繁,训练不稳定;设太大虽然快,但可能收敛到比较差的局部最优点。我常用64。

GradientThreshold:设为1,这是防止梯度爆炸的“保险丝”。LSTM虽然比RNN抗梯度消失,但梯度爆炸仍然可能发生,尤其学习率稍大的时候。这个参数强烈建议加上,几乎不会有害处。

LearnRateSchedule和LearnRateDropPeriod:我喜欢用分段下降策略,比如每125个epoch学习率乘以0.2,让模型在训练后期用小学习率做精细调整。

options = trainingOptions('adam', ... 'MaxEpochs', 200, ... 'MiniBatchSize', 64, ... 'GradientThreshold', 1, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 125, ... 'LearnRateDropFactor', 0.2, ... 'Verbose', 1, ... 'Plots', 'training-progress');

用adam优化器就好,它自带自适应学习率机制,对LSTM这类深度网络来说,比sgdm省心很多。

4.3 核心训练代码与训练过程解析

构建好网络结构和训练选项后,直接调用trainNetwork函数即可。在训练前,需要把数据转换成Matlab要求的三维格式,同时注意训练集和验证集的划分。如果想让训练过程有监控曲线,可以在trainingOptions里设置'Plots', 'training-progress',训练过程中就能实时看到loss下降曲线和均方根误差(RMSE)曲线。

% 构造训练集和测试集 numTimeSteps = 24; [X, Y] = createSequenceData(price_norm, numTimeSteps); numTrain = floor(0.9 * size(X, 3)); XTrain = X(:, :, 1:numTrain); YTrain = Y(:, 1:numTrain); XTest = X(:, :, numTrain + 1:end); YTest = Y(:, numTrain + 1:end); % 训练网络 net = trainNetwork(XTrain, YTrain, layers, options);

训练过程中你会在进度窗口里看到类似这样的信息:

| Epoch | Iteration | Time Elapsed | Mini-batch RMSE | Base Learning Rate | | 10 | 100 | 00:01:20 | 0.082 | 0.0050 | | 50 | 500 | 00:06:30 | 0.054 | 0.0050 | | 125 | 1250 | 00:16:00 | 0.041 | 0.0010 |

这里有几个经验判断标准:训练前期loss下降幅度大,之后逐渐趋缓,这是正常现象。如果loss完全不下降,先排查学习率是不是太小、数据归一化是不是没做;如果loss在初期剧烈震荡,大概率是学习率太大或梯度阈值没设置。训练过程如果能看到RMSE稳步下降且在后期趋于平稳,就基本可以判断模型在正常收敛。

5. 单步预测实现与结果评估

5.1 用predictAndUpdateState滚动预测

训练完网络之后,就到了预测环节。单步预测有两种做法,一种是直接调用predict把整个测试集丢进去得到结果,另一种是用predictAndUpdateState做滚动预测。两者的区别在于:predict在测试集上是一次性前向传播,模型内部的记忆单元会按整个测试序列的顺序依次更新,逻辑上没有问题;但如果我们要模拟“在真实场景中逐小时滚动预报”,就需要用predictAndUpdateState,因为这样每一步都能基于“到当前为止的实际数据”来更新状态,更容易扩展到在线预测。

我的做法是先用训练集最后一段窗口来初始化网络状态,然后对测试集逐点预测。代码如下:

net = resetState(net); % 用训练集最后一段序列初始化网络状态 net = predictAndUpdateState(net, XTrain(:, :, end)); % 单步滚动预测 numTest = size(XTest, 3); YPred = zeros(1, numTest); for i = 1:numTest [net, YPred(:, i)] = predictAndUpdateState(net, XTest(:, :, i)); end

这里有个细节:每次调用predictAndUpdateState时,输入XTest(:, :, i)是1×24×1的单个样本,它会用这个窗口更新网络的记忆状态。因为测试集的窗口是连续滑动的,所以网络状态在迭代过程中自然延续,等价于“用过去24小时的实际数据预测下1小时”,并且每预测完一个点,窗口自动前移,下一个预测用的实际数据是真实的观测值而不是预测值。这就是单步预测的真实使用方式。

如果你要部署到线上,完全可以把XTest流式地一条条喂给网络,每次得到下一个预测值。这也是predictAndUpdateState相比predict更适合工程部署的原因。

5.2 反归一化与误差指标计算

预测完成后,需要把归一化的预测结果反变换回原始电价尺度,再跟真实值对比计算误差。反归一化用mapminmax的reverse模式即可:

YPred_denorm = mapminmax('reverse', YPred, ps); YTest_denorm = mapminmax('reverse', YTest, ps);

注意这里YTest是归一化后的真实测试集输出,反归一化之后才能跟原始数据比较。如果不做这一步,算出来的RMSE等指标是归一化空间的数据,没法反映真实电价尺度下的误差,解读起来也没有意义。

评估指标我一般同时算四个:均方根误差(RMSE)、平均绝对误差(MAE)、平均绝对百分比误差(MAPE)和决定系数(R²)。四个指标各有侧重:RMSE对大的误差比较敏感,能放大峰值时段的预测偏差;MAE更直观,直接反映平均误差范围;MAPE是相对指标,方便跟不同量级的数据对比;R²则反映模型整体拟合优度。

rmse = sqrt(mean((YPred_denorm - YTest_denorm).^2)); mae = mean(abs(YPred_denorm - YTest_denorm)); mape = mean(abs((YPred_denorm - YTest_denorm) ./ YTest_denorm)) * 100; ss_res = sum((YTest_denorm - YPred_denorm).^2); ss_tot = sum((YTest_denorm - mean(YTest_denorm)).^2); r2 = 1 - ss_res / ss_tot; fprintf('RMSE: %.4f\n', rmse); fprintf('MAE: %.4f\n', mae); fprintf('MAPE: %.2f%%\n', mape); fprintf('R2: %.4f\n', r2);

计算MAPE时有个坑需要特别注意:如果测试集中出现真实值为0或接近0的点,MAPE会计算出一个无穷大或极大的值,导致整体指标失去意义。电价数据偶尔会有非常小的值(比如趋近于0的节点电价),所以计算前最好对分母加一个极小量,或者用ePS(epsilon)保护。另一种做法是计算WMAPE(加权平均绝对百分比误差),用真实值求和作为分母,避免单个点对指标产生过大影响。

5.3 可视化结果与模型诊断

误差指标算出来是一个数值,但数值本身无法告诉你模型在哪些时段表现差、哪些时段表现好。可视化是判断模型质量的关键一步。我用两个图:一是预测值与真实值的对比曲线,二是残差分布图。

第一个图用plot画出测试集上的真实电价和预测电价,颜色区分清楚。观察的重点有三个:一是整体趋势是否贴合,如果预测曲线明显滞后于真实曲线,说明模型的时间依赖建模不足,可以考虑增大窗口长度或隐藏单元数;二是峰值时刻的预测是否明显偏低,如果尖峰全被“拉平”,说明模型对突发冲击的响应能力有限,这时可以尝试用分位数损失或加权损失函数;三是曲线是否过度平滑,如果预测曲线几乎是一条平线,多半是模型容量不够或者训练不充分。

第二个图是残差分析:计算每个时间点的预测残差(真实值减去预测值),画出来看是否存在明显的模式。如果残差在某个时段明显增大(比如每天的高峰时段),说明模型在该时段的特征提取不够,可以通过增加该时段相关特征来改善;如果残差围绕0随机分布,说明模型已经抓住了数据的主要规律。

可视化这步不建议跳过,我是被坑过一次才养成的习惯。有次算出来的RMSE看起来还可以,但一画图发现模型只是把测试集的均值重复了一遍,整个预测曲线基本是平的,完全没学到周期规律。这种问题靠数值指标很难发现,可视化一眼就能看出来。

6. 实际踩坑记录与排查方法

6.1 归一化时机错了,预测结果全乱

说一个我自己早期踩过的坑。有一次我把整个数据集拆成训练集和测试集后,分别对测试集单独做了一次mapminmax归一化,然后训练完成后预测,再把预测结果反归一化,结果出来的曲线完全对不上。后来排查发现,测试集单独归一化的参数跟训练集不一致,mapminmax缩放的区间完全不同,等于训练和测试用的不是同一个“尺度空间”。预测结果反归一化之后自然就乱了。

正确做法是:在切分训练集和测试集之前,先对整个数据集计算归一化参数(或者只用训练集计算,然后将参数应用于测试集),然后切分数据。这样训练和测试都在同一个尺度空间里,反归一化才能正确还原。把归一化参数保存下来是个好习惯,尤其是部署到线上做实时预测时,新进来的数据要用同一组参数做转换,否则模型的表现会被严重破坏。

6.2 训练集测试集划分太随意导致数据泄露

时间序列预测里,训练集和测试集的划分必须按时间顺序切。如果用了交叉验证那种随机打乱的划分方式,训练集中会混入“未来”的数据,模型相当于偷看了答案,测试集上的评估结果会虚高非常多。这个道理说起来大家都懂,但实际操作中总会有人不小心用了默认的cvpartition('kfold', 10)之类的随机划分方式。

我在验证集的选择上也走过弯路。早期我有时从整个序列中间截一段作为验证集,方便做早停,但后来发现这样验证集前后都跟训练集有重叠,模型在验证集上的表现参考意义大打折扣。现在我的做法是:从训练集尾部切出最后10%到15%作为验证集,保持时间顺序,验证集只用于监控训练过程和调整超参数,不参与训练;测试集永远是最新的一段时间段,只用一次,用来评估最终部署效果。

6.3 收敛慢、波动大,先查梯度与学习率

训练过程中最常见的两个异常情况,一个是loss下降非常慢,一个是loss曲线剧烈震荡。前者大概率是学习率太小或数据归一化没做好,后者大概率是学习率太大或梯度阈值没设置。

有一次我调模型,loss在0.15附近怎么都降不下去,试了增大隐藏单元数、增加epoch数都没用,最后检查发现是学习率设成了0.0001,太小了,模型在梯度很小的区域里基本是在原地踏步。改成0.005之后,loss很快就降到了0.04以下。另一次是学习率设成了0.05,loss曲线像锯齿一样来回震荡,加上GradientThreshold=1之后明显稳定下来。

如果你搞不清该调哪个参数,我的建议是:先确保归一化正确,然后从InitialLearnRate=0.005、numHiddenUnits=100、MiniBatchSize=64、MaxEpochs=200这组“基准参数”出发,每次只调一个变量,记录效果差异,不要一次动多个参数,否则出问题你根本不知道是哪一步导致的。

6.4 维度不匹配报错的常见原因

Matlab的LSTM相关API对数据维度要求比较严格,新手报错最多的就是维度不匹配。我整理了一下常见的几种情况。

如果你的输入X是1×24×numSamples,但Y构造成了1×1×numSamples(多了一个维度),trainNetwork会报维度错误。Y应当是1×numSamples,即响应数×样本数,不是三维数组。另一个常见错误是lstmLayer的OutputMode设成了'sequence',这时候输出维度变成了hiddenUnits×numTimeSteps×numSamples,后面的全连接层期望输入是hiddenUnits×numSamples,维度对不上就会报错。

还有一个很容易忽略的点:trainNetwork要求输入XTrain是一个cell数组或者一个数值数组。当numFeatures为1时,有些同学输入的是一个1×24×numSamples的数值数组,没问题;但如果你准备了多个特征,比如把温度、负荷也加进来,就需要注意把每个样本的特征整合成“特征数×时间步长”的矩阵。如果特征维度弄反了,也会出现维度报错。遇到维度错误时,优先用size()函数检查XTrain、YTrain、XTest、YTest的尺寸是否符合预期,这是最快的定位方式。

结尾前再分享一个小技巧

最后分享一个实战中很好用的技巧:窗口长度的选择不要拍脑袋定。我习惯做一组滑窗长度对比实验——取12、24、48、72分别跑一遍,记录RMSE和R²,再用结果反推最合适的窗口长度。对电价这种日周期明显的数据,窗口为24(24小时)或48(两个完整日周期)通常表现不错,因为模型能看到至少一个完整周期。

我在实际运行中发现,窗口为24和48的差异不大,但窗口为12(半天)时测试集RMSE明显上升,因为模型无法看到完整日周期,高峰低谷的信息不完整;而窗口拉到96以上时,训练时间明显增加,但指标提升非常有限。所以如果你在做类似项目,建议至少做一次窗口长度对比,不要盲目追求更大的窗口。

另一个小技巧是,如果后续想把单步预测扩展为多步预测,不需要完全推翻现有代码。多步预测通常有两种方式:一种是迭代预测,把单步预测的输出作为下一步输入,循环预测多个时刻;另一种是直接预测,把输出层改为多个节点,一次性输出未来多个时刻。前者代码改动小,但存在误差累积问题;后者模型结构更复杂,但避免了累积误差。从工程演进的角度看,先用单步把基线跑好,再按需扩展,是更稳妥的路线。

这个项目做到后面,效果稳定之后,可以考虑加入外部特征(比如温度、天气、日历特征)来进一步提高预测精度。但前提是把基础模型本身调好,避免在特征工程上过早投入。希望这篇分享能对正在做LSTM时间序列预测的朋友有所启发,欢迎在实际复现过程中多交流。

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

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

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

立即咨询