第十六章 原理篇:DETR

news2024/11/26 11:26:56

脑子不好使啊!复习过的东西过几天就忘记了!


参考教程:
论文地址:[pdf]End-to-End Object Detection with Transformer
源码地址:https://github.com/facebookresearch/detr

文章目录

  • 概述
  • the DETR model
    • DETR architecture
      • CNN backbone
      • Transformer encoder
      • Transformer decoder
      • FFN
    • DETR loss
  • 代码实现
    • DETR
    • backbone and position
      • IntermediateLayerGetter
      • Joiner
      • position encoding
    • transformer
      • Encoder
      • Decoder
    • FFN
    • Loss
      • aux loss
      • loss_labels
      • loss_boxes

概述

在之前的章节中我们介绍过transformer的原理和transformer在图像分类任务的代表作,DETR作为在目标检测领域使用Transformer的开山之作,给出了一个高小的端到端的目标检测框架。

使用DETR时,可以省去繁琐的非极大值抑制和 生成anchor的步骤,它直接输出的就是目标框的预测结果。

在这里插入图片描述
DETR把目标检测变成了一个集合预测问题。它采用基于transformer的encoder-decoder结构。transformer中的自注意力机制可以把元素之间的交互建模成一个序列的形式,这种结构很适合做集合预测,而DETR就可以看作起到了把一个图像序列转换成集合序列的作用。

在这里插入图片描述
在上图中可以看到,和传统的two-stage目标检测模型相比,DETR的流程有多简单,只需要一个encoder-decoder就可以直接得到预测框和类别。而一个two-stage的模型则要先选出proposal-》过滤-》分类和修正-》再过滤才能得到最终的结果。

DETR的优势不只在于高效便捷,它的目标检测的performance也相当不错,在大物体检测的效果上比faster r-cnn更胜一筹。

the DETR model

集合预测有两个核心的组成:1. 一个集合预测的损失函数,用来完成预测结果和ground truth的匹配;2. 一个能够得到一个目标检测结果的集合的模型结构。

我们先来看一下这个结构。
在这里插入图片描述

DETR architecture

上图给出了比较简洁明了的DETR的模型结构,它由三个主要部分构成。

  1. CNN backbone: 得到一系列的图像的特征,作为transformer部分的输入。
  2. Encoder-Decoder transformer:
    1. Encoder: Encoder部分以图像特征作为输入,并加入了位置编码。
    2. Decoder: Decoder部分引入了名为object queries的输入,它与我们最终的目标检测结果有关。
  3. FFN: feed forward network部分用于进行class和bounding box的预测。

CNN backbone

对CNN backbone没有明确的要求,常用的那些backbone都可以在这里使用。对于一个输入 x i m g ∈ R 3 × H 0 × W 0 x_{img} \in R^{3\times H_0\times W_0} ximgR3×H0×W0,希望在经过backbone后,得到的输出 f ∈ R C × H × W f\in R^{C\times H\times W} fRC×H×W,我们希望 C = 2048 , H , W = H 0 32 , W 0 32 C=2048,H,W=\frac{H_0}{32},\frac{W_0}{32} C=2048H,W=32H0,32W0

Transformer encoder

在这里插入图片描述
在不考虑batch的情况下,我们的图像是一个三维的输入,但是transformer要求我们的输入是二维的N*D,N代表序列的个数,D代表序列的维度。

所以我们CNN backbone的输出,在进入transformer前要先经过转换,从 f ∈ R C × H × W f\in R^{C\times H\times W} fRC×H×W变成 f ∈ R d × H W f\in R^{d\times H W} fRd×HW。d是使用1*1卷积降维后得到的新维度。

因为transformer的特征之间没有空间位置信息,所以这里还需要增加一个position embedding。在DETR中position embedding不是直接加载image的patch embedding上的(在DETR中就是我们backbone得到的feature,这里我叫他patch embedding是为了好理解),从图中可以看出,这个position embedding是加在了每个encoder block的Q和K上。

Transformer decoder

在这里插入图片描述

在decoder的部分,也遵循了标准的transformer中的decoder结构。输入同样是二维的N*D。在decoder中,不仅使用self-attention,还有encoder-decoder attention,即使用来自encoder的V、K和来自decoder的Q进行attention。

