首页 计算机视觉

Paper:The Devil is in the Channels: Mutual-Channel Loss for Fine-Grained Image Classification(TIP 2021)

1 Motivation and Advantage

简单来说,就是想做细粒度的图像分类(特征细腻,类间差异小,类内差异大等原因)。别人都是通过关键区域标注、注意力机制等方法解决,这篇文章作者另辟蹊径,从损失函数入手,很有创意。

但是作者不想给模型增加额外的网络参数,所以提出只用一个新的损失函数就能够很好地解决细粒度问题,并且提出的损失函数具有很好的移植性,理论上能用于现有的各种网络架构。

2 Total Effect

在这里插入图片描述

  • 上面部分:传统的细粒度分类模型,关注的是整个特征图,同时关注到了一幅图中的三个关键区域
  • 下面部分:MC-Loss方法,每个类别有多个通道,每个通道都对应一个判别区域

后面在方法介绍中细说

3 Methodology of MC-Loss

细粒度识别框架的整体流程(经典的细粒度分类模型,不针对具体某一种模型):

在这里插入图片描述

损失函数结合了Cross Entropy(CE)和 MC-Loss两者,将卷积神经网络最后一层的输出特征作为本文损失函数的输入。

总体的损失函数表示为CE和MC的加权和:

$Loss(F)=L_{CE}(F)+\mu\times L_{MC}(F)$

  • CE:促使网络提取有利于全局目标类别的判别特征
  • MC:关注不同局部判别块的特征

其中$F$是卷积层提取的特征,其中$F\in R^{N\times H\times W}$中$N$表示channel数,$H W$表示特征图的高和宽。

在MC-Loss中,$N=c\times \xi$,$c$表示类别总数,$\xi$表示每类的channel数(需要注意:$\xi$是一个超参,一般大于2,在paper中作者假定$\xi$是个定值)

对于某个类别$i$,特征表示为$F_i\in R^{\xi\times W\times H}, i=0, 1, 2, ..., c-1$

获取到各类别特征后,特征集表示为$F=\{F_0, F_1, ..., F_{c-1}\}$,把特征集输入流程分别计算CE和MC两个损失。

MC-Loss由两部分组成:判别模块和多样性,在paper中表示为$L_{dis}(F)$和$L_{div}(F)$,所以$L_{MC}(F)$表示为:

$L_{MC}(F)=L_{dis}(F)-\lambda L_{div}(F)$

在这里插入图片描述

  • 判别性模块(discriminality component)

作者设定$\xi$个channel的特征表示一个类别,这个模块要求这些特征关注某个特定的类别并且每个channel的特征要有足够的判别性。

$L_{dis}(F)$表示如下:

在这里插入图片描述

  • CWA:使用mask进行channel-attention,mask是个01掩模,随机选择一半的通道$\lfloor \xi \rfloor$为0,$M_i=diag(\text{Mask}_i)$为一个对角矩阵,其中0对应的位置(channel)被消除,其实就等同于是channel级别的dropout机制

    维度:$c\times \xi \times WH \to c\times \frac{\xi}{2} \times WH$

  • CCMP:跨channel的max pooling,在WH空间位置上选取最大值(avg pooling不适用细粒度分类?)

    $c\times \frac{\xi}{2} \times WH\to c\times 1 \times WH$

  • GAP:在空间维度进行avg pooling

    $c\times 1 \times WH \to c\times 1$

  • Softmax

借鉴别人博客中的一些解释:THE MUTUAL-CHANNEL LOSS (MC-LOSS,附代码分析)

在这里插入图片描述

  • 多样性模块(diversity component)

这个模块的目的是同一类别的$\xi$个channel应该关注图像的不同区域,而不是同一类的所有channel都关注最有判别性的区域。多样性组件不能单独用于分类,它充当判别器的正则化项,隐式发现图像中不同的原始区域的损失。

$L_{div}(F)$相当于计算各个channel特征间的距离,相比于欧氏距离和KL散度计算量更小。

$L_{div}(F)$表示如下:

在这里插入图片描述

  • Softmax:先对特征的每一位置Softmax,变为预测类别
  • CCMP:选取一个类别的$\xi$个channel中各空间位置的最大值
  • Sum:在空间位置求和,得到各类别的预测概率在所有channel上的和
  • Average:对各类别求均值,值越大表示模型对于所有类别,不同的channel都关注到了图像的不同区域 (故越大越好)

