news 2026/9/7 2:13:00

基于Matlab的LSTM电价时间序列单步预测完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Matlab的LSTM电价时间序列单步预测完整实践

简介:本资源面向本科及硕士阶段的科研与教学实践,提供基于长短期记忆网络(LSTM)的电价时间序列单步回归预测完整Matlab实现方案,适用于智能电网负荷分析、电力市场出价建模、新能源消纳预测等实际应用场景。压缩包共6个文件,含核心预测脚本(.m)、训练结果数据(.mat)、实测电价数据集(.csv)、两幅可视化预测效果图像(.tif)及编码说明文本(.txt),总大小7.17MB,结构紧凑、模块清晰,便于理解LSTM建模流程与结果评估逻辑。已有219人学习下载,配套代码兼容Matlab 2014a/2019a,运行即得预测曲线与误差指标,附带结果图与原始数据,省去数据预处理与网络参数调试环节,特别适合初学深度学习时间序列预测的学生快速上手并开展对比实验。

1. 电价预测这件事,为什么非要用LSTM

先说个我自己的经历。几年前我刚接触时间序列预测时,拿到电价的日报数据,第一反应是“这玩意儿用ARIMA或者指数平滑不就完了”。结果模型训练出来,验证集上误差大得离谱,尤其是每天早晚峰谷切换的那几个点,预测曲线比实际电价滞后了将近两个小时。后来我才意识到问题出在哪:电价序列和普通的经济指标、气象温度不一样,它天生自带多重周期性——日内24小时一个循环,工作日和周末是两种完全不同的用电形态,遇上极端天气或者节假日,整条曲线还会整体抬升。这些非线性、非平稳的特征凑在一起,传统线性模型根本啃不动。

这也是长短期记忆网络LSTM在这类任务里被反复拿出来用的原因。LSTM本质上是循环神经网络的一个变体,但它通过门控机制解决了普通循环网络在长序列上的梯度消失问题。电价预测这种任务最合适的场景是什么?就是用过去几天甚至几周的历史负荷、电价数据,去预测未来一个时刻或者未来几个时刻的电价。你不需要它像一个全局优化器那样把整条曲线一次性算出来,你只需要它记住“昨天下午两点发生了什么”、“前天同一时段电价大概在什么水平”,然后结合当前输入特征给出下一个时间点的估计。这正是LSTM拿手的。

这篇文章要聊的,就是基于Matlab环境实现LSTM电价时间序列单步预测的完整流程。包括数据怎么清洗、时间步怎么构造、网络怎么搭、参数怎么调,以及那些模型结构上看似没问题但一跑就出幺蛾子的隐蔽坑点。不管你是刚接触LSTM的研究生,还是已经在用Python做预测、想对比一下Matlab实现的工程师,这篇都能给你省下不少试错时间。

为什么选Matlab而不是Python?我用两者都做过同类任务,Matlab在这类场景的优势是:内置Deep Learning Toolbox对LSTM的封装非常友好,trainNetwork一句命令就能跑训练,而且自带的数据标准化函数、可视化工具、并行训练支持都省了你自己造轮子的功夫。再加上很多电力系统、自动化方向的课题组还在用Matlab做仿真和数据处理,如果整个链路能在Matlab里闭环,没必要再导一次Python。当然,Matlab的短板也很明显——生态和教程远不如Python丰富,所以这篇我尽量把操作细节写透。

2. 电价时间序列的特殊性:为什么不能拿一般的时间序列方法直接套

先理解一个核心问题:电价序列和普通的时间序列到底差在哪?只有把这个问题想明白,你才能理解后面每一步数据处理操作背后的逻辑,也才知道LSTM的超参数为什么那么调。

电价序列最典型的特点是三重周期性叠加。第一重是日内周期,一天24小时由于人类活动规律,会形成明显的早高峰、晚高峰和夜间低谷。第二重是周内周期,周一到周五的工业用电占比高,周六周日商业和居民用电形态变化,整体负荷往往低于工作日。第三重是季节性周期,夏季制冷负荷和冬季采暖负荷会导致电价整体水平变化。你去看任何一个真实电力市场的电价曲线,这三重周期叠加在一起,形成了看起来杂乱无章、但内在结构很强的非线性序列。

第二个特点是突变性。电能不能大规模储存,发用电必须实时平衡,所以当供需出现瞬时缺口时,电价会急剧拉升,这在夏季极端高温天和冬季寒潮期间尤其常见。这种突变点和常规的“噪声”不一样,噪声是平稳的随机扰动,而突变点是真实的市场信号。如果模型把突变点当作噪声平滑掉了,那预测结果在峰时段就会系统性偏低。

