Explicação detalhada do DETR

Código-fonte do Github: facebookresearch/detr
Código-fonte da versão da anotação do Github: HuKai97/detr-annotations
Documento: Detecção de objetos de ponta a ponta com Transformers
Reimpresso: [análise do código-fonte DETR]

visão geral

DETR significa DE tection TR ansformer. É um modelo CV proposto pelo Facebook AI Research Institute. É usado principalmente para detecção de alvos e também pode ser usado para tarefas de segmentação. Este modelo usa o Transformer para substituir as complexas rotinas tradicionais de detecção de alvos , como dois estágios ou um estágio, baseado em âncora ou sem âncora, pós-processamento nms, etc.; também não usa algumas técnicas coquetes, como como usar a fusão de recursos de escala múltipla, use alguns tipos especiais de convolução (como convolução de grupo, convolução variável, convolução de geração dinâmica etc.) aprimoramento de dados.Todo o processo é usar CNN para extrair recursos e, em seguida, codificar e decodificar para obter a saída prevista .

insira a descrição da imagem aqui

Pode-se dizer que o trabalho geral é muito sólido. Embora o efeito não seja tão bom quanto o SOTA, é de grande importância usar o Transformer, que geralmente é considerado pelos alquimistas como pertencente ao campo da PNL, para transfronteiriços para o campo CV, e pode funcionar.Também vale a pena aprender. Esse tipo de trabalho que rompe a tradição e cria a era costuma ser popular, como Faster R-CNN e YOLO, e você pode ver que muitos trabalhos subsequentes são aprimorados com base neles.

Em poucas palavras, o DETR considera a tarefa de detecção de alvos como um problema de previsão definido. Para uma imagem, um certo número de objetos é predito fixamente (o original é 100, que pode ser alterado no código) e o modelo é baseado no relação entre esses objetos e o contexto global na imagem. A relação gera diretamente a previsão definida em paralelo, ou seja, o Transformer decodifica os resultados da previsão de todos os objetos na imagem de uma só vez. Esse recurso paralelo torna o DETR muito eficiente.

Características :

  1. End-to-end: NMS e Anchor são removidos, não há tantos hiperparâmetros, a quantidade de cálculo é bastante reduzida e toda a rede fica muito simples;
  2. Baseado no Transformer: O Transformer é introduzido na tarefa de detecção de alvo pela primeira vez;
  3. Uma nova função de perda baseada em conjunto é proposta: o método de correspondência de gráfico bipartido é usado para forçar o modelo a produzir um conjunto de quadros de previsão exclusivos, e cada objeto gerará apenas um quadro de previsão, convertendo assim o problema de detecção de alvo em uma previsão definida problema , portanto, o NMS não é usado para obter o efeito de ponta a ponta;
  4. Além disso, um conjunto de consultas de objetos científicos e recursos on-line globais gerados pelo codificador são inseridos no decodificador e as 100 caixas de previsão de saída final são forçadas diretamente em paralelo para substituir a âncora;
    desvantagens :
  5. O efeito de detecção em objetos grandes é muito bom, mas o efeito de monitoramento em objetos pequenos não é muito bom; o treinamento é relativamente lento;
  6. Devido ao design e inicialização da consulta, leva muito tempo para treinar o modelo DETR do zero;
    vantagens :
  7. A velocidade e a precisão do conjunto de dados COCO são semelhantes ao Faster RCNN; pode ser estendido para muitas tarefas subdivididas, como segmentação, rastreamento, multimodalidade, etc.;

insira a descrição da imagem aqui

Processo :

  1. Depois que a imagem de entrada passa pela rede CNN, a matriz de recursos da imagem é obtida;
  2. Endireite a imagem e adicione codificação de posição;
  3. Insira as informações de correlação dos recursos de aprendizado no codificador Transformer;
  4. Use a saída do codificador e a consulta de objeto como entrada do decodificador para obter as informações decodificadas;
  5. Passe as informações decodificadas para o FFN para obter informações de previsão;
  6. Julgar se a informação de previsão FFN contém o objeto alvo real;
  7. Se houver, imprima a caixa e a categoria previstas, caso contrário, imprima uma classe sem objeto.

princípio

Função de perda com base na previsão do conjunto

Correspondência de gráficos bipartidos para determinar caixas de previsão efetivas

N (100) quadros de previsão são previstos e gt é M quadros, geralmente N>M, então como calcular a perda?
Aqui, primeiro realizamos uma correspondência bipartida desses 100 quadros de previsão e quadros gt e primeiro determinamos qual quadro de previsão cada gt corresponde e, finalmente, calcule a perda total de quadros de previsão M e quadros gt M.

Na verdade, é muito simples. Supondo que haja uma matriz agora, a abcissa são os 100 quadros de previsão que previmos e a ordenada é o quadro gt. Calcule o custo de cada quadro de previsão e todos os outros quadros gt separadamente, formando assim uma matriz de custo e, em seguida, determine como atribuir todas as caixas gt às caixas de previsão correspondentes para minimizar o custo total final.

O método de cálculo aqui é o algoritmo húngaro clássico , que geralmente é feito chamando a função linear_sum_assignment no pacote scipy. A entrada para esta função é a matriz de custo e gera um conjunto de índices de linha e um índice de coluna correspondente, fornecendo a melhor alocação.

Portanto, por meio das etapas acima, determina-se quais quadros de predição nos 100 quadros de predição finais serão usados ​​como quadros de predição efetivos e quais quadros de predição serão chamados de backgrounds. Em seguida, calcule a perda final do quadro de predição efetivo e do quadro gt (o número de quadros de predição efetivos é igual ao número de quadros gt).

função de perda

