5 个特征、2 个输出:让 DBO 替 CNN-Transformer 挑结构,再用 SHAP 把黑箱撬开
这套代码最初是为了回答一个很具体的问题。
手上只有 200 组数据,5 个输入、2 个要同时预测的输出。CNN-Transformer 能拟合,但结构参数全得手调:卷积核取 3 还是 6,特征图 8 还是 64,池化窗口多大、步长多少,注意力头开几个。这几项互相咬着,改一个,另外几个的最优值就跟着变。
手工试到第十几组的时候我放弃了,把挑参数这件事丢给了优化算法。
于是有了这套 DBO-CNN-Transformer 多输出回归 + SHAP 归因的 MATLAB 工程。
一、研究背景:小样本、多输出、结构参数难定
回归任务本身不新鲜,难的是三个约束叠在一起。
样本少。 200 组,按 8:2 划分后训练集 160、测试集 40。这个量级下,模型容量稍大就过拟合,稍小就欠拟合,容错空间很窄。
输出多。 y1 和 y2 要一次预测出来,不是训两个模型。多输出意味着共享主干特征,也意味着一处结构失配会同时污染两个目标。
结构参数空间离散且耦合。 卷积核、特征图、池化窗口、池化步长、注意力头,五个参数直接影响张量形状能不能接得上。比如池化窗口 3、步长 2,5 个特征长度的序列卷完只剩 2 个位置,展开维度就变了,位置编码层的输入维数必须跟着改。手工调参本质上是在一张带约束的离散网格上乱走。
把这五件事交给群智能算法,是很自然的想法。DBO(Dung Beetle Optimizer,蜣螂优化)是近两年被大量引用的一个算法,结构简单、参数少,很适合这种低维、评估昂贵的问题。
二、这套代码实际做了四件事
- 自动搜索网络结构。 DBO 在 5 维空间里找一组最优的(卷积核、特征图、池化窗口、池化步长、注意力头数),目标函数是模型在测试集上的误差。
- 多输出回归训练与评估。 找到最优结构后重训一次,输出 RMSE、R²、MAE 三项指标,训练集和测试集分开算,两个输出分别算。
- 与未优化基线对照。 相同数据下再训一个默认参数的 CNN-Transformer,画雷达图、预测对比图、百分比误差图,把优化前后的差别摆出来。
- SHAP 归因 + 新数据预测。 用 Shapley 值拆开每个特征对预测的贡献,出蜂群图和重要性条形图;再把新工况输入丢进去,一次性批量输出预测值。
三、技术路线

整条链条是串行咬合的:
数据准备 → 归一化 → 8:2 划分 → DBO 外层寻优(内层每次都完整训练一遍模型)→ 用最优结构重训 → 指标评估与基线对照 → SHAP 归因 → 新数据批量预测。
关键点在第四步。DBO 每评估一次适应度,就要完整训练一个 500 轮 epoch 的网络。种群 10、迭代 10,意味着 100 次完整训练。这个代价不低,也是后文要谈的一个隐患。
四、模型长什么样
整个网络是「CNN 前端 + Transformer 后端」的串联结构。
4.1 前端:把 5 个特征当成一条一维信号
输入不是图像,是 5 个特征值。代码用 sequenceInputLayer([5 1 1]) 接收,sequenceFoldingLayer 把它折成可卷积的张量,然后两层卷积加一层最大池化:
- 卷积核尺寸
[k, 1],k由 DBO 搜索,是沿特征方向滑动的 - 卷积后接 ReLU
- 最大池化窗口
[p, 1]、步长[s, 1],把序列长度压短
这里有个细节值得说清楚。卷积核是沿特征轴滑动的,它捕捉的是「相邻特征之间的局部组合」,不是时间上的前后依赖。特征在表里的排列顺序因此会影响结果:把强相关的两个特征排在一起,卷积更容易学到它们的交互。这一点在工程上常被忽略,但它是这类「一维卷积处理表格特征」做法的隐含前提。
池化后序列长度按标准公式算:
L_out = floor((L_in - p) / s) + 1
5 个特征,pool=3、step=2 时,L_out = floor((5-3)/2) + 1 = 2。特征图 64 个,展开后得到 64 × 2 = 128 维的序列特征。代码里那段 if bestpool > beststep ... else ... 的判断,就是为不同组合推算这个展开维数,供位置编码层对齐使用。
4.2 后端:位置编码 + 两层多头注意力
展开后的 128 维特征送进位置编码层。位置编码用的还是 Transformer 原版的正弦形式:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
编码结果与原始特征相加(残差支路),再进两层自注意力。
第一层带 'AttentionMask','causal',每个位置只能看见自己和前面的位置;第二层不加掩码,全局可见。这个设计在序列预测里很常见,代码沿用了同样的套路------虽然这里的「序列」只有 2 个位置,因果掩码的实际作用有限。
自注意力的核心还是那个缩放点积:
Attention(Q, K, V) = softmax(Q·Kᵀ / √d_k)·V
多头则是把通道切成 h 份并行算,再拼回去。numKeyChannels = 头数 × 4,DBO 搜出来的 3 头对应 12 个键通道。
最后 indexing1dLayer("last") 取序列最后一个位置的输出,接全连接层映射到 2 个输出,回归层收尾。

