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的代码(可替换成自己的神经网络)。