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来说,则无需添加任何位置编码。

代码示例:

class DecoEncoder(nn.Module):
    '''Define Deco Encoder'''
    def __init__(self, enc_dims=[120,240,480], enc_depth=[2,6,2]): 
        super().__init__()
        self._encoder = ConvNeXt(depths=enc_depth, dims=enc_dims) 

    def forward(self, src):    
        output = self._encoder(src)
        return output # [2, 480, 34, 31]


ConvNeXt部分:

class Block(nn.Module):
    r""" ConvNeXt Block. 
    Args:
        dim (int): Number of input channels.
        drop_path (float): Stochastic depth rate. Default: 0.0
        layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
    """
    def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):
        super().__init__()
        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)
        self.norm = LayerNorm(dim, eps=1e-6)
        self.pwconv1 = nn.Linear(dim, 4 * dim) 
        self.act = nn.GELU()
        self.pwconv2 = nn.Linear(4 * dim, dim)
        self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)), 
                                    requires_grad=True) if layer_scale_init_value > 0 else None
        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()

    def forward(self, x):
        input = x  # [2, 120, 34, 31] # [2, 240, 34, 31] # [2, 480, 34, 31]
        x = self.dwconv(x)
        x = x.permute(0, 2, 3, 1) 
        x = self.norm(x)
        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]
        x = self.act(x)
        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]
        if self.gamma is not None:
            x = self.gamma * x
        x = x.permute(0, 3, 1, 2) 

        x = input + self.drop_path(x)
        return x  # [2, 120, 34, 31] # [2, 240, 34, 31] # [2, 480, 34, 31]

class ConvNeXt(nn.Module):
    r""" ConvNeXt
        A PyTorch impl of : `A ConvNet for the 2020s`  -
          https://arxiv.org/pdf/2201.03545.pdf
    Args:
        depths (tuple(int)): Number of blocks at each stage. Default: [2, 6, 2]
        dims (int): Feature dimension at each stage. Default: [120, 240, 480]
        drop_path_rate (float): Stochastic depth rate. Default: 0.
        layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
    """

    def __init__(self, 
                 depths=[2, 6, 2], dims=[120, 240, 480], drop_path_rate=0., 
                 layer_scale_init_value=1e-6, 
                 ):
        super().__init__()

        self.depths = depths

        self.downsample_layers = nn.ModuleList() 
        for i in range(len(depths)-1):
            downsample_layer = nn.Sequential(
                    LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),
                    nn.Conv2d(dims[i], dims[i+1], kernel_size=1),
            )
            self.downsample_layers.append(downsample_layer)  # (0): Sequential((0): LayerNorm() (1): Conv2d(120, 240, kernel_size=(1, 1), stride=(1, 1)))
                                                             # (1): Sequential((0): LayerNorm() (1): Conv2d(240, 480, kernel_size=(1, 1), stride=(1, 1)))
        self.stages = nn.ModuleList() 
        dp_rates=[x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] 
        cur = 0
        for i in range(len(depths)):
            stage = nn.Sequential(
                *[Block(dim=dims[i], drop_path=dp_rates[cur + j], 
                layer_scale_init_value=layer_scale_init_value) for j in range(depths[i])]
            )
            self.stages.append(stage)
            cur += depths[i]

        self.norm = nn.LayerNorm(dims[-1], eps=1e-6) 
        self.apply(self._init_weights)
        
    def with_pos_embed(self, tensor, pos: Optional[Tensor]):
        return tensor if pos is None else tensor + pos

    def forward(self, src):
        src=self.forward_features(src)
        return src   

    def forward_features(self, src):
        for i in range(len(self.depths)-1):
            src = self.stages[i](src)  # [2, 120, 34, 31]->[2, 120, 34, 31] # [2, 240, 34, 31]->[2, 240, 34, 31]
            src = self.downsample_layers[i](src) # [2, 120, 34, 31]->[2, 240, 34, 31] # [2, 240, 34, 31]->[2, 480, 34, 31]
        src = self.stages[len(self.depths)-1](src)
        return src  # [2, 480, 34, 31]

    def _init_weights(self, m):
        if isinstance(m, (nn.Conv2d, nn.Linear)):
            trunc_normal_(m.weight, std=.02)
            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送入检测头,以进行后续的分类和回归。

