关于深度学习:基于-EasyCV-复现-DETR-和-DABDETRObject-Query-的正确打开方式

56次阅读

共计 9482 个字符,预计需要花费 24 分钟才能阅读完成。

DETR 是最近几年最新的指标检测框架,第一个真正意义上的端到端检测算法,省去了繁琐的 RPN、anchor 和 NMS 等操作,间接输出图片输入检测框。DETR 的胜利次要归功于 Transformer 弱小的建模能力,还有匈牙利匹配算法解决了如何通过学习的形式 one-to-one 的匹配检测框和指标框。

尽管 DETR 能够达到跟 Mask R-CNN 相当的精度,然而训练 500 个 epoch、收敛速度慢,小指标精度低的问题都饱受诟病。后续一系列的工作都围绕着这几个问题开展,其中最精彩的要属 Deformable DETR,也是现在检测的刷榜必备,Deformable DETR 的奉献不单单只是将 Deformable Conv 推广到了 Transformer 上,更重要的是提供了很多训练好 DETR 检测框架的技巧,比方模拟 Mask R-CNN 框架的 two-stage 做法,如何将 query embed 拆分成 content 和 reference points 两局部组成,如何将 DETR 拓展到多尺度训练,还有通过 look forward once 进行 boxes 预测等技巧,在 Deformable DETR 之后,大家仿佛找到了如何关上 DETR 框架的正确形式。其中对 object query 代表什么含意,以及如何更好的利用 object query 做检测,产生了许多有价值的工作,比方 Anchor DETR、Conditional DETR 等等,其中 DAB-DETR 做的尤为彻底。DAB-DETR 将 object query 看成是 content 和 reference points 两个局部,其中 reference points 显示的示意成 xywh 四维向量,而后通过 decoder 预测 xywh 的残差对检测框迭代更新,另外还通过 xywh 向量引入地位注意力,帮忙 DETR 放慢收敛速度,本文将基于 EasyCV 复现的 DETR 和 DAB-DETR 算法具体介绍一下如何正确的应用 object query 来晋升 DETR 检测框架的性能。

DETR

DETR 应用 set loss function 作为监督信号来进行端到端训练,而后同时预测所有指标,其中 set loss function 应用 bipartite matching 算法将 pred 指标和 gt 指标匹配起来。间接将指标检测工作看成 set prediction 问题,使训练过程变的简洁,并且防止了 anchor、NMS 等简单解决。

DETR 次要奉献有两个局部:architecture 和 set prediction loss。

1.Architecture

DETR 先用 CNN 将输出图像 embedding 成一个二维表征,而后将二维表征转换成一维表征并联合 positional encoding 一起送入 encoder,decoder 将大量固定数量的已学习的 object queries(能够了解为 positional embeddings)和 encoder 的输入作为输出。最初将 decoder 失去的每个 output embdding 传递到一个共享的前馈网络 (FFN),该网络能够预测一个检测后果(包含类和边框) 或着“没有指标”的类。

1.1 Transformer

1.1.1 Encoder

将 Backbone 输入的 feature map 转换成一维表征,失去 特色图,而后联合 positional encoding 作为 Encoder 的输出。每个 Encoder 都由 Multi-Head Self-Attention 和 FFN 组成。和 Transformer Encoder 不同的是,因为 Encoder 具备地位不变性,DETR 将 positional encoding 增加到每一个 Multi-Head Self-Attention 中,来保障指标检测的地位敏感性。

