简介:基于MATLAB与QRLSTM分位数回归长短期记忆网络的时间序列区间预测程序包,面向风速、负荷、功率等单变量预测场景,适合需要从点预测进阶到区间预测的研究者与工程师。QRLSTM借助LSTM提炼序列长期依赖,再利用分位数回归输出不同置信水平下的上下界,有效刻画预测不确定性,弥补传统点预测信息量不足的局限。资源共5个文件,4个M脚本覆盖自定义分位数回归层、网络训练、预测输出与PICP/PIMWP等区间评价指标,另附1个Excel数据文件,输入输出均为单变量,便于替换自有数据后直接运行,运行环境为MATLAB 2018及以上版本。压缩包仅20KB,轻量紧凑,当前已有495人学习下载。读者可结合完整源码与数据,系统掌握基于QRLSTM的时间序列区间预测建模流程、分位数损失函数设计及预测区间覆盖率、区间宽度等评估方法,减少从零搭建的重复工作,可直接用于风速、负荷、功率预测等科研或工程对比实验。
1. 区间预测不是点预测加个误差条:QRLSTM 到底改了什么
做时间序列预测的人迟早会撞上一堵墙:LSTM 输出的是一个点,而业务方要的是一个区间。电价要上下限,风速要置信带,股票回撤要最坏情况——点预测给出均值或中位数,但无法回答"最差会到多少"。传统的做法是在残差上假设正态分布,加两个标准差当作区间,可真实残差偏偏厚尾、偏态、异方差。QR-LSTM(分位数回归长短期记忆神经网络)换了个思路:不预测期望,直接预测条件分位数,让网络自己学出不同概率水平下的取值边界。这样做出来的区间不依赖残差分布假设,覆盖率和区间宽度都更可信。
这篇博文从损失函数推导出发,给出在 MATLAB 中从数据预处理、网络搭建到训练预测的完整路径,代码可以直接复制改写。适合已经跑通 LSTM 点预测、想进一步做区间预测的工程师,也适合在学术论文里需要对比区间预测方法的读者。整套方案用 MATLAB 原生深度学习工具箱实现,不需要额外安装第三方库。
2. 分位数回归与 LSTM 的组合逻辑:为什么在 MATLAB 里用 QRLSTM
2.1 分位数回归损失函数:从对称误差到非对称误差
普通回归最小化均方误差,得到的是条件均值。分位数回归最小化的是 pinball loss(分位数损失),核心在于给正负误差分配不对称的权重。对分位点 $\tau \in (0,1)$,单个样本的损失定义为:
L(y, y_pred, tau) = (y - y_pred) * tau if y >= y_pred (y_pred - y) * (1 - tau) if y < y_pred理解这个函数的关键在于"高估和低估的代价不同"。当预测值低于真实值时,误差项带着权重 $\tau$;反之带着 $1-\tau$。取 $\tau=0.5$ 时两边权重相等,最小化它得到的是条件中位数,比 MSE 对异常值鲁棒得多。取 $\tau=0.9$ 时,低估的代价远高于高估,优化器会偏向输出偏高的预测值,从而逼近第 90 百分位。
将这个损失函数接在 LSTM 输出层之后,就得到 QRLSTM。网络结构本身与普通 LSTM 没有区别,变化的只是损失函数和输出层的激活函数选择。输出层通常用线性激活(或 identity),因为分位数回归不允许输出被压缩到固定范围,否则尾部行为会被截断。
在 MATLAB 里,pinball loss 不需要自己写循环。用自定义损失函数或直接在训练循环里计算均可。下面这段代码定义了一个可用的分位数损失函数:
function loss = pinballLoss(Y, YPred, tau) % Y: 真实值,大小 [numResponses, numTimeSteps, numObservations] % YPred: 预测值,与 Y 同尺寸 % tau: 分位点,标量 0 < tau < 1 err = Y - YPred; loss = mean(mean(max(tau * err, (tau - 1) * err), 2), 1); end这段逻辑里max(tau * err, (tau - 1) * err)用矢量化的方式实现了分段定义:当err为正时取tau * err,为负时取(tau - 1) * err。两个mean依次对时间步和样本求平均,输出一个标量损失值。注意err的符号含义:Y - YPred > 0表示低估,正是上文说的 $\tau$ 权重对应的情况。
2.2 从三个分位点到完整区间:覆盖率和区间宽度怎么权衡
单次训练只输出一个分位数。要得到区间,需要对多个 $\tau$ 值分别训练模型,或者让网络输出多个值。$\tau=0.1$ 和 $\tau=0.9$ 的预测值围成的区间就是 80% 预测区间。区间质量看两个指标:覆盖率(真实值落在区间内的比例)和平均区间宽度。这两个指标天然互相牵制——区间越宽覆盖率越高,但预测也就越没信息量。
这里有一个常见误用:把多分位模型拆成多个独立 LSTM 训练。每个模型随机初始化不同,收敛路径不同,会导致 $\tau=0.1$ 的输出偶尔高于 $\tau=0.9$ 的输出,即分位数穿越问题。解决办法有两个,推荐第二个:
- 训练后排序:预测完成后对输出做单调化排序。实现简单,但破坏网络输出的连续性。
- 共享隐藏层多头输出:在 LSTM 层之上加一个全连接层,输出维度设为分位点个数,在损失函数中累加多个 $\tau$ 的 pinball loss。参数共享让不同分位数输出自然保持相关性,穿越问题大幅减少。
MATLAB 中第二种做法的损失计算代码如下:
function loss = multiQuantileLoss(Y, YPred, taus) % Y: [numResponses, numTimeSteps, numObservations] % YPred: [numQuantiles, numTimeSteps, numObservations] % taus: 行向量,例如 [0.1 0.5 0.9] numQ = numel(taus); loss = 0; for i = 1:numQ tau = taus(i); err = Y - YPred(i, :, :); loss = loss + mean(mean(max(tau * err, (tau - 1) * err), 2), 1); end loss = loss / numQ; end这个循环对分位点遍历,每个 $\tau$ 计算独立的 pinball loss 后取平均。维度上 YPred 第一维是分位点,MATLAB 的自动广播机制会把 Y 广播到每个分位点对应的通道上。这里的mean(mean(..., 2), 1)与上一节一致。这种做法比独立训练三个模型更稳定,训练时间也短得多,代价是最后一个全连接层要让每个分位点有独立的权重通道。
3. MATLAB 里复现 QRLSTM 的最小完整流程
3.1 数据构造:先验证可行性,再换自己的数据
完整复现 QRLSTM 不需要一上来就处理复杂的真实业务数据。用一个带趋势和周期性的模拟时间序列验证代码链路,比直接拿真实数据调试更高效。下面是构造示例数据的代码:
rng(42); T = 1000; t = (1:T)'; X = sin(2*pi*t/50) + 0.05*t + 0.5*randn(T, 1);这段代码生成 1000 个时间步的合成数据,包含周期项、线性趋势和高斯噪声。用它可以快速检验整个训练链路是否跑得通。
数据集划分时,用前 800 个时间步训练,后 200 个测试。构造 LSTM 训练样本需要把时间序列转换成「输入窗口到输出」的监督学习格式。常用做法是用前numTimeSteps个连续值预测下一位:
numTimeSteps = 20; XTrain = zeros(numTimeSteps, 780, 1); YTrain = zeros(1, 780, 1); for i = 1:780 XTrain(:, i, 1) = X(i:i+numTimeSteps-1, 1); YTrain(:, i, 1) = X(i+numTimeSteps, 1); end这段代码里XTrain的维度是[时间步, 样本数, 特征数],对应 MATLAB 深度学习工具箱要求的序列格式。循环中每个样本取 20 个连续时间点作为特征,第 21 个点作为目标值。得到 780 个样本是因为前 800 个时间步中,最后一个完整窗口需要 20 个点做输入,因此最多到第 780 个索引处取目标。测试集同理:
XTest = zeros(numTimeSteps, 180, 1); YTest = zeros(1, 180, 1); for i = 1:180 XTest(:, i, 1) = X(800+i-1 : 800+i+numTimeSteps-2, 1); YTest(:, i, 1) = X(800+i+numTimeSteps-1, 1); end这里测试起点选在 800,样本从800+i-1开始取,与训练集的结构保持一致。注意YTest中第一个样本的真实值对应原始序列的第 821 个点,后面的预测评估要以这个对齐关系为准,忽略这个问题很容易在画图时把预测曲线平移一个窗口。
3.2 网络定义:LSTM 层后面接什么输出层
QRLSTM 的网络主体与标准 LSTM 回归网络一致,唯一区别在输出维度。以三分位点为例,输出层需要 3 个神经元。用 MATLAB 的层数组定义:
numFeatures = 1; numHiddenUnits = 64; numQuantiles = numel(taus); % taus 已在上面定义 layers = [ sequenceInputLayer(numFeatures, 'Name', 'in') lstmLayer(numHiddenUnits, 'Name', 'lstm1') dropoutLayer(0.2, 'Name', 'drop') fullyConnectedLayer(numQuantiles, 'Name', 'fc_out') regressionLayer('Name', 'rout') ];sequenceInputLayer接受[特征数]参数,对应XTrain第三维为 1。lstmLayer的numHiddenUnits是核心超参数,64 个隐藏单元在中小规模数据上足够建模复杂依赖。dropoutLayer(0.2)放在 LSTM 之后全连接之前,防止过拟合,0.2表示随机丢弃 20% 的神经元输出。最终fullyConnectedLayer(numQuantiles)输出 3 个值。
问题在于regressionLayer在 MATLAB 中内置的损失是均方误差,不能直接用于分位数回归。因此这里不能用标准的trainNetwork一步训练,而要用dlnetwork配合自定义训练循环:
dlnet = dlnetwork(layers(1:end-1)); % 去掉 regressionLayerdlnetwork接受Layer数组,但要求最后一层不能是输出层。layers(1:end-1)截断掉最后的regressionLayer,保留从输入到全连接的完整计算图。后面的训练循环用modelGradients函数计算损失和梯度,再调用adamupdate更新参数。
3.3 训练循环:手动迭代才能自定义损失
训练函数的核心是数据转换成dlarray,在前向传播后计算 pinball loss,再用自动微分求梯度。完整的训练代码片段如下:
X = dlarray(XTrain, 'CTB'); Y = dlarray(YTrain, 'CTB'); numEpochs = 200; miniBatchSize = 64; learningRate = 0.01; trailingAvg = []; trailingAvgSq = []; for epoch = 1:numEpochs % 打乱数据顺序 idx = randperm(size(X, 2)); X = X(:, idx, :); Y = Y(:, idx, :); for i = 1:floor(size(X, 2)/miniBatchSize) batchIdx = (i-1)*miniBatchSize+1 : i*miniBatchSize; XBatch = X(:, batchIdx, :); YBatch = Y(:, batchIdx, :); % 计算梯度 [loss, gradients] = dlfeval(@modelGradients, dlnet, XBatch, YBatch, taus); % Adam 更新 [dlnet, trailingAvg, trailingAvgSq] = adamupdate(dlnet, gradients, ... trailingAvg, trailingAvgSq, epoch, learningRate); end enddlarray(XTrain, 'CTB')中的格式标签说明:C为通道维、T为时间维、B为批量维。dlfeval是 MATLAB 自动微分入口,它会把modelGradients函数内部所有操作记录在计算图中。adamupdate实现了完整的 Adam 优化器,trailingAvg和trailingAvgSq分别保存一阶和二阶动量,初始为空数组时函数会自动初始化。
modelGradients函数定义如下:
function [loss, gradients] = modelGradients(dlnet, X, Y, taus) YPred = forward(dlnet, X); loss = multiQuantileLoss(Y, YPred, taus); gradients = dlgradient(loss, dlnet.Learnables); endforward在前向传播时不累积梯度,比predict更高效。multiQuantileLoss函数沿用 2.2 节的定义。关键在dlgradient(loss, dlnet.Learnables),它返回损失对所有可学习参数的梯度,张量结构由 MATLAB 自动匹配。注意multiQuantileLoss内部的所有操作必须保持可微,max和mean都是可微操作,不需要额外处理。
3.4 预测与区间组装:三层境界
训练完成后,预测代码与普通 LSTM 没有区别:
dlYPred = predict(dlnet, dlarray(XTest, 'CTB')); YPred = extractdata(dlYPred); % YPred 尺寸: [3, 180, 1] lower = squeeze(YPred(1, :, :)); median = squeeze(YPred(2, :, :)); upper = squeeze(YPred(3, :, :));extractdata把dlarray转回普通数值数组。矩阵重构后lower、median、upper是三个长度为 180 的列向量。画区间图时用fill函数:
figure; t = 1:180; fill([t fliplr(t)], [lower' fliplr(upper')], [0.85 0.9 1], 'EdgeColor', 'none'); hold on; plot(t, YTest, 'k-', 'LineWidth', 1.5); plot(t, median, 'r--', 'LineWidth', 1);fill的第一个参数是 x 坐标序列,[t fliplr(t)]把正向和反向的时间轴拼接,构成多边形的回程路径。第二个参数对应 y 坐标,前半段用下界、后半段用翻转的上界,闭合出一个填充区域。颜色[0.85 0.9 1]是浅蓝色,虚线红表示中位数预测,黑色实线是真实值。这样一张图就能直观看出区间覆盖率与宽度。
4. 训练 QRLSTM 的三个必调参数与常见坑
4.1 学习率:分位数损失对学习率更敏感,配合学习率调度
分位数损失的梯度没有 MSE 那样平缓的二次形式。在分位点接近 0 或 1 时,pinball loss 在误差为负区间的斜率是 $1-\tau$,接近 0,梯度极小;在误差为正区间斜率是 $\tau$,很大。这种不对称梯度分布使得固定学习率容易在两个区域间震荡。实际调试中,初始学习率建议设置在 0.001 到 0.01 之间,配合piecewiseLearningRate调度:
learnRate = 0.01; dropFactor = 0.5; dropPeriod = 50; if mod(epoch, dropPeriod) == 0 learnRate = learnRate * dropFactor; end每 50 轮学习率减半,分位数损失在这种调度下比固定学习率收敛更快。调试时观察训练损失曲线,若损失震荡剧烈且不下降,优先把初始学习率调低十倍再试。
4.2 分位点个数:五个分位点通常够用,九分位留作验证
分位点选得越多,区间边界更平滑,能更精确地描述分布形状。但每个分位点对应输出层的一个神经元,分位点太多会让全连接层参数量线性增长,小数据集容易过拟合。常见做法是取奇数个对称分位点,比如 [0.1 0.5 0.9] 或 [0.05 0.25 0.5 0.75 0.95]。选三个点时训练最快;选五个点时区间边界更细,可以画出不同置信水平的嵌套区间。
增加分位点数量不需要修改网络结构之外的代码,只需改taus向量。训练时间大致线性增长,因为前向和反向传播的矩阵乘法维度变大了,但 LSTM 本身的参数量不变,所以增幅有限。经验上五个点对比三个点的训练时间增加约 20% 到 30%。
4.3 时间窗口长度:20 到 50 步是安全区间,长了不一定好
LSTM 理论上有长时间记忆能力,但 QRLSTM 的分位数回归目标让网络把更多容量分配给分布尾部,而不是序列模式记忆。窗口设得越长,输入维度越高,训练数据量需求越大,尾部估计反而越不稳定。对日频或小时频数据,窗口 20 到 50 步一般足够捕捉短期自相关。
判断窗口是否合适,看训练损失下降曲线:如果验证集区间覆盖率在训练后期抖动剧烈,可能是窗口过长导致过拟合。也可以做一个快速实验:同一数据集分别用 10、20、40 步训练三个模型,比较测试集平均区间宽度。宽度最小且覆盖率达标的窗口即为当前数据的最佳窗口。
4.4 常见坑:分位数穿越、NaN 梯度、标签错位
分位数穿越问题在多头输出模型中依然可能发生。排查方法很简单:预测完成后统计lower > upper的比例,如果超过 1%,说明模型对尾部学习不充分。解决手段除了共享隐藏层外,还可以在损失函数中加一个惩罚项,强制分位数按序排列:
function loss = orderedQuantileLoss(Y, YPred, taus) loss = multiQuantileLoss(Y, YPred, taus); penalty = 0; for i = 1:numel(taus)-1 penalty = penalty + mean(mean(max(0, YPred(i,:,:) - YPred(i+1,:,:)))); end loss = loss + 0.1 * penalty; endpenalty对相邻分位点的逆序差值取正部,即只有逆序时才产生惩罚,排序正确时惩罚为 0。系数0.1是推荐起点,太大则会让网络牺牲损失精度来强制排序。
NaN 梯度是训练循环常见问题,通常由数据中包含NaN或Inf导致。在dlfeval抛出梯度计算错误时,第一反应是检查原始数据是否干净,而不是怀疑网络结构。用any(isnan(XTrain(:)))一行代码排除。
5. 区间校准与一致性:QRLSTM 上线前的最后一公里
区间预测做出来只是第一步,上线前要验证一件事:预测区间在统计上是否可信。如果 90% 区间实际覆盖率只有 70%,业务方据此做决策会高估风险。这引出一个叫"区间校准"的概念:测试集上的经验覆盖率要接近名义覆盖率。
经验覆盖率计算方式是真实值落在预测区间内的样本比例。结合 3.4 节的预测结果,用如下代码计算:
coverage = mean(YTest >= lower & YTest <= upper, 'all'); fprintf('名义覆盖率 80%%,实际覆盖率 %.2f%%\n', coverage * 100);理想情况下输出实际覆盖率接近 80。如果偏差超过 5 个百分点,优先检查训练轮数是否足够。分位数回归的收敛速度通常比点预测慢,因为尾部区域的梯度信号稀疏。200 轮不够就加到 500 轮,观察验证集覆盖率是否趋于稳定。
覆盖率达标后的另一个诊断指标是 Pinball Score,它同时惩罚过窄和过宽的区间,是区间预测任务的标准评估指标。为了实现上的一致性验证,对比两个 QRLSTM 变体或对比 QRLSTM 与 Bootstrap LSTM 区间方法,Pinball Score 提供了可比较的量化基准。计算代码:
pinballScores = zeros(numel(taus), size(YPred, 2)); for i = 1:numel(taus) err = YTest' - YPred(i, :); pinballScores(i, :) = max(taus(i) * err, (taus(i) - 1) * err); end meanPinball = mean(pinballScores, 'all');这个分数结合了区间覆盖率和宽度两个维度的信息,数值越低越好。切换数据集或调整参数时,对比meanPinball比单独看覆盖率更稳健。例如区间特别宽时覆盖率肯定高但 Pinball Score 会恶化,这能防止过度保守的预测。
最后检查分位点之间的单调性,用all(YPred(1,:) <= YPred(2,:) & YPred(2,:) <= YPred(3,:))一键验证。若穿越比例不低,4.4 节的排序惩罚项要在训练时加入而不是事后补救。经过覆盖率与 Pinball Score 双重验证的 QRLSTM,才具备进入生产环境的资格。
本文还有配套的精品资源,点击获取