Explicación detallada de DETR

Código fuente de Github: facebookresearch/detr
Versión de anotación de Github código fuente: HuKai97/detr-annotations
Documento: Detección de objetos de extremo a extremo con transformadores
Reimpreso: [análisis de código fuente de DETR]

descripción general

DETR significa DE tection TR ansformer.Es un modelo de CV propuesto por Facebook AI Research Institute.Se utiliza principalmente para la detección de objetivos y también se puede utilizar para tareas de segmentación. Este modelo utiliza Transformer para reemplazar las complejas rutinas tradicionales de detección de objetivos , como la de dos etapas o de una etapa, basada en ancla o sin ancla, posprocesamiento nms, etc.; tampoco utiliza algunas técnicas coquetas, como como usar la fusión de características de escala múltiple, usar algunos tipos especiales de convolución (como convolución de grupo, convolución variable, convolución de generación dinámica, etc.) para extraer características, mapear diferentes tipos de mapas de características para desacoplar tareas de clasificación y regresión, e incluso es mejora de datos Todo el proceso es usar CNN para extraer características y luego codificar y decodificar para obtener el resultado previsto .

inserte la descripción de la imagen aquí

Se puede decir que el trabajo en general es muy sólido. Aunque el efecto no es tan bueno como SOTA, es de gran importancia utilizar el Transformador, que los alquimistas suelen considerar perteneciente al campo de la PNL, para cruzar fronteras. al campo CV, y puede funcionar. También vale la pena aprender. Este tipo de trabajo que rompe con la tradición y crea la era suele ser popular, como Faster R-CNN y YOLO, y puede ver que muchos trabajos posteriores se mejoran sobre la base de ellos.

En pocas palabras, DETR considera la tarea de detección de objetivos como un problema de predicción establecido.Para una imagen, se predice de forma fija un cierto número de objetos (el original es 100, que se puede cambiar en el código), y el modelo se basa en el relación entre estos objetos y el contexto global en la imagen. La relación genera directamente el conjunto de predicción en paralelo, es decir, el Transformador decodifica los resultados de la predicción de todos los objetos en la imagen al mismo tiempo. Esta característica paralela hace que DETR sea muy eficiente.

Características :

  1. De extremo a extremo: se eliminan NMS y Anchor, no hay tantos hiperparámetros, la cantidad de cálculo se reduce considerablemente y toda la red se vuelve muy simple;
  2. Basado en Transformer: Transformer se introduce en la tarea de detección de objetivos por primera vez;
  3. Se propone una nueva función de pérdida basada en conjuntos: el método de coincidencia de gráfico bipartito se usa para obligar al modelo a generar un conjunto de marcos de predicción únicos, y cada objeto solo generará un marco de predicción, convirtiendo así el problema de detección de objetivos en una predicción establecida. Problema, por lo que NMS no se utiliza para lograr el efecto de extremo a extremo;
  4. Además, un conjunto de consultas de objetos científicos y características globales en línea generadas por el codificador se ingresan en el decodificador, y los 100 cuadros de predicción de salida final se fuerzan directamente en paralelo para reemplazar el ancla;
    desventajas :
  5. El efecto de detección en objetos grandes es muy bueno, pero el efecto de monitoreo en objetos pequeños no es muy bueno, el entrenamiento es relativamente lento;
  6. Debido al diseño y la inicialización de la consulta, lleva mucho tiempo entrenar el modelo DETR desde cero;
    ventajas :
  7. La velocidad y precisión en el conjunto de datos COCO es similar a Faster RCNN, se puede extender a muchas tareas subdivididas, como segmentación, seguimiento, multimodalidad, etc.;

inserte la descripción de la imagen aquí

Proceso :

  1. Después de que la imagen de entrada pase a través de la red CNN, se obtiene la matriz de características de la imagen;
  2. Enderezar la imagen y agregar codificación de posición;
  3. Ingrese la información de correlación de las funciones de aprendizaje en el codificador del transformador;
  4. Use la salida del codificador y la consulta del objeto como entrada del decodificador para obtener la información decodificada;
  5. Pase la información decodificada a FFN para obtener información de predicción;
  6. juzgar si la información de predicción de FFN contiene el objeto objetivo real;
  7. Si lo hay, genere el cuadro y la categoría predichos; de lo contrario, genere una clase sin objeto.

