论文阅读:Interpretable and Accurate Fine-grained Recognition via Region Grouping

Interpretable and Accurate Fine-grained Recognition via Region Grouping

通过区域分组实现可解释且准确的细粒度识别。根据,特征图获得每个语义部分的分配图,再计算出每个语义的特征向量,形成特征矩阵,后续变换和分类。

摘要

本文方法核心:在深度神经网络中集成基于区域的部分发现和属性。通过对象部分的分割以及识别它们对分类的贡献来解释结果。为了便于在没有直接监督的情况下学习对象部分,探索了出现对象部分的简单先验当与基于区域的部分发现和归因相结合时,可产生保持较高准确性的可解释模型。

1 引言

尽管模型的解释可以在多个方面进行,但解释模型的至少一种方法是分割对象部分的有意义区域,并进一步确定其对决策的贡献。如何设计一个可解释的深层模型,能学着发现对象部分并评估其对视觉识别的重要性。

部分发现,即在没有显式监督信息的情况下学习目标部分,本身就很难。来自卷积网络的特征可用于将像素分组为一组视觉上相干的区域,从中可以选择辨别的子集进行识别。 希望仅以对象标签为指导,希望分组有助于找到视觉上截然不同的部分,并且选择过程将确定它们对分类的贡献。

基于区域的部分发现的主要挑战是,没有明确的监督信号来定义部分区域。 必须结合有关对象部分的先验知识以促进学习。 本文的核心创新是探索关于对象部分的简单先验:给定单个图像,部分的出现遵循Beta分布,例如大多数鸟类图像中很可能会出现鸟头。 这种简单的先验知识与基于区域的部分发现相结合,可以识别出有意义的对象部分,且结果可解释的深度模型仍然非常准确。

本文模型学习了对象部分的字典,可以将2D特征图分组为部分片段。这是通过将像素特征与学习字典中的部分表示进行比较完成的。从结果片段中合并基于区域的特征,然后通过注意力机制选择片段的子集进行分类。

在训练过程中,针对每个部分的出现强制执行先前的Beta分布,保证每个批处理中都是二分类的。 这是通过最大程度地减小部分在先发生与经验发生之间的Earth Mover距离来完成的。训练期间,模型仅受带正则化项的对象标签的监督。 在测试过程中,模型输出目标部分的分段,分段部分的重要性和预测的标签。 模型的解释是通过部分分割和其对分类的贡献来进行的。

在三个细粒度数据集上进行实验,获取可解释性和准确性:

  1. 可解释性,将模型中的输出区域片段与带注释的对象部分进行了比较。在较小数据集上,局部定位误差很小。
  2. 准确性,用于细粒度分类的标准度量

2 相关研究

深度网络的探索

许多方法专注于开发激活图和/或滤波器权重的可视化工具。其他工作试图在输入图像中识别区分区域。量化基准,将网络单元的激活与人工注释的概念蒙版进行比较。、

深度模型的可解释性

许多研究开发了可通过其设计解释的深层模型。 或者,可以为可移植模型设计新的网络体系结构。

本文模型试图明确编码对象部分的概念,和以往工作的不同:

  1. 采用感知分组以提供基于图像段的解释
  2. 学习是通过对象部分出现之前的强先验来进行正则化的

部分发现

bounding box、annotations、弱监督/无监督、注意力。

本文研究试图找到部分并确定它们对细粒度分类的重要性,但考虑了对象部分出现的显式正则化

弱监督语义分割

探索了新颖的正则化方法来学习分割对象部分。考虑了细粒度分类的背景下弱监督的零件分割。还探索了一种分厂不同的部分出现的先验。

基于区域的识别

模型将分割和分类组合成一个深层模型,从而链接到基于区域的识别或更广泛的组成学习的努力。

本文模型对CNN特征进行分组。以前的工作都没有关注分组的质量,因此不能直接用于解释。

3 方法

image-20210430200301190