这里的N也代表了预测结果的个数。N是多少就代表你每个图片会预测多少个框。

decoder的输入又被称为object query,希望这个object query能体现出位置信息,所以它其实也是一个position embedding。同样地,它也被加到self-attention的Q和K上。而在encoder-decoder attention的部分,decoder的position embedding也被加到了来自decoder的Q上,来自encoder的K则加上来了encoder的position embedding。

FFN

FFN的预测结果包括box的中心坐标、宽和高,线性层也会完成物体类别的预测。

因为预测结果的N通常是比图片中实际的框的数量要多的,所以在这里使用了一个额外的class label表示没有物体 “no object”。

DETR loss

因为decoder的部分输入是n个object queries,所以DETR只能预测N个结果。N的数量是固定的,和groundtruth会存在数量上的不一致。所以会引发一个问题:怎么衡量预测结果的准确性。

DETR把它当作一个二分图匹配的问题来解决。假设ground truth是 y y y,预测结果是 y ^ = { y i ^ } i = 1 N \hat{y} = \{\hat{y_i}\}^N_{i=1} y^={yi^}i=1N。一般情况下N是比图片中实际的框的数量要大的,我们把 y y y也当作一个大小为N的集合(用“no object”补全)。

匹配结果的损失函数可以表示为
σ ^ = a r g m i n ∑ i N L m a t c h ( y i , y ^ σ ( i ) ) \hat{\sigma} = arg min \sum_i^NL_{match}(y_i,\hat{y}_{\sigma(i)}) σ^=argminiNLmatch(yi,y^σ(i))
最优匹配通过匈牙利算法来计算,这个loss的目的就是让最优匹配的结果的差距最小化。这个匹配结果的损失不仅考虑分类,也考虑bounding box,所以它可以被看作 y i = ( c i , b i ) y_i = (c_i, b_i) yi=(ci,bi)。c是class,b是代表了中心坐标和宽高的向量。
L m a t c h ( y i , y ^ σ ( i ) ) = − 1 { c i ≠ ⊘ } p ^ σ ( i ) ( c i ) + 1 { c i ≠ ⊘ } L b o x ( b i , b ^ σ ( i ) ) L_{match}(y_i,\hat{y}_{\sigma(i)}) = -1_{\{c_i\neq\oslash\}}\hat{p}_{\sigma(i)}(c_i) + 1_{\{c_i\neq\oslash\}}L_{box}(b_i,\hat{b}_{\sigma(i)}) Lmatch(yi,y^σ(i))=1{ci=}p^σ(i)(ci)+1{ci=}Lbox(bi,b^σ(i))
上面这个公式是一个pair的损失,下一步就是计算所有pair的总的损失。
L H u n g a r i a n ( y , y ^ ) = ∑ i = 1 N [ − l o g p ^ σ ^ ( i ) ( c i ) + 1 { c i ≠ ⊘ } L b o x ( b i , b ^ σ ^ ( i ) ) ] L_{Hungarian}(y,\hat{y}) = \sum^N_{i=1}[-log\hat{p}_{\hat{\sigma}(i)}(c_i)+ 1_{\{c_i\neq\oslash\}}L_{box}(b_i,\hat{b}_{\hat\sigma(i)})] LHungarian(y,y^)=i=1N[logp^σ^(i)(ci)+1{ci=}Lbox(bi,b^σ^(i))]
在实际使用中,作者对“no object”的类别会做一个down-weight的处理,为了缓解类别不均衡的问题。

公式中的 L b o x ( ) L_{box}() Lbox(),作者使用的是l1loss和iouloss的组合。
L b o x ( b i , b ^ σ ( i ) ) = λ i o u L i o u ( b i , b ^ σ ( i ) ) + λ L 1 ∣ ∣ b i − b ^ σ ( i ) ∣ ∣ 1 L_{box}(b_i,\hat{b}_{\sigma(i)}) = \lambda_{iou}L_{iou}(b_i,\hat{b}_{\sigma(i)}) + \lambda_{L1}||b_i - \hat{b}_{\sigma(i)}||_1 Lbox(bi,b^σ(i))=λiouLiou(bi,b^σ(i))+λL1∣∣bib^σ(i)1