Previsão:Específico+FrequênciaL
Húngaro ( y , y ^ ) = ∑ i = 1 N [ − log ⁡ p ^ σ ^ ( i ) ( ci ) + 1 { ci ≠ ∅ } L box ( bi , b ^ σ ^ ( i ) ) ] \mathcal{L}_{\text {húngaro}}(y, \hat{y})=\sum_{i=1}^{N}\left[-\log \hat{ p}_{ \hat{\sigma}(i)}\left(c_{i}\right)+\mathbb{1}_{\left\{c_{i}\right\varnothing\right\}}\ mathcal{L} _{\text {box}}\left(b_{i}, \hat{b}_{\hat{\sigma}}(i)\right)\right]euHúngaro ( s ,y^)=eu = 1n[ -pouco tempop^p^ (eu)( ceu)+1{ ceu= }eucaixa ( beu,b^p^( i ) ]
perda de classificação: perda de entropia cruzada, perda de regressão de log de remoção :
Perda GIOU + Perda L1

Código fonte

todo o quadro

  • Espinha dorsal :

    • Função: extrair características da imagem e reduzir a escala da imagem
    • Estrutura: Resnet
    • Entrada: um lote de imagem [B, 3, W, H]
    • Saída: mapa de recursos [B, 2048, W_feat, H_feat] da última camada do Resnet50
  • camada de transição :

    • Função: Converter a saída do backbone para a forma exigida pelo codificador e preparar outras variáveis ​​solicitadas pelo Transformer, como máscaras, posicional_embedding, etc.
  • Transformador :

    • Estrutura: codificador, decodificador
    • Entrada total:
      • x : mapa de recursos de entrada [B, 256, W_feat, H_feat]
      • mask : máscara de preenchimento efetiva no codificador e decodificador [B, W_feat, H_feat]
      • query_embed : O vetor incorporado de consulta no decodificador [num_query(100), 256]
      • positional_embedding : o vetor de incorporação de codificação posicional do recurso iamge, query_pos no codificador, key_pos no decodificador [B, 256, W_feat, H_feat]
    • Produção total:
      • out_dec : a saída do decodificador** (saída da consulta de destino)****[num_dec_layers(6), B, num_query(100), 256]**, embora mantemos a saída de todas as camadas do decodificador, mas deseja A saída final é apenas a saída da última camada da camada do decodificador
      • memória : a saída do codificador [B, 256, W_feat, H_feat]
  • Codificador no Transformer :

    • Função: executar codificação de posição e auto-atenção no mapa de recursos e agregar os recursos globais do mapa de recursos por meio de auto-atenção
    • 结构:auto_atenção、normalização、rede feedforward、normalização
    • digitar:
      • consulta : Insira a consulta, que é o mapa de recursos achatado [W_feat*H_feat, B, 256]
      • value , key : Na auto-atenção, k e v são calculados por q, então a entrada é None
      • query_key_padding_mask : máscara de preenchimento efetiva do mapa de recursos [B, W_feat*H_feat]
      • query_positional_embedding : codificação de posição do mapa de recursos [W_feat*H_feat, B, 256]
    • saída:
      • encoder_out : saída de mapa de recursos achatado de auto-atenção [W_feat*H_feat, B, 256]
  • Decodificador no Transformer :

    • efeito:
      • A primeira parte é realizar a auto-atenção na consulta do objeto, de modo que cada consulta do objeto preste atenção às diferentes informações do objeto
      • A segunda parte é realizar atenção cruzada na consulta de objeto e encoder_out, para que a consulta de objeto possa encontrar diferentes objetos no mapa de recursos
    • estrutura:
      • auto atenção (auto_atenção, normalização)
      • atenção cruzada (atenção cruzada, normalização, rede de feed forward, normalização)
    • auto atenção :
      • digitar:
        • query : Inicializa consulta de objeto [num_layer(6), num_query(100), B, 256]
        • chave, valor : Na auto-atenção, k e v são calculados por q, Nenhum
        • query_positional_embedding : consulta de codificação posicional [num_layer(6), num_query(100), B, 256]
        • máscara : Na auto atenção do decodificador, não há entrada de máscara Nenhum
      • saída:
        • Consulta de objeto de auto-atenção [num_layer(6), num_query(100), B, 256]
    • atenção cruzada :
      • digitar:
        • query : A consulta de objeto obtida por auto-atenção, ou seja, a saída do módulo de auto-atenção [num_layer(6), num_query(100), B, 256]
        • chave, valor : Em atenção cruzada, k e v são a saída do codificador [N, B, 256]
        • query_positional_embedding : consulta de codificação posicional [num_layer(6), num_query(100), B, 256]
        • key_positional_embedding : a codificação posicional da chave, ou seja, a codificação posicional do mapa de recursos [W_feat*H_feat**, **B, 256]
        • key_padding_mask: chave, que é a máscara de preenchimento efetiva do mapa de recursos [B, W_feat*H_feat]
      • saída:
        • out_dec : saída do decodificador [num_layers(6), B, num_query(100), 256]
        • memória : saída do codificador [W_feat*H_feat, B, 256]

O processo de construção do modelo é dividido em:

  1. Construindo DETR: Backbone + Transformer + MLP (Multilayer Perceptron, rede neural multicamada)
  2. Função de perda de inicialização: critério + processamento pós-inicialização: pós-processadores
def build(args):
    # the `num_classes` naming here is somewhat misleading.
    # it indeed corresponds to `max_obj_id + 1`, where max_obj_id
    # is the maximum id for a class in your dataset. For example,
    # COCO has a max_obj_id of 90, so we pass `num_classes` to be 91.
    # As another example, for a dataset that has a single class with id 1,
    # you should pass `num_classes` to be 2 (max_obj_id + 1).
    # For more details on this, check the following discussion
    # https://github.com/facebookresearch/detr/issues/108#issuecomment-650269223
    # 最大类别ID+1
    num_classes = 20 if args.dataset_file != 'coco' else 91
    if args.dataset_file == "coco_panoptic":
        # for panoptic, we just add a num_classes that is large enough to hold
        # max_obj_id + 1, but the exact value doesn't really matter
        num_classes = 250
    device = torch.device(args.device)

    # 搭建backbone resnet + PositionEmbeddingSine
    backbone = build_backbone(args)

    # 搭建transformer
    transformer = build_transformer(args)

    # 搭建整个DETR模型
    model = DETR(
        backbone,
        transformer,
        num_classes=num_classes,
        num_queries=args.num_queries,
        aux_loss=args.aux_loss,
    )

    # 是否需要额外的分割任务
    if args.masks:
        model = DETRsegm(model, freeze_detr=(args.frozen_weights is not None))

    # HungarianMatcher()  二分图匹配
    matcher = build_matcher(args)

    # 损失权重
    weight_dict = {
    
    'loss_ce': 1, 'loss_bbox': args.bbox_loss_coef}
    weight_dict['loss_giou'] = args.giou_loss_coef
    if args.masks:   # 分割任务  False
        weight_dict["loss_mask"] = args.mask_loss_coef
        weight_dict["loss_dice"] = args.dice_loss_coef
    # TODO this is a hack
    if args.aux_loss:   # 辅助损失  每个decoder都参与计算损失  True
        aux_weight_dict = {
    
    }
        for i in range(args.dec_layers - 1):
            aux_weight_dict.update({
    
    k + f'_{
      
      i}': v for k, v in weight_dict.items()})
        weight_dict.update(aux_weight_dict)

    losses = ['labels', 'boxes', 'cardinality']
    if args.masks:
        losses += ["masks"]

    # 定义损失函数
    criterion = SetCriterion(num_classes, matcher=matcher, weight_dict=weight_dict,
                             eos_coef=args.eos_coef, losses=losses)
    criterion.to(device)

    # 定义后处理
    postprocessors = {
    
    'bbox': PostProcess()}

    # 分割
    if args.masks:
        postprocessors['segm'] = PostProcessSegm()
        if args.dataset_file == "coco_panoptic":
            is_thing_map = {
    
    i: i <= 90 for i in range(201)}
            postprocessors["panoptic"] = PostProcessPanoptic(is_thing_map, threshold=0.85)

    return model, criterion, postprocessors

Construir um modelo

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):
        """ Initializes the model.
        Parameters:
            backbone: torch module of the backbone to be used. See backbone.py
            transformer: torch module of the transformer architecture. See transformer.py
            num_classes: number of object classes
            num_queries: number of object queries, ie detection slot. This is the maximal number of objects
                         DETR can detect in a single image. For COCO, we recommend 100 queries.
            aux_loss: True if auxiliary decoding losses (loss at each decoder layer) are to be used.
        """
        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)
        # 回归
        self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3)
        # self.query_embed 类似于传统目标检测里面的anchor 这里设置了100个  [100,256]
        # nn.Embedding 等价于 nn.Parameter
        self.query_embed = nn.Embedding(num_queries, hidden_dim)
        self.input_proj = nn.Conv2d(backbone.num_channels, hidden_dim, kernel_size=1)
        self.backbone = backbone
        self.aux_loss = aux_loss   # True

    def forward(self, samples: NestedTensor):
        """ The forward expects a NestedTensor, which consists of:
               - samples.tensor: batched images, of shape [batch_size x 3 x H x W]
               - samples.mask: a binary mask of shape [batch_size x H x W], containing 1 on padded pixels

            It returns a dict with the following elements:
               - "pred_logits": the classification logits (including no-object) for all queries.
                                Shape= [batch_size x num_queries x (num_classes + 1)]
               - "pred_boxes": The normalized boxes coordinates for all queries, represented as
                               (center_x, center_y, height, width). These values are normalized in [0, 1],
                               relative to the size of each individual image (disregarding possible padding).
                               See PostProcess for information on how to retrieve the unnormalized bounding box.
               - "aux_outputs": Optional, only returned when auxilary losses are activated. It is a list of
                                dictionnaries containing the two above keys for each decoder layer.
        """
        if isinstance(samples, (list, torch.Tensor)):
            samples = nested_tensor_from_tensor_list(samples)
        # out: list{0: tensor=[bs,2048,19,26] + mask=[bs,19,26]}  经过backbone resnet50 block5输出的结果
        # pos: list{0: [bs,256,19,26]}  位置编码
        features, pos = self.backbone(samples)

        # src: Tensor [bs,2048,19,26]
        # mask: Tensor [bs,19,26]
        src, mask = features[-1].decompose()
        assert mask is not None

        # 数据输入transformer进行前向传播
        # self.input_proj(src) [bs,2048,19,26]->[bs,256,19,26]
        # mask: False的区域是不需要进行注意力计算的
        # self.query_embed.weight  类似于传统目标检测里面的anchor 这里设置了100个
        # pos[-1]  位置编码  [bs, 256, 19, 26]
        # hs: [6, bs, 100, 256]
        hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]

        # 分类 [6个decoder, bs, 100, 256] -> [6, bs, 100, 92(类别)]
        outputs_class = self.class_embed(hs)
        # 回归 [6个decoder, bs, 100, 256] -> [6, bs, 100, 4]
        outputs_coord = self.bbox_embed(hs).sigmoid()
        out = {
    
    'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]}
        if self.aux_loss:   # True
            out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord)
        # dict: 3
        # 0 pred_logits 分类头输出[bs, 100, 92(类别数)]
        # 1 pred_boxes 回归头输出[bs, 100, 4]
        # 3 aux_outputs list: 5  前5个decoder层输出 5个pred_logits[bs, 100, 92(类别数)] 和 5个pred_boxes[bs, 100, 4]
        return out

    @torch.jit.unused
    def _set_aux_loss(self, outputs_class, outputs_coord):
        # this is a workaround to make torchscript happy, as torchscript
        # doesn't support dictionary with non-homogeneous values, such
        # as a dict having both a Tensor and a list.
        return [{
    
    'pred_logits': a, 'pred_boxes': b}
                for a, b in zip(outputs_class[:-1], outputs_coord[:-1])]

Espinha dorsal

O backbone inclui principalmente duas partes da extração de recursos da CNN e codificação de posição.
A primeira é chamar a função build_backbone em models/Backbone.py para criar o Backbone:

def build_backbone(args):
    # 搭建backbone
    # 位置编码  PositionEmbeddingSine()
    position_embedding = build_position_encoding(args)
    train_backbone = args.lr_backbone > 0   # 是否需要训练backbone  True
    return_interm_layers = args.masks       # 是否需要返回中间层结果 目标检测False  分割True
    # 生成backbone  resnet50
    backbone = Backbone(args.backbone, train_backbone, return_interm_layers, args.dilation)
    # 将backbone输出与位置编码相加   0: backbone   1: PositionEmbeddingSine()
    model = Joiner(backbone, position_embedding)
    model.num_channels = backbone.num_channels   # 512
    return model

