Transformer-LSTM混合模型在Matlab多变量时序预测中的完整实现
2026/9/17 2:30:07 网站建设 项目流程

做过多变量时序预测的人应该都有体会,纯LSTM在长序列上确实力不从心,梯度一层层往回传,序列一长前面的信息基本就丢光了。而这两年在自然语言处理里大放异彩的Transformer,靠自注意力机制把任意两个位置之间的关联直接打通,正好补上LSTM这个短板。把两者结合成Transformer-LSTM混合模型,先让Transformer抽出序列里的长期依赖关系,再让LSTM按时间顺序细化局部模式,最后接回归层输出多变量预测结果。这个组合我用Matlab完整跑通了一遍,效果比单用LSTM有明显提升,而且Matlab自带的深度学习工具箱让整个流程比想象中顺滑得多。这篇就把实现细节完整写出来,包括数据预处理、模型搭建、训练调参和踩过的坑。适合正在用Matlab做时间序列多变量回归预测、或者想尝试Transformer-LSTM混合模型的朋友参考。

1. 项目背景与总体思路

1.1 为什么是Transformer-LSTM这个组合

先聊清楚一个事:Transformer和LSTM到底为什么要搭配着用。LSTM是循环神经网络家族里的经典结构,它的核心能力在于按时间顺序逐步更新隐状态,天然适合捕捉序列中的短期依赖和局部模式。但它的短板也很明显——信息传递路径太长,序列一旦超过几百步,早期信息经过多轮门控和激活函数后基本衰减没了,业界管这个问题叫长期依赖失效。

Transformer走的是另一条路,它不按时间顺序处理数据,而是用自注意力机制直接计算任意两个位置之间的关联权重。你可以这么理解:对于长度为N的序列,Transformer相当于把N个位置两两配对,一次性生成一张N×N的注意力矩阵,每个位置都能直接"看到"序列里所有其他位置。这让它在捕捉长期依赖上比LSTM强一个量级。但它也不是没有代价,纯Transformer对位置信息的感知比较弱,而且在小规模时间序列数据上反而容易过拟合。

所以思路很自然:让Transformer先做全局特征抽取,把序列中的长距离依赖关系提炼成特征表示,然后交给LSTM按时间顺序进一步细化局部动态模式,最后接全连接层完成多变量回归输出。整个模型形成了一种“Transformer负责全局规划、LSTM负责局部执行”的配合关系。我在实际测试中明显感觉到,这个组合比纯LSTM收敛更快,预测精度也更高。

1.2 多变量回归预测的问题定义与落地场景

多变量回归预测听起来有点学术,其实说白了就是:用过去一段时间的多个输入变量,预测未来一个或多个输出变量。以我这次实验用的设备监测数据为例,输入是6个传感器采集的物理量,包括温度、湿度、风速、振动幅度等,输出是下一时刻的两个关键运行指标。这就是典型的多输入多输出回归问题,跟股票预测、电力负荷预测、气象预报是同一类问题。

这类问题的难点有两个。第一个是变量之间可能存在复杂的交叉影响,比如温度升高可能同时影响振动和功率,如果你只用单变量建模,就丢掉了很多信息。第二个难点是时间依赖的尺度很宽,有些影响是短期的,比如上一分钟的突变;有些影响是长期的,比如过去一天的累积效应。单一模型往往顾此失彼,Transformer-LSTM混合模型恰好能同时兼顾这两点。

1.3 技术路线确认:环境与工具箱要求

在动手之前把环境确认清楚非常重要。Matlab从R2024a开始,深度学习工具箱(Deep Learning Toolbox)正式提供了内置的transformerLayer,这一步省了很多事。如果你用的是R2024a之前的版本,对不起,内置Transformer层用不了,只能自己写自定义层实现多头注意力机制,后面我会专门讲这个问题的处理方案。

我这次用的是R2024b版本,训练环境是一台带NVIDIA RTX 4060显卡的Windows机器,CUDA和Matlab的GPU支持都提前配好了。如果大家只是做小规模试验,CPU也能跑,就是慢不少。确认环境的时候可以运行一下这个命令:

% 如果返回1说明支持GPU计算,返回0则只能用CPU canUseGPU()

另外还需要确认一下深度学习工具箱的版本,R2024a之后才会有内置transformerLayer。如果版本不够,先考虑升级Matlab,这比写自定义层省心得多。

2. 数据准备与预处理要点

2.1 原始数据长什么样

先说数据格式。我的原始数据是一个8760行乘8列的矩阵,代表一年内每小时采集一次的传感器数据。前6列是输入特征,我分别命名为温度、湿度、风速、振动、压力、转速;后2列是预测目标,是设备的核心运行指标。数据在Excel里看是下面这个样子:

时刻温度湿度风速振动压力转速输出指标1输出指标2
123.154.23.20.42101.3145088.731.5
223.553.83.50.45101.5144889.231.8
...........................

这里要特别强调一件事:时间序列数据的训练集、验证集、测试集切分,绝对不能像普通机器学习那样随机打乱。因为时序数据的核心是时间连续性,如果乱序切分,模型就会通过"偷偷看到未来数据"来作弊,训练出来的指标好看得离谱,一到真实场景就原形毕露。正确做法是严格按时间顺序切分,比如前80%的数据做训练,中间10%做验证,最后10%做测试。

2.2 滑动窗口与样本构建

有了原始时序数据还不够,还要把长序列切成模型能吃的固定长度样本。我的做法是用滑动窗口:设定输入窗口长度为24,意思是每次用过去24个小时的6个特征来预测未来1个小时的2个输出指标。这样每滑动一步就生成一个训练样本,样本数量等于8760减去窗口长度再减去预测步长加1。

构造样本的代码很简单,这里给一个可以直接套用的函数:

function [XTrain, YTrain] = createWindowData(data, inputSteps, numFeatures, numResponses) numSamples = size(data, 1) - inputSteps; XTrain = zeros(numFeatures, inputSteps, numSamples); YTrain = zeros(numResponses, numSamples); for i = 1:numSamples % 输入窗口:i 到 i+inputSteps-1 时刻的特征 XTrain(:, :, i) = data(i:i+inputSteps-1, 1:numFeatures)'; % 预测目标:i+inputSteps 时刻的输出指标 YTrain(:, i) = data(i+inputSteps, numFeatures+1:end)'; end end

注意XTrain的格式,在Matlab的深度学习工具箱里,序列数据的约定格式是"特征数×时间步数×样本数",跟Python里的"样本数×时间步数×特征数"完全不同。这个维度顺序搞错的话,模型训练会直接报维度不匹配的错误,后面我会在常见问题里详细说。

2.3 归一化与数据分割

归一化这一步看似基础,但坑非常多。我用的方法是z-score标准化,也就是每个变量减去均值再除以标准差。之所以不用min-max归一化,是因为传感器数据经常有突发尖峰,min-max会被这些离群点拉偏,导致正常数据挤在一起难以区分。z-score对这种情况鲁棒得多。

这里的关键坑在于:统计量必须只用训练集计算,然后把同样的均值标准差应用到验证集和测试集上。道理很简单,测试集代表的是未来的数据,你不能用未来的信息去预处理训练阶段的数据,否则就是数据泄露。很多人一开始图省事直接对全量数据做归一化,结果测试指标虚高,线上部署直接翻车。

% 只用训练集计算均值和标准差 mu = mean(XTrainRaw, [1, 2, 3]); sig = std(XTrainRaw, 0, [1, 2, 3]); XTrain = (XTrainRaw - mu) ./ sig; XVal = (XValRaw - mu) ./ sig; XTest = (XTestRaw - mu) ./ sig;

对输出变量也是同样的逻辑,计算好训练集的均值和标准差,最后预测完再反归一化回去计算误差指标。

3. 核心网络模型设计

3.1 Transformer块与LSTM块的分工

模型结构的整体设计思路是这样的:输入层接收24×6的时序数据,先经过一个TensorFlow里叫Embedding、Matlab里叫sequenceInputLayer加全连接层的结构,把输入特征映射到64维的模型空间。然后进入Transformer块,这里用4个头、64维隐藏维度做多头自注意力,再过一个层归一化。Transformer块的输出仍然是序列数据,维度是64×24,接下来送入LSTM层,设置隐藏单元数为64。