代码示例:

class DecoDecoder(nn.Module):
    '''Define Deco Decoder'''
    def __init__(self, decoder_layer, num_layers, norm=None, return_intermediate=False, qH=10, qW=10):
        super().__init__()
        self.layers = _get_clones(decoder_layer, num_layers)
        self.num_layers = num_layers
        self.norm = norm
        self.return_intermediate = return_intermediate
        self.qH = qH
        self.qW = qW

    def forward(self, tgt, memory, bs, d_model, query_pos: Optional[Tensor] = None):
        output = tgt
        intermediate = []

        for layer in self.layers:
            output=output.permute(1, 2, 0).view(bs, d_model,self.qH,self.qW)
            output = layer(output, memory, query_pos=query_pos)
            output=output.flatten(2).permute(2, 0, 1)
            if self.return_intermediate:
                intermediate.append(self.norm(output))

        if self.norm is not None:
            output = self.norm(output)
            if self.return_intermediate:
                intermediate.pop()
                intermediate.append(output)

        if self.return_intermediate:
            return torch.stack(intermediate)
        
        return output.unsqueeze(0)

class DecoDecoderLayer(nn.Module):
    '''Define a layer for Deco Decoder'''
    def __init__(self,d_model, normalize_before=False, qH=10, qW=10,
                 drop_path=0.,layer_scale_init_value=1e-6):
        super().__init__()
        self.normalize_before = normalize_before
        self.qH = qH
        self.qW = qW

        # The SIM module   
        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)
        self.norm1 = LayerNorm(d_model, eps=1e-6)
        self.pwconv1_1 = nn.Linear(d_model, 4 * d_model) 
        self.act1 = nn.GELU()
        self.pwconv1_2 = nn.Linear(4 * d_model, d_model)
        self.gamma1 = nn.Parameter(layer_scale_init_value * torch.ones((d_model)), 
                                    requires_grad=True) if layer_scale_init_value > 0 else None
        self.drop_path1 = DropPath(drop_path) if drop_path > 0. else nn.Identity()

        # The CIM module
        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)
        self.norm2 = LayerNorm(d_model, eps=1e-6)
        self.pwconv2_1 = nn.Linear(d_model, 4 * d_model) 
        self.act2 = nn.GELU()
        self.pwconv2_2 = nn.Linear(4 * d_model, d_model)
        self.gamma2 = nn.Parameter(layer_scale_init_value * torch.ones((d_model)), 
                                    requires_grad=True) if layer_scale_init_value > 0 else None
        self.drop_path2 = DropPath(drop_path) if drop_path > 0. else nn.Identity()


    def forward(self, tgt, memory, query_pos: Optional[Tensor] = None):
        # SIM
        b, d, h, w = memory.shape  # [2, 480, 34, 31]
        tgt2 = tgt + query_pos # tgt以及query_pos均由nn.Embedding(100,480)生成,并reshape到[2, 480, 10, 10]
        tgt2 = self.dwconv1(tgt2)
        tgt2 = tgt2.permute(0, 2, 3, 1) # (b,d,10,10)->(b,10,10,d)
        tgt2 = self.norm1(tgt2)
        tgt2 = self.pwconv1_1(tgt2) # [2, 10, 10, 480]->[2, 10, 10, 1920]
        tgt2 = self.act1(tgt2)
        tgt2 = self.pwconv1_2(tgt2)  # [2, 10, 10, 1920]->[2, 10, 10, 480]
        if self.gamma1 is not None:
            tgt2 = self.gamma1 * tgt2
        tgt2 = tgt2.permute(0,3,1,2) # (b,10,10,d)->(b,d,10,10)
        tgt = tgt + self.drop_path1(tgt2)

        # CIM
        tgt = F.interpolate(tgt, size=[h,w])  # [2, 480, 10, 10] -> [2, 480, 34, 31]
        tgt2 = tgt + memory 
        tgt2 = self.dwconv2(tgt2)
        tgt2 = tgt2+tgt 
        tgt2 = tgt2.permute(0, 2, 3, 1) # (b,d,h,w)->(b,h,w,d)
        tgt2=self.norm2(tgt2)
        
        # FFN
        tgt = tgt2
        tgt2 = self.pwconv2_1(tgt2)  # [2, 34, 31, 480]->[2, 34, 31, 1920]
        tgt2 = self.act2(tgt2)
        tgt2 = self.pwconv2_2(tgt2)  # [2, 34, 31, 1920]->[2, 34, 31, 480]
        if self.gamma2 is not None:
            tgt2 = self.gamma2 * tgt2
        tgt2 = tgt2.permute(0,3,1,2) # (b,h,w,d)->(b,d,h,w)
        tgt = tgt.permute(0,3,1,2) # (b,h,w,d)->(b,d,h,w)
        tgt = tgt + self.drop_path1(tgt2)  # [2, 480, 34, 31]

        # pooling
        m = nn.AdaptiveMaxPool2d((self.qH, self.qW))
        tgt = m(tgt) # [2, 480, 10, 10]

        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取得了很不错的效果,比很多以前的检测器都取得了更好的性能。


