☰
MATLAB多输入多输出DNN全连接时间序列预测详解
2026/10/15 5:00:34 网站建设 项目流程

这个标题我太熟悉了:MATLAB、DNN全连接神经网络、多输入多输出多变量时间序列预测,后缀还跟着“代码已调试成功,可一键运行,每一行都有详细注释”。很多朋友拿到这类模板代码,第一反应是能跑就行,但真正拿到实际项目里就懵了——窗口怎么滑、输出几个步长、归一化用哪一段的数据、多步预测是递归还是直接输出?这篇就把整条链路拆开讲透,不仅讲怎么跑通,也讲怎么改。如果你是刚接触时间序列预测的MATLAB用户,或者想快速出一个DNN全连接基线模型,这篇文章值得从头到尾看一遍。

1. 项目背景与核心思路

1.1 多变量时间序列预测到底在预测什么

先明确一个概念:所谓“多变量时间序列”,指的是同一时间轴上同时观测多个属性。比如一套传感器系统同时记录温度、湿度、压力,或者一个交易系统同时记录成交量、价格、波动率。这些属性彼此存在耦合关系,单独去预测某一个,往往会丢掉交叉信息。

“多输入多输出”在这个项目里有两种理解方式。第一种是模型的输入是多变量,输出也是多个变量的未来值;第二种是输入是历史多变量窗口,输出是未来多个时间步长。标题里两个“多”字其实都占了:构建输入时用多个变量的历史数据,构建输出时既可以有多个变量,也可以有多个未来步长。也就是说,这是一个从“过去多变量矩阵”到“未来多变量矩阵”的映射问题。

形式化一点:假设当前时刻为 t,历史窗口长度为 W,变量个数为 F,未来预测步数为 H。输入可以展开为一维向量:

x(t) = [x1(t-W+1), x2(t-W+1), …, xF(t-W+1), …, x1(t), x2(t), …, xF(t)]

输出可以是:

y(t) = [x1(t+1), …, xF(t+1), …, x1(t+H), …, xF(t+H)]

也可以只预测部分未来步长或部分变量。项目里最常见的是“多输入 + 多输出通道”的做法:网络输出层节点数等于未来需要预测的总数值个数,也就是 H×F 或其中几个关键变量的 H 个步长。

这个建模思路的本质是把时间序列预测转换成监督学习。DNN全连接网络在这个任务里扮演的角色就是那个“映射器”:输入是拼接后的历史片段,输出是未来片段,中间用若干层全连接加激活函数去拟合复杂的非线性关系。用生活化的比方说,就像一个人看过过去几天的天气,要直接报出未来三天的天气,网络要学的就是历史和未来之间那个说不清道不明的对应关系。

1.2 为什么用DNN全连接而不是LSTM/Transformer

很多朋友一听到时间序列预测,第一反应就是LSTM。但在这个项目里选择DNN全连接,并不是因为LSTM不能用,而是因为全连接网络在特定条件下有自己的优势。

第一,收敛速度快,训练过程相对容易把控。全连接网络没有循环结构,不存在长时依赖问题,反向传播路径短,训练时不容易出现梯度消失或梯度爆炸,尤其适合快速验证数据是否可行。

第二,代码简洁,易调试。一个完整可运行的全连接网络模型核心代码不过几十行,维度问题一眼就能看出来,这对初学者非常友好。我见过不少朋友第一次上手LSTM,光是被“序列长度、特征维度、batch维度”这三个概念就绕晕了,而全连接网络只有“一行样本、一列特征”两层含义。

第三,对“表格化”后的时间序列数据天然适配。滑动窗口将时间序列变成了“一行一条样本”的表格形式,全连接网络处理表格数据是非常经典的方案,训练稳定可靠。

第四,某些实际场景的数据量并不大,复杂模型不一定有优势。当样本只有几百条或几千条时,LSTM很容易过拟合,DNN全连接配合正则化和早停,往往能拿到更稳的结果。

