赞
踩
在开始介绍RT-DETR这个网络之前,我们首先需要先了解DETR这个系列的网络与我们常提及的anchor-base以及anchor-free存在着何种差异。
首先我们先简单讨论一下anchor-base以及anchor-free两者的差异与共性:
1、两者差异:顾名思义,这两者一个显而易见的差别就是有无anchor,anchor-base是需要手工选取不同比例大小的anchor来得到proposals,而anchor-free则不需要。当然两者具体差异肯定不是这么几句话就能说的清的,这里不做详细讨论所以按下不表。
2、两者共性:两者虽然在获取proposals的原理方面存在不同,但是其最终还是存在了一个多对一的问题,也就是会对同一个目标产生多个proposal,也即最后两者最后都是需要通过阈值选取以及nms去过滤掉多余的预测框。此处可参考:Nms-Free时代
但是当问题涉及到了工业或者生活等其他领域落地问题上面的时候,无论是anchor-base还是anchor-free所面临的这中两套阈值问题(要先完成阈值筛选(Confidence threshold)和非极大值抑制(NMS)处理两个关键步骤)总是会对时效性产生很大的影响。
在引言中,我们提到了无论是anchor-base还是anchor-free其实都存在着一个阈值问题,而这个阈值问题就决定了绕不开nms这个机制,nms这一机制的存在也就导致无法真正意义上去实现一个端到端的简易部署的网络模型,而DETR既不需要proposal也不需要anchor,而是直接利用Transformers这种能全局建模的能力,通过将目标检测视为一个集合预测问题。同时由于这种全局建模能力使得DETR系列网络不会输出那么多冗余的框。在此处个人水平有限害怕个人水平对读者的理解造成扭曲,因此在这就不对DETR做过多解释如果有兴趣的可以移步沐神的精讲视频:李沐DETR论文精读
而DETR系列网络究竟有多简洁,上述一小段文字自然无法很好的解释其简洁性,大家可以通过下面视频截图的50行代码看出DETR的简洁性。
首先RT-DETR网络的检测动机其实非常明确,从这个命名也能看出来,其是想打造一个能实际用于工业界的DETR网络。但是对于DETR而言虽然其具备简单易用的特点,然而其实时性较差,在DETR的论文中的实验可以看到其FPS较之于Faster-RCNN更低(如下图所示),因此这是DETR的一个缺点。同时DETR还有另外一个缺点,就是其虽然借助于自注意机制带来的全局信息使得对于大目标检测效果表现较好,但是其对于小目标的检测效果相对而言较弱,当然在RT-DETR中也未解决这个问题,因此此处只提一下按下不表。
好了,既然目前已经知道DETR有一个实时性较差的缺点,那么接下来就应该对造成推理耗时高的原因进行一下分析,在RT-DETR中作者认为在Encoder部分作用在
S
5
S_5
S5(最顶层特征,即
C
5
C_5
C5只是换个表达方式),这样就可以大幅度减小计算量、提高计算速度,同时不会损伤模型性能,这样做的的原因是作者是基于一下两点考虑:
于个人而言感觉这个观点很类似YOLOF的观点,在YOLOF中也是直接使用顶层的特征层,有感兴趣的可以去看看YOLOF这篇文章。
而为了验证这个观点,作者的团队设计了若干对照组实验,实验如下图所示:
对于对照组(a),其结构就是DINO-R50,但移除了DINO-R50中的多尺度encoder,直接将
S
3
S_3
S3、
S
4
S_4
S4和
S
5
S_5
S5拼接在一起,交给后续的网络去处理,得到最终的输出。注意,这里的拼接是先将二维的
H
×
W
H×W
H×W拉平成
H
W
HW
HW然后再去拼接:
H
1
W
1
+
H
2
W
2
+
H
3
W
3
H_1W_1+H_2W_2+H_3W_3
H1W1+H2W2+H3W3。
对于对照组(b),作者团队在(a)基础上,加入了单尺度的Transformer Encoder(SSE),仅包含一层Encoder层,分别处理三个尺度的输出,注意,三个尺度共享一个SSE,而不是为每一个尺度都设计一个独立的SSE。通过这一共享的操作,三个尺度的信息是可以实现一定程度的信息交互。最后将处理结果拼接在一起,交给后续的网络去处理,得到最终的输出。
对于对照组©,作者团队使用多尺度的Transformer Encoder(MSE),大概是Deformable DETR所采用的那一类MSE结构。将三个尺度的特征拼接在一起后,交由MSE来做处理,使得三个尺度的特征同时完成“尺度内”和“跨尺度”的信息交互和融合,最后将处理结果,交给后续的网络去处理,得到最终的输出。
对于对照组(d),作者团队则先用共享的SSE分别处理每个尺度的特征,然后再使用PAN-like的特征金字塔网络去融合三个尺度的特征,最后将融合后的多尺度特征拼接在一起,交给后续的网络去处理,得到最终的输出。
对于对照组(e),作者团队则使用一个SSE只处理 S 5 S_5 S5特征,即所谓的AIFI模块,随后再使用CCFM模块去完成跨尺度的特征融合,最后将融合后的多尺度特征拼接在一起,交给后续的网络去处理,得到最终的输出。
最终,对比结果如上方的图所示,我们简单地来分析一下这组对比试验说明了什么样的结论。
综上,这组对比试验证明了两件事:1) Transformer的Encoder部分只需要处理high-level特征,既能大幅度削减计算量、提升计算速度,同时也不会损伤到性能,甚至还有所提升;2) 对于多尺度特征的交互和融合,我们仍可以采用CNN架构常用的PAN网络来搭建,只需要一些细节上的调整即可。
另外,我们仔细来看一下这个对照组(e),注意,根据论文给出的结果和源代码,所谓的CCFM其实还是PaFPN,如下方的图10所示,其中的Fusion模块就是一个CSPBlock风格的模块。
为了看得更仔细一些,我们来看一下官方源码,如下方的Python代码所示。依据论文的设定,HybridEncoder
包含AIFI和CCFM两大模块,其中AIFI就是Transformer的Encoder部分,而CCFM其实就是常见的PaFPN结构:首先用若干层1x1卷积将所有的特征的通道数都映射至同一数目,如256,随后再进行top-down和bottom-up两部分的特征融合。
# https://github.com/PaddlePaddle/PaddleDetection/blob/develop/ppdet/modeling/transformers/hybrid_encoder.py class HybridEncoder(nn.Layer): __shared__ = ['depth_mult', 'act', 'trt', 'eval_size'] __inject__ = ['encoder_layer'] def __init__(self, in_channels=[512, 1024, 2048], feat_strides=[8, 16, 32], hidden_dim=256, use_encoder_idx=[2], num_encoder_layers=1, encoder_layer='TransformerLayer', pe_temperature=10000, expansion=1.0, depth_mult=1.0, act='silu', trt=False, eval_size=None): super(HybridEncoder, self).__init__() self.in_channels = in_channels self.feat_strides = feat_strides self.hidden_dim = hidden_dim self.use_encoder_idx = use_encoder_idx self.num_encoder_layers = num_encoder_layers self.pe_temperature = pe_temperature self.eval_size = eval_size # channel projection self.input_proj = nn.LayerList() for in_channel in in_channels: self.input_proj.append( nn.Sequential( nn.Conv2D(in_channel, hidden_dim, kernel_size=1, bias_attr=False), nn.BatchNorm2D( hidden_dim, weight_attr=ParamAttr(regularizer=L2Decay(0.0)), bias_attr=ParamAttr(regularizer=L2Decay(0.0))))) # encoder transformer self.encoder = nn.LayerList([ TransformerEncoder(encoder_layer, num_encoder_layers) for _ in range(len(use_encoder_idx)) ]) act = get_act_fn( act, trt=trt) if act is None or isinstance(act, str, dict)) else act # top-down fpn self.lateral_convs = nn.LayerList() self.fpn_blocks = nn.LayerList() for idx in range(len(in_channels) - 1, 0, -1): self.lateral_convs.append( BaseConv(hidden_dim, hidden_dim, 1, 1, act=act)) self.fpn_blocks.append( CSPRepLayer( hidden_dim * 2, hidden_dim, round(3 * depth_mult), act=act, expansion=expansion)) # bottom-up pan self.downsample_convs = nn.LayerList() self.pan_blocks = nn.LayerList() for idx in range(len(in_channels) - 1): self.downsample_convs.append( BaseConv(hidden_dim, hidden_dim, 3, stride=2, act=act)) self.pan_blocks.append( CSPRepLayer( hidden_dim * 2, hidden_dim, round(3 * depth_mult), act=act, expansion=expansion)) def forward(self, feats, for_mot=False): assert len(feats) == len(self.in_channels) # get projection features proj_feats = [self.input_proj[i](feat) for i, feat in enumerate(feats)] # encoder if self.num_encoder_layers > 0: for i, enc_ind in enumerate(self.use_encoder_idx): h, w = proj_feats[enc_ind].shape[2:] # flatten [B, C, H, W] to [B, HxW, C] src_flatten = proj_feats[enc_ind].flatten(2).transpose( [0, 2, 1]) if self.training or self.eval_size is None: pos_embed = self.build_2d_sincos_position_embedding( w, h, self.hidden_dim, self.pe_temperature) else: pos_embed = getattr(self, f'pos_embed{enc_ind}', None) memory = self.encoder[i](src_flatten, pos_embed=pos_embed) proj_feats[enc_ind] = memory.transpose([0, 2, 1]).reshape( [-1, self.hidden_dim, h, w]) # top-down fpn inner_outs = [proj_feats[-1]] for idx in range(len(self.in_channels) - 1, 0, -1): feat_heigh = inner_outs[0] feat_low = proj_feats[idx - 1] feat_heigh = self.lateral_convs[len(self.in_channels) - 1 - idx]( feat_heigh) inner_outs[0] = feat_heigh upsample_feat = F.interpolate( feat_heigh, scale_factor=2., mode="nearest") inner_out = self.fpn_blocks[len(self.in_channels) - 1 - idx]( paddle.concat( [upsample_feat, feat_low], axis=1)) inner_outs.insert(0, inner_out) # bottom-up pan outs = [inner_outs[0]] for idx in range(len(self.in_channels) - 1): feat_low = outs[-1] feat_height = inner_outs[idx + 1] downsample_feat = self.downsample_convs[idx](feat_low) out = self.pan_blocks[idx](paddle.concat( [downsample_feat, feat_height], axis=1)) outs.append(out) return outs
而对于其中解码器的decoder部分使用的是"DINO Head"的decoder部分,也即使用了DINO的一个“去噪思想”(具体我未了解过,有感兴趣的可以自行了解)。但是看官方源码可知在推理阶段的时候,RT-DETR的decoder部分与DETR并无区别,由于我只关注推理阶段因此关于"去噪思想"这部分各位可以详细看看DINO以及官方源码自行了解。
class RTDETRTransformer(nn.Layer): __shared__ = ['num_classes', 'hidden_dim', 'eval_size'] def __init__(self, num_classes=80, hidden_dim=256, num_queries=300, position_embed_type='sine', backbone_feat_channels=[512, 1024, 2048], feat_strides=[8, 16, 32], num_levels=3, num_decoder_points=4, nhead=8, num_decoder_layers=6, dim_feedforward=1024, dropout=0., activation="relu", num_denoising=100, label_noise_ratio=0.5, box_noise_scale=1.0, learnt_init_query=True, query_pos_head_inv_sig=False, eval_size=None, eval_idx=-1, eps=1e-2): super(RTDETRTransformer, self).__init__() assert position_embed_type in ['sine', 'learned'], \ f'ValueError: position_embed_type not supported {position_embed_type}!' assert len(backbone_feat_channels) <= num_levels assert len(feat_strides) == len(backbone_feat_channels) for _ in range(num_levels - len(feat_strides)): feat_strides.append(feat_strides[-1] * 2) self.hidden_dim = hidden_dim self.nhead = nhead self.feat_strides = feat_strides self.num_levels = num_levels self.num_classes = num_classes self.num_queries = num_queries self.eps = eps self.num_decoder_layers = num_decoder_layers self.eval_size = eval_size # backbone feature projection self._build_input_proj_layer(backbone_feat_channels) # Transformer module decoder_layer = TransformerDecoderLayer( hidden_dim, nhead, dim_feedforward, dropout, activation, num_levels, num_decoder_points) self.decoder = TransformerDecoder(hidden_dim, decoder_layer, num_decoder_layers, eval_idx) # denoising part self.denoising_class_embed = nn.Embedding( num_classes, hidden_dim, weight_attr=ParamAttr(initializer=nn.initializer.Normal())) self.num_denoising = num_denoising self.label_noise_ratio = label_noise_ratio self.box_noise_scale = box_noise_scale # decoder embedding self.learnt_init_query = learnt_init_query if learnt_init_query: self.tgt_embed = nn.Embedding(num_queries, hidden_dim) self.query_pos_head = MLP(4, 2 * hidden_dim, hidden_dim, num_layers=2) self.query_pos_head_inv_sig = query_pos_head_inv_sig # encoder head self.enc_output = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.LayerNorm( hidden_dim, weight_attr=ParamAttr(regularizer=L2Decay(0.0)), bias_attr=ParamAttr(regularizer=L2Decay(0.0)))) self.enc_score_head = nn.Linear(hidden_dim, num_classes) self.enc_bbox_head = MLP(hidden_dim, hidden_dim, 4, num_layers=3) # decoder head self.dec_score_head = nn.LayerList([ nn.Linear(hidden_dim, num_classes) for _ in range(num_decoder_layers) ]) self.dec_bbox_head = nn.LayerList([ MLP(hidden_dim, hidden_dim, 4, num_layers=3) for _ in range(num_decoder_layers) ]) self._reset_parameters() def forward(self, feats, pad_mask=None, gt_meta=None, is_teacher=False): # input projection and embedding (memory, spatial_shapes, level_start_index) = self._get_encoder_input(feats) # prepare denoising training if self.training: denoising_class, denoising_bbox_unact, attn_mask, dn_meta = \ get_contrastive_denoising_training_group(gt_meta, self.num_classes, self.num_queries, self.denoising_class_embed.weight, self.num_denoising, self.label_noise_ratio, self.box_noise_scale) else: denoising_class, denoising_bbox_unact, attn_mask, dn_meta = None, None, None, None target, init_ref_points_unact, enc_topk_bboxes, enc_topk_logits = \ self._get_decoder_input( memory, spatial_shapes, denoising_class, denoising_bbox_unact,is_teacher) # decoder out_bboxes, out_logits = self.decoder( target, init_ref_points_unact, memory, spatial_shapes, level_start_index, self.dec_bbox_head, self.dec_score_head, self.query_pos_head, attn_mask=attn_mask, memory_mask=None, query_pos_head_inv_sig=self.query_pos_head_inv_sig) return (out_bboxes, out_logits, enc_topk_bboxes, enc_topk_logits, dn_meta)
同样在RT-DETR中提及的IoU-aware Query Selection其实在推理阶段也不存在,其是在训练期间约束检测器对高 IoU 的特征产生高分类分数,对低 IoU 的特征产生低分类分数。因此若只关注推理阶段的读者,这部分也可无需关注,而关于IoU-aware Query Selection此部分其实是在assignment阶段和计算loss的阶段,classification的标签都换成了 “IoU软标签” :
所谓的“IoU软标签”,就是指将预测框与GT之间的IoU作为类别预测的标签。熟悉YOLO工作的读者一定对此不会陌生,其本质就是已经被广泛验证了的IoU-aware。在最近的诸多工作里,比如RTMDet、DAMO-YOLO等工作中,都有引入这一理念,去对齐类别和回归的差异。之所以这么做,是因为按照以往的one-hot方式,完全有可能出现“当定位还不够准确的时候,类别就已经先学好了”的“未对齐”的情况,毕竟类别的标签非0即1。但如果将IoU作为类别的标签,那么类别的学习就要受到回归的调制,只有当回归学得也足够好的时候,类别才会学得足够好,否则,类别不会过快地先学得比回归好,因此后者显式地制约着前者。
最后先看一下论文在实验部分给出的对比表格,如下方的图所示:
由于在这里我考虑的是RT-DETR的实时性、GFLOPS以及相应的检测性能,因此我只关注其与YOLO系列的对比,而在YOLO系列中目前使用较多的是YOLOV5以及PPYOLOE网络。而RT-DETR与这两个网络对应的L与X对比能看到其FPS是V5的一倍,同样也比PP高出十来FPS,而算力开销也是与两者接近,检测性能同样高出了这两者几个点。同时RT-DETR这个网络也继承了DETR网络只要支持CNN与Transformer就能使用以及其推理阶段简洁性的特点。不难看出在未来DETR系列的网络应能全面铺开,而且其网络的简洁性对于实际部署工作也是有利的。
总而言之,在未来完完全全可以在实时的DETR网络方面好好期待一把。因此于此处记录一下阅读这篇文章以及DETR的读后感。受限于个人能力其中纰漏肯定还有不少,欢迎各位大力指出,共同成长。
参考链接:
《目标检测》-第33章-浅析RT-DETR
超越YOLOv8,飞桨推出精度最高的实时检测器RT-DETR!
PaddlePaddle
DETR 论文精读
Nms-Free时代
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。