在 2020 年 5 月,Facebook AI 团队(现 Meta AI)发布了一个名为 DETR(Detection Transformer)的模型,这彻底改变了我们对计算机视觉任务的传统认知。你可能会问,卷积神经网络(CNN)已经如此强大,为什么还需要 Transformer?在这篇文章中,我们将深入探讨 DETR 的核心架构,探讨它是如何将自然语言处理中的 Transformer 思想引入目标检测的,以及为什么它被称为目标检测领域的“一股清流”。
我们会发现,DETR 最大的魅力在于它的简洁性——它不再需要繁琐的锚框、非极大值抑制(NMS)等复杂的手工设计组件。让我们开始这段技术探索之旅吧。
为什么我们需要 DETR?传统方法的痛点
在 DETR 出现之前,目标检测领域主要由两派算法统治:
- 两阶段检测器(如 Faster R-CNN):首先生成一系列候选区域,然后对这些区域进行分类和回归。这种方法精度高,但速度慢,且涉及到非常复杂的后处理步骤。
- 单阶段检测器(如 YOLO、SSD):直接在图像上进行密集采样并预测。虽然速度快,但需要精心设计锚框的大小和比例,调参非常繁琐。
这些传统方法都有一个共同的“痛点”:它们都严重依赖锚框和非极大值抑制(NMS)。锚框的设计需要先验知识,而 NMS 是为了去除重复检测框的后处理步骤,这不仅增加了推理时间,还引入了许多需要调整的超参数。
DETR 的出现,正是为了解决这些问题。它利用 Transformer 的全局注意力机制,将目标检测视为一个直接的集合预测问题。
DETR 的核心设计理念
让我们从宏观角度看看 DETR 是如何工作的。它的核心思想可以概括为:
通过 CNN Backbone 提取图像特征,利用 Transformer Encoder-Decoder 架构建立图像特征与目标查询之间的依赖关系,最后通过二分图匹配来直接输出最终的预测框集合。
这听起来很抽象?别担心,我们一步步拆解。
DETR 的架构流程详解
整个过程主要分为四个步骤,让我们逐一剖析。
#### 步骤 1:特征提取
虽然 DETR 引入了 Transformer,但它并没有完全抛弃 CNN。对于图像数据,CNN 在处理局部特征方面依然是王者。
我们将输入图像送入一个标准的 CNN Backbone(通常是 ResNet-50 或 ResNet-101)。CNN 输出的是一个低分辨率的特征图。例如,如果输入是 $800 imes 1333$ 的图像,经过 Backbone 后可能变成 $2000 imes 33 imes 66$ 的特征张量(C=2048通道)。
#### 步骤 2:位置编码与序列化
Transformer 本身并不具备空间感,它不知道像素点之间的相对位置。因此,DETR 引入了位置编码。
因为 Transformer 处理的是序列数据,我们需要将 CNN 输出的 2D 特征图展平为 1D 序列。同时,我们将固定的位置编码(可以是正弦波编码,也可以是可学习的编码)加到特征上。这样,模型就能“感知”到每个特征在原图中的空间位置。
#### 步骤 3:Transformer 编码器
带有位置编码的特征序列被送入 Transformer 编码器。在这里,编码器利用自注意力机制来理解图像的全局上下文信息。
不同于 CNN 只感受局部感受野,Transformer 编码器让图像中的每一个点都能直接关注到图像中的其他所有点。这对于处理大物体或者物体之间的遮挡关系非常有帮助。
#### 步骤 4:Transformer 解码器与目标查询
这是 DETR 最神奇的部分。解码器接收两部分输入:
- 来自编码器的图像特征。
- 可学习的目标查询。
你可以把“目标查询”想象成 N 个占位符(例如 N=100)。在训练开始前,这些查询只是随机初始化的向量。但是,通过训练,模型会学会让第 1 个查询去寻找“图像中最显著的物体”,第 2 个查询去寻找“第二显著的物体”,以此类推。
解码器通过交叉注意力层,让这些查询去“询问”图像特征:“这里有物体吗?如果有,它的位置在哪里?”
步骤 5:预测前馈网络(FFN)
解码器输出的 N 个向量,每一个都会被送入一个独立的前馈网络(FFN)。这个 FFN 负责输出两个结果:
- 类别标签:预测这个框里是什么物体(或者是“无物体”/背景)。
- 边界框坐标:预测框的中心点坐标以及宽和高。
关键突破:二分图匹配损失
你可能会问:既然有 100 个预测输出,而真实图片里可能只有 3 个物体,我们怎么计算损失?怎么知道哪个预测框对应哪个真实物体?
这正是 DETR 最大的创新点之一:匈牙利算法。
DETR 并没有像传统方法那样让模型预测“这个锚框属于哪个物体”,而是直接寻找预测集合与真实集合之间的最优一对一匹配。
具体来说,我们会寻找一种匹配方案,使得所有匹配对的损失之和最小。这里的损失函数包含两部分:
- 类别损失:预测类别与真实类别的差异(通常使用交叉熵)。
- 边界框损失:预测框与真实框的坐标差异(通常使用 L1 损失 + GIoU 损失)。
一旦找到了最优匹配(比如预测框 5 对应真实物体 A,预测框 12 对应真实物体 B),我们就只计算这些配对的损失。剩下的没有匹配到的预测框,会被强行归类为“无物体”类(即“Null”类)。这直接替代了 NMS 的作用,因为模型被训练成每个物体只输出一个唯一的框。
代码实战:理解 DETR 的核心组件
为了更好地理解,让我们看一段简化版的代码逻辑。这里我们不直接贴出 DETR 的数千行代码,而是通过 Python 模拟其核心流程,特别是预测头部的逻辑。
#### 示例 1:模拟 MLP 预测头部
在 DETR 中,Transformer 解码器输出后,接的是一个简单的全连接层(MLP)。我们可以这样理解它:
import torch
import torch.nn as nn
class DETRPredictionHead(nn.Module):
"""
DETR 的预测头部。
接收 Transformer 解码器的输出,并将其映射为类别和边界框。
"""
def __init__(self, hidden_dim, num_classes, num_queries):
super().__init__()
# 类别预测分支:输出 (batch_size, num_queries, num_classes + 1)
# +1 是因为我们要保留一个“no object”的背景类别
self.class_embed = nn.Linear(hidden_dim, num_classes + 1)
# 边界框预测分支:输出 (batch_size, num_queries, 4)
# 4 代表:中心坐标 以及宽高
self.bbox_embed = nn.Linear(hidden_dim, 4)
def forward(self, features):
"""
features: 来自解码器的输出,形状通常为 (N, batch_size, hidden_dim)
这里的 N 是对象查询的数量,例如 100
"""
# 将输出维度从 转置为 (batch_size, num_queries, hidden_dim)
features = features.permute(1, 0, 2)
# 预测类别分数
class_logits = self.class_embed(features)
# 预测边界框坐标
# 注意:这里通常还会接一个 Sigmoid 或 ReLU 来限制坐标范围
# 在原论文中,bbox_embed 通常预测相对于图像宽高的归一化坐标
bbox_coords = self.bbox_embed(features).sigmoid()
return class_logits, bbox_coords
# 模拟使用场景
batch_size = 2
num_queries = 100 # DETR 默认通常设为 100
hidden_dim = 256
num_classes = 91 # COCO 数据集类别数
model = DETRPredictionHead(hidden_dim, num_classes, num_queries)
# 假设这是 Transformer 解码器传来的特征
simulated_decoder_output = torch.rand(num_queries, batch_size, hidden_dim)
pred_logits, pred_boxes = model(simulated_decoder_output)
print(f"预测类别形状: {pred_logits.shape}") # 应该是 [2, 100, 92]
print(f"预测框形状: {pred_boxes.shape}") # 应该是 [2, 100, 4]
#### 示例 2:理解位置编码
DETR 使用固定的位置编码。让我们看看如何生成一个简单的 2D 正弦位置编码,这也是 Transformer 最初论文中介绍的方法,DETR 对其进行了适配。
import math
import torch
def gen_pos_enc(dim, max_len=5000):
"""
生成标准 Transformer 的位置编码矩阵。
这里我们模拟一维的位置编码,DETR 实际上将其扩展到了二维。
"""
pe = torch.zeros(max_len, dim)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, dim, 2).float() * (-math.log(10000.0) / dim))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
return pe
# DETR 中更常见的实现是配合特征图的 展平操作
# 假设特征图大小是 H=20, W=30, C=256
H, W, C = 20, 30, 256
# 生成网格坐标
coords_h = torch.arange(H).float() / H
coords_w = torch.arange(W).float() / W
# 生成二维网格
grid_y, grid_x = torch.meshgrid(coords_h, coords_w)
# 展平并拼接
# 这里的思路类似于构建 mask,然后将其编码
# 实际代码中,DETR 使用 row_embed 和 col_embed 分别处理 y 和 x 坐标
print("位置编码是让 Transformer 理解空间关系的关键。如果没有它,")
print("Transformer 会把图像视为一袋乱序的像素点。")
模型评估与性能分析
在 COCO 数据集上的评估显示,DETR 表现出了非常有趣的特点:
- 大目标表现优异:得益于 Transformer 强大的全局上下文建模能力,DETR 在检测大物体时表现非常出色,甚至超过了 Faster R-CNN。
- 小目标挑战:在最初的版本中,DETR 在检测小物体时有些吃力。这主要是因为 Backbone 输出的特征图分辨率较低,丢失了细节信息。后来的改进版本(如 Deformable DETR)通过引入可变形注意力机制解决了这个问题。
实际应用与最佳实践
如果你想在项目中尝试 DETR,这里有一些实战建议:
- 训练时间问题:DETR 的收敛速度比传统的检测器要慢。通常需要更长的训练时间(例如在 COCO 上需要 300-500 个 epoch)才能达到最佳性能。这是因为模型需要时间来学习“如何”去关注不同的物体位置。
- 内存消耗:Transformer 的自注意力机制计算复杂度是 $O(N^2)$。如果你的检测任务需要非常高分辨率的特征图,显存占用会非常大。在这种情况下,可以考虑使用 Deformable DETR,它将注意力限制在采样点周围,极大地提高了效率。
- 推理速度:一旦训练完成,DETR 的推理速度非常有竞争力,因为它省去了 NMS 后处理步骤,这在实时应用中是一个巨大的优势。
常见问题与解决方案
Q:为什么我的 DETR 损失不下降?
A:检查一下辅助损失。在 DETR 的论文中,除了主要损失外,中间层的解码器输出也会被计算辅助损失,这有助于加速收敛。如果没有这部分,训练会非常困难。
Q:如何设置 Object Queries 的数量?
A:这通常是一个超参数。对于 COCO 数据集,100 个是标准值。如果你的特定数据集图片中物体非常多,可以适当增加这个数值,但会带来计算开销。
总结
DETR 是目标检测领域的一次大胆尝试。它告诉我们,我们不一定需要那么多手工设计的归纳偏置(如锚框、NMS)。通过端到端的集合预测和强大的 Transformer 架构,我们可以得到一个更简洁、更优雅的解决方案。
虽然原版 DETR 还存在训练周期长、小目标检测弱的问题,但它开启了一个全新的研究方向。现在,当我们谈论目标检测的新架构时,Transformer 已经成为了不可或缺的话题。如果你正在寻找一个既能处理复杂场景,又能简化后处理流程的模型,DETR 及其衍生系列绝对值得你深入学习。
希望这篇文章能帮助你理解 DETR 的精髓。接下来,你可以尝试下载官方代码,在自己的数据集上跑一跑,看看这个“AI 大脑”是如何像人类一样注视世界并找出物体的。