关键假设是,可通过对一组图像特征 X 1 : N X_{1:N} X1:N中每个部分 d k d_k dk发生强制执行先验分布来规范学习:给定 X 1 : N X_{1:N} X1:N,令 p ( d k ∣ X 1 : N ) p(d_k|X_{1:N}) p(dkX1:N)为部分 d k d_k dk在集合 X 1 : N X_{1:N} X1:N中出现的条件概率。假设 p ( d k ∣ X 1 : N ) p(d_k|X_{1:N}) p(dkX1:N)遵循一个U形分布,其作用类似于概率二进制开关,可以控制“开”和“关”的概率。(不知道什么意思)

image-20210430215842371

3.1 部分分割和正则化

部分分配

相似度投影单元: ( X , D ) → Q (X,D)\to Q (X,D)Q

image-20210430205324820

每个通道用来生成分配到某个语义的可能性

发现部分

平滑处理更有效,平滑操作有助于消除特征图上的异常值。

部分检测器定义为 t k = max ⁡ i j G ∗ Q k t_k=\max_{ij}G∗Q^k tk=maxijGQk,其中G是2D高斯核,∗是卷积运算。 t k t_k tk ( 0 , 1 ) (0,1) (0,1)的范围内。 此外,将k个部分检测器的输出串联到所有部分的出现向量 τ = [ t 1 , t 2 , . . . , t K ] T ∈ ( 0 , 1 ) K τ= [t_1,t_2,...,t_K] ^T∈(0,1)^K τ=[t1t2...tK]T01K上。

正则

关键思想是规范每个部分的出现,通过强制部分出现的经验分布和先验U形Beta分布对齐来完成的

给定 N N N个样本,串联所有向量 τ n τ_n τn来估计经验分布 p ( d k ∣ X 1 : N ) p(d_k|X_{1:N}) p(dkX1:N)形成矩阵 T = [ τ 1 , τ 2 , . . . , τ N ] ∈ ( 0 , 1 ) K × N T = [τ_1,τ_2,...,τ_N]\in(0,1)^{K×N} T=[τ1τ2...τN](01)K×N假设 p ^ ( d k ∣ X 1 : N ) \hat p(d_k|X_{1:N}) p^(dkX1:N)已知(比如,服从Beta分布),根据以下公式计算Earth Mover距离:

image-20210430210813610

在小批量训练中,可以通过将积分替换为小批量内样本的总和来计算该距离,从而得出 F − 1 F^{-1} F1 F ^ − 1 \hat F^{-1} F^1之间的L1距离。 在实践中发现,使用对数函数重新调整CDF的逆很有帮助,从而提高了训练的稳定性。

image-20210430211346931

其中 τ k ∗ τ^∗_k τk T T T(大小N)的第k行向量的排序版本(升序),[τ∗ k] i是τ∗ k的第i个元素。 是为数值稳定性添加的一个小值。使用对数重定标度可以克服由等式1中的softmax函数引入的梯度消失问题。即使部分 d k d^{k} dk远离当前批处理中的所有特征向量,即等式1中的 q i j k q^k_{ij} qijk值也很小,由于重新缩放, d k d_k dk仍然可以接收非零梯度。

3.2 区域特征提取与归因

image-20210430212025672

z k z_k zk是来自分配给部件 d k d_k dk的像素的区域特征。将 z k z_k zk组合在一起,获得区域特征集 Z = [ z 1 , z 2 , . . . , z K ] ∈ R D × K Z = [z_1,z_2,...,z_K]\in R^{D×K} Z=[z1,z2,...,zK]RD×K。 使用具有几个残差块的子网 f z f_z fz进一步变换 Z Z Z,变换后的特征为 f z ( Z ) f_z(Z) fz(Z)
此外,在 Z Z Z上方附加了注意模块,以预测每个区域的重要性。 这是通过子网络 f f f实现的,该子网络由 a = s o f t m a x ( f a ( Z T ) ) a = softmax(f_a(Z^T)) a=softmax(fa(ZT))给出,结果将进一步用于分类。

3.3 基于注意力的分类

image-20210430212557475

a a a为区域特征 Z Z Z的调制器, a a a中的较大值表示一个更重要的分类区域

根据 a a a可以回溯特征图上每个像素的贡献,,通过使用 Q T a Q^Ta QTa来完成, Q ∈ R K × H W Q\in R^{K×HW} QRK×HW

4 实验

cub上效果才87.3%,对细粒度分类而言一般般

image-20210430213110651