principio

Función de pérdida basada en la predicción del conjunto

Coincidencia de gráficos bipartitos para determinar cuadros de predicción efectivos

Se predicen N (100) marcos de predicción, y gt es M marcos, generalmente N>M, entonces, ¿cómo calcular la pérdida?
Aquí, primero realizamos una comparación bipartita de estos 100 marcos de predicción y marcos gt, y primero determinamos qué marco de predicción a cada gt le corresponde, y finalmente calcular la pérdida total de M tramas de predicción y M tramas gt.

De hecho, es muy simple. Suponiendo que ahora hay una matriz, la abscisa son los 100 marcos de predicción que predijimos, y la ordenada es el marco gt. Calcule el costo de cada marco de predicción y todos los demás marcos gt por separado, formando así una matriz de costos y luego determine cómo asignar todos los cuadros gt a los cuadros de predicción correspondientes para minimizar el costo total final.

El método de cálculo aquí es el clásico algoritmo húngaro , que generalmente se realiza llamando a la función linear_sum_assignment en el paquete scipy. La entrada a esta función es la matriz de costos y genera un conjunto de índices de fila y un índice de columna correspondiente, lo que brinda la mejor asignación.

Por lo tanto, a través de los pasos anteriores, se determina qué tramas de predicción de las 100 tramas de predicción finales se utilizarán como tramas de predicción efectivas y qué tramas de predicción se denominarán fondos. Luego calcule la pérdida final del marco de predicción efectivo y el marco gt (el número de marcos de predicción efectivos es igual al número de marcos gt).

función de pérdida

Pronóstico: Específico+FrecuenciaL
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]Lhúngaro ( y ,y^)=yo = 1norte[ -iniciar sesiónpag^pag^ (yo)( doyo)+1{ doyo= }Lcaja ( segundoyo,b^pag^( i ) ) ]
pérdida de clasificación: pérdida de entropía cruzada, eliminar
pérdida de regresión logarítmica: pérdida GIOU + pérdida L1

código fuente

todo el marco

  • Columna vertebral :

    • Función: extrae las características de la imagen y reduce la escala de la imagen
    • Estructura: Resnet
    • Entrada: un lote de imagen [B, 3, W, H]
    • Salida: mapa de características [B, 2048, W_feat, H_feat] de la última capa de Resnet50
  • capa de transición :

    • Función: Convierte la salida del backbone a la forma requerida por el codificador y prepara otras variables requeridas por el Transformador, como máscaras, posicionamiento_embedding, etc.
  • Transformador :

    • Estructura: codificador, decodificador
    • Entrada total:
      • x : mapa de características de entrada [B, 256, W_feat, H_feat]
      • máscara : máscara de relleno efectiva en codificador y decodificador [B, W_feat, H_feat]
      • query_embed : el vector incrustado de consulta en el decodificador [num_query(100), 256]
      • positional_embedding : el vector de incrustación de codificación posicional de la función iamge, query_pos en el codificador, key_pos en el decodificador [B, 256, W_feat, H_feat]
    • Salida total:
      • out_dec : la salida del decodificador** (salida de consulta de destino)****[num_dec_layers(6), B, num_query(100), 256]**, aunque mantenemos la salida de todas las capas del decodificador, pero quiero La salida final es solo la salida de la última capa de la capa del decodificador
      • memoria : la salida del codificador [B, 256, W_feat, H_feat]
  • Codificador en Transformador :

    • Función: realizar la codificación de posición y la autoatención en el mapa de características, y agregar las características globales del mapa de características a través de la autoatención
    • 结构:auto_atención, normalización, red de retroalimentación, normalización
    • ingresar:
      • consulta : Ingrese la consulta, que es el mapa de características plano [W_feat*H_feat, B, 256]
      • value , key : En autoatención, k y v se calculan mediante q, por lo que la entrada es Ninguno
      • query_key_padding_mask : máscara de relleno efectiva del mapa de características [B, W_feat*H_feat]
      • query_positional_embedding : codificación de posición del mapa de características [W_feat*H_feat, B, 256]
    • producción:
      • encoder_out : mapa de características aplanadas de autoatención de salida [W_feat*H_feat, B, 256]
  • Decodificador en Transformador :

    • efecto:
      • La primera parte es realizar la autoatención en la consulta del objeto, de modo que cada consulta del objeto preste atención a la información diferente del objeto.
      • La segunda parte es realizar una atención cruzada en la consulta del objeto y encoder_out, de modo que la consulta del objeto pueda encontrar diferentes objetos en el mapa de características.
    • estructura:
      • autoatención(auto_atención、normalización)
      • atención cruzada (atención cruzada, normalización, red de avance, normalización)
    • atención propia :
      • ingresar:
        • consulta : Inicializar consulta de objeto [num_layer(6), num_query(100), B, 256]
        • clave, valor : En autoatención, k y v son calculados por q, Ninguno
        • query_positional_embedding : consulta de codificación posicional [num_layer(6), num_query(100), B, 256]
        • máscara : En la autoatención del decodificador, no hay entrada de máscara Ninguno
      • producción:
        • Consulta de objeto de autoatención [num_layer(6), num_query(100), B, 256]
    • atención cruzada :
      • ingresar:
        • consulta : La consulta de objeto obtenida a través de la autoatención, es decir, la salida del módulo de autoatención [num_layer(6), num_query(100), B, 256]
        • clave, valor : en atención cruzada, tanto k como v son la salida del codificador [N, B, 256]
        • query_positional_embedding : consulta de codificación posicional [num_layer(6), num_query(100), B, 256]
        • key_positional_embedding : la codificación posicional de la clave, es decir, la codificación posicional del mapa de características [W_feat*H_feat**, **B, 256]
        • key_padding_mask: clave, que es la máscara de relleno efectiva del mapa de características [B, W_feat*H_feat]
      • producción:
        • out_dec : salida del decodificador [num_layers(6), B, num_query(100), 256]
        • memoria : salida del codificador [W_feat*H_feat, B, 256]

