ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Matlab中Bagging时间序列预测:滞后特征构造与TreeBagger实践

Matlab中Bagging时间序列预测:滞后特征构造与TreeBagger实践 时间序列预测做多了以后很多人会有一个共同感受单模型拟合阶段的误差再好看放到未知时间片段上预测曲线还是会像心电监护仪一样上下乱跳。我在Matlab里做设备关键指标预测和负荷趋势测算时被这个问题折磨过很久后来转到基于集成模型的Bagging方法上才算是把预测结果从“碰运气”变成了“稳定可用”。这篇文章就把我实际验证过的Bagging时间序列预测方案完整拆开讲一遍包括滞后特征怎么构造、TreeBagger的关键参数怎么给、如何避免时间维度上的数据泄露以及哪些坑是教科书不会告诉你的。这篇内容更适合在Matlab里已经有基本的数据处理和循环基础、想把预测结果真实落地的人。如果你正在用LSTM折腾数据量不算大的单变量时间序列或者被单棵决策树、单一模型的方差折磨到怀疑人生都可以先停一下试试Bagging的思路。它的优点是逻辑直观、代码量不夸张、对异常值和突变段的容忍度也比很多单一模型高用一句话概括就是不追求某一次预测的惊艳而是把一堆有一点小毛病但整体方向没错的模型平均起来让犯错彼此抵消。1. Bagging算法为什么刚好打在时间序列预测的痛点上1.1 Bagging做的事重采样、并行训练、平均输出Bagging这个名字来自Bootstrap Aggregating核心动作就两步。第一步从原始训练样本里有放回地随机抽取多个子样本集每个子样本集的大小通常和原始样本一致但由于是有放回抽样每个子集里大约会有三分之一的样本是重复的同时也会漏掉一些原始样本。第二步在每个子样本集上独立训练一个基学习器预测阶段把这些基学习器的输出取平均回归问题平均分类问题投票。听起来平平无奇但它在统计学上的意义很明确如果一个模型对训练数据的微小变化非常敏感那么它本身就是高方差的而Bagging恰好能把这种方差压下去。比如决策树你换一批训练样本树的切分点可能完全不一样但只要这些切分点并不是完全错误的多棵树平均之后局部的过拟合噪音就会被互相抵消对应的真实趋势则会在投票或平均中保留下来。1.2 时间序列预测里增益最大的环节是方差还是偏差很多文章讲Bagging只会说“降低方差”但放到时间序列预测里这件事需要说得更具体一点。时间序列数据通常带有连续的漂移、周期波动和局部突变单棵树拟合出来的预测函数往往会在某些时间片段上偏离真实值这种偏离不是均值意义上的系统性偏差而是不同时间片上的随机偏移。Bagging对系统性偏差无能为力但对这种随机偏移有很明显的平滑效果。理论上有一个很经典的误差分解公式泛化误差可以拆成偏差、方差和噪声三部分。Bagging是典型的不怎么改变偏差、专注于降低方差的方法。它的前提是基学习器本身足够不稳定也就是方差占主导。如果你拿线性回归做Bagging效果通常很有限因为线性回归在重采样下结构变化不大但决策树是出了名的“重生一次变一个样”的模型所以树集成几乎成了Bagging和随机森林的标准组合。在时间序列场景里这种特性正好匹配你需要模型去适应局部非线性又不想让某一个时间段的偶然抖动主导最后的结果那么树Bagging就是个天然加分项。1.3 为什么不直接上LSTM图省事现在一提时间序列预测大量文章第一反应就是LSTM。但LSTM有几个现实问题一是训练过程容易受随机初始化影响同样的数据跑两次结果都不一样在小样本上尤其明显二是超参数多隐藏层数量、学习率、dropout、时间步长都要调调参成本高三是对异常值和样本量要求很高工业数据里能拿到的干净历史序列往往只有几百个点LSTM在这种规模下很难发挥出应有的优势。我不是说Bagging能替代LSTM它是另一种思路当数据规模中等偏小、你要快速得到一个稳健预测基线的时候树Bagging是性价比最高的方案。用Matlab的TreeBagger写下来从数据准备好到出预测结果和误差指标基本可以在二十分钟内完成而且结果具备可解释性还能输出预测区间。这套东西在落地项目里当基线模型、当最终预测模型、甚至当LSTM结果的交叉验证参照物都很有价值。2. 时序数据进Bagging之前的那道坎滞后特征矩阵2.1 从一列原始序列到一张监督学习表格Bagging本身不会“理解”时间它处理的是一张普通的特征矩阵和标签向量。所以要把时间序列预测问题翻译成Bagging能解决的问题用过去的若干个观测值去预测当前的观测值。假设原始序列是y1、y2、y3一直到yN我取滞后期数maxLag3那么第一条样本的特征就是(y1, y2, y3)标签是y4第二条样本的特征是(y2, y3, y4)标签是y5。以此类推最终得到一张形状为(N-maxLag)行、maxLag列的特征矩阵以及对应的标签列。这个过程在时间序列领域叫滑窗或延迟嵌入。你不需要理解太深的动力学理论只需要明白窗口宽度决定了模型能“看到”多长的历史。窗口太短模型缺少必要的上下文信息窗口太长特征矩阵变大、训练变慢而且可能引入太多和预测点关系不大的噪声。一个常见的经验值是先用自相关函数或者偏自相关函数看一眼再设定为几个候选值做对比。2.2 窗口宽度maxLag怎么定Matlab里有现成的函数可以做这件事。对平稳序列调用autocorr(y)能画出自相关图自相关衰减到置信区间之外的最大滞后阶数可以当作参考窗口调用parcorr(y)则能看偏自相关它能在剔除中间滞后影响后帮你找到真正有解释力的滞后点。偏自相关图里某个阶数突然截断或者明显低于置信线往往就是合适的窗口位置。实际项目里我很少把一个窗口值用到死通常是先看偏自相关给一个范围比如偏自相关显示第2阶之后都不显著那窗口选2到4之间如果序列有很强的季节效应比如24小时周期或者12个月周期窗口里至少要包含一个完整周期否则模型永远学不到周期性反弹的规律。注意这个判断本身也属于特征工程它对于Bagging这类模型的影响力甚至比树的数量更关键。2.3 训练/测试的时序约束一句话守住底线监督样本造好之后最容易犯的错误是直接随机打乱样本划分训练集和测试集。在普通机器学习里这是标准操作在时间序列里这叫“偷看未来”。因为相邻几行样本的时间段高度重叠如果测试集里的某条样本和训练集里某条样本只差一个时间步模型可能已经把答案背下来了测试误差会低估得一塌糊涂。我的原则是时间序列预测的样本划分必须按照原始时间顺序训练集永远取前面的时间点测试集永远取后面的时间点。而且凡是参与了特征构造的滞后值如果它属于测试时间段就不能偷偷出现在训练样本里。这一条看似简单但真实项目里数据清洗、特征拼接步骤一多很容易在某个环节不小心把未来的数据带到训练矩阵中。后面我会专门讲一个我自己踩过的例子。3. 手把手写一套可复现的Matlab Bagging预测代码3.1 环境与数据准备Matlab官方统计和机器学习工具箱里包含了TreeBagger、fitensemble等函数我用的是TreeBagger版本方面R2021a之后都支持更早的版本只要拥有Statistics and Machine Learning Toolbox也能跑通。为了让文章可以完全复现我不使用需要外部下载的数据集而是生成一段合成的非线性周期序列。这段序列带有两个频率不同的正弦周期和随机噪声形状上很接近某些设备运行指标的真实形态方便你替换成自己的CSV数据。rng(42); T 1000; time (1:T); season 15*sin(2*pi*time/24) 6*sin(2*pi*time/7); noise 5*randn(T, 1); y season noise;如果你有自己的历史数据可以直接把y替换成load(your_data.mat)或者readmatrix(your_data.csv)得到的一维列向量。我建议先把数据画出来看一眼确认没有缺失值和明显的异常跳点再做下一步。缺失值在滑窗构造中会像病毒一样蔓延整行务必提前处理。3.2 监督样本构造代码和边界细节下面这段代码把向量y转换成特征矩阵X和标签向量Y。核心循环从第maxLag1行开始因为前maxLag个点没有足够的历史来充当特征。maxLag 8; X []; Y []; for i (maxLag1):T X(end1, :) y(i-maxLag:i-1); Y(end1, :) y(i); end这里的写法是最容易理解的版本但X(end1,:)在数据量很大时会有额外的内存复制开销。如果序列长度超过几万建议用预分配nSamples T - maxLag; X zeros(nSamples, maxLag); Y zeros(nSamples, 1); for i 1:nSamples X(i, :) y(i:imaxLag-1); Y(i, :) y(imaxLag); end我实际使用中更喜欢第二种写法它的索引逻辑非常直观第i条样本用第i到第imaxLag-1个观测作为特征预测第imaxLag个观测。注意两种做法在索引上表示的滞后关系是一致的选哪种看你习惯。3.3 TreeBagger训练参数怎么给比较稳训练部分的代码短得惊人trainNum 800; Xtrain X(1:trainNum, :); Ytrain Y(1:trainNum); nTrees 100; bag TreeBagger(nTrees, Xtrain, Ytrain, ... Method, regression, ... MinLeafSize, 5, ... NumPredictorsToSample, all, ... OOBPrediction, on);TreeBagger第一个参数是树的数量第二个和第三个分别是特征矩阵和标签列之后是键值对参数。Method设成regression是因为做连续数值预测MinLeafSize是叶节点最少样本数它起到控制单棵树复杂度、防止过拟合作坊的作用具体取值取决于你的样本量如果训练样本只有几百条尝试MinLeafSize在3到8之间都合理NumPredictorsToSample表示每次分裂时随机抽取的候选特征数量我把它设成all可以让模型把全部滞后值都纳入考虑因为滞后特征相互之间有较强相关性适当让每棵树都见到全部特征在单变量序列里反而更稳。如果想减少训练耗时可以把NumPredictorsToSample设成ceil(maxLag/3)也就是随机森林常见的默认风格效果通常也很接近。我做过对比在滞后特征只有几个维度时all和ceil(maxLag/3)的差异并不大。OOBPrediction开启后模型会记录每棵树没有见过的样本计算袋外预测误差。这个误差可以当作一个不需要单独划分验证集的内部估计在模型调参时很有参考价值。3.4 预测、指标计算与可视化测试集我选择了时间顺序上靠后的样本并且保证测试样本的特征只用到测试区间之前或者逐步滚动得到的真实观测避免任何未来信息泄漏。testIdx (trainNum1):(size(X,1)); Xtest X(testIdx, :); Ytest Y(testIdx, :); [Yhat, Ysd] predict(bag, Xtest); rmse sqrt(mean((Ytest - Yhat).^2)); mae mean(abs(Ytest - Yhat)); mape mean(abs((Ytest - Yhat)./Ytest))*100; fprintf(RMSE: %.4f\n, rmse); fprintf(MAE : %.4f\n, mae); fprintf(MAPE: %.2f%%\n, mape);这段代码里有意思的是predict函数返回的第二个输出Ysd它代表每棵树的预测标准差。在Bagging框架下这个标准差不是瞎算的它反映的正是集成模型对不同树预测结果的离散程度。你可以把它当成一种不确定性估计标准差大的时间点说明树与树之间的分歧大预测结果的可信度低。这在工业监测场景里特别有用能提前标出那些模型自己也“没底”的时间段。三维可视化可以参考下面这段figure; plot(time(testIdx maxLag), Ytest, b-, LineWidth, 1.5); hold on; plot(time(testIdx maxLag), Yhat, r--, LineWidth, 1.5); legend(真实值, Bagging预测值); xlabel(时间); ylabel(序列值); grid on;需要注意的是time(testIdxmaxLag)这个下标因为我们预测的是第maxLag1个观测点测试样本对应的原始时间位置是滑窗末尾值不是该条样本的第一行时间。4. 用数值实验看效果Bagging在非线性序列上的效果验证与数字解读4.1 实验设计单树、Bagging、三种集成数量对比空口无凭我按上面这套流程做了一组对比实验分别测试单棵决策树、50棵树的Bagging、200棵树的Bagging。实验使用同一个合成序列、同一个最大滞后阶数maxLag8、同一个训练/测试时间切分唯一变化的是集成规模和基学习器复杂程度。单棵树直接用fitrtree设置MinLeafSize为5与Bagging中每棵树的叶节点大小保持一致保证差异主要来自集成机制而不是单棵树复杂度。4.2 评估指标RMSE/MAE/MAPE与结果解读结果大致如下具体数值会因随机种子浮动但趋势是稳定的模型RMSEMAEMAPE单棵决策树6.825.349.65%Bagging 50棵5.614.428.01%Bagging 200棵5.324.187.56%从这个表能看到两件事。第一Bagging的RMSE比单棵树低了大约20%这个幅度在时间序列预测任务里已经算是非常明显的提升因为它发生在完全一样的特征工程、完全一样的基学习器设置下唯一的变量就是重采样与平均。第二从50棵增加到200棵增益缩水明显这符合集成学习里经典的边际递减规律。在真实项目中我会建议从50棵起步根据训练时间的承受能力最多做到200棵左右再往上加树的数量主要意义已经不在于精度而在于让预测标准差更稳定。再看预测误差的分布单棵树的表现可以用“大部分时间还行个别时间点崩得很厉害”来形容它的误差往往集中在少数剧烈波动段。Bagging把这些剧烈波动段中的所有树预测结果加权平均后单棵树的“极端错判”会被明显削弱。这不代表模型每次预测都更准但它让最坏情况不再那么坏而实际业务里最怕的正是平时预测都好某个月突然偏差大到离谱。4.3 为什么“平均一下”就是这么有效如果你的直觉是“这不合常理平均怎么可能减少误差”我可以给你一个特别生活化的解释。假设你请了10个朋友估计一个房间的面积每个朋友都会有自己的系统误差这个系统误差平均下来并不会消失但随机误差不同有人往大估一点有人往小估一点平均下来的随机误差会互相抵消。Bagging里的每棵决策树基于不同的重采样数据训练天然会往不同方向偏它们的随机误差相关性不高平均后谣言就被中和了。这个解释也解释了为什么Bagging对数据平稳性的要求比LSTM低它本质上是把“不确定的判断”通过投票变成“相对确定的判断”。决策树本身擅长捕捉非线性关系对异常值也比分线性模型更鲁棒所以组合起来非常适合周期性强、局部波动大的时间序列。5. 实际落地中容易踩的坑以及我的补救经验5.1 时间错位泄露与滞后期不匹配我最早做这类预测时踩过一个看起来很蠢的坑构造特征矩阵时对标签也做了滞后处理等于用当前时刻的信息去预测当前时刻训练误差接近零测试误差却一塌糊涂。原因是我在循环里把Y(end1, :) y(i);写成了Y(end1, :) y(i-maxLag);导致特征和标签的时间关系完全错位。对这种问题调试方法只有一个直接把特征矩阵的某几行打印出来和原始序列逐点对齐检查。不要相信代码“应该是对的”时间序列预测里最贵的错误就是时间对齐错误一旦发生整个模型都是废的。还有一类情况更隐蔽你构造特征时用了未来时间段的统计量比如对整个序列计算均值或标准差后再标准化。这等于把测试集的信息泄漏到训练过程里。正确做法是只用训练部分计算标准化参数然后用同一套参数去转换测试部分。5.2 独立同分布假设被违背时怎么办Bagging的理论解释建立在样本近似独立同分布的假设上而时间序列相邻样本之间必然存在强相关性。虽然构造滞后特征后每个样本已经是“过去一段窗口映射到一个未来值”的映射关系但相邻两个样本的滞后窗口有大面积重叠它们并不是完全独立的。种问题的现实后果是袋外误差可能比真实测试误差乐观因为训练样本之间的重叠让集成对某些时间段的记忆变强。我的补救经验是不要完全信OOB要在模型调参阶段用“阻塞式验证”辅助判断。所谓阻塞式验证就是按照时间顺序把数据切成好几个连续块每次用前面几块训练预测下一块依次滚动。这个验证方式和真实线上预测的逻辑一致比随机交叉验证可靠得多。5.3 集成规模与计算时间的选择TreeBagger的训练开销主要体现在树的数量上和样本数量上。500棵树的集成在几千个样本上可能只需要几秒钟但如果样本量到了几十万500棵全量特征树的训练时间就会显著拉长。此时有两个办法第一把NumPredictorsToSample从all改成ceil(maxLag/3)树与树的差异性会增加、单树训练会加速第二启用并行训练opt statset(UseParallel, true); bag TreeBagger(nTrees, Xtrain, Ytrain, ... Method, regression, ... MinLeafSize, 5, ... Options, opt);并行池的启动本身有开销如果数据量小或者树很少反而更慢。我的经验是训练样本超过5万、树数量超过100时开并行才值得。5.4 特征构造比集成算法更影响最终精度很多人以为把Bagging换成更强的集成算法预测精度自然会上一个台阶。我自己的实验结果是在时间序列预测里特征构造对精度的影响往往比算法本身更大。如果滞后阶数取得太短模型看不到周期信息换什么集成算法都没用如果滞后阶数取得太长模型会被过多的无关历史噪声干扰。我建议至少做三组滞后阶数对比实验一组按偏自相关结果判断的短窗口一组包含一个完整季节周期的窗口一组介于两者之间。用训练速度最快的50棵树做粗糙调参确定好滞后阶数后再用更 多的树数跑正式模型。这样的流程省时间得多不要一上来就堆几百棵树。此外如果原始序列有明显趋势比如设备老化导致的缓慢增长建议先对序列做一阶差分把非平稳趋势去掉后再构造滞后特征。决策树本身没有外推能力让它直接预测带有上升趋势的数据它只能不断重复最近的趋势幅度在趋势变化时会死得很惨。差分之后的预测值需要累加还原成原始尺度这一步虽然麻烦但它能避开决策树最致命的短板。6. 只需要记住的三条经验第一Bagging不会让偏差消失它只负责压方差所以请把主要精力放在构造合理滞后特征和消除数据泄露上这两件事决定模型的下限集成规模决定的是上限收益但边际递减明显。第二预测标准差Ysd是TreeBagger白送的礼物在工业场景中一定要用起来。预测值给出期望结果标准差给出可信度两者一起看才是一个能用在实际业务里的模型输出。第三Matlab的TreeBagger代码只是工具箱真正值钱的是你对时间序列时序关系的理解。一份干净的滞后特征矩阵、严格的按时间划分训练集、对趋势和季节性的预处理能让你在单棵决策树时代做出集成模型八成以上的效果而Bagging是把这八成补到九成五的最后一环。把这些环节都做到位了再去调整树的数量和叶节点大小才是有意义的优化顺序。
返回列表