当前位置:   article > 正文

详细理解(学习笔记) | DETR(整合了Transformer的目标检测框架) DETR入门解读以及Transformer的实操实现

detr

一、概述

DETR,全称 DEtection TRansformer,是Facebook提出的基于Transformer的端到端目标检测网络,发表于ECCV2020。

原文:
链接
源码:
链接

DETR 端到端目标检测网络模型,是第一个将Transformer成功整合为检测pipline中心构建块的目标检测框架模型。基于Transformers的端到端目标检测,没有NMS后处理步骤、真正的实现不利用anchor,且其与Faster RCNN相比,超越了后者。

在COCO数据集上,对比效果图如下:

图片来自原文
由上图可以看出,DETR的效果很不错。基于ResNet50的DETR取得了和经过各种精调的Faster-RCNN相当的效果。同时DETR大目标检测上性能是最好的,但是小目标上稍差,而且基于match的loss导致学习很难收敛(即难以学习到最优的情况)。Deformable DETR的出现对这两个问题进行了比较好的改进。

Detection TransformerDETR的新框架的主要组成部分是基于集合的全局损失函数,该损失函数通过二分匹配和transformer编码器-解码器体系结构强制进行唯一的预测。给定一个固定的学习对象查询的小集合,DETR会考虑目标对象与全局图像上下文之间的关系,并直接并行输出最终的预测集合。

与许多其他现代检测器不同,新模型在概念上很简单,并且不需要专门的库。DETR与具有挑战性的COCO对象检测数据集上公认的且高度优化的Faster R-CNN baseline具有同等的准确性和运行时性能。此外,可以很容易地将DETR迁移到其他任务例如全景分割

Detection Transformer可以预测所有物体的剧烈运动,并通过设置损失函数进行端到端训练,该函数可以在预测的物体与地面真实物体之间进行二分匹配。DETR通过删除多个手工设计的后处理过程例如nms,对先验知识进行编码的组件来简化检测流程。与大多数现有的检测方法不同,DETR不需要任何自定义层,因此可以在包含标准CNN和转换器类的任何框架中轻松复制。

二、Transformer

Transformer,原文,自被提出以来,便迅速得到了广泛地应用.它的核心是注意力机制的叠加使用,使得AI模型有选择地聚焦于输入的某些部分,因此推理更加高效。不仅在NLP领域上有了显著的成果,现在更是被挪用到了CV领域。它本质上仍然是一个Encoder-Decoder的结构,encoder和decoder均是一种Self-Attention模块的多重叠加,通过编码-解码的形式,实现以多重多头注意力学习获取重要特征,然后两相结合实现整合得到图像的“上下文语序信息”,从而更好地进行目标检测,模块结构如下图所示:

在这里插入图片描述
在这里插入图片描述
Encoder模块:
在这里插入图片描述

和传统的序列模型如RNN相比,Transformer主要改进于:

  1. 将RNN变为多个自注意力Self-Attention结构的叠加
  2. 并行计算序列中任意元素相对于其他所有元素的相关性,高效地提取上下文中的相关性,并引入多头注意力Multi-head Attention机制,从多角度提取特征。
  3. 位置编码来描述序列的前后信息,取代了RNN串行的计算过程。

Transformer的pytorch实现

使用pytorch接口展示Transformer的实际用法如下这篇文章
Transformer实战

pytorch 对Transformer的封装是通过 torch.nnTransformer 来实现的,其主要包含以下几个参数:

torch.nn.Transformer(d_model: int = 512,nhead: int = 8,num_encoder_layers: int = 6,num_decoder_layers: int = 6,dim_feedforward: int = 2048,dropout: float = 0.1,activation: str = 'relu',custom_encoder: Optional[Any] = None,custom_decoder: Optional[Any] = None)
  • 1

其中,
d_model 是 word embedding 的 channel 数,
n_head 是多头注意力的头的个数,
num_encoder_layers 和 num_decoder_layers 分别对应编码器和解码器的自注意模块的叠加的次数,
dim_feedforward对应编码器-解码器中的 Linear层的维度。

nn.Transformer 的 forward 函数实现了编码和解码的过程:

forward(src: torch.Tensor,tgt: torch.Tensor,src_mask: Optional[torch.Tensor] = None,tgt_mask: Optional[torch.Tensor] = None,memory_mask: Optional[torch.Tensor] = None,src_key_padding_mask: Optional[torch.Tensor] = None,tgt_key_padding_mask: Optional[torch.Tensor] = None,memory_key_padding_mask: Optional[torch.Tensor] = None)→ torch.Tensor
  • 1

其中,必须输入的两个参数是 src和tgt,分别对应于编码器的输入inputs和解码器的输入outputs。tgt的作用类似于一种条件约束,Decoder的第一层的tgt输入是一个词嵌入向量,从第二层开始是前一层的计算结果。