El proceso de construcción del modelo se divide en:

  1. Construcción DETR: Backbone + Transformador + MLP (Perceptrón multicapa, red neuronal multicapa)
  2. Función de pérdida de inicialización: criterio + procesamiento posterior a la inicialización: posprocesadores
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 un 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])]

Columna vertebral

Backbone incluye principalmente dos partes de extracción de características CNN y codificación de posición.
El primero es llamar a la función build_backbone en models/Backbone.py para crear 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

Aquí, primero se llama a la función build_position_encodingpara generar la codificación de posición de seno y coseno position_embedding: [batchsize, 256, H/32, W/32], donde los primeros 128 de 256 son la codificación de posición en la dirección y, y los últimos 128 es la codificación de posición en la dirección x; luego llame a la clase Backbone para generar un par ResNet50 Los datos de entrada se someten a extracción de características para obtener un mapa de características [tamaño de lote, 2048, H/32, W/32]. Finalmente, Joiner fusiona y almacena los dos para su uso posterior.

CNN

Para crear ResNet50, primero llame a la clase 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 clase se hereda de la clase BackboneBase, y CNN llama directamente al modelo en torchvision.models, así que mire directamente a la clase 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 clase sigue llamando al modelo en torchvision.models, y luego ingresa los datos de imagen preprocesados ​​[bs, 3, 608, 810] y los datos de máscara [bs, 608, 810] en el modelo (esta información de imagen se pasa al relleno del pad datos, y los datos de la máscara son para registrar qué posiciones de píxeles de estas imágenes son almohadilla, lo cual es verdadero, y los datos efectivos reales sin almohadilla son falsos). Después de la propagación directa, se llama a la función IntermediateLayerGetter para extraer el mapa de características de la capa correspondiente, y se obtiene el mapa de características [bs, 2048, 19, 26] de la imagen original muestreada 32 veces, y la máscara correspondiente a esta característica mapa [bs, 19, 26].

Codificación posicional

La codificación posicional es una codificación posicional. Aquí es principalmente para llamar a la función build_position_encoding en models/position_encoding.py para crear una codificación de posición:

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