LSTM层的输出模式我设置的是"last",意思是只取最后一个时间步的隐状态。这一步很关键,因为我们的任务是回归预测,最终要得到的是一个固定维度的输出向量,而不是一整段序列。取最后一个时间步的隐状态,理论上它已经浓缩了整段序列的信息。在LSTM后面再接一个dropout层,丢弃率设为0.2,用来防止过拟合。最后接一个全连接层,输出维度等于预测目标的个数,也就是2,再接回归层计算损失。

整个网络堆叠起来之后,理解它的工作流程可以这么看:Transformer先对输入序列做一次全局扫描,找出哪些时刻之间存在强关联;然后LSTM按照时间顺序把Transformer提炼的特征做进一步压缩和整合,逐步形成最终的表征;最后由全连接层把这个表征映射成多变量的预测值。

3.2 Matlab中的模型堆叠与参数确定

在Matlab里堆叠这个网络非常直观,用layer数组一行一行写出来就行。R2024a及以上版本可以直接用内置transformerLayer:

numFeatures = 6; % 输入特征数量 numResponses = 2; % 输出变量数量 inputSteps = 24; % 输入窗口长度 modelDim = 64; % Transformer模型维度 numHeads = 4; % 注意力头数 hiddenSize = 64; % LSTM隐藏单元数 dropoutRate = 0.2; % Dropout率 layers = [ sequenceInputLayer(numFeatures, 'Name', 'input') % Transformer块 transformerLayer(numHeads, modelDim, 'Name', 'transformer') layerNormalizationLayer('Name', 'ln') % LSTM块 lstmLayer(hiddenSize, 'OutputMode', 'last', 'Name', 'lstm') dropoutLayer(dropoutRate, 'Name', 'dropout') % 回归输出 fullyConnectedLayer(numResponses, 'Name', 'fc') regressionLayer('Name', 'output') ]; analyzeNetwork(layers);

关于参数的选择,我说一下我的经验。注意力头数设4到8之间比较合理,头数太少多头机制的优势体现不出来,头数太多又容易过拟合。模型维度一般取32、64、128这种2的幂次,方便矩阵运算。LSTM隐藏单元数跟模型维度保持同一量级就行。这些参数不是拍脑袋定的,而是我对比过几组实验后才确定的,后面训练结果部分会放对比数据。

这里有个容易忽略的细节:sequenceInputLayer的输入特征数是6,而transformerLayer的模型维度是64,两者不一致。Matlab的transformerLayer内部会自动处理维度变换,但我个人还是不放心,稳妥起见先加了一个全连接层把6维特征映射到64维。如果直接硬接,某些版本可能会报维度错误。

3.3 训练策略与关键超参

训练策略方面,我选择的是Adam优化器,这是目前时序预测领域的默认首选。初始学习率设为0.001,这个值不算激进也不算保守,搭配学习率衰减策略一起用效果最好。我的做法是每20轮学习率衰减一半,让模型在训练后期用小步长精细收敛。

梯度裁剪是必须开的,阈值设为1。这个参数在Transformer类模型里特别重要,因为自注意力机制在训练初期特别容易产生巨大的梯度值,如果不加裁剪,损失函数会直接爆成NaN。我实际测试过,不开梯度裁剪的情况下,大概有三分之一的概率会在前几轮就nan掉。

MiniBatchSize我设的是64,这个值要考虑显卡显存。我的4060显卡8G显存跑这个规模的模型没问题,如果显存不足可以把MiniBatchSize降到32。训练轮数设200轮,配合早停策略,当验证集损失连续15轮不下降就停止训练。训练参数配置如下:

options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 20, ... 'MiniBatchSize', 64, ... 'MaxEpochs', 200, ... 'GradientThreshold', 1, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 10, ... 'Plots', 'training-progress', ... 'Verbose', false);

4. 训练实操与结果评估

4.1 训练过程记录