Aqui, a função é chamada primeiro build_position_encodingpara gerar a codificação de posição de seno e cosseno position_embedding: [batchsize, 256, H/32, W/32], onde o primeiro 128 de 256 é a codificação de posição na direção y e o último 128 é a codificação de posição na direção x; em seguida, chame a classe Backbone para gerar um par ResNet50 Os dados de entrada são submetidos à extração de recursos para obter um mapa de recursos [batchsize, 2048, H/32, W/32]. Por fim, o Joiner mescla e armazena os dois para uso posterior.

CNN

Para criar o ResNet50, primeiro chame a classe Backbone:

class Backbone(BackboneBase):
    """ResNet backbone with frozen BatchNorm."""
    def __init__(self, name: str,
                 train_backbone: bool,
                 return_interm_layers: bool,
                 dilation: bool):
        # 直接掉包 调用torchvision.models中的backbone
        backbone = getattr(torchvision.models, name)(
            replace_stride_with_dilation=[False, False, dilation],
            pretrained=is_main_process(), norm_layer=FrozenBatchNorm2d)
        # resnet50  2048
        num_channels = 512 if name in ('resnet18', 'resnet34') else 2048
        super().__init__(backbone, train_backbone, num_channels, return_interm_layers)

Esta classe é herdada da classe BackboneBase e a CNN chama diretamente o modelo em archvision.models, então olhe diretamente para a classe BackboneBase

class BackboneBase(nn.Module):

    def __init__(self, backbone: nn.Module, train_backbone: bool, num_channels: int, return_interm_layers: bool):
        super().__init__()
        for name, parameter in backbone.named_parameters():
            # layer0 layer1不需要训练 因为前面层提取的信息其实很有限 都是差不多的 不需要训练
            if not train_backbone or 'layer2' not in name and 'layer3' not in name and 'layer4' not in name:
                parameter.requires_grad_(False)
        # False 检测任务不需要返回中间层
        if return_interm_layers:
            return_layers = {
    
    "layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"}
        else:
            return_layers = {
    
    'layer4': "0"}
        # 检测任务直接返回layer4即可  执行torchvision.models._utils.IntermediateLayerGetter这个函数可以直接返回对应层的输出结果
        self.body = IntermediateLayerGetter(backbone, return_layers=return_layers)
        self.num_channels = num_channels

    def forward(self, tensor_list: NestedTensor):
        """
        tensor_list: pad预处理之后的图像信息
        tensor_list.tensors: [bs, 3, 608, 810]预处理后的图片数据 对于小图片而言多余部分用0填充
        tensor_list.mask: [bs, 608, 810] 用于记录矩阵中哪些地方是填充的(原图部分值为False,填充部分值为True)
        """
        # 取出预处理后的图片数据 [bs, 3, 608, 810] 输入模型中  输出layer4的输出结果 dict '0'=[bs, 2048, 19, 26]
        xs = self.body(tensor_list.tensors)
        # 保存输出数据
        out: Dict[str, NestedTensor] = {
    
    }
        for name, x in xs.items():
            m = tensor_list.mask  # 取出图片的mask [bs, 608, 810] 知道图片哪些区域是有效的 哪些位置是pad之后的无效的
            assert m is not None
            # 通过插值函数知道卷积后的特征的mask  知道卷积后的特征哪些是有效的  哪些是无效的
            # 因为之前图片输入网络是整个图片都卷积计算的 生成的新特征其中有很多区域都是无效的
            mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0]
            # out['0'] = NestedTensor: tensors[bs, 2048, 19, 26] + mask[bs, 19, 26]
            out[name] = NestedTensor(x, mask)
        # out['0'] = NestedTensor: tensors[bs, 2048, 19, 26] + mask[bs, 19, 26]
        return out

Esta classe ainda está chamando o modelo em archivision.models, e então insere os dados da imagem pré-processada [bs, 3, 608, 810] e os dados da máscara [bs, 608, 810] no modelo (esses dados da imagem são passados ​​pelo preenchimento do bloco dados, e os dados da máscara são para registrar quais posições de pixel dessas imagens são pad, o que é True, e os dados reais efetivos sem pad são False). Após a propagação para frente, a função IntermediateLayerGetter é chamada para extrair o mapa de recursos da camada correspondente, e o mapa de recursos [bs, 2048, 19, 26] da imagem original reduzido em 32 vezes é obtido e a máscara correspondente a esse recurso mapa [bs, 19, 26].

Codificação Posicional

Codificação posicional é codificação posicional. Aqui é principalmente para chamar a função build_position_encoding em models/position_encoding.py para criar a codificação de posição:

def build_position_encoding(args):
    """
    创建位置编码
    args: 一系列参数  args.hidden_dim: transformer中隐藏层的维度   args.position_embedding: 位置编码类型 正余弦sine or 可学习learned
    """
    # N_steps = 128 = 256 // 2  backbone输出[bs,256,25,34]  256维度的特征
    # 而传统的位置编码应该也是256维度的, 但是detr用的是一个x方向和y方向的位置编码concat的位置编码方式  这里和ViT有所不同
    # 二维位置编码   前128维代表x方向位置编码  后128维代表y方向位置编码
    N_steps = args.hidden_dim // 2
    if args.position_embedding in ('v2', 'sine'):
        # TODO find a better way of exposing other arguments
        # [bs,256,19,26]  dim=1时  前128个是y方向位置编码  后128个是x方向位置编码
        position_embedding = PositionEmbeddingSine(N_steps, normalize=True)
    elif args.position_embedding in ('v3', 'learned'):
        position_embedding = PositionEmbeddingLearned(N_steps)
    else:
        raise ValueError(f"not supported {
      
      args.position_embedding}")

    return position_embedding

Pode-se ver que o código-fonte implementa dois tipos de codificação de posição, uma é a codificação de posição absoluta seno-cosseno, que não requer aprendizado de parâmetro adicional, e a outra é a codificação de posição absoluta que pode ser aprendida. O artigo original usa codificação de posição absoluta seno-cosseno, e o código também usa isso por padrão, então aqui apresentamos principalmente a classe PositionEmbeddingSine:

class PositionEmbeddingSine(nn.Module):
    """
    Absolute pos embedding, Sine.  没用可学习参数  不可学习  定义好了就固定了
    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    # 128维度  x/y  = d_model/2
        self.temperature = temperature        # 常数 正余弦位置编码公式里面的10000
        self.normalize = normalize            # 是否对向量进行max规范化   True
        if scale is not None and normalize is False:
            raise ValueError("normalize should be True if scale is passed")
        if scale is None:
            # 这里之所以规范化到2*pi  因为位置编码函数的周期是[2pi, 20000pi]
            scale = 2 * math.pi  # 规范化参数 2*pi
        self.scale = scale

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors   # [bs, 2048, 19, 26]  预处理后的 经过backbone 32倍下采样之后的数据  对于小图片而言多余部分用0填充
        mask = tensor_list.mask   # [bs, 19, 26]  用于记录矩阵中哪些地方是填充的(原图部分值为False,填充部分值为True)
        assert mask is not None
        not_mask = ~mask   # True的位置才是真实有效的位置

        # 考虑到图像本身是2维的 所以这里使用的是2维的正余弦位置编码
        # 这样各行/列都映射到不同的值 当然有效位置是正常值 无效位置会有重复值 但是后续计算注意力权重会忽略这部分的
        # 而且最后一个数字就是有效位置的总和,方便max规范化
        # 计算此时y方向上的坐标  [bs, 19, 26]
        y_embed = not_mask.cumsum(1, dtype=torch.float32)
        # 计算此时x方向的坐标    [bs, 19, 26]
        x_embed = not_mask.cumsum(2, dtype=torch.float32)

        # 最大值规范化 除以最大值 再乘以2*pi 最终把坐标规范化到0-2pi之间
        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)   # 0 1 2 .. 127
        # 2i/2i+1: 2 * (dim_t // 2)  self.temperature=10000   self.num_pos_feats = d/2
        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   # 正余弦括号里面的公式
        # x方向位置编码: [bs,19,26,64][bs,19,26,64] -> [bs,19,26,64,2] -> [bs,19,26,128]
        pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
        # y方向位置编码: [bs,19,26,64][bs,19,26,64] -> [bs,19,26,64,2] -> [bs,19,26,128]
        pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
        # concat: [bs,19,26,128][bs,19,26,128] -> [bs,19,26,256] -> [bs,256,19,26]
        pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)

        # [bs,256,19,26]  dim=1时  前128个是y方向位置编码  后128个是x方向位置编码
        return pos

对照公式:
PE ( pos , 2 i ) = sin ⁡ ( pos / 1000 0 2 i / d model ) PE ( pos , 2 i + 1 ) = cos ⁡ ( pos / 1000 0 2 i / d model ) \begin{ alinhado} P E_{(pos, 2 i)} & =\sin \left(\text { pos } / 10000^{2 i / d_{\text {model }}}\right) \\ P E_{(pos , 2 i+1)} & =\cos \left(pos / 10000^{2 i / d_{\text {model }}}\right) \end{aligned}P E( pos , 2 i ) _P E( pos , 2 i + 1 ) _=pecado(  pos  /1000 02i / d _modelo )=porque( p os /1000 02i / d _modelo )
Minha compreensão de alguns pontos-chave:

  1. Aqui, o código de posição é construído por meio da máscara. A máscara registra se cada posição de pixel no mapa de recursos é um pad. Somente a posição em que é Falso é uma posição válida e o código de posição precisa ser construído;
  2. Em relação à normalização do valor máximo: por causa do método de codificação seno-coseno, a ideia é mapear a fórmula de passagem de cada posição para o intervalo de 0~2Π (também pode ser 4Π, 6Π, 8Π..., porque é uma função periódica, mas geralmente o padrão é 2Π), então x_embed e y_embed precisam ser normalizados antes de serem introduzidos na fórmula;
  3. Em relação ao método de codificação de posição: A razão aqui é codificar x e y respectivamente (codificação de posição bidimensional), em vez de codificação de posição unidimensional como transformador. A principal consideração é que o transformador é aplicado no modelo de linguagem, que é naturalmente unidimensional, então unidimensional pode ser mais adequado, e DETR é uma estrutura de detecção de alvo aplicada em tarefas de imagem. Em tarefas de imagem, é claro, dois Codificação de posição dimensional O efeito pode ser melhor;
  4. Dessa forma, para cada posição (x, y), o valor de codificação correspondente à sua coluna está nas primeiras 128 dimensões da dimensão do canal, e o valor da codificação de sua linha está nas últimas 128 dimensões da dimensão do canal. Desta forma, cada posição no mapa de recursos corresponde ao valor de codificação de diferentes dimensões.

Claro, como um aprendizado, você também pode olhar para o segundo método de codificação de posição absoluta: codificação de posição que pode ser aprendida:

class PositionEmbeddingLearned(nn.Module):
    """
    Absolute pos embedding, learned.
    可以发现整个类其实就是初始化了相应shape的位置编码参数,让后通过可学习的方式学习这些位置编码参数
    """
    def __init__(self, num_pos_feats=256):
        super().__init__()
        # nn.Embedding  相当于 nn.Parameter  其实就是初始化函数
        self.row_embed = nn.Embedding(50, num_pos_feats)
        self.col_embed = nn.Embedding(50, num_pos_feats)
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.uniform_(self.row_embed.weight)
        nn.init.uniform_(self.col_embed.weight)

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors
        h, w = x.shape[-2:]   # 特征图h w
        i = torch.arange(w, device=x.device)
        j = torch.arange(h, device=x.device)
        x_emb = self.col_embed(i)   # 初始化x方向位置编码
        y_emb = self.row_embed(j)   # 初始化y方向位置编码
        # concat x y 方向位置编码
        pos = torch.cat([
            x_emb.unsqueeze(0).repeat(h, 1, 1),
            y_emb.unsqueeze(1).repeat(1, w, 1),
        ], dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(x.shape[0], 1, 1, 1)
        return pos

Pode-se descobrir que a classe inteira realmente inicializa os parâmetros de codificação de posição da forma correspondente e, em seguida, aprende esses parâmetros de codificação de posição por si mesma de uma maneira que pode ser aprendida. O código é relativamente simples.

Transformador

todo o quadro

Primeiro olhe para a interface de chamada:

def build_transformer(args):
    return Transformer(
        d_model=args.hidden_dim,
        dropout=args.dropout,
        nhead=args.nheads,
        dim_feedforward=args.dim_feedforward,
        num_encoder_layers=args.enc_layers,
        num_decoder_layers=args.dec_layers,
        normalize_before=args.pre_norm,
        return_intermediate_dec=True,
    )

Chame a classe Transformer diretamente:

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__()
        """
        d_model: 编码器里面mlp(前馈神经网络  2个linear层)的hidden dim 512
        nhead: 多头注意力头数 8
        num_encoder_layers: encoder的层数 6
        num_decoder_layers: decoder的层数 6
        dim_feedforward: 前馈神经网络的维度 2048
        dropout: 0.1
        activation: 激活函数类型 relu
        normalize_before: 是否使用前置LN
        return_intermediate_dec: 是否返回decoder中间层结果  False
        """
        # 初始化一个小encoder
        encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        encoder_norm = nn.LayerNorm(d_model) if normalize_before else None
        # 创建整个Encoder层  6个encoder层堆叠
        self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm)

        # 初始化一个小decoder
        decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        decoder_norm = nn.LayerNorm(d_model)
        # 创建整个Decoder层  6个decoder层堆叠
        self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm,
                                          return_intermediate=return_intermediate_dec)

        # 参数初始化
        self._reset_parameters()

        self.d_model = d_model  # 编码器里面mlp的hidden dim 512
        self.nhead = nhead      # 多头注意力头数 8

    def _reset_parameters(self):
        for p in self.parameters():
            if p.dim() > 1:
                nn.init.xavier_uniform_(p)

    def forward(self, src, mask, query_embed, pos_embed):
        """
        src: [bs,256,19,26] 图片输入backbone+1x1conv之后的特征图
        mask: [bs, 19, 26]  用于记录特征图中哪些地方是填充的(原图部分值为False,填充部分值为True)
        query_embed: [100, 256]  类似于传统目标检测里面的anchor  这里设置了100个   需要预测的目标
        pos_embed: [bs, 256, 19, 26]  位置编码
        """
        # bs  c=256  h=19  w=26
        bs, c, h, w = src.shape
        # src: [bs,256,19,26]=[bs,C,H,W] -> [494,bs,256]=[HW,bs,C]
        src = src.flatten(2).permute(2, 0, 1)
        # pos_embed: [bs, 256, 19, 26]=[bs,C,H,W] -> [494,bs,256]=[HW,bs,C]
        pos_embed = pos_embed.flatten(2).permute(2, 0, 1)
        # query_embed: [100, 256]=[num,C] -> [100,bs,256]=[num,bs,256]
        query_embed = query_embed.unsqueeze(1).repeat(1, bs, 1)
        # mask: [bs, 19, 26]=[bs,H,W] -> [bs,494]=[bs,HW]
        mask = mask.flatten(1)

        # tgt: [100, bs, 256] 需要预测的目标query embedding 和 query_embed形状相同  且全设置为0
        #                     在每层decoder层中不断的被refine,相当于一次次的被coarse-to-fine的过程
        tgt = torch.zeros_like(query_embed)
        # memory: [494, bs, 256]=[HW, bs, 256]  Encoder输出  具有全局相关性(增强后)的特征表示
        memory = self.encoder(src, src_key_padding_mask=mask, pos=pos_embed)
        # [6, 100, bs, 256]
        # tgt:需要预测的目标 query embeding
        # memory: encoder的输出
        # pos: memory的位置编码
        # query_pos: tgt的位置编码
        hs = self.decoder(tgt, memory, memory_key_padding_mask=mask,
                          pos=pos_embed, query_pos=query_embed)
        # decoder输出 [6, 100, bs, 256] -> [6, bs, 100, 256]
        # encoder输出 [bs, 256, H, W]
        return hs.transpose(1, 2), memory.permute(1, 2, 0).view(bs, c, h, w)

A análise cuidadosa desta classe revelará que, embora não entendamos os detalhes do modelo por enquanto, a estrutura principal do modelo foi definida. Todo o Transformer é, na verdade, a entrada do mapa de recursos src (redução de dimensão para 256), src_key_padding_mask (gravando se cada posição do mapa de recursos é preenchida e o pad não precisa calcular a atenção) e o código de posição pos no TransformerEncoder Na verdade, o TransformerEncoder é composto de TransformerEncoderLayer; em seguida, insira a saída do codificador, a máscara, o código de posição e o código de consulta no TransformerEncoder, e o TransformerEncoder é composto de TransformerDecoderLayer.

Portanto, o seguinte é dividido em dois módulos, TransformerEncoder e TransformerDecoder, para entender os detalhes específicos do Transformer.

TransformerEncoder

Esta parte é para chamar a função _get_clones, copiar 6 classes TransformerEncoderLayer e, em seguida, inserir as 6 classes TransformerEncoderLayer por sua vez para propagação direta, calcular continuamente a autoatenção do mapa de recursos e aprimorar continuamente o mapa de recursos e, finalmente, obter o mais forte (a maioria das informações) saída do mapa de recursos: [h*w, bs, 256]. Vale a pena observar que a forma de todo o mapa de recursos do processo TransformerEncoder é constante.

class TransformerEncoder(nn.Module):

    def __init__(self, encoder_layer, num_layers, norm=None):
        super().__init__()
        # 复制num_layers=6份encoder_layer=TransformerEncoderLayer
        self.layers = _get_clones(encoder_layer, num_layers)
        # 6层TransformerEncoderLayer
        self.num_layers = num_layers
        self.norm = norm  # layer norm

    def forward(self, src,
                mask: Optional[Tensor] = None,
                src_key_padding_mask: Optional[Tensor] = None,
                pos: Optional[Tensor] = None):
        """
        src: [h*w, bs, 256]  经过Backbone输出的特征图(降维到256)
        mask: None
        src_key_padding_mask: [h*w, bs]  记录每个特征图的每个位置是否是被pad的(True无效   False有效)
        pos: [h*w, bs, 256] 每个特征图的位置编码
        """
        output = src

        # 遍历这6层TransformerEncoderLayer
        for layer in self.layers:
            output = layer(output, src_mask=mask,
                           src_key_padding_mask=src_key_padding_mask, pos=pos)

        if self.norm is not None:
            output = self.norm(output)

        # 得到最终ENCODER的输出 [h*w, bs, 256]
        return output

def _get_clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for i in range(N)])

TransformerEncoderLayer

Diagrama da estrutura do codificador:
insira a descrição da imagem aqui

Camada do Encoder = atenção multi-head + add&Norm + feed forward + add&Norm, o foco está na atenção multi-head.

class TransformerEncoderLayer(nn.Module):

    def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1,
                 activation="relu", normalize_before=False):
        super().__init__()
        """
        小encoder层  结构:multi-head Attention + add&Norm + feed forward + add&Norm
        d_model: mlp 前馈神经网络的dim
        nhead: 8头注意力机制
        dim_feedforward: 前馈神经网络的维度 2048
        dropout: 0.1
        activation: 激活函数类型
        normalize_before: 是否使用先LN  False
        """
        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

    def with_pos_embed(self, tensor, pos: Optional[Tensor]):
        # 这个操作是把词向量和位置编码相加操作
        return tensor if pos is None else tensor + pos

    def forward_post(self,
                     src,
                     src_mask: Optional[Tensor] = None,
                     src_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None):
        """
        src: [494, bs, 256]  backbone输入下采样32倍后 再 压缩维度到256的特征图
        src_mask: None
        src_key_padding_mask: [bs, 494]  记录哪些位置有pad True 没意义 不需要计算attention
        pos: [494, bs, 256]  位置编码
        """
        # 数据 + 位置编码  [494, bs, 256]
        # 这也是和原版encoder不同的地方,这里每个encoder的q和k都会加上位置编码  再用q和k计算相似度  再和v加权得到更具有全局相关性(增强后)的特征表示
        # 每用一层都加上位置编码  信息不断加强  最终得到的特征全局相关性最强  原版的transformer只在输入加上位置编码  作者发现这样更好
        q = k = self.with_pos_embed(src, pos)
        # multi-head attention   [494, bs, 256]
        # q 和 k = backbone输出特征图 + 位置编码
        # v = backbone输出特征图
        # 这里对query和key增加位置编码 是因为需要在图像特征中各个位置之间计算相似度/相关性 而value作为原图像的特征 和 相关性矩阵加权,
        # 从而得到各个位置结合了全局相关性(增强后)的特征表示,所以q 和 k这种计算需要+位置编码  而v代表原图像不需要加位置编码
        # nn.MultiheadAttention: 返回两个值  第一个是自注意力层的输出  第二个是自注意力权重  这里取0
        # key_padding_mask: 记录backbone生成的特征图中哪些是原始图像pad的部分 这部分是没有意义的
        #                   计算注意力会被填充为-inf,这样最终生成注意力经过softmax时输出就趋向于0,相当于忽略不计
        # attn_mask: 是在Transformer中用来“防作弊”的,即遮住当前预测位置之后的位置,忽略这些位置,不计算与其相关的注意力权重
        #            而在encoder中通常为None 不适用  decoder中才使用
        src2 = self.self_attn(q, k, value=src, attn_mask=src_mask,
                              key_padding_mask=src_key_padding_mask)[0]
        # add + norm + feed forward + add + norm
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
        src = src + self.dropout2(src2)
        src = self.norm2(src)
        return src

    def forward_pre(self, src,
                    src_mask: Optional[Tensor] = None,
                    src_key_padding_mask: Optional[Tensor] = None,
                    pos: Optional[Tensor] = None):
        src2 = self.norm1(src)
        q = k = self.with_pos_embed(src2, pos)
        src2 = self.self_attn(q, k, value=src2, attn_mask=src_mask,
                              key_padding_mask=src_key_padding_mask)[0]
        src = src + self.dropout1(src2)
        src2 = self.norm2(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src2))))
        src = src + self.dropout2(src2)
        return src

    def forward(self, src,
                src_mask: Optional[Tensor] = None,
                src_key_padding_mask: Optional[Tensor] = None,
                pos: Optional[Tensor] = None):
        if self.normalize_before:  # False
            return self.forward_pre(src, src_mask, src_key_padding_mask, pos)
        return self.forward_post(src, src_mask, src_key_padding_mask, pos)  # 默认执行

Existem vários pontos-chave (diferente do codificador original do transformador):

  1. Por que q e k de cada codificador estão codificados na posição +? Se você aprendeu transformadores, geralmente adiciona codificação de posição à entrada do transformador e o qkv de cada codificador é igual, sem adicionar codificação de posição. Aqui, q e k são primeiro adicionados com codificação de posição e, em seguida, q e k são usados ​​para calcular a similaridade e, finalmente, ponderados com v para obter uma representação de recurso mais relevante globalmente (melhorada). Cada camada é adicionada com codificação de posição, e as informações globais de cada camada são continuamente reforçadas e, finalmente, os recursos globais mais fortes podem ser obtidos;
  2. Por que q e k + codificação de posição, mas v não precisa adicionar codificação de posição? Como q e k são usados ​​para calcular a similaridade/correlação entre cada posição no recurso de imagem, e o recurso global calculado após a codificação de posição é mais correlacionado, e v representa a imagem original, portanto, não há necessidade de adicionar codificação de posição;

TransformerDecoder

A estrutura do Decoder é semelhante a do Encoder. Ele também usa _get_clones para copiar 6 cópias da classe TransformerDecoderLayer e, em seguida, encaminha a entrada das 6 classes TransformerDecoderLayer por sua vez, mas diferentemente, o Decoder precisa inserir a saída de as 6 TransformerDecoderLayer e as 6 camadas seguintes A saída participará do cálculo de perda em conjunto.

class TransformerDecoder(nn.Module):

    def __init__(self, decoder_layer, num_layers, norm=None, return_intermediate=False):
        super().__init__()
        # 复制num_layers=decoder_layer=TransformerDecoderLayer
        self.layers = _get_clones(decoder_layer, num_layers)
        self.num_layers = num_layers   # 6
        self.norm = norm               # LN
        # 是否返回中间层 默认True  因为DETR默认6个Decoder都会返回结果,一起加入损失计算的
        # 每一层Decoder都是逐层解析,逐层加强的,所以前面层的解析效果对后面层的解析是有意义的,所以作者把前面5层的输出也加入损失计算
        self.return_intermediate = return_intermediate

    def forward(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: [100, bs, 256] 需要预测的目标query embedding 和 query_embed形状相同  且全设置为0
                            在每层decoder层中不断的被refine,相当于一次次的被coarse-to-fine的过程
        memory: [h*w, bs, 256]  Encoder输出  具有全局相关性(增强后)的特征表示
        tgt_mask: None
        tgt_key_padding_mask: None
        memory_key_padding_mask: [bs, h*w]  记录Encoder输出特征图的每个位置是否是被pad的(True无效   False有效)
        pos: [h*w, bs, 256]                 特征图的位置编码
        query_pos: [100, bs, 256]    query embedding的位置编码  随机初始化的
        """
        output = tgt   # 初始化query embedding  全是0

        intermediate = []  # 用于存放6层decoder的输出结果

        # 遍历6层decoder
        for layer in self.layers:
            output = layer(output, memory, tgt_mask=tgt_mask,
                           memory_mask=memory_mask,
                           tgt_key_padding_mask=tgt_key_padding_mask,
                           memory_key_padding_mask=memory_key_padding_mask,
                           pos=pos, query_pos=query_pos)
            # 6层结果全部加入intermediate
            if self.return_intermediate:
                intermediate.append(self.norm(output))

        if self.norm is not None:
            output = self.norm(output)
            if self.return_intermediate:
                intermediate.pop()
                intermediate.append(output)
        # 默认执行这里
        # 最后把  6x[100,bs,256] -> [6(6层decoder输出),100,bs,256]
        if self.return_intermediate:
            return torch.stack(intermediate)

        return output.unsqueeze(0)   # 不执行