Puede verse que el código fuente implementa dos tipos de codificación de posición, una es la codificación de posición absoluta seno-coseno, que no requiere aprendizaje de parámetros adicional, y la otra es la codificación de posición absoluta aprendible. El documento original usa codificación de posición absoluta seno-coseno, y el código también usa esto de forma predeterminada, por lo que aquí presentamos principalmente la clase 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 modelo ) PE ( pos , 2 i + 1 ) = cos ⁡ ( pos / 1000 0 2 i / d modelo ) \begin{ alineado} P E_{(pos, 2 i)} & =\sin \left(\text { pos } / 10000^{2 i / d_{\text {modelo }}}\right) \\ P E_{(pos , 2 i+1)} & =\cos \left(pos / 10000^{2 i / d_{\text {modelo}}}\right) \end{alineado}educación física( pos , 2 i ) _educación física( pos , 2 i + 1 ) _=pecado(  posición  /1000 02 i / dmodelo )=porque( p o /1000 02 i / dmodelo )
Mi comprensión de algunos puntos clave:

  1. Aquí, el código de posición se construye a través de la máscara. La máscara registra si cada posición de píxel en el mapa de características es un pad. Solo la posición donde es Falso es una posición válida, y el código de posición debe construirse;
  2. Respecto a la normalización del valor máximo: debido al método de codificación seno-coseno, la idea es mapear la fórmula de paso de cada posición al rango de 0~2Π (también puede ser 4Π, 6Π, 8Π..., porque es una función periódica, pero generalmente usamos 2Π por defecto, por lo que x_embed y y_embed deben normalizarse antes de incluirse en la fórmula;
  3. Con respecto al método de codificación de posición: la razón aquí es codificar x e y respectivamente (codificación de posición bidimensional), en lugar de codificación de posición unidimensional como un transformador. La consideración principal es que el transformador se aplica en el modelo de lenguaje, que es naturalmente unidimensional, por lo que unidimensional puede ser más adecuado, y DETR es un marco de detección de objetivos que se aplica en tareas de imagen. -codificación de posición dimensional El efecto puede ser mejor;
  4. De esta forma, para cada posición (x, y), el valor de codificación correspondiente a su columna está en las primeras 128 dimensiones de la dimensión del canal, y el valor de codificación de su fila está en las últimas 128 dimensiones de la dimensión del canal. De esta forma, cada posición en el mapa de características corresponde al valor de codificación de diferentes dimensiones.

Por supuesto, como aprendizaje, también puede ver el segundo método de codificación de posición absoluta: codificación de posición aprendible:

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

Se puede encontrar que toda la clase en realidad inicializa los parámetros de codificación de posición de la forma correspondiente, y luego aprende estos parámetros de codificación de posición por sí mismo de una manera fácil de aprender. El código es relativamente simple.

Transformador

todo el marco

Primero mira la interfaz de llamada:

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

Llame a la clase Transformer directamente:

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)

Un análisis cuidadoso de esta clase revelará que, aunque por el momento no comprendemos los detalles del modelo, se ha definido el marco principal del modelo. El Transformador completo es en realidad la entrada del mapa de funciones src (reducción de la dimensión a 256), src_key_padding_mask (registrar si cada posición del mapa de funciones está rellena y el panel no necesita calcular la atención) y el código de posición pos en el TransformerEncoder El TransformerEncoder en realidad está compuesto por TransformerEncoderLayer, luego ingrese la salida del codificador, la máscara, el código de posición y el código de consulta en TransformerEncoder, y TransformerEncoder está compuesto por TransformerDecoderLayer.

Por lo tanto, lo siguiente se divide en dos módulos, TransformerEncoder y TransformerDecoder, para comprender los detalles específicos de Transformer.

TransformadorCodificador

Esta parte es para llamar a la función _get_clones, copiar 6 clases de TransformerEncoderLayer y luego ingresar las 6 clases de TransformerEncoderLayer a su vez para la propagación hacia adelante, calcular continuamente la autoatención del mapa de características y mejorar continuamente el mapa de características, y finalmente obtener el más fuerte (información Most) mapa de características de salida: [h*w, bs, 256]. Vale la pena señalar que la forma de todo el mapa de características del proceso de TransformerEncoder es 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)])

TransformadorCodificadorCapa

Diagrama de estructura del codificador:
inserte la descripción de la imagen aquí

Capa de codificador = atención de múltiples cabezales + agregar y norma + alimentación hacia adelante + agregar y norma, el foco está en la atención de múltiples cabezales.

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)  # 默认执行