当然,DNN的短板也要说清楚:它没有时序建模的先验结构。如果输入特征里没有明显的滞后信息,模型很难自动学到“周期规律”这种时序特性。所以实际操作中需要靠滑动窗口人为把时间依赖塞进特征,相当于用特征工程弥补结构的不足。这也是为什么“窗口怎么滑”比“网络怎么搭”更影响最终效果。

选择DNN全连接本质上是在“结构复杂度”和“工程效率”之间找一个平衡点。先用它跑通流程、验证数据、建立基线,再考虑是否需要升级到LSTM或带注意力机制的模型。

1.3 整体架构设计

项目整体分五个环节:数据准备、归一化、滑动窗口构造、网络训练、预测回放与评估。数据准备负责把原始多变量序列整理成时间对齐的矩阵;归一化把所有变量压到同一量级,让全连接层的权重更新更平稳;滑动窗口负责把时间序列转成“X-Y”监督学习样本;网络训练负责拟合映射关系;预测回放负责把模型输出还原成原始尺度,并计算误差、画图展示。

这里强调一下数据构造的顺序,很多人会在“划分训练集和测试集”之后再做归一化,或者随机打乱样本,这两种做法在时间序列项目里都要谨慎。随机打乱会破坏时间顺序,导致未来信息泄露到训练集里,看起来训练误差很好,实际线上预测一塌糊涂;而先划分再归一化则可能导致测试集单独用一个尺度,指标横向对比时失去意义。可靠的做法是先对整个原始序列拟合归一化参数,再按时间顺序构造样本、切分数据集。

整个流程结束之后产出的是:一张预测曲线对比图、一组多步预测指标、一个可以复用的训练脚本。这就是标题里“有图有真相”的含义。

2. 数据准备与预处理

2.1 数据形态与读取方式

在MATLAB里,多变量时间序列最常用的存储方式是“每个时刻一行、每个变量一列”的矩阵。假设我们有3个变量、1000个时刻,数据就是一个1000×3的矩阵。

代码里我习惯先定义一个辅助函数,把原始矩阵读进来后按时间顺序排好,同时把时间轴单独存一份,方便后面画图。如果数据是Excel或CSV格式,用readmatrix直接读取即可;如果数据来自传感器,通常还需要做缺失值和异常值处理。这个项目里可以先用模拟数据,真实数据流程完全相同,把数据文件换成你自己的矩阵就行。

读取数据之后,先别急着跑网络,用plot把每个变量的曲线都画一遍。这一步看起来朴素,却能避免很多后续麻烦。比如某个变量有一段异常跳变,拉高了整体方差,如果没发现,后面模型可能会一直试图拟合那些异常点;比如变量之间存在明显相关性,那你对“多变量输入”的收益会更有信心;再比如序列本身有趋势或季节周期,这会影响你对窗口大小的选择。

2.2 滑动窗口机制:从时间序列到监督学习

窗口构造是核心。假设历史窗口长度W=10,预测步数H=3,变量数F=3。那么从第1条到第10条数据,可以构造出第一条样本:输入是把第1到第10时刻共10×3=30个数值展开成一行,输出是第11时刻到第13时刻共3×3=9个数值。然后滑动到2~11时刻构造第二条样本,输入是第2到第11时刻的30个数值,输出是第12到第14时刻的9个数值,依次类推。

这样做的结果是样本数等于总长度 – W – H + 1。如果变量数多、窗口大,输入维度是会膨胀的。比如20个变量、50个窗口,展开后就是1000维。全连接网络可以处理这个维度,但参数量会跟着变大,训练时间变长,这时需要适当增加正则化或减少隐藏层宽度。

代码示例:

function [X, Y] = createSlidingWindow(data, windowSize, horizon) % 输入:data为N×F矩阵,N为时刻数,F为变量数 % 输出:X为样本数×(windowSize*F)输入矩阵,Y为样本数×(horizon*F)输出矩阵 numSamples = size(data, 1) - windowSize - horizon + 1; numFeatures = size(data, 2) * windowSize; numResponses = size(data, 2) * horizon; X = zeros(numSamples, numFeatures); Y = zeros(numSamples, numResponses); for i = 1:numSamples xWin = data(i:i+windowSize-1, :); yWin = data(i+windowSize:i+windowSize+horizon-1, :); X(i, :) = xWin(:)'; Y(i, :) = yWin(:)'; end end

这个循环每次构造一条样本,逻辑清晰,缺点是样本量大时有点慢。数据量上万条时可以直接向量化,但循环版本更适合理解原理。构造完成之后,X和Y分别就是训练所需的输入矩阵和输出矩阵。

窗口大小的选择,我的经验是先从10或20起步,用验证集做一组小对比实验。窗口太小,模型看不到足够历史;窗口太大,遥远过去的弱相关变量会被当成有效信息,反而干扰预测。预测步数H则根据实际需求定,如果业务只需要看未来3个时刻,就不要一步到位预测30个时刻,多输出步数越少,模型承担的学习压力越小。

2.3 归一化:顺序和尺度

归一化的原因很直接:不同物理量纲的数值范围可能天差地别,比如温度是0~40,压力可能是90000~110000。如果不归一化,网络权重在训练初期会为了适配不同尺度而剧烈震荡,收敛很慢,甚至出现梯度爆炸。

推荐使用z-score标准化。先计算整个序列每个变量的均值和标准差,再对整列做变换。MATLAB里可以这样写:

mu = mean(data, 1); sigma = std(data, 1); dataNorm = (data - mu) ./ sigma;

这里一个很重要的坑是:均值和标准差必须在测试数据参与构造样本之前计算。如果在构造完X-Y样本后分别对训练集和测试集做归一化,会引入不一致的量纲,导致预测回放时指标失真。正确流程是:先对原始序列做归一化,再构造滑动窗口样本。

还有一个细节:如果变量包含明显的趋势项或季节性,归一化并不会消除这些成分。模型预测的是原始值还是差分后的变化量,会直接影响窗口设计和网络学习难度。通常来说,如果序列非平稳,我会先对序列做一阶差分,把预测目标变成“下一个时刻相对于当前时刻的变化量”,等预测出变化量之后,再用上一步真实值或者预测值把绝对值累加回去。这种做法在DNN全连接网络里很有用,因为网络不需要自己学“趋势”这种时间结构,只需要学“变化”和“变量之间的耦合关系”。

2.4 训练集、验证集、测试集划分

时间序列数据集划分和普通机器学习不同,不能随机打乱。我通常按比例切分:前70%作为训练集,中间15%作为验证集,最后15%作为测试集。验证集用于训练过程中观察是否过拟合,测试集只在训练完全结束后做一次最终评估。

如果样本数量较少,也可以采用滚动验证,但这里不展开。切分样本时要注意:滑动窗口构造出来的样本之间存在重叠,相邻样本天然有相关性,这会造成训练集和验证集之间的信息重叠,轻微一点没关系,但测试集必须和训练集在时间上完全隔离,否则指标会虚高。

划分时还要留意验证集和测试集的时间跨度是否具有代表性。如果测试集恰好落在整个序列最平稳的一段,模型误差看起来会特别低,但这不代表真实性能。反过来,如果测试集包含训练集没出现过的异常突变,误差会突然变大,也不一定是模型坏了。在评估时要先看序列形态,再下结论。

3. MATLAB环境与工具箱准备

3.1 环境要求与硬件配置

跑这个项目需要MATLAB的机器学习与深度学习工具箱,也就是包含trainNetwork的那一套。一般R2019b以上版本都支持本文用到的函数,R2021之后某些函数的默认行为有变化,代码里最好显式指定参数,避免版本差异。