此外,作者还是用了名为auxiliary losses的方法,对于每一个decoder block的输出都计算一次loss,并且这样也确实带来了效果的提升。

代码实现

代码部分主要参考源码:https://github.com/facebookresearch/detr。
首先回顾一下DETR的流程。

  1. 使用backbone提取图像特征:这里用的是最深层的featuremap。
  2. 创建position embedding。这个embedding主要在encoder相关的部分使用。
  3. transformer encoder结构,主要用于进行一个全局特征的提取。
  4. 创建object query。这个也可以看作一个position embedding,主要在decoder相关的部分使用。
  5. transformer decoder结构,寻找图像中的物体。
  6. FFN 预测目标检测的结果。

DETR

首先我们来看一下源码中DETR这个class。
它的__init__()函数中的传入参数包括:

  1. backbone:你打算使用的backbone
  2. transformer:构造好的transformer
  3. num_classes:数据集中物体种类的数量。
  4. num_queries:object queries的数量,也就代表了每张图中能预测的物体的最大数量。
  5. aux_loss:是否要使用aux_loss。
class DETR(nn.Module):
    """ This is the DETR module that performs object detection """
    def __init__(self, backbone, transformer, num_classes, num_queries, aux_loss=False):
        super().__init__()
        self.num_queries = num_queries
        self.transformer = transformer
        hidden_dim = transformer.d_model
        self.class_embed = nn.Linear(hidden_dim, num_classes + 1) # 增加了一个“on object”的类别
        self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3)
        self.query_embed = nn.Embedding(num_queries, hidden_dim) # 你的object query
        self.input_proj = nn.Conv2d(backbone.num_channels, hidden_dim, kernel_size=1)
        self.backbone = backbone
        self.aux_loss = aux_loss

我们只看构造函数的部分,各个module已经很清晰明了啦。配合着我们说过的DETR的流程。

  1. self.backbone 用于获得featuremap的backbone
  2. self.input_proj 用于将backbone获得的featuremap降维,得到的featuremap维度一般是2048,这个有点太大了。
  3. self.transformer 你的transformer 包含了encoder和decoder。
  4. self.query_embed 你的object query。
  5. self.class_embed 和 self.bbox_embed用于预测类别和bbox。

然后我们再看一下forward的部分。

if isinstance(samples, (list, torch.Tensor)):
            samples = nested_tensor_from_tensor_list(samples)
        features, pos = self.backbone(samples)

        src, mask = features[-1].decompose()
        assert mask is not None
        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]}
        if self.aux_loss:
            out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord)
        return out

这里的self.backbone(samples)的返回结果有两个,一个是features一个是pos,因为在源码中position_embed的部分和backbone结合在一起做了。

将输入们一股脑地放到transformer里面,得到输出hs。
并用hs来进行bbox和class的预测。

backbone and position

在论文给出的代码中,position embedding和backbone的实现还是比较简单的。

在下面这段代码中,self.backbone使用的是resenet50去掉池化和全连接的部分,用于提取特征图,x = self.backbone(inputs)的结果是维度=2048的32倍下采样featuremap。
然后使用self.conv这个1*1卷积进行降维。并在flatten后和pos_embedding组合在一起作为输入。

当然这段代码实际上和原论文中描述的原理是不一致的,因为论文里的position embedding是加在Q和K上的,并不是直接和输入组合在一起的。

下面这段代码只是说明了DETR在实现上是多么简单,不需要依赖别的package。