五、DBO 怎么搜这五个参数
DBO 把种群分成四类角色:滚球蜣螂(生产者)、产卵蜣螂、小蜣螂、偷窃蜣螂。角色不同,位置更新公式不同。
生产者 按 r2 < 0.9 分两条路。第一条是带方向扰动的直线推进:
x_new = x + 0.3·|x - x_worst| + a·0.1·x_prev a ∈ {-1, 1}
第二条是偏转推进,用正切函数做角度偏转:
x_new = x + tan(θ)·|x - x_prev| θ = k·π/180
θ 取 0°、90°、180° 时正切无意义或发散,代码里做了跳过处理。
产卵区与觅食区 的边界随迭代收缩,收缩系数 R = 1 - t/T:
X1 = best·(1 - R) X2 = best·(1 + R)
跟随者在收缩后的边界内做随机加权插值:
x_new = best + r1·(x - X1) + r2·(x - X2)
偷窃者围绕全局最优做高斯扰动:
x_new = best + N(0,1)·(|x - best_cur| + |x - best|) / 2
每次更新后都做边界截断,保证解落在 [lb, ub] 内。
初始种群不是纯随机,代码里给了 9 种混沌映射可选(Tent、Chebyshev、Singer、Logistic、Sine、Circle、立方、Hénon、广义 Logistic),本工程用的是 label = 1,即 Tent 映射,目的是让初始个体在空间里铺得更均匀。
搜索空间和取值方式如下:
| 变量 | 下限 | 上限 | 取整方式 |
|---|---|---|---|
| 卷积核大小 | 2 | 6 | round |
| 特征图指数 n(实际 2^n) | 3 | 7 | round,得 8~128 |
| 最大池化窗口 | 1 | 4 | round |
| 最大池化步长 | 1 | 2 | ceil |
| 注意力头数 | 2 | 6 | round |
**适应度函数。**代码里是这样算的:
fitness = sqrt(mean(sum(re - y_test)).^2)
先按输出列求和、再求均值、平方后开方,等价于「两个输出的残差总和取平均后的绝对值」。这个式子的含义是平均偏差,不是逐点误差。
六、SHAP 怎么算
SHAP 的底子是合作博弈论里的 Shapley 值。把每个特征看成博弈的参与者,模型预测值看成总收益,第 j 个特征的贡献就是它在所有可能联盟里的边际贡献的加权平均:
φ_j = Σ_{S ⊆ F\{j}} [ |S|!·(n-|S|-1)! / n! ] · [ f(S ∪ {j}) - f(S) ]
F是全部特征的集合,n是特征总数S是不含 j 的任意特征子集f(S)是只用集合 S 里特征时的模型预测- 权重项保证每种联盟规模被公平对待
这个定义天然满足四条性质:有效性(所有 SHAP 值加起来等于预测值减去基准值)、对称性、哑元性(没贡献的特征值为 0)、可加性。
工程上有两种实现路径:Kernel SHAP 采样近似,或者像本工程这样直接枚举全部 2^(n-1) 种子集。5 个特征,每个特征枚举 16 种联盟,完全算得动,所以这里用的是精确枚举,没有采样误差。
基准值取测试集各特征的均值。计算时的做法是:需要「屏蔽」某个特征时,把这个特征替换成均值,其余特征保持样本原值。这个操作对应 SHAP 里的 interventional 干预式取值,而不是条件期望式。

