1. 红外小目标检测的痛点与DASI+MDCR的破局思路
红外小目标检测这个方向,做过的人都知道有多折磨。目标可能只占几个像素,背景里云层边缘、地面热源、传感器噪声,随便一个干扰源的信噪比都可能比目标本身还高。传统做法是在U-Net骨架上堆跳层连接,把浅层的高分辨率特征和深层的语义特征拼在一起,指望浅层能保住小目标的定位信息。但实际跑下来你会发现,浅层特征里混着大量背景杂波,直接拼接等于把噪声也一并送进了解码器,小目标反而被淹没了。
我最初做这个方向时也是老老实实按U-Net的套路来,编码器用几层卷积下采样,解码器逐级上采样,跳层连接用concat或者add。结果在公开数据集上跑出来的虚警率高得离谱,检测率勉强及格但根本没法落地。后来分析特征图才发现问题所在:浅层特征虽然空间分辨率高,但通道之间的响应差异极大,有些通道几乎全是背景纹理,有些通道才真正对目标敏感。简单拼接相当于让解码器自己去学哪些通道有用,但小目标的监督信号本来就弱,网络根本学不过来。
DASI和MDCR这两个模块就是冲着这个矛盾去的。DASI负责在跳层连接路径上做通道维度的自适应筛选,把浅层特征里真正有用的通道挑出来再往上传;MDCR则是在解码阶段做多尺度膨胀卷积的残差融合,让不同感受野的特征在通道维度上重新加权组合。两者配合起来,相当于给U-Net的跳层连接装了一个“智能阀门”和一个“多尺度混合器”,既保住了小目标的空间位置,又压住了背景杂波。
这套方案适合谁呢?如果你已经在用U-Net做红外小目标检测,但被虚警率和漏检率卡住了,那DASI和MDCR可以直接嵌到你现有的网络里,改动量不大但效果提升明显。如果你刚入门这个方向,建议先把U-Net的基线跑通,再逐步加入这两个模块做消融实验,这样能更清楚地看到每个模块的贡献。下面我会从设计思路、模块细节、实操步骤到踩坑经验,完整拆一遍这套方案。
2. 为什么跳层连接不够用:从特征冲突说起
2.1 浅层特征与深层特征的语义鸿沟
U-Net的跳层连接本质上是一种特征复用策略。编码器浅层输出的特征图分辨率高,每个像素对应原图的一小块区域,所以定位精度好;但浅层卷积核的感受野小,看到的只是局部纹理,语义信息弱。深层特征经过多次下采样,感受野大,能区分“这是目标”还是“这是背景”,但分辨率低,小目标在深层特征图上可能只剩一个像素甚至消失。
跳层连接把这两类特征拼在一起,出发点是好的:让解码器同时拿到“在哪里”和“是什么”的信息。但问题在于,浅层特征里的“在哪里”往往伴随着大量的“这里也可能是背景”。红外图像里,云层边缘的梯度、地面建筑的轮廓、传感器坏点,在浅层特征图上都有很强的响应。这些响应和真实小目标的响应混在同一个通道里,解码器很难区分。
我做过一个统计,在典型的红外小目标数据集上,浅层特征图里对目标敏感的通道占比不到15%,剩下85%的通道主要响应背景杂波。跳层连接把这85%的噪声通道也传给了解码器,相当于给解码器增加了大量无效甚至有害的信息。网络要花很多容量去抑制这些噪声,留给小目标检测的容量就不够了。
2.2 直接拼接带来的梯度干扰
从梯度传播的角度看,跳层连接的concat操作会让浅层特征的梯度直接回传到编码器浅层。这本身是好事,能缓解梯度消失。但浅层特征里那些背景杂波通道的梯度也会一并回传,导致浅层卷积核在更新时被背景杂波的梯度主导,真正对目标敏感的卷积核反而得不到足够的更新信号。
我试过在跳层连接上加一个固定的通道注意力,比如SE模块,让网络自己学通道权重。效果有提升,但不够稳定。原因是SE模块的全局平均池化会丢失空间信息,而小目标检测恰恰对空间位置极其敏感。全局池化把整张特征图压成一个通道描述符,小目标那点微弱的响应在平均过程中被背景稀释了,学出来的通道权重自然偏向背景。
2.3 DASI的切入角度:空间感知的通道筛选
DASI的全称是Dual Attention Spatial Interaction,核心思路是在跳层连接路径上同时做通道注意力和空间注意力,但和SE不同的是,它的通道注意力不是基于全局平均池化,而是基于空间维度的统计量。具体来说,DASI会先对浅层特征做一次空间维度的最大池化和平均池化,把两个结果拼接后送进一个小型卷积网络,生成通道权重。这样做的理由是:最大池化能保留小目标的最强响应,平均池化能反映背景的整体水平,两者结合能让通道权重更准确地判断哪些通道对目标敏感。
空间注意力部分则是沿着通道维度做最大池化和平均池化,生成一张空间权重图,告诉网络“特征图的哪些位置更值得关注”。这两路注意力是并行计算的,最后通过一个可学习的融合系数加权组合。我实测下来,这种双路注意力的设计比单纯用SE或CBAM在红外小目标场景下更稳,虚警率能降两到三个百分点。
3. DASI模块的详细拆解与实现要点
3.1 DASI的整体结构设计
DASI模块的输入是编码器浅层特征图,假设形状为C×H×W。模块内部先分成两个分支:通道注意力分支和空间注意力分支。通道注意力分支对输入做全局最大池化和全局平均池化,得到两个C×1×1的向量,拼接后经过一个两层MLP,第一层把通道数压缩到C/r,第二层恢复回C,最后用Sigmoid激活得到通道权重向量。空间注意力分支则是对输入沿通道维度做最大池化和平均池化,得到两个1×H×W的图,拼接后经过一个7×7卷积,再用Sigmoid激活得到空间权重图。
两个分支的输出通过逐元素相乘的方式融合:通道权重先和原始特征图相乘,得到通道加权后的特征;空间权重再和这个结果相乘,得到最终输出。这里有一个细节:通道注意力和空间注意力的顺序可以调换,我试过先空间后通道和先通道后空间,在红外小目标场景下差异不大,但先通道后空间收敛稍快一点。
3.2 通道注意力分支的参数选择
通道注意力分支里的压缩比r是个关键参数。r太大,MLP的容量不够,学出来的通道权重区分度低;r太小,参数量上去了,容易过拟合。我在几个红外数据集上做了对比实验,r取8到16之间比较合适。具体来说,如果浅层特征的通道数C是64,r取8,MLP第一层输出8个通道;如果C是128,r取16,第一层输出8个通道。这样MLP的参数量控制在几千到一万左右,既不会太轻量导致欠拟合,也不会太重导致过拟合。
还有一个容易忽略的点:全局最大池化和全局平均池化的结果在拼接前要不要做归一化?我的经验是不需要。因为后续的MLP里有BatchNorm层,会自动处理尺度问题。但如果你的网络里没有BN,那最好在拼接前对两个向量做L2归一化,否则最大池化的值域和平均池化的值域差异太大会影响MLP的收敛。
3.3 空间注意力分支的卷积核尺寸
空间注意力分支用的7×7卷积是个经验值。我试过3×3、5×5、7×7和9×9,在红外小目标场景下7×7的效果最好。原因是小目标本身尺寸小,3×3的感受野太小,学出来的空间权重图过于局部化,容易把目标周围的背景也标成高权重;9×9的感受野太大,空间权重图过于平滑,小目标的位置信息被模糊了。7×7刚好能在“聚焦目标”和“抑制背景”之间取得平衡。
另外,这个7×7卷积的输入是沿通道维度池化后的2通道特征图,输出是1通道。卷积核的初始化建议用Kaiming初始化,偏置初始化为0。Sigmoid激活前的数值范围要控制好,如果发现空间权重图大部分区域都接近0.5,说明卷积核学到的区分度不够,可以适当增大卷积核的初始化方差。
3.4 DASI的代码实现与集成位置
import torch import torch.nn as nn class DASI(nn.Module): def __init__(self, channels, reduction=8): super(DASI, self).__init__() self.channel_mlp = nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) self.spatial_conv = nn.Conv2d(2, 1, kernel_size=7, padding=3) self.sigmoid = nn.Sigmoid() def forward(self, x): # 通道注意力 max_pool = torch.max(x, dim=(2, 3))[0] avg_pool = torch.mean(x, dim=(2, 3)) channel_att = self.channel_mlp(max_pool) + self.channel_mlp(avg_pool) channel_att = channel_att.view(x.size(0), x.size(1), 1, 1) x_channel = x * channel_att # 空间注意力 max_spatial = torch.max(x_channel, dim=1, keepdim=True)[0] avg_spatial = torch.mean(x_channel, dim=1, keepdim=True) spatial_input = torch.cat([max_spatial, avg_spatial], dim=1) spatial_att = self.sigmoid(self.spatial_conv(spatial_input)) out = x_channel * spatial_att return out集成位置很关键。DASI应该放在跳层连接路径上,也就是编码器浅层输出之后、与解码器特征拼接之前。不要放在编码器内部,因为编码器内部的浅层特征还没经过足够的非线性变换,通道间的区分度不够。也不要放在解码器之后,因为那时候特征已经和深层语义融合了,再做通道筛选意义不大。
4. MDCR模块:多尺度膨胀卷积的残差融合
4.1 MDCR要解决的核心问题
DASI解决了跳层连接里浅层特征的通道筛选问题,但解码器本身还有一个短板:单一尺度的卷积核感受野固定,而红外小目标的尺寸变化范围可能很大。有的目标只有2×2像素,有的可能有10×10像素。如果解码器只用3×3卷积,感受野固定,大一点的目标可能被拆成多个局部响应,小目标又可能被过度平滑。
MDCR的思路是在解码器的每个上采样阶段,并行使用多个不同膨胀率的膨胀卷积,让网络同时看到多个尺度的上下文信息。膨胀卷积的好处是不增加参数量就能扩大感受野,而且不会像池化那样丢失空间分辨率。多个膨胀率的输出在通道维度上拼接后,经过一个通道注意力模块重新加权,最后和原始特征做残差连接。
4.2 膨胀率的选择与组合策略
膨胀率的选择不是随便定的。我试过几组组合:(1,2,3)、(1,2,4)、(1,3,5)、(2,4,6)。在红外小目标数据集上,(1,2,4)这组表现最稳定。原因是膨胀率1对应原始感受野,适合小目标;膨胀率2对应中等感受野,适合中等目标;膨胀率4对应大感受野,适合稍大目标或提供背景上下文。膨胀率3和5的间隔不够均匀,导致某些尺度的目标没有对应的感受野覆盖。
还有一个细节:膨胀卷积的padding要设置成和膨胀率匹配,否则特征图尺寸会变。对于kernel_size=3的膨胀卷积,padding等于膨胀率时,输出尺寸和输入一致。比如膨胀率2,padding=2;膨胀率4,padding=4。这个一定要算清楚,否则拼接时尺寸对不上会报错。
4.3 通道注意力的嵌入方式
MDCR里的通道注意力我建议用轻量级的SE模块,压缩比取4。原因是MDCR本身已经引入了多尺度信息,通道注意力的作用主要是让网络自己决定哪个尺度的特征更重要,不需要太复杂的注意力机制。SE的全局平均池化在这里反而合适,因为多尺度特征已经包含了空间信息,通道注意力只需要做尺度选择。
具体流程是:三个膨胀卷积的输出在通道维度拼接,得到3C通道的特征图;然后经过SE模块,生成3C维的通道权重;加权后的特征再经过一个1×1卷积压缩回C通道;最后和原始输入做残差相加。这个1×1卷积的作用是融合多尺度信息并恢复通道数,不能用3×3代替,因为3×3会再次引入固定感受野,破坏多尺度的设计初衷。
4.4 MDCR的代码实现与参数配置
class MDCR(nn.Module): def __init__(self, channels, dilations=[1, 2, 4], reduction=4): super(MDCR, self).__init__() self.branches = nn.ModuleList() for d in dilations: self.branches.append( nn.Conv2d(channels, channels, kernel_size=3, padding=d, dilation=d, bias=False) ) self.bn = nn.BatchNorm2d(channels * len(dilations)) self.se = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels * len(dilations), channels * len(dilations) // reduction, 1), nn.ReLU(inplace=True), nn.Conv2d(channels * len(dilations) // reduction, channels * len(dilations), 1), nn.Sigmoid() ) self.fusion = nn.Conv2d(channels * len(dilations), channels, 1) def forward(self, x): branch_outs = [branch(x) for branch in self.branches] concat = torch.cat(branch_outs, dim=1) concat = self.bn(concat) se_weight = self.se(concat) weighted = concat * se_weight fused = self.fusion(weighted) return x + fusedMDCR的集成位置是在解码器的每个上采样阶段之后。具体来说,解码器先做上采样,然后和DASI处理过的跳层特征拼接,拼接后的特征送进MDCR做多尺度融合,最后再经过常规的卷积层。这样MDCR既能处理跳层特征里的多尺度信息,也能处理上采样带来的棋盘效应。
5. 完整网络的搭建与训练实操
5.1 网络整体架构设计
基于U-Net骨架,编码器用4层下采样,每层两个3×3卷积加BN和ReLU,下采样用2×2最大池化。解码器对应4层上采样,每层先做转置卷积或双线性插值上采样,然后和DASI处理过的跳层特征拼接,再送进MDCR,最后两个3×3卷积。输出层用1×1卷积把通道数降到1,Sigmoid激活得到概率图。
这里有一个设计选择:上采样用转置卷积还是双线性插值?我试过两种,在红外小目标场景下双线性插值+后续卷积的效果更稳。转置卷积容易产生棋盘伪影,而小目标对伪影非常敏感,伪影可能被误检成目标。双线性插值虽然没有可学习参数,但配合MDCR的多尺度融合,效果反而更好。
5.2 损失函数的选择与调参
红外小目标检测的损失函数不能只用交叉熵。原因是正负样本极度不平衡,小目标可能只占整张图的几十个像素,交叉熵会被负样本主导。我常用的组合是Focal Loss加Dice Loss,权重各占0.5。Focal Loss的gamma取2,alpha取0.25,这样能聚焦难分类的样本;Dice Loss直接优化预测图和真值图的重叠度,对小目标更友好。
还有一个技巧:在损失函数里加一个针对小目标的加权项。具体做法是计算真值图里每个连通区域的面积,面积小于阈值的区域给更高的权重。我设的阈值是9个像素,小于9个像素的目标权重乘以3。这个加权项加在Focal Loss里,能让网络更关注小目标。实测下来,检测率能提升两个百分点左右。
5.3 数据增强策略
红外小目标的数据增强不能照搬自然图像的那套。随机裁剪、旋转、翻转这些常规操作可以用,但颜色抖动、亮度调整要慎用,因为红外图像的灰度值直接对应温度,改变灰度可能把目标变成背景。我常用的增强组合是:随机水平翻转、随机垂直翻转、随机旋转90度的整数倍、随机裁剪到256×256。另外加一个随机缩放,缩放比例在0.8到1.2之间,模拟不同距离的目标。
还有一个针对小目标的增强:随机在背景区域粘贴一些目标patch。具体做法是从其他图像里裁剪出目标区域,随机粘贴到当前图像的背景区域。这个操作能增加小目标的多样性,但要注意粘贴的位置不能和已有目标重叠,否则真值图会乱。我一般控制粘贴的目标数量不超过3个,粘贴位置离已有目标至少20个像素。
5.4 训练超参数与收敛判断
优化器用Adam,初始学习率1e-3,每20个epoch衰减到原来的0.5。Batch size根据显存来,8到16之间。训练epoch数一般100到150,但不要只看epoch数,要看验证集上的指标。我通常监控三个指标:检测率、虚警率和F1分数。检测率和虚警率是一对矛盾,检测率上去了虚警率往往也上去。我的经验是,当F1分数连续10个epoch不再提升时就可以停了,再训下去容易过拟合。
学习率预热也很重要。前5个epoch用线性预热,从1e-5慢慢升到1e-3。原因是DASI和MDCR里的注意力模块初始权重是随机的,如果一开始就用大学习率,注意力权重会震荡得很厉害,网络很难收敛。预热能让注意力模块先稳定下来,再进入正常训练。
6. 消融实验与效果对比
6.1 基线U-Net与加入DASI的对比
我在一个包含5000张红外图像的数据集上做了对比。基线U-Net的检测率是82.3%,虚警率是15.7%,F1分数是0.831。加入DASI后,检测率提升到85.1%,虚警率降到12.4%,F1分数0.862。提升主要来自虚警率的下降,说明DASI确实把浅层特征里的背景杂波通道抑制住了。
具体看特征图可视化,基线U-Net的跳层连接特征里,云层边缘的响应很强,解码器把这些响应误判成了目标。加入DASI后,云层边缘的响应被通道注意力压下去了,真实目标的响应保留了下来。空间注意力则进一步聚焦到目标所在的位置,背景区域的权重明显降低。
6.2 加入MDCR后的进一步提升
在DASI的基础上再加MDCR,检测率提升到87.6%,虚警率降到10.2%,F1分数0.887。MDCR的贡献主要体现在对不同尺寸目标的适应性上。基线U-Net对2×2到4×4像素的小目标检测率只有75%左右,加入MDCR后提升到84%。原因是多尺度膨胀卷积让网络同时看到了不同感受野的信息,小目标不会被单一尺度的卷积核平滑掉。
还有一个发现:MDCR对密集小目标场景的提升更明显。当图像里有多个相邻小目标时,基线U-Net容易把它们合并成一个检测框,加入MDCR后能更好地区分开。原因是多尺度特征里包含了不同尺度的上下文,网络能根据上下文判断哪些响应是独立目标,哪些是同一目标的多个部分。
6.3 与其它注意力模块的横向对比
| 模块组合 | 检测率 | 虚警率 | F1分数 | 参数量 |
|---|---|---|---|---|
| U-Net基线 | 82.3% | 15.7% | 0.831 | 7.8M |
| U-Net+SE | 83.5% | 14.2% | 0.843 | 7.9M |
| U-Net+CBAM | 84.1% | 13.8% | 0.849 | 8.1M |
| U-Net+DASI | 85.1% | 12.4% | 0.862 | 8.0M |
| U-Net+MDCR | 84.8% | 13.1% | 0.857 | 8.3M |
| U-Net+DASI+MDCR | 87.6% | 10.2% | 0.887 | 8.5M |
从表里能看出来,DASI和MDCR单独用都有提升,但组合起来提升最大。参数量只增加了0.7M,推理速度在RTX 3060上从45FPS降到38FPS,仍然满足实时性要求。SE和CBAM虽然参数量增加不多,但提升幅度有限,说明在红外小目标场景下,通用的注意力模块不如针对性的DASI有效。
7. 常见问题与排查技巧实录
7.1 训练不收敛或loss震荡
这是最常见的问题。首先检查学习率是不是太大了,DASI和MDCR里的注意力模块对学习率很敏感,建议先用1e-4跑几个epoch看看loss是否稳定下降。如果loss震荡,把学习率降到1e-4甚至5e-5。其次检查BatchNorm的momentum参数,默认0.1可能太大,改成0.01能让统计量更稳定。还有一个可能的原因是DASI里的Sigmoid激活导致梯度消失,可以在Sigmoid前加一个可学习的缩放系数,初始值设为1.0,让网络自己调整。
7.2 虚警率居高不下
如果虚警率降不下来,先可视化DASI的通道注意力权重,看看是不是某些背景通道的权重还是很高。如果是,说明通道注意力的MLP容量不够,把压缩比r从8降到4,增加MLP的参数量。另外检查空间注意力图,如果背景区域的空间权重也接近1,说明7×7卷积的感受野太大,改成5×5试试。还有一个容易被忽略的点:训练数据里负样本的比例。如果负样本太少,网络没见过足够多的背景模式,虚警率自然高。建议负样本和正样本的比例至少3:1。
7.3 小目标漏检严重
漏检通常是因为小目标的响应在特征图里太弱。先检查MDCR的膨胀率组合,如果全是大于2的膨胀率,小目标的感受野覆盖不到,把膨胀率1加进去。然后检查损失函数的小目标加权项,阈值是不是设得太低,导致小目标没拿到足够的权重。还有一个技巧:在DASI的空间注意力分支里,对最大池化的结果加一个可学习的温度系数,让最大响应的权重更突出。温度系数初始值设为1.0,训练中会自动调整。
7.4 推理速度不达标
DASI和MDCR都会增加计算量,如果推理速度不够,可以从几个方面优化。第一,把DASI的通道注意力MLP从两层改成一层,参数量减半,精度损失很小。第二,MDCR的膨胀卷积分支从三个减到两个,去掉膨胀率4的分支,对中等尺寸目标的影响不大。第三,把BN层和卷积层融合,推理时能省不少时间。第四,如果部署在边缘设备上,可以把DASI和MDCR的通道数减半,精度会降一点但速度提升明显。
7.5 不同数据集上的泛化问题
DASI和MDCR在某个数据集上调好的参数,换到另一个数据集上可能效果下降。原因是不同数据集的背景杂波类型不同,通道注意力和空间注意力的权重分布会变。我的经验是,换数据集时不要重新训练整个网络,只微调DASI和MDCR的注意力模块,编码器和解码器的卷积层冻结。微调时学习率用1e-4,跑20个epoch左右就能适应新数据集。如果效果还是不好,说明两个数据集的差异太大,需要在DASI里加一个域适应模块,但这超出了本文的范围。
8. 实操心得与后续扩展方向
这套方案我前后调了大概三个月,踩过的坑比写出来的多得多。最大的体会是:注意力模块不是越多越好,DASI和MDCR的组合之所以有效,是因为它们分别解决了跳层连接和解码器两个不同环节的问题,职责清晰。如果再加第三个注意力模块,提升会非常有限,反而增加过拟合风险。
还有一个心得:可视化比指标更重要。训练过程中一定要定期可视化DASI的通道权重和空间权重,看看网络到底在关注什么。我遇到过好几次指标看着还行但可视化一看全是背景的情况,这种模型在实际场景里根本没法用。可视化能帮你发现指标掩盖的问题。
后续可以扩展的方向有几个。一是把DASI的通道注意力从全局池化改成局部池化,用滑动窗口的方式计算通道权重,这样能保留更多空间信息。二是把MDCR的膨胀卷积换成可变形卷积,让感受野能自适应目标的形状。三是把DASI和MDCR的思路迁移到视频红外小目标检测,利用时序信息进一步抑制虚警。这几个方向我都在试,有进展再分享。