# 首先是init的部分
self.backbone = nn.Sequential(*list(resnet50(pretrained=True).children())[:-2]) # 忽略了最后的池化和全连接的resnet50
self.conv = nn.Conv2d(2048, hidden_dim,1)
self.row_embed = nn.Parameter(torch.rand(50,hidden_dim//2))
self.col_embed = nn.Parameter(torch.rand(50, hidden_dim//2))

# 下面是forward的部分
x = self.backbone(inputs)
h = self.conv(x)
H, W = h.shape[-2:]
pos = torch.cat([self.col_embed[:W].unsqueeze(0).repeat(H,1,1),
			self.row_embed[:H].unsqueeze(1).repeat(1,W,1),
			],dim=-1).flatten(0,1).unsqueeze(1)

现在我们继续来看github上官方源码是怎么做的。

在官方源码中,position_embedding和backbone被组合到了一起。

IntermediateLayerGetter

DETR的backbone中用到了这个class。

https://github.com/pytorch/vision/blob/main/torchvision/models/_utils.py#L8

class IntermediateLayerGetter(nn.ModuleDict):
	def __init__(self, model: nn.Module, return_layers: Dict[str, str]) -> None:

这个class的作用是,给定model和想要返回的层。在forward阶段,返回的结果是一个OrderedDict,它的key是你指定的返回层对应的名字,它的val是当前层的输出结果。

def forward(self, x):
        out = OrderedDict()
        for name, module in self.items():
            x = module(x)
            if name in self.return_layers:
                out_name = self.return_layers[name]
                out[out_name] = x
        return out

在backbone的实现中是这样定义 return_interm_layers的

if return_interm_layers:
	return_layers = {"layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"}
else:
	return_layers = {'layer4': "0"}

也就是说你想要中间层的结果,IntermediateLayerGetter的forward阶段会以名字“0,1,2,3”返回四个层的输出,如果你不需要中间层的结果,那么只会返回名为“0”的layer4的输出。

下面为backbone的forward阶段。这里的self.body = IntermediateLayerGetter(backbone, return_layers=return_layers)

假设我们没有使用interm_layer,那么self.body(tensor_list.tensors)的返回结果是layer4的输出的featuremap。

那么最终得到的这个out就是{‘0’: nestedtensor}。

   def forward(self, tensor_list: NestedTensor):
        xs = self.body(tensor_list.tensors)
        out: Dict[str, NestedTensor] = {}
        for name, x in xs.items():
            m = tensor_list.mask
            assert m is not None
            mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0]
            out[name] = NestedTensor(x, mask)
        return out

Joiner

在源码中,backbone和position encoding被一个名为Joiner的类组合在一起,我们先看一下这个类做了什么。

在Joiner是一个nn.Sequential(),我们之前学过,nn.Sequential()会把传入的module顺序组合在一起,并在forward阶段顺序调用。在Joiner中,它重写了forward()的部分。

class Joiner(nn.Sequential):
    def __init__(self, backbone, position_embedding):
        super().__init__(backbone, position_embedding)

    def forward(self, tensor_list: NestedTensor):
        xs = self[0](tensor_list)
        out: List[NestedTensor] = []
        pos = []
        for name, x in xs.items():
            out.append(x)
            # position encoding
            pos.append(self[1](x).to(x.tensors.dtype))

        return out, pos

这个forward里的self[0]就是我们的backbone,self[1]就是position_embedding这个module。

已知self[0](tensor_list)返回的结果是一个OrderedDict。假设我们没有使用中间层,那么这里返回的xs是{“0”: nestedtensor}。

这里返回的out就是nestedtensor的list,pos就是使用每一个nestedtensor做输入得到的position embedding的list。

position encoding

https://github.com/facebookresearch/detr/blob/main/models/position_encoding.py

源码中提供了两个position encoding的方式。
这个具体直接看源码吧。

在Joiner中,假如你使用了interm_layer,那么backbone就会有多层的输出,每个输出都会用position encoding生成一个位置编码。

位置编码的大小是 B , 1 , H , W B, 1, H, W B,1,H,W。这里的位置编码和VIT以及SWIN中的还是有些差别的。

VIT和SWIN中在最开始都把一整个图片分成了多个patch,它的位置编码还包括了这个patch在整个图片中的位置。但是在DETR中它其实输入就是一个patch,而不是多个patch。在swin和transformer,它的position embedding是一个和token大小一致的编码,你的输入是N*D,那么你的position embedding也是N*D。

在DETR中,position embedding不是很在乎你的N的大小,它只看了你的D的位置,也就是 H , W H,W H,W。所以position encoding才可以和backbone直接join在一起做,它不需要考虑backbone下一步用卷积降维的结果。

transformer

源码链接: https://github.com/facebookresearch/detr/blob/main/models/transformer.py

源码中transformer的部分用的也是自定义的模型,因为这里相对pytorch中的版本是有修改的,比如说它要求每个block的Q和K都需要加上position embedding。

直观来看,一个transformer由encoder和decoder两部分组成。它的__init__()部分主要是定义了一个encoder和一个decoder,所以我们直接来看一下forward部分的代码。

对于一个大小为 b s , c , h , w bs, c, h, w bs,c,h,w的输入,首先要把它转成一个token的格式,在这里转为了 h ∗ w , b s , c h*w, bs, c hw,bs,c。它的位置编码因为要与token直接相加,所以也需要改,变成了 h ∗ w , b s , 1 h*w, bs, 1 hw,bs,1

object query 也做了一些格式上的变化。原本的object query大小是 n , d n, d n,d,被变成了 n , b s , d n,bs,d n,bs,d

query_embed = query_embed.unsqueeze(1).repeat(1, bs, 1)

这里要解释一下,这种写法是因为nn.MultiheadAttention中的batch_first默认是False。

Query embeddings of shape :math:(L, E_q) for unbatched input, :math:(L, N, E_q) when batch_first=False
or :math:(N, L, E_q) when batch_first=True, where :math:L is the target sequence length,

然后输入和编码们一起被送进 encoder和decoder中,分别得到encoder的输出memory和decoder的输出hs,并最终返回这两个结果。

encoder的输出memory和输入src大小是一致的,都是 h ∗ w , b s , c h*w, bs, c hw,bs,c,所以最后先permute成 b s , c , h ∗ w bs, c, h*w bs,c,hw后又展开为 b s , c , h , w bs, c, h, w bs,c,h,w

decoder的输出原本应该和输入tgt的大小是一致的(也就是object query的大小),都是 n , b s , d n,bs,d n,bs,d,在实际上它在最终输出前增加了一个额外的维度,变成了 1 , n , b s , d 1,n,bs,d 1,n,bs,d,然后transpose后最终得到的是 1 , b s , n , d 1, bs, n, d 1,bs,n,d

事实上在decoder的过程中是可能会返回多层结果的,假如需要多层结果,这里得到的decoder的输出就是 k , n , b s , d k,n,bs,d k,n,bs,d,k表示层数。所以单个结果的时候用unsqueeze也是为了保证返回结果的shape的一致性。

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)

Encoder

我们先来看一下encoder的部分。一个encoder由多个encoder block/layer组成。在每一个layer中,都需要完成 MSA + MLP,此外还搭配上Norm+add残差。和标准的encoder的结构是一样的。

来看一下源码的layer的__init__部分。该有的组件都有:

  1. MSA + 残差和 + 归一化
  2. MLP + 残差和 + 归一化

这里的MSA就是self.self_attn。
MLP由两个全连接self.linear1,self.linear2加激活函数组合完成。

def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1,
                 activation="relu", normalize_before=False):
        super().__init__()
        self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        # Implementation of Feedforward model
        self.linear1 = nn.Linear(d_model, dim_feedforward)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(dim_feedforward, d_model)

        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)

        self.activation = _get_activation_fn(activation)
        self.normalize_before = normalize_before