TransformerDecoderLayer

Diagrama da estrutura da camada do decodificador:
insira a descrição da imagem aqui

camada do decodificador = Atenção Multi-Head Mascarada + Add&Norm + Atenção Multi-Head + add&Norm + feed forward + add&Norm. O ponto-chave está nas duas camadas de Atenção. Compreender os princípios e as diferenças entre essas duas camadas é a chave para entender o Decodificador.

class TransformerDecoderLayer(nn.Module):

    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)
        self.multihead_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.norm3 = nn.LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
        self.dropout3 = nn.Dropout(dropout)

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

    def with_pos_embed(self, tensor, pos: Optional[Tensor]):
        return tensor if pos is None else tensor + pos

    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: 需要预测的目标 query embedding  负责预测物体  用于建模图像当中的物体信息  在每层decoder层中不断的被refine
             [100, bs, 256]  和 query_embed形状相同  且全设置为0
        memory: [h*w, bs, 256]  Encoder输出  具有全局相关性(增强后)的特征表示
        tgt_mask: None
        memory_mask: None
        tgt_key_padding_mask: None
        memory_key_padding_mask: [bs, h*w]  记录Encoder输出特征图的每个位置是否是被pad的(True无效   False有效)
        pos: [h*w, bs, 256]  encoder输出特征图的位置编码
        query_pos: [100, bs, 256]  query embedding/tgt的位置编码  负责建模物体与物体之间的位置关系  随机初始化的
        tgt_mask、memory_mask、tgt_key_padding_mask是防止作弊的 这里都没有使用
        """
        # 第一个self-attention的目的:找到图像中物体的信息 -> tgt
        # 第一个多头自注意力层:输入qkv都和Encoder无关  都来自于tgt/query embedding
        # 通过第一个self-attention  可以不断建模物体与物体之间的关系  可以知道图像当中哪些位置会存在物体  物体信息->tgt
        # query embedding  +  query_pos
        q = k = self.with_pos_embed(tgt, query_pos)
        # masked multi-head self-attention  计算query embedding的自注意力
        tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]

        # add + norm
        tgt = tgt + self.dropout1(tgt2)
        tgt = self.norm1(tgt)

        # 第二个self-attention的目的:不断增强encoder的输出特征,将物体的信息不断加入encoder的输出特征中去,更好地表征了图像中的各个物体
        # 第二个多头注意力层,也叫Encoder-Decoder self attention:key和value来自Encoder层输出   Query来自Decoder层输入
        # 第二个self-attention 可以建模图像 与 物体之间的关系
        # 根据上一步得到的tgt作为query 不断的去encoder输出的特征图中去问(q和k计算相似度)  问图像当中的物体在哪里呢?
        # 问完之后再将物体的位置信息融合encoder输出的特征图中(和v做运算)  这样我得到的v的特征就有 encoder增强后特征信息 + 物体的位置信息
        # query = query embedding  +  query_pos
        # key = encoder输出特征 + 特征位置编码
        # value = encoder输出特征
        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]
        # ada + norm + Feed Forward + add + norm
        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)

        # [100, bs, 256]
        # decoder的输出是第一个self-attention输出特征 + 第二个self-attention输出特征
        # 最终的特征:知道图像中物体与物体之间的关系 + encoder增强后的图像特征 + 图像与物体之间的关系
        return tgt

    def forward_pre(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):
        tgt2 = self.norm1(tgt)
        q = k = self.with_pos_embed(tgt2, query_pos)
        tgt2 = self.self_attn(q, k, value=tgt2, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]
        tgt = tgt + self.dropout1(tgt2)
        tgt2 = self.norm2(tgt)
        tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt2, 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)
        tgt2 = self.norm3(tgt)
        tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2))))
        tgt = tgt + self.dropout3(tgt2)
        return tgt

    def forward(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):
        if self.normalize_before:
            return self.forward_pre(tgt, memory, tgt_mask, memory_mask,
                                    tgt_key_padding_mask, memory_key_padding_mask, pos, query_pos)
        return self.forward_post(tgt, memory, tgt_mask, memory_mask,
                                 tgt_key_padding_mask, memory_key_padding_mask, pos, query_pos)

Resuma o que o decodificador está fazendo:

  1. Da saída final do Codificador, obtemos a versão aprimorada da memória de recursos da imagem e as informações de posição pos do recurso;
  2. A informação do objeto tgt na imagem é customizada, inicializada para todos 0, e a informação de posição do objeto query_pos na imagem é inicializada aleatoriamente;
  3. A primeira auto-atenção: qk=tgt+query_pos, v=tgt, calcula a correlação entre objetos na imagem e é responsável por modelar as informações do objeto na imagem. O tgt1 final é a versão aprimorada das informações do objeto. Essas posições A informação contém a relação posicional entre os objetos;
  4. A segunda auto-atenção: q=tgt+qyery_pos, k=memory+pos, v=memory, use as informações do objeto tgt como consulta, vá para a memória do recurso de imagem para perguntar (calcule sua correlação), pergunte ao objeto na imagem Cadê? Depois que a pergunta é concluída, as informações de posição do objeto são integradas ao recurso de imagem (v). Todo o processo é responsável por modelar a relação entre o recurso de imagem e o recurso de objeto. O resultado final é um recurso de imagem mais forte tgt2, incluindo a saída do codificador Recursos de imagem aprimorados + recursos de localização de objetos.
  5. Finalmente, tgt1 + tgt2 = a saída do recurso de imagem aprimorada pelo Codificador + informação do objeto + informação da posição do objeto é usada como saída do decodificador;

Questão 1
Algumas pessoas podem se perguntar por que a informação do objeto tgt definida aqui é inicializada com todos os 0s, e a informação da posição do objeto query_pos é inicializada aleatoriamente, mas pode expressar um significado tão complicado? É obviamente inicializado para todos 0 ou inicializado aleatoriamente, como o modelo sabe o que eles representam? Na verdade, isso está relacionado à função de perda. Depois que a função de perda é definida, a rede aprende continuamente calculando a perda, o retorno do gradiente e, finalmente, aprendido tgt e query_pos são os significados expressos aqui. Isso é o mesmo que a perda de regressão.Depois de definir esses quatro canais para representar xywh, como a rede sabe? É através do retorno do gradiente da função de perda que a rede continua aprendendo e finalmente sabe que esses quatro canais representam xywh.

Questão 2
Por que usamos tgt1 + tgt2 como saída do decodificador aqui? Em vez de usar tgt1 ou tgt2 sozinho?

  1. Em primeiro lugar, tgt1 representa as informações do objeto na imagem + as informações de localização do objeto, mas não possui muitos recursos de imagem, o que não é aceitável, e o efeito de previsão final definitivamente não é bom (prever a categoria do objeto é definitivamente não muito preciso);
  2. Em segundo lugar, os recursos de imagem da versão aprimorada do codificador representado por tgt2 + as informações de posição do objeto, faltam as informações do objeto, o que não é aceitável, e o efeito de previsão final definitivamente não é bom (prevendo a posição de o objeto definitivamente não é muito preciso); portanto, os dois são
    comparáveis. Os recursos adicionados são usados ​​como saída do decodificador para prever a categoria e a posição do objeto, e o efeito é o melhor.

Função de perda + pós-processamento

Cálculo de perda: SetCriterion

Primeiro, a função de perda será definida em detr.py:

criterion = SetCriterion(num_classes, matcher=matcher, weight_dict=weight_dict,eos_coef=args.eos_coef, losses=losses)
criterion.to(device)

Em seguida, chame a função de critério após o raciocínio direto no train_one_epoch de engine.py para calcular a perda:

# 前向传播
outputs = model(samples)
# 计算损失  loss_dict: 'loss_ce' + 'loss_bbox' + 'loss_giou'    用于log日志: 'class_error' + 'cardinality_error'
loss_dict = criterion(outputs, targets)
# 权重系数 {'loss_ce': 1, 'loss_bbox': 5, 'loss_giou': 2}
weight_dict = criterion.weight_dict   
# 总损失 = 回归损失:loss_bbox(L1)+loss_bbox  +   分类损失:loss_ce
losses = sum(loss_dict[k] * weight_dict[k] for k in loss_dict.keys() if k in weight_dict)

Bem, vamos focar na classe SetCriterion:

class SetCriterion(nn.Module):
    """ This class computes the loss for DETR.
    The process happens in two steps:
        1) we compute hungarian assignment between ground truth boxes and the outputs of the model
        2) we supervise each pair of matched ground-truth / prediction (supervise class and box)
    """
    def __init__(self, num_classes, matcher, weight_dict, eos_coef, losses):
        """ Create the criterion.
        Parameters:
            num_classes: number of object categories, omitting the special no-object category
            matcher: module able to compute a matching between targets and proposals
            weight_dict: dict containing as key the names of the losses and as values their relative weight.
            eos_coef: relative classification weight applied to the no-object category
            losses: list of all the losses to be applied. See get_loss for list of available losses.
        """
        super().__init__()
        self.num_classes = num_classes     # 数据集类别数
        self.matcher = matcher             # HungarianMatcher()  匈牙利算法 二分图匹配
        self.weight_dict = weight_dict     # dict: 18  3x6  6个decoder的损失权重   6*(loss_ce+loss_giou+loss_bbox)
        self.eos_coef = eos_coef           # 0.1
        self.losses = losses               # list: 3  ['labels', 'boxes', 'cardinality']
        empty_weight = torch.ones(self.num_classes + 1)
        empty_weight[-1] = self.eos_coef   # tensro: 92   前91=1  92=eos_coef=0.1
        self.register_buffer('empty_weight', empty_weight)
        
    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
                      dict: 'pred_logits'=Tensor[bs, 100, 92个class]  'pred_boxes'=Tensor[bs, 100, 4]  最后一个decoder层输出
                             'aux_output'={list:5}  0-4  每个都是dict:2 pred_logits+pred_boxes 表示5个decoder前面层的输出
             targets: list of dicts, such that len(targets) == batch_size.   list: bs
                      每张图片包含以下信息:'boxes'、'labels'、'image_id'、'area'、'iscrowd'、'orig_size'、'size'
                      The expected keys in each dict depends on the losses applied, see each loss' doc
        """
        # dict: 2   最后一个decoder层输出  pred_logits[bs, 100, 92个class] + pred_boxes[bs, 100, 4]
        outputs_without_aux = {
    
    k: v for k, v in outputs.items() if k != 'aux_outputs'}

        # 匈牙利算法  解决二分图匹配问题  从100个预测框中找到和N个gt框一一对应的预测框  其他的100-N个都变为背景
        # Retrieve the matching between the outputs of the last layer and the targets  list:1
        # tuple: 2    0=Tensor3=Tensor[5, 35, 63]  匹配到的3个预测框  其他的97个预测框都是背景
        #             1=Tensor3=Tensor[1, 0, 2]    对应的三个gt框
        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)   # int 统计这整个batch的所有图片的gt总个数  3
        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()   # 3.0

        # 计算最后层decoder损失  Compute all the requested losses
        losses = {
    
    }
        for loss in self.losses:
            losses.update(self.get_loss(loss, outputs, targets, indices, num_boxes))

        # 计算前面5层decoder损失  累加到一起  得到最终的losses
        # In case of auxiliary losses, we repeat this process with the output of each intermediate layer.
        if 'aux_outputs' in outputs:
            for i, aux_outputs in enumerate(outputs['aux_outputs']):
                indices = self.matcher(aux_outputs, targets)   # 同样匈牙利算法匹配
                for loss in self.losses:   # 计算各个loss
                    if loss == 'masks':
                        # Intermediate masks losses are too costly to compute, we ignore them.
                        continue
                    kwargs = {
    
    }
                    if loss == 'labels':
                        # Logging is enabled only for the last layer
                        kwargs = {
    
    'log': False}
                    l_dict = self.get_loss(loss, aux_outputs, targets, indices, num_boxes, **kwargs)
                    l_dict = {
    
    k + f'_{
      
      i}': v for k, v in l_dict.items()}
                    losses.update(l_dict)
        # 参加权重更新的损失:losses: 'loss_ce' + 'loss_bbox' + 'loss_giou'    用于log日志: 'class_error' + 'cardinality_error'
        return losses

