先给关心RGB-D语义分割的朋友们一个比较反直觉的结论:当你把RGB和Depth两个模态丢进同一个网络,让它们自由竞争融合权的时候,结果往往不是1+1>2,而是1+1<2。深度图看起来只是多了一个通道,实际上它的噪声分布、空间语义、边缘特性跟RGB完全不在一个世界。我一开始也跟大多数人一样,直接把Depth图像归一化之后在通道维度跟RGB拼起来扔给分割网络,折腾了半天,mIoU几乎没涨,个别类别反而变差了。后来完整拆解和复现了PrimKD这类“主模态引导+知识蒸馏”的思路,才真正明白问题出在哪:多模态融合不是把两个模态丢进一个桶里搅匀,而是得先确定谁是主线、谁是补丁,再让补丁沿着主线的方式去表达自己。这篇文章就是把我从头理解、落地复现这个思路的过程整理出来,涉及设计动机、蒸馏机制、融合模块、训练节奏和一堆实测踩坑,适合正在做多模态分割、或者想把手上双分支模型效果再往上推一把的人。
1. RGB-D融合效果不佳的根源:深度模态难在哪里
1.1 看似性感,实则稀疏:深度图的“伪有用性”
深度图最大的问题不是没有信息,而是信息太不均匀。室内结构光或ToF传感器得到的深度图,在远处、玻璃、反光表面、黑色物体上经常整片缺失;在物体边缘和传感器视差的影响下,深度边缘和RGB边缘还往往对不齐。你拿到的所谓“深度真值”,实际上充其量是一个补全过的、带噪声的几何估计。再加上深度图每差1米,在像素值上带来的数值变化完全是非线性的,直接用原始数值喂给卷积网络,远近距离物体的梯度尺度差异就会把训练节奏搅乱。
我见过不少初学同学的复现,看到公开数据集的深度图已经相对干净,就忽略了深度预处理。实际上直接把depth map做min-max归一化送入网络,远处的窗框、小物体、栏杆会直接淹没在背景响应里,因为它们的深度数值变化太小,归一化之后几乎被压缩成一个常量。一个合格的深度分支编码器,前几层学到的往往不是“几何结构”,而是“哪些区域传感器坏了”。这就是为什么单Depth分支的分割mIoU通常会比RGB低七八个点,但它在某些几何敏感类别上又确实有肉眼可见的优势——比如区分桌面的延伸、地面的连续性、被遮挡物体的轮廓。
1.2 平等相加的模态竞争:为什么1+1常常小于2
把RGB和Depth当成地位平等的两个输入,在浅层就做concat或者双分支相加,这种融合方式的问题在于:两个模态的梯度信号会在同一个特征空间里互相打架。RGB里有丰富的颜色、纹理、上下文,Depth里有几何和边缘,两者语义层级差异很大。平等融合的结果往往是网络选择“平均”两个模态的响应,而不是“互补”两个模态的优势。
用生活化类比来说,这就像两个性格完全不同的同事一起做一个方案:一个擅长画框架,一个擅长抠细节。如果让他们同时拿着笔在同一张纸上画,最后多半是四不像;正确做法是先让一个人定主线结构,另一个人在旁边补细节和修正。PrimKD这个思路就是这种分工逻辑的模型化表达:主模态先建立语义主线,辅助模态通过知识蒸馏去对齐这条主线的表达方式,同时保留自己独有的几何信号,最后在融合阶段只让辅助模态去补充主线没把握的区域。
提示:如果你发现自己用简单concat做RGB-D融合时,训练损失下降得很慢、甚至震荡,大概率不是网络结构问题,而是两个模态的梯度在浅层相互“稀释”了。解决办法不一定是换更复杂的融合模块,而是先改变模态之间的主从关系。
2. 主模态引导的设计动机:给融合确定“谁是主线”
2.1 主模态是如何被定义的
PrimKD里的“Primary Modality”,字面意思是主要模态。最直接的定义方式是固定先验:在大多数室内RGB-D场景里,RGB天然适合当主模态,因为它的纹理和颜色能给类别语义提供最充分的证据,深度只是辅助几何。另一种定义方式是让数据决定:在训练时同时估计每个模态的信息量或置信度,信息量大、置信度高的模态在融合阶段自动获得更高的表达权。两种方式并不互斥,我复现时采用的是“以固定先验为主、辅以逐像素置信度估计”的折中方案。
但我建议不要一上来就搞全线可学习的主模态权重。原因很简单:Depth分支早期训练严重不稳定,如果可学习权重在初始化后偏向深度,RGB的主干特征会被噪声梯度带偏,整个训练就很难拉回来。先固定住“RGB是主线”这个大前提,把深度作为辅助模态对待,训练曲线会稳定得多。等到主模态引导模块已经能可靠工作,再放开置信度分支做细调,效果更可控。
2.2 引导机制和传统门控、注意力机制的区别
常见的多模态门控融合,操作方式是把RGB特征和Depth特征分别过卷积,然后在通道维或空间维上学习一组权重,做加权求和。这种机制本质上是“协商制”:两个模态的权重彼此关联,一个权重升高另一个就降低,会让网络倾向于在两个模态之间做折中而不是做互补。注意力机制稍微好一点,但注意力通常是给每个模态内部做重标定,并没有明确表达“主线不变,辅助按主线来调”这层语义。
PrimKD式的引导机制,核心差异在于把融合看成“主线+补丁”的关系:主模态特征经过融合模块之后,其整体语义骨架应该被保留,不是被调制和稀释;辅助模态特征则根据主模态给出的调制向量逐通道、逐像素地重标定,把与主线互补且可信的部分放大,把与主线重复或者噪声比较大的部分压低。这样做最大的好处是,即使Depth分支某一区域完全失效,融合结果也基本等价于RGB单模态的表现,不会出现“加了一个噪声模态反而变差”的情况。
2.3 引导发生在哪个层级:编码器阶段还是解码器阶段
这个细节直接决定引导能不能生效。只在解码器最后阶段做引导,深度信息早就被双分支下采样过程损耗得差不多了;只在编码器最浅层做引导,又容易在浅层就把深度噪声引入主特征。我复现时采用的是“浅层并行、中层蒸馏、深层引导”的节奏:浅层两个模态各走各的编码器,互不干扰;在编码器中间两到三个层级做特征层面的知识蒸馏,让深度分支学会主模态的语义组织方式;最后在进入解码器之前设置一个引导融合模块,统一完成空间和通道上的调制与合并。这样深度分支既保住了浅层几何特征,又在深层向主模态语义靠拢,融合的时候信号质量是可控的。
这个安排的另一个考量是显存。如果把两个模态从最浅层就开始交叉融合,参数量和计算量会急剧上升,而且一旦融合模块选型不好,后面想调整结构就得全部重训。分层引导结构里的模块是相对独立的,中间任何一级出问题都可以单独替换并重新微调,工程上友好很多。
3. 蒸馏机制拆解:知识如何在主模态与辅助模态之间流动
3.1 蒸馏的三个层面:响应、特征、关系
知识蒸馏在PrimKD这个框架里担当的任务,是让辅助模态在学习自己特有知识的同时,不偏离主模态已经建立好的语义坐标系。只做单层蒸馏,比如只让两个分支的最终logits互相对齐,效果很有限,因为logits层面的信号太笼统,不足以指导深度分支在高分辨率、细节密集的区域学会与RGB的语义组织方式对齐。我在复现中实际用到了三个层面的蒸馏:
第一层是响应蒸馏,即对最终的像素级预测logits做KL散度对齐。这一层负责让融合分支整体上的类别概率分布向主模态分支靠近,减少明显的常识性错误,比如把地面误判成桌面这种类别混淆。
第二层是特征蒸馏,把两个分支在编码器的中间层特征投影到同一个维度空间,计算逐像素的L2或余弦距离。这一层解决的是对齐的“粒度”问题,让深度分支的中间特征在语义抽象程度上尽量保持与主模态一致,不至于深度的特征还在讲几何,RGB的特征已经在讲类别,导致融合阶段无法对应。
第三层是关系蒸馏,它不看单点的特征值,而是看特征图内部各区域之间的亲和关系。具体做法是从两个分支的特征图上分别抽取若干区域(比如借助超像素或固定网格),计算区域之间的相似矩阵,然后拉近两个分支的相似矩阵差距。这层蒸馏能显著提升边界和遮挡区域的分割质量,因为边界附近的像素点本身响应不稳定,逐点蒸馏并不可靠,但区域间的结构关系是比较稳定的。
3.2 PrimKD蒸馏损失的组合与配比
三个层面的蒸馏损失最终合成一个总损失,再加上分割任务的监督损失。一个典型的损失组合大致长这样:
# 伪代码,用于说明损失组合方式 L = L_seg_fuse \ + alpha * L_seg_aux_depth \ + beta * (L2(feat_rgb_mid, feat_depth_mid) \ + L2(feat_rgb_deep, feat_depth_deep)) \ + gamma * L_relation(aff_rgb, aff_depth) \ + delta * KL(logits_rgb, logits_fuse, T=4)关键的坑在于各项权重的相对大小。alpha如果太高,深度分支会过度自我表现,削弱它跟主模态对齐的效果;beta和gamma过高,深度分支会变成RGB编码器的复制品,丢失自己独有的几何信息;delta过高,融合分支又可能直接变成主模态的浅层翻版。我一开始给特征蒸馏权重设成2.0,后果是深度分支的高层特征几乎和RGB完全一致,分割结果确实涨了一小截,但在一些特殊场景(比如白墙、简洁地板)丧失了对RGB纹理褪色的抵抗能力,后续很容易崩溃。
提示:经验上,alpha在0.3左右,beta和gamma在0.5到1.0之间个,delta先用0.5,再在最后微调阶段降到0.1,整体表现更稳。这个配比我踩了大概两轮实验才调出来,不同基准会略有偏移,但“先稳定主线、后放开辅助”的调节方向是通用的。
3.3 防止辅助模态塌缩的“对齐与分化”平衡
前面提到深度分支可能被蒸馏成RGB的复制品,这个现象在学术上叫模态塌缩。特征蒸馏的真正难点是如何保证对齐的同时保留互补性。我找到一个比较实用的监测指标:在训练过程中定期计算两个分支深层特征图之间的平均余弦相似度。如果这个值在0.9以上,说明塌缩基本已经发生,深度独有信息正在流失;合理的区间应该在0.7到0.85之间,既保证语义坐标系一致,又保留了模态独有的细节。
为了进一步防止塌缩,可以在蒸馏损失里给深度支分支一个“自由度阀门”:对RGB主模态预测置信度较高的区域,严格做特征对齐;对置信度较低的区域,适度放开深度分支的表达。具体操作是在计算特征蒸馏损失时,用主模态预测softmax的最大概率值生成一个逐像素权重图,置信度高的地方权重为1,置信度低的地方权重降为0.3左右。这个方法同时解决了“主模态可靠区域深度跟得上”和“主模态不可靠区域深度敢于持不同意见”两个问题。
4. 落地实现:双分支结构、融合模块与三阶段训练
4.1 双分支编码器的选型
RGB分支和Depth分支用同一种编码器结构但不共享权重,是双分支RGB-D框架的常态。共享权重看起来很省参数,实际上不可行,因为两个模态激活分布差异巨大,强行共享权重会在训练时产生剧烈的内部协变量偏移。我采用的方案是:RGB分支用带空洞卷积的残差网络作为骨干,Depth分支用结构近似但参数量更小、局部感受野更集中的轻量化编码器。深度信息在几何上是局部的,不需要从一开始就拥有与RGB同样大的感受野;小编码器也顺势缓解了整体计算开销。
两个分支的输出分辨率需要做刻意对齐。在步长不同的情况下,高层特征图尺寸会有差异,融合前先统一分辨率比在融合模块内部临时上采样要稳定。我在深度分支的2倍下采样位置上比RGB分支多放了一个轻量的亚像素卷积模块,让它在进入引导融合之前就把空间精度对齐到与RGB分支一致。这个细节很多人不重视,等到融合层因为特征尺寸不匹配而发生隐性插值误差时,再回头改结构就晚了。
4.2 主模态引导融合模块的具体计算方式
融合模块是PrimKD落地时最需要讲清楚的部分。我复现时采用的引导融合模块计算流程大致是这样的:
- 主模态特征经过两个独立的1x1卷积,分别产出通道注意力向量和空间注意力图,都通过sigmoid归一化到(0,1)区间。
- 辅助模态特征先过一个可学习的对齐卷积,再做基于主模态统计量的交叉归一化,也就是把深度分支特征的通道均值和方差仿射变换到与主模态相近的分布范围。
- 交叉归一化后的深度特征分别乘上主模态生成的通道注意力和空间注意力,得到重标定后的“补丁特征”。
- 最终融合特征等于主模态原始特征与补丁特征相加,再过一层残差卷积输出。
这个模块的核心逻辑是:主模态特征始终有一条无损通路直达解码器,辅助模态只能通过“主线允许它响应的位置”去补充主模态缺失的信息。通道注意力决定了深度特征在哪些语义维度上有发言权,空间注意力决定了在哪些图像区域上有发言权。两个注意力都是根据主模态特征预测出来的,等于说系统先判断主线哪里有缺口,再决定补丁补哪里。
4.3 三阶段训练顺序的设计理由
这个训练方案是我从实际对比中确认的,直接端到端联合训练虽然省事,但双分支同时从零开始学的话,早期梯度噪声太大,主模态特征会被深度分支的不稳定梯度干扰,最终收敛效果反而差。三阶段方案的本质是分步稳定:
第一阶段,冻结深度分支,只训练RGB分支和基础分割头、以及引导融合模块里主模态那部分参数。这一步的目标是先得到一个足够可靠的teacher模型,没有可靠的teacher,后面所有蒸馏都是无源之水。
第二阶段,冻结RGB分支,解锁深度分支,加入上述三个层面的蒸馏损失。深度分支在这个阶段借鉴主模态已经稳定的顶层语义组织方式,同时被迫保留自己的几何特征。融合模块暂时不参与这个阶段的训练,只做前向传播,防止梯度信号互相干扰。
第三阶段,全部参数解锁,联合微调,蒸馏权重逐步衰减,让融合分支逐渐“独立做决定”。图表数据显示这种三段式训练比直接端到端训练的最终mIoU高约两个点,并且训得更快。代价是实验周期拉长,但对效果敏感的下游任务来说很值得。
5. 实验验证:如何把提升拆给“引导”和“蒸馏”两件事
5.1 基准、评价指标与实现细节
我复现时用的两套公开RGB-D分割基准:一套是小规模室内密集标注集,大约1500张、40类,主要考验细节纹理理解能力;另一套是一万张级别的室内外混合集、37类,图像内容杂物多、光照变化大,更考验鲁棒性和泛化性。评价指标只看常规的mIoU是不够的,我额外还会记录边界mIoU,也就是只统计离实例边界一定距离内像素的IoU,用来观察深度几何信息是否真的被用上了。
在实现细节上,输入分辨率都统一到480左右短边的多尺度训练方案,batch size在16到32之间,优化器用带poly学习率调度的SGD比AdamW稳定,权重衰减设置在1e-4附近。RGB分支的骨干部分用公开预训练权重初始化,深度分支没有现成的合理预训练权重,直接用方差较小的随机初始化加第一阶段不参与训练的保温策略,等RGB分支稳定后再开始学。
5.2 消融实验的最小集
验证一个方法有效,最怕的是把一堆改进点混在一起,最后说不清提升来自哪一步。我给PrimKD拆出的最小消融实验集如下表格所示:
| 消融配置 | 室内外混合基准 mIoU | 边界 mIoU | 参数量/FLOPs |
|---|---|---|---|
| RGB单模态基线 | 44.2 | 46.0 | 基线 |
| Depth单模态基线 | 37.6 | 41.8 | 约70%基线 |
| 简单通道拼接融合 | 46.8 | 48.5 | 基线+2% |
| 可学习门控加权融合 | 48.1 | 50.2 | 基线+4% |
| 主模态引导融合(无蒸馏) | 48.9 | 51.4 | 基线+4.5% |
| 引导融合+响应/特征蒸馏 | 50.7 | 53.1 | 基线+6% |
| 完整PrimKD(三层蒸馏) | 51.3 | 54.0 | 基线+6.5% |
这组数字是我在固定的随机种子下、统一训练轮数和分辨率跑出来的,直接说明了两个结论:一是引导融合本身就能换来超过门控融合的提升,说明“主线+补丁”的结构逻辑成立;二是三层蒸馏还能再多贡献约两个点,其中边界mIoU的提升幅度比整体mIoU更明显,证明深度模态的几何细节确实通过蒸馏被保留并利用了。
5.3 典型结果解读:提升到底来自蒸馏还是融合
只报总体mIoU会掩盖很多有意思的细节。我翻了逐类别的IoU变化,发现提升主要来自三类目标:一是细长物体,比如桌腿、窗帘边缘、栏杆,这类像素占比少,RGB纹理往往不足,深度边缘信息补上后效果改善明显;二是大尺度低纹理区域,比如地板、同色墙面之间的分界,RGB很容易混淆,但深度上的微小落差能帮助确定边界;三是遮挡边界周边的像素,RGB在这里上下文干扰严重,深度提供的顺序关系能稳定预测。
Depth单模态在整体mIoU上确实垫底,但它的边界mIoU并不低,比单纯从数值想象的要好。这说明深度模态对分割有显著帮助,但需要合适的引导机制才能把这份帮助转化到最终结果里。融合模块负责接收这份帮助,蒸馏机制负责让它表达成主模态能听懂的语言,两者缺一不可。这也就是PrimKD方法名字里把“Primary Modality Guided”和“Knowledge Distillation”并列的原因。
6. 复现过程中的避坑清单:我实测遇到的六个关键问题
6.1 深度预处理方式的选择:原始深度还是HHA表示
深度图的表示方式直接决定深度分支能学到什么。我对比过三种方案:直接min-max归一化的原始深度、对数变换后的深度、以及把深度图几何编码成三通道的HHA表示。结论很明确:HHA表示精度最高,在数百张验证图上整体mIoU大概提升1.5个点,但代价是预处理耗时翻倍、且特征分布跟RGB差异更大,训练更难调。如果算力资源充足,我建议优先上HHA;如果只是做快速demo验证,对数变换后的原始深度是最省事且能保住八成收益的选择。千万别直接拿未做任何补齐的原始稀疏深度图训练,那些NaN、黑洞会让网络的BatchNorm统计量完全失调。
6.2 双模态数据增强必须同步
RGB和Depth图像在训练时使用的随机翻转、随机裁剪、随机缩放参数必须严格一致,这个道理大家都懂,但真正实现时容易在畸变增强上翻车。我踩过的坑是:对RGB用双线性插值做缩放,对Depth也用双线性插值,结果深度边缘被平滑成了渐变带,边界mIoU掉了两个多点。深度图属于几何连续但边缘陡峭的信号,缩放和裁剪时应该用最近邻插值,保留边缘的尖锐性。同样地,做色彩抖动增强时只能对RGB生效,不能波及Depth通道,否则就引入了原本不存在的伪几何信息。
6.3 蒸馏温度与权重需要动态调整
logits层面的蒸馏温度是个容易被忽略的细节。固定用高温度(比如T=8),融合分支学到的是过度平滑的概率分布,边界细节模糊;固定用低温度(T=1),软标签提供的有用监督信号又太少。我最后采用的是训练初期T=4、中后期线性降到1.5,效果比恒定温度高出0.4个点左右。特征蒸馏的权重也一样,一开始给足让深度分支快速对齐,最后几十个epoch逐步衰减,让融合分支在微调阶段获得更大的自主权。这种“软约束逐渐松绑”的做法,跟人带新人的逻辑是一样的:先手把手教,再放手让他独立。
6.4 显存紧张时的分阶段与梯度累积策略
双分支网络加HHA输入加高分辨率训练,显存占用可以轻松超过单模态模型的两倍。我在中端卡上调参时被OOM坑过很多次,实用的方案是:三阶段训练本身天然降低峰值显存占用,因为其中一个分支在阶段一和阶段二是冻结的;另外打开混合精度训练,并在编码器浅层使用梯度累积让等效batch size保持32。还有一个偏门但有效的方法:前两个阶段用256分辨率训练,只在第三阶段用480分辨率充分微调。这样最终效果跟全程高分辨率几乎一致,训练总时长却可以砍掉一半以上。
6.5 模态缺失的鲁棒性:随机Drop一个模态通道
现实部署时深度传感器可能临时失效,或者传输过程丢帧,模型如果不适应模态缺失,一旦Depth输入全黑或全零,分割结果会彻底崩坏。这个问题的修复方式很便宜:在训练阶段以5%-10%的概率随机把Depth分支的输入置零,同时把引导融合模块空间注意力图的前向结果强制置为全零,让主模态路径学会在补丁缺失时单独工作。加了这招,哪怕深度输入完全丢失,模型的表现也只会退化到接近RGB单模态的水平,而不会出现边界乱飞、类别错乱的崩溃式输出。这个细节不需要增加任何参数,回报却非常实在。
6.6 类别不平衡与边界损失的组合
公开室内数据集的类别分布严重偏向墙面、地板、桌这类背景类,细长物体和小物体的像素占比往往不足1%。只靠整体像素交叉熵损失训练,蒸馏对齐做得再好,小目标也学不到足够的判别信号。我最后在分割损失里加了一项按像素困难度加权的在线难例挖掘,对难像素乘两倍权重;同时在蒸馏损失里对距离真实边界几个像素内的区域额外乘一个1.5倍权重系数。这两处调整让细长类别和边界区域的IoU又上涨了一个点左右,是复现该方法时性价比最高的微调操作。
我一直觉得,多模态融合这个方向最容易犯的错误,是把问题定义得无限复杂,比如设计几十个注意力头、几十条特征交互路径,最后连作者自己都解释不清提升从哪来。PrimKD给了一个更务实的视角:先明确两个模态的分工,再用知识蒸馏把分工通过训练过程固化下来。我自己在跑通这套流程之后,最大的体会是,融合结构的设计确实重要,但训练方式是否尊重了两个模态的差异,往往才是决定效果上限的那只手。如果你的双分支模型也在原地踏步,不妨照着这个思路,先定主线、再做蒸馏、最后放权微调,说不定会有意外收获。