我们来看一下forward的部分。源码中实现了两个forward,一个是forward_pre,一个是forward_post。两个实现的区别在于normalize的位置。本质上还是一样的。我们以forward_post为例子进行介绍。

forward_post更符合我们对整个流程的认知。

  1. 得到QKV,进行self attention
  2. norm + 残差和
  3. 经过MLP
  4. norm + 残差和
def forward_post(self,
                     src,
                     src_mask: Optional[Tensor] = None,
                     src_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(src, pos) # 加上位置编码
        src2 = self.self_attn(q, k, value=src, attn_mask=src_mask,           key_padding_mask=src_key_padding_mask)[0] # self attn
        src = src + self.dropout1(src2) # 残差和
        src = self.norm1(src) # norm
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src)))) # MLP
        src = src + self.dropout2(src2) # 残差和
        src = self.norm2(src) # norm
        return src

src就是我们的输入,我们的K和Q要加上位置信息,所以这里使用了self.with_pos_embed进行一个加法的计算。

经过self attention得到结果src2,和输入src进行残差和+norm的计算。
然后再经过MLP层,再次残差和+nrom的计算,得到最终的输出。

这个layer和我们之前写过的encoderlayer除了加位置编码这个处理外,没有任何的区别。

在Encoder中会连续使用n次这个layer,每次都要使用这个位置编码对K和Q进行处理。

