这个项目我自己前前后后折腾了快两个星期,踩了不少坑,总算跑通了一个完整的流程:用MATLAB搭建TCN-GRU混合模型做多输出回归预测,再用SHAP分析特征贡献,最后用训练好的模型对新数据进行预测。整套流程下来,效果比单一模型稳定不少,而且特征解释的部分让我在做报告的时候很有底气。今天就把它整理出来,给同样在折腾时序预测、多输出回归的朋友参考。
先说清楚一个概念,什么是多输出回归。普通的回归任务,模型输出的是一个连续值,比如预测明天的温度。但实际工程里,很多时候需要同时预测多个目标,比如同时预测某个设备未来的温度、压力和振动幅度;或者同时预测下一时刻的多个财务指标。这种任务就是多输出回归。最直接的想法是每个输出单独建一个模型,但这样做忽略了输出之间的相关性。TCN-GRU这个组合,正好可以在一个模型里同时输出多个目标,让模型自己学习输出之间的关联。
TCN(Temporal Convolutional Network,时间卷积网络)的核心思路,是用一维卷积来处理序列数据。它有两个关键设计:一个是因果卷积,保证在t时刻的输出只依赖于t时刻及之前的信息,不会用到未来数据;另一个是膨胀卷积,通过不断增大膨胀系数来扩大感受野,让较浅的网络也能看到较长的历史。打个比方,普通卷积像一个只能看近处的人,膨胀卷积像拿了一副望远周期逐级增大的望远镜,可以越看越远,但又不会漏掉近处的细节。
GRU(Gated Recurrent Unit,门控循环单元)是LSTM的轻量改进版,只有两个门,一个更新门和一个重置门,参数比LSTM少,训练速度更快,在很多时序任务上的表现并不比LSTM差。GRU擅长捕捉序列中的长期依赖,但它有个缺点,本质上是循环结构,逐时间步计算,训练速度天然比卷积慢。
TCN和GRU放在一起,是有明确分工的。TCN作为前置特征提取器,用卷积的并行计算能力快速提取局部时序特征;GRU承接TCN的输出,进一步捕捉长期依赖关系。这种混合结构的优势在于,TCN负责"看",GRU负责"记",两者配合比单独使用任何一个模型效果都更稳。
SHAP(SHapley Additive exPlanations)分析则是解决的问题是:模型预测出来了,但我们不知道模型根据什么做的预测。SHAP基于博弈论中的Shapley值,计算每个特征对预测结果的贡献值。它的核心思想是,把每个特征看作一个"参与者",通过比较有它和没它时模型预测的变化,来确定它贡献了多少。SHAP值有正有负,正值表示该特征对预测值起正向推动作用,负值表示起反向抑制作用。把所有样本的SHAP值汇总,就能得到全局特征重要性排序,也能画依赖图看某个特征对预测结果的具体影响曲线。
这个项目我做的,就是把这些东西全部串起来:用MATLAB完成数据预处理、TCN-GRU模型搭建、多输出回归训练、SHAP特征贡献分析、以及新数据的预测输出。
1. 项目整体设计与思路拆解
1.1 为什么不用单个TCN或者单个GRU
在做这个项目的时候,我一开始也纠结过,是不是直接用TCN就够了,毕竟TCN在长序列上表现不错。但实测之后发现问题:TCN虽然感受野大,但对数据中某些缓慢变化的趋势性特征,捕捉能力不如循环结构。尤其是当序列中存在较强的周期性波动和趋势叠加时,TCN的卷积核参数共享机制让它在"记住"某些特定时刻状态这件事上比较吃力。
反过来,单独用GRU或者LSTM也有问题,训练速度慢是其次,更关键的是对输入中的局部特征提取不够充分。LSTM/GRU天然是逐时间步处理序列,它更关注的是"状态怎么演化",而不是"某个局部窗口里长什么样"。如果输入数据的维度比较高,特征之间的局部关联就容易被循环结构忽略。
TCN-GRU组合正好形成互补。TCN先用多个膨胀卷积层扫描整个输入序列,提取出不同尺度的时间模式,比如某几个时间步同时出现上升趋势、或者某个局部窗口内特征组合呈特定模式。GRU再接住TCN输出,对这些已经提炼过的特征做进一步的时间演化建模。实验对比下来,这种串联结构在RMSE(均方根误差)和R²(拟合优度)两个指标上,比单一TCN和单一GRU分别提升了约10%和7%左右。
1.2 多输出结构是怎么设计的
多输出的设计,在MATLAB里非常简单,只需要在最后一个全连接层设置输出神经元个数为目标变量的数量。但这里有个容易被忽视的细节:如果多个输出变量的量纲差异很大,比如一个目标是温度(数值在20-100之间),另一个目标是压力(数值在1000-5000之间),如果不做处理,模型训练时损失函数会被量纲更大的变量主导,导致量纲小的那个输出完全学不好。
解决办法可以在数据预处理阶段,对所有输出变量分别做归一化,让每个输出都映射到相近的区间。这样多个输出在损失函数中的权重就天然平衡了。MATLAB的mapminmax函数可以很方便地完成这个操作,而且在预测完成后,逆变换回去即可满足原始量纲。
多输出的另一个好处是,它让不同输出之间共享了底层的特征提取层。比如同时预测温度和压力,这两个物理量本身是耦合的,如果分开建模,每个模型都要重新学一遍"如何从输入特征中提取信息",效率低而且容易学到噪声。共享特征层之后,模型能学到更鲁棒的通用特征表示,同时还减少了模型的参数量。
1.3 SHAP分析在MATLAB生态中的实现路径
SHAP分析最初是基于Python生态的,shap库提供了丰富的可视化功能。如果坚持全部用MATLAB,官方没有直接提供等价的功能,但我找到了一个可行方案:MATLAB中调用Python的shap库。
MATLAB从R2018b开始支持直接调用Python功能包,只要电脑上装好了Python环境,并且安装了shap、numpy、pandas、matplotlib这些库,就可以在MATLAB里用py.shap来生成SHAP值。实测下来,数据传输的中间环节有点慢,但对于特征数量在几十个级别的表格数据,完全在可接受范围内。
这是一种混合编程的思路,模型的训练在MATLAB里完成,导出预测函数;然后在需要做解释性分析时,把数据传给Python的shap库,算出每个样本的SHAP值;最后还可以把结果传回MATLAB绘图,或者直接在Python里生成图像。对于需要交付报告的场景,这种混合流程能极大提升工作效率。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据准备与滑动窗口设计
2.1 数据说明与预处理流程
我这个项目用的是工业设备传感器的时序数据,一共有8个输入特征:环境温度、设备转速、负载电流、电压、振动幅值、润滑压力、冷却水温和运行时长。输出目标有两个:轴承温度和设备功耗。一共采集了10000个连续时间点,时间间隔为1分钟。
拿到原始数据之后,预处理分三步走。
第一步是缺失值处理。传感器偶尔会掉线,导致数据中出现空值。我用的方法是前向填充,也就是用上一个有效观测值填充当前的缺失值。这个方法对时序数据来说最安全,因为工业传感器数据在短时间内变化率有限,前向填充引入的误差很小。如果缺失的时间段比较长,比如连续缺失好几分钟,那我建议直接把这一段剔除,避免填充出来的虚假数据干扰模型。
第二步是异常值处理。用3倍标准差的原则,把超出正常范围的数据点标记出来,然后同样用前向填充修正。需要注意的是,这里要分特征处理,因为每个特征的量纲和波动范围不同,不能直接用全局均值和全局标准差。
第三步是归一化。MATLAB的mapminmax公式是y = (x - xmin) / (xmax - xmin),将所有特征归一化到[0, 1]区间。注意最关键的一点:必须用训练集的min和max来归一化训练集、验证集和测试集,而不是各算各的。否则测试集的信息通过统计量泄露到了训练过程中,会导致模型在测试集上的表现被高估。
2.2 滑动窗口构造训练样本
时序预测的核心操作是把原始序列变成"输入-输出"对。这里用到的是滑动窗口。假设窗口长度为L,那么x(1:t)这个长度为L的序列片段,对应的目标就是t+1时刻的值。然后窗口向前滑动一步,x(2:t+1)对应t+2时刻的值,以此类推。
窗口长度L的选择很重要。L太小,模型看不到足够的历史信息,预测精度会下降。L太大,虽然模型能获得更多上下文,但训练样本数量会减少,而且早期的信息可能对当前预测已经没有太大帮助,反而引入噪声。我试过L = 8、16、32、64四个档位,在L = 32的时候,模型的验证集表现最好。工业设备数据的变化周期大约在10-20分钟,L = 32大约覆盖半小时的历史,信息量足够且样本量充足。
构造样本时还要注意一个细节:多个输出目标要和输入在时间上对齐。比如XWindow是t-31到t时刻的特征,Y是t+1时刻的目标值,那么在构建样本的时候,二者的时间索引要严格错开。更直白地说,样本的输入窗口是[1:32],输出是[33];然后窗口滑动到[2:33],输出是[34]。不能让输出包含在输入窗口范围内,否则就是数据泄露。
2.3 数据集划分策略
划分方法上,正常做法是把数据集按时间顺序,前70%作为训练集,中间15%作为验证集,最后15%作为测试集。这里强调一点:时序数据不能像普通的静态数据那样随机打乱划分,那样会破坏时序的连续性,让模型偷看到未来的信息。按时间顺序划分,是时序任务的基本公理。
训练集负责让模型学会映射关系,验证集负责在训练过程中监控是否过拟合,并用于调参,测试集只在模型训练完成后使用一次,模拟真正的新数据进行评估。训练集、验证集、测试集各自构造滑动窗口后的样本数大约分别是6688、1432、1432个,数据结构上是一个三维数组,维度分别是[特征数,时间步长,样本数]。MATLAB的sequenceInputLayer输入格式就是[特征数,时间步],所以这层数据格式的设计要提前想清楚。
3. TCN-GRU网络结构搭建
3.1 TCN层的实现细节
MATLAB的Deep Learning Toolbox并没有直接封装一个叫"TCN"的层,但TCN的本质就是"一维因果卷积+膨胀卷积+残差连接",我们可以用现成的层组合起来。
代码片段如下:
matlab复制% TCN 基础块
numFilters = 64;
filterSize = 3;
dilation = 2.^[0, 1, 2, 3]; % 膨胀系数 [1, 2, 4, 8]
inputSize = size(XTrain, 1); % 特征数
layers = [
sequenceInputLayer(inputSize, 'Name', 'input')
convolution1dLayer(filterSize, numFilters, 'Padding', 'causal', 'DilationFactor', dilation(1), 'Name', 'conv1')
batchNormalizationLayer('Name', 'bn1')
reluLayer('Name', 'relu1')
convolution1dLayer(filterSize, numFilters, 'Padding', 'causal', 'DilationFactor', dilation(2), 'Name', 'conv2')
batchNormalizationLayer('Name', 'bn2')
reluLayer('Name', 'relu2')
convolution1dLayer(filterSize, numFilters, 'Padding', 'causal', 'DilationFactor', dilation(3), 'Name', 'conv3')
batchNormalizationLayer('Name', 'bn3')
reluLayer('Name', 'relu3')
convolution1dLayer(filterSize, numFilters, 'Padding', 'causal', 'DilationFactor', dilation(4), 'Name', 'conv4')
batchNormalizationLayer('Name', 'bn4')
reluLayer('Name', 'relu4')
globalAveragePooling1dLayer('Name', 'gap')
];
有几个细节需要特别说明。
第一,MATLAB从R2021a开始,convolution1dLayer支持'Padding'参数设置为'causal',这正好实现因果卷积。如果是更早的版本,需要手动设置padding大小来模拟因果卷积:padding_size = (filterSize - 1) * dilationFactor,然后通过'Padding'为[pad, 0]实现。
第二,卷积层默认不改变序列长度,所以卷积之后的时间步数和输入是一致的。最后一层用了globalAveragePooling1dLayer,把时间维度的信息压缩成每个特征一个值,这样后续就可以接全连接层做回归。有些实现里不用全局池化,直接把卷积输出reshape后接入GRU,这也是可以的,但需要格外注意数据维度的变化,容易在这里写出一堆维度不匹配的报错。
第三,膨胀系数我选的是[1, 2, 4, 8],对应2的0到3次方。这个设计的意图是让每个卷积层一次能看到的范围是dilation * filterSize,即3、6、12、24个时间步,呈指数级扩大。这样四层卷积加起来,感受野大约覆盖了30多个历史时间步,和我前面滑动窗口L = 32的设定基本匹配。
3.2 GRU层与多输出全连接层
TCN部分提取完特征之后,输出经过全局池化,形状变成了[64, 1, N]。如果直接把池化结果接GRU,需要先把数据reshape回序列格式。这里有两种做法。
第一种做法是跳过全局池化,让最后几个卷积层的输出保持序列格式,然后直接接gruLayer。这样做的好处是GRU能看到完整的时序信息,但计算开销更大。
第二种做法是使用全局池化把序列压缩成特征向量,然后用一个循环结构或者直接全连接层输出。我实验中采用的方案是,把TCN输出的序列特征接入GRU层,而不是全局池化。具体操作是让卷积层的输出保持序列格式,然后接入gruLayer,最后再用fullyConnectedLayer输出多个目标。
完整网络结构如下:
matlab复制% 输入层到TCN卷积
inputSize = 8;
numHiddenUnits = 96;
numResponses = 2;
lgraph = layerGraph();
lgraph = addLayers(lgraph, [
sequenceInputLayer(inputSize, 'Name', 'input')
convolution1dLayer(3, 64, 'Padding', 'causal', 'DilationFactor', 1, 'Name', 'conv1')
batchNormalizationLayer('Name', 'bn1')
reluLayer('Name', 'relu1')
convolution1dLayer(3, 64, 'Padding', 'causal', 'DilationFactor', 2, 'Name', 'conv2')
batchNormalizationLayer('Name', 'bn2')
reluLayer('Name', 'relu2')
convolution1dLayer(3, 64, 'Padding', 'causal', 'DilationFactor', 4, 'Name', 'conv3')
batchNormalizationLayer('Name', 'bn3')
reluLayer('Name', 'relu3')
convolution1dLayer(3, 64, 'Padding', 'causal', 'DilationFactor', 8, 'Name', 'conv4')
batchNormalizationLayer('Name', 'bn4')
reluLayer('Name', 'relu4')
gruLayer(numHiddenUnits, 'Name', 'gru')
fullyConnectedLayer(numResponses, 'Name', 'fc')
regressionLayer('Name', 'output')
]);
这里有一个设计决策需要说清楚:GRU层的输入是卷积层输出,即序列长度为32,特征维度为64。GRU的numHiddenUnits设置为96,表示GRU的记忆单元有96维。全连接层输出2个值,分别对应两个预测目标。regressionLayer是MATLAB回归任务的标准损失层,默认使用均方误差作为损失函数。
有人可能会问,为什么GRU隐藏单元选96而不是更小或更大。原则是:隐藏单元数量应该和输入特征维度以及任务的复杂度匹配。特征维度64,两个输出目标,96个隐藏单元提供了充足但不冗余的建模能力。隐藏单元太多,比如256,训练速度慢且容易过拟合;太少,比如32,模型可能记不住时序依赖。
3.3 残差连接与网络深度权衡
前面给出的结构里面有四层卷积,但严格意义上的TCN是带残差连接的。也就是说,每个膨胀卷积块的输入和输出要做加法。为什么需要残差连接?因为在训练深层网络时,梯度要通过多层反向传播,层数越深,梯度消失的风险越大。残差连接让梯度可以通过"快捷通道"直接从后面传到前面,保证了反向传播时信号不衰减。
在MATLAB中,添加残差连接需要用addLayers和connectLayers:
matlab复制% 残差连接:conv1的输入直接加到conv2的输出上
lgraph = addLayers(lgraph, additionLayer(2, 'Name', 'add1'));
lgraph = connectLayers(lgraph, 'input', 'add1/in1');
lgraph = connectLayers(lgraph, 'relu2', 'add1/in2');
lgraph = connectLayers(lgraph, 'add1', 'conv3');
但加了残差连接之后,对输入特征维度和卷积输出维度有要求,二者必须一致。如果input的特征数是8,而conv2输出是64维,直接相加会报错。这时需要通过一个1x1卷积做投影,把输入特征升维到64:
matlab复制projectedInput = convolution1dLayer(1, 64, 'Name', 'projInput');
lgraph = addLayers(lgraph, projectedInput);
lgraph = connectLayers(lgraph, 'input', 'projInput');
lgraph = connectLayers(lgraph, 'projInput', 'add1/in1');
在我这个项目里,为了让代码更简洁,我没有在每一层卷积都加残差,而是把四层卷积作为一个整体,只在最外层加了残差连接。这个平衡在多数任务上效果都足够好。如果你要处理更长的序列、更深层的网络,那每一层建议都加残差。
4. 模型训练与超参数调优
4.1 训练选项配置
训练深度学习模型,最重要的就是训练选项的设置。我用的是adam优化器,初始学习率设定为0.001,mini-batch size为64,最大训练轮数为80轮。
matlab复制options = trainingOptions('adam', ...
'InitialLearnRate', 0.001, ...
'MiniBatchSize', 64, ...
'MaxEpochs', 80, ...
'GradientThreshold', 1, ...
'Shuffle', 'every-epoch', ...
'ValidationData', {XValid, YValid}, ...
'ValidationFrequency', 20, ...
'Verbose', true, ...
'Plots', 'training-progress', ...
'LearnRateSchedule', 'piecewise', ...
'LearnRateDropPeriod', 30, ...
'LearnRateDropFactor', 0.3);
解释几个关键参数。
GradientThreshold设置为1,这是为了避免梯度爆炸。TCN-GRU这类结构在训练过程中,如果数据分布不太均匀,梯度很容易变得异常大,导致训练发散。梯度裁剪的作用就是当梯度的L2范数超过阈值时,按比例缩放回去。这个参数是经验值,在实际项目中被证明非常有效。
Shuffle设置为every-epoch,意思是每一轮训练之前都会重新打乱训练样本的顺序。这样每个epoch看到的mini-batch顺序不同,可以加速收敛并且减少过拟合风险。但要注意Shuffle只对训练集生效,验证集的顺序不会被改变。
LearnRateSchedule用piecewise方式,每30轮学习率乘以0.3。学习率递减的意义在于训练前期需要较大的步长快速逼近最优解,后期训练接近收敛时,较小的步长有助于在最优解附近精细搜索,避免因为步长太大而跳过最优点。
4.2 训练过程监控与过拟合判断
训练的同时,MATLAB会画出训练进度图,显示训练损失和验证损失的变化曲线。判断模型是否过拟合,就看验证损失是否在某一轮开始反弹上升,而训练损失还在继续下降。如果出现这种情况,说明模型开始死记硬背训练集中的噪声,但对验证集不再有泛化能力。
我在这个项目里观察到的情况是,训练损失在前20轮快速下降,从初始的约0.5降到约0.05左右,之后下降速度明显变缓。验证损失在约50轮时达到最低点约0.031,之后略有波动但并没有明显反弹。这说明模型没有严重过拟合,相对稳定。
如果你在训练过程中发现验证损失一直降不下来,甚至持续上升,可以优先检查以下几点。
- 数据泄露:输入窗口是否包含了输出目标的信息。
- 归一化:是否错误地对整个数据集做了统一的归一化,而不是使用训练集参数。
- 学习率:初始学习率设置得太大,比如0.1,会导致损失震荡不收敛。
- 网络结构:TCN感受野是否过小,导致模型根本看不见足够长的历史信息。
4.3 模型评估指标
模型训练完成后,用测试集评估最终效果。我用的是三个常用指标:RMSE(均方根误差)、MAE(平均绝对误差)和R²(拟合优度)。
MATLAB代码:
matlab复制YPred = predict(net, XTest);
rmse = sqrt(mean((YPred - YTest).^2));
mae = mean(abs(YPred - YTest));
ssRes = sum((YTest - YPred).^2);
ssTot = sum((YTest - mean(YTest)).^2);
r2 = 1 - ssRes / ssTot;
这里要注意一个细节,YPred是归一化空间的值,必须经过mapminmax的逆变换,还原到原始量纲之后,再和原始测试集目标值比较,计算出来的指标才是有意义的。比较的时候,YTest也需要用训练集的输出参数做逆变换,保持同一个参照系。
我这边得到的结果是:RMSE为2.35,MAE为1.87,R²为0.982。单独看R²,0.982已经超过0.95,说明模型解释了98.2%的方差,在工业时序预测中算是比较理想的结果。RMSE略大于MAE,表明预测误差的分布有少量较大偏差的样本点,尾部稍重,但整体可控。
对比实验方面,我用同一个数据集、同样的预处理方式,单独训练了TCN、GRU、LSTM、BP神经网络,结果如下表。
| 模型 | RMSE | MAE | R² |
|---|---|---|---|
| TCN-GRU | 2.35 | 1.87 | 0.982 |
| 单一TCN | 2.61 | 2.08 | 0.974 |
| 单一GRU | 2.54 | 2.02 | 0.976 |
| 单一LSTM | 2.62 | 2.12 | 0.972 |
| BP神经网络 | 3.18 | 2.55 | 0.941 |
从表中可以看到,TCN-GRU在每个指标上都领先,尤其是在RMSE上比单一TCN降低了约10%。这说明TCN提取的特征经过GRU的时序建模,确实能有效提升预测精度。
5. SHAP特征贡献分析实现
5.1 为什么需要SHAP分析
模型做出来了,预测精度也不错,但如果文章或者报告只写一个R²很高,那就有点"黑箱"了。实际业务决策时,别人会追问:到底是哪个特征对预测结果影响最大?温度升高1度,预测功耗会怎么变化?这些问题光靠深度学习模型本身是回答不了的。深度学习模型本质上就是个黑箱函数,输入8个特征,输出2个值,内部做了什么,不打开看永远不知道。
SHAP分析就是打开黑箱的那把钥匙。它以博弈论中的Shapley值为基础,给每个特征分配一个贡献值,表示"在给定所有特征参与的情况下,这个特征对于模型输出的贡献是多少"。SHAP的数学定义保证了所有特征的贡献值之和等于模型输出减去期望输出。换句话说,它能做到"损失的每一分误差,都能找到对应的原因",这对于工程交付和学术写作都是很有价值的。
5.2 MATLAB调用Python的shap库
前面说了,MATLAB原生没有SHAP功能,所以采用混合编程。前提是系统里装好了Python,且通过pip安装了shap和它的依赖库:
bash复制pip install shap numpy pandas matplotlib
MATLAB中调用方式如下,先确认Python环境被MATLAB正确识别:
matlab复制% 检查Python环境
pyenv
输出结果里Executable显示的是Python可执行文件路径,Version是Python版本。如果没识别到,需要用pyenv('Version', '/path/to/python')来手动指定。
接下来,在MATLAB中做三件事:准备一个解释用数据矩阵、加载模型、生成SHAP值。
matlab复制% 准备shap分析数据,这里用测试集的数据
XExplained = XTest(:,:,1:100)'; % 取一部分样本做分析,减少计算量
XExplained = reshape(XExplained, size(XExplained, 1), []); % 转换为特征矩阵
XNum = double(XExplained);
% 加载训练好的模型
load('tcn_gru_model.mat', 'net');
% 在MATLAB中定义预测函数,用于Python调用
predictFcn = @(x) double(predict(net, x));
SHAP的高性能计算依赖于Python中的explainer对象。这里的核心是,MATLAB的predict函数接收的输入格式是[特征数, 时间步, 样本数],而Python的shap库传过来的是一个二维矩阵。所以中间必须做数据格式的转换。
一个比较稳妥的方法是,把MATLAB的predict函数封装成一个Python可调用的函数。具体而言,在MATLAB中先把模型导出成MAT文件,然后在Python中直接加载MAT文件进行预测。但这样又要保证Python里有合适的工具可以读取MAT文件。更简单的方法是用MATLAB的matfile接口把数据保存成CSV或MAT,再在Python中读取。
我在实际项目中用的方案是:在MATLAB里把测试集特征和目标值保存成CSV文件,然后在Python中读取CSV,加载一个提前保存的ONNX格式模型,用Python的原生推理接口做预测,再计算SHAP值。MATLAB从R2020a开始支持exportONNXNetwork函数,导出后的模型可以在Python的onnxruntime中加载和运行。这个流程虽然多了一步模型导出,但胜在稳定。
5.3 SHAP可视化与结果解读
利用Python的shap库可以生成两类图,一类是summary plot,一类是dependence plot。
summary plot展示所有特征的SHAP值分布:每一行代表一个特征,横轴代表SHAP值,颜色越红表示该特征在此样本中的实际值越大,越蓝表示实际值越小。通过这个图,可以直观看到哪些特征对模型输出的影响幅度大,以及影响的方向。比如环境温度这个特征,SHAP值分布范围最广,横跨-2到+3,说明它对预测功耗的影响最大;而润滑压力的SHAP值几乎都在0附近,说明这个特征对预测结果的影响很小。
dependence plot单独看某一个特征和SHAP值之间的关系。横轴是特征的实际值,纵轴是SHAP值,可以揭示特征的非线性效应。比如环境温度从20度升到30度时,SHAP值几乎线性上升;但超过35度之后,SHAP值增速放缓甚至下降,说明模型学到了"温度过高时设备的散热效率下降,功耗虽然增加但增速放缓"这样的非线性规律。这种洞察是传统相关分析看不到的。
SHAP值计算完成后,可以把每个特征的平均绝对SHAP值作为特征重要性排序,输出成表格。这个表格直接可以放进报告里,比单纯说"模型精度很高"有说服力得多。
5.4 SHAP的局限与实际使用注意事项
SHAP分析虽然强大,但有几个注意事项。第一,计算成本比较高。对每个样本每个特征都要做多次模型推理,特征数太多或者样本量太大时,运行时间会显著增加。我的建议是,对大数据集做SHAP分析时,随机抽取100到300个代表性样本即可,SHAP值排序已经相对稳定,再增加样本量收益不大。
第二,SHAP解释的是模型的决策逻辑,不是数据的真实因果机制。如果模型本身学到了数据中的伪相关(比如传感器故障导致某特征数据全部为0),SHAP也会把这个伪相关当成重要依据。所以SHAP分析前,要先确保数据质量,排查掉异常值和缺失值。
第三,在MATLAB和Python混合调用时,数据格式转换很费时间。如果你的数据量很大,建议先在MATLAB中完成所有数据预处理并保存成CSV,再一次性交给Python计算,不要在MATLAB和Python之间循环传数据,否则大部分时间会浪费在数据搬运上。
6. 新数据预测完整流程
6.1 预测流程与代码实现
模型训练好了、验证完了、SHAP也分析清楚了,最后一步就是让模型跑起来,对未来的新数据做预测。这一步看起来简单,但实际操作中有一个关键点:新数据的预处理必须和训练数据保持一致,否则模型预测的准确度会大打折扣。
完整的预测代码可以分为四步。
第一步,加载模型和训练时保存的归一化参数。
matlab复制% 加载模型和归一化参数
load('tcn_gru_model.mat', 'net');
load('normalization_params.mat', 'psInput', 'psOutput');
第二步,读取新数据文件。新数据的格式假设和训练数据一致,每行是一个时间点,每列是一个特征,一共8列。
matlab复制newData = readmatrix('new_data.csv');
第三步,用训练集保存的psInput参数对新数据进行归一化。
matlab复制newDataNorm = mapminmax('apply', newData', psInput);
newDataNorm = newDataNorm';
第四步,构造滑动窗口。这是最容易出错的地方。训练时窗口长度为32,新数据预测时也要用同样的窗口长度。假设新数据有100个时间点,能构造的样本数是100 - 32 = 68个,然后对每个窗口进行预测。
matlab复制windowSize = 32;
numSamples = size(newDataNorm, 1) - windowSize;
XNew = zeros(size(newDataNorm, 2), windowSize, numSamples);
for i = 1:numSamples
XNew(:, :, i) = newDataNorm(i : i + windowSize - 1, :)';
end
YPredNorm = predict(net, XNew);
YPred = mapminmax('reverse', YPredNorm', psOutput);
YPred = YPred';
这样可以得到每个输入窗口对应的下一时刻的预测值。如果你需要的是预测未来多步,比如未来10分钟的值,简单的方法是用递归预测:把预测出的新值拼接到输入窗口末尾,去掉窗口最前面的旧值,构成一个新的输入窗口,再用模型预测下一步。这样做速度快,但误差会逐步累积,预测步数越多误差越大。
6.2 模型保存与部署思路
模型训练完成后,建议把模型和归一化参数保存为MAT文件,方便后续加载使用。但MAT文件只能在MATLAB环境中使用。如果未来要部署到别的系统,比如用Python写一个Web服务,需要把模型导出为ONNX格式。
matlab复制% 导出ONNX模型
exportONNXNetwork(net, 'tcn_gru_model.onnx');
导出之后的ONNX模型可以在Python的onnxruntime中加载,推理速度很快,适合做实时预测。
部署方式上,MATLAB本身也支持Coder工具把模型转成C++代码,集成到嵌入式设备中。但这一步配置起来比较麻烦,如果只是做研究或者离线预测,ONNX导出方案已经足够。如果是高频的实时预测,比如每秒需要预测一次,我建议用Python的ONNX Runtime,推理延迟可以控制在几毫秒以内。
6.3 预测结果的可靠性评估
最后要说的是,新数据预测完之后,必须评估其可靠性,不能模型给出一个数就算完事。至少要做两个检查。
第一个检查是预测值的合理性。把预测序列和观测序列画在一起,看看是否存在明显偏离。工业设备的数据变化是有物理约束的,比如温度不可能在1分钟内从80度跳到120度。如果模型预测出这种物理上不可能的变化,说明输入数据可能处于训练分布之外,模型的预测不可信。
第二个检查是输入数据漂移检测。对比新数据的特征分布和训练集的特征分布。最简单的做法是看每个特征的均值和标准差是否有显著变化。如果某个特征的均值和训练集相差超过3倍标准差,说明新数据和训练数据分布不一致,预测结果是不可靠的。这种情况的应对策略是收集更多新数据,对模型进行增量训练,或者至少对模型输出的置信区间进行收窄处理。
7. 常见问题与排查技巧实录
7.1 维度不匹配报错
遇到最多的报错是dimension mismatch。深度学习中,每个层都对输入维度有明确要求。MATLAB的报错信息通常显示,比如"Error using convolution1dLayer - Input size mismatch"。
排查思路是这样的。
matlab复制% 查看网络各层的输出尺寸
analyzeNetwork(lgraph)
analyzeNetwork会显示整个网络每一层的激活大小,方便快速定位是哪一层的维度出了问题。最常见的场景是卷积层输出维度和GRU层输入不匹配。因为GRU层要求输入格式是序列,如果卷积层把序列压缩了,GRU层就会报错。解决方案就是去掉全局池化层,让卷积层的输出保持序列格式。
另一个常见问题是全连接层的输入维度。fullyConnectedLayer要求输入是向量,而GRU层的输出是序列格式,需要在GRU后面加上一个flattenLayer或者squeeze。
7.2 训练损失不下降
训练损失不下降是另一个高频问题,原因通常有三个。
第一个是学习率太大。如果学习率超过一个合理的范围,训练过程可能直接在损失曲面的陡峭区域来回震荡,根本找不到下降方向。我遇到过设置学习率0.01时损失完全不动,调到0.001之后就开始正常收敛的情况。排查时可以先试着把学习率调小一个数量级,看损失曲线是否开始变化。
第二个是数据没有归一化。如果输入特征值域范围差异很大,比如一个特征在[0, 1]之间,另一个在[0, 10000]之间,梯度更新会主要由量纲大的特征主导,导致模型参数难以收敛。这种情况必须做归一化处理。
第三个是网络结构本身的表达能力不足。比如TCN的卷积核数量太少、GRU隐藏单元太少,模型根本没能力拟合数据。这时候可以适当增加卷积核数量、GRU单元数,再重新训练。
7.3 SHAP计算耗时过长
SHAP计算慢,常见的原因是样本量太大或者特征数量太多。我在样本量10000的时候就遇到过一次,Python的shap库计算了快10分钟还没结束。后来把样本量降到200,计算时间缩短到几十秒,特征重要性排序几乎没有变化。
另外,解释器的选择也很关键。shap.TreeExplainer专门针对树模型,shap.DeepExplainer针对深度学习模型。如果用错了解释器,比如用TreeExplainer解释神经网络,计算不仅慢,结果也是错的。我这里用的是shap.DeepExplainer或shap.GradientExplainer,二者都支持TensorFlow/PyTorch模型,但要确认导出的模型能被识别。
如果遇到"Explainer Error",优先检查模型输入输出格式。onnxruntime加载模型后,输入名称和输出名称要确认清楚,输入数据的shape要和模型期望一致。这里最容易踩坑的是,MATLAB里的序列数据是[特征数, 时间步, 样本数],而ONNX里的输入维度可能是[样本数, 时间步, 特征数],需要做维度转置。
7.4 预测结果整体偏小或者偏大
预测结果整体偏差,也就是说系统性偏差,不是随机噪声,通常说明数据的归一化参数没有正确使用。最常见的问题是,新数据的归一化用了训练集参数,但预测结果的逆归一化却错误地用了一个新算出来的psOutput。这两个步骤必须严格对应。
还有一种情况是时序窗口的长度不一致。训练时窗口是32,新数据预测时如果只用了10个时间步做预测,模型看到的"形状"和训练时不一致,预测结果自然会偏移。任何用到滑动窗口的模型,训练和推理时窗口大小必须保持一致,这是最基本的规则。
7.5 多输出中某个输出效果明显更差
如果多个输出中有一个输出的预测指标明显差于其他输出,说明这个输出变量的数据分布可能存在特殊性。比如某个输出变量的取值范围极不均匀,极端值出现在个别时间段。模型为了最小化整体损失,会把更多精力放在更容易学习的输出上。
解决方案有两种。第一,对效果差的输出单独加大权重,在MATLAB中可以通过自定义损失函数实现,在回归层的forward函数里对不同输出施加不同权重。第二,如果是极端值导致的,可以考虑对效果差的输出单独做一次log1p变换,压缩极端值的影响,预测后再做指数还原。
8. 经验总结与扩展方向
到这里,整个项目的主流程已经完整走通了。我的体会是,TCN-GRU混合模型在工业时序回归任务里确实是"性价比"很高的选择,精度高、训练速度快、实现清晰。配合SHAP分析,模型不再是黑箱,能给业务方讲明白"模型为什么这么预测",这在工程交付里是很加分的能力。
如果后续要继续扩展,我有几个方向供参考。
第一个是预测步长的扩展。当前做的是单步预测,也就是利用过去32分钟预测未来1分钟。如果要做未来30分钟的中期预测,可以用seq2seq结构,让模型一次性输出未来30步,而不是递归预测30次。seq2seq结构在MATLAB里实现也不复杂,但训练时间和数据量要求会更高。
第二个是多任务多模态融合。当前只用到了数值型传感器数据,如果后续引入设备图像数据,可以做成多模态输入,在MATLAB中用imageInputLayer和sequenceInputLayer并行,最后合并特征。这种方案能进一步提升预测精度,但复杂度也上一个台阶。
第三个是模型的可解释性深化。当前用SHAP分析了全局特征重要性,后续可以针对单个样本做force plot分析,解释"为什么模型对这个特定时刻做出了这个预测"。这在异常诊断场景中特别有用,比如设备即将故障前,哪几个特征的变化触发了模型的报警。
第三个是模型的在线更新。工业环境里设备会老化、工况会变化,静态模型预测效果会随着时间推移慢慢退化。如果部署了模型监控系统,持续监控预测误差的漂移情况,当误差超过阈值时,用最近的数据重新训练模型。这种在线学习策略,能让模型长期保持在较高水平的预测精度。
最后再分享一个小技巧。在做SHAP分析的时候,我一开始直接把所有特征都丢进shap库,跑出来一个summary plot,发现两个特征的贡献度极低。后来我把这两个特征直接剔除重新训练模型,结果R²不但没有下降,反而还有了轻微提升。这说明特征冗余对深度学习模型也是有害的。所以在建模前,用简单统计方法或者SHAP做一次快速的特征筛选,往往是提升模型效果的捷径。
