DETR: End-to-End Object Detection with Transformers [暴力美学]

DETR: End-to-End Object Detection with Transformers 网络解析
说明:

  1. 个人理解,如有错误请及时提出。
  2. 由于自己电脑驱动较低不满足440及以上,所以目前网络中张量的具体维度不太清楚,后续如有条件再更新博客。
  3. 不得不感叹论文作者的数学功底扎实、知识涉猎广博。
  4. 太暴力了,依靠一些精巧的结构和强大的硬件支持替代了大家精心设计的anchor、nms等结构。
  5. 对于小目标、多目标场景仍然较差。
  6. 论文涉及的东西很多,本博客逐步添加,欢迎提出修改意见。

资源:

  1. 论文地址
  2. github地址
  3. 李宏毅老师Transformer视频B站版
  4. Yannic Kilcher大神的论文讲解,“你懂得”网站的地址。另外提供国内CSDN下载(待上传),欢迎支持

本博客目录

    • 总体结构
    • Transformer
      • Multi-Head Self-Attention
      • ADD & Norm
      • FFN
      • Spatial positional encoding
      • Object queries
    • Bipartite Matching
    • 损失函数

总体结构

detr网络总体结构图
detr网络总体结构图2

  1. 整个网络思路非常清晰,首先是将图像[3, H, W]送入常用的卷积神经网络backbone来提取特征,以resnet为例,那么在backbone最后这里得到的特征图[2048, H/32, W/32]
  2. 然后将来自backbone的特征图展开,变为[C, HW]的形状,送入由编码器和解码器组成的Transformer结构中
  3. 最后是将预测得到的“集合”与真值进行匹配(匈牙利算法),通过最小化损失(代价)来一次性预测检测(实例分割)结果

Transformer

在这里插入图片描述
如上图所示,左图为DETR中的transformer结构,右图为文章Attention Is All You Need中的结构图,基本上还是一致的。
李宏毅老师讲到transformer实际上是seq2seq model with ‘self-attention’,所以下面着重来讲一下DETR里面涉及到的一些细节问题

Multi-Head Self-Attention

其实NLP中transformer需要处理的是一些序列数据,那么为了处理序列数据首先可以想到的就是RNN结构,这种结构考虑了上下文关系,相对于CNN(感受野问题)来讲具有优势。但是如下图所示的RCNN模块存在一个问题:任务难以并行
RNN
因为an to bn 的计算依赖 an-1 to bn-1的中间结果,这就意味着任务必须是串行的,这对于目前所说的大数据,并行计算,云计算来讲是不合适的。而相对于RNN来讲CNN则更加适合并行计算,那么self-attention模块就是一个典型的合理解。如下图所示:
在这里插入图片描述
那么这里的核心思想就是怎么去构建这个self-attention结构。(x1, x2, x3, x4)表示我们的输入,(b1, b2, b3, b4)表示我们的输出
在这里插入图片描述
这里q, k, v可以按照词袋模型的思路来理解,那么q是输入的矩阵,而k则是数据字典,那么v就是我们将将输入的矩阵q通过对应数据字典进行编码后形成的新的特征矩阵。因此他们的公式可以分别表示为

qi=Wqai
q^i = W^qa^i

qi=Wqai

ki=Wkai
k^i = W^ka^i

ki=Wkai

vi=Wvai
v^i = W^va^i

vi=Wvai
有了这个数学表示,那么接下来我们需要的是拿q1与(k1, k2, k3, k4)进行点乘,然后是q2, q3, q4,这个过程表示如下
在这里插入图片描述
其中

α1,i=q1?ki/d
\alpha_{1,i} = q^1\cdot k^i/ \sqrt{d}

α1,i?=q1?ki/d?

α^1,i=exp(α1,i)/jexp(α1,j)
\hat{\alpha}_{1,i} = exp(\alpha_{1,i})/\sum_j{exp(\alpha_{1,j})}

α^1,i?=exp(α1,i?)/j∑?exp(α1,j?)
d是q和k的维度。到这里我们获得了输入x1与四个数据字典k相关的“权值”,那么为了获得最后的“词袋模型编码后的向量”,就需要alpha-head与v进行一定的操作,如下图所示
在这里插入图片描述

b1=iα^1,ivi
b^1 = \sum_i{\hat{\alpha}_{1,i}v^i}