A função inteira está fazendo principalmente duas coisas:

  1. Chame a função self.matcher para combinar N (número gt) quadros de predição reais de 100 quadros de predição e combine o quadro de predição correspondente a cada quadro gt;
  2. Chame self.get_loss para calcular cada perda

Algoritmo húngaro, correspondência de gráfico bipartido: self.matcher

Para o princípio do algoritmo húngaro, você pode dar uma olhada neste blog clássico: Notas de estudo do algoritmo (5): Algoritmo húngaro
No DETR, a classe HungarianMatcher em models/matcher.py implementa o algoritmo de correspondência húngaro:

class HungarianMatcher(nn.Module):
    """This class computes an assignment between the targets and the predictions of the network

    For efficiency reasons, the targets don't include the no_object. Because of this, in general,
    there are more predictions than targets. In this case, we do a 1-to-1 matching of the best predictions,
    while the others are un-matched (and thus treated as non-objects).
    """

    def __init__(self, cost_class: float = 1, cost_bbox: float = 1, cost_giou: float = 1):
        """Creates the matcher

        Params:
            cost_class: This is the relative weight of the classification error in the matching cost
            cost_bbox: This is the relative weight of the L1 error of the bounding box coordinates in the matching cost
            cost_giou: This is the relative weight of the giou loss of the bounding box in the matching cost
        """
        super().__init__()
        self.cost_class = cost_class
        self.cost_bbox = cost_bbox
        self.cost_giou = cost_giou
        assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, "all costs cant be 0"
    
    # 不需要更新梯度  只是一种匹配方式
    @torch.no_grad()
    def forward(self, outputs, targets):
        """ Performs the matching

        Params:
            outputs: This is a dict that contains at least these entries:
                 "pred_logits": Tensor of dim [batch_size, num_queries, num_classes]=[bs,100,92] with the classification logits
                 "pred_boxes": Tensor of dim [batch_size, num_queries, 4]=[bs,100,4] with the predicted box coordinates

            targets: list:bs This is a list of targets (len(targets) = batch_size), where each target is a dict containing:
                 "labels": Tensor of dim [num_target_boxes]=[3] (where num_target_boxes is the number of ground-truth
                           objects in the target) containing the class labels
                 "boxes": Tensor of dim [num_target_boxes, 4] containing the target box coordinates

        Returns:
            A list of size batch_size, containing tuples of (index_i, index_j) where:
                - index_i is the indices of the selected predictions (in order)
                - index_j is the indices of the corresponding selected targets (in order)
            For each batch element, it holds:
                len(index_i) = len(index_j) = min(num_queries, num_target_boxes)
        """
        # batch_size  100
        bs, num_queries = outputs["pred_logits"].shape[:2]

        # We flatten to compute the cost matrices in a batch
        # [2,100,92] -> [200, 92] -> [200, 92]概率
        out_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1)  # [batch_size * num_queries, num_classes]
        # [2,100,4] -> [200, 4]
        out_bbox = outputs["pred_boxes"].flatten(0, 1)  # [batch_size * num_queries, 4]

        # Also concat the target labels and boxes
        # [3]  idx = 32, 1, 85  concat all labels
        tgt_ids = torch.cat([v["labels"] for v in targets])
        # [3, 4]  concat all box
        tgt_bbox = torch.cat([v["boxes"] for v in targets])

        # 计算损失   分类 + L1 box + GIOU box
        # Compute the classification cost. Contrary to the loss, we don't use the NLL,
        # but approximate it in 1 - proba[target class].
        # The 1 is a constant that doesn't change the matching, it can be ommitted.
        cost_class = -out_prob[:, tgt_ids]

        # Compute the L1 cost between boxes
        cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)

        # Compute the giou cost betwen boxes
        cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))

        # Final cost matrix   [100, 3]  bs*100个预测框分别和3个gt框的损失矩阵
        C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou
        C = C.view(bs, num_queries, -1).cpu()  # [bs, 100, 3]

        sizes = [len(v["boxes"]) for v in targets]   # gt个数 3

        # 匈牙利算法进行二分图匹配  从100个预测框中挑选出最终的3个预测框 分别和gt计算损失  这个组合的总损失是最小的
        # 0: [3]  5, 35, 63   匹配到的gt个预测框idx
        # 1: [3]  1, 0, 2     对应的gt idx
        indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))]
        
        # list: bs  返回bs张图片的匹配结果
        # 每张图片都是一个tuple:2
        # 0 = Tensor[gt_num,]  匹配到的正样本idx       1 = Tensor[gt_num,]  gt的idx
        return [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices]