蜂群图的横轴是 SHAP 值,颜色是特征本身的高低。
七、参数设定
| 项目 | 取值 |
|---|---|
| 样本总量 | 200(5 输入 / 2 输出) |
| 训练集 : 测试集 | 160 : 40(8:2) |
| 归一化 | mapminmax 映射到 0, 1 |
| 优化器 | Adam |
| 最大训练轮数 | 500 |
| 初始学习率 | 0.01 |
| 学习率策略 | piecewise,每 200 轮 × 0.1 |
| 每轮打乱 | 是 |
| DBO 种群规模 | 10 |
| DBO 迭代次数 | 10 |
| 混沌初始化 | Tent 映射 |
| 注意力键通道 | 头数 × 4 |
| 最大位置数 | 128 |
| 训练设备 | CPU |
八、结果:训练集赢了,测试集没赢
DBO 收敛得很快。第 1 到第 5 代,适应度停在 5.85×10⁻³ 不动;第 6 代直接掉到 9×10⁻⁴,之后七代再没变化。

最终搜出来的结构是:卷积核 6、特征图 64、池化窗口 3、池化步长 2、注意力头 3。基线用的是卷积核 3、特征图 8、池化窗口 2、步长 1、注意力头 4。
指标如下。
| 输出 | 数据集 | 模型 | RMSE | R² | MAE |
|---|---|---|---|---|---|
| 输出1 | 训练集 | DBO 优化后 | 0.921 | 0.9906 | 0.759 |
| 输出1 | 训练集 | 未优化 | 1.015 | 0.9886 | 0.798 |
| 输出1 | 测试集 | DBO 优化后 | 1.733 | 0.9412 | 1.408 |
| 输出1 | 测试集 | 未优化 | 1.611 | 0.9492 | 1.319 |
| 输出2 | 训练集 | DBO 优化后 | 0.920 | 0.9907 | 0.757 |
| 输出2 | 训练集 | 未优化 | 1.014 | 0.9887 | 0.795 |
| 输出2 | 测试集 | DBO 优化后 | 1.734 | 0.9412 | 1.410 |
| 输出2 | 测试集 | 未优化 | 1.613 | 0.9491 | 1.326 |

看这张表,训练集上 DBO 赢得很干净:RMSE 从 1.015 降到 0.921,降了 9.3%,R² 从 0.9886 抬到 0.9906。测试集反过来,RMSE 从 1.611 涨到 1.733,R² 从 0.9492 掉到 0.9412。

40 个样本的曲线图上能看出来,尖峰基本跟上了,谷底偶尔差半个身位。这种「大趋势对、局部偏差」的模式,恰恰是平均偏差型适应度最容易放过的情况。
优化后的模型在训练集上更准,在测试集上更差。
这不是个例,原因也找得到。回去看适应度函数:
fitness = sqrt(mean(sum(re - y_test)).^2)
sum(re - y_test) 是把所有样本的残差加起来。正负残差在这个求和里互相抵消,一个「有的点高估 3、有的点低估 3」的结构,适应度会非常漂亮,但它逐点误差很大。DBO 实际上在优化「整体偏差」这一个很窄的量,而 RMSE 衡量的是「逐点波动」,两者不是一回事。搜索方向从一开始就偏了。
代价还体现在算力上。种群 10、迭代 10、每次评估完整训练 500 轮,总共 100 次训练,最后换来一组在测试集上略差的参数。这笔账不太划算。
要改,其实只需一行。把适应度换成标准的逐点 RMSE:
matlab
fitness = sqrt(mean((re(:) - y_test(:)).^2));
这样目标函数和评估指标就对齐了,同时还能省掉一部分无效搜索。另外种群 10、迭代 10 的预算偏小,混沌初始化带来的多样性在 100 次评估里根本来不及展开。五个参数离散化之后,搜索空间是 5×5×4×2×5 = 1000 种组合,100 次评估最多也就踩到十分之一,还难免重复。
顺带说一句,测试集只有 40 个样本。0.949 和 0.941 之间这 0.008 的差距,落在几个样本的波动里就可能翻过来。小样本回归的指标对比,我现在的习惯是至少跑三次不同随机划分,看均值再说。
九、SHAP 给出的答案很干净
两个输出的归因结果几乎一模一样:
| 特征 | 输出1 平均绝对 SHAP | 输出2 平均绝对 SHAP | 占比 |
|---|---|---|---|
| x2 | 2.769 | 2.768 | 75.2% |
| x3 | 0.345 | 0.333 | 9.4% |
| x5 | 0.312 | 0.317 | 8.5% |
| x4 | 0.146 | 0.148 | 4.0% |
| x1 | 0.109 | 0.112 | 3.0% |

