新闻详情

新闻详情

首页 / 资讯中心 / 详情

MATLAB中LSTM回归预测的SHAP可解释性分析实践

发布时间:2026/8/26 9:06:34
MATLAB中LSTM回归预测的SHAP可解释性分析实践
简介机器学习模型在回归预测任务中的高精度往往伴随着“黑箱”困境尤其当业务方或评审专家追问“为何得到这个预测结果”时单纯依赖网络权重无从解释。SHAPShapley Additive Explanations基于博弈论中的Shapley值将模型预测值公平地分解到各输入特征上能够回答“哪些特征影响最大”“具体样本中特征如何推高或拉低结果”等关键问题。在时间序列场景下LSTM网络因隐状态与门控机制的复杂耦合解释难度远高于表格模型而SHAP恰好提供了适用于深度模型的近似求解路径。本文将完整梳理从数据组织、LSTM训练到MATLAB与Python协同计算SHAP值的工程链路并结合全局重要性排序、单样本力图和时间步依赖图等可视化手段帮助工程师将模型输出转化为可验证、可沟通的结论。该方案适用于负荷预测、设备状态预测等需要向论文或业务方交代特征贡献度的工业场景。 回归预测这件事很多做MATLAB的工程师都卡在同一个地方模型跑出来了精度也不差但领导或审稿人一句“为什么是这个结果”直接把人问住。我去年做一个设备状态预测项目就碰到这种情况LSTM验证集R²到了0.93结果评审会上被追问“这几个时刻预测值为什么突然跳高”我只能说神经网络学到了某种非线性关系——这话说了等于没说。后来我把LSTM和SHAP结合在一起用MATLAB做训练用SHAP做可解释分析总算能把“为什么这么预测”这件事讲清楚。这篇文章就是想把这条完整链路整理出来包括数据组织、LSTM训练、SHAP计算路径和结果解读写给同样在用MATLAB做时序回归预测、又需要向论文或业务方交代特征贡献度的读者。1. 回归预测用LSTM最难回答的往往不是精度问题1.1 黑箱模型在工程场景里的真实困境我在多个预测类项目里观察到一个共性现象模型精度只要做到一定水平业务方和评审专家对“准不准”的质疑就开始转向“为什么信你”。比如你预测某个时间点的负荷是320kW对方会问是气温主导的还是前几个小时的趋势主导的和星期几有没有关系如果是一个LSTM你基本没法从网络权重里直接读出这种归因。原因在于LSTM的隐状态是输入特征、历史信息和门控机制三者反复耦合的结果每一时刻的隐状态都携带了压缩后的历史信息这种表示方式很难用某个权重值来解释。这不是LSTM独有的问题但LSTM在时间序列预测里用得特别多而时间序列特征天然具有滞后性、相关性和重复模式解释难度比表格类特征更高。比如同样一个“温度”特征今天12点的温度和昨天12点的温度对预测的影响可能完全不同。随机森林或XGBoost还能用feature_importance_给个粗糙排序LSTM连这个接口都没有。所以在工程层面我们需要的外部解释工具必须回答三个问题哪些输入特征对预测结果影响最大具体到某一次预测哪些特征把结果推高、哪些把结果压低这种影响在不同样本和时间条件下是否稳定SHAP恰好就是围绕这三类问题设计的。1.2 SHAP来补位它到底能回答哪几个问题SHAPSHapley Additive exPlanations的核心思想来自博弈论里的Shapley值。它把每次预测当成一个“合作博弈”所有特征共同参与最终产出预测值。SHAP要做的就是公平地把这个预测值拆解到每个特征头上。公平性是关键。每个特征的贡献不能简单看“去掉它之后预测值变化多少”因为特征之间会互相影响。比如温度和湿度两个特征单独去掉温度可能影响不大但去掉温度同时去掉湿度影响却很大这个增量应该怎么分Shapley值用一种加权平均的方式解决了这个问题对特征所有可能的“加入顺序”做平均得到每个特征的边际贡献。这样算出来的值满足可加性也就是所有特征的SHAP值加起来等于该样本的预测值减去所有样本的平均预测值。数学表达上假设特征全集为N特征i的Shapley值为[ \phi_i \sum_{S \subseteq N \setminus {i}} \frac{|S|!(|N|-|S|-1)!}{|N|!} [f(S \cup {i}) - f(S)] ]这个公式在实际计算里是组合爆炸级别的N个特征就有2^N个子集组合所以SHAP在实际工程中全部使用近似算法。常见的近似算法包括Kernel SHAP、DeepExplainer、GradientExplainer和TreeSHAP。对LSTM这种深度模型DeepExplainer和GradientExplainer都是可用选项。不过直接套公式会忽略一个关键点公式里的S是“去除某个特征后的子集”。对LSTM这样的时序模型输入是二维的——特征维度和时间步维度。每次“去除特征”意味着把整个时间步上的对应特征全部mask掉或者用保留条件分布的方式重新采样。这比表格数据要麻烦得多后面我会展开讲这部分。1.3 从“为什么是它”到SHAP值的数学直觉SHAP值虽然数学定义严谨但用起来时直觉比公式更重要。我习惯这样理解把所有特征随机打乱后模型输出平均值为基准线。现在依次加入各个特征每加入一个特征模型输出的平均变化量就是这个特征的平均边际贡献。SHAP值可以理解为“在所有可能的加入顺序下这个边际贡献的期望”。举个例子。一个设备故障预测问题输入特征有振动幅值、温度、转速、润滑压力四个。某个样本预测故障概率0.8基线概率0.5。SHAP分解可能写为振动幅值0.15、温度0.1、转速-0.02、润滑压力0.07。这就很直观这个样本最显著的“异常推手”是振动幅值而转速反而在压低故障概率。需要注意的是SHAP值的正负只代表方向不代表因果关系。它是对模型内部决策路径的一种解释而不是对物理世界因果机制的证明。这个边界必须和评审方讲清楚。2. 数据组织和LSTM训练回归预测里最容易翻车的三个环节2.1 数据预处理归一化、序列划分、样本构造MATLAB里做LSTM回归预测数据预处理是决定成败的第一关。很多人的代码在单步预测时跑通了一旦改成多特征窗口预测就崩多半是数据格式没搞对。先说我这次示例用的数据结构。假设我们要预测下一时刻的电力负荷输入特征是前一时刻的负荷、气温、湿度、风速、星期几五个特征。那LSTM的输入格式应该是numFeatures × numTimeSteps × numObservations也就是“特征维 × 时间步 × 样本数”的三维数组。我自己在实际项目中踩过最大的坑是归一化。LSTM对输入尺度非常敏感因为它内部的激活函数是tanh和sigmoid输入值域过大或过小都会导致梯度饱和。正确的做法是在训练集上计算均值和标准差然后用同一组参数归一化训练集和测试集。千万不能用全样本统计量——那等于在测试时偷偷用了未来信息属于一种数据泄漏。归一化代码我给一个标准的写法% 假设XTrain是numFeatures×numTimeSteps×numObservations mu mean(XTrain, [2 3]); % 每个特征维度上的均值 sigma std(XTrain, 0, [2 3]); % 每个特征维度上的标准差 for i 1:size(XTrain, 3) XTrain(:,:,i) (XTrain(:,:,i) - mu) ./ sigma; end for i 1:size(XTest, 3) XTest(:,:,i) (XTest(:,:,i) - mu) ./ sigma; end然后是滑动窗口构造样本。假设原始数据有1000个时间点每个时间点5个特征要用过去24个小时预测下一个点的负荷那样本数就是1000-24976个。每个样本是5×24的矩阵对应的时间步顺序是从旧到新。预测目标Y是N×1的向量。lag 24; numFeatures 5; numSamples length(rawData) - lag; XTrain zeros(numFeatures, lag, numSamples); YTrain zeros(numSamples, 1); for i 1:numSamples XTrain(:,:,i) rawData(i:ilag-1, :); YTrain(i) rawData(ilag, 1); % 假设预测目标是第一列负荷 end注意一个细节窗口滑动的步长。如果原始数据是小时级且需要预测每天特定时刻的值步长可以保持1让样本之间有重叠如果数据量非常大可以每隔几步采样一次减少样本量缩短训练时间。重叠样本会让训练集内部相关性增强但实际操作中影响有限不用过于担心。2.2 网络结构设计层数、隐藏单元、Dropout怎么定LSTM网络结构的设计我建议遵循“从简单到复杂”的迭代思路。第一版不要追求复杂结构先跑通一个单层LSTM加一个全连接层的baseline然后根据训练集和验证集的误差差距决定要不要加深、要不要加Dropout。最基础的回归预测网络结构如下numHiddenUnits 64; layers [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, OutputMode, last) dropoutLayer(0.2) fullyConnectedLayer(1) regressionLayer];这里几个关键参数值得解释。OutputMode设置为last因为我们是用整段历史窗口预测下一个时刻的单个值如果要做多步预测需要改sequence。numHiddenUnits是LSTM隐状态维度不是时间步数它决定了网络记忆能力的上限。64个单元对大部分单特征或五六个特征的中等规模问题已经够用不需要一开始就上128甚至256那样只会增加过拟合风险。Dropout层放在LSTM输出之后、全连接之前作用是在训练时随机丢弃一部分隐状态输出迫使网络不过度依赖某一个时间步的信息。这里有个细节Dropout在MATLAB的LSTM层里本身也有一个参数叫Dropout选项在新版本里是lstmLayer(..., Dropout, 0.2)。但更实际的做法是在LSTM层后面单独加dropoutLayer两者效果类似位置略有差异。LSTM层内的Dropout会作用在输入门、遗忘门和候选门的计算上层间Dropout作用在隐状态输出上。我个人习惯在层间加因为更直观。数据集分成训练集、验证集、测试集三份比例大致为70% / 15% / 15%。验证集用于早停和超参数选择测试集最后用一次。很多人图省事只分训练集和测试集然后反复调参去“追”测试集分数这个做法会导致测试集失去独立性。2.3 训练参数学习率、批次大小、早停的三组配置训练参数我直接给一组经过验证的基准配置你可以在基础上调整options trainingOptions(adam, ... MaxEpochs, 100, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 30, ... LearnRateDropFactor, 0.3, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 20, ... Plots, training-progress, ... Verbose, false);InitialLearnRate选0.005是我在这些预测任务里比较稳妥的起点。学习率太高比如0.1会让损失曲线震荡太低比如0.0001又会让收敛非常慢。如果你发现训练损失在快速下降但验证损失波动剧烈说明学习率偏大或批次太小。MiniBatchSize选32是一个平衡值。大批次128梯度更稳定但容易收敛到平坦的极小值小批次8或16噪声大但泛化有时更好。LSTM对批次大小比CNN更敏感因为每个样本的序列长度可能不同批次内需要padding导致有效计算量浪费。早停是用验证损失驱动的。MATLAB的trainingOptions里没有直接的EarlyStopping参数但可以用ValidationPatience配合输出网络来实现或者在训练完成后手动判断。更简单的做法是在代码里循环训练每次训练后保存验证损失最小的那次权重if validationLoss bestValLoss bestValLoss validationLoss; bestNet net; end这里要特别提醒训练完成后不要直接用训练过程里最后一次迭代的网络而应该用验证损失最低的那版网络。深度学习训练中后期过拟合几乎是必然发生的最后几次迭代的模型可能已经过拟合。3. MATLAB环境下的SHAP计算两条现实可行路径3.1 路径一训练在MATLAB解释在Python这是目前技术上最省事、结果最可靠的方案。MATLAB 2023a及以上版本可以方便地调用Python所以我们可以把LSTM预测函数暴露给Python用现成的shap库计算SHAP值。但这里有一个难点需要讲明白MATLAB训练出的SeriesNetwork或DAGNetwork对象不能被Python的shap库直接读取。shap.DeepExplainer需要的是TensorFlow或Keras模型shap.KernelExplainer只需要一个预测函数但它自己会调用这个函数多次来估计Shapley值。所以实际可行路线有两条支线支线A在Keras里重新搭建一个结构完全相同的LSTM在相同数据上重新训练然后用这个Keras模型做SHAP解释。因为训练数据相同、结构相同两个模型的预测行为非常接近SHAP结果可以近似代替。这是我在实操中最常用的方式。支线B把MATLAB模型导出为ONNX再用ONNX Runtime加载包装成一个函数后交给shap.KernelExplainer。这条路不需要重新训练但LSTM的ONNX转换和部署兼容性有时会出问题尤其是自定义层。对于大多数项目我推荐支线A。LSTM训练本来就不算贵一个中等规模数据集在GPU或好的CPU上几分钟就能训完。与其折腾权重导出不如用Keras重训一个行为近似的模型然后放心地去解释它。以下是Keras里对应模型的搭建代码import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense model Sequential() model.add(LSTM(64, input_shape(24, 5))) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizeradam, lossmse) model.fit(X_train, y_train, epochs50, batch_size32, validation_split0.15)然后就可以用SHAP库进行解释import shap # background_data是从训练集中随机抽样的部分样本 background X_train[np.random.choice(X_train.shape[0], 50, replaceFalse)] # GradientExplainer适合LSTM类模型 explainer shap.GradientExplainer(model, background) # 对测试集前n个样本计算SHAP值 shap_values explainer.shap_values(X_test[:100])GradientExplainer的原理是通过梯度计算特征的重要性速度比KernelExplainer快很多而且对LSTM的时序结构天然友好。如果GradientExplainer在特定模型上表现不稳定可以退回到KernelExplainer但计算时间会成倍增加。3.2 路径二MATLAB内置API实现Kernel SHAP近似有些读者不接受Python中转希望完全在MATLAB里完成。这个需求合理但你要清楚代价MATLAB目前没有官方的SHAP工具箱只能自行实现Kernel SHAP的近似版本。Kernel SHAP的核心思想是用带权重的线性回归来逼近Shapley值。对每个样本随机采样若干特征子集S构造一个二进制mask向量zz中为1的位置表示对应特征参与预测为0的位置表示该特征被mask掉。mask的方式一般是用训练集中该特征的均值填充。然后计算模型在z对应输入下的预测值用加权线性回归求解特征贡献。对LSTM输入mask操作需要特别设计。LSTM输入是24×5的矩阵掩码是按特征维度做的比如mask掉第2列“气温”就把整条时间序列上的第2列全部替换为均值。这样SHAP值表示的是“气温这个特征在全部历史时间上的整体贡献”。核心的MATLAB实现思路如下function shapValues kernelSHAP_lstm(predictFcn, x, background, nsamples) % predictFcn: 输入单个样本返回一个预测值 % x: 待解释的样本numFeatures×numTimeSteps % background: 背景数据用于获取均值填充 T size(x, 2); numFeatures size(x, 1); featureMeans mean(background, 2); % 生成随机mask二值矩阵 masks double(rand(numFeatures, nsamples) 0.5); masks(:, 1) 0; % 保证至少有一个子集是空集 masks(:, 2) 1; % 保证至少有一个子集是全集 % 对每个mask构造输入计算预测差值 ... end这种实现方式的准确性取决于几个近似假设一是mask特征后用该特征的均值填充相当于假设特征独立这在时间序列里常常不成立二是采样次数有限Kernel SHAP的估计存在方差。这也是为什么我建议优先使用Python路径——它在条件采样和采样策略上更成熟。3.3 两种路径怎么选我的详细建议以我的实际经验路径一Python解释在大多数情况下是正确选择。原因有三个第一shap库的实现经过了大量社区验证DeepExplainer和GradientExplainer对LSTM的适配性比手写Kernel SHAP好得多。第二Python的SHAP库自带丰富的可视化工具summary plot、force plot、dependence plot这些图不用重新画。第三维护成本低后续如果模型结构调整解释代码基本不用改。路径二适合那些“数据不能出MATLAB环境”的场景。比如某些项目数据受管控不允许复制到其他环境。这种情况下手写Kernel SHAP是无奈但可行的方案但你要做好心理准备计算速度可能很慢一个样本的解释可能要几百次模型调用。关于MATLAB调用Python的具体操作有一个细节值得提醒% 检查MATLAB能否找到Python解释器 pe pyenv; disp(pe.Executable) % 如果Versions为空需要手动设置 % pyenv(Version, C:\Python39\python.exe)然后可以在MATLAB中直接调用Python模块shap py.importlib.import_module(shap);但注意从MATLAB向Python传递多维数组时需要先用py.numpy.array转换。这个环节容易遇到数据类型转换问题我的做法是最小化MATLAB和Python之间的数据交互在Python中保存所有中间结果而不是反复传递。4. 结果解读三种可视化的正确打开方式4.1 全局特征重要性排序图先看“面”SHAP最常见的可视化是summary bar plot也就是把所有样本的SHAP绝对值按特征求平均然后按大小排序。这个图的含义是在模型整体看来哪个特征平均贡献最大。我从实际项目里截取一个例子数值经过简化| 特征 | 平均|SHAP值| 说明 | |------|--------|------| | 前一时刻负荷 | 0.052 | 历史状态主导 | | 气温 | 0.034 | 第二重要 | | 湿度 | 0.018 | 中等影响 | | 风速 | 0.009 | 影响较小 | | 星期几 | 0.003 | 几乎不参与 |这种排序图适合放进报告或论文里快速说明模型依赖的主要输入。但要注意平均绝对SHAP值是一种全局平均度量它会把正负贡献抵消掉。如果某个特征对一半样本推高预测值对另一半样本拉低预测值平均绝对值可能很大而平均SHAP值接近0。所以做排序图要使用绝对值平均不要使用带符号的平均值。排序图还容易给人一个误导排在前面的特征一定比后面的特征“更重要”。在特征存在强相关时SHAP会把贡献在相关特征之间分摊导致排序结果不稳定。比如气温和体感温度高度相关单独用一个特征时它的SHAP值很高两个都用时各自的SHAP值都会下降。这不是SHAP算错了而是交互分配的自然结果。4.2 样本级力图看“点”全局排序看完整貌后必须回到单样本层面看细节。force plot是SHAP最直观的单样本可视化一个基线值加上各特征的正负贡献最终推算出预测值。比如某个测试样本的预测负荷为356kW基线值为298kW。SHAP分解后可能类似基线值298kW前一时刻负荷42kW特征值为380kW处于历史高位气温18kW特征值为-5°C低温推高负荷湿度-6kW湿度较高轻微拉低负荷风速4kW星期几0kW最终预测2984218-64356kW这种分解让评审人员能直观理解“这个预测主要来自哪里”。在实际汇报中我发现单样本力图比全局排序图更能说服业务方因为它直接关联到了具体业务场景。在Python里打印单样本贡献的代码如下shap.initjs() # 对第i个测试样本画力图 shap.force_plot(explainer.expected_value, shap_values[0][i], X_test[i])不过force plot在MATLAB环境下没法直接显示需要把计算好的shap_values保存为.mat文件后在MATLAB里重新绘图或者用Python的matplotlib保存成图片再嵌入报告。我的习惯是把期望值和每个样本的SHAP值存成一个表格直接用MATLAB的barh函数画水平条形图效果完全够用。4.3 依赖图和时间步维度交互看“机制”第三个重要可视化是dependence plot画某个特征的原始取值与SHAP值之间的散点关系。这个图回答的问题是特征值从小变大时它对预测的影响是如何变化的是线性的、非线性的还是存在阈值效应例如气温特征的依赖图可能显示气温低于10°C时SHAP值为正低温推高负荷气温高于25°C时SHAP值又变为正高温推高负荷空调负荷上升在15~22°C之间SHAP值接近零。这种U型关系在描述数据时非常重要直接展现了模型的非线性学习结果。对LSTM这种时序模型还有一个普通表格模型没有的解读维度时间步上的贡献分布。同样一个特征在预测窗口早期和晚期的影响可能不同。比如“前一时刻负荷”这个特征往往在窗口接近末端的几个时间步贡献更大因为距离预测点更近。这个现象可以通过把SHAP值按时间步聚合来观察对每个时间步把所有样本的SHAP值在该步上取平均。这种按时间步聚合的观察角度是LSTM场景下SHAP分析独有的优势也是论文里比较讨巧的分析点。5. 我踩过的坑和调试建议5.1 时间步长选择的两个坑时间步长lag是LSTM预测里第一个要定下来的超参数也是最容易被随意设置的一个。我见过有人用24小时做步长原因是“一天24小时比较自然”但模型在验证集上表现很差。步长过短会丢失有效的历史背景步长过长则会把无关噪声也塞进模型而且样本量会减少。判断步长是否合适一个实用方法是看自相关函数。对目标序列做lag plot观察不同滞后阶数下的自相关系数。如果滞后5阶后自相关系数已经衰减到接近0步长取24大多是在浪费算力。另一个坑是跨周期效应。如果要预测的是工作日和周末模式明显不同的数据单纯增加时间步长不如在特征里显式加入“星期几”作为编码特征。LSTM虽然能自动学习时间依赖但对周期跨度较长的模式学习效率并不高把领域知识编码成特征往往比让网络自己学更划算。5.2 序列构造中的数据泄漏这个问题隐蔽性很强一旦发生模型在验证集上的表现会好得让人兴奋但部署后直接崩盘。我归纳出两种典型的泄漏场景第一种是归一化泄漏。用全样本的均值和标准差来归一化训练集和测试集等于在训练时已经“看到了”测试集的信息。严格的做法是只用训练集统计量然后把同一组统计量应用到测试集甚至验证集也要用训练集的统计量。第二种是滑窗构造泄漏。比如要构造“用当前时刻和过去23小时预测下一时刻负荷”的样本如果不小心让预测目标时刻的数据进入了输入窗口模型就看到了答案。这个错误在代码里很容易出现索引偏移写错一位输入窗口的结束位置和预测目标的开始位置重叠。检查方法也很简单把样本矩阵的最后一列和预测目标对比如果两者来自同一个时间点那就是泄漏。更自动化一点随机打乱预测目标的顺序再训练看模型分数是否依然很高。如果打乱后分数还很高说明泄漏非常严重。5.3 SHAP计算慢的解决SHAP计算慢是默认状态特别是对LSTM这样的深度模型。GradientExplainer虽然比KernelExplainer快但如果样本量大、时间步长、特征数多仍然可能跑很久。我的经验是分三步控制计算量第一背景数据集不要太大。shap.GradientExplainer的背景样本取50~100个足够背景数据是用于近似条件分布不是用来训练的取几百上千个只会增加计算时间边际收益很小。第二解释样本数量控制在100以内。如果你只想看整体特征重要性排序随机抽100个测试样本算SHAP值就够了不需要对全部测试集计算。第三如果一定要对全量测试集计算可以分块并行处理。Python的multiprocessing或者MATLAB的parfor都能派上用场但要注意线程安全问题——SHAP解释器在多进程环境下有时候需要重新初始化。5.4 解释结果与直觉不符的排查当你辛辛苦苦算出SHAP值发现某个明显重要的特征排名很低不要急着怀疑算错了。先检查三件事第一特征是否已经归一化如果两个特征量纲差异巨大比如一个在0~1一个在几百到几千模型训练可能已经偏向某个特征SHAP只是如实反映了模型学到的结果。第二是否有特征高度相关这个前面提过相关特征之间的SHAP值会被分摊导致单个特征的重要性看起来偏低。如果你需要的是“独立贡献”层面的解释可以考虑对特征做去相关或只在模型里保留一个代表特征。第三预测目标本身是否可预测如果验证集的误差本身很大SHAP结果自然“乱”。这在时间序列里很常见——某些时间段比如节假日前后的变化本身就是难以预测的模型在这个时间段给出的SHAP值往往不稳定。我在实际项目里处理这类问题的一个习惯是先把SHAP值和预测残差放在一起看。如果某个特征的SHAP值很大但预测值错误说明模型可能过度依赖该特征。这种发现对下一步特征工程非常有帮助。比如我那个设备预测项目SHAP分析发现“前一时刻负荷”在白天时段的贡献远大于夜晚进一步看数据才发现白天负荷的波动模式本身就比夜晚更有规律。后来我在特征里加入“时刻分段”编码模型在夜晚时段的预测精度也进一步提升。如果你也在做一个需要给别人讲清楚的预测项目我的建议是不要等到评审前才补解释而是在模型训练完就开始做SHAP分析。因为解释能反过来暴露模型的问题也能帮你发现数据里的结构规律。先跑通一条“MATLAB训练Python解释”的完整链路再根据实际数据情况调整解释粒度这条路是目前最省力也最稳妥的选择。本文还有配套的精品资源点击获取
网站建设 高端定制 企业官网