Na verdade, é primeiro calcular a perda total de cada caixa de previsão (100) e cada caixa gt para formar uma matriz de perda C e, em seguida, chamar o algoritmo húngaro escrito por scipy.optimize.linear_sum_assignment. O princípio da correspondência é o mínimo "soma da perda" (aqui A perda não é a perda real, aqui é apenas um método de medição, que é diferente do método de cálculo da perda), e o único quadro de previsão responsável correspondente a cada gt é obtido, e outros quadros de previsão serão ser automaticamente classificado como plano de fundo.

linear_sum_assignment, insira uma matriz métrica (matriz de custo) de um gráfico bipartido, calcule o método de atribuição de peso mínimo da matriz métrica deste gráfico bipartido e retorne o índice de linha da matriz (caixa de previsão idx) e o índice de coluna (gt box idx) correspondente ao esquema correspondente.

Calcular perda: self.get_loss

self.get_loss é uma função definida pela classe SetCriterion:

    def get_loss(self, loss, outputs, targets, indices, num_boxes, **kwargs):
        loss_map = {
    
    
            'labels': self.loss_labels,
            'cardinality': self.loss_cardinality,
            'boxes': self.loss_boxes,
            'masks': self.loss_masks
        }
        assert loss in loss_map, f'do you really want to compute {
      
      loss} loss?'
        return loss_map[loss](outputs, targets, indices, num_boxes, **kwargs)

Ao mesmo tempo, a perda de classificação (self.loss_labels), perda de regressão (self.boxes) e perda de cardinalidade são chamadas. No entanto, a perda de cardinalidade é usada apenas para log e não participa das atualizações de gradiente, portanto não será descrita aqui. Além disso, se for uma tarefa de segmentação, há também um cálculo de perda de segmentação de máscara, que não será descrito aqui por enquanto.

Perda de classificação: self.loss_labels

Perda de classificação self.loss_labels:

    def loss_labels(self, outputs, targets, indices, num_boxes, log=True):
        """Classification loss (NLL)
        targets dicts must contain the key "labels" containing a tensor of dim [nb_target_boxes]
        outputs:'pred_logits'=[bs, 100, 92] 'pred_boxes'=[bs, 100, 4] 'aux_outputs'=5*([bs, 100, 92]+[bs, 100, 4])
        targets:'boxes'=[3,4] labels=[3] ...
        indices: [3] 如:5,35,63  匹配好的3个预测框idx
        num_boxes:当前batch的所有gt个数
        """
        assert 'pred_logits' in outputs
        src_logits = outputs['pred_logits']  # 分类:[bs, 100, 92类别]

        # idx tuple:2  0=[num_all_gt] 记录每个gt属于哪张图片  1=[num_all_gt] 记录每个匹配到的预测框的index
        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)
        # 正样本+负样本  上面匹配到的预测框作为正样本 正常的idx  而100个中没有匹配到的预测框作为负样本(idx=91 背景类)
        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}

        # 日志 记录Top-1精度
        if log:
            # TODO this should probably be a separate loss, not hacked in this one here
            losses['class_error'] = 100 - accuracy(src_logits[idx], target_classes_o)[0]

        # losses: 'loss_ce': 分类损失
        #         'class_error':Top-1精度 即预测概率最大的那个类别与对应被分配的GT类别是否一致  这部分仅用于日志显示 并不参与模型训练
        return losses

    def _get_src_permutation_idx(self, indices):
        # permute predictions following indices
        # [num_all_gt]  记录每个gt都是来自哪张图片的 idx
        batch_idx = torch.cat([torch.full_like(src, i) for i, (src, _) in enumerate(indices)])
        # 记录匹配到的预测框的idx
        src_idx = torch.cat([src for (src, _) in indices])
        return batch_idx, src_idx