# 一层 encoder 代码如下
class TransformerEncoderLayer(nn.Module):

    def __init__(self,
                 d_model,
                 nhead,
                 dim_feedforward=2048,
                 dropout=0.1,
                 activation='relu',
                 normalize_before=False):
        super().__init__()
        self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        # Implementation of Feedforward model
        self.linear1 = nn.Linear(d_model, dim_feedforward)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(dim_feedforward, d_model)

        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)

        self.activation = _get_activation_fn(activation)
        self.normalize_before = normalize_before

    def with_pos_embed(self, tensor, pos: Optional[Tensor]):
        return tensor if pos is None else tensor + pos

    def forward(self,
                src,
                src_mask: Optional[Tensor] = None,
                src_key_padding_mask: Optional[Tensor] = None,
                pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(src, pos)
        src2 = self.self_attn(
            q,
            k,
            value=src,
            attn_mask=src_mask,
            key_padding_mask=src_key_padding_mask)[0]
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
        src = src + self.dropout2(src2)
        src = self.norm2(src)
        return src

1.1.2 Decoder
因为 Decoder 也具备地位不变性,Decoder \(N \) object query(能够了解为学习不同 object 的 positional embedding)必须是不同,以便生成不同 object 的 embedding,并且同时把它们增加到每一个 Multi-Head Attention 中。\(N \) 个 object queries 通过 Decoder 转换成一个 output embedding,而后 output embedding 通过 FFN 独立解码出 \(N \) 个预测后果,蕴含 box 和 class。对输出 embedding 同时应用 Self-Attention 和 Encoder-Decoder Attention,模型能够利用指标的互相关系来进行全局推理。和 Transformer Decoder 不同的是,DETR 的每个 Decoder 并行输入 \(N \) 个对象,Transformer Decoder 应用的是自回归模型,串行输入 \(N \) 个对象,每次只能预测一个输入序列的一个元素。

# 一层 decoder 代码如下
class TransformerDecoderLayer(nn.Module):

    def __init__(self,
                 d_model,
                 nhead,
                 dim_feedforward=2048,
                 dropout=0.1,
                 activation='relu',
                 normalize_before=False):
        super().__init__()
        self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        self.multihead_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        # Implementation of Feedforward model
        self.linear1 = nn.Linear(d_model, dim_feedforward)
        self.dropout = nn.Dropout(dropout)
        self.linear2 = nn.Linear(dim_feedforward, d_model)

        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
        self.dropout3 = nn.Dropout(dropout)

        self.activation = _get_activation_fn(activation)
        self.normalize_before = normalize_before

    def with_pos_embed(self, tensor, pos: Optional[Tensor]):
        return tensor if pos is None else tensor + pos

    def forward(self,
                 tgt,
                 memory,
                 tgt_mask: Optional[Tensor] = None,
                 memory_mask: Optional[Tensor] = None,
                 tgt_key_padding_mask: Optional[Tensor] = None,
                 memory_key_padding_mask: Optional[Tensor] = None,
                 pos: Optional[Tensor] = None,
                 query_pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(tgt, query_pos)
        tgt2 = self.self_attn(
            q,
            k,
            value=tgt,
            attn_mask=tgt_mask,
            key_padding_mask=tgt_key_padding_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,
            attn_mask=memory_mask,
            key_padding_mask=memory_key_padding_mask)[0]
        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
1.1.3 FFNFFN

由 3 层 perceptron 和一层 linear projection 组成。FFN 预测出 box 的归一化核心坐标、长、宽和 class。DETR 预测的是固定数量的 N 个 box 的汇合,并且 N 通常比理论指标数要大的(其中 DETR 默认设置为 100 个,而 DAB-DETR 设置为 300 个),并且应用一个额定的空类来示意预测失去的 box 不存在指标。

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

2.Set prediction loss

DETR 模型训练的次要艰难是如何依据 gt 掂量预测后果 (类别、地位、数量)。DETR 提出的 loss 函数能够产生 pred 和 gt 的最优双边匹配(确定 pred 和 gt 的一对一关系),而后优化 loss。将 \(y \) 示意为 gt 的汇合,示意为 \(N \)个预测后果的汇合。假如大于图片指标数,\(y \)能够认为是用空类 (无指标) 填充的大小为 \(N \)的汇合。搜寻两个汇合 \(N \)个元素的不同排列程序,使得 loss 尽可能的小的排列程序即为二分图最大匹配(Bipartite Matching),公式如下:

其中示意 pred 和 gt 对于元素 \(i \)的匹配 loss。其中二分图匹配通过匈牙利算法 (Hungarian algorithm) 失去。匹配 loss 同时思考了 pred class 和 pred box 的准确性。每个 gt 的元素 \(i \)能够看成,示意 class label(可能是空类)示意 gt box,将元素 \(i \)二分图匹配指定的 pred class 示意为,pred box 示意为。
第一步先找到一对一匹配的 pred 和 gt,第二步再计算 hungarian loss。hungarian loss 公式如下:

其中 联合了 L1 loss 和 generalized IoU loss,公式如下:

# HungarianMatcher 通过计算出 cost_bbox,cost_class,cost_giou 来一对一匹配预测框和 gt 框,而后返回匹配的索引对,最初通过索引对计算出 loss 值

# Final cost matrix
C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou
C = C.view(bs, num_queries, -1).cpu()

sizes = [len(v['boxes']) for v in targets]
indices = [linear_sum_assignment(c[i])
    for i, c in enumerate(C.split(sizes, -1))
]
return [(torch.as_tensor(i, dtype=torch.int64),
         torch.as_tensor(j, dtype=torch.int64)) for i, j in indices]

DAB-DETR

DAB-DETR 将 object query 看成是 content 和 reference points 两个局部,其中 reference points 显示的示意成 xywh 四维向量,而后通过 decoder 预测 xywh 的残差对检测框迭代更新,另外还通过 xywh 向量引入地位注意力,帮忙 DETR 放慢收敛速度。

在 DAB-DETR 之前,有许多工作对如何设置 reference points 进行过深刻的摸索:Conditional DETR 通过 256 维的可学习向量学习失去 xy 参考点,而后将地位信息引入 transformer decoder 中;Anchor DETR 参考点看成是 xy,而后通过学习的形式失去 256 维的向量,将地位信息引入 transformer decoder 中,并且通过逐级迭代失去检测框的 xy;Defomable DETR 则是通过 256 维可学习向量失去 xywh 参考 anchor,通过逐级迭代失去检测框;DAB-DETR 则更为彻底,吸百家之长,通过 xywh 学习 256 维的向量,将地位信息引入 transformer decoder 中,并且通过逐级迭代失去检测框。至此,reference points 的应用形式逐步清朗起来,显示的示意为 xywh,而后学习成 256 维向量,引入地位信息,每层 transformer decoder 学习 xywh 的残差,逐级叠加失去最初的检测框。

# DAB-DETR 将 object query 显示的拆分为 content 和 pos 两种属性

# 将 query_embed 显示的示意为 xywh,示意 pos 属性,通过 MLP 学习成 256 维的 pos 特色
self.query_embed = nn.Embedding(num_queries, query_dim)
# get sine embedding for the query vector
reference_points = self.query_embed.sigmoid()
obj_center = reference_points[..., :2]
query_sine_embed = gen_sineembed_for_position(obj_center)
query_pos = self.ref_point_head(query_sine_embed)

# content_embed 初始化为全 0 的 256 维特色
tgt = torch.zeros(
                    self.num_queries,
                    bs,
                    self.embed_dims,
                    device=query_embed.device)

另外,DAB-DETR 为了更充沛的利用 xywh 这种更为显示的 reference points 示意形式,进一步的引入了 Width & Height-Modulated Multi-Head Cross-Attention,其实简略来讲就是在 cross-attention 中引入地位 xywh 失去的地位注意力,这一点改良能够极大的放慢 decoder 的收敛速度,因为原始的 DETR 相当于是在全图学习到地位注意力,DAB-DETR 能够间接关注到要害地位,这也是 Deformable DETR 为啥能放慢收敛的起因,实质就是更要害的稠密地位采样能够放慢 decoder 收敛速度。

# 通过 MLP 的学习,调整 query_sine_embed 的 attn 地位,进一步放慢收敛速度

# modulated HW attentions
if self.modulate_hw_attn:
    refHW_cond = self.ref_anchor_head(output).sigmoid()  # nq, bs, 2
    query_sine_embed[..., self.d_model //
                     2:] *= (refHW_cond[..., 0] /
                             obj_center[..., 2]).unsqueeze(-1)
    query_sine_embed[..., :self.d_model //
                     2] *= (refHW_cond[..., 1] /
                            obj_center[..., 3]).unsqueeze(-1)

复现后果

Tutorial

接下来,咱们将通过一个理论的例子介绍如何基于 EasyCV 进行 DAB-DETR 算法的训练,也能够在该链接查看具体步骤。

一、装置依赖包

如果是在本地开发环境运行,能够参考该链接装置环境。若应用 PAI-DSW 进行试验则无需装置相干依赖,在 PAI-DSW docker 中已内置相干环境。二、数据筹备

你能够下载 COCO2017 数据,也能够应用咱们提供了示例 COCO 数据

wget http://pai-vision-data-hz.oss-cn-zhangjiakou.aliyuncs.com/data/small_coco_demo/small_coco_demo.tar.gz && tar -zxf small_coco_demo.tar.gz

mkdir -p data/  && mv small_coco_demo data/coco

data/coco 格局如下:

data/coco/
├── annotations
│   ├── instances_train2017.json
│   └── instances_val2017.json
├── train2017
│   ├── 000000005802.jpg
│   ├── 000000060623.jpg
│   ├── 000000086408.jpg
│   ├── 000000118113.jpg
│   ├── 000000184613.jpg
│   ├── 000000193271.jpg
│   ├── 000000222564.jpg
│       ...
│   └── 000000574769.jpg
└── val2017
    ├── 000000006818.jpg
    ├── 000000017627.jpg
    ├── 000000037777.jpg
    ├── 000000087038.jpg
    ├── 000000174482.jpg
    ├── 000000181666.jpg
    ├── 000000184791.jpg
    ├── 000000252219.jpg
         ...
    └── 000000522713.jpg

二、模型训练和评估

以 vitdet-base 为示例。在 EasyCV 中,应用配置文件的模式来实现对模型参数、数据输出及增广形式、训练策略的配置,仅通过批改配置文件中的参数设置,就能够实现试验配置进行训练。能够间接下载示例配置文件。

查看 easycv 装置地位

# 查看 easycv 装置地位
import easycv
print(easycv.__file__)

export PYTHONPATH=$PYTHONPATH:root/EasyCV

执行训练命令

单机 8 卡:CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m 
torch.distributed.launch --
nproc_per_node=8 --
master_port=29500 tools/train.py 
configs/detection/dab-
detr/dab_detr_r50_8x2_50e_coco.p
y --work_dir easycv/dab_detr -
-launcher pytorch

执行评估命令

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5
,6,7 python -m 
torch.distributed.launch --
nproc_per_node=8 --
master_port=29500 tools/eval.py 
configs/detection/dab-
detr/dab_detr_r50_8x2_50e_coco.p
y easycv/dab_detr/epoch_50.pth -
-launcher pytorch --eval

Reference

代码实现:
DETR

DAB-DETR

EasyCV 往期分享
基于 EasyCV 复现 ViTDet:单层特色超过 FPN

MAE 自监督算法介绍和基于 EasyCV 的复现

EasyCV 开源|开箱即用的视觉自监督 +Transformer 算法库

正文完
 0