最近在做 UC Merced 遥感场景分类实验,一开始只是想简单跑一下ResNet50,看看这个模型的能力怎么样,分类结果如何。但是跑完后注视着 accuracy 也不知道能看出来个啥,心里只想着“哇,居然高达90%多”。于是我开始思考:这个模型到底在看什么,它到底是基于什么对图像进行分类的?后面查阅了大量资料,并在ChatGPT的帮助下,使得我对ResNet模型有了更深入的了解,所以我想写一篇文章主要记录一下我从 “models.resnet50()” 一路到

Grad-CAM

Feature Map

CBAM Attention

模型空间特征分析

的整个学习过程。


一、项目背景

UC Merced 是一个经典遥感场景分类数据集。包含 airplane、harbor、forest、river、runway 等21个类别,每类100张图像。我这里使用 ResNet50、ImageNet预训练、PyTorch 完成分类任务。


二、一开始其实只想“跑通”

最开始的时候,我其实对 ResNet50 并不理解。甚至:

layer1

layer2

layer3

layer4

到底是什么我都不知道。只是会:

model = models.resnet50(weights= models.ResNet50_Weights.DEFAULT)

然后训练。但后来我发现,如果只会调用模型,根本不能真正理解深度学习,没有太大意义。所以我后面开始一点点分析:

  • tensor shape
  • residual block
  • feature map
  • Grad-CAM

整个过程其实比单纯调包有意思很多。


三、先看一下 baseline 效果

这里是我使用 ResNet50 的训练结果:

整体 accuracy 其实已经不低。但我后来发现,accuracy 并不能说明一切。


四、混淆矩阵

我开始看混淆矩阵,看看模型的预测结果到底怎么样,如图所示:

我发现有些类别非常容易混淆,比如:

  • airplane ↔ storagetanks
  • dense residential ↔ medium residential

这不禁让我开始思考:CNN到底是怎么看出来的,它究竟是在关注图像的什么地方来做出判断的。


五、Grad-CAM

后来我开始尝试 Grad-CAM 可视化,并详细阅读了Grad-CAM简介(我觉得这位博主说得通俗易懂,非常佩服),说实话第一次看到的时候有些震惊,因为我第一次真正看到模型到底在关注哪里,比如:

上图中左图是原图像,右图是Grad-CAM可视化之后的热力图,并且均是模型预测错的图像,反复观察后发现很多情况下模型好像只关注局部区域,例如在 airplane 分类里:模型可能只看到:

  • 一个亮区域
  • 一个边缘
  • 一小块纹理

就将其预测为 airplane,而不是看到完整的飞机结构,这也就导致模型很容易分不清同样具有该特征的图像,如 storagetanks。

后来我开始查阅资料和文献,理解到卷积本身其实就是局部感受野,也就是说模型天然更容易学习局部纹理、边缘等特征,而不是全局空间关系。尤其遥感图像很多类别之间纹理和颜色都相似,所以在这上面CNN的局限性就暴露了出来。


六、尝试加入 CBAM

于是我开始思考有没有什么办法能够缓解这种症状,后来在ai的帮助下我接触到 attention,并开始查阅资料理解 CBAM(Convolutional Block Attention Module),其结构如图所示:

其核心其实就是 Channel Attention Module 和 Spatial Attention Module 两个子模块。其中,Channel Attention Module 主要关注“什么特征更重要”,Spatial Attention Module 主要关注图像的哪些位置更重要。(原论文地址:https://arxiv.org/abs/1807.06521)在仔细阅读两个模块的工作机理后,我自己手写了一个代码来实现,也是为了检测自己到底有没有真正理解。并且考虑到特征图经过layer4之后已经是高级语义特征,于是我将 CBAM 插入到l ayer4 和 avgpool 层之间,如图:

import torch.nn as nn
import torch
from torchvision import models

class CBAM(nn.Module):
    def __init__(self, channels, kernel_size=7, reduction=16):
        super().__init__()
        
        # 首先将输入的feature map经过两个并行的MaxPool层和AvgPool层
        self.channel_maxpool = nn.AdaptiveMaxPool2d(1)
        self.channel_avgpool = nn.AdaptiveAvgPool2d(1)
        
        # 激活函数Sigmoid
        self.sigmoid = nn.Sigmoid()
        
        # Share MLP模块
        self.MLP = nn.Sequential(
            nn.Conv2d(channels, channels//reduction, kernel_size=1, bias=False),
            nn.ReLU(inplace=True),
            nn.Conv2d(channels//reduction, channels, kernel_size=1, bias=False)
        )
        
        # Channel Attention Module模块的卷积运算
        self.spatial_conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size//2, bias=False)
    
    # 前向传播
    def forward(self, x):

        # Channel Attention
        channel_maxpool = self.MLP(self.channel_maxpool(x))
        channel_avgpool = self.MLP(self.channel_avgpool(x))
        channel_out = self.sigmoid(channel_maxpool+channel_avgpool)
        x = channel_out * x
        
        # Spatial Attention
        out_max, _ = torch.max(x, dim=1, keepdim=True)
        out_avg = torch.mean(x, dim=1, keepdim=True)
        spatial_out = self.sigmoid(self.spatial_conv(torch.cat((out_max, out_avg), dim=1)))
        x = spatial_out * x

        return x

class resnet50_cbam(nn.Module):
    def __init__(self, num_classes=21):
        super().__init__()
        
        # 加载resnet50
        backbone = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)

        self.conv1 = backbone.conv1
        self.bn1 = backbone.bn1
        self.relu = backbone.relu
        self.maxpool = backbone.maxpool

        self.layer1 = backbone.layer1
        self.layer2 = backbone.layer2
        self.layer3 = backbone.layer3
        self.layer4 = backbone.layer4
        
        # 插入CBAM
        self.cbam = CBAM(backbone.fc.in_features)
        self.avgpool = backbone.avgpool
        self.fc = nn.Linear(backbone.fc.in_features, num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
        
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.cbam(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

加入CBAM后的网络结构如下图所示:

这也让我对 tensor 在不同的 Attention 模块里到底是怎么变化的有了更深刻的印象。模型重新建立完成后我重新开始了训练,结果显示 accuracy 相差不大,但是相比于 baseline,模型关注的区域明显变化了,如下图所示(左图为 resnet50,右图为 resnet50_cbam):

由此可见,在进行 CBAM 操作后模型关注的区域变得全局了不少,并且关注的地方也渐渐正确了一点。


七、Feature Map可视化

当时为了弄清楚模型到底在学些什么东西,我打印出了耕地图像分别经过resnet50模型的layer1、layer2、layer3、layer4后的部分特征图,如下图所示:

layer1:

layer2:

layer3:

layer4:

起初经过layer1和layer2后的特征图我还觉得比较正常,但是经过layer3,特别是layer4之后,我发现经过layer4的特征图变得一片黑,后面深入理解CNN后,才知道这是正常现象。因为随着网络层数的加深,特征图的空间分辨率会逐渐降低、通道数逐渐增加、感受野不断增大,以及特征信息从低级细节(边缘、颜色、纹理)向高级语义(物体部件、场景类别)转变。


八、总结

通过这一个小项目我意识到了一些东西:首先深度学习绝不仅仅是跑通模型就够了,针对模型的性能好坏和实际业务的需求还需要我们能够发现问题、分析问题和解决问题;其次,我也真正开始理解了 CNN 到底怎么看图像、Attention到底在干什么、feature map到底是什么以及 Grad-CAM 的必要性。以前我只是跑通一个模型就过去了,现在我会开始思考模型为什么会这么预测,以及有了分析模型的一些手段,我觉得这对我来说是一个很大的变化。

最后我想说,虽然加入了 Attention 模块,但 CNN 对全局空间关系建模能力依旧有限,所以后续我准备继续探究:ViT、Swin Transformer、CNN 与 Transformer 的区别,看看 Transformer 是否真的更擅长遥感图像中的空间结构建模。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