社区计算机视觉课程文档

DEtection TRansformer (DETR)

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

DEtection TRansformer (DETR)

架构概述

DETR主要用于目标检测任务,即在图像中检测物体的过程。例如,模型的输入可以是一张道路图片,模型的输出可以是[('car',X1,Y1,W1,H1),('pedestrian',X2,Y2,W2,H2)],其中X、Y、W、H分别表示边界框位置的x、y坐标以及边界框的宽度和高度。传统的物体检测模型如YOLO包含手工设计的特征,例如锚框先验,这需要对物体位置和形状进行初始猜测,影响下游训练。然后使用后处理步骤来去除重叠的边界框,这需要仔细选择其过滤启发式方法。DEtection TRansformer,简称DETR,通过在特征提取骨干之后使用编码器-解码器Transformer来简化检测器,以并行方式直接预测边界框,需要最少的后处理。

DETR架构图 DETR的模型架构始于一个CNN骨干,类似于其他基于图像的网络,其输出经过处理并输入到Transformer编码器中,生成N个嵌入。编码器嵌入与学习到的位置嵌入(称为对象查询)相加,并用于Transformer解码器中,生成另外N个嵌入。最后一步,每个N个嵌入都通过单独的前馈层来预测边界框的宽度、高度、坐标以及对象类别(或是否存在对象)。

主要特点

编码器-解码器

与其他Transformer一样,Transformer编码器需要CNN骨干的输出是一个序列。因此,大小为[dimension, height, width]的特征图被缩小并展平为[dimension, less than height x width]编码器的特征图 左图:可视化了特征图中256个维度中的12个。每个维度提取原始猫图像的一些特征,同时缩小了原始图像。一些维度更关注猫的图案;一些维度更关注床单。 右图:保持原始特征维度256,宽度和高度进一步缩小并展平为850。
由于Transformer是置换不变的,因此在编码器和解码器中都添加了位置嵌入,以提醒模型嵌入在图像中的位置。在编码器中,使用固定位置编码,而在解码器中,使用学习到的位置编码(对象查询)。固定编码类似于原始Transformer论文中使用的编码,其中编码由在不同特征维度上具有不同频率的正弦函数定义。它在没有任何学习参数的情况下提供了位置感,通过图像上的位置进行索引。学习到的编码也通过位置进行索引,但每个位置都有一个单独的编码,该编码在整个训练过程中学习,以模型理解的方式表示位置。

基于集合的全局损失函数

在流行的目标检测模型YOLO中,损失函数包括边界框、对象存在性(即对象存在于感兴趣区域的概率)和类别损失。损失是在每个网格单元的多个边界框上计算的,这是一个固定数量。另一方面,在DETR中,该架构预计以置换不变的方式生成唯一的边界框(即,输出中检测的顺序无关紧要,并且边界框必须不同,不能全部相同)。因此,需要匹配来评估预测的质量。

二分匹配
二分匹配是一种计算真实边界框与预测框之间一对一匹配的方法。它寻找真实与预测边界框以及类别之间相似度最高的匹配。这确保了最接近的预测将与相应的真实值匹配,以便在损失函数中正确调整边界框和类别。如果不进行匹配,与真实值顺序不一致的预测即使是正确的也会被标记为不正确。

使用DETR检测对象

要查看如何使用Hugging Face Transformer执行DETR推理的示例,请参阅DETR.ipynb

DETR的演变

可变形DETR

DETR的两个主要问题是收敛过程缓慢和小型物体检测次优。可变形注意力
第一个问题通过使用可变形注意力解决,它减少了需要关注的采样点数量。传统注意力由于全局注意力而效率低下,并严重限制了图像可以具有的分辨率。该模型只关注每个参考点周围固定数量的采样点,并且参考点由模型根据输入学习。例如,在一张狗的图像中,参考点可能在狗的中心,采样点靠近耳朵、嘴巴、尾巴等。

多尺度可变形注意力模块
第二个问题与YOLOv3的解决方法类似,即引入了多尺度特征图。在卷积神经网络中,早期层提取较小的细节(例如线条),而后期层提取较大的细节(例如轮子、耳朵)。以类似的方式,可变形注意力的不同层导致不同级别的分辨率。通过将编码器中一些这些层的输出连接到解码器,它可以使模型检测多种大小的对象。

条件DETR

条件DETR也旨在解决原始DETR中训练收敛缓慢的问题,导致收敛速度提高了6.7倍以上。作者发现对象查询是通用的,并且不特定于输入图像。在解码器中使用条件交叉注意力,查询可以更好地定位边界框回归区域。条件DETR的解码器层 左图:DETR解码器层。右图:条件DETR解码器层
上图中比较了原始DETR和条件DETR解码器层,主要区别在于交叉注意力块的查询输入。作者区分了内容查询cq(解码器自注意力输出)和空间查询pq。原始DETR只是简单地将它们相加。在条件DETR中,它们被连接起来,cq关注对象的内容,pq关注边界框区域。
空间查询pq是解码器嵌入和对象查询都投影到相同空间(分别变为T和ps)并相乘的结果。前一层解码器嵌入包含边界框区域的信息,对象查询包含每个边界框的学习参考点信息。因此,它们的投影结合成一个表示,允许交叉注意力测量它们与编码器输入和正弦位置嵌入的相似性。这比只使用对象查询和固定参考点的DETR更有效。