Hay varios puntos clave (diferentes del codificador de transformador original):

  1. ¿Por qué q y k de cada codificador están codificados en la posición +? Si ha aprendido transformadores, generalmente agrega codificación de posición a la entrada del transformador, y el qkv de cada codificador es igual, sin agregar codificación de posición. Aquí, tanto q como k se agregan primero con codificación de posición, y luego q y k se usan para calcular la similitud y, finalmente, se ponderan con v para obtener una representación de características más globalmente relevante (mejorada). Cada capa se agrega con codificación de posición, y la información global de cada capa se fortalece continuamente, y finalmente se pueden obtener las características globales más fuertes;
  2. ¿Por qué q y k + codificación de posición, pero v no necesita agregar codificación de posición? Debido a que q y k se usan para calcular la similitud/correlación entre cada posición en la característica de la imagen, y la característica global calculada después de la codificación de la posición está más correlacionada, y v representa la imagen original, por lo que no es necesario agregar la codificación de la posición;

TransformadorDecodificador

La estructura del decodificador es similar a la del codificador. También usa _get_clones para copiar 6 copias de la clase TransformerDecoderLayer y luego reenvía la entrada de las 6 clases de TransformerDecoderLayer, pero de manera diferente, el decodificador necesita ingresar la salida de los 6 TransformerDecoderLayer y las siguientes 6 capas. La salida participará en el cálculo de pérdida en 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)   # 不执行

TransformadorDecodificadorCapa

Diagrama de estructura de la capa del decodificador:
inserte la descripción de la imagen aquí

capa del decodificador = Atención multicabezal enmascarada + Add&Norm + Atención multicabezal + add&Norm + avance + add&Norm. El punto clave radica en las dos capas de Atención. Comprender los principios y las diferencias entre estas dos capas es la clave para comprender Decoder.

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 lo que está haciendo el decodificador:

  1. A partir de la salida final del codificador, obtenemos la versión mejorada de la memoria de características de la imagen y la información de posición pos de la característica;
  2. La información del objeto tgt en la imagen se personaliza, se inicializa a todo 0, y la información de posición del objeto query_pos en la imagen se inicializa aleatoriamente;
  3. La primera autoatención: qk=tgt+query_pos, v=tgt, calcula la correlación entre los objetos en la imagen y es responsable de modelar la información del objeto en la imagen. El tgt1 final es la versión mejorada de la información del objeto. Estas posiciones La información contiene la relación posicional entre objetos;
  4. La segunda autoatención: q=tgt+qyery_pos, k=memory+pos, v=memory, use la información del objeto tgt como consulta, vaya a la memoria de características de la imagen para preguntar (calcule su correlación), pregunte el objeto en la imagen ¿Dónde está? Una vez finalizada la pregunta, la información de posición del objeto se integra en la característica de imagen (v). Todo el proceso es responsable de modelar la relación entre la característica de imagen y la característica del objeto. El resultado final es una característica de imagen más fuerte tgt2, incluyendo la salida del codificador Funciones de imagen mejoradas + funciones de ubicación de objetos.
  5. Finalmente, tgt1 + tgt2 = la característica de imagen mejorada emitida por el Codificador + información de objeto + información de posición de objeto se usa como salida del decodificador;

Pregunta 1
Algunas personas pueden preguntarse por qué la información del objeto tgt definida aquí se inicializa en ceros, y la información de posición del objeto query_pos se inicializa aleatoriamente, pero puede expresar un significado tan complicado. Obviamente, se inicializa a todos 0 o se inicializa aleatoriamente, ¿cómo sabe el modelo lo que representan? En realidad, esto está relacionado con la función de pérdida.Después de definir la función de pérdida, la red aprende continuamente mediante el cálculo de la pérdida, el retorno de gradiente y, finalmente, los significados expresados ​​aquí son tgt y query_pos aprendidos. Esto es lo mismo que la pérdida de regresión Después de definir estos cuatro canales para representar xywh, ¿cómo sabe la red? Es a través del retorno de gradiente de la función de pérdida, la red continúa aprendiendo y finalmente sabe que estos cuatro canales representan xywh.