Perceber:

  1. perda de classificação = perda de entropia cruzada;
  2. Amostras positivas + amostras negativas = 100, número de amostras positivas = número de GTs, número de amostras negativas = 100 - número de GTs;
  3. 92 categorias, idx=91 significa categoria de fundo;
  4. Observe que há uma função _get_src_permutation_id aqui, que é principalmente sobre achatar o quadro de previsão. Originalmente, tinha a dimensão de lote, mas agora é achatada em uma dimensão, o que é conveniente para o cálculo subsequente de perdas;
  5. Uma precisão class_error: Top-1 também é calculada aqui para exibição de log;

perda de regressão: self.boxes

self.boxes de perda de regressão:

    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.
        outputs:'pred_logits'=[bs, 100, 92] 'pred_boxes'=[bs, 100, 4] 'aux_outputs'=5*([bs, 100, 92]+[bs, 100, 4])
        targets:'boxes'=[3,4] labels=[3] ...
        indices: [3] 如:5,35,63  匹配好的3个预测框idx
        num_boxes:当前batch的所有gt个数
        """
        assert 'pred_boxes' in outputs
        # idx tuple:2  0=[num_all_gt] 记录每个gt属于哪张图片  1=[num_all_gt] 记录每个匹配到的预测框的index
        idx = self._get_src_permutation_idx(indices)

        # [all_gt_num, 4]  这个batch的所有正样本的预测框坐标
        src_boxes = outputs['pred_boxes'][idx]
        # [all_gt_num, 4]  这个batch的所有gt框坐标
        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')

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

        # 计算GIOU损失
        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

        # 'loss_bbox': L1回归损失   'loss_giou': giou回归损失  
        return losses

Perceber:

  1. Perda de regressão: calcula apenas a perda de regressão de todas as amostras positivas;
  2. Perda de Regressão = Perda L1 + Perda GIOU

Pós-processamento bbox: PostProcess

Esta parte é o link de teste. Após a propagação direta, a perda é calculada para exibição de log e o índice de coco é calculado.
Também primeiro defina a função de pós-processamento em detr.py:

# 定义后处理
postprocessors = {
    
    'bbox': PostProcess()}	

Em seguida, chame a função PostProcess após o raciocínio direto na avaliação de engine.py para pós-processar os 100 quadros previstos:

# 前向传播
outputs = model(samples)
# 后处理
# orig_target_sizes = [bs, 2]  bs张图片的原图大小
orig_target_sizes = torch.stack([t["orig_size"] for t in targets], dim=0)
# list: bs    每个list都是一个dict  包括'scores'  'labels'  'boxes'三个字段
# scores = Tensor[100,]  这张图片预测的100个预测框概率分数
# labels = Tensor[100,]  这张图片预测的100个预测框所属类别idx
# boxes = Tensor[100, 4] 这张图片预测的100个预测框的绝对位置坐标(相对这张图片的原图大小的坐标)
results = postprocessors['bbox'](outputs, orig_target_sizes)

classe PostProcess:

class PostProcess(nn.Module):
    """ This module converts the model's output into the format expected by the coco api"""
    @torch.no_grad()
    def forward(self, outputs, target_sizes):
        """ Perform the computation
        Parameters:
            outputs: raw outputs of the model
                     0 pred_logits 分类头输出[bs, 100, 92(类别数)]
                     1 pred_boxes 回归头输出[bs, 100, 4]
                     2 aux_outputs list: 5  前5个decoder层输出 5个pred_logits[bs, 100, 92(类别数)] 和 5个pred_boxes[bs, 100, 4]
            target_sizes: tensor of dimension [batch_size x 2] containing the size of each images of the batch
                          For evaluation, this must be the original image size (before any data augmentation)
                          For visualization, this should be the image size after data augment, but before padding
        """
        # out_logits:[bs, 100, 92(类别数)]
        # out_bbox:[bs, 100, 4]
        out_logits, out_bbox = outputs['pred_logits'], outputs['pred_boxes']

        assert len(out_logits) == len(target_sizes)
        assert target_sizes.shape[1] == 2

        # [bs, 100, 92]  对每个预测框的类别概率取softmax
        prob = F.softmax(out_logits, -1)
        # prob[..., :-1]: [bs, 100, 92] -> [bs, 100, 91]  删除背景
        # .max(-1): scores=[bs, 100]  100个预测框属于最大概率类别的概率
        #           labels=[bs, 100]  100个预测框的类别
        scores, labels = prob[..., :-1].max(-1)

        # cxcywh to xyxy  format   [bs, 100, 4]
        boxes = box_ops.box_cxcywh_to_xyxy(out_bbox)
        # and from relative [0, 1] to absolute [0, height] coordinates  bs张图片的宽和高
        img_h, img_w = target_sizes.unbind(1)
        scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1)
        boxes = boxes * scale_fct[:, None, :]  # 归一化坐标 -> 绝对位置坐标(相对于原图的坐标)  [bs, 100, 4]

        results = [{
    
    'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)]

        # list: bs    每个list都是一个dict  包括'scores'  'labels'  'boxes'三个字段
        # scores = Tensor[100,]  这张图片预测的100个预测框概率分数
        # labels = Tensor[100,]  这张图片预测的100个预测框所属类别idx
        # boxes = Tensor[100, 4] 这张图片预测的100个预测框的绝对位置坐标(相对这张图片的原图大小的坐标)
        return results

Pode-se ver que o pós-processamento é realmente para contar os resultados da previsão, remover a classe de fundo e obter as pontuações de probabilidade das categorias, rótulos e coordenadas de posição absoluta das 100 caixas de previsão previstas para cada imagem.

Então, finalmente, envie este resultado para coco_evaluator para calcular os indicadores relacionados ao coco.

Ao prever, de fato, nossos objetos preditos finais geralmente não têm objetos 100. Como lidamos com isso neste momento? Geralmente, um limite (0,7) da pontuação de probabilidade de previsão é definido e os quadros de previsão maiores que essa previsão serão retidos e exibidos no final, e os quadros de previsão menores que a previsão serão descartados.

Foco de aprendizado de código-fonte

  1. backbone:Codificação Posicional(PositionEmbeddingSine);
  2. Transformer:TransformerEncoderLayer + TransformerDecoderLayer;
  3. Função de perda: algoritmo húngaro, correspondência de gráfico bipartido (self.matcher)
  4. Pós-processamento: PostProcess

alguns problemas

  1. Por que o ViT só tem codificador e o DETR usa codificador + decodificador?
    Codificador: A auto-atenção do codificador executa principalmente a modelagem global e aprende características globais. Através desta etapa, cada objeto na imagem pode ser basicamente separado tanto quanto possível; Decodificador: Neste momento, use a auto-atenção do Decodificador e faça a detecção de alvo
    E tarefas de segmentação, o modelo pode dividir ainda mais a área de ponto extremo do limite do objeto em uma divisão mais precisa, tornando o reconhecimento de borda mais preciso;
  2. Qual é o uso da consulta de objeto?
    A consulta de objeto é usada para substituir a âncora . Ao introduzir uma consulta de objeto que pode ser aprendida, o modelo pode aprender automaticamente quais áreas na imagem podem ter objetos. Finalmente, 100 dessas áreas com possíveis objetos podem ser encontradas por meio da consulta de objeto. Em seguida, encontre o quadro de previsão efetivo entre os 100 quadros de previsão por meio da correspondência de gráficos bipartidos e calcule a perda.
    Portanto, a consulta de objeto tem o papel de substituir a âncora, e encontra a área onde pode haver objetos de forma apreensível, sem causar um grande número de quadros redundantes devido ao uso da âncora.

citar

  1. Explicação do código-fonte da estação b: trabalhadores da linha de montagem blindados
  2. Zhihu [Irmão Buffalo]: Interpretação do código-fonte DETR
  3. Explicação do código-fonte CSDN [esquilo trabalhando duro]: notas do código-fonte DETR (1)
  4. Explicação do código-fonte CSDN [esquilo trabalhando duro]: notas do código-fonte DETR (2)
  5. CSDN: codificação de posição no Transformer (codificação de posição um)
  6. Sabendo que o CV não será eliminado- [análise do código-fonte, detecção de destino, estrela transfronteiriça DETR (1), visão geral e inferência do modelo]
  7. Sabendo que o CV não será eliminado- [análise do código-fonte, detecção do alvo, estrela transfronteiriça DETR (2), processo de treinamento do modelo e processamento de dados]
  8. Sabendo que o CV não será eliminado- [Detecção de alvo de análise de código-fonte DETR estrela transfronteiriça (3), Backbone e codificação de posição]
  9. Sabendo que CV não será eliminado- [Detecção de alvo de análise de código-fonte DETR estrela transfronteiriça (4), Detecção com transformador]
  10. Sabendo que o CV não será eliminado- [análise de código-fonte, detecção de destino, estrela cruzada DETR (5), função de perda e algoritmo de correspondência húngaro]
  11. Sabendo que o CV não será eliminado- [análise de código-fonte, detecção de destino, estrela transfronteiriça DETR (6), saída do modelo e geração de previsão]
  12. Leitura intensiva de documentos DETR e análise da estrutura do modelo
  13. MMDet——interpretação do código-fonte DETR

Acho que você gosta

Origin blog.csdn.net/Lc_001/article/details/129429264
Recomendado
Clasificación