Feature Boosting, Suppression, and Diversification for Fine-Grained Visual Classification
这是一篇2021年放在arxiv上面的一篇关于细粒度识别的文章,提出了两个新颖的模块,称之为:FBSM特征增强和抑制模块,FDM特征融合模块。经验证、效果显著!
论文地址: https://arxiv.org/abs/2103.02782
Github地址: https://github.com/chaomaer/FBSD
机构:National Key Laboratory for Novel Software Technology
Nanjing University, Nanjing 210023, China
论文阅读
1.问题动机
从有区别的局部区域学习特征表示在细粒度视觉分类中起着关键作用。利用注意力机制提取零件特征已经成为一种趋势。然而,这些方法有两个主要的局限性:首先,它们通常只关注最显著的部分,而忽略了其他不明显但可区分的部分。其次,他们孤立地对待不同的零件特征,而忽略了它们之间的关系。
2.解决思路
通过引入两个轻量级模块,它们可以很容易地插入到现有的卷积神经网络中。一方面,引入了一个特征增强和抑制模块FBSM,该模块增强特征图中最显著的部分以获得特定于零件的表示,并抑制它以迫使后续网络挖掘其他潜在零件。另一方面,引入了一个特征多样化模块,它从相关的部件特定表示中学习语义互补的信息。结构图如下;
2.1FBSM
首先是从主干网络中的某一特定的层输出的特征图X开始,送入FBSM模块,具体来说:
一、获得最显著性区域特征图
1、先是把特征向量沿着宽维度分割成k部分,得到x(i)
2、接着同等对待这k部分特征向量、先经过1 × 1 卷积把通道数变为一,接着跟上BatchNorm2d函数和Relu函数进行数据的归一化。
3、然后跟上自适应的平均池化层进行压缩空间维度信息,使每一部分的空间高和宽大小变为1 × 1 、得到每一部分的重要性因子。
4、再把这k部分在宽维度上拼接起来得到B’。
5、在对宽维度的k个数据进行归一化,得到每一部分对应的归一化后的分数(即重要性因子)。
6、再使用torch.repeat_interleave函数将宽维度上的size恢复到输入到FBSM时的size。
7、此时已经可以根据分数判断最显著性的区域,紧接着根据公式获取最显著性区域的特征图
X
b
X_b
Xb:
8、此时最显著性的特征图已经可以水平送入到下一阶段,即FDM模块。
二、获取最显著性区域被抑制的特征图
1、通过之前获得的最显著性部分、即可获得抑制特征图
X
s
X_s
Xs,根据公式可求得抑制性特征图中每一部分的重要性因子,我个人理解为除了之前得到的最显著性区域以外的部分都是重要的(用1表示),就是要迫使网络学习除了最显著性特征以外的其他的具有区别性的特征。
2、与原始特征图点乘得到抑制性特征图
X
s
X_s
Xs
3、把抑制性特征图
X
s
X_s
Xs垂直送入到下一个FBSM模块
In short, the functionality of FBSM can be expressed as:
F
B
S
M
(
X
)
=
(
X
b
,
X
s
)
FBSM(X)=(X_b,X_s)
FBSM(X)=(Xb,Xs). Given feature maps X, FBSM outputs part-specific feature and potential feature maps
. Since suppresses the most salient part in current stage, other potential parts will stand out after feeding into the following stage
2.2FDM
看论文看的头大,还是看代码理解的更容易理解。下面结合代码讲述一下FDM的工作原理。
1、FDM的输入为上一模块FDSM的输出、即最显著性特征图
X
b
1
X_{b1}
Xb1,
X
b
2
X_{b2}
Xb2,
X
b
3
X_{b3}
Xb3,然后进过卷积运算,通道数先都变为512,再都变为1024,最后得到通道注意力图att1,att2,att3。代码以第一层特征图为例、如下:
#fm1就表示FBSM输出的显著性特征图,即Xb1,以第一层为例
att1 = self.conv_block1(fm1)
#feature_size = 512,part_feature = 1024
self.conv_block1 = nn.Sequential(
BasicConv(chans[0], feature_size, kernel_size=1, stride=1, padding=0, relu=True),#chans[0]为输入特征图的通道数
BasicConv(feature_size, part_feature, kernel_size=3, stride=1, padding=1, relu=True),
)
2、将得到的通道注意力送入FDM模块(进行特征的融合,即多样化处理),注意注意力向量两两结合的方式,1和2、1和3、2和3。最后可以得到两张注意图相互之间互补的信息。
new_d1_from2, new_d2_from1 = self.inter(att1, att2) # 1 2
print("new_d1_from2.shape:{:}".format(new_d1_from2.shape))
print("new_d2_from1.shape:{:}".format(new_d2_from1.shape))
new_d1_from3, new_d3_from1 = self.inter(att1, att3) # 1 3
print("new_d1_from3.shape:{:}".format(new_d1_from3.shape))
print("new_d3_from1.shape:{:}".format(new_d3_from1.shape))
new_d2_from3, new_d3_from2 = self.inter(att2, att3) # 2 3
print("new_d2_from3.shape:{:}".format(new_d2_from3.shape))
print("new_d3_from2.shape:{:}".format(new_d3_from2.shape))
self.inter = FDM()
class FDM(nn.Module):
def __init__(self):
super(FDM, self).__init__()
self.factor = round(1.0/(28*28), 3)
print("self.factor:{:}".format(self.factor))
def forward(self, fm1, fm2):
b, c, w1, h1 = fm1.shape
_, _, w2, h2 = fm2.shape
fm1 = fm1.view(b, c, -1) # B*C*S
print("==========FDM==========")
print("fm1.shape:{:}".format(fm1.shape))
fm2 = fm2.view(b, c, -1) # B*C*M
print("fm2.shape:{:}".format(fm2.shape))
fm1_t = fm1.permute(0, 2, 1) # B*S*C
# may not need to normalize
fm1_t_norm = F.normalize(fm1_t, dim=-1)
fm2_norm = F.normalize(fm2, dim=1)
M = -1 * torch.bmm(fm1_t_norm, fm2_norm) # B*S*M
M_1 = F.softmax(M, dim=1)
M_2 = F.softmax(M.permute(0, 2, 1), dim=1)
new_fm2 = torch.bmm(fm1, M_1).view(b, c, w2, h2)
new_fm1 = torch.bmm(fm2, M_2).view(b, c, w1, h1)
return self.factor*new_fm1,self.factor* new_fm2
注意论文里面有这样一句话,我想可以这样理解:
Y
p
1
p
2
Y{^{p_2}_{p_1}}
Yp1p2表示P1相对于P2的互补信息,那么
Y
p
2
p
1
Y{^{p_1}_{p_2}}
Yp2p1表示P2相对于P1的互补信息,确实有点绕啊,但是却很好的表达了特征和融合的思想。
3、接下来,就是把刚才得到的互补的信息进行融合得到增强后的特征(此时的两两融合不同于之前的组合方式、可以回头看下框架图的PCM部分)总之,作者设计的很巧妙。
gamma = HyperParams['gamma']
att1 = att1 + gamma*(new_d1_from2 + new_d1_fr`在这里插入代码片`om3)
print("==========FDM_OUT==========")
print("att1.shape(att1 = att1 + gamma*(new_d1_from2 + new_d1_from3)):{:}".format(att1.shape))
att2 = att2 + gamma*(new_d2_from1 + new_d2_from3)
print("att2.shape(att2 = att2 + gamma*(new_d2_from1 + new_d2_from3)):{:}".format(att2.shape))
att3 = att3 + gamma*(new_d3_from1 + new_d3_from2)
print("att3.shape(att3 = att3 + gamma*(new_d3_from1 + new_d3_from2)):{:}".format(att3.shape))
4、对上一步得到的增强后的特征图进行分层池化(TopK)
xl1 = self.pool(att1)
self.pool = TopkPool()
class TopkPool(nn.Module):
def __init__(self):
super(TopkPool, self).__init__()
def forward(self, x):
b, c, _, _ = x.shape
x = x.view(b, c, -1)
topkv, _ = x.topk(5, dim=-1)
return topkv.mean(dim=-1)
5、最后进行线性分类
xc1 = self.classifier1(xl1)
self.classifier1 = nn.Sequential(
nn.BatchNorm1d(part_feature),
nn.Linear(part_feature, feature_size),
nn.BatchNorm1d(feature_size),
nn.ELU(inplace=True),
nn.Linear(feature_size, class_num)
至此结束!
论文复现
1.环境
此外,我还使用了Cudnn进行训练加速。
torch.backends.cudnn.benchmark = True
2.运行程序
修改config.py文件里的参数,然后运行:
python train.py
2.1Resnet50主干
bs=20,epoch=200
2.2Densenet161主干
bs=10,epoch=265
3.训练得到的精度与论文里的比较
我认为质量很高了,精度非常接近。力挺!我训练的竟然在Densenet161还超过了论文里的精度!