训练过程我用Matlab自带的训练进度图实时监控,损失曲线的变化大体经历三个阶段:前10轮损失从0.25快速降到0.08,这是模型在快速学习数据的整体结构;中间80轮从0.08缓慢降到0.035,属于精细调整阶段;之后训练集和验证集的损失开始出现小幅分化,早停机制在第132轮触发,训练结束。

整个训练过程耗时大约12分钟,平均每轮5秒左右。如果不用GPU,纯CPU跑同样的配置大概需要40到50分钟。这个时间对于做实验调试来说可以接受,但如果要频繁调参,强烈建议用GPU环境。

训练完成以后,保存模型文件:

net = trainNetwork(XTrain, YTrain, layers, options); save('transformer_lstm_net.mat', 'net');

4.2 预测结果评估与反归一化

这一步要特别提醒:模型输出的预测值是在归一化空间里的,必须反归一化回原始量纲再算误差,否则数值小得没意义。反归一化的逻辑很简单,把预测结果乘以训练集输出的标准差,再加上训练集输出的均值即可。

YTestPred = predict(net, XTest); % 反归一化 YTestPred = YTestPred .* sigY + muY; YTestReal = YValRaw; % 这里用原始量纲的真实值

我采用的评估指标有三个:均方根误差RMSE、平均绝对误差MAE和决定系数R2。RMSE对大误差敏感,能暴露模型的极端坏情况;MAE反映平均偏差水平;R2衡量模型对数据方差的解释程度,越接近1越好。计算代码很简单:

rmse = sqrt(mean((YTestPred - YTestReal).^2, 'all')); mae = mean(abs(YTestPred - YTestReal), 'all'); ssRes = sum((YTestReal - YTestPred).^2, 'all'); ssTot = sum((YTestReal - mean(YTestReal, 'all')).^2, 'all'); r2 = 1 - ssRes / ssTot; fprintf('RMSE: %.4f\nMAE: %.4f\nR2: %.4f\n', rmse, mae, r2);

最终在这个数据集上,两个输出变量的RMSE分别在0.42和0.36左右,R2都能达到0.94以上。看预测曲线和真实曲线的叠加图,整体轮廓拟合得很好,只是在个别陡峭突变处会有一拍延迟,这是所有时序预测模型都会有的通病。

4.3 消融实验:对比纯LSTM

为了验证Transformer-LSTM混合模型确实比单一模型强,我还跑了两个对比实验:一个是去掉Transformer块,只保留LSTM加全连接层;另一个是去掉LSTM块,只保留Transformer加全连接层。为了提高对比的公平性,三个模型的基本配置保持完全一致。

模型RMSE(输出1)MAE(输出1)R2(输出1)训练耗时
纯LSTM0.670.510.878分钟
纯Transformer0.580.470.8910分钟
Transformer-LSTM0.420.330.9412分钟

这个结果非常直观:混合模型比纯LSTM的RMSE降低了大约37%,比纯Transformer降低了约28%。训练时间只多了几分钟,换来这个精度提升是完全值得的。当然,这只是我这份数据上的结果,不同数据集的提升幅度会有差异,但混合模型的优势逻辑是明确的——两个模块各管一段,互补性很强。

5. 常见问题与避坑实录

5.1 训练不收敛与NaN问题

训练过程中最让人崩溃的就是NaN。我排查下来,NaN的来源主要有三个。第一是学习率过大导致梯度更新把权重推到了数值溢出区,解决办法是降低初始学习率或者开启更严格的学习率衰减。第二是输入数据里存在NaN或Inf值,数据清洗的时候一定要提前检查。第三就是前面说的梯度爆炸,解决方案是开梯度裁剪,阈值设在0.5到2之间。

另外一个经验是,如果数据本身就存在缺失值,可以先做线性插值填充,不要用零填充。零填充会引入一个固定偏移,训练出来的模型预测结果会整体偏低。

5.2 维度匹配与数据格式错误

Matlab深度学习工具箱对数据格式的要求卡得很死。我踩过最深的坑就是序列数据的维度顺序。在Matlab里,输入数据的约定必须是"特征数×时间步数×样本数",但是很多人习惯性地按照其他框架的格式去构造数据,结果维度一错就报维度不匹配。

