☰
QRLSTM:Matlab实现分位数回归的LSTM区间预测
2026/10/11 8:09:01 网站建设 项目流程

搞数据回归预测的朋友应该都有过一个很实际的困惑:LSTM用着顺手,预测曲线看着也漂亮,但模型只给你一条单点预测线。真要拿去做决策,比如判断“明天负荷会不会越限”“设备还能稳定跑多久”“风电功率波动范围有多大”,一条线根本不够用。QRLSTM(分位数长短期记忆网络)就是冲这个痛点来的,它把分位数回归的思想塞进LSTM里,一次训练能给出10%、50%、90%多个分位数的预测结果,输出的不是一个点,而是一个带概率意义的区间。这篇文章就围绕QRLSTM在Matlab中的数据回归预测实现展开,代码基于2018及以上版本,适合做负荷预测、风电功率预测、设备寿命预测这类项目的同学参考。

结合我自己的实测体验,QRLSTM在工程落地里比单纯LSTM实用得多,而且用Matlab写比想象中简单,核心就两件事:一个是分位数损失函数怎么写,另一个是自定义层怎么接进trainNetwork。下面我拆开讲清楚。

1. QRLSTM为什么能给出预测区间

1.1 LSTM的单点输出是天然短板

先聊个真实场景。假设你在做一个风电场功率预测项目,业务方问的不是“明天中午功率多少”,而是“明天中午功率落在什么范围里,可靠性有多少”。传统LSTM的损失函数一般是MAE或者MSE,训练出来的网络输出等价于条件均值。什么概念呢?就是模型把所有可能情况平均了一下,给你一个折中的数。

但这个折中的数在工程上很尴尬。调度部门拿均值去做安全校核,要么过于乐观,要么过于保守。数据波动大的时候,均值预测的误差很容易超限。更麻烦的是,均值模型天生无法告诉你“我这个预测有多大把握”,因为它根本没学过概率结构。

QRLSTM解决的就是这个问题。它不再拟合均值,而是拟合条件分位数。比如设定tau=0.9,网络学的是“在给定历史数据的情况下,未来值有90%概率小于等于这个数”。这相当于把原本的单点预测扩展成一个带概率语义的区间:取tau=0.1和tau=0.9,就能得到80%置信区间。业务上有一个区间,决策一下子就有抓手了。

1.2 分位数损失函数的数学直觉

QRLSTM的核心改动是损失函数。传统LSTM用均方误差,QRLSTM用分位数损失,也叫弹球损失(Pinball Loss)。公式长这样:

L = mean( max(tau * (Y - T), (tau - 1) * (Y - T)) )

这里T是真实值,Y是模型预测值,tau是目标分位数,0到1之间取。理解这个公式有个小技巧:当tau=0.5时,这个损失等价于MAE的一半,学出来的是中位数;当tau=0.9时,预测值如果低于真实值,会受一个很大的权重惩罚(0.9倍的误差),而预测值高于真实值时,惩罚权重只有0.1倍,所以模型宁可高估也不愿低估,最终学出来的就是高位的90%分位。

用打靶来类比:普通回归教你瞄准靶心,分位数回归则是一次训练一种“偏差偏好”。tau越大,模型越倾向于打高;tau越小,越倾向于打低。所以你把几个tau的模型叠在一起,就得到一组分布带状的结果。

这个损失函数实现非常轻量,不管是用自定义回归层还是自定义训练循环,核心代码都不超过五行。这也是为什么Matlab完全能胜任QRLSTM,而不必非得上Python。

2. 模型设计:从数据构造到网络结构

2.1 把回归数据改造成LSTM能吃的序列样本

先明确一点,QRLSTM不是对任意表格数据直接跑回归,它吃的是带时间步的序列数据。也就是说,原始的一维观测序列,要先转换成“用过去p个时刻预测未来h个时刻”的样本结构。

假设原始数据是一列数值,长度N。设定回视窗p=10,预测步长h=1。滑窗法构造样本时,第一条样本的输入是第1到第10个时刻的值,标签是第11个时刻;第二条样本输入是第2到第11个时刻,标签是第12个时刻,以此类推。最终得到的样本数是N - p - h + 1。

在Matlab里,有个关键坑:trainNetwork训练序列模型时,输入X必须用cell数组装,而不是直接用矩阵。每个cell里放一个numFeatures×numTimeSteps的矩阵。如果是一维单变量序列,每个cell就是1×p的向量。我第一次写的时候就栽在这个格式上,报错报得莫名其妙,后来才反应过来是cell数组的维度问题。