其他可选参数中,[src/tgt/memory]_mask是一个掩码数组,定义了计算Attention的策略,对应于原文的3.1节。一个通俗解释为:一个词序列中,每个词只能被它前面的词所影响,所以这个词后面的所有位置都需要被忽略,所以在计算Attention的时候,该词向量和它后面的词向量的相关性为0。(然而,实际上每个词特别是在中文中,每个词应该是和上下文语义、语序相关,才能更好的学习到该词的具体含义。

[src,tgt,memory]_key_padding_mask也是掩码数组,定义了src, tgt和memory中哪些位置需要保留,哪些需要忽略。

三、DETR

DETR的思路和传统的目标检测的本质思路有相似之处,但表现方式很不一样。传统的方法比如Anchor-based方法本质上是对预定义的密集anchors进行类别的分类和边框系数的回归。DETR则是将目标检测视为一个集合预测问题(集合和anchors的作用类似)。由于Transformer本质上是一个序列转换的作用,因此,可以将DETR视为一个从图像序列到一个集合序列的转换过程。该集合实际上就是一个可学习的位置编码(文章中也称为object queries或者output positional encoding,代码中叫作query_embed)。

DETR 的网络结构图(算法流程):
在这里插入图片描述
DETR使用的Transformer结构:
在这里插入图片描述
spatial positional encoding是作者自己提出的二维空间位置编码方法,该位置编码分别被加入到了encoderself attentiondecodercross attention,同时object queries也被加入到了decoder的两个attention中。而原版的Transformer将位置编码加到了input和output embedding中。值得一提的是,作者在消融实验中指出即使不给encoder添加任何位置编码,最终的AP也只比完整的DETR下降了1.3个点。

代码基于PyTorch重写了TransformerEncoderLayer, TransformerDecoderLayer类,用到的PyTorch接口只有nn.MultiheadAttention类。源码需要PyTorch 1.5.0以上版本。

代码核心位于models/transformer.pymodels/detr.py

Transformer.py

class Transformer(nn.Module):

    def __init__(self, d_model=512, nhead=8, num_encoder_layers=6,
                 num_decoder_layers=6, dim_feedforward=2048, dropout=0.1,
                 activation="relu", normalize_before=False,
                 return_intermediate_dec=False):
        super().__init__()
        encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        encoder_norm = nn.LayerNorm(d_model) if normalize_before else None
        self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm)
        decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        decoder_norm = nn.LayerNorm(d_model)
        self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm,
                                          return_intermediate=return_intermediate_dec)

    def forward(self, src, mask, query_embed, pos_embed):
        # flatten NxCxHxW to HWxNxC
        bs, c, h, w = src.shape
        src = src.flatten(2).permute(2, 0, 1)
        pos_embed = pos_embed.flatten(2).permute(2, 0, 1)
        query_embed = query_embed.unsqueeze(1).repeat(1, bs, 1)
        mask = mask.flatten(1)

        tgt = torch.zeros_like(query_embed)
        memory = self.encoder(src, src_key_padding_mask=mask, pos=pos_embed)
        hs = self.decoder(tgt, memory, memory_key_padding_mask=mask,
                          pos=pos_embed, query_pos=query_embed)
        return hs.transpose(1, 2), memory.permute(1, 2, 0).view(bs, c, h, w)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

Transformer类包含了一个Encoder和一个Decoder对象。相关类的实现均可在transformer.py中找到。重点看forward函数,有一个对输入tensor的变换操作:# flatten NxCxHxW to HWxNxC。

结合PyTorch中对src和tgt的形状定义可以发现,**DETR的思路是将backbone输出特征图的像素展开成一维后当成了序列长度,而batch和channel的定义不变。**故而DETR可以计算特征图的每一个像素相对于其他所有像素的相关性,这一点在CNN中是依靠感受野来实现的,可以看出Transformer能够捕获到比CNN更大的感受范围。

DETR在计算attention的时候没有使用masked attention,因为将特征图展开成一维以后,所有像素都可能是互相关联的,因此没必要规定mask。而src_key_padding_mask是用来将zero_pad的部分给去掉。

forward函数中有两个关键变量pos_embedquery_embed。其中pos_embed是位置编码,位于models/position_encoding.py.

position_encoding.py

针对二位特征图的特点,DETR实现了自己的二维位置编码方式。实现代码如下:

class PositionEmbeddingSine(nn.Module):
    """
    This is a more standard version of the position embedding, very similar to the one
    used by the Attention is all you need paper, generalized to work on images.
    """
    def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
        super().__init__()
        self.num_pos_feats = num_pos_feats
        self.temperature = temperature
        self.normalize = normalize
        if scale is not None and normalize is False:
            raise ValueError("normalize should be True if scale is passed")
        if scale is None:
            scale = 2 * math.pi
        self.scale = scale

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors
        mask = tensor_list.mask
        assert mask is not None
        not_mask = ~mask
        y_embed = not_mask.cumsum(1, dtype=torch.float32)
        x_embed = not_mask.cumsum(2, dtype=torch.float32)
        if self.normalize:
            eps = 1e-6
            y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale
            x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale

        dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device)
        dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats)

        pos_x = x_embed[:, :, :, None] / dim_t
        pos_y = y_embed[:, :, :, None] / dim_t
        pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
        return pos
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

而里面的,mask是一个位置掩码数组,对于一个没有经过zero_pad的图像,它的mask是一个全为0的数组。

对照代码,可以看出DETR是为二维特征图的 x和 y 方向各自计算了一个位置编码,每个维度的位置编码长度为num_pos_feats(该数值实际上为hidden_dim的一半),对x或 y,计算奇数位置的正弦,计算偶数位置的余弦,然后将pos_xpos_y拼接起来得到一个NHWD的数组,再经过permute(0,3,1,2),形状变为NDHW,其中D等于hidden_dim。这个hidden_dim是Transformer输入向量的维度,在实现上,要等于CNN backbone输出的特征图的维度。所以pos code和CNN输出特征的形状是完全一样的。

src = src.flatten(2).permute(2, 0, 1)         
pos_embed = pos_embed.flatten(2).permute(2, 0, 1)
  • 1
  • 2

将CNN输出的featurespos code均进行flattenpermute操作,将形状变为SNE,符合PyTorch的输入形状定义。在TransformerEncoder中,会将src和pos_embed相加。可自行查看代码。

detr.py

class DETR

该类封装了整个DETR计算流程。首先来看论文中反复提到的object queries到底是什么。

答案就是query_embed

代码中,query_embed实际上就是一个embedding数组:

self.query_embed = nn.Embedding(num_queries, hidden_dim)
  • 1

其中,num_queries是预定义的目标查询的个数,代码中默认为100。它的意义是:**根据Encoder编码的特征,Decoder将100个查询转化成100个目标。**通常100个查询已经足够了,很少有图像能包含超过100个目标(除非超密集的任务),相比之下,基于CNN的方法要预测的anchors数目动辄上万,计算代价实在是很大。

Transformerforward函数中定义了一个和query_embed形状相同的全为0的数组target,然后在TransformerDecoderLayerforward中把query_embedtarget相加(这里query_embed的作用表现的和位置编码类似),在self attention中作为querykey;在multi-head attention中作为query

class TransformerDecoderLayer(nn.Module):
    def forward_post(self, tgt, memory,
                     tgt_mask: Optional[Tensor] = None,
                     memory_mask: Optional[Tensor] = None,
                     tgt_key_padding_mask: Optional[Tensor] = None,
                     memory_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None,
                     query_pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(tgt, query_pos)
        tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]
        tgt = tgt + self.dropout1(tgt2)
        tgt = self.norm1(tgt)
        tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt, query_pos),
                                   key=self.with_pos_embed(memory, pos),
                                   value=memory, attn_mask=memory_mask,
                                   key_padding_mask=memory_key_padding_mask)[0]
        tgt = tgt + self.dropout2(tgt2)
        tgt = self.norm2(tgt)
        tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
        tgt = tgt + self.dropout3(tgt2)
        tgt = self.norm3(tgt)
        return tgt
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

object queries在经过decoder的计算以后,会输出一个形状为TNE的数组,其中T是object queries的序列长度,即100,N是batch size,E是特征channel。

最后通过一个Linear层输出class预测,通过一个多层感知机结构输出box预测

self.class_embed = nn.Linear(hidden_dim, num_classes + 1)
self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3)

#forward
hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]
outputs_class = self.class_embed(hs)
outputs_coord = self.bbox_embed(hs).sigmoid()
out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

分类输出的通道为num_classes+1,类别从0开始,背景类别为num_classes。

class SetCriterion

该类负责 loss 的计算。

基于CNN的方法会计算每个anchor的预测结果,然后利用预测结果和ground truth box之间计算iou,挑选iou大于一定阈值的那些anchors作为正样本,来回归它们的classbox deltas。类似的,DETR也会计算每个object query的prediction,但DETR会直接计算box的四个角的归一化值,而不再基于box deltas:

然后将这些object predictionsground truth box之间进行二分匹配。DETR使用匈牙利算法来完成这一匹配过程。

完整流程图:
在这里插入图片描述
假如有N个目标,那么100个object predictions中就会有N个能够匹配到这N个ground truth,其他的都会和“no object”匹配成功,这些predictions的类别label就会被分配为num_classes,即表示该prediction是背景。

