DETR: End-to-End Object Detection with Transformers 网络解析
说明:
- 个人理解,如有错误请及时提出。
- 由于自己电脑驱动较低不满足440及以上,所以目前网络中张量的具体维度不太清楚,后续如有条件再更新博客。
- 不得不感叹论文作者的数学功底扎实、知识涉猎广博。
- 太暴力了,依靠一些精巧的结构和强大的硬件支持替代了大家精心设计的anchor、nms等结构。
- 对于小目标、多目标场景仍然较差。
- 论文涉及的东西很多,本博客逐步添加,欢迎提出修改意见。
资源:
- 论文地址
- github地址
- 李宏毅老师Transformer视频B站版
- Yannic Kilcher大神的论文讲解,“你懂得”网站的地址。另外提供国内CSDN下载(待上传),欢迎支持
本博客目录
- 总体结构
- Transformer
- Multi-Head Self-Attention
- ADD & Norm
- FFN
- Spatial positional encoding
- Object queries
- Bipartite Matching
- 损失函数
总体结构
- 整个网络思路非常清晰,首先是将图像[3, H, W]送入常用的卷积神经网络backbone来提取特征,以resnet为例,那么在backbone最后这里得到的特征图[2048, H/32, W/32]
- 然后将来自backbone的特征图展开,变为[C, HW]的形状,送入由编码器和解码器组成的Transformer结构中
- 最后是将预测得到的“集合”与真值进行匹配(匈牙利算法),通过最小化损失(代价)来一次性预测检测(实例分割)结果
Transformer
如上图所示,左图为DETR中的transformer结构,右图为文章Attention Is All You Need中的结构图,基本上还是一致的。
李宏毅老师讲到transformer实际上是seq2seq model with ‘self-attention’,所以下面着重来讲一下DETR里面涉及到的一些细节问题
Multi-Head Self-Attention
其实NLP中transformer需要处理的是一些序列数据,那么为了处理序列数据首先可以想到的就是RNN结构,这种结构考虑了上下文关系,相对于CNN(感受野问题)来讲具有优势。但是如下图所示的RCNN模块存在一个问题:任务难以并行
因为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
ki=Wkai
vi=Wvai
有了这个数学表示,那么接下来我们需要的是拿q1与(k1, k2, k3, k4)进行点乘,然后是q2, q3, q4,这个过程表示如下
其中
α1,i?=q1?ki/d
α^1,i?=exp(α1,i?)/j∑?exp(α1,j?)
d是q和k的维度。到这里我们获得了输入x1与四个数据字典k相关的“权值”,那么为了获得最后的“词袋模型编码后的向量”,就需要alpha-head与v进行一定的操作,如下图所示
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?)
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的数目通常来讲每张图像上数目是不同的,这里就引入了几个问题:
- N的个数是一个超参,比如DETR设置的是100
- 论文中讲到NGT数目不等于N时就用(null,b)来填充
- N和NGT匹配过程使用的是匈牙利算法,代码中为了加快速度进行计算时NGT其实没有进行填充,N中元素没有成功与NGT配对的就被视作背景
- 不得不说构思非常巧妙,转化为了求取最小代价的过程,但是也不得不说这个是大力出奇迹,DETR训练的缓慢本人猜测与这个相关,等有环境可以运行代码的时候测试下
损失函数
看到网上很多分析DETR损失函数的,所以这里我就不介绍了,如果有需要再说吧。不过值得一提的是,DETR也用到了这个思想:从不同深度提取特征图进行损失计算,可以缩短损失反向传播到不同深度的路径,降低梯度消失造成的影响,加快网络收敛并在一定程度上提高精度。
暂时更新到这里…