image-20210430213337003

image-20210430213412273

image-20210430213423225

局限性和讨论

模型在iNaturalist上失败了。模型可能无法将像素分组为部分区域,有时会产生不正确的显着性贴图。在iNaturalist上使用超过5K的细粒度类别时,U形分布可能无法描述零件的出现。模型不对部分之间的相互作用进行建模,而是需要中等到较大的批量来估计部分出现的经验分布。 因此,未来方向是探索对象部分的更好先验。

5 结论

提出了一种可解释的深度模型。模型利用了对象部分出现的先验,并将基于区域的零件发现和归因集成到了深度网络中。 经过图像级别标签的训练,可以预测对象部分的分配图,部分区域和对象标签的注意图,证明了在对象分类和对象零件定位方面的出色结果。

6 代码

代码中居然没有预训练部分,模型直接修改了resnet的结构。主要是下面的:

# import libs
import math
import numpy as np
import torch
import torch.nn as nn


class GroupingUnit(nn.Module):

    def __init__(self, in_channels, num_parts):
        super(GroupingUnit, self).__init__()
        self.num_parts = num_parts
        self.in_channels = in_channels

        # params
        self.weight = nn.Parameter(torch.FloatTensor(num_parts, in_channels, 1, 1))
        self.smooth_factor = nn.Parameter(torch.FloatTensor(num_parts))

    def reset_parameters(self, init_weight=None, init_smooth_factor=None):
        if init_weight is None:
            # msra init
            nn.init.kaiming_normal_(self.weight)
            self.weight.data.clamp_(min=1e-5)
        else:
            # init weight based on clustering
            assert init_weight.shape == (self.num_parts, self.in_channels)
            with torch.no_grad():
                self.weight.copy_(init_weight.unsqueeze(2).unsqueeze(3))

        # set smooth factor to 0 (before sigmoid)
        if init_smooth_factor is None:
            nn.init.constant_(self.smooth_factor, 0)
        else:
            # init smooth factor based on clustering 
            assert init_smooth_factor.shape == (self.num_parts,)
            with torch.no_grad():
                self.smooth_factor.copy_(init_smooth_factor)

    def forward(self, inputs):
        assert inputs.dim() == 4

        # 0. store input size
        batch_size = inputs.size(0)
        in_channels = inputs.size(1)
        input_h = inputs.size(2)
        input_w = inputs.size(3)
        assert in_channels == self.in_channels

        # 1. generate the grouping centers
        grouping_centers = self.weight.contiguous().view(1, self.num_parts, self.in_channels).expand(batch_size,
                                                                                                     self.num_parts,
                                                                                                     self.in_channels)

        # 2. compute assignment matrix
        # - d = -\|X - C\|_2 = - X^2 - C^2 + 2 * C^T X
        # C^T X (N * K * H * W)
        inputs_cx = inputs.contiguous().view(batch_size, self.in_channels, input_h * input_w)
        cx_ = torch.bmm(grouping_centers, inputs_cx)
        cx = cx_.contiguous().view(batch_size, self.num_parts, input_h, input_w)
        # X^2 (N * C * H * W) -> (N * 1 * H * W) -> (N * K * H * W)
        x_sq = inputs.pow(2).sum(1, keepdim=True)
        x_sq = x_sq.expand(-1, self.num_parts, -1, -1)
        # C^2 (K * C * 1 * 1) -> 1 * K * 1 * 1
        c_sq = grouping_centers.pow(2).sum(2).unsqueeze(2).unsqueeze(3)
        c_sq = c_sq.expand(-1, -1, input_h, input_w)
        # expand the smooth term
        beta = torch.sigmoid(self.smooth_factor)
        beta_batch = beta.unsqueeze(0).unsqueeze(2).unsqueeze(3)
        beta_batch = beta_batch.expand(batch_size, -1, input_h, input_w)
        # assignment = softmax(-d/s) (-d must be negative)
        assign = (2 * cx - x_sq - c_sq).clamp(max=0.0) / beta_batch
        assign = nn.functional.softmax(assign, dim=1)  # default dim = 1

        # 3. compute residual coding
        # NCHW -> N * C * HW
        x = inputs.contiguous().view(batch_size, self.in_channels, -1)
        # permute the inputs -> N * HW * C
        x = x.permute(0, 2, 1)

        # compute weighted feats N * K * C
        assign = assign.contiguous().view(batch_size, self.num_parts, -1)
        qx = torch.bmm(assign, x)

        # repeat the graph_weights (K * C) -> (N * K * C)
        c = grouping_centers

        # sum of assignment (N * K * 1) -> (N * K * K)
        sum_ass = torch.sum(assign, dim=2, keepdim=True)

        # residual coding N * K * C
        sum_ass = sum_ass.expand(-1, -1, self.in_channels).clamp(min=1e-5)
        sigma = (beta / 2).sqrt()
        out = ((qx / sum_ass) - c) / sigma.unsqueeze(0).unsqueeze(2)

        # 4. prepare outputs
        # we need to memorize the assignment (N * K * H * W)
        assign = assign.contiguous().view(
            batch_size, self.num_parts, input_h, input_w)

        # output features has the size of N * K * C 
        outputs = nn.functional.normalize(out, dim=2)
        outputs_t = outputs.permute(0, 2, 1)

        # generate assignment map for basis for visualization
        return outputs_t, assign

    # name
    def __repr__(self):
        return self.__class__.__name__ + ' (' \
               + str(self.in_channels) + ' -> ' \
               + str(self.num_parts) + ')'