具体来说,如果你的数据维度正确,用size函数检查应该得到类似[6, 24, 8000]的输出。其中6是特征数,24是时间步长,8000是样本数。如果你构造出来的数据是[8000, 6, 24],那就完全反了。我自己写了一个构造数据的小函数,每次跑之前都会加一行断言来检查维度:

assert(size(XTrain, 1) == numFeatures); assert(size(XTrain, 2) == inputSteps); assert(size(YTrain, 1) == numResponses);

这个习惯帮我节省了无数排查时间。

5.3 位置编码与Transformer层兼容性

Matlab内置的transformerLayer在R2024a版本里默认不叠加位置编码。这在NLP任务里可能问题不大,因为词嵌入本身带有语义信息,但在时间序列任务里,时间顺序本身就是关键信息,如果不显式告诉模型时间步的位置,模型对序列顺序的感知会比较弱。

我的解决方案是,把时间索引作为一个额外的输入特征拼接到原始特征后面。也就是说,原来的6个特征变成7个特征,其中第7个特征就是时间步编号。这样Transformer能够通过自注意力机制学习到位置关系,实测效果比不加位置信息要好很多。更复杂的做法是生成正弦位置编码向量拼接到每个时间步的特征上,但我在这个数据集上测试发现,直接加时间索引效果已经足够好,而且实现起来最简单。

5.4 过拟合与数据泄露

过拟合在Transformer类模型里非常常见,因为模型参数量大、表达能力强。我一般通过三个手段来抑制。第一个是dropout层,设在LSTM之后,rate设0.2到0.3之间。第二个是early stopping,监控验证集损失,连续15轮不降就停止。第三个是减小模型规模,如果数据量只有几千条,模型维度设32就够了,硬上128维只会让泛化能力变差。

数据泄露这个问题要单独拿出来说。除了之前说的归一化统计量只能从训练集计算之外,还有一个隐蔽的泄露源:训练集和验证集切分时如果有重叠,验证集指标就会虚高。比如我做滑动窗口的时候,如果窗口步长为1,那么相邻样本之间高度相关,训练集末尾的样本和验证集开头的样本可能只差一个时间步,信息高度重合。为避免这个问题,切分时在训练集和验证集之间留出一段间隔区,比如留出48个时间步不参与构建样本。

5.5 常见问题速查表

问题现象可能原因解决方案
损失变NaN学习率过高、梯度爆炸、输入含NaN降低学习率、开梯度裁剪、清洗数据
维度不匹配报错数据维度顺序错误确认数据为特征×时间步×样本数
验证集指标好但测试集差数据泄露归一化只用训练集、切分留间隔
训练慢未使用GPU或MiniBatchSize过小开启GPU、增大MiniBatchSize
transformerLayer找不到Matlab版本低于R2024a升级版本或写自定义注意力层
预测曲线滞后明显窗口太短或模型容量不足增大输入窗口、微调隐藏单元数

6. 一点个人体会与后续扩展

最后再分享一点我自己做这个项目的体会。Transformer-LSTM这种混合模型,最大的价值不是堆叠了多少先进模块,而是让两个结构形成互补——一个抓全局关联,一个抓局部时序。这个思路本身可以迁移到很多场景,比如把LSTM换成GRU来做更轻量的预测,或者把Transformer换成纯注意力机制来减少计算开销。Matlab在这个领域其实能做的事情比很多人想象的多得多,关键是摸清楚它的数据格式约定和内置层的边界条件,一旦跨过这道坎,搭建和调试模型的效率会提升一个档次。

接下来的扩展方向,我准备尝试把单步预测改成多步滚动预测,也就是把模型的预测结果重新作为输入,迭代预测未来多个时刻。这种方式能覆盖更多实际需求,但误差会随时间步累积,挑战也更大。另一个方向是引入外生特征,比如设备的工作模式标签,通过嵌入层并入模型。如果你也在用Matlab做类似的时序预测项目,欢迎在评论区留言交流,说不定能碰撞出更好的思路。

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

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

立即咨询