Decoder

decoder 部分和 encoder有一点区别,因为decoder部分除了self_attn外还要和encoder联动做一个cross_attn。

decoderlayer的__init__()部分相对于encoderlayer,增加了一个self.multihead_attn用于做cross_attn,然后增加了对应的norm和dropout,别的没有改变。

所以我们直接来看forward的部分,还是用forward_post做例子。

首先看一下输入参数包括哪些。

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):

tgt就是你的decoder的输入,memory是你的encoder的输出。pos是你的encoder的position embedding。query_pos就是你的object query。

这里的tgt在初始化时是一个和object query大小一致的全0向量,每一个decoder layer的输出都会成为下一层的输入的tgt。

一个decoder layer的流程是这样的:

  1. 输入进行self attention
  2. 上一步的结果作为q 和来自encoder的k和v做cross-attention
  3. 正常的MLP

来看一下代码中是如何完成的。
第一步是self attention。在这一步中是encoder中一样,Q和K上要加上位置编码。然后输出结果还要进行残差和+norm。

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)

我们现在得到的tgt,将作为Q用于下一步的cross attention的计算。memory作为来自encoder的输出,会给我们提供K和V。

在这个self.multihead_attn中,输入的Q是加上decoder的位置编码的tgt,输入的K是加上了encoder的位置编码的encoder的输出memory,V是不加位置编码的memory。然后再进行残差和+norm的计算。

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)

现在这个tgt,在最后接上MLP就可以输出了。

tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
tgt = tgt + self.dropout3(tgt2)
tgt = self.norm3(tgt)

整个流程和一般的decoder也没有什么区别,主要是不管是selfattention和crossattention的部分,Q和K都要注意加上位置编码。

FFN

这一部分还是要回到DETR这个大class里面。
我们对DETR整体进行回顾,这里会将__init__()中的代码和foward()的放在一起,直观地看每一部分的作用。。

在backbone的部分我们得到了featuremap和position embedding。

self.backbone = backbone # __init__直接传进来一个backbone的module

features, pos = self.backbone(samples) # forward的部分,backbone的输出结果是两个list,第一个list里的元素是nestedtensor,第二个list的元素是position embedding

然后我们的feature map是要使用一个1*1卷积进行降维的。然后作为transformer的输入。

self.transformer = transformer # __init__直接传进来一个transformer的module
self.input_proj = nn.Conv2d(backbone.num_channels, hidden_dim, kernel_size=1) # 用于降维的1*1卷积
self.query_embed = nn.Embedding(num_queries, hidden_dim) # object query

src, mask = features[-1].decompose() # 这里只使用了list的最后一项,所以不是很明白假如返回了中间层的话会起到什么作用。
assert mask is not None
hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]  # 这里的输入的src要经过降维的,输入的pos也是最后一个featuremap得到的pos embedding,都没用到别的层的。

我们可以看到这里只用的了transformer的第一个输出。之前我们说过它会返回两个结果,第一个是decoder的结果,第二个是encoder的结果,所以这里只看decoder的结果。

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

outputs_class = self.class_embed(hs)
outputs_coord = self.bbox_embed(hs).sigmoid()

这两个比较简单,感觉也没有什么要说的。

Loss

源码链接: https://github.com/facebookresearch/detr/blob/main/models/detr.py

源码中的loss也是自定义了一个名为SetCriterion的类,其中也实现了多种类型的loss,我们重点看一下分类loss和位置loss。

在进行损失计算前,首先要完成输出的100个cls+bbox和ground truth的匹配,这里是使用匈牙利算法完成的,源码就不看了,链接放在这里。

匈牙利算法

aux loss

在DETR的forward阶段,直接返回的结果是:

 out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]}

假如你打算使用aux_loss,那么就会增加decoder的中间层的输出结果,用于计算。

if self.aux_loss:
	out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord)
        
@torch.jit.unused
def _set_aux_loss(self, outputs_class, outputs_coord):
	return [{'pred_logits': a, 'pred_boxes': b}
            for a, b in zip(outputs_class[:-1], outputs_coord[:-1])]

loss_labels