b1=i∑?α^1,i?vi
同理很容易就可以计算出(b1, b2, b3, b4),具体的矩阵推导可以查看李宏毅老师的视频和PPT,很容易可以理解为什么说这个结构可以替代RNN进行并行加速。而multi-head self-attention就很好理解了,就是多层head的堆叠,这是深度学习中很常见的网络构建方式,下图是一个2 heads的例子,可以与上图进行对比,很明显可以明白差异。
在这里插入图片描述

ADD & Norm

这里Add就是矩阵的加法,Norm指的是Layer Norm,其主要是Batch Norm是在一个batch之间来正则化,而Layer Norm则只是考虑一个图上的正则化。具体可以阅读文献。

FFN

DETR中的FFN实质上就是FC+ReLu+FC这种形式

Spatial positional encoding

引入这个张量的原因是因为输入Transformer的张量被转换成了[c, HW],对于图像来说就失去了像素的空间分布信息,这不符合Transformer处理序列数据的初衷,那么就势必要引入位置编码。
这个张量作者做了两种尝试,不过由于实验效果基本一致,所以就采用了人工生成的方式。
一种是学习得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PositionEmbeddingLearned(nn.Module):
    """
    Absolute pos embedding, learned.
    """
    def __init__(self, num_pos_feats=256):
        super().__init__()
        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:]
        i = torch.arange(w, device=x.device)
        j = torch.arange(h, device=x.device)
        x_emb = self.col_embed(i)
        y_emb = self.row_embed(j)
        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

第二种是人工生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class PositionEmbeddingSine(nn.Module):
    """
    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
        self.temperature = temperature
        self.normalize = normalize
        if scale is not None and normalize is False:
            raise ValueError("normalize should be True if scale is passed")
        if scale is None:
            scale = 2 * math.pi
        self.scale = scale

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors
        mask = tensor_list.mask
        assert mask is not None
        not_mask = ~mask
        y_embed = not_mask.cumsum(1, dtype=torch.float32)
        x_embed = not_mask.cumsum(2, dtype=torch.float32)
        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)
        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
        pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
        return pos

人工生成位置编码的方式还是延续了NLP中Transformer位置编码的生成方式,不同的是因为图像是三维[C, H, W],因此除了通道维之外DETR在H和W方向上分别进行了编码。核心公式表示为

PEpos,2i=sin(pos/100002i/dmodel)
PE_{pos,2i} = sin(pos/10000^{2i/d_{model}})

PEpos,2i?=sin(pos/100002i/dmodel?)

PEpos,2i+1=cos(pos/100002i/dmodel)
PE_{pos,2i+1} = cos(pos/10000^{2i/d_{model}})

PEpos,2i+1?=cos(pos/100002i/dmodel?)
至于说为什么这样子编码可以表示位置不同,可以参考这个博客

Object queries

这个张量是在解码过程中引入的,它的维度和输出的目标集合数量是一致的,可以大致理解为“向量表示的图像上的关注点”。DETR中是通过学习得到的,初始化代码如下所示

1
self.query_embed = nn.Embedding(num_queries, hidden_dim)

调用的时候

1
hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]

Bipartite Matching

不同于常见的检测器,DETR没有使用NMS,通常来讲预测出来的目标集合为N,每个元素是(类别, 坐标)即(c, b)。而真值NGT的数目通常来讲每张图像上数目是不同的,这里就引入了几个问题:

  1. N的个数是一个超参,比如DETR设置的是100
  2. 论文中讲到NGT数目不等于N时就用(null,b)来填充
  3. N和NGT匹配过程使用的是匈牙利算法,代码中为了加快速度进行计算时NGT其实没有进行填充,N中元素没有成功与NGT配对的就被视作背景
  4. 不得不说构思非常巧妙,转化为了求取最小代价的过程,但是也不得不说这个是大力出奇迹,DETR训练的缓慢本人猜测与这个相关,等有环境可以运行代码的时候测试下

损失函数

看到网上很多分析DETR损失函数的,所以这里我就不介绍了,如果有需要再说吧。不过值得一提的是,DETR也用到了这个思想:从不同深度提取特征图进行损失计算,可以缩短损失反向传播到不同深度的路径,降低梯度消失造成的影响,加快网络收敛并在一定程度上提高精度。

暂时更新到这里…