当前位置:   article > 正文

DECO: Query-Based End-to-End Object Detection with ConvNets 学习笔记

DECO: Query-Based End-to-End Object Detection with ConvNets 学习笔记

论文地址:https://arxiv.org/pdf/2312.13735.pdf
源码地址:https://github.com/xinghaochen/DECO

近年来,Detection Transformer (DETR) 及其变体在准确检测目标方面显示出巨大的潜力。对象查询机制使DETR系列能够直接获得固定数量的目标预测,并简化了检测 pipeline。同时,最近的研究还表明,通过适当的架构设计,ConvNeXt这样的卷积网络(ConvNets)也可以与 transformers等变压器实现竞争性能。为此,在本文中,作者探讨了是否可以使用 ConvNet 而不是复杂的 transformer 架构构建基于查询的端到端目标检测框架。所提出的框架,即检测ConvNet(DECO),由骨干和卷积编码器-解码器架构组成。作者精心设计了DECO编码器,并为DECO解码器提出了一种新颖的机制,通过卷积层在对象查询和图像特征之间执行交互。将 DECO 与具有挑战性的 COCO 基准上的先前检测器进行了比较。尽管简单,但DECO在检测精度和运行速度方面具有竞争力。具体来说,使用 ResNet-50 和 ConvNeXt-Tiny 骨干网,DECO 在 COCO val 上分别获得 38.6% 和 40.8% 的 AP,分别为 35 和 28 FPS,优于 DETR 模型。作者的 DECO+ 集成了先进的多尺度功能模块,以 34 FPS 的速度实现了 47.8% 的 AP。作者希望提出的DECO为设计目标检测框架带来另一个视角。


网络架构

        DETR的主要特点是利用Transformer Encoder-Decoder的结构,对一张输入图像,利用一组Query跟图像特征进行交互,可以直接输出指定数量的检测框,从而可以摆脱对NMS等后处理操作的依赖。作者提出的DECO总体架构上跟DETR类似,也包括了Backbone来进行图像特征提取,一个Encoder-Decoder的结构跟Query进行交互,最后输出特定数量的检测结果。唯一的不同在于,DECO的Encoder和Decoder是纯卷积的结构,因此DECO是一个由纯卷积构成的Query-Based端对端检测器。


编码器

        DETR 的 Encoder 结构替换相对比较直接,我们选择使用4个ConvNeXt Block来构成Encoder结构。具体来说,Encoder的每一层都是通过叠加一个7x7的深度卷积、一个LayerNorm层、一个1x1的卷积、一个GELU激活函数以及另一个1x1卷积来实现的。此外,在DETR中,因为Transformer架构对输入具有排列不变性,所以每层编码器的输入都需要添加位置编码,但是对于卷积组成的Encoder来说,则无需添加任何位置编码。

代码示例:

  1. class DecoEncoder(nn.Module):
  2. '''Define Deco Encoder'''
  3. def __init__(self, enc_dims=[120,240,480], enc_depth=[2,6,2]):
  4. super().__init__()
  5. self._encoder = ConvNeXt(depths=enc_depth, dims=enc_dims)
  6. def forward(self, src):
  7. output = self._encoder(src)
  8. return output # [2, 480, 34, 31]