模型:


class ResNet(nn.Module):

    def __init__(self, block, layers, num_classes=1000, num_parts=32):
        super(ResNet, self).__init__()

        # model params
        self.inplanes = 64
        self.n_parts = num_parts
        self.num_classes = num_classes

        # modules in original resnet as the feature extractor
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)

        # the grouping module
        self.grouping = GroupingUnit(256 * block.expansion, num_parts)
        self.grouping.reset_parameters(init_weight=None, init_smooth_factor=None)

        # post-processing bottleneck block for the region features
        self.post_block = nn.Sequential(
            Bottleneck1x1(1024, 512, stride=1, downsample=nn.Sequential(
                nn.Conv2d(1024, 2048, kernel_size=1, stride=1, bias=False),
                nn.BatchNorm2d(2048))),
            Bottleneck1x1(2048, 512, stride=1),
            Bottleneck1x1(2048, 512, stride=1),
            Bottleneck1x1(2048, 512, stride=1),
        )

        # an attention for each classification head
        self.attconv = nn.Sequential(
            Bottleneck1x1(1024, 256, stride=1),
            Bottleneck1x1(1024, 256, stride=1),
            nn.Conv2d(1024, 1, kernel_size=1, stride=1, padding=0, bias=True),
            nn.BatchNorm2d(1),
            nn.ReLU(),
        )

        # the final batchnorm
        self.groupingbn = nn.BatchNorm2d(2048)

        # linear classifier for each attribute
        self.mylinear = nn.Linear(2048, num_classes)

        # initialize convolutional layers with kaiming_normal_, BatchNorm with weight 1, bias 0
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

        # initialize the last bn in residual blocks with weight zero
        for m in self.modules():
            if isinstance(m, Bottleneck) or isinstance(m, Bottleneck1x1):
                nn.init.constant_(m.bn3.weight, 0)
            elif isinstance(m, BasicBlock):
                nn.init.constant_(m.bn2.weight, 0)

    # layer generation for resnet backbone
    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):

        # the resnet backbone
        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)

        # grouping module upon the feature maps outputed by the backbone
        region_feature, assign = self.grouping(x)
        region_feature = region_feature.contiguous().unsqueeze(3)

        # generate attention
        att = self.attconv(region_feature)
        att = F.softmax(att, dim=2)

        # non-linear layers over the region features
        region_feature = self.post_block(region_feature)

        # attention-based classification
        # apply the attention on the features
        out = region_feature * att
        out = out.contiguous().squeeze(3)

        # average all region features into one vector based on the attention
        out = F.avg_pool1d(out, self.n_parts) * self.n_parts
        out = out.contiguous().unsqueeze(3)

        # final bn
        out = self.groupingbn(out)

        # linear classifier
        out = out.contiguous().view(out.size(0), -1)
        out = self.mylinear(out)

        return out, att, assign

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值