ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Seq2SeqRNN+Transformer编码器:MATLAB多变量时序预测实现

Seq2SeqRNN+Transformer编码器:MATLAB多变量时序预测实现 简介面向熟悉MATLAB和深度学习框架的研发人员与数据科学家这份资源围绕Seq2SeqRNN与Transformer编码器混合架构系统讲解多变量时间序列预测项目的完整实现。文档从项目背景、目标与意义入手梳理长序列依赖捕获、多变量交互建模、数据质量噪声干扰、模型训练资源消耗等挑战及对应解决方案随后给出模型架构描述与代码示例并涵盖训练调优、性能评估、GUI界面设计、目录结构部署等落地环节。压缩包为单份docx文档大小仅81KB便于携带与查阅。目前已有453人学习下载。文档不仅说明如何构建端到端预测系统还突出动态多头自注意力机制、噪声鲁棒性设计等特点并讨论智能制造、金融市场、能源管理等应用领域及未来改进方向可帮助读者快速理解混合时序建模思路降低在MATLAB中实现Seq2SeqRNN-Transformer的入门门槛。1. 多变量时间序列预测的痛点Seq2SeqRNN 与 Transformer 编码器怎么搭多变量时间序列预测与单变量最大的不同在于你不仅要猜准每个变量的自身走势还要让模型看懂变量之间的联动关系。比如一台制冷设备的温度、压力、电流、振动四个传感器温度异常升高可能来自外部环境波动也可能来自压力变化滞后导致的工况偏移预测模型如果只按时间轴挨个看数据很容易遗漏这种跨变量、跨时间步的相互作用。做工业数据预测的工程师应该都遇到过这类场景单变量 LSTM 跑得挺好一换到多变量数据集RMSE 掉不下来误差集中在某些变量同时剧烈变化的时段。要处理这种问题常见的技术方案是把编码端从“逐时间步压缩状态”的递归结构换成能够同时观察整段输入窗口的 Transformer 编码器。这就是标题里“Seq2SeqRNN 结合 Transformer 编码器”的核心思路让 RNN 家族继续负责时间顺序上的状态递推让 Transformer 编码器负责捕捉输入序列内部任意两个位置之间的关联。这里的“Seq2SeqRNN”指序列到序列的递归网络编码器解码器结构本身不丢弃Transformer 插入的是编码端或者特征交互层而不是把整个模型推倒重来。下面按我实际实现这种模型时的路径来拆解先讲清楚为什么这样组再给出 MATLAB 可执行的网络搭法和训练配置最后聊怎么验证预测结果不是偶然调参出来的。2. 多变量建模原理拆解RNN 的递归瓶颈与 Transformer 的自注意力互补2.1 递归网络处理长时间跨度的结构性弱点RNN 系列模型天然按时间顺序消费序列t 时刻的隐状态 h(t) 由 h(t-1) 和 x(t) 决定。这种递归结构保证了时间因果性但也带来了两个问题。第一个是对长距离依赖的捕捉能力有限LSTM 和 GRU 通过门控机制缓解梯度消失但仍然需要经过多个时间步逐步传递信息如果两个关键变量的相关性跨越了几十个观测点中间这些步会把信号逐步稀释。第二个是多变量交互被隐式建模每个时刻的输入 x(t) 是一个向量RNN 的输入层矩阵 W_x 能对向量内部各维度做线性组合但这只是一次线性混合并没有显式回答“温度在滞后 10 个时间步之后对电流的影响有多大”。用 Seq2Seq 结构时编码器通常读完整段输入得到一个上下文向量解码器再逐步生成预测。这种结构对短期预测有效原因在于未来近几步与输入序列最后几步强相关RNN 的近期记忆能力足够覆盖。但预测步长一旦拉长上下文向量作为固定维度的“信息瓶口”容量有限编码器最后几个时间步的信息会覆盖掉之前的重要模式。2.2 Transformer 编码器自注意力在变量交互上的优势Transformer 编码器把输入序列看作一组 Token任意两个 Token 之间都计算注意力权重。对多变量时间序列来说Token 的切分粒度有两种常见做法一种是把每个时间步的观测向量长度为变量数当作 Token自注意力在时间步之间计算变量交互由前馈网络和输入嵌入矩阵隐式处理另一种是把每个变量的整段历史当作 Token自注意力在变量之间计算。多数工业场景下取第一种原因是不需要额外整理变量维度顺序RNN 编码器输出后直接变换一下排列就可以喂给 Transformer。自注意力给多变量预测带来的实际收益是“跨步查看”。无论两个时间点隔了多远注意力权重的计算路径长度都是常数这一点比 RNN 的链式路径短得多。Transformer 编码器内部的多头注意力让不同注意力头分别关注不同交互模式比如一个头关注温度和电流的同步变化另一个头关注压力变化经过若干步后对振动的滞后影响。这种机制在没有人工特征工程的情况下让模型自己决定该看多远、侧重哪些变量组合。2.3 Seq2SeqRNN 与 Transformer 编码器的标准融合位置我见过的递归网络与 Transformer 融合方案大体有三类第一类是 RNN 作为位置编码替代品输入先经过一层 LSTM 得到每个时间步的隐状态再把这些隐状态作为 Transformer 编码器的输入序列相当于 RNN 先替代了正余弦位置编码让 Transformer 拿到带时序先验的特征序列第二类是模型两端并行RNN 分支处理短期模式Transformer 分支处理长期依赖最后拼特征第三类是只在解码端使用 Transformer编码端保持 RNN。标题明确写了“Seq2SeqRNN 结合 Transformer 编码器”我按第一类来讲这也是 MATLAB 里用深度学习工具箱实现时最顺手的一种组合。注意一个细节如果 RNN 的隐状态序列直接喂给 TransformerTransformer 的位置编码可以不加因为 LSTM 隐状态本身已经带时间顺序的递推痕迹。但实际中我还是建议加原因是 LSTM 的隐状态在特征空间中不会自然编码“我在序列中的绝对位置”尤其当输入序列不等长时位置信息会弱化。做法是定义一个与隐状态维度相同的位置嵌入矩阵加到 LSTM 输出上再进 Transformer。这个位置嵌入可以直接用正余弦公式生成不需要学习参数。3. MATLAB 实现 Seq2SeqRNN-Transformer 的数据准备与网络搭法3.1 从原始表格到滑动窗口样本集不管用什么网络结构多变量时间序列的第一步是把原始记录切成监督学习样本。假设原始数据是一个 N×K 的矩阵N 是观测时间点数K 是变量数我们希望用过去 inputLen 个时间步预测未来 horizon 个时间步。MATLAB 中常用 datastore 或者直接对数值矩阵操作来实现。下面这段函数按时间顺序生成训练样本function [XTrain, YTrain] createSlidingWindows(data, inputLen, horizon) % data: N x K 数值矩阵每一行是一个时间点 % inputLen: 回看窗口长度 % horizon: 预测未来多少步 % 输出是 cell 数组方便后续 sequenceInputLayer 使用 numSamples size(data, 1) - inputLen - horizon 1; XTrain cell(numSamples, 1); YTrain cell(numSamples, 1); for i 1:numSamples XTrain{i} data(i:iinputLen-1, :); % K x inputLenMATLAB 序列数据用 特征x时间步 YTrain{i} data(iinputLen:iinputLenhorizon-1, :); % K x horizon这里直接预测一个窗口 end end矩阵转置是这里最容易错的地方。MATLAB 的 sequenceInputLayer 默认识别的序列格式是 特征维 × 时间步 × 观测样本而绝大多数原始数据是按行存时间点的所以必须转置一次。输出维度同样要转置否则训练时报维度不匹配错误信息通常是指向 fullyConnectedLayer 的输入大小不一致。做预测时如果目标是只预测最后一个变量的未来值就把 YTrain 改为只保留目标变量那行输出维度变成 1×horizon与全变量输出的做法在 loss 上差异不大但收敛速度会更快。3.2 数据归一化要用训练集统计量不能用全局统计量多变量数据各列量纲差很多压力和温度直接拼在一起训练梯度会被量纲大的特征主导。常见做法是先做 Z-score 归一化但必须注意只从训练集计算均值和标准差然后把同样的参数应用到验证集和测试集。这个细节决定了验证指标的可信度。% 假设 dataMat 是原始数据矩阵先按 7:2:1 切分时间顺序 trainLen floor(size(dataMat, 1) * 0.7); valLen floor(size(dataMat, 1) * 0.2); trainRaw dataMat(1:trainLen, :); valRaw dataMat(trainLen1:trainLenvalLen, :); testRaw dataMat(trainLenvalLen1:end, :); mu mean(trainRaw, 1); sigma std(trainRaw, 0, 1); trainNorm (trainRaw - mu) ./ sigma; valNorm (valRaw - mu) ./ sigma;这里如果用整个数据集的均值去归一化测试集的统计信息会通过均值和标准差间接泄漏到训练过程中最后测试结果会偏高。工程上的判断标准是任何在拟合阶段计算的统计量都只能来自训练集。对深度学习模型这个要求比传统机器学习更严格因为多层网络会把这种泄漏放大。3.3 用深度学习工具箱构建网络结构MATLAB 实现这个模型有两种做法第一种是用内建层直接搭一个 layerGraph适合快速验证第二种是自定义训练循环配合 dlnetwork方便加位置编码和自定义损失函数。先说内建层方案。numFeatures size(trainNorm, 2); numHiddenUnits 128; numHeads 4; ffnDim 512; horizon 24; % 注意transformerLayer 要求输入特征维度等于 ModelDimension % 这里 LSTM 输出的隐藏单元数就作为 ModelDimension layers [ sequenceInputLayer(numFeatures, Name, input) lstmLayer(numHiddenUnits, OutputMode, sequence, Name, lstm_enc) transformerLayer(NumHeads, numHeads, ModelDimension, numHiddenUnits, ... FeedForwardDimensions, ffnDim, AttentionDropout, 0.1, ... Name, transformer_enc) fullyConnectedLayer(numFeatures * horizon, Name, fc_out) regressionLayer ]; lgraph layerGraph(layers);transformerLayer 在 MATLAB 深度学习工具箱里从 R2021a 开始提供输入格式要求是特征 × 时间步 × 观测样本这与 LSTM 的 sequence 输出格式完全吻合。ModelDimension 必须与上一层输出特征数相等否则会报维度不匹配错误。fullyConnectedLayer 的输出节点数设为 numFeatures × horizon 是为了直接输出整个预测窗口但这样会把时间信息拆散网络需要自己学会重排收敛较慢。另一种做法是把这一层换成一个 flatten 加全连接或者在自定义循环里只保留时序结构。3.4 自定义训练循环添加位置编码内建层方案快但有个明显缺陷transformerLayer 内部不自动加位置编码整个模型输入到 Transformer 之前的序列顺序信息完全依赖 LSTM 隐状态来承载。前面已经提过这个信息不够。我实际训练时会在 LSTM 输出后面手动加一个正余弦位置编码矩阵然后把结果作为 Transformer 的输入。这只在自定义训练循环里才能干净实现因为需要在前向传播中插入一个常量加法。function dlnet buildModel(numFeatures, numHiddenUnits, numHeads, ffnDim) % 使用 dlnetwork 构建模型输入是原始序列前向时在损失函数里加位置编码 layers [ featureInputLayer(numFeatures, Normalization, none, Name, input) lstmLayer(numHiddenUnits, OutputMode, sequence, Name, lstm_enc) transformerLayer(NumHeads, numHeads, ModelDimension, numHiddenUnits, ... FeedForwardDimensions, ffnDim, AttentionDropout, 0.05, Name, transformer_enc) fullyConnectedLayer(numFeatures * 24, Name, fc) ]; dlnet dlnetwork(layers); end % 生成正余弦位置编码形状为 1 x inputLen x numHiddenUnits function posEnc sinusoidalPositionEncoding(inputLen, numHiddenUnits) posEnc zeros(inputLen, numHiddenUnits); for pos 1:inputLen for i 1:floor(numHiddenUnits/2) angle pos / (10000 ^ ((2*i) / numHiddenUnits)); posEnc(pos, 2*i-1) sin(angle); posEnc(pos, 2*i) cos(angle); end end posEnc reshape(posEnc, 1, inputLen, numHiddenUnits); % 从 [seqLen, hid] 转为 [1, seqLen, hid] posEnc dlarray(posEnc, SCB); % 标记为 S:时间 C:通道 B:批量 enddlarray 的维度标签在 MATLAB 中至关重要。SCB 表示第一个维度是时间步 S第二个是通道 C也就是变量或特征第三个是批量 B。LSTM 输出格式是 S×C×B位置编码必须匹配这个维度顺序。代码里 reshape 这一步容易写成 [inputLen, 1, numHiddenUnits]那样维度标签就全乱了。前向传播时直接在损失函数里把 posEnc 加到 LSTM 输出上function [loss, grad] modelLoss(dlnet, X, Y, horizon, inputLen, numHiddenUnits) % X 是原始输入序列 dlarray (time × feature × batch) % Y 是目标输出 dlarray (feature × horizon × batch) lstmOut forward(dlnet, X); % 这块中间层输出需要额外处理见下方说明 % 实际做法是拆开网络分别前向完整代码见配套 posEnc sinusoidalPositionEncoding(inputLen, numHiddenUnits); stackedOut lstmOut posEnc; % 广播相加 % 后续接 transformer 的前向 pred fullyConnect(stackedOut); % 示意 loss mse(pred, Y); grad dlgradient(loss, dlnet.Learnables); end这里必须说明一个我踩过的坑直接用forward(dlnet, X)会走完整条网络到全连接层无法在中间插入位置编码。解决方法是把模型拆成两段第一段是 LSTM第二段是 Transformer 加全连接分别用dlnetwork的子网络提取参数或者在构建网络时用layerGraph把 Transformer 和输出层单独组装LSTM 单独组装前向时先过 LSTM加位置编码再过 Transformer。代码里注释掉的那行就是提醒你注意这个结构。4. 不收敛、过拟合与局部极小训练参数与超参数调优清单4.1 训练循环中的梯度裁剪和早停设置Transformer 的残差连接和前馈网络对梯度幅值很敏感稍大的学习率或者数值稍大的输入就容易造成梯度爆炸表现为某个 epoch 的 loss 突然变成 NaN。Seq2SeqRNN 部分虽然 LSTM 已经内建梯度裁剪机制但 MATLAB 中 LSTM 的梯度裁剪并不覆盖 Transformer 层。自定义训练循环里必须显式做全局梯度裁剪。下面给出实际可用的训练循环骨架。options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MaxEpochs, 80, ... MiniBatchSize, 32, ... GradientThreshold, 1.0, ... Shuffle, never, ... Verbose, true); % 如果你的网络可以整体过 trainNetwork直接用 % net trainNetwork(XTrain, YTrain, layers, options); % 但自定义循环写法如下 learnRate 1e-3; gradDecay 0.9; gradDecaySq 0.999; averageGrad []; averageSqGrad []; for epoch 1:80 iteration 0; for i 1:numIterations [XBatch, YBatch] getBatch(trainNorm, i); [loss, grads] dlfeval(modelLoss, dlnet, XBatch, YBatch); [dlnet, averageGrad, averageSqGrad] adamupdate(dlnet, grads, ... averageGrad, averageSqGrad, iteration1, learnRate, gradDecay, gradDecaySq); end valLoss computeValLoss(dlnet, valNorm); fprintf(Epoch %d, TrainLoss%.4f, ValLoss%.4f\n, epoch, loss, valLoss); enddlfeval是 MATLAB 深度学习自定义训练循环的入口modelLoss 函数内部用dlgradient求梯度。adamupdate 直接更新 dlnet.Learnables与优化器相关的历史梯度由 averageGrad 和 averageSqGrad 两个变量保持。注意Shuffle设置成 never对时间序列预测这几乎是强制要求随机打乱批顺序等于把未来的数据泄漏到训练过程中的每个 batch 里。关于早停不建议只看验证 loss多变量预测中验证 loss 的震荡通常比单变量大。我会把早停条件设置为“验证 loss 连续 10 个 epoch 不更新全局最优则停止”并且把最优那轮的模型参数用dlnet.save单独存下来。这个策略能有效防止 Transformer 在训练后期把注意力权重积累到对训练集过拟合的状态。4.2 超参数表从初始值开始不以论文默认值结尾超参数初始参考值调整方向影响表现numHiddenUnitsLSTM128过拟合或欠拟合时增减决定 Transformer 输入特征维度也决定模型容量numHeads注意力头数4输入窗口长或变量多可以调到 8头数太多会稀释每个头的梯度FeedForwardDimensions512数据量大可以到 1024影响非线性变换容量但成倍增加参数AttentionDropout0.1过拟合时提到 0.2抑制注意力层的过拟合InitialLearnRate1e-3不收敛降到 5e-4Transformer 对学习率比 LSTM 更敏感MiniBatchSize32显存不足或序列长降到 16影响 loss 平滑度LSTM 隐藏单元数与 Transformer 的 ModelDimension 绑定所以这条参数牵一发动全身。我一般先把隐藏单元数定为 128训一版看损失曲线再逐步调。numHeads 与序列长度没有必须整除的关系但建议 numHeads 能整除 ModelDimensionMATLAB 的 transformerLayer 内部做多头拆分时要求 ModelDimension 能被 NumHeads 整除否则直接报错。训练开始时关注的最重要信号不是训练 loss而是训练 loss 与验证 loss 的差距。第 5 个 epoch 附近如果验证 loss 不降但训练 loss 下降很快大概率是位置编码没有加上或者 Dropout 设了 0。Transformer 需要位置编码的程度比许多人设想的要高尤其是在输入序列长度超过 64 时去掉位置编码的模型只能学到时间步的顺序统计特征无法区分具体是第几个时间步。4.3 维度不匹配与 grad 为 NaN 的排错顺序维度不匹配是初版代码最常见的问题。LSTM 的OutputMode如果设成 last输出是 C×B 的向量Transformer 需要 S×C×B 的序列输入就会报“输入尺寸不一致”。这种错误很容易被当成模型结构写错了实际只是字符串大小写和输出模式的问题。解决方法是把 LSTM 的 OutputMode 改为 sequence然后在需要整体语义向量的地方手动取lstmOut(:, end, :)。梯度中出现 NaN 的排查顺序我固定按三步来第一步检查输入数据有没有 NaN 或 Inf数据归一化之后即便原始数据没问题也可能因为某些列标准差为 0 而产生 NaN第二步检查位置编码的形状posEnc 如果有维度是输入序列长度的一半广播相加时会静默地变成重复拼接不报错但梯度异常第三步看学习率Transformer 层在 1e-2 学习率下几乎必炸这是最常被忽略的原因。按这个顺序排查绝大多数神经网络训练的疑难杂症都能定位到具体原因。5. 验证预测效果的窗口外推与变量归因检查5.1 按时间顺序评估禁止乱序交叉验证时间序列模型验证的一个行业共识是不能随机打乱样本做 k 折交叉验证因为相邻时间点的样本高度相关打乱后模型相当于见到了“未来的邻近点”验证分数会比线上真实表现高很多。这个模型的验收入口应该是一次训练多个不同起点的滚动预测。testRMSE zeros(size(testNorm, 2), 1); testMAPE zeros(size(testNorm, 2), 1); for v 1:size(testNorm, 2) pred predictRolling(dlnet, testNorm, inputLen, horizon); truth testNorm(:, v); err pred - truth; testRMSE(v) sqrt(mean(err.^2)); testMAPE(v) mean(abs(err) ./ abs(truth eps)); endpredictRolling函数模拟的是真实业务中“拿到最近 inputLen 个观测点预测未来 horizon 个点”的流程。第一轮用测试集最前面的 inputLen 个点做输入预测后续 horizon 个点第二轮输入窗口向后滑动一小步继续预测。注意预测的 horizon 窗口内部不代入真实值只使用模型前一步的输出作为输入这叫做自回归预测。如果每一步都把真实观测值喂进去验证的就不是系统在真实环境中的性能而是理想条件下的上界。5.2 与基线 LSTM 对比时观察三个时段一个 Seq2SeqRNN-Transformer 模型值不值得保留要跟普通 Seq2Seq LSTM 或者其他基线对照。我会把预测误差按时间分成三段看输入窗口末尾附近、窗口外推到中期、长期预测段。Transformer 编码器的优势主要出现在第三段也就是预测步长超过输入序列长度 1/3 之后。前两段 LSTM 本身表现就尚可差距不明显甚至因为 Transformer 参数更多、训练难度大短期预测误差可能略高。对比时画一张时间轴折线图横轴是测试集中的时间点纵轴是每个变量的预测值。这张图的作用是直接暴露有没有相位偏移问题。如果注意力头数过多模型可能学到两三个点之间的局部抖动表现为预测曲线比真实曲线平滑度差很多震荡明显如果 FeedForwardDimensions 过大而训练数据不足预测曲线会整体偏保守趋向于预测平均值附近的区域。这两种情况单看 RMSE 或者 MAPE 都不容易发现必须看曲线形态。5.3 用输入扰动法近似观察变量归因Transformer 编码器的一个实用好处是可以做变量归因分析理解模型到底在依赖哪些变量之间的交互。虽然 MATLAB 的 transformerLayer 不直接导出注意力权重我们可以用输入扰动法做近似归因把测试集中某一个变量的所有观测值替换成该变量的均值观察预测误差变化量变化量越大说明模型对这个变量的敏感性越高。function sensitivity variableSensitivity(dlnet, X, Y, v, mu, sigma) % 把第 v 个变量替换成均值看看预测误差变大多少 X_pert X; X_pert(v, :, :) mu(v); pred_orig predict(dlnet, X); pred_pert predict(dlnet, X_pert); err_orig mean(abs(pred_orig - Y), all); err_pert mean(abs(pred_pert - Y), all); sensitivity (err_pert - err_orig) / (err_orig eps); end这里的可解释性结论只适合放在项目中作为“模型行为记录”不能直接当成因果结论。它回答的是“模型在这个数据集上学到的变量依赖权重”而不是“变量之间真实存在什么物理关系”。我通常会把每个变量的敏感性按从大到小排序与业务侧的变量重要性预期对照一次能得到模型是否在学习符合常识的关系的证据。这个检查也能帮你定位一种冷门失效模式如果某个理论上本该重要的变量敏感性几乎为零往往是数据的单位或者归一化过程有问题模型找到了绕过这个变量的捷径。最后如果你要在实际系统中用这个模型建议加一个 Walk-Forward 验证脚本每个月末用截至当月的全部数据重新训练一遍并在下一个月的数据上做一次完整评估日志里记录模型版本和数据版本避免半年之后说不清楚线上模型是在哪份数据上训练出来的。本文还有配套的精品资源点击获取
返回列表