x2 一个特征吃掉了四分之三的贡献量。蜂群图里也能看出来,x2 那一行的点在横轴上从 -6 铺到 +6,跨度约 12,而输出的整体量级也在几十这个区间,说明它和预测值基本是同向的线性关系,颜色越红的点越靠右。
x3、x5 的贡献量级小一个数量级,点几乎全挤在 0 附近,只在 ±0.5 的小范围内摆动。x1、x4 更平。
两个输出的重要性排序完全一致(x2 > x3 > x5 > x4 > x1),说明这 2 个目标确实由同一组因素驱动,共享主干的设计是合理的。如果两个输出的归因排序差异很大,反倒说明多输出共享主干是个错误选择,该拆成两个模型。
这里要说清楚一件事:**SHAP 解释的是模型,不是物理规律。**x2 贡献最大,只代表在你训练出的这个网络里,x2 的取值变化会最剧烈地改变输出;它是不是真实机理里的主控因素,还得靠实验或者领域知识去验证。
十、运行环境
- MATLAB ,需要 Deep Learning Toolbox。代码用到
selfAttentionLayer、positionEmbeddingLayer,这两个层在近两年的版本里才提供,建议 R2024a 及以上,旧版本会直接报未知层错误。 - **无需 GPU。**训练选项写的是
'ExecutionEnvironment','cpu',200 组样本的规模用 CPU 完全够。 - 依赖
mapminmax,属于 Deep Learning Toolbox 自带函数。 - 主文件
main.m一键运行,fit.m(适应度)、DBO.m(优化器)、ys.m(混沌映射)、zhibiao.m(指标)、yuan.m(基线模型)、shapley_function.m(归因)、newpre.m(新数据预测)全部由主文件调用,单独跑没有意义。 - 数据文件:
回归数据.xlsx(前 5 列为输入,后 2 列为输出)、新的多输入.xlsx(待预测工况)、新的输出.xlsx(预测结果自动写出)。
十一、能用在哪些地方
这套流程的骨架是「小样本 + 多输出 + 结构要调 + 结果要能解释」,对应的场景比想象中多:
**材料与工艺。**成分配比、烧结温度、保温时间推力学性能或导电率,往往一次实验成本高、样本只有几十上百组。SHAP 能直接告诉你哪个工艺参数在主导性能,这对下一步做实验设计比一个 R² 有用得多。
**装备与结构。**多个测点响应同时预测,比如不同位置的应力、位移、温度。多输出共享主干可以捕捉测点之间的相关性。
**能耗与环境。**能耗和排放两个目标一起回归,两者通常由同一批运行参数驱动,正好对应本工程里「两个输出归因排序一致」的情形。
**替代昂贵仿真。**用有限元或 CFD 算出来的几百组样本训一个代理模型,之后新工况秒出结果,newpre.m 做的就是这件事:归一化参数复用训练时的 psin、psout,直接 predict 再反归一化,54 组新工况一次算完。
**实验补点。**SHAP 排序靠前的特征,就是值得加密取样的维度;排在末尾的特征,可以考虑从输入里删掉,降低采集成本。
十二、最后
这套代码最有价值的地方,不是那个 0.99 的 R²。
是它把三件事串成了一条能自我检查的链:优化算法替你搜结构,指标告诉你搜得好不好,SHAP 告诉你模型凭什么这么判断。三者对不上的时候,问题就暴露出来了------这个工程里就暴露了一个:适应度函数算的是平均偏差,评估指标算的是逐点误差,方向不一致,优化自然跑偏。
自动化调参最容易骗人的地方也在这。它总能给你一组「更好」的数字,但那个「好」是相对它自己的目标函数说的。目标函数写错一行,后面一百次训练都在为错误的方向服务。
所以每次看到优化算法给出漂亮结果,我现在都会先回头读一遍目标函数。
代码与数据均在本地运行,文中所有数字来自实际运行输出。