DETR推理

您可以使用Hugging Face Hub上现有的DETR模型进行推理,如下所示

from transformers import DetrImageProcessor, DetrForObjectDetection
import torch
from PIL import Image
import requests

url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# initialize the model
processor = DetrImageProcessor.from_pretrained(
    "facebook/detr-resnet-101", revision="no_timm"
)
model = DetrForObjectDetection.from_pretrained(
    "facebook/detr-resnet-101", revision="no_timm"
)

# preprocess the inputs and infer
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)

# convert outputs (bounding boxes and class logits) to COCO API
# non max supression above 0.9
target_sizes = torch.tensor([image.size[::-1]])
results = processor.post_process_object_detection(
    outputs, target_sizes=target_sizes, threshold=0.9
)[0]

for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
    box = [round(i, 2) for i in box.tolist()]
    print(
        f"Detected {model.config.id2label[label.item()]} with confidence "
        f"{round(score.item(), 3)} at location {box}"
    )

输出如下。

Detected cat with confidence 0.998 at location [344.06, 24.85, 640.34, 373.74]
Detected remote with confidence 0.997 at location [328.13, 75.93, 372.81, 187.66]
Detected remote with confidence 0.997 at location [39.34, 70.13, 175.56, 118.78]
Detected cat with confidence 0.998 at location [15.36, 51.75, 316.89, 471.16]
Detected couch with confidence 0.995 at location [-0.19, 0.71, 639.73, 474.17]

DETR的PyTorch实现

原始论文中DETR的实现如下所示

import torch
from torch import nn
from torchvision.models import resnet50


class DETR(nn.Module):
    def __init__(
        self, num_classes, hidden_dim, nheads, num_encoder_layers, num_decoder_layers
    ):
        super().__init__()
        self.backbone = nn.Sequential(*list(resnet50(pretrained=True).children())[:-2])
        self.conv = nn.Conv2d(2048, hidden_dim, 1)
        self.transformer = nn.Transformer(
            hidden_dim, nheads, num_encoder_layers, num_decoder_layers
        )
        self.linear_class = nn.Linear(hidden_dim, num_classes + 1)
        self.linear_bbox = nn.Linear(hidden_dim, 4)
        self.query_pos = nn.Parameter(torch.rand(100, hidden_dim))
        self.row_embed = nn.Parameter(torch.rand(50, hidden_dim // 2))
        self.col_embed = nn.Parameter(torch.rand(50, hidden_dim // 2))

    def forward(self, inputs):
        x = self.backbone(inputs)
        h = self.conv(x)
        H, W = h.shape[-2:]
        pos = (
            torch.cat(
                [
                    self.col_embed[:W].unsqueeze(0).repeat(H, 1, 1),
                    self.row_embed[:H].unsqueeze(1).repeat(1, W, 1),
                ],
                dim=-1,
            )
            .flatten(0, 1)
            .unsqueeze(1)
        )
        h = self.transformer(
            pos + h.flatten(2).permute(2, 0, 1), self.query_pos.unsqueeze(1)
        )
        return self.linear_class(h), self.linear_bbox(h).sigmoid()

逐行阅读前向函数:

骨干
输入图像首先通过ResNet骨干网络,然后通过卷积层,将维度降低到hidden_dim

x = self.backbone(inputs)
h = self.conv(x)

它们在__init__函数中声明。

self.backbone = nn.Sequential(*list(resnet50(pretrained=True).children())[:-2])
self.conv = nn.Conv2d(2048, hidden_dim, 1)

位置嵌入

虽然在论文中,固定嵌入和训练嵌入分别用于编码器和解码器,但作者为了简化,在实现中两者都使用了训练嵌入。

pos = (
    torch.cat(
        [
            self.col_embed[:W].unsqueeze(0).repeat(H, 1, 1),
            self.row_embed[:H].unsqueeze(1).repeat(1, W, 1),
        ],
        dim=-1,
    )
    .flatten(0, 1)
    .unsqueeze(1)
)

它们在这里声明为nn.Parameter。行和列嵌入结合起来表示图像中的位置。

self.row_embed = nn.Parameter(torch.rand(50, hidden_dim // 2))
self.col_embed = nn.Parameter(torch.rand(50, hidden_dim // 2))

调整大小
在进入Transformer之前,大小为(batch size, hidden_dim, H, W)的特征被重塑为(hidden_dim, batch size, H*W)。这使它们成为Transformer的序列输入。

h.flatten(2).permute(2, 0, 1)

Transformer
nn.Transformer函数将第一个参数作为编码器的输入,第二个参数作为编码器的输入。正如您所看到的,编码器接收到调整大小的特征与位置嵌入相加,而解码器接收到query_pos,即解码器位置嵌入。

h = self.transformer(pos + h.flatten(2).permute(2, 0, 1), self.query_pos.unsqueeze(1))

前馈网络
最后,输出(一个大小为(query_pos_dim, batch size, hidden_dim)的张量)通过两个线性层。

return self.linear_class(h), self.linear_bbox(h).sigmoid()

其中第一个预测类别。为“无对象”类别添加了一个额外的类别。

self.linear_class = nn.Linear(hidden_dim, num_classes + 1)

第二个线性层预测边界框,输出大小为4,用于xy坐标、高度和宽度。

self.linear_bbox = nn.Linear(hidden_dim, 4)

参考文献

< > 在 GitHub 上更新