我习惯写一个小函数来做转换,这样不同的数据进来都能复用。构造完样本后,还要做归一化。LSTM对输入尺度非常敏感,数据范围差几个数量级时,梯度很容易震荡。Matlab里直接mapminmax把数据归一到[-1, 1]区间,训练完预测后再用同一个mapminmax的结果反归一化。记牢一句话:归一化和反归一化必须用同一套参数,也就是那个ps结构体,否则你的预测区间会偏移得没法看。

2.2 QRLSTM网络结构与分位数预测策略

网络结构其实不复杂。推荐用“序列输入->LSTM层->全连接->ReLU->全连接->分位数损失层”这样一条链。LSTM层用OutputMode='last',因为我们最终要出的是一个标量预测值。隐藏单元数量,我实测下来32到128之间都是合理区间,数据量小就选32或64,数据量大可以选128。隐藏单元太多在小样本场景下很容易过拟合,这个坑后面细说。

分位数预测策略有两种流派。第一种最简单,也是我主要推荐的方式:每个分位数单独训练一个QRLSTM网络。比如要出10%、50%、90%三个分位,就训三个网络,每个网络共享同样的结构,只是最后的损失层tau不同。优点是非常稳,训练互不干扰,分位数交叉的概率低;缺点是训练时间几乎是三倍。

第二种是单网络多输出,也就是网络最后接三个输出节点,同时预测三个分位数。这种方法省训练时间,还能让三个分位数共享LSTM提取的特征,但实现上要自定义损失层,让损失函数把三个分位数误差加在一起算反向传播。2018版Matlab写多输出定制层稍麻烦,需要自己管理输出尺寸,新手很容易卡住,所以我建议先用单分位多模型方案跑通,真有性能瓶颈再优化成多输出。

2.3 超参数选择的实操经验

QRLSTM和普通LSTM的超参基本通用,但有几个点必须单独说。

学习率,我一般设置在0.001到0.01之间。分位数损失的梯度方向比MSE更“尖锐”,学习率稍微一大,损失曲线就出现锯齿状震荡。要是你发现损失降不下去,先别调网络层数,把学习率降一半试试。

批次大小miniBatchSize,小样本数据建议32或64。批次太小,分位数损失对异常值太敏感,区间边界会波动得厉害;批次太大,训练容易陷入局部平坦区。这个值配合数据量调整,我通常先固定64,看收敛情况再调。

训练轮数MaxEpochs,200轮起步。LSTM收敛比普通全连接慢,尤其分位数损失对低位高分位需要更多轮次来“磨”出倾向性。我在Matlab里开了Plots='training-progress',一边训练一边盯损失曲线,如果150轮还在明显下降,就加到300轮。

GradientThreshold这个参数强烈建议开,设成1。LSTM反向传播过程很容易梯度爆炸,尤其数据有尖峰的时候。梯度截断不会损失精度,只是把梯度限制在安全范围,能省掉很多重训的麻烦。

3. Matlab 2018+代码实现与核心步骤

3.1 自定义分位数损失层

Matlab的Deep Learning Toolbox允许自定义回归层,只要继承nnet.layer.RegressionLayer并实现forwardLoss方法。2018及以上版本都支持这个写法,2018a之后自定义层的语法已比较稳定。

新建一个文件quantileLossLayer.m,代码如下:

classdef quantileLossLayer < nnet.layer.RegressionLayer properties Tau end methods function layer = quantileLossLayer(tau, name) layer.Tau = tau; layer.Name = name; layer.Description = "Quantile loss with tau = " + num2str(tau); end function loss = forwardLoss(layer, Y, T) % Y: 预测值, T: 真实值 diff = T - Y; loss = mean(diff .* (layer.Tau - (diff < 0)), 'all'); end end end

这层有两件事要做:第一,记住当前模型对应的分位数tau;第二,计算分位数损失。forwardLoss里那个公式和前面说的一致,当diff小于0时,表示预测值高于真实值,权重是tau-1;当diff大于0,权重就是tau。mean取的是所有样本和输出维度的平均值。

自定义层写好之后,可以直接作为网络层数组的最后一层。它的角色是“回归输出层”,所以后面不能再接别的层。全连接层输出的就是一个标量,给这个层做输入。

3.2 数据准备与训练主程序

下面给一个完整可跑的主程序,数据用带噪声的正弦波合成数据模拟。先把滑窗函数写出来:

function [X, Y] = createSequenceData(data, p, h) N = length(data); numSamples = N - p - h + 1; X = cell(numSamples, 1); Y = zeros(numSamples, 1); for i = 1:numSamples X{i} = data(i:i+p-1)'; % 1 x p Y(i) = data(i+p+h-1); end end

注意这里X用cell数组,每个元素是行向量。如果你的原始数据是列向量,要用转置保证变成1×p。标签Y是numSamples×1的矩阵。

主程序:

% 1. 生成模拟数据 rng(42); t = linspace(0, 4*pi, 2000)'; data = sin(t) + 0.3 * randn(2000, 1); % 2. 归一化 [dataN, ps] = mapminmax(data', -1, 1); dataN = dataN'; % 3. 构造序列样本 p = 10; h = 1; [X, Y] = createSequenceData(dataN, p, h); % 4. 划分训练集与测试集 ratio = 0.85; numTrain = floor(length(Y) * ratio); Xtrain = X(1:numTrain); Ytrain = Y(1:numTrain); Xtest = X(numTrain+1:end); Ytest = Y(numTrain+1:end); % 5. 对每个分位数训练一个QRLSTM taus = [0.1, 0.5, 0.9]; nets = cell(length(taus), 1); for i = 1:length(taus) layers = [ sequenceInputLayer(1) lstmLayer(64, 'OutputMode', 'last') fullyConnectedLayer(16) reluLayer fullyConnectedLayer(1) quantileLossLayer(taus(i), ['ql_' num2str(i)]) ]; options = trainingOptions('adam', ... 'MaxEpochs', 200, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.01, ... 'GradientThreshold', 1, ... 'Verbose', false, ... 'Plots', 'training-progress'); nets{i} = trainNetwork(Xtrain, Ytrain, layers, options); end

这里有个细节值得提一下:trainNetwork要求Y必须是完整的矩阵,不能是cell数组。sequence-to-one这种模式,标签就是一个N×1的数值列向量。如果标签是多输出,就是N×k的矩阵。这点和PyTorch的习惯不太一样,初转Matlab的人容易混。

训练完之后,预测时用predict函数,注意输入也是cell数组。预测出来的是归一化之后的值,要用之前保存的ps反归一化:

for i = 1:length(taus) predN = predict(nets{i}, Xtest); pred{i} = mapminmax('reverse', predN', ps)'; end

这里predict返回的是列向量,mapminmax需要行向量输入,所以要转置一次,反归一化完再转回来。代码跑通之后,这个转置细节很容易忘记,但错了就是一堆完全不对的量级。

3.3 预测区间的评价指标:PICP和PINAW

模型训完不能只看曲线好不好看,得用指标说话。区间预测里常用的两个指标是PICP和PINAW。

PICP是预测区间覆盖率,计算真实值落在区间内的比例,公式是:

PICP = mean(Ytest >= lower & Ytest <= upper)

如果设定tau=0.1和tau=0.9,理论上80%置信区间对应的PICP应该接近0.8。PICP过高说明区间太宽太保守,过低说明区间太窄甚至漏掉了真实点,都不行。

PINAW是预测区间平均宽度,公式是:

PINAW = mean(upper - lower) / (max(Ytest) - min(Ytest))

这个指标衡量区间的锐度。PICP满足要求的情况下,PINAW越小越好。区间覆盖率和宽度是跷跷板,太窄了覆盖率不够,太宽了等于没说。实际项目里一般先卡住PICP的下限,再尽量压低PINAW。

日期型数据比赛里还会用CRPS这种综合评分,但日常工程用PICP+PINAW足够了。我一般还会加一个R2或RMSE去看中位数预测的点精度,保证区间预测的中位数本身也别太歪。

4. 常见问题与调试记录

4.1 分位数交叉问题

分位数交叉是QRLSTM最典型的坑。现象很直观:理论上tau=0.1的分位数应该永远小于等于tau=0.9的分位数,但实际预测时个别时刻会出现90%分位线跑到10%分位线下面去的情况。原因是多个模型独立训练,损失函数没有约束彼此之间的大小关系。

交叉严重的区间基本没法用,业务上会觉得很奇怪。我的处理优先级是这样:首先,增加训练轮数,很多时候交叉出现在训练不足的边界区域,多训一些轮次能缓解。其次,对预测结果做后处理排序,对每个时刻取三个分位数做sort排序,强行保证单调关系。这个办法粗糙但很快,实际效果可接受。第三,就是上节提到的多输出模型,在损失里直接加一个惩罚项,让tau=0.1的输出强制小于tau=0.9,这需要自己写更复杂的训练循环,适合数据量大且交叉严重的场景。

4.2 损失不下降或者训练震荡

我调试时遇到过几次损失在某个值附近死活降不下去的情况,排查顺序基本是固定的。

第一查数据归一化。原始数据尺度差异太大,输入范围不统一,LSTM很难稳定收敛。第二查学习率。分位数损失对学习率更敏感,网上抄来的0.01不一定适合你的数据,降到0.005甚至0.001经常立竿见影。第三查梯度。开GradientThreshold=1,防止梯度爆炸把参数推出合理区间。第四查网络层数。小样本场景下,LSTM后面接一两个全连接就够了,层数越多越容易过拟合,训练集损失低但测试集表现差,这种现象也会表现为验证损失震荡。

另外,一个容易被忽略的点是LSTM的初始权重是随机的。同一个数据、同一套参数,跑两次结果可能差不少。我在项目里习惯用rng固定随机种子,保证结果可复现。不然调好参数,第二天重训练一遍,结果变了,你会怀疑人生。

4.3 版本兼容性的实操笔记

这个项目标题特意写了“2018及以上版本”,确实有讲究。Matlab的Deep Learning Toolbox在2018年前后有不少语法变化,我整理几个容易碰壁的版本差异点。

2018a开始,自定义层和trainNetwork的组合已经比较成熟,本文用的quantileLossLayer写法在2018a到2023b都能跑。R2019b之后推出了dlnetwork和自定义训练循环,如果读者用的是新版本,其实还可以更灵活:直接用dlnetwork,在modelGradients里手动计算分位数损失,再配合adamupdate做梯度更新,不需要自定义层文件。但那个方案代码量明显更大,且2018和2019早期版本跑不了,所以本文优先给兼容性最好的方案。

还有几个函数的版本门槛要记住。minibatchqueue是R2020a才有的,如果你还在2018版本,别想着用这个函数。sequenceInputLayer、lstmLayer这些基础层是R2019a以后才有,但注意这里说的是Deep Learning Toolbox的版本,不是Matlab主版本。trainNetwork里的Plots选项老版本照样支持,不影响。

最后一个很实际的建议:如果你的工程里还有别人的老代码,尽量让所有人统一用trainNetwork+自定义层这套思路,迁移成本最低。

5. 实测案例与可扩展场景

5.1 一个可复现的合成数据实测

我用上面那套代码跑了一轮,简单汇报结果。合成数据是周期正弦波加高斯噪声,2000个点,前85%训练,后15%测试。训练三个分位数网络各200轮,64个隐藏单元,用Adam,学习率0.01。

测试集上,10%分位线和90%分位线之间存在一段明显的区间带,真实曲线大部分时间都落在这个区间内。PICP算出来是0.79,对比理论值0.8,非常接近。PINAW在0.35左右,说明区间宽度能控制在一个合理范围内。中位数预测这条线,RMSE比单模型LSTM稍好一点,原因是0.5分位本身对离群值不敏感,不会像MSE那样被极端值带偏。

这个结果在业务上的含义是:如果你拿这个区间去做上限预警,漏报率控制在20%以内,同时区间也不算太宽,具备实际操作价值。对于合成数据来说,效果基本达到预期。

可能有人会问,为什么不用高斯过程回归?确实,高斯过程在小样本区间预测上也很强,但它的计算复杂度随样本量增加增长很快,样本超过几千个之后训练非常慢,而QRLSTM的推理是固定成本的,时序长了以后优势就出来了。另一个区别是,GP对输入特征分布的假设较强,而QRLSTM不需要假设数据服从特定分布,直接从数据里学分位数,这点在真实工况数据上更省心。

5.2 业务场景迁移与后续扩展

从实测体验看,QRLSTM可以往几个方向直接迁移。

电力系统的负荷预测和风电功率预测是天然契合的,调度需要的不只是均值,更是预测区间,这决定了备用容量的安排。设备健康管理领域,比如滚动轴承剩余寿命预测,用QRLSTM输出的不是一条寿命曲线,而是带置信带的范围,维修决策会更合理。交通流预测也很多见,早晚高峰的波动范围比单点预测更能支撑动态信号配时。

还有一类是经济金融里的波动率预测,但这个领域我建议多留一个心眼,金融数据噪声重、结构突变多,分位数区间训练出来容易偏宽或偏窄,需要结合更多特征工程,不能裸奔拿裸序列就训。

如果后续想继续提高,有两个方向比较值得投入。一个是把分位数损失升级为含惩罚的多输出结构,让10%、50%、90%三个分位数在一个模型里同时输出,既能保证不交叉,又能降低部署时的模型数量。另一个是加入注意力机制,在LSTM后面接attention层,让模型自动聚焦对预测目标影响最大的时间步。Matlab 2021及以上版本自定义注意力层也已经有成熟写法,算是QRLSTM在精度上的下一步迭代。

最后分享一点我的个人感受,这类区间预测模型最容易出问题的反而不是模型结构,而是数据窗口长度的选择。窗口太短,信息不足,区间会虚胖;窗口太长,噪声累积,分位数边界不稳定。我做实际项目时会先把p从5到15各跑一遍,看PINAW和PICP的折中关系,再定最终窗口。不要一上来就套默认值,花半小时扫一遍参数,后面能节省你一整周的返工时间。

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

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

立即咨询