ConvNeXt部分:

  1. class Block(nn.Module):
  2. r""" ConvNeXt Block.
  3. Args:
  4. dim (int): Number of input channels.
  5. drop_path (float): Stochastic depth rate. Default: 0.0
  6. layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
  7. """
  8. def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):
  9. super().__init__()
  10. self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim) # Conv2d(120, 120, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=120) # Conv2d(240, 240, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=240) # Conv2d(480, 480, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=480)
  11. self.norm = LayerNorm(dim, eps=1e-6)
  12. self.pwconv1 = nn.Linear(dim, 4 * dim)
  13. self.act = nn.GELU()
  14. self.pwconv2 = nn.Linear(4 * dim, dim)
  15. self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)),
  16. requires_grad=True) if layer_scale_init_value > 0 else None
  17. self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
  18. def forward(self, x):
  19. input = x # [2, 120, 34, 31] # [2, 240, 34, 31] # [2, 480, 34, 31]
  20. x = self.dwconv(x)
  21. x = x.permute(0, 2, 3, 1)
  22. x = self.norm(x)
  23. x = self.pwconv1(x) # [2, 34, 31, 120] -> [2, 34, 31, 480] # [2, 34, 31, 240] -> [2, 34, 31, 960] # [2, 34, 31, 480] -> [2, 34, 31, 1920]
  24. x = self.act(x)
  25. x = self.pwconv2(x) # [2, 34, 31, 480] -> [2, 34, 31, 120] # [2, 34, 31, 960] -> [2, 34, 31, 240] # [2, 34, 31, 1920] -> [2, 34, 31, 480]
  26. if self.gamma is not None:
  27. x = self.gamma * x
  28. x = x.permute(0, 3, 1, 2)
  29. x = input + self.drop_path(x)
  30. return x # [2, 120, 34, 31] # [2, 240, 34, 31] # [2, 480, 34, 31]
  31. class ConvNeXt(nn.Module):
  32. r""" ConvNeXt
  33. A PyTorch impl of : `A ConvNet for the 2020s` -
  34. https://arxiv.org/pdf/2201.03545.pdf
  35. Args:
  36. depths (tuple(int)): Number of blocks at each stage. Default: [2, 6, 2]
  37. dims (int): Feature dimension at each stage. Default: [120, 240, 480]
  38. drop_path_rate (float): Stochastic depth rate. Default: 0.
  39. layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
  40. """
  41. def __init__(self,
  42. depths=[2, 6, 2], dims=[120, 240, 480], drop_path_rate=0.,
  43. layer_scale_init_value=1e-6,
  44. ):
  45. super().__init__()
  46. self.depths = depths
  47. self.downsample_layers = nn.ModuleList()
  48. for i in range(len(depths)-1):
  49. downsample_layer = nn.Sequential(
  50. LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),
  51. nn.Conv2d(dims[i], dims[i+1], kernel_size=1),
  52. )
  53. self.downsample_layers.append(downsample_layer) # (0): Sequential((0): LayerNorm() (1): Conv2d(120, 240, kernel_size=(1, 1), stride=(1, 1)))
  54. # (1): Sequential((0): LayerNorm() (1): Conv2d(240, 480, kernel_size=(1, 1), stride=(1, 1)))
  55. self.stages = nn.ModuleList()
  56. dp_rates=[x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]
  57. cur = 0
  58. for i in range(len(depths)):
  59. stage = nn.Sequential(
  60. *[Block(dim=dims[i], drop_path=dp_rates[cur + j],
  61. layer_scale_init_value=layer_scale_init_value) for j in range(depths[i])]
  62. )
  63. self.stages.append(stage)
  64. cur += depths[i]
  65. self.norm = nn.LayerNorm(dims[-1], eps=1e-6)
  66. self.apply(self._init_weights)
  67. def with_pos_embed(self, tensor, pos: Optional[Tensor]):
  68. return tensor if pos is None else tensor + pos
  69. def forward(self, src):
  70. src=self.forward_features(src)
  71. return src
  72. def forward_features(self, src):
  73. for i in range(len(self.depths)-1):
  74. src = self.stages[i](src) # [2, 120, 34, 31]->[2, 120, 34, 31] # [2, 240, 34, 31]->[2, 240, 34, 31]
  75. src = self.downsample_layers[i](src) # [2, 120, 34, 31]->[2, 240, 34, 31] # [2, 240, 34, 31]->[2, 480, 34, 31]
  76. src = self.stages[len(self.depths)-1](src)
  77. return src # [2, 480, 34, 31]
  78. def _init_weights(self, m):
  79. if isinstance(m, (nn.Conv2d, nn.Linear)):
  80. trunc_normal_(m.weight, std=.02)
  81. nn.init.constant_(m.bias, 0)