在计算label的损失时,要用的是预测结果是pred_logits,它的大小应该是 b s , n , c l s + 1 bs, n, cls+1 bs,n,cls+1,bs是batchsize,n是object query的数量,也就是我们要预测的框的数量,cls+1是包含了’no object’类后的物体类别总数。

首先要构建一个分类的target。

 target_classes = torch.full(src_logits.shape[:2], self.num_classes,dtype=torch.int64, device=src_logits.device)

这个可以理解成构建一个大小为(bs,n)的target,并用self.num_classes这个数进行填充。因为实际上的类别是0到self.num_classes-1,所以这个self.num_classes 在这里代表的其实是“no object”这一类。

别的有真实类别的target是

target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices)])

这个会被直接拼到target_classes上去。

target_classes[idx] = target_classes_o

这样就得到了一个和输出logits数量/class都保持一致的target。

然后再使用cross_entropy计算结果。

loss_ce = F.cross_entropy(src_logits.transpose(1, 2), target_classes, self.empty_weight)

loss_boxes

loss_boxes的流程和loss_label差不多。

一个区别是,class也会计算“no object”的损失,但是boxes直接算对应的boxes的损失。

所以可以看到在loss_labels中使用的logits是全部的cls_logits,而loss_boxes中则挑出了匹配成功的部分。

src_boxes = outputs['pred_boxes'][idx]

loss_bbox使用的是L1loss和iou loss组合的结果。

还是一样先找出target。

 target_boxes = torch.cat([t['boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0)

然后计算L1损失。

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

再计算iou损失。

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)))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/746413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot篮球论坛系统

篮球论坛管理方面的任务繁琐,以至于每年都在篮球论坛管理这方面投入较多的精力却效果甚微,篮球论坛系统的目标就是为了能够缓解篮球论坛管理工作方面面临的压力,让篮球论坛管理方面的工作变得更加高效准确。 本项目在开发和设计过程中涉及到原理和技术有: B/S、java技术和MySQL…

Git❀详细使用教程

Git❀详细使用教程 一、Git简介1.1 什么是Git?1.2 Git的特点1.3 集中式与分布式的区别?1.4 Git工作流程图 二、Git安装与常用命令2.1 Git环境配置2.1.1 下载与安装2.1.2 基本配置2.1.3 为常用指令设置别名(可选)2.1.4 解决GitBash…

ChatGPT炒股:从上市公司招股说明书中批量提取发明专利表格

上市公司招股说明书通常会详细列明公司的发明专利,而通过企业的发明专利可以了解企业未来的业务布局情况,怎么把这些发明专利列表都批量提取出来呢? 随机打开几个上市公司的招股说明书,可以看到发明专利这一内容,共同的特征是都有…

Python的面向对象从入门到精通(简单易懂)

目录 1 初识对象 2. 成员方法 3. 类和对象 4. 构造方法 5. 其它内置方法 6. 封装 7. 继承 8. 复写 9. 类型的注解 10. 多态 1 初识对象 1. 生活中或是程序中,我们都可以使用设计表格、生产表格、填写表格的形式组织数据 2. 进行对比,在程序中&a…

进程间通信-无名管道

#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <string.h> int main(int argc, char **argv) {int pfd[2];int ret;pid_t pid;char buf[20]{0};retpipe(pfd);//创建一个无名管道if(ret<0){perror("pipe");ret…

lombok和guava工具

写在前面 本文看下如何通过使用lombok和guava来提高生产力。 1&#xff1a;lombok 我们开发程序时&#xff0c;如read method&#xff0c;write method&#xff0c;tostring&#xff0c;构造函数等&#xff0c;其写法其实都是固定和重复的&#xff0c;因此就可以考虑使用程序…

如何操作MySQL数据库基本数据

目录 一、MySQL数据库概念 数据 表&#xff08;数据表&#xff09; 数据库 数据库管理系统 数据库的建立和维护功能 数据定义功能 数据操纵功能 数据库的运行管理功能 通信功能 数据流向 二、主流数据库分类 1.SQL Server 数据库 &#xff08;微软分公司产品&…

Linux网络第六章——PXE高效批量网络装机

高效&#xff1a;速度快 批量&#xff1a;多台同时安装 网络&#xff1a;必须在网络环境 PXE&#xff1a;自动装机 需要&#xff1a; 1、DHCP实现地址自动分配 2、FTP把安装文件传输给客户机 3、xinetd&#xff1a;新一代的网络守护进程服务程序&#xff0c;管理轻量级…