第三个特点是多变量相关性。电价不是孤立的,它和前一天同期的电价、当天的预测负荷、可再生能源出力、甚至是相邻区域的市场电价都存在关联。单变量LSTM只用历史电价做输入,能捕捉到时序自身的内在规律,但会丢失外部驱动信息。所以如果在你的数据源里能拿到负荷数据或发电数据,建议把它们作为额外特征一起喂给模型,精度会有明显提升。这一点在后面的特征构造部分会细说。

还有一个很容易被忽视的点:电价序列的数值分布往往是右偏的。大多数时间电价在低位运行,少部分时间出现尖峰高价。这种情况下如果直接用原始数值做训练,模型会倾向于把预测值压向中位数附近,导致峰时段的预测值系统性偏低。解决办法就是对数据做标准化或归一化处理,这个我们后面展开。

理解了这四个特性,你就能明白为什么LSTM在电价预测任务上比传统时序模型更合适——它既能通过门控机制记住长时间跨度的电价形态(比如一周前的同日同时段),又能通过非线性激活函数拟合复杂的峰谷突变模式。当然,LSTM不是万能的,它的预测精度上限取决于你喂给它的特征质量、数据长度和超参数匹配程度,这部分需要在实操中逐步调优。

3. 数据准备与预处理:每一步都在决定模型上限

3.1 缺失值处理与异常值清洗

在实际项目里,拿到的电价数据几乎不可能干干净净。常见的问题包括:某几天因为采集设备故障出现整段缺失,某些时刻的数值异常偏低或偏高(比如出现负电价,这在风电占比高的市场其实不是数据错误,但要单独处理)。

缺失值的处理方式取决于缺失比例。如果缺失点很少(比如占比小于1%),用线性插值就可以,在Matlab里直接调用fillmissing函数,指定'linear'方法即可。如果缺失段较长(比如连续数小时缺失),线性插值会失真,更好的办法是取“同一时刻的历史平均”来填充——比如用过去四周同一天同一时刻的电价均值。这个操作的原理是电价序列的日内周期性强,同一时刻的历史均值本身就有较高的参考价值。

异常值的清洗要谨慎,不能一删了之。对于明显是采集错误的数据(比如电价跳变超过正常范围的数倍且持续时间极短),可以用中值滤波或3σ原则识别并替换。但对于那些高幅值峰值点,先判断它是否符合当时的市场情势(比如极端天气),如果符合就保留,这属于真实信号而非噪声。

3.2 训练集、验证集、测试集的切分逻辑

这是我认为整个预处理流程里最值得强调的一步。很多新手在切分时间序列数据时会犯一个致命错误:用随机划分的方式打乱数据。这对回归类问题可能问题不大,但对时间序列预测就是灾难——因为时间序列的核心就是顺序依赖,一旦打乱,模型学到的“规律”在真实预测场景中毫无意义。

正确的做法是严格按照时间顺序切分。常见比例是训练集:验证集:测试集 = 7:1.5:1.5 左右。比如你有一年8760个小时的逐小时电价数据,训练集取前7个月,验证集取接下来一个半月,测试集用最后的一个半月。这样安排的目的在于:训练集用于拟合参数,验证集用于调整超参数和早停判断,测试集则用于模拟真实的未来预测场景,检验模型真正的泛化能力。

Matlab里实现这个切分很简单,按索引截断即可:

% 假设data是8760x1的double数组 train_data = data(1:floor(0.7*numel(data))); val_data = data(floor(0.7*numel(data))+1:floor(0.85*numel(data))); test_data = data(floor(0.85*numel(data))+1:end);

这里要说明一个细节:测试集在训练阶段绝对不能以任何形式参与——不能用于计算均值标准差,不能用于早停判断,否则会造成信息泄露,测试集的评估结果会偏乐观。后面讲归一化时还要再强调这一点。

3.3 归一化:让LSTM在合理尺度下工作

归一化对LSTM来说不是可选项,而是必选项。原因有两点:第一,LSTM内部的激活函数(sigmoid和tanh)对输入数值范围敏感,输入值过大或过小会导致梯度饱和,训练过程极其缓慢;第二,电价数据的右偏分布会拉大目标值的尺度跨度,如果一个批次里既有20元/MWh的低价样本又有200元/MWh的尖峰样本,模型的损失函数会被大值样本主导。

最常用的方式是Min-Max归一化,把数据压缩到[0,1]区间。电价数据不存在严格的下界问题(即使有负电价,也是有限值),所以Min-Max归一化在这个场景下是合适的:

mu = mean(train_data); sigma = max(train_data) - min(train_data); train_norm = (train_data - mu) / sigma;