解码器

        相比而言,Decoder的替换则复杂得多。Decoder的主要作用为对图像特征和Query进行充分的交互,使得Query可以充分感知到图像特征信息,从而对图像中的目标进行坐标和类别的预测。Decoder主要包括两个输入:Encoder的特征输出和一组可学的查询向量(Query)。把Decoder的主要结构分为两个模块:自交互模块(Self-Interaction Module, SIM)和交叉交互模块(Cross-Interaction Module, CIM)。

        这里,SIM模块主要融合Query和上层Decoder层的输出,这部分的结构,可以利用若干个卷积层来组成,使用9x9 depthwise卷积和1x1卷积分别在空间维度和通道维度进行信息交互,充分获取所需的目标信息以送到后面的CIM模块进行进一步的目标检测特征提取。Query为一组随机初始化的向量,该数量决定了检测器最终输出的检测框数量,其具体的值可以随实际需要进行调节。对DECO来说,因为所有的结构都是由卷积构成的,因此我们把Query变成二维,比如100个Query,则可以变成10x10的维度。

        CIM模块的主要作用是让图像特征和Query进行充分的交互,使得Query可以充分感知到图像特征信息,从而对图像中的目标进行坐标和类别的预测。对于Transformer结构来说,利用cross attention机制可以很方便实现这一目的,但对于卷积结构来说,如何让两个特征进行充分交互,则是一个最大的难点。

        要把大小不同的SIM输出和encoder输出全局特征进行融合,必须先把两者进行空间对齐然后进行融合,首先我们对SIM的输出进行最近邻上采样:

        使得上采样后的特征与Encoder输出的全局特征有相同的尺寸,然后将上采样后的特征和encoder输出的全局特征进行融合,然后进入深度卷积进行特征交互后加上残差输入:

        最后将交互后的特征通过FNN进行通道信息交互,之后pooling到目标数量大小得到decoder的输出embedding:

        最后将得到的输出embedding送入检测头,以进行后续的分类和回归。

