
如果你手上积压着一批时间序列数据老板让你做预测你第一反应可能是LSTM。但等你把数据整理好、环境配好、训练一轮之后发现LSTM不是不可以而是对大部头项目来说有点“杀鸡用牛刀”。我前两年接到过不少这类需求试过一轮之后反而最常用的是NARX——带外部输入的非线性自回归网络。这玩意儿在MATLAB里的实现代码量不大模型结构也透明2018B及以上版本跑起来很顺特别适合做中短期预测。这篇内容不是教科书式的原理复述是我把NARX从理论到代码、从开环训练到闭环预测一路踩坑之后整理出来的一套实操方案。后面会给你能直接跑的MATLAB代码、参数怎么敲、为什么这样敲以及我实测中遇到的那些“训练误差漂亮、一预测就翻车”的真实问题。如果你正准备用MATLAB做时间序列预测尤其是手里只有一两列外部变量、又没有精力上深度模型这篇文章应该能帮你省下至少两天的摸索时间。1. 选NARX而不是LSTM先搞懂它到底在做什么作为一个在多个预测项目里反复横跳的人我的第一个建议是不要因为“热门”去选模型。先看清楚NARX到底在做什么再判断它适不适合你的数据。1.1 NARX的数学结构一句顶一万句NARX的全称是Nonlinear Autoregressive with Exogenous Inputs翻译过来就是“带外部输入的非线性自回归”。它的核心公式并不复杂y(t) f( y(t-1), y(t-2), …, y(t-d), x(t-1), x(t-2), …, x(t-d) )一句话理解当前时刻的结果由过去d步的历史结果、以及过去d步的外部输入共同决定。这里的x就是外生变量也就是“外部输入”。比如你要预测电力负荷历史上的负荷值是y当天的温度、湿度、节假日标记都可以作为x你要预测河水位历史水位是y降雨量就是x。这个结构和LSTM相比有本质区别吗没有。LSTM内部是通过门控机制决定“记住多少历史”NARX则是直接给网络开了d个时间窗——把最近d步的y和x作为输入特征喂给一个普通的前馈网络。好处是什么训练速度快、参数少、不需要像LSTM那样调sequence长度、batch大小等一系列超参数。坏处也很明显窗口宽度d是固定的超过d步的长期依赖NARX天然抓不到。所以NARX最适合的场景是被预测变量和近端历史关联较强外部输入变量可解释性明确预测步数在几步到几十步这个量级。如果你要预测股票一年后的价格那别指望NARX。1.2 开环与闭环训练和预测是两回事这是我在项目里见过最多人搞混的地方。NARX网络在MATLAB里有两种形态开环Open Loop和闭环Close Loop。开环模式下训练时网络输入里用的是真实的历史y值。也就是说t时刻预测的时候y(t-1)、y(t-2)这些反馈输入来自数据集里已经发生过的真实值。这样每个时间步都“知道”标准答案训练信号清晰误差小、收敛快。闭环模式下网络输入里的反馈值用的是网络自己的预测结果而不是真实值。训练完放到实际使用场景中因为未来没有真实值可用刚才预测出来的y(t1)会作为y(t2)的输入一步错、步步错。这就是为什么很多新手发现训练集上误差千分之一一到预测阶段曲线就开始发散飘走。在MATLAB里的常规操作是先开环训练一个表现很好的网络然后调用closeloop把它转换成闭环模式用于实际预测。很多人训练完直接拿net去预测得到的结果当然是错的——因为net还是开环状态它根本不知道要用自己的输出做反馈。1.3 和LSTM、ARIMA对比什么时候NARX更划算我做过几次这种并排对比可以直接说说感受。ARIMA适合纯线性、没有明显外部因素影响的单变量序列。它的优势是数学可解释性强但对外部变量无能为力。你要是想加入温度、事件标记这类因素ARIMA得扩展成ARIMAX参数估计也麻烦。LSTM神经网络建模能力强能抓长程依赖但训练数据要求多、调参维度多、收敛慢。小样本数据集上很容易过拟合跑一次实验的时间成本也高。NARX正好卡在中间能吃外部输入能表达非线性关系模型结构相对简单训练快。样本量数百到几千条都能训不像LSTM动辄需要上万条数据撑场子。我做过的对比项目里中等规模数据下NARX的预测精度并不输给LSTM而且训练时间只有LSTM的十分之一。当然这不是说NARX全面优于LSTM而是说“先想清楚再选模型”很关键。数据量小、特征关系清楚的场景没必要一上来就搞深度学习。2. 数据准备与参数敲定预测失败的根源往往在训练之前很多人拿着代码就急于train结果训练误差很好看测试集上一塌糊涂。我自己的经验是模型的成败在训练之前就已经定了六成。NARX虽然有外生变量加持但它并不是魔法数据没伺候好网络结构再精巧也是空转。2.1 数据预处理归一化不是可选项NARX网络里的激活函数通常以S型或双曲正切函数为主输入值范围如果远超[-1,1]网络在饱和区几乎是“什么都不学”。所以我每次做预测之前第一步一定是归一化把目标序列和外生变量全部缩放到[-1,1]区间。MATLAB里可以直接用mapminmax[loadNorm, loadPS] mapminmax(loadData); [tempNorm, tempPS] mapminmax(tempData);这里面的坑在于训练、验证、测试的归一化参数必须只从训练集上计算然后用同一套参数去缩放验证集和测试集。否则测试集的信息会偷跑进训练过程导致评估指标虚高。我见过不少论文里没说清这一点结果换一段新数据表现就崩。实操上我会把全序列按时间顺序切成三段前70%训练、中间15%做验证控制训练停止、最后15%测试。切完之后只拿训练段计算mapminmax的参数再对三段分别执行归一化。2.2 反馈延迟d和隐含层神经元怎么选NARX网络有四个核心参数输入延迟input delay、反馈延迟feedback delay、隐含层神经元数、训练算法。在MATLAB里输入延迟和反馈延迟通常设成一样也就是公式里的d。我用过的经验区间是这样的参数常用范围说明延迟d1到5太小抓不住短期相关性太大会引入噪声并增加参数数量隐含层神经元5到20神经元越多拟合能力越强但过拟合风险也随之上升训练算法trainlm / trainbr / trainscg小样本优先trainbr中大规模用trainlm训练轮数300到1000配合验证集早停防止过度训练刚开始做的时候我建议d从1到时延逐个试比如d1、2、3、4各训练一次比较测试集上的RMSE。不要只看训练集误差开环训练误差低不代表闭环预测好这一点我会在后面展开。隐含层神经元数量我通常从10开始。如果训练集误差下去了但测试集误差很大就是过拟合把神经元数量降下来或者改用trainbr这种带贝叶斯正则化的训练算法。神经元太少则会出现训练集误差本身下不去的“欠拟合”这时再往上加。2.3 训练集/验证集/测试集的划分坑时间序列数据和普通表格数据最大的不同是不能随机打乱。打乱顺序等于把未来的信息泄漏到了过去模型学到的“规律”在真实场景中不存在。NARX训练时用到的preparets函数会按时间对齐输入和反馈数据一旦乱序整个延迟结构都失去意义。正确的做法是顺序切分。还有一个很多人容易忽视的点不要用MATLAB神经网络工具箱里默认的divideind因为它默认会把数据随机打散。要显式指定按索引划分一般这么写net.divideFcn divideind; net.divideParam.trainInd trainInd; net.divideParam.valInd valInd; net.divideParam.testInd testInd;trainInd、valInd、testInd是三个索引向量分别代表训练、验证、测试区间在原始序列中的位置。这样能保证验证集永远在时间上都晚于训练集、测试集又最晚符合真实预测的方向。3. MATLAB完整实现从数据加载到预测结果接下来是最核心的部分。我直接给一套完整、可运行的MATLAB代码。场景就用一个最典型的需求根据历史负荷值和当日温度预测未来时段的负荷值。你可以把数据换成任何业务场景只要保证X是外部输入、T是预测目标即可。3.1 网络创建与数据转换先搞清preparets的输入格式很多新手卡在这一步。MATLAB神经网络工具箱里的时间序列函数要求输入输出都是行向量的cell数组而不是普通矩阵。比如原始数据是500个点每个点是一条一日数据那么X应该是1x500的cell每个cell元素是一个m维列向量m是外生变量个数T是1x500的cell每个元素是一个标量列向量值为该时刻的目标值。转换代码% 假设 loadData 是 N x 1 的负荷列向量 % 假设 tempData 是 N x 1 的温度列向量 % 归一化 [loadNorm, loadPS] mapminmax(loadData); [tempNorm, tempPS] mapminmax(tempData); % 转成 cell 结构这是 preparets 需要的格式 T num2cell(loadNorm); % 目标序列1 x N 的 cell每列一个点 X num2cell(tempNorm); % 外生输入1 x N 的 cell如果有多个外生变量把每个变量作为一行拼成矩阵再用num2cell转。创建网络我推荐直接用narxnetinputDelays 1:2; % 外部输入的延迟窗口 feedbackDelays 1:2; % 目标反馈的延迟窗口 hiddenSizes 10; % 隐含层神经元数 net narxnet(inputDelays, feedbackDelays, hiddenSizes);这里1:2表示取过去2步对应公式里的d2。用view(net)可以打开网络结构图先确认结构和预期一致再继续。如果你做的是纯自回归、没有任何外部输入那就用narnet(feedbackDelays, hiddenSizes)数据准备时只给T就行。3.2 preparets和开环训练标准操作流程preparets这个函数的作用是把我们手头的时间序列转换成网络训练时真正需要的输入矩阵、初始状态和输出矩阵。它要求传入三个参数网络、外生输入序列、目标序列其中外生输入序列的格式是多个序列并列的cell数组没有外生输入时用空cell数组{}占位[Xs, Xi, Ai, Ts] preparets(net, X, {}, T);这里X是外部输入Ts是目标输出。Xs是用于网络输入的数据Xi和Ai是延迟产生的初始状态。preparets会按照网络结构自动把前d个时间点切到初始状态里保证训练从第d1步开始这一点非常关键——你不需要自己手动截取序列。接下来设置训练参数并训练net.trainFcn trainlm; % 列文伯格-马夸尔特算法 net.trainParam.epochs 500; net.trainParam.min_grad 1e-6; net.divideFcn divideind; net.divideParam.trainInd trainInd; net.divideParam.valInd valInd; net.divideParam.testInd testInd; % 训练开环网络 [net, tr] train(net, Xs, Ts, Xi, Ai);训练完成之后先看tr.best_epoch和验证集误差曲线。如果验证误差在某个epoch之后开始上升说明后面都是在过拟合训练集这里就该用早停的权重。判断标准很简单训练曲线和验证曲线一开始都下降然后验证曲线掉头向上那个拐点就是最佳训练轮数。3.3 闭环预测与反归一化真正要交付的预测值训练结束之后千万不要直接把net拿来预测。前面说过训练用的是开环实际预测要用闭环。把训练好的网络转成闭环模式netc closeloop(net); view(netc); % 确认反馈输入已经换成了网络自身输出然后用preparets准备闭环预测的数据。这里的输入还是整套历史数据但网络内部会在前几个时间点用真实值作为初始状态之后切换到自己的预测值作为反馈。代码是[Xs, Xi, Ai, Ts] preparets(netc, X, {}, T); Y netc(Xs, Xi, Ai);需要注意的是闭环预测的输出Y仍然是在[-1,1]区间归一化过的值必须反归一化才能变成真实量纲的预测值Y_raw mapminmax(reverse, Y, loadPS);到这里你已经得到了一份完整的闭环预测序列。如果你要预测的是未来step步也就是T序列结束之后的点操作会再复杂一点需要用最后几个真实历史值初始化网络状态然后把外部输入的未来值如果测得或估得逐一喂进去。这个我们放到后面进阶部分单独说。3.4 效果评价RMSE、MAPE、R²怎么算预测出来不算完你总得给业务方一个“这模型准不准”的交代。我常用的指标有三个。% Y是闭环预测值T_true是测试集真实值反归一化后 err Y - T_true; rmse sqrt(mean(err.^2)); mape mean(abs(err ./ T_true)) * 100; ss_res sum(err.^2); ss_tot sum((T_true - mean(T_true)).^2); r2 1 - ss_res / ss_tot;RMSE的优点是量纲直观和原始数据同一单位MAPE以百分比表示相对误差适合跨数据集对比R²表示模型解释了多少方差越接近1越好。这三个我每次必算方便跟其他模型横向比。另外预测类任务最好画两张图训练段的拟合图和测试段的开闭环对比图。测段对比图是最直观的说服材料一张图上同时画真实值、开环预测、闭环预测老板一眼能看出问题。4. 2018B及以上版本踩坑实录从闭门造车到顺利出图这套代码我在2018B到2023B的好几个版本上都跑过整体兼容性不错但有一些坑是真真切切踩过以后才知道的。拿出来逐条讲能帮你少走我这几个弯路。4.1 narxnet和narnet的版本陷阱MATLAB的帮助文档在不同版本里推荐的网络创建方式一直在变。早期版本大量使用newff、newnarx这些接口2018B之后的文档主推narxnet和narnet同时告诉你用preparets来准备数据。但网上老帖子特别多代码里经常混着newff甚至20年前的古早写法在2018B以上的版本会报错或者行为完全不同。我建议一个判断标准配置文件里只要出现preparets基本就是新接口如果还用sim配{1:N}这种手写延迟结构尽量尽早改造。另外narxnet返回的是开环网络narnet返回的是没有外部输入的自回归网络。你要是直接在项目里复制别人的narnet代码、然后又试图把外部输入塞进去会发现输入参数数量对不上。想用外生变量一定记得用narxnet输入延迟、反馈延迟分别设好。4.2 随机初始权重导致的不可复现问题神经网络初始权重是随机的意味着同一份代码、同一个数据两次跑出来的结果可能差不少特别是数据量小的时候。这会给项目验收和调试带来很大麻烦。解决方案有两个。第一个是固定随机种子在训练前加一行rng(42); % 固定随机数生成器保证可复现第二个是用trainbr训练算法。它自带贝叶斯正则化对初始值的敏感度相对低一些而且在小样本数据上表现稳定。我的习惯是接触到一个新数据集时先用rng固定种子跑三遍看结果的方差是不是可接受方差大了就先怀疑数据量太少或网络过大而不是直接用某一次的好结果去汇报。4.3 多步预测发散闭环不等于一劳永逸很多人遇到的经典问题闭环预测出来的未来曲线前几步还行越往后越平甚至直接跑歪。原因有两个。一是开环训练时网络习惯了“踩在真实值肩膀上”一到闭环模式下反馈输入变成自身预测值误差逐递归放大这就是误差累积效应。二是外部输入在未来时段的取值没有真实值你需要用估计值或均值填充这个额外误差也会叠加进去。我的兜底方案是三条第一控制预测步数不要一口气预测几十步改成“预测一步然后把真实值或修正后的值滑入窗口继续预测”这种滑动窗口滚动预测在工程上远比长跑快预测可靠。第二如果外部输入的未来值有周期性或可按小时/按天推算先做一次初步预测或直接用近邻均值填。第三如果发散得实在厉害把闭环问题改造成“滞后预测”也就是把目标序列平移一个步长做监督学习虽然这样预测的不再是纯未来值但在业务上常常够用。4.4 实测数据集的完整结果展示分享一组我在一个真实日负荷数据集上的结果数据来自某地过去两年的日度负荷和温度记录样本量730条。我按70%训练、15%验证、15%测试做顺序切分d3隐含层神经元10个trainlm训练。训练完成后测试集闭环预测指标是RMSE约为实际负荷均值的3.2%MAPE大约在3.8%R²在0.94上下。坦白说这个结果不算惊艳但已经能满足业务上“日负荷预报误差在5%以内”的要求。同样数据用LSTM调参调了两天最终效果也就比NARX好了不到0.5%的MAPE训练时间却翻了快二十倍。这里我也要强调NARX对非平稳趋势敏感。如果原始序列有明显上升或下降趋势直接喂给网络预测往往会滞后一拍。我通常先对目标序列做一阶差分让网络预测“增量”而不是“绝对值”预测结束后再累积回去。这么做能让RMSE再往下降一截。5. 这套模型还能怎么玩NARX在几个真实场景里的使用体会最后一部分讲一些我在实际项目中折腾NARX的扩展心得。如果你确认要用NARX这些体会能帮你把它的价值发挥得更充分。5.1 外部输入的特征工程别只喂原始数据我在日负荷预测项目里外部输入最开始只有当天气温。加上“前一天同时刻负荷”、“周几”作为外生变量之后测试集RMSE立刻下降了近10个百分点。原因并不神秘——负荷本身就有强烈的“相似日”效应周六和周一模式完全不同。把星期编码成数值特征比如周一0、周七6能极大减轻网络自己从历史里硬学周期规律的负担。做NARX特征工程时我建议把以下几类信息作为外生变量候选可预测的环境变量温度、湿度、风速时间上下文小时、星期几、是否节假日滞后统计量过去24小时均值、过去7天同一时刻值业务事件标记活动日、促销日、特殊事件日每加一个变量都要在验证集上看误差有没有真的下降。没有下降就果断剔除不要贪多。5.2 模型融合NARX当主力其他模型当裁判在我的工作流里NARX往往不是孤立使用的。我会同时跑一个ARIMA模型和一个线性回归基准模型。如果某个指标NARX连线性回归都没跑赢那说明问题多半出在特征上而不是模型不够强。线性基准模型的代码量很小但作用极大——它能告诉你“提升空间还有多少”。另一种融合方式是让NARX输出和LSTM输出做简单平均集成或者用NARX预测的结果作为LSTM的一个额外输入特征。这种做法在竞赛里很常见能稳定降低方差。不过代价是复杂度上升只有在单模型精度已经不够用的时候我才推荐上。5.3 未来多步预测时的具体操作前面答应过讲未来多步预测这里补上。假设训练好的网络已经转换成闭环模式netc现在数据结束在时间点N想预测N1到NM的目标值并且已经有了外部输入未来值X_future% 取最后feedbackDelays个真实目标值作为初始反馈状态 % 先把历史段准备一次提取初始状态 [Xs_hist, Xi_hist, Ai_hist, ~] preparets(netc, X_hist, {}, T_hist); % 初始化预测序列 numPredSteps M; Y_pred zeros(1, numPredSteps); X_futCell num2cell(X_future); % 外部输入未来值已归一化 % 逐步预测每步把预测输出作为下一步反馈并同步更新外部输入窗口 for i 1:numPredSteps [Xs_step, Xi_hist, Ai_hist] preparets(netc, X_futCell(i), {}, {}); y_step netc(Xs_step, Xi_hist, Ai_hist); Y_pred(i) y_step{1}; % 把当前预测值补充到反馈状态中此处省略内部状态更新细节 end坦白说这段代码里的循环状态更新细节比较多不同版本的MATLAB在Ai_hist的内部长度处理上有一丁点差异。我实际项目里更常用的是“滚动重新训练”策略每次预测一个点等真实值出来之后把它加进历史集重新训练一次网络。这个策略虽然每次都要花几分钟训练但模型始终在用最新数据长期跟踪场景下效果远好于一次性训练完闭着眼睛预测几十步。5.4 工程落地时的三点提醒第一数据保存时记得把历史归一化参数一起存下来.mat文件里放loadPS、tempPS部署预测时还要用它们对新的输入做同样的缩放。第二网络训练完成后直接把net和netc都保存预测线上环境里直接加载闭环网络不要再现训。第三模型评估报告里务必写清楚“开环训练指标”和“闭环测试指标”的区别我见过太多汇报成绩时只说开环训练误差、实际部署后被测试集光速打脸的情况。写在最后把NARX用到今天我对它的定位越来越清晰它是一个“快速、透明、够用”的时间序列预测工具。在数据量不大、特征关系明确、预测步数不夸张的场景下它比LSTM省力得多比ARIMA灵活得多。你不需要在调参的汪洋大海里挣扎只要把数据切好、延迟设对半天之内就能交付一个不算粗糙的预测原型。我个人的心得是别把希望全押在模型结构上。数据切分是否顺序合理、归一化参数是否泄漏、闭环预测的误差累积有没有控制这些工程细节多数时候比换一个更复杂的模型更能提升最终结果。希望你拿到这篇文章的代码之后也能在自己数据集上跑出一个稳定的基线再考虑要不要往深度学习的方向继续走。