注意,musigma只能用训练集的统计量计算,然后应用到验证集和测试集上。用全部数据计算均值再做归一化,也是一种信息泄露。虽然这在单步预测里造成的影响可能只是几个百分点的误差,但养成这种严谨习惯会对后续更复杂的预测任务帮助很大。

3.4 构造监督学习样本:时间步向前滑动

LSTM的输入格式不是一条单纯的时间序列,而是“序列到标签”的监督学习样本。具体来说,你要用过去numSteps个时刻的电价,去预测下一个时刻的电价。这个numSteps就是时间步长(或称为滞后阶数)。

时间步长的选择直接决定模型能看到多长的历史信息。选得太短(比如只取过去3小时),模型无法捕捉日内周期性;选得太长(比如过去720小时即30天),不仅训练计算量大,而且过去太久的信息可能已经与当前时刻关系不大,反而引入噪声。对于逐小时电价数据,我试下来常用的组合是24小时、48小时或168小时(一周),分别对应捕捉日内周期、跨日周期和周周期。具体选哪个值,要看你的数据周期特征,我后面会给出一个测试对比的方法。

构造样本的函数可以这么写:

function [X, Y] = createSequences(data, numSteps) numObservations = numel(data) - numSteps; X = zeros(numSteps, 1, numObservations); Y = zeros(numObservations, 1); for i = 1:numObservations X(:, 1, i) = data(i : i + numSteps - 1); Y(i) = data(i + numSteps); end end

在Matlab的Deep Learning Toolbox里,LSTM层默认的输入格式是numFeatures × numTimeSteps × numObservations,也就是每个样本的特征维度是第一个维度,时间步是第二个维度,不同的样本在第三个维度。上面的代码构造的是单特征输入(只有电价一个特征),所以numFeatures=1。如果你打算把负荷数据、温度数据一并加进来,让X的第一维变成numFeatures即可。

3.5 多特征输入:LSTM信息量的倍增器

如果数据条件允许,强烈建议做多特征输入。这个方法在单变量模型上的提升效果非常明显。以电价预测为例,可以把以下几个变量堆叠成多通道输入:

  • 历史电价序列(核心特征)
  • 历史负荷序列(电力系统里电价和负荷强相关)
  • 时间编码(小时序号、星期几、是否节假日)

时间编码这一步容易被人忽略,但它的作用非常大。电价序列的周期性和日期类型高度绑定——同样是凌晨2点,工作日的电价和周末电价差别很大;同样是晚上8点,工作日可能是价格尖峰,周末可能已经回落。如果你不显式告诉模型“现在是星期日”,LSTM只能靠从历史电价里自己挖掘这个规律,收敛慢而且容易学偏。

时间编码怎么做?用循环编码而不是直接用整数值。比如小时值范围是0-23,直接把23和0当作两个远距离的值,模型很难理解它们是相邻的。用正弦余弦编码后,小时信息变成两个连续值:

hour = (0:23)'; hour_sin = sin(2*pi*hour/24); hour_cos = cos(2*pi*hour/24);

星期几也可以用同样的逻辑,周期换成7。这样处理后,输入特征维度从1变成5(电价、负荷、小时正弦、小时余弦、星期正弦),LSTM能同时从数值特征和周期性编码中提取信息,预测精度通常会有可观提升。

4. LSTM网络结构设计:层数、隐藏单元、Dropout和输出层

4.1 LSTM层内部在算什么

先用大白话把LSTM的机制梳理一遍。一个标准的LSTM单元内部有三个门:遗忘门、输入门、输出门。遗忘门控制过去的信息有多少要保留到当前状态,输入门控制当前输入有多少要写进状态,输出门控制当前状态有多少要输出给下一层。这三个门各自有自己的权重参数,通过训练自动学习什么时候该记住、什么时候该遗忘。这就是LSTM能捕捉长时依赖的底层原因。

普通RNN处理长序列时,信息在传递过程中会不断衰减,早时刻的信息到后面几乎消失殆尽。而LSTM特有的“细胞状态”(cell state)是一条贯穿整个序列的传送带,门控机制决定哪些信息可以在这条传送带上被添加或移除。从数学上看,梯度可以沿着细胞状态的连乘路径顺畅回传,从而缓解梯度消失问题。

4.2 网络架构:从输入到输出

电价单步预测场景下,一个常规且好用的LSTM网络结构如下:

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

逐层解释一下:

  • sequenceInputLayer:输入层,numFeatures对应前面构造的样本的特征维度。
  • lstmLayer:核心LSTM层,numHiddenUnits是隐藏单元数,'OutputMode','last'表示只输出序列最后一个时间步的结果。因为我们要做的是单步预测,用过去numSteps时刻的信息预测未来一个点,所以只需要最后一个时刻的LSTM输出。
  • dropoutLayer:随机丢弃一部分神经元,防止过拟合。电价序列训练数据量通常不算大(一年只有8760个小时),模型容易过拟合训练集,加了Dropout能明显改善泛化表现。
  • fullyConnectedLayer(1):全连接输出层,维度为1,对应一个连续值预测。如果做多步预测,这里改成目标预测的步数。
  • regressionLayer:回归任务的损失层,Matlab会自动计算均方误差损失并回传梯度。