Pregunta 2
¿Por qué usamos tgt1 + tgt2 como salida del decodificador aquí? ¿En lugar de usar tgt1 o tgt2 solo?

  1. En primer lugar, tgt1 representa la información del objeto en la imagen + la información de ubicación del objeto, pero no tiene demasiadas características de imagen, lo que no es aceptable, y el efecto de predicción final definitivamente no es bueno (predecir la categoría del objeto es definitivamente no muy precisa);
  2. En segundo lugar, las características de la imagen de la versión mejorada del codificador representada por tgt2 + la información de posición del objeto, carece de la información del objeto, lo cual no es aceptable, y el efecto de predicción final definitivamente no es bueno (predecir la posición de el objeto definitivamente no es muy preciso); por lo tanto, los dos son
    comparables. Las características adicionales se utilizan como salida del decodificador para predecir la categoría y la posición del objeto, y el efecto es el mejor.

Función de pérdida + posprocesamiento

Cálculo de pérdida: SetCriterion

Primero, la función de pérdida se definirá en detr.py:

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

Luego llame a la función de criterio después del razonamiento directo en train_one_epoch de engine.py para calcular la pérdida:

# 前向传播
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)

Bueno, centrémonos en la clase 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

Toda la función está haciendo principalmente dos cosas:

  1. Llame a la función self.matcher para hacer coincidir N (número gt) marcos de predicción reales de 100 marcos de predicción y hacer coincidir el marco de predicción correspondiente a cada marco gt;
  2. Llame a self.get_loss para calcular cada pérdida

Algoritmo húngaro, coincidencia de gráficos bipartitos: self.matcher

Para conocer el principio del algoritmo húngaro, puede echar un vistazo a este blog clásico: Notas de estudio de algoritmos (5): Algoritmo húngaro
En DETR, la clase DanishMatcher en models/matcher.py implementa el algoritmo de coincidencia 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]

De hecho, es primero calcular la pérdida total de cada cuadro de predicción (100) y cada cuadro gt para formar una matriz de pérdida C, y luego llamar al algoritmo húngaro escrito por scipy.optimize.linear_sum_assignment.El principio de coincidencia es el mínimo "suma de pérdida" (aquí La pérdida no es la pérdida real, aquí es solo un método de medición, que es diferente del método de cálculo de pérdida), y se obtiene el único marco de predicción responsable correspondiente a cada gt, y otros marcos de predicción lo harán clasificarse automáticamente como fondo.

linear_sum_assignment, ingrese una matriz métrica (matriz de costo) de un gráfico bipartito, calcule el método de asignación de peso mínimo de la matriz métrica de este gráfico bipartito y devuelva el índice de fila de matriz (idx de cuadro de predicción) y el índice de columna (idx de cuadro gt) correspondiente al esquema de emparejamiento.

Calcular pérdida: self.get_loss

self.get_loss es una función definida por la clase 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)

Al mismo tiempo, se denominan pérdida de clasificación (self.loss_labels), pérdida de regresión (self.boxes) y pérdida de cardinalidad. Sin embargo, la pérdida de cardinalidad solo se usa para el registro y no participa en las actualizaciones de gradiente, por lo que no se describirá aquí. Además, si se trata de una tarea de segmentación, también existe un cálculo de pérdida de segmentación de máscara, que no se describirá aquí por el momento.

Pérdida de clasificación: self.loss_labels

Pérdida de clasificación 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

Aviso:

  1. pérdida de clasificación = pérdida de entropía cruzada;
  2. Muestras positivas + muestras negativas = 100, número de muestras positivas = número de GT, número de muestras negativas = 100 - número de GT;
  3. 92 categorías, idx=91 significa categoría de fondo;
  4. Tenga en cuenta que aquí hay una función _get_src_permutation_id, que se trata principalmente de aplanar el marco de predicción. Originalmente, tenía la dimensión de lote, pero ahora se aplana a una dimensión, lo que es conveniente para el cálculo posterior de pérdidas;
  5. Un class_error: la precisión Top-1 también se calcula aquí para la visualización del registro;

pérdida de regresión: self.boxes

pérdida de regresión self.boxes:

    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

Aviso:

  1. Pérdida de regresión: solo calcule la pérdida de regresión de todas las muestras positivas;
  2. Pérdida de regresión = Pérdida L1 + Pérdida GIOU

Posprocesamiento de bbox: PostProcess

Esta parte es el enlace de prueba.Después de la propagación directa, se calcula la pérdida para la visualización del registro y se calcula el índice de coco.
También defina primero la función de posprocesamiento en detr.py:

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