代码示例:

  1. class DecoDecoder(nn.Module):
  2. '''Define Deco Decoder'''
  3. def __init__(self, decoder_layer, num_layers, norm=None, return_intermediate=False, qH=10, qW=10):
  4. super().__init__()
  5. self.layers = _get_clones(decoder_layer, num_layers)
  6. self.num_layers = num_layers
  7. self.norm = norm
  8. self.return_intermediate = return_intermediate
  9. self.qH = qH
  10. self.qW = qW
  11. def forward(self, tgt, memory, bs, d_model, query_pos: Optional[Tensor] = None):
  12. output = tgt
  13. intermediate = []
  14. for layer in self.layers:
  15. output=output.permute(1, 2, 0).view(bs, d_model,self.qH,self.qW)
  16. output = layer(output, memory, query_pos=query_pos)
  17. output=output.flatten(2).permute(2, 0, 1)
  18. if self.return_intermediate:
  19. intermediate.append(self.norm(output))
  20. if self.norm is not None:
  21. output = self.norm(output)
  22. if self.return_intermediate:
  23. intermediate.pop()
  24. intermediate.append(output)
  25. if self.return_intermediate:
  26. return torch.stack(intermediate)
  27. return output.unsqueeze(0)
  28. class DecoDecoderLayer(nn.Module):
  29. '''Define a layer for Deco Decoder'''
  30. def __init__(self,d_model, normalize_before=False, qH=10, qW=10,
  31. drop_path=0.,layer_scale_init_value=1e-6):
  32. super().__init__()
  33. self.normalize_before = normalize_before
  34. self.qH = qH
  35. self.qW = qW
  36. # The SIM module
  37. self.dwconv1 = nn.Conv2d(d_model, d_model, kernel_size=9, padding=4, groups=d_model) # Conv2d(480, 480, kernel_size=(9, 9), stride=(1, 1), padding=(4, 4), groups=480)
  38. self.norm1 = LayerNorm(d_model, eps=1e-6)
  39. self.pwconv1_1 = nn.Linear(d_model, 4 * d_model)
  40. self.act1 = nn.GELU()
  41. self.pwconv1_2 = nn.Linear(4 * d_model, d_model)
  42. self.gamma1 = nn.Parameter(layer_scale_init_value * torch.ones((d_model)),
  43. requires_grad=True) if layer_scale_init_value > 0 else None
  44. self.drop_path1 = DropPath(drop_path) if drop_path > 0. else nn.Identity()
  45. # The CIM module
  46. self.dwconv2 = nn.Conv2d(d_model, d_model, kernel_size=9, padding=4, groups=d_model) # Conv2d(480, 480, kernel_size=(9, 9), stride=(1, 1), padding=(4, 4), groups=480)
  47. self.norm2 = LayerNorm(d_model, eps=1e-6)
  48. self.pwconv2_1 = nn.Linear(d_model, 4 * d_model)
  49. self.act2 = nn.GELU()
  50. self.pwconv2_2 = nn.Linear(4 * d_model, d_model)
  51. self.gamma2 = nn.Parameter(layer_scale_init_value * torch.ones((d_model)),
  52. requires_grad=True) if layer_scale_init_value > 0 else None
  53. self.drop_path2 = DropPath(drop_path) if drop_path > 0. else nn.Identity()
  54. def forward(self, tgt, memory, query_pos: Optional[Tensor] = None):
  55. # SIM
  56. b, d, h, w = memory.shape # [2, 480, 34, 31]
  57. tgt2 = tgt + query_pos # tgt以及query_pos均由nn.Embedding(100,480)生成,并reshape到[2, 480, 10, 10]
  58. tgt2 = self.dwconv1(tgt2)
  59. tgt2 = tgt2.permute(0, 2, 3, 1) # (b,d,10,10)->(b,10,10,d)
  60. tgt2 = self.norm1(tgt2)
  61. tgt2 = self.pwconv1_1(tgt2) # [2, 10, 10, 480]->[2, 10, 10, 1920]
  62. tgt2 = self.act1(tgt2)
  63. tgt2 = self.pwconv1_2(tgt2) # [2, 10, 10, 1920]->[2, 10, 10, 480]
  64. if self.gamma1 is not None:
  65. tgt2 = self.gamma1 * tgt2
  66. tgt2 = tgt2.permute(0,3,1,2) # (b,10,10,d)->(b,d,10,10)
  67. tgt = tgt + self.drop_path1(tgt2)
  68. # CIM
  69. tgt = F.interpolate(tgt, size=[h,w]) # [2, 480, 10, 10] -> [2, 480, 34, 31]
  70. tgt2 = tgt + memory
  71. tgt2 = self.dwconv2(tgt2)
  72. tgt2 = tgt2+tgt
  73. tgt2 = tgt2.permute(0, 2, 3, 1) # (b,d,h,w)->(b,h,w,d)
  74. tgt2=self.norm2(tgt2)
  75. # FFN
  76. tgt = tgt2
  77. tgt2 = self.pwconv2_1(tgt2) # [2, 34, 31, 480]->[2, 34, 31, 1920]
  78. tgt2 = self.act2(tgt2)
  79. tgt2 = self.pwconv2_2(tgt2) # [2, 34, 31, 1920]->[2, 34, 31, 480]
  80. if self.gamma2 is not None:
  81. tgt2 = self.gamma2 * tgt2
  82. tgt2 = tgt2.permute(0,3,1,2) # (b,h,w,d)->(b,d,h,w)
  83. tgt = tgt.permute(0,3,1,2) # (b,h,w,d)->(b,d,h,w)
  84. tgt = tgt + self.drop_path1(tgt2) # [2, 480, 34, 31]
  85. # pooling
  86. m = nn.AdaptiveMaxPool2d((self.qH, self.qW))
  87. tgt = m(tgt) # [2, 480, 10, 10]
  88. return tgt

多尺度特征

        跟原始的DETR一样,上述框架得到的DECO有个共同的短板,即缺少多尺度特征,而这对于高精度目标检测来说是影响很大的。Deformable DETR通过使用一个多尺度的可变形注意力模块来整合不同尺度的特征,但这个方法是跟Attention算子强耦合的,因此没法直接用在我们的DECO上。为了让DECO也能处理多尺度特征,我们在Decoder输出的特征之后,采用了RT-DETR提出的一个跨尺度特征融合模块。


实验

        在COCO上进行实验,在保持主要架构不变的情况下将DECO和DETR进行了比较,比如保持Query数量一致,保持Decoder层数不变等,仅将DETR中的Transformer结构按上文所述换成卷积结构。可以看出,DECO取得了比DETR更好的精度和速度的Trade-off。

        把搭配了多尺度特征后的DECO跟更多目标检测方法进行了对比,其中包括了很多DETR的变体,从下图中可以看到,DECO取得了很不错的效果,比很多以前的检测器都取得了更好的性能。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/288197?site
推荐阅读
相关标签
  

闽ICP备14008679号