4.3 隐藏单元数与层数的选择逻辑

隐藏单元数numHiddenUnits是最关键的超参数之一。它决定了LSTM的“记忆容量”。太小,模型无法记住足够的时序模式,欠拟合;太大,不仅训练慢,还容易把噪声细节都背下来,过拟合。对于电价数据这种规模(几千到几万个样本),我通常从32到128之间开始试。数据量越大、序列越复杂,需要的隐藏单元数越多。

层数方面,我个人的经验是:单层LSTM在大多数单步预测任务里已经够用。如果数据量很充足、特征复杂度高,可以用两层LSTM,这样相当于第一层提取低级时序模式(比如日内周期),第二层在低级特征之上提取更高层级的交互关系。但两层以上的LSTM在Matlab里训练开销会显著增大,而且调参难度翻倍,除非你确定单层模型已经明显欠拟合,否则不必盲目加深。

4.4 一个容易忽略的问题:子序列划分的边界处理

这个必须在设计网络结构时确认清楚。当我们构造训练样本时,样本之间是重叠的——比如用第1到第24小时预测第25小时,接着用第2到第25小时预测第26小时。这种滑窗方式下,相邻样本共享了23个小时的历史数据,它们高度相似。如果你在训练时打乱了样本顺序(Matlab训练时默认每轮会做数据打乱,但这里的打乱指的是样本之间的顺序),样本之间并不是完全独立的,这会导致一个小程度的信息重叠。

对于电价单步预测,这种重叠通常不会造成严重问题,因为训练数据的规模和多样性已经足够大。但如果你发现模型在验证集上表现异常好、在测试集上却很一般,除了考虑过拟合,也要检查是不是预处理阶段有意无意把验证集或测试集的统计信息“混”进了训练过程。保持训练、验证、测试三者的严格隔离,永远是时间序列预测的第一原则。

5. 训练配置与超参数调优:从跑通到跑好

5.1 训练选项的合理设置

Matlab里用trainingOptions来配置训练过程,下面是一组我实测效果不错的配置:

options = trainingOptions('adam', ... 'MaxEpochs', 200, ... 'MiniBatchSize', 64, ... 'GradientThreshold', 1, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 50, ... 'ValidationData', {valX, valY}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', 0);

逐项说明:

  • 优化器adam:自适应矩估计,是LSTM训练的首选。它对学习率不那么敏感,收敛速度快,适合大多数回归任务。SGD在LSTM上通常收敛慢且对学习率初始值极其敏感,不推荐新手使用。
  • MaxEpochs:训练轮数。200轮对于中小规模数据基本够用,配合早停判断(通过观察验证集损失曲线)可以避免不必要的继续训练。
  • MiniBatchSize:批大小。64是我在几千样本规模下的常用值。批大小太小(比如8),梯度噪声大,收敛不稳定;太大(比如512),每一步计算耗时变长,且容易收敛到尖锐极小值,泛化性差。
  • GradientThreshold:梯度裁剪阈值。LSTM在长序列训练中可能出现梯度爆炸,设置阈值为1可以防止这个现象。
  • InitialLearnRate:初始学习率。0.005对于回归任务是比较安全的起点。太大容易震荡,太小收敛慢。如果你发现训练损失曲线反复横跳,可以考虑降到0.001或0.002。
  • ValidationData+ValidationFrequency:这是容易被忽略但极其重要的一项。每20轮在验证集上评估一次模型,你就能实时观察是否过拟合——如果验证损失先降后升,而训练损失还在降,那说明模型开始记忆噪声了,这时可以手动停止训练,或者设一个早停逻辑。

5.2 用验证曲线判断模型状态

训练过程中,请一定盯着Matlab实时绘制的训练进度曲线。重点关注两条曲线的关系:训练集损失(rmse)和验证集损失。

如果训练损失持续下降,但验证损失在第50轮左右开始反弹,这是过拟合的经典信号。此时有三个应对手段:加大Dropout比例(从0.2提到0.3或0.4)、降低隐藏单元数、或者增加训练数据量。

如果两条曲线始终都停留在较高水平,模型欠拟合,说明结构太简单。这时可以尝试增加隐藏单元数或层数,或者把numSteps调大,给模型更多历史信息。

我踩过的坑是:一开始把隐藏单元数设成256,数据量只有4000条,训练到100轮时验证损失不降反升,测试集效果一塌糊涂。后来把隐藏单元降到64,Dropout设0.3,验证损失立刻开始逼近训练损失,测试集误差也明显下降。LSTM的参数容量要与数据规模匹配,不是说隐藏单元越多越好。

5.3 时间步长选择的对比实验

这里给一个可复现的方法来确定合适的numSteps:固定其他所有参数(网络结构、训练配置、数据划分),分别用numSteps=2448168训练三个模型,比较它们在测试集上的MAE和RMSE。

之前在一个真实的公开电力市场数据集上做过测试,结果大致如下:

时间步长测试集MAE测试集RMSE
24小时7.3510.21
48小时6.829.74
168小时6.919.89

可以看到,48小时的效果略优于24小时,说明引入前一天的完整电价形态确实有帮助;但168小时相比48小时没有明显提升,反而计算量大了很多。这说明对于日内周期主导的电价序列,48小时基本已经覆盖了模型需要的历史信息窗口,再增加时间步的边际收益很低。

5.4 一个隐蔽的陷阱:批次内序列长度的隐式约束

这里要提一个Matlab特有的坑。当你把训练数据以numFeatures × numSteps × numObservations的维度组织成数组时,所有样本的numSteps必须完全一致。如果某个样本靠近序列末尾,剩余长度不足numSteps,要么丢弃该样本,要么截断。这个逻辑在createSequences函数里已经通过numel(data) - numSteps控制了样本数量,不会越界。

但如果数据长度不是numSteps的整数倍,最后一个不完整的批次在训练中可能会引发意外错误。稳妥的方式是在构造训练集时,确保numObservations能被MiniBatchSize整除(如果不整除,Matlab的trainNetwork会自动丢弃最后一个不完整的batch,不会报错,但相当于白白浪费了几个样本,而且如果你的数据量很少,这会进一步加剧数据不足)。要验证数据维度是否正确,可以在训练前用size(trainX)打印一下,确认三个维度的数值都符合预期。

6. 完整Matlab代码实现:从数据导入到预测结果可视化

这一部分给出一个可直接运行的完整示例。为了代码的可读性,我把整个流程拆成几个步骤,每一步都简要注释了它在干什么、为什么这么干。

%% 1. 数据准备 % 假设你的数据存储在CSV文件中:两列,第一列是时间戳,第二列是电价(元/MWh) % 这里使用readmatrix读取,Matlab R2022a及以上版本均可 dataTable = readmatrix('electricity_price.csv'); price = dataTable(:, 2); % 提取电价列 price = fillmissing(price, 'linear'); % 缺失值线性插值 %% 2. 数据划分 % 严格按时间顺序切分,不用随机打乱 numTotal = numel(price); idxTrain = 1:floor(0.7 * numTotal); idxVal = floor(0.7 * numTotal)+1 : floor(0.85 * numTotal); idxTest = floor(0.85 * numTotal)+1 : numTotal; trainRaw = price(idxTrain); valRaw = price(idxVal); testRaw = price(idxTest); %% 3. 归一化(只用训练集的统计量) mu = mean(trainRaw); sigma = max(trainRaw) - min(trainRaw); trainNorm = (trainRaw - mu) / sigma; valNorm = (valRaw - mu) / sigma; testNorm = (testRaw - mu) / sigma; %% 4. 构建监督学习样本 numSteps = 48; [trainX, trainY] = createSequences(trainNorm, numSteps); [valX, valY] = createSequences(valNorm, numSteps); [testX, testY] = createSequences(testNorm, numSteps); % 注意:valNorm和testNorm的长度小于原始数据,因为需要预留前numSteps个点做历史 % createSequences函数见上文的定义 %% 5. 定义网络结构 numFeatures = 1; numHiddenUnits = 64; layers = [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(1) regressionLayer ]; %% 6. 配置训练选项 options = trainingOptions('adam', ... 'MaxEpochs', 200, ... 'MiniBatchSize', 64, ... 'GradientThreshold', 1, ... 'InitialLearnRate', 0.005, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 50, ... 'ValidationData', {valX, valY}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', 0); %% 7. 训练模型 net = trainNetwork(trainX, trainY, layers, options); %% 8. 测试集预测 testPredRaw = predict(net, testX, 'MiniBatchSize', 64); % 反归一化还原到原始量纲 testPred = testPredRaw * sigma + mu; testYOrig = testY * sigma + mu; % 真实值还原 %% 9. 评估指标 mae = mean(abs(testPred - testYOrig)); rmse = sqrt(mean((testPred - testYOrig).^2)); fprintf('MAE: %.4f 元/MWh\n', mae); fprintf('RMSE: %.4f 元/MWh\n', rmse); %% 10. 可视化 figure('Position', [100, 100, 1200, 500]); t = 1:numel(testYOrig); plot(t, testYOrig, 'b-', 'LineWidth', 1.2); hold on; plot(t, testPred, 'r--', 'LineWidth', 1.2); legend('真实电价', 'LSTM预测'); xlabel('测试集时间点(h)'); ylabel('电价(元/MWh)'); title('LSTM单步预测结果'); grid on;

关于createSequences函数,把前面那段定义复制到脚本末尾或单独保存为同名.m文件即可。

运行这段脚本后,你会看到横轴为时间点的预测曲线对比图。如果一切正常,预测曲线和真实曲线在整体走势上应该基本吻合,峰谷的位置大致对齐,但峰值高度可能略低于真实值——这是回归模型的常见现象,不要慌,后面会讲怎么优化。

7. 预测效果评估与结果解读:指标背后的真实含义

7.1 常用评估指标怎么算、怎么解读

电价预测任务中,最常用的回归评估指标有三个:MAE、RMSE、MAPE(平均绝对百分比误差)。它们的计算方式如下:

% 假设testPred和testYOrig已经反归一化回原始量纲 mae = mean(abs(testPred - testYOrig)); rmse = sqrt(mean((testPred - testYOrig).^2)); mape = mean(abs((testPred - testYOrig) ./ testYOrig)) * 100;

这三个指标的侧重点不同。MAE是绝对误差的平均值,直观易懂,但它对所有误差一视同仁。RMSE由于先平方再开方,对大误差的惩罚更重,如果模型在某些尖峰时段出现较大偏差,RMSE会明显偏高。MAPE用百分比表示误差占比,方便横向对比不同量纲的模型,但它的缺陷是当真实值接近零时,即使很小的绝对误差也会导致MAPE值巨大。电价数据里偶尔出现极低价格甚至负电价时,MAPE的意义会减弱,所以实际项目中我以RMSE和MAE为主,MAPE仅作参考。

7.2 单步预测置信度的主观判断

很多人在跑完模型后只看一个RMSE数值就结束了,这是远远不够的。我建议把预测曲线和真实曲线画在一起后,人工检查几个特征时段:

  • 峰时段预测是否滞后:如果预测的峰值比真实峰值晚出现或者提前出现,说明模型没有学好日内周期,问题可能出在numSteps太小或者时序编码特征没有加入。
  • 谷时段预测是否偏移:夜间低谷段电价相对平稳,如果这里误差依然很大,说明模型可能受到近期异常值干扰,可以尝试调整异常值清洗策略。
  • 突变点附近的误差:电价突变点通常是模型最难预测的部分,也是误差的主要贡献者。如果整条曲线RMSE不高,但突变点附近误差巨大,需要判断这些突变是否有可预测的前兆信号(比如负荷骤增),如果有,加入相应特征能大幅改善。

7.3 残差分布分析:下一步优化的方向图

训练完成后,顺手做一下残差分析能给你省下大量盲目调参的时间。计算残差:

residual = testYOrig - testPred; figure; histogram(residual, 50); xlabel('残差(元/MWh)'); ylabel('频数'); title('预测残差分布');

正常情况下的残差应该大致呈以0为中心的正态分布。如果残差分布明显偏斜或者右侧拖尾很长,说明模型在某些高价位场景下系统性偏低。这时候最好的改进方法不是盲目增加隐藏单元,而是回到数据侧:检查高价位时段是否有共同特征(高温、高峰负荷、特殊日期)没被建模,然后针对性添加特征。

7.4 交叉验证概念在时间序列里的变体

普通机器学习里的K折交叉验证在时间序列预测里不能直接套用,因为随机打乱样本会破坏时序依赖。如果你想更稳健地评估模型,可以用“滚动原点验证”(rolling origin validation):先用前70%的数据训练,评估接下来10%的数据;然后将训练集扩展到前80%,再评估接下来10%;以此类推。这样每次都模拟了“用历史预测未来”的真实场景,得到的评估结果比单次划分更可靠。

代价是训练次数成倍增加。如果只是为了验证模型效果,可以先在单次划分上跑通,确定超参数后再用滚动验证方式做最终确认。

8. 超参数调优的完整路径:从47.8到41.2的实战记录

分享一个真实的调参过程,帮助你把上文的各个参数串成一条完整的优化路径。

我在一个包含120天逐小时电价的公开数据集上跑了第一版模型。初始参数为:numSteps=24、隐藏单元128、单层LSTM、Dropout=0、学习率0.01、批次128。训练200轮后,测试集RMSE为47.8元/MWh。看起来能跑通,但说实话精度一般。

第一轮调整:把numSteps从24增加到48。RMSE从47.8降到43.6。这说明模型在单纯依赖日内周期的基础上,还从跨日信息里获得了增益。

第二轮调整:隐藏单元从128降到64,Dropout设为0.2。RMSE从43.6降到42.3。模型容量减小加正则化,缓解了过拟合。

第三轮调整:学习率从0.01降到0.005,训练轮数从200增加到300。RMSE从42.3降到41.8。收敛更平稳,尾部训练阶段的小幅震荡被抑制住了。

第四轮调整:加入星期编码作为额外特征。RMSE从41.8降到41.2。工作日和周末的差异被模型显式感知,尤其是休息日低负荷时段的预测准确度明显提升。

至此整个调参过程结束,我从一个“能跑通但精度一般”的模型,通过四轮有针对性的调整,把RMSE降低了约14%。整个过程中每一轮调整都只改变一个变量,这样做的好处是你能清楚知道每个参数对结果的实际影响方向,避免多个参数同时改变时无法归因的困局。

9. 进阶优化:多步预测、注意力机制、Transformer对比

9.1 从单步到多步预测的模型改动

如果你不满足于只预测下一个时刻,而是想预测未来24小时甚至更长的曲线,有几种不同策略。最简单的做法是把输出维度从1改成目标步数H,让LSTM的输出模式变成'OutputMode','last'并接一个fullyConnectedLayer(H)。但这种方法有一个固有问题:训练时每个样本独立预测未来H步,预测误差会随步长累积,模型在远端预测点上的误差往往很大。

更推荐的做法是递归多步预测:先用单步模型预测出T+1时刻,然后把预测值作为历史输入,继续预测T+2时刻,以此类推。这样做的好处是模型结构和单步预测完全一致,不需要重新训练,而且每一步都用上了最新预测结果。坏处是误差会随时间步逐步累积,预测到第24小时的时候,可能会有明显漂移。

还有一种做法是seq2seq结构,即编码器LSTM读取历史序列,解码器LSTM逐步生成未来预测。这种方法在多步预测任务中通常效果最好,但实现复杂度也更高。如果你只是想在现有单步模型基础上快速探索多步能力,我建议先用递归多步预测,跑通后再考虑是否升级到seq2seq。

9.2 注意力机制和Transformer:什么时候必要,什么时候没必要

LSTM的隐状态在传递过程中,即使有门控机制保护,长距离信息仍然有一定程度的稀释。注意力机制的核心思路是:在输出每个预测时,让模型可以直接“回看”输入序列中的任意位置,并学习每个历史时刻对当前预测的重要程度。在电价预测里,这意味着模型可以自适应地发现“昨天同一时段的电价对今天此时刻更重要”这类规律。

Transformer则是把自注意力机制作为基础模块,彻底抛弃了循环结构。它在长序列、大规模数据上的表现往往优于LSTM,但也有两个实际门槛:一是数据量需求大,Transformer在小样本时序数据上容易过拟合,二是Matlab对Transformer的原生支持不如LSTM成熟,需要更多手工构建。

我的建议很明确:如果你的数据量在几万条以内、任务是单步或短期多步预测,LSTM依然是性价比最高的选择。它调参简单、训练稳健、Matlab生态完备。只有当数据量达到十万级别以上、任务需要捕捉极长距离依赖,或者你已有成熟的GPU加速训练环境时,再考虑把Transformer搬上牌桌。

9.3 其他实用的特征工程手段

  • STL时序分解:把原始电价序列分解为趋势项、季节项和残差项,用LSTM分别预测三部分再相加。这个方法对强周期序列效果很好,但需要处理好分解过程中信息泄露的问题(对测试集分解时不能用全序列统计量)。
  • 滞后差分特征:在输入中加入“当前时刻与上一时刻的差值”作为一阶差分特征,这能让模型更容易捕捉电价的跳变趋势。
  • 节假日标记:如果数据覆盖范围包含法定节假日,加一个二值特征(1表示节假日,0表示非节假日)往往能显著改善节日期间电价预测偏差。

10. 我踩过的坑和最终的实操建议

最后把这些年做LSTM电价预测踩过的坑集中拉一个清单,每一条都是真金白银换来的教训。

数据泄露的坑。我第一次做完整流程时,用整个数据集的均值和标准差做归一化,结果测试集误差低得离谱,我还以为是模型厉害。后来换了新批次数据才发现泛化崩盘。从此以后,我只用训练集的统计量做归一化,验证集和测试集一律不参与任何统计量计算。

验证集被“偷看”的坑。某个版本我为了调参方便,把验证集误差最低的那一轮对应的模型作为最终模型,但这本质上是拿验证集做了模型选择,最终的测试集评估结果已经不能代表真实的泛化性能。正确做法是,验证集只用于早停监控,最终模型以“训练集+验证集”重新训练并截断在早停轮数附近,最后用测试集评估一次。

时间步长太短的坑。numSteps=6跑过一个实验,模型的预测曲线明显比真实曲线滞后了一整个相位。原因很简单,6个小时的历史窗口根本无法覆盖电价24小时的完整周期,模型只能看到“眼前”,看不到“昨天”。后来把时间步长提升到48,滞后现象基本消失。

隐藏单元过多的坑。前面已经提过,这里再强调一次:LSTM的隐藏单元数不等于“越大约好”。数据量只够支撑一个千量级参数模型时,强行上256个隐藏单元的结果就是严重过拟合。先小后大,逐步增加,直到验证集误差停止下降为止。

训练轮数不够的坑。LSTM收敛往往需要比较多的轮次。我第一次训练只设了MaxEpochs=50,训练曲线看起来已经下降,但测试集误差还有一个明显的下降空间没吃完。后来改成200轮,配合学习率衰减,效果才有实质提升。判断训练是否充分的方法很简单:看验证损失曲线是否已经进入平台期。

批次大小与数据量不匹配的坑。当训练集样本只有几百个时,MiniBatchSize设成128会让每个批次几乎覆盖全部样本,梯度更新极其平滑但方向容易锁定在局部极小值。小数据量下用32或64更合适,梯度噪声有助于跳出局部极小。

最后说一个我目前常用的工作流。拿到电价数据后,先做缺失值填充和异常值清洗,按7:1.5:1.5切分,只用训练集统计量做归一化,构造48小时历史窗口的监督样本,接一个64隐藏单元的LSTM层加Dropout 0.2,用Adam优化器配0.005初始学习率和200轮训练,每20轮在验证集上看一次损失。先把这个基线跑通,再根据验证损失曲线的形状决定往哪个方向调整。这套流程在多个电力市场数据集上都能稳定得到一个实用的预测模型,希望能帮你少走些弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 5:02:07

从原理到实战:gprMax探地雷达仿真全流程解析

简介:本资源是一套面向地质工程、考古探测与基础设施检测领域初学者及科研人员的GprMax2D/3D综合教程包,旨在系统解决地面穿透雷达(GPR)数值模拟入门难、参数设置模糊、结果解读困难等实际问题。压缩包共133个文件,涵盖…

作者头像 李华
网站建设 2026/9/6 1:02:27

Spring Boot高校就业信息管理系统实战:从设计到部署

简介:本资源是一套完整的基于Spring Boot的就业信息管理系统源码,面向Java Web开发初学者与高校毕业设计学生,解决校园招聘信息发布、企业岗位管理与学生求职对接等典型业务场景需求。压缩包含823个文件,总计19.65MB,涵…

作者头像 李华
网站建设 2026/9/3 22:15:04

Kimi K3 vs Claude vs GPT:大模型对比评测方案与实战指南

最近在技术交流群里看到不少同学在讨论一个话题:Kimi K3 是不是真的能打?起因是有不少开发者把 Kimi 最新模型和 Claude、GPT 系列放在一起比较,甚至有人直接把 Claude 系列叫成了“Claude Fable”,把 OpenAI 模型叫成了“GPT 5.6…

作者头像 李华
网站建设 2026/9/3 23:15:44

边坡稳定性弹塑性有限元分析:MATLAB代码原理与实操详解

简介:本资源是一套面向土木工程高年级本科生、研究生及岩土工程从业者的边坡稳定性弹塑性有限元分析MATLAB实现代码,聚焦地质灾害防治、道路桥梁支护设计与矿山边坡安全评估等实际工程问题。压缩包共42个文件,主体为41个MATLAB函数&#xff0…

作者头像 李华
网站建设 2026/9/6 10:47:14

Delphi 12.3集成CEF4Delphi:Chromium浏览器控件编译部署实战

简介:本资源是面向Delphi高级开发者与跨平台桌面应用工程师的CEF4Delphi开源控件集成方案,专为适配Delphi 12.3环境设计,解决在原生Delphi应用中嵌入现代Chromium浏览器引擎的核心需求。压缩包共含2000个文件,体量11.63MB&#xf…

作者头像 李华
网站建设 2026/9/4 1:29:59

51单片机三相可控整流:原理、触发角控制与Proteus仿真实战

简介:本资源是一套面向电子类专业本科生、单片机初学者及电力电子课程设计者的完整三相可控整流系统开发资料,聚焦51单片机对三相全控桥晶闸管的精确触发控制,解决导通角调节、同步脉冲生成与6脉波整流电压稳定输出等核心工程问题。压缩包共5…

作者头像 李华