Luego llame a la función PostProcess después del razonamiento directo en la evaluación de engine.py para postprocesar los 100 fotogramas 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)

Clase de postproceso:

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

Puede verse que el posprocesamiento consiste en realidad en contar los resultados de la predicción, eliminar la clase de fondo y obtener las puntuaciones de probabilidad de las categorías, etiquetas y coordenadas de posición absolutas de los 100 cuadros de predicción predichos para cada imagen.

Luego, finalmente envíe este resultado a coco_evaluator para calcular los indicadores relacionados con coco.

Al predecir, de hecho, nuestros objetos predichos finales generalmente no tienen objetos 100. ¿Cómo lo manejamos en este momento? Generalmente, se establece un umbral (0,7) de la puntuación de probabilidad de predicción, y los marcos de predicción más grandes que esta predicción se conservarán y mostrarán al final, y los marcos de predicción más pequeños que la predicción se descartarán.

Enfoque de aprendizaje del código fuente

  1. backbone:Codificación posicional(PositionEmbeddingSine);
  2. Transformador:TransformerEncoderLayer + TransformerDecoderLayer;
  3. Función de pérdida: algoritmo húngaro, coincidencia de gráficos bipartitos (self.matcher)
  4. Post-procesamiento: PostProcess

algunos problemas

  1. ¿Por qué ViT solo tiene codificador y DETR usa codificador + decodificador?
    Codificador: la autoatención del codificador realiza principalmente modelado global y aprende características globales. A través de este paso, cada objeto en la imagen se puede separar básicamente tanto como sea posible; Decodificador: en este momento, use la autoatención del decodificador y luego realice la detección de objetivos
    y tareas de segmentación, el modelo puede dividir aún más el área del punto extremo del límite del objeto en una división más precisa, lo que hace que el reconocimiento de bordes sea más preciso;
  2. ¿Cuál es el uso de la consulta de objetos?
    La consulta de objeto se usa para reemplazar el ancla . Al introducir una consulta de objeto aprendible, el modelo puede aprender automáticamente qué áreas de la imagen pueden tener objetos. Finalmente, 100 de esas áreas con posibles objetos se pueden encontrar a través de la consulta de objeto. Luego encuentre el marco de predicción efectivo entre los 100 marcos de predicción a través de la comparación de gráficos bipartitos y luego calcule la pérdida.
    Por lo tanto, la consulta de objetos desempeña el papel de reemplazar el ancla y encuentra el área donde puede haber objetos de una manera aprendible, sin causar una gran cantidad de marcos redundantes debido al uso del ancla.

cita

  1. Explicación del código fuente de la estación b: trabajadores de la línea de montaje blindados
  2. Zhihu [Brother Buffalo]: Interpretación del código fuente DETR
  3. CSDN [ardilla trabajando duro] explicación del código fuente: notas del código fuente DETR (1)
  4. CSDN [ardilla trabajando duro] explicación del código fuente: notas del código fuente DETR (2)
  5. CSDN: codificación de posición en Transformer (codificación de posición uno)
  6. Sabiendo que CV no será eliminado- [análisis de código fuente, detección de objetivos, estrella transfronteriza DETR (1), descripción general e inferencia del modelo]
  7. Sabiendo que CV no será eliminado- [análisis de código fuente, detección de objetivos, estrella transfronteriza DETR (2), proceso de entrenamiento de modelos y procesamiento de datos]
  8. Sabiendo que CV no será eliminado- [Análisis de código fuente, detección de objetivos, estrella transfronteriza DETR (3), Codificación de posición y columna vertebral]
  9. Sabiendo que CV no será eliminado- [Análisis de código fuente, detección de objetivos, estrella transfronteriza DETR (4), Detección con transformador]
  10. Sabiendo que CV no será borrado- [análisis de código fuente, detección de objetivos, estrella cruzada DETR (5), función de pérdida y algoritmo de coincidencia húngaro]
  11. Sabiendo que CV no será borrado- [análisis de código fuente, detección de objetivos, estrella transfronteriza DETR (6), salida del modelo y generación de predicciones]
  12. Lectura intensiva de artículos DETR y análisis de la estructura del modelo.
  13. MMDet: interpretación del código fuente DETR

Supongo que te gusta

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