End-to-End Object Detection with Transformers[DETR]
- 背景
- 概述
- 相关技术
- 输入
- 提取特征
- 获取position_embedding
- transformer
- encoder
- decoder
- 回归
- 总结
背景
最近在做机器翻译的优化,接触的模型就是transformer, 为了提升性能,在cpu和GPU两个平台c++重新写了整个模型,所以对于机器翻译中transformer的原理细节还是有一定的理解,同时以前做文档图片检索对于图像领域的目标检测也研究颇深,看到最近各大公众号都在推送这篇文章就简单的看了一下,感觉还是蛮有新意的,由于该论文开源,所以直接就跟着代码来解读整篇论文。
概述
整体来看,该模型首先是经历一个CNN提取特征,然后得到的特征进入transformer, 最后将transformer输出的结果转化为class和box.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def forward(self, samples): """ 这一段代码时从源码detr.py的DETR中抽出来的代码,为了逻辑清爽,删除了一些 细枝末节的内容,核心逻辑如下 """ #backbone模型中核心就是图中的CNN模型,可以自己选择resnet,vgg什么的,features就是卷积后的输出 features, pos = self.backbone(samples)#sample 就是图片,大小比如(3,200,250) src, mask = features[-1].decompose() #transformer模型处理一波 hs = transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0] #transformer模型的最终结果为hs,将其分别进入class和box的模型中处理得到class和box outputs_class = class_embed(hs) outputs_coord = bbox_embed(hs).sigmoid() out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]} return out |
下面是大致的推理过程:
相关技术
输入
作者这里封装了一个类,感觉多此一举,假如我们输入的是如下两张图片,也就说batch为2:
img1 = torch.rand(3, 200, 200),
img2 = torch.rand(3, 200, 250)
1 | x = nested_tensor_from_tensor_list([torch.rand(3, 200, 200), torch.rand(3, 200, 250)]) |
这里会转成nested_tensor, 这个nestd_tensor是什么类型呢?简单说就是把{tensor, mask}打包在一起, tensor就是我么的图片的值,那么mask是什么呢? 当一个batch中的图片大小不一样的时候,我们要把它们处理的整齐,简单说就是把图片都padding成最大的尺寸,padding的方式就是补零,那么batch中的每一张图都有一个mask矩阵,所以mask大小为[2, 200,250], tensor大小为[2,3,200,250]。
提取特征
接下里就是把tensor, 也就是图片输入到特征提取器中,这里作者使用的是残差网络,我做实验的时候用多个resnet-50, 所以tensor经过resnet-50后的结果就是[2,2048,7,8],下面是残差网络最后一层的结构。
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): FrozenBatchNorm2d()
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): FrozenBatchNorm2d()
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): FrozenBatchNorm2d()
(relu): ReLU(inplace=True)
别忘了,我们还有个mask, mask采用的方式F.interpolate,最后得到的结果是[2,7,8]
获取position_embedding
这里作者使用的三角函数的方式获取position_embediing, 如果你对位置编码不了解,你可以这样理解,“我爱祖国”,“我”位于第一位,如果编码后不加入位置信息,那么“我”这个字的编码信息就是不完善的,所以这里也一样,下面是源码,有兴趣的可以推导一下,position_embediing的输入是上面的NestedTensor={tensor,mask}, 输出最终pos的size为[1,2,256,7,8]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 |
transformer
transformer分为编码和解码,下面分别介绍:
encoder
经过上面一系列操作以后,目前我们拥有src=[ 2, 2048,7,8],mask=[2,7,8], pos=[1,2,256,7,8]
1 | hs = transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]# |
input_proj:一个卷积层,卷积核为1*1,说白了就是将压缩通道的作用,将2048压缩到256,所以传入transformer的维度是压缩后的[2,256,7,8]。
self.query_embed.weight:现在还用不到,在decoder的时候用的到,到时候再说。
来看一下transformer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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__() # encode # 单层 encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout, activation, normalize_before) encoder_norm = nn.LayerNorm(d_model) if normalize_before else None # 由6个单层组成整个encoder self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm) #decode decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward, dropout, activation, normalize_before) decoder_norm = nn.LayerNorm(d_model) self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm, return_intermediate=return_intermediate_dec) |
为了更清楚看到具体模型结构
根据代码和模型结构可以看到,encoder部分就是6个TransformerEncodeLayer组成,而每一个编码层又由1个self_attention, 2个ffn,2个norm。
在进行encoder之前先还有个处理:
1 2 3 4 | bs, c, h, w = src.shape# 这个和我们上面说的一样[2,256,7,8] src = src.flatten(2).permute(2, 0, 1) # src转为[56,2,256] pos_embed = pos_embed.flatten(2).permute(2, 0, 1)# pos_embed 转为[56,2,256] mask = mask.flatten(1) #mask 转为[2,56] |
encoder的输入为:src, mask, pos_embed,接下来捋一捋第一个单层encoder的过程
1 2 3 4 5 6 7 8 9 | q = k = self.with_pos_embed(src, pos)# pos + src src2 = self.self_attn(q, k, value=src, key_padding_mask=mask)[0] #做self_attention,这个不懂的需要补一下transfomer的知识 src = src + self.dropout1(src2)# 类似于残差网络的加法 src = self.norm1(src)# norm,这个不是batchnorm,很简单不在详述 src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))#两个ffn src = src + self.dropout2(src2)# 同上残差加法 src = self.norm2(src)# norm return src |
根据模型的代码可以看到单层的输出依然为src[56, 2, 256],第二个单层的输入依然是:src, mask, pos_embed。循环往复6次结束encoder,得到输出memory, memory的size依然为[56, 2, 256].
decoder
encoder结束后我们来看decoder, 先看代码:
1 2 3 | tgt = torch.zeros_like(query_embed) hs = self.decoder(tgt, memory, memory_key_padding_mask=mask, pos=pos_embed, query_pos=query_embed) |
现在来找输入:
- memory:这个就是encoder的输出,size为[56,2,256]
- mask:还是上面的mask
- pos_embed:还是上面的pos_embed
- query_embed:?
- tgt: 每一层的decoder的输入,第一层的话等于0
所以目前我们只要知道query_embed就行了,这个query_embed其实是一个varible,size=[100,2,256],由训练得到,结束后就固定下来了。到目前为止我们获得了decoder的所有输入,和encoder一样我们先来看看单层的decoder的运行流程:
如果你不知道100是啥,那你多少需要看一眼论文,这个100表示将要预测100个目标框,你问为什么是100框,因为作者用的数据集的目标种类有90个,万一一个图上有90个目标你至少都能检测出来吧,所以100个框合理。此外这里和语言模型的输入有很大区别,比如翻译时自回归,也就是说翻译出一个字,然后把这个字作为下一个解码的输入(这里看不懂的可以去看我博客里将transformer的那一篇),作者这里直接用[100, 256]作为输入感觉也是蛮厉害的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | q = k = self.with_pos_embed(tgt, query_pos)# tgt + query_pos, 第一层的tgt为0 tgt2 = self.self_attn(q, k, value=tgt, key_padding_mask=mask)[0]# 同上 tgt = tgt + self.dropout1(tgt2) tgt = self.norm1(tgt) tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt, query_pos), key=self.with_pos_embed(memory, pos), value=memory, key_padding_mask=mask)[0]#交叉attention 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) return tgt |
这里的难点可能是交叉attention,也叫encoder_decoder_attention, 这里利用的是encoder的输出来参与计算,里面的计算细节同样可以参考这里,经过上面六次的处理,最后得到的结果为[100,2,256], 返回的时候做一个转换,最终的结果transpose(1, 2)->[100,256,2]。
回归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MLP(nn.Module): """ Very simple multi-layer perceptron (also called FFN)""" def __init__(self, input_dim, hidden_dim, output_dim, num_layers): super().__init__() self.num_layers = num_layers h = [hidden_dim] * (num_layers - 1) self.layers = nn.ModuleList(nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])) def forward(self, x): for i, layer in enumerate(self.layers): x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x) return x self.class_embed = nn.Linear(hidden_dim, num_classes + 1) self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3) outputs_class = self.class_embed(hs) outputs_coord = self.bbox_embed(hs).sigmoid() out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]} |
这几行代码就不解释了,至于为什么是output_calss[-1], 作为思考题留给大家,如果整个源码撸一遍的话就会知道原因,总的来说最后回归的逻辑比较简单清晰,下面是最后的结果:
pred_logits:[2,100,92]
outputs_coord:[2,100,4]
总结
以上就是整个DETR的推理过程,在训练的时候还涉及到100个框对齐的问题,也不难这里就不再讲述了,如果想彻底理解整个模型,你需要对卷积,attention有比较深刻的理解,不然即使看懂了流程也不明白为什么这样做,该论文的坑位目测还不少,而且对于目标检测的模型来说这个代码量算是少的了,给起来也快,需要毕业的孩纸抓紧啦,哈哈哈