赎金信(力扣)思维 JAVA

给你两个字符串&#xff1a;ransomNote 和 magazine &#xff0c;判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以&#xff0c;返回 true &#xff1b;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。 示例 1&#xff1a; 输入&#xff…

请求响应-响应-案例

案例需求 加载并解析emp.xml文件中的数据&#xff0c;完成数据处理&#xff0c;并在页面展示 emp.xml文件代码如下&#xff1a; <?xml version"1.0" encoding"UTF-8" ?> <emps><emp><name>金毛狮王</name><age>5…

Leangoo领歌敏捷工具标签管理上线~

在Leangoo领歌中&#xff0c;标签通常用作对任务的分类&#xff0c;或任务的优先级区分。 每创建一个看板设置一次标签&#xff0c;有点繁琐&#xff0c;现在标签管理功能上线&#xff0c;可以统一设置标签&#xff0c;统一导入标签。 标签设置步骤&#xff1a; 第一步&…

机器人xacro文件转换成urdf文件方法,并在rviz可视化

一、进入工作空间&#xff0c;source一下 cd cat_ws source devel/setup.bash二、进入xacro所在的文件夹&#xff0c;完成xacro文件到urdf文件的转换 cd src/kinova-ros/kinova_description/urdf/然后执行下面命令 rosrun xacro xacro.py two_arm_robot_example_standalone.…

注解和反射04(Java)

#拓展 获取泛型信息 反射操作泛型&#xff1a; package reflection;import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import java.util.Map;//通过反射获取泛型 public class Test06{…

进制转换解析

进制 进制介绍 对于整数&#xff0c;有四种表示方式&#xff1a; 二进制&#xff1a;0,1 &#xff0c;满 2 进 1.以 0b 或 0B 开头。 十进制&#xff1a;0-9 &#xff0c;满 10 进 1。 八进制&#xff1a;0-7 &#xff0c;满 8 进 1. 以数字 0 开头表示。 十六进制&#xff1…

力扣题库刷题笔记42--接雨水(未通过)

1、题目如下&#xff1a; 2、个人Python代码实现&#xff08;部分用例超时&#xff09; 本地执行大概超过30S&#xff0c;力扣显示超时 3、个人Python代码思路&#xff1a; 当且仅当nums[i] < nums[i1]&#xff0c;nums[i] < nums[i-1]&#xff0c;此时nums[i]才能接到雨…

运动规划概述

运动规划概述 前端路径搜索后端轨迹生成MPD & MPC地图表示多智能体路径规划MAPF 前端路径搜索 不希望花费太多的算力和代价&#xff0c;因此通常把高维问题降成低维问题&#xff0c;先找到一条粗劣可行的低维解。 基于搜索的路径规划 图搜索问题 Dijkstra and A* 、 Jump…

第四十六章Java包(package)

在编写 Java 程序时&#xff0c;随着程序架构越来越大&#xff0c;类的个数也越来越多&#xff0c;这时就会发现管理程序中维护类名称也是一件很麻烦的事&#xff0c;尤其是一些同名问题的发生。有时&#xff0c;开发人员还可能需要将处理同一方面的问题的类放在同一个目录下&a…

python相关

1 首先下载python环境 win r 输入python 下载对应版本的环境 建议使用python3以上的版本。 2 windows环境下安装selenium 安装easy_install https://pypi.python.org/pypi/setuptools在此链接 然后python setup.py&#xff0c;看到了以下字样&#xff0c;则表明成功 这里需…

16万字市智慧人社项目建设方2023WORD

导读&#xff1a;原文《16万字市智慧人社项目建设方2023WORD》word&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 目 录 第 1 章 项目概述 1.1 项目名称 1.2 …

串行FLASH文件系统FatFs-移植过程

目录 串行FLASH文件系统FatFs-移植过程 准备工作及移植前说明 底层disk接口程序API配置 中间层API功能实现及常用文件操作函数使用 文件系统偏移 中文文件名和长文件名 串行FLASH文件系统FatFs-移植过程 准备工作及移植前说明 我们需要在SPI——读写串行FLASH的基础驱动程…