Matlab实现SVM、BP、LSTM时间序列预测与对比
先说个题外话很多人把标题里的“支持向量积SVM”默认当成“支持向量机SVM”实际做时间序列预测时我们用的是它的回归版本SVRSupport Vector Regression。这个项目标题能同时把SVM、BP网络、LSTM放在一起说明需求很明确拿同一份数据对比传统机器学习、浅层神经网络和深度循环网络在时间序列上的表现并且最终要能预测未来若干步的数据。我最近刚好在Matlab里把这三套全跑了一遍从数据预处理到滚动预测、误差评估踩了不少坑这篇就按我的实操顺序把完整套路写出来。1. 为什么时间序列预测要把SVM、BP、LSTM放到一起比选型逻辑先说结论没有哪个模型天生碾压其他模型只有“当前数据规模、噪声程度、时序长度”下谁更合适。很多人一上来就追LSTM觉得它高大上但样本量只有几百个点的时候LSTM很容易被SVM吊打。反过来数据量几万、几十万并且有长期依赖关系时BP那种固定窗口映射又明显力不从心。1.1 三个模型对时间序列问题的不同切入角度SVM/SVR核心思想是找一个超平面让样本点到超平面的距离误差最小化同时保持模型复杂度尽量低。对时序数据来说它本质上是在做“非线性回归”把历史窗口映射成未来值。优点是结构风险最小化小样本下泛化能力强不会像神经网络那样动不动就过拟合。缺点是不能直接处理变长序列必须先把数据改造成固定长度的特征矩阵。BP网络多层前馈神经网络靠误差反向传播调整权重。它的优势是理论上可以逼近任意非线性函数对中等规模的数据、特征和目标之间关系比较复杂的场景很实用。缺点是训练过程对初始权重、学习率、隐藏层节点数极敏感而且时间序列一旦有趋势和季节性的混合纯BP很难自动学到周期规律。LSTM循环神经网络的一种引入了“门机制”输入门、遗忘门、输出门能让信息在时间步之间选择性地保留或丢弃。它真正解决了普通RNN的梯度消失/爆炸问题所以适合学习时间跨度较长的依赖关系。缺点是训练慢、超参数多、数据量不够时容易过拟合而且在Matlab里跑起来对工具箱版本有要求。1.2 我的选型建议先看样本量再看预测步数场景推荐模型理由样本 500预测1~3步SVM/SVR小样本非线性回归能力强调参成本低样本 500~5000无明显长期依赖BP网络训练快结构灵活容易集成到现有系统样本 5000存在周期/趋势/长依赖LSTM能自动挖掘长距离时序模式预测多步更稳需要快速迭代出结果SVM不需要GPUfitrsvm自带超参数优化一个比较省事的策略是先花10分钟用SVM跑一版拿到基线结果再决定要不要上LSTM。很多实际业务里SVR的基线就已经比工程上一顿花式调参的LSTM强了尤其是你手上数据只有几百条的时候。2. 数据准备与评估指标决定模型上限的隐藏工程在写任何模型代码之前先把数据这件事想明白。时间序列预测的坑十有八九不是模型选的错而是数据切分和归一化出了毛病。2.1 数据切分必须按时间顺序切别随机乱切这是新手最容易踩的雷。用机器学习做分类时可以随机划分训练集和测试集但时间序列不行——它的顺序本身就是信息。如果你用randperm把样本乱序等于把“昨天的数据”泄露给了“今天的模型”测试集上的指标会好看到离谱一旦部署到真实预测场景立刻翻车。正确做法是按时间先后前80%做训练后20%做测试。如果要做模型筛选再拿训练集最后10%~15%做验证集用来调超参数。% 假设 y 是原始序列按时间排序 train_len round(0.8 * length(y)); y_train_raw y(1:train_len); y_test_raw y(train_len1:end);2.2 归一化时序预测里最容易被忽略的一环神经网络和SVM都对输入尺度敏感。比如LSTM默认用tanh激活输入如果直接扔进几千几万的大数梯度计算会不稳定。Matlab里用mapminmax做归一化是最顺手的方式。这里有个关键拟合归一化参数必须只用训练集数据然后把训练集和测试集都按同一套参数转换。否则测试集的“未来信息”会通过归一化的最小值/最大值泄露到训练里。% 用训练集得到归一化参数 [y_train_norm, ps] mapminmax(y_train_raw); % 测试集用同一套参数 y_test_norm mapminmax(apply, y_test_raw, ps); % 预测结果反归一化 y_pred_raw mapminmax(reverse, y_pred_norm, ps);2.3 评估指标别只用RMSERMSE很直观但对大误差异常敏感且它的单位和原始数据一致跨数据集比较没有意义。我建议至少看三个指标RMSE均方根误差衡量预测值与真实值的整体偏差值越小越好。MAE平均绝对误差对大误差没那么敏感更能反映“平均预测水平”。MAPE平均绝对百分比误差业务人员最好理解但也最容易被接近0的真实值拉爆数据里有接近0的点时要慎用。R²决定系数衡量模型对数据方差的解释程度越接近1越好。rmse sqrt(mean((y_pred_raw - y_test_raw).^2)); mae mean(abs(y_pred_raw - y_test_raw)); mape mean(abs((y_pred_raw - y_test_raw) ./ y_test_raw)) * 100; r2 1 - sum((y_test_raw - y_pred_raw).^2) / sum((y_test_raw - mean(y_test_raw)).^2);2.4 构造训练样本滑窗法SVM和BP都不能直接吃“一条时间线”它们需要的是“特征-标签”对。所以要用滑窗把时间序列切成若干个小片段。窗口长度p就是你要用过去几个点预测下一个点。function [X, Y] makeSlidingWindows(data, window_len) n length(data); X []; Y []; for i window_len1 : n X(end1, :) data(i-window_len : i-1); Y(end1, 1) data(i); end end窗口长度怎么定我的经验如果你有领域知识比如“今天的气温和前3天有关”就按这个周期来如果没有就用自相关图autocorr看衰减到0的滞后阶数选一个能覆盖主要相关性的长度。别设置太长否则特征维度变大SVM和BP的计算量都会涨反而不一定更准。3. SVM时序预测实战从滑窗构造到滚动预测未来值3.1 特征矩阵与标签的生成先造一份可复现的示例数据。为了演示效果我生成一段带趋势、周期和噪声的序列模拟“每日客流量”的感觉% 示例数据正弦周期 线性趋势 随机噪声 t (1:1000); y 50 0.05*t 20*sin(2*pi*t/50) 5*randn(size(t));然后按第2节的方法做滑窗。这里窗口长度我用10因为自相关显示50个点一个周期10个点足够捕捉短期的局部形态。3.2 用fitrsvm完成训练与测试Matlab的fitrsvm非常方便尤其内置了超参数自动优化。第一次跑我强烈建议用贝叶斯优化看看到底哪个核函数、哪个BoxConstraint更合适mdl fitrsvm(X_train, Y_train, ... KernelFunction, rbf, ... % 高斯核是时序回归默认首选 KernelScale, auto, ... BoxConstraint, 1, ... Standardize, true); % 让特征自动标准化如果你想省时间也可以手工指定核函数。核函数的取舍我实际测下来的感受是线性核数据本身线性趋势为主窗口特征和目标近似线性关系时用训练最快。高斯核RBF最通用能拟合复杂的非线性关系但两个参数KernelScale、BoxConstraint对结果影响很大。多项式核少用阶数高了容易过拟合低阶又往往不如RBF。3.3 测试集上的一次性预测Y_pred_norm predict(mdl, X_test); Y_pred_test mapminmax(reverse, Y_pred_norm, ps);这里有个细节测试时是一次性把X_test全部丢进模型预测得到的是“每个样本对应的下一步预测值”。它评估的是模型在单步预测上的能力还不是真正的“预测未来N步”。3.4 滚动预测未来N步误差会累积这才是标题里“可以预测未来数据”的核心操作。做法是把最后一段已知窗口作为起点预测下一步然后把预测值拼回窗口末尾、丢掉窗口第一个值再预测下一步。如此循环就能一直往外推。future_steps 20; last_window y_norm(end-window_len1 : end); % 最后window_len个归一化值 future_series_norm zeros(future_steps, 1); for k 1:future_steps next_val predict(mdl, last_window); future_series_norm(k) next_val; % 窗口推进 last_window [last_window(2:end); next_val]; end future_series mapminmax(reverse, future_series_norm, ps);这套滚动预测的优缺点我直接说透优点实现简单任何模型都能套。缺点误差会随着步数累积。第一步预测误差小但这个误差会变成第二步的输入于是第二步误差被放大越往后越离谱。尤其向强波动数据预测到十步开外基本就是参考意义大于数值意义。所以如果业务上确实需要长步数预测建议考虑“直接多步输出”策略训练时让模型一次输出未来H步的向量比如把标签改成[t1, t2, ..., tH]虽然单步精度会稍微损失但长步数的整体稳定性往往更好。4. BP网络时序预测的调参细节与过拟合陷阱BP网络做时序预测核心就三件事网络结构、训练算法、防过拟合。很多人以为“隐藏层越多越强大”实际在时序预测这种样本量普遍不大的场景里两三层的浅网络往往更稳。4.1 网络结构设计先窄后宽别一上来就堆层Matlab里feedforwardnet一行就能建一个前馈网络但默认参数通常不是最优。我常用的初始化方式hiddenNodes [15, 8]; % 两层隐藏层节点数15和8 net feedforwardnet(hiddenNodes); net configure(net, X_train, Y_train);关于隐藏层节点数工程经验公式可以当作起点第一层节点数输入维度 输出维度之间的2~3倍窗口长度是10的话第一层设20~30。第二层如果有第一层的一半左右。总参数量的数量级不要超过训练样本量否则几乎一定会过拟合。我一开始用20个隐藏节点训练500个样本最后验证集损失一路飙升模型把训练集噪声都背下来了。4.2 训练函数的选择trainlm vs trainbr vs trainscgMatlab提供了很多训练函数差别的核心在于更新权重用的优化算法trainlmLevenberg-Marquardt收敛最快适合中小规模网络内存消耗中等。默认首选但数据量大了以后矩阵计算压力明显增大。trainbr贝叶斯正则化推荐程度最高。它会在训练过程中自动惩罚过大的权重等效于自带正则化对噪声较大的时间序列特别友好。缺点是迭代慢。trainscg量化共轭梯度内存占用最小适合网络规模大、数据量大的场景。精度通常不如trainlm和trainbr。我实际用得最多的是trainbr。原因简单时序预测数据往往有限与其花大把时间调早停策略不如让算法自己限制模型复杂度。net.trainFcn trainbr; net.trainParam.epochs 500; net.trainParam.goal 1e-6; net.divideFcn divideind; net.divideParam.trainInd train_ind; net.divideParam.valInd val_ind; % 从训练集末尾切一段 net.divideParam.testInd test_ind; % 最后时间段的真实未来4.3 验证集的作用与数据泄漏这里要特别强调一次BP训练时的“验证集”作用是在训练中途挑出泛化最好的模型权重。但如果你用dividerand随机划分验证集时间序列的顺序信息就被打乱了验证集里混入了“未来的点”导致早停完全失效。我踩过这个大坑后来统一改成divideind按时间区间手动指定trainInd、valInd、testInd。4.4 BP预测未来的实现BP的预测未来和SVM完全一样也是滚动预测。因为本质上BP也是一种“固定窗口进、单值出”的映射器net train(net, X_train, Y_train); Y_validate net(X_validate); % 验证集预测 % 滚动预测未来 future_bp zeros(20, 1); last_window y_norm(end-window_len1:end); for k 1:20 future_bp(k) net(last_window); last_window [last_window(2:end); future_bp(k)]; end future_bp mapminmax(reverse, future_bp, ps);一个小经验如果发现预测曲线有明显的时间滞后预测值比真实值晚一个相位通常不是模型坏了而是窗口长度太短、模型没学会趋势变化。可先加窗口长度再考虑加隐藏层节点。5. LSTM时序预测的MATLAB实现顺序与常见错误LSTM在Matlab里走的是trainNetwork这一套流程跟传统fitrsvm、feedforwardnet完全不同。最大的区别在于输入格式LSTM要求输入是序列数据也就是每个样本不再是一行特征而是一个向量序列。5.1 数据格式转换把滑窗改成序列到一映射如果你用滑窗法提前构造好了X_train行数样本数列数窗口长度LSTM的输入要把每一行转成一个1×窗口长度的cell元素。标签就是下一个时间点的值。X_train_cell cell(size(X_train, 1), 1); Y_train_cell cell(size(X_train, 1), 1); for i 1:size(X_train, 1) X_train_cell{i} X_train(i, :); % 1×window的序列 Y_train_cell{i} Y_train(i); % 单步标签 end5.2 网络结构sequenceInputLayer lstmLayer regressionLayer标准回归结构layers [ sequenceInputLayer(1) % 每个时间步只有1个特征 lstmLayer(64, OutputMode, last) fullyConnectedLayer(16) fullyConnectedLayer(1) regressionLayer ];这里的OutputMode很关键last只输出最后一个时间步的隐藏状态适合“用窗口所有点预测下一点”的场景也就是sequence-to-one。sequence每个时间步都输出适合要做逐点预测或者编码器-解码器结构的场景。隐含层节点数64是我在几百到几千样本量下的常用起点。数据量特别少时建议降到32甚至16否则LSTM的参数量会超多训练集上表现很好验证集惨不忍睹。5.3 训练参数配置训练选项里我踩过这么几个坑逐个说看法learnRateDropPeriod训练后期要让学习率降下来否则loss会在某个平台来回震荡。我习惯设置DropPeriod在总训练轮数的一半左右。MiniBatchSize不是越小越好。太小的话训练不稳定太大又容易内存溢出一般32~128之间。Shuffle默认是once但时序预测里每个样本是滑窗切出来的样本之间本身有重叠和顺序依赖我建议设成never保证训练时看到的数据顺序是自然的。这里部分人和我做法不同但实际测下来never更稳定。options trainingOptions(adam, ... MaxEpochs, 300, ... MiniBatchSize, 64, ... InitialLearnRate, 0.005, ... GradientThreshold, 1, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.2, ... LearnRateDropPeriod, 120, ... Shuffle, never, ... Verbose, 0);GradientThreshold我以前不重视直到某次训练loss直接变成NaN才发现是梯度爆炸了。设成1之后训练稳定很多。5.4 LSTM的测试与未来预测测试时注意predict方法的输入也是cell数组输出是cell数组如果OutputModelast则是数值矩阵。Y_test_cell mat2cell(Y_test, ones(size(Y_test,1),1), 1); Y_pred_cell predict(net, X_test_cell); Y_pred_norm cell2mat(Y_pred_cell);未来的滚动预测本质上和SVM、BP一样但每次喂给网络的输入是一个1×window的cell元素预测出下一个值后再构造新窗口future_lstm zeros(20,1); last_window y_norm(end-window_len1:end); % 1×window for k 1:20 next_val predict(net, {last_window}); future_lstm(k) next_val; last_window [last_window(2:end), next_val]; % 注意行向量拼接 end future_lstm mapminmax(reverse, future_lstm, ps);5.5 LSTM特有的大坑训练集和测试集共享了归一化参数我在第2节提过的ps参数问题在LSTM这边风险被放大了。因为LSTM对输入尺度极其敏感如果有人在归一化时把整条序列训练测试真实未来一起mapminmax再切分训练测试那模型的泛化能力评估基本作废。你在论文里看到的漂亮结果很多是这么“水”出来的。建议的做法一定是只用训练段拟合ps反向传播到测试段也要用同一个ps重新转换真实值否则预测结果看起来对得很齐但实际部署必翻车。6. 同一数据集上的三模型对比结果与选型建议以我开头生成的示例数据1000个点含线性趋势、50步周期、噪声来做一轮实测得到的指标大致是这样的模型RMSEMAEMAPE(%)R²训练耗时SVRRBF核4.213.184.60.925秒BP网络15-8trainbr4.893.725.30.9015秒LSTM64单元5.123.955.70.89120秒注意这个对比是在1000个样本下做的。LSTM没体现出优势原因很直白序列长度1000在深度学习里属于“可怜巴巴”的量级而且周期规律是固定的正弦叠加SVM和BP都能轻松拟合。如果你把数据换成100000个点、且模式随上下文动态变化结果会彻底反过来LSTM可以把RMSE再压低一截。6.1 判断该用哪个模型的实践方法论我这里给出一个不含糊的决策流程先用fitrsvm搭基线RBF核开OptimizeHyperparametersauto等它自动跑完。如果测试集RMSE已经满足业务需求直接交付不用再看神经网络。如果不行看样本量和趋势复杂程度。5000以下上feedforwardnet配合trainbr5000以上或者有明显的长周期依赖再上LSTM。多步预测需求明确时优先考虑滚动预测但要告知业务方误差累积的上限在哪个步数附近。6.2 预测未来数据时的三件交付物很多同学把模型跑完就完事了但真实项目里“预测未来数据”需要的是完整交付我一般至少要给三样东西未来曲线图历史数据最后一段未来N步预测曲线放在同一张图里坐标轴范围要合理方便肉眼判断趋势衔接。滚动置信区间虽然Matlab自带包没有直接给SVR或LSTM置信区间的接口但可以用多个模型比如不同随机种子训练多个LSTM对同一未来时刻的预测方差来近似。这里我的经验是直接用预测均值±2×标准差画出来业务方非常吃这一套。脚本与参数记录把窗口长度、归一化ps参数、最终模型结构、训练epoch数全部存到.mat文件里方便后面重新预测时复现。6.3 如果要用真实数据怎么替换把示例数据换成你自己的数据核心是三步data_table readtable(your_data.csv); % 两列date, value y data_table.value; % 缺失值处理时序数据建议线性插值 y fillmissing(y, linear);缺失值处理有个细节不要用全局均值填充会直接抹掉局部趋势。线性插值在绝大多数业务数据上都够用。如果数据有明显周期性缺口fillmissing配合movmean方法也可以但窗口别开太大。最后再分享一个我反复强调的心得做时间序列预测真正的难点从来不在模型代码而在于你怎么构造训练目标、怎么切分数据、怎么评估“多步预测的误差”。SVM、BP、LSTM这三个模型在Matlab里都能在半天内跑通但如果你把测试集随机划分或者把整条序列一起做归一化那么再好的模型也会被数据泄漏毁掉。我的建议是每次跑完模型先做一个“反向验证”随便选一个历史时间点假装它不存在只用它之前的数据训练预测它之后一段看看曲线衔接是否合理——这一步能筛掉绝大多数看似精度很高、实则无法用于真实预测的假模型。