这样的设计非常不错,是DETR中的一大亮点,也是其特点之一,使其在理论上每个object query都有唯一匹配的目标,不会存在重叠,所以DETR不需要nms进行后处理。

其根据匹配结果进行损失函数 loss 的计算。这里不再列出损失函数的计算公式。

class SetCriterion(nn.Module):
    def forward(self, outputs, targets):
        """ This performs the loss computation.
        Parameters:
             outputs: dict of tensors, see the output specification of the model for the format
             targets: list of dicts, such that len(targets) == batch_size.
                      The expected keys in each dict depends on the losses applied, see each loss' doc
        """
        outputs_without_aux = {k: v for k, v in outputs.items() if k != 'aux_outputs'}

        # Retrieve the matching between the outputs of the last layer and the targets
        indices = self.matcher(outputs_without_aux, targets)

        # Compute the average number of target boxes accross all nodes, for normalization purposes
        num_boxes = sum(len(t["labels"]) for t in targets)
        num_boxes = torch.as_tensor([num_boxes], dtype=torch.float, device=next(iter(outputs.values())).device)
        if is_dist_avail_and_initialized():
            torch.distributed.all_reduce(num_boxes)
        num_boxes = torch.clamp(num_boxes / get_world_size(), min=1).item()

        # Compute all the requested losses
        losses = {}
        for loss in self.losses:
            losses.update(self.get_loss(loss, outputs, targets, indices, num_boxes))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

通过self.matcheroutputs_without_auxtargets进行匹配。匈牙利算法会返回一个indices tuple,该tuple包含了srctarget的index。具体匹配过程请参考models/matcher.py。

分类 loss

分类loss采用的是交叉熵损失,针对所有predictions

def loss_labels(self, outputs, targets, indices, num_boxes, log=True):
    src_logits = outputs['pred_logits']

    idx = self._get_src_permutation_idx(indices)
    target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices)])
    target_classes = torch.full(src_logits.shape[:2], self.num_classes,
                                    dtype=torch.int64, device=src_logits.device)
    target_classes[idx] = target_classes_o

    loss_ce = F.cross_entropy(src_logits.transpose(1, 2), target_classes, self.empty_weight)
    losses = {'loss_ce': loss_ce}

    return losses
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

target_classes_o是按target index获取的所有匹配成功的真值类别,并按src index将其放入target_classes的对应位置。匹配失败的predictionstarget_classes中用self.num_classes来填充。函数_get_src_permutation_idx的作用是从indices tuple中取得srcbatch index和对应的match index

box loss

box loss采用了l1 lossgiou loss,针对匹配成功的predictions

def loss_boxes(self, outputs, targets, indices, num_boxes):
    """Compute the losses related to the bounding boxes, the L1 regression loss and the GIoU loss
       targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4]
       The target boxes are expected in format (center_x, center_y, w, h), normalized by the image size.
    """
    assert 'pred_boxes' in outputs
    idx = self._get_src_permutation_idx(indices)
    src_boxes = outputs['pred_boxes'][idx]
    target_boxes = torch.cat([t['boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0)

    loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction='none')

    losses = {}
    losses['loss_bbox'] = loss_bbox.sum() / num_boxes

    loss_giou = 1 - torch.diag(box_ops.generalized_box_iou(
        box_ops.box_cxcywh_to_xyxy(src_boxes),
        box_ops.box_cxcywh_to_xyxy(target_boxes)))
    losses['loss_giou'] = loss_giou.sum() / num_boxes
    return losses
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

target_boxes 是按target index获取的所有匹配成功的真值boxsrc_boxes是按src index获取的匹配成功的predictions,计算它们之间的l1_loss和giou loss

DETR 在全景分割上的应用(浅看)

Decoder的每个object embedding加上一个mask head就可以实现像素级分割的功能
mask head可以和box embed联合训练,也可以训练完了box embed以后再单独训练mask head

DETR的做法和Mask-RCNN比较像,都是在给定的box prediction的基础上预测该box对应instancesegmentation。这里DETRmask head输出的attention map进行上采样以后,和backbone的一些分支进行相加,实现一个FPN的功能,然后将所有的box对应的mask map加粗样式进行bitwise argmax操作,得到最终的分割图。

最后(个人见解)

Transformer结构在CV领域的应用,表明了CV和NLP两大计算机AI领域的密切关联,计算机领域间各大细分领域间的互相促进关系。不过,Transformer结构在语义语序上的巨大效果始终还是在自然语言处理上更为显著,而将其应用大CV领域,个人认为是因为其对时间序列这类特征数据的特殊作用,使得其在目标检测、图像理解等时序相关的任务上有了很不错成效。但是,或许后续的改进会使得两者相通以得到更好的结果。

本文仅作学习分享,笔记记录,侵权请联系删除

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/337842
推荐阅读
相关标签
  

闽ICP备14008679号