http://www.niftyadmin.cn/n/5443742.html

相关文章

基于Springboot的闲置图书分享(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的闲置图书分享(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构&…

华为OD机试 - 按照路径替换二叉树(Java 2024 C卷 200分)

目录 专栏导读一、题目描述二、输入描述三、输出描述1、输入2、输出 四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2024C卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试(JAVA)真题&#xff…

【单元测试】一文读懂java单元测试

目录 1. 什么是单元测试2. 为什么要单元测试3. 单元测试框架 - JUnit3.1 JUnit 简介3.2 JUnit 内容3.3 JUnit 使用3.3.1 Controller 层单元测试3.3.2 Service 层单元测试3.3.3 Dao 层单元测试3.3.4 异常测试3.3.5 测试套件测多个类3.3.6 idea 中查看单元测试覆盖率3.3.7 JUnit …

六、大模型-Prompt-API介绍

GPT-3.5 API 提供了一个名为 openai.ChatCompletion.create() 的端点,您可以使用该端点来与 ChatGPT 进行交互。以下是 openai.ChatCompletion.create() 的详细 API 说明: 请求参数: messages:一个数组,包含对话中的消…

Jenkins的快速入门

文章目录 一、Jenkins是什么?二、Jenkins安装和持续集成环境配置1.持续集成流程说明2.Gitlab代码托管服务器安装Gitlab简介:Gitlab安装Gitlab的使用切换中文添加组创建用户将用户添加到组创建项目idea中代码上传Gitlab 总结 一、Jenkins是什么&#xff1…

9. Linux 信号详解

Hi, 大家好! 本篇开始讲解Linux中信号的基本知识。 一、信号基本概念 信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程, 其实是在软件层次上对中断机制的一种模拟。 信号编号: 每个信号都有一个唯一…

构建空间场景轻应用,Mapmost Alpha来啦【文末赠书(10本)--第二期】

文章目录: 一、Mapmost Alpha 介绍二、Mapmost Alpha应对数字孪生业务痛点解决之道2.1 Mapmost Alpha 提供海量城市底板2.2 Mapmost Alpha 提供便捷的配置管理工具2.3 Mapmost Alpha 提供一键式部署发布和分享 三、沉浸式体验Mapmost Alpha3.1 创建应用3.2 新手指导…

【Charles如何对手机APP进行抓包和弱网测试】

一、Charles对APP抓包 1、前提条件: 1)电脑上必须安装Charles工具,如没有安装可参考:【Charles抓包工具下载安装详细操作步骤】-CSDN博客 2)手机和电脑必须在同一个局域网内(连接同一个WiFi)…