所以,这也是为什么$L_{MC}(F)=L_{dis}(F)-\lambda L_{div}(F)$这里是做减法

值得说明的是:$L_{div}(F)\in [1,\xi]$,取最大值时表示不同channel的特征注意到了图像的不同区域,取最小值时表示不同的channel的特征只注意到图像的同一区域

在这里插入图片描述

4 Conclusion

在这里插入图片描述

MC-Loss通过$L_{dis}(F)$使一个类别的$\xi$个channel尽可能学习该类别最有判别性的特征(如上图中同类别的多个通道都变成了同颜色),$L_{div}(F)$使各channel关注图像的不同空间位置(如上图中一组$\xi$个channel的特征的不同位置有了加深区域)

5 Core Code

def Mask(nb_batch, channels):
    foo = [1] * 2 + [0] * 1  # [1, 1, 0]
    bar = []
    for i in range(200):
        random.shuffle(foo)
        bar += foo
    # after the loop: bar->600
    bar = [bar for i in range(nb_batch)]
    bar = np.array(bar).astype("float32")
    bar = bar.reshape(nb_batch, 200 * channels, 1, 1)
    bar = torch.from_numpy(bar)
    bar = bar.cuda()
    bar = Variable(bar)
    return bar


# calculate L_MC
def supervisor(x, targets, height, cnum):
    mask = Mask(x.size(0), cnum)
    
    # calculate L_div
    branch = x
    branch = branch.reshape(branch.size(0), branch.size(1), branch.size(2) * branch.size(3))
    # Softmax
    branch = F.softmax(branch, 2)
    branch = branch.reshape(branch.size(0), branch.size(1), x.size(2), x.size(2))
    # CCMP
    branch = my_MaxPool2d(kernel_size=(1, cnum), stride=(1, cnum))(branch)
    branch = branch.reshape(branch.size(0), branch.size(1), branch.size(2) * branch.size(3))
    # SUM
    loss_2 = 1.0 - 1.0 * torch.mean(torch.sum(branch, 2)) / cnum  # set margin = 3.0
    
    # calculate L_dis
    # CWA
    branch_1 = x * mask  # channel-wise dropout
    # CCMP
    branch_1 = my_MaxPool2d(kernel_size=(1, cnum), stride=(1, cnum))(branch_1)  
    # GAP
    branch_1 = nn.AvgPool2d(kernel_size=(height, height))(branch_1)  
    branch_1 = branch_1.view(branch_1.size(0), -1)
    # Softmax
    loss_1 = criterion(branch_1, targets)
    
    return [loss_1, loss_2]  # MC_loss includes 2 parts: dis and div


class model_bn(nn.Module):
    def __init__(self, feature_size=512, classes_num=200):
        super(model_bn, self).__init__()
        self.features_1 = nn.Sequential(*list(VGG('VGG16').features.children())[:34])
        self.features_2 = nn.Sequential(*list(VGG('VGG16').features.children())[34:])
        self.max = nn.MaxPool2d(kernel_size=2, stride=2)
        self.num_ftrs = 600 * 7 * 7
        self.classifier = nn.Sequential(
            nn.BatchNorm1d(self.num_ftrs),
            # nn.Dropout(0.5),
            nn.Linear(self.num_ftrs, feature_size),
            nn.BatchNorm1d(feature_size),
            nn.ELU(inplace=True),
            # nn.Dropout(0.5),
            nn.Linear(feature_size, classes_num),
        )

    def forward(self, x, targets):
        x = self.features_1(x)
        x = self.features_2(x)
        if self.training:
            MC_loss = supervisor(x, targets, height=14, cnum=3)
        x = self.max(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        loss = criterion(x, targets)  # cross_entropy loss
        if self.training:
            return x, loss, MC_loss  # return ce_loss and MC_loss
        else:
            return x, loss
        
# in train function
out, ce_loss, MC_loss = net(inputs, targets)
loss = ce_loss + args["alpha_1"] * MC_loss[0] + args["beta_1"] * MC_loss[1]
loss.backward()

上面即是MC-Loss的核心代码,其余部分其实就是搭建CNN的代码(可替换成自己的神经网络)。




文章评论

目录