硬件方面,全连接网络对硬件要求不高,普通办公电脑CPU都能跑,甚至比GPU更高效,因为样本量不大时GPU通信开销反而浪费时间。这个项目我实测下来,CPU训练几十秒就能完成,完全不需要单独配置CUDA环境。

运行脚本前,建议先执行ver命令查看工具箱是否完整。如果缺少工具箱,调用trainNetwork时会直接报错,这时候先去安装相应工具箱,别在代码里纠结半天。另外,如果内存比较紧张,数据量又很大,注意及时用clear清理不再使用的大变量,避免训练过程中出现内存不足。

3.2 核心API与常用函数对照

训练一个DNN全连接模型,核心函数就几个,我用一张表给它们梳理清楚:

函数作用使用注意点
featureInputLayer定义输入特征维度第一个参数要等于输入列数,可指定归一化方式
fullyConnectedLayer添加全连接层输出节点数由上一层需求决定
reluLayer添加ReLU激活函数隐藏层常用,输出前不加也行
regressionLayer定义回归损失回归任务默认使用均方误差
trainingOptions配置训练超参数求解器、学习率、验证集、绘图开关都在这里
trainNetwork启动训练输入XTrain、YTrain、layers、options
predict对新样本做预测输出维度与YTrain列数一致

可能有人会问,为什么不用神经网络模式识别的nftool或者直接写手写反向传播。这两个选择要么交互式流程不好批量实验,要么重复造轮子且数值可靠性差。直接用trainNetwork配合layers数组,既保留灵活性,又能自动处理权重初始化、梯度和损失计算,是工程上的最优选择。

3.3 代码文件组织与一键运行思路

项目标题强调“一键运行”,实际操作时我也会把代码组织成单个主脚本,按段落顺序执行。从数据读取、预处理、滑动窗口、网络训练到绘图,每个环节用注释分隔。这样别人拿到脚本,不需要手动修改执行顺序,从上往下跑就能得到结果。

为了调试方便,可以在主脚本最开头加一个clear; close all; clc;,清理工作区变量、关闭历史图形、清空命令窗口。这不是玄学,而是避免上一次运行时留下的变量影响本次计算。比如上一次运行改变了某个全局变量,下一次运行时如果没清理,结果可能对不上。

4. DNN模型搭建与训练配置

4.1 网络结构:输入、隐藏层、输出怎么定

以多输入多输出任务为例,网络结构可以设计成:

输入层 → 全连接层(128) → ReLU → 全连接层(64) → ReLU → 全连接层(输出个数) → 回归层

我习惯把隐藏层设置成“先宽后窄”,因为输入维度往往很大,第一层宽一点能充分组合特征,后面逐渐收缩,起到压缩信息的效果。隐藏层数量和宽度不是越大越好。某个模拟项目里我试过把两个隐藏层都加到256个节点,训练误差降得更快,但验证误差反而上升,典型的过拟合。后来改成128和64,加上Dropout,效果反而更可靠。

网络中还要考虑Dropout层和L2正则化。Dropout在训练时随机丢一部分节点,相当于隐式集成多个子网络,能有效缓解过拟合;L2正则化则限制权重不能过大。全连接网络参数量大,这个正则化几乎是必须的。

具体代码:

numFeatures = windowSize * numVariables; numResponses = horizon * numVariables; layers = [ featureInputLayer(numFeatures, 'Normalization', 'none', 'Name', 'input') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(64, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(numResponses, 'Name', 'outputLayer') regressionLayer('Name', 'regression') ];

这里有个容易踩的坑:featureInputLayer的Normalization参数如果设为zscore,训练时会自动再做一次标准化。如果数据已经在外部做过标准化,这里设成none,避免重复标准化导致尺度被改两遍。我在实际项目中倾向于全部外部处理,网络内部不再做标准化,这样测试阶段能够直接用训练集统计量进行归一化,更可控。

4.2 训练选项详解:学习率、批次大小、早停

trainingOptions里影响最大的几个参数:

  • solver:用adam,自适应学习率,训练全连接网络收敛快。
  • MiniBatchSize:我常设32或64。太小噪声大,太大更新次数少。
  • MaxEpochs:配合早停来设,先给一个较大值,比如200。
  • InitialLearnRate:全连接网络我用0.001起步,如果损失不降就降到0.0001。
  • ValidationData:传入验证集,配合OutputNetwork判断是否保存最佳模型。
  • Plots:设为training-progress可以实时看损失曲线,这是调试时非常重要的手段。
options = trainingOptions('adam', ... 'MiniBatchSize', 32, ... 'MaxEpochs', 200, ... 'InitialLearnRate', 0.001, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 10, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true);

训练过程中,验证损失如果连续多轮不再下降而是上升,说明网络开始过拟合。trainNetwork本身不支持中途手动停,所以更简单的方案是设大epoch,配合OutputNetwork选最优验证模型。

ValidationFrequency控制每隔多少轮计算一次验证损失。设得太小会频繁验证,拖慢训练;设得太大又可能看不到过拟合的开始点。MiniBatchSize的选择也要看内存,数据量大时大batch会一次性占用很多内存,如果MATLAB报内存不足,优先减小批次而不是减小网络。

4.3 训练过程监控:损失曲线与过拟合判断

训练完成后,先看训练损失和验证损失。理想状态是两个损失都平稳下降,最后稳定在较低区间。如果训练损失很低但验证损失高,过拟合。如果两个损失都高,要么网络容量不足,要么数据特征没有构造好。

一个容易被忽略的检验是看预测结果是否存在“延迟复刻”。输出曲线如果只是把输入曲线平移了很小的幅度,说明网络在“记住上一步的值”而不是学习动态变化,这种模型换一段数据就崩。看到这种曲线,优先检查窗口是否太短、数据是否没有做差分、训练集和测试集是否分布不一致。

训练损失曲线本身也能看出不少信息。如果损失像锯齿一样剧烈抖动,很可能是学习率偏大或batch太小;如果损失下降一段时间后进入平台期,可能是网络容量饱和,也可能是数据本身信息量不足。我通常不会完全依赖训练结束后的指标,而是会把这套监控流程固定下来,每次实验都看一眼曲线形状。

5. 预测回放与结果评估

5.1 直接多步还是递归多步

多步预测在模型输出上有两种范式。第一种是“直接多步预测”,输出层直接输出未来H步的所有值,上文的网络就是这个风格。第二种是“递归多步预测”,模型只输出未来一步,然后把预测值拼回输入窗口,继续预测下一步。

直接多步的优势是训练时模型知道要一次预测整个未来窗口,不会在递归过程中累积误差,缺点是输出维度大时学习难度大。递归多步的网络更简单,但误差会随着递归步数累积,越往后越偏。实际应用里我更推荐“先直接,再混合”:模型输出H步的关键变量,第H+1步以后如果还需要继续推,就把预测值作为新数据重新走一遍预测循环。

预测回放阶段,一个完整的循环是:取测试集第一条输入窗口,得到未来H步预测,把预测结果存起来;再把第一条输入窗口向后推一位,得到下一条样本的H步预测。这样逐条滑动,最终每个测试时刻都会有多条预测值落在其上,取平均值作为该时刻的最终预测。这种方式也叫滑动验证,它比只预测一整段测试序列更接近真实使用场景。

5.2 反归一化与指标计算

网络输出是标准化空间里的数值,必须先反归一化才能和真实值对比。反归一化公式就是标准化的逆向操作:

predict_real = predict_norm * sigma + mu

这里要注意:如果预测目标是多个变量,每个变量要用自己的sigma和mu还原,不能套错。我在代码里把mu和sigma按变量分别存好,还原时对预测矩阵逐列操作。

指标计算我建议在反归一化之后做,这样得到的误差是原始物理量纲,能直接跟业务指标对比。如果拿到的是标准化后的误差,普通人很难判断一个0.02的误差到底算好还是坏。

5.3 常见评估指标对照

时间序列回归任务常用四个指标:

指标含义适用场景
RMSE均方根误差,对大误差敏感需要惩罚严重偏差时
MAE平均绝对误差,更稳健数据存在零星离群点时
MAPE平均绝对百分比误差变量量纲差异大时
R2决定系数,衡量解释方差比例模型整体拟合度比较

R2的计算简单说就是1减去“预测残差平方和”除以“真实值偏差平方和”。如果R2为负,说明模型还不如直接取历史均值,基本可以判定模型失效。

MAPE有一个陷阱:当真实值接近0时,分母趋近0,误差会瞬间放大,所以使用前要检查数据是否包含接近0的值。如果包含,建议改用对称MAPE或者只统计非零部分。

5.4 画图呈现,让结果说话

画图是“有图有真相”的关键环节。我常用的方式是每个变量一张子图,横轴是时间,画真实值和预测值两条曲线,再画一条竖线标记测试集开始位置。如果窗口用多步预测,可以叠加一个误差区间阴影。

fig = figure('Position', [100, 100, 1200, 600]); for v = 1:numVariables subplot(numVariables, 1, v); plot(time, realData(:, v), 'b-', 'LineWidth', 1.5); hold on; plot(time, predData(:, v), 'r--', 'LineWidth', 1.5); xline(testStart, 'k--'); legend('真实值', '预测值', '测试集起点'); title(sprintf('变量 %d 预测对比', v)); end

图上加上起点分割线后,能一眼看出训练段和预测段的效果差异。如果预测段蓝色和红色两条线贴得很紧,说明模型有效;如果红色在预测段很快退化,通常就是递归误差累积或特征信息不足。

绘图时如果变量太多,子图会太密,我一般只展示前3个变量,或者把多变量序列做PCA之后展示主成分曲线。画图的目的是让读者快速理解模型表现,而不是把所有信息都堆上去。

6. 完整代码逐块解析

这一部分给一份可以直接运行的代码框架。由于项目标题特别强调“每一行都有详细注释,代码已调试成功”,我这里也按注释风格给出主干代码,并逐块说明。

6.1 第一步:生成或读取多变量数据

没有真实数据时,先用模拟数据验证流程。生成三路相互关联的信号,保证代码能直接跑通:

% 生成1000条模拟数据,3个变量,时间间隔1 rng(42); t = (1:1000)'; data = zeros(1000, 3); data(:,1) = sin(t/20) + 0.05 * randn(1000, 1); data(:,2) = cos(t/25) + 0.1 * data(:,1) + 0.03 * randn(1000, 1); data(:,3) = data(:,1).^2 + 0.2 * data(:,2) + 0.08 * randn(1000, 1);

真实项目里替换成readmatrix从文件读取即可。模拟数据的好处是结果可靠,方便对比不同网络结构。用randn加噪声是为了模拟真实数据里的随机波动,否则三条变量之间关系太干净,预测指标会漂亮得不真实。

6.2 第二步:归一化与滑动窗口

读取数据后,先做z-score归一化,再构造窗口。顺序不能乱。

% 先计算均值和标准差 mu = mean(data, 1); sigma = std(data, 1); % 对整个原始序列做归一化 dataNorm = (data - mu) ./ sigma; windowSize = 10; horizon = 3; % 调用自定义函数构造监督学习样本 [X, Y] = createSlidingWindow(dataNorm, windowSize, horizon);

构造完成后,X的每一行代表一条历史窗口,Y的每一行代表对应未来的多变量值。检查一下size(X, 1)是否等于样本数,size(Y, 2)是否等于输出维度,这能避免后续维度报错。

6.3 第三步:划分数据集

% 按时间顺序切分 ratioTrain = 0.7; ratioVal = 0.15; numTrain = floor(size(X, 1) * ratioTrain); numVal = floor(size(X, 1) * ratioVal); XTrain = X(1:numTrain, :); YTrain = Y(1:numTrain, :); XVal = X(numTrain+1:numTrain+numVal, :); YVal = Y(numTrain+1:numTrain+numVal, :); XTest = X(numTrain+numVal+1:end, :); YTest = Y(numTrain+numVal+1:end, :);

注意这里必须按顺序切,不能随机permute。如果数据量充足,也可以考虑把验证集放在训练集之后、测试集之前,这样时间关系最自然。

6.4 第四步:定义网络和训练

% 输入特征数等于一列X的长度 numFeatures = size(XTrain, 2); % 输出数等于一列Y的长度 numResponses = size(YTrain, 2); layers = [ featureInputLayer(numFeatures, 'Normalization', 'none', 'Name', 'input') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(64, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(numResponses, 'Name', 'outputLayer') regressionLayer('Name', 'regression') ]; options = trainingOptions('adam', ... 'MiniBatchSize', 32, ... 'MaxEpochs', 200, ... 'InitialLearnRate', 0.001, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 10, ... 'OutputNetwork', 'best-validation', ... 'Plots', 'training-progress', ... 'Verbose', false); net = trainNetwork(XTrain, YTrain, layers, options);

训练完成后net里保存的就是训练好的模型。OutputNetwork设为best-validation意味着保存验证损失最低的那版权重,这比最后一轮权重更可靠。

6.5 第五步:预测、反归一化、指标

% 用测试集输入做预测 YPredNorm = predict(net, XTest); % 反过来还原到原始量纲 YPred = YPredNorm .* sigma + mu; % 测试集目标也要还原 YReal = YTest .* sigma + mu; % 分变量、分步长计算误差 rmse = sqrt(mean((YReal - YPred).^2, 1)); mae = mean(abs(YReal - YPred), 1);

如果要做递归多步,就需要在循环里调用predict,这里先展示直接一步输出全部H步的做法,最简单也最可控。

6.6 第六步:绘图

figure; for v = 1:numVariables subplot(numVariables, 1, v); idx = (numTrain+numVal+1):size(X,1); plot(t(idx), YReal(:, v), 'b-'); hold on; plot(t(idx), YPred(:, v), 'r--'); end

如果变量数多,子图会太密,可以改成只看第一主成分或只展示前3个变量。绘图目的不是把所有变量都堆上去,而是让读者一眼看出模型在测试集上的拟合程度。

7. 常见问题与排查心得

7.1 损失不下降怎么办

这是全连接网络最容易碰到的问题。先检查数据是否做了归一化,如果特征里有一个变量量级过大,梯度会被这个变量主导,权重更新乱七八糟。其次检查学习率,Adam默认的0.001在很多任务上可行,但网络层数加深后可能需要0.0001。还可以把隐藏层节点翻倍,看看是否是表达能力不够。

如果这些都试过还是不行,退一步看数据本身。窗口长度是不是太长,导致每个样本输入维度很大但有效信息很少;输出目标是不是分布极不均匀,比如大部分时间都接近某个常数。这两个问题靠调网络参数解决不了,得回到特征构造上。

7.2 预测曲线滞后明显

预测结果看起来像“真实曲线右移了一位”,这是时间序列预测里非常经典的现象,本质是模型学到了“用上一时刻的值近似当前时刻的值”。解决办法是增大窗口长度,让模型看到更长的历史变化;或者对非平稳序列做一阶差分,让模型预测变化量而不是绝对值;再或者减少预测步数,先保证单步精度。

滞后现象在评估时需要用滞后损失专门判断,常见的做法是计算不同时滞下预测值与真实值的相关性,如果相关性最高的时滞恰好是1步,说明网络确实在走捷径。模型没有真正学会动态规律,这个模型不能上线。

7.3 维度不匹配报错

报错信息往往会指向layer input size和training data size不一致。记住一条:featureInputLayer的第一个参数必须等于size(XTrain, 2),fullyConnectedLayer输出层必须等于size(YTrain, 2)。如果构造样本时不小心把输入输出顺序搞反,也会出现这类问题。建议在训练前设置断点或打印size信息,省得在trainNetwork内部排查。

还有一个容易忽略的地方:如果YTrain只有一列,而网络输出层节点数不小心设成了多列,regressionLayer可能不会直接报错,但训练过程会很奇怪。检查方式就是在训练之前打印一下size(YTrain, 2)和最后一个全连接层的输出节点数。

7.4 预测结果全是均值

网络把输出回归到了训练样本的平均值附近,这是欠拟合的典型表现。输入特征可能没有给足信息,网络觉得预测“整体平均水平”是最小化损失的最优解。可以尝试增加窗口长度、增加隐藏层宽度、加入更多滞后特征,或者调整损失函数权重,把较大误差的样本权重提上去。

从数据侧看,如果历史窗口里的信息确实不足以预测未来,比如两个毫无关联的变量强行拼在一起,那模型学到的自然只能是均值。这种情况下不要硬加网络复杂度,先审视特征有没有意义。

7.5 验证集比测试集表现好

听起来像是不可能,但确实发生过。最常见的原因是划分时验证集恰好落在了一段相对平稳的序列上,模型在这段上几乎不需要预测多少波动,误差自然小。另一个原因是有时模型训练不充分,验证集简单导致看起来更好。这不算bug,但也提醒我们评估模型不能只看一组指标,要看整条预测曲线的形态。

7.6 调试心得速查表

我这里整理了一张很实用的速查表,做类似项目时可以直接对照定位:

症状可能原因优先排查方向
训练损失高数据未归一化、网络容量不足检查数据和节点数
训练损失下降慢学习率太小调大InitialLearnRate
训练损失震荡学习率太大或batch太小调小学习率或调大batch
验证损失高于训练损失过拟合加Dropout、加L2、增大数据
预测曲线滞后输入信息不足加窗口、做差分
预测值全部接近均值欠拟合加节点、加特征
维度报错X或Y维度不一致打印size核对

8. 一点经验与后续扩展

8.1 把模板代码迁移到真实数据

把这段代码放到真实数据上之前,我的习惯是先画一遍完整时间序列,观察缺失值、突变点、周期性,再决定窗口大小和预测步数。窗口大小不是越大越好,太长会把遥远的无关信息也塞进去,反而拉低精度;太短又学不到足够的历史模式。通常可以先从10或20起步,用验证集做一组小对比实验。

训练脚本里我还会固定随机种子,保证每次跑的结果可复现。数据量小的时候,随机种子对结果影响很大,如果不在rng这里固定,换一台机器或者换一次运行得到的结果可能差异明显,没办法做对比实验。

代码注释密、能一键跑通,是这类模板最大的价值,但模板能给你的只是起点。真正的模型效果好坏,还是要靠你对数据的理解、对异常情况的排查、对多步预测策略的选择来决定。

8.2 模型升级方向

如果DNN全连接已经能跑出一个不错的基线,下一步可以根据数据特性升级模型。时间序列依赖明显的场景,可以换LSTM或GRU;想进一步提升历史特征的重要性,可以叠加注意力机制;如果变量特别多,可以先做特征筛选,用PCA降维后再进全连接网络。

预测策略上也可以做更多文章,比如改成seq2seq结构做真正的多步预测,或者引入差分、季节项分解,把趋势项和残差项分开建模。还有一点值得留意:如果业务对“未来某一段区间”更关心,可以在评估指标上做加权,把近期的预测误差算得更重,让模型优化方向更贴合业务目标。

这也是做时间序列项目最有意思的地方:每次换一份数据,都会遇到新的坑,而每填一个坑,你对模型的认识就又深了一层。

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

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

立即咨询