Transformer系列-4丨DETR模型和代码解析

1 前言

往期的文章中,笔者从网络结构和代码实现角度较为深入地和大家解析了Transformer模型Vision Transformer模型(ViT)以及BERT模型,其具体的链接如下:

本期内容,笔者想和大家聊一聊2020年非常火热的一个目标检测模型,叫做DEtection TRansformer,缩写为DETR

之所以火热的原因,并非这个模型的性能有多好,或者运行速度有多快。相反,DETR 的性能仅能与2015年提出的Faster R-CNN媲美,如下表1所示。

图1

那么为什么这个模型能火起来呢?

通常来说,性能速度是衡量检测模型的重要指标,为什么DETR性能一般,并且训练很困难(训练所需epoch非常长),也能受到如此大的关注?

这里直接抛出答案,该模型火的具体原因可以归纳为:

  • (1)真正做到了End-to-End检测。在我们熟知的基于CNN的检测模型中,不论是单阶段(如YOLO),双阶段(Faster R-CNN),亦或者是无锚框设计(Anchor-free)的CenterNet,其在进行训练或者测试时,都需要人为设置一些前处理或者后处理操作,比如设置锚框来提供参考,或者利用非极大值(NMS)抑制来筛除多余的框。然而,DETR抛弃了几乎所有的前处理和后处理操作,使模型做到了真正的End-to-End

  • (2)第一个将Transformer拓展到目标检测领域中,基本没有对Transformer做什么结构上的改动,且性能仍有上升空间。这个性质对研究者来说就是一个福音,没有对模型改动且性能有上升空间,意味着后续的研究者可以紧跟这个方向,通过对网络模型结构或者训练目标的优化来展开自己的研究,进而产出自己的成果。

基于上述提到的原因,DETR一跃成为目标检测领域的热点范式。在提出工作的近一年内,涌现大量相关工作,比如Deformable DETRSwin Transformer等。在后续的文章中,我们也会一一进行解析,欢迎大家关注。

这里我们推荐几个不错的视频课程和解析类的文章,笔者也是通过阅读原文+观看相应的文章/视频,加深了对DETR的理解:

【B站】李沐老师团队:DETR 论文精读【论文精读】,网址:https://www.bilibili.com/video/BV1GB4y1X72R/?spm_id_from=333.788&vd_source=beab624366b929b20152279cfa775ff6

【知乎】用Transformer做object detection:DETR, 网址:https://zhuanlan.zhihu.com/p/267156624

建议大家在看本文前,可以自行复习一下Transformer的相关知识。本文的代码如下:

https://github.com/facebookresearch/detr

2 DETR解析

2.1 简介

DETR的简要结构图如下图2所示。其大致可以归纳为以下几部分:

  • (1)将输入图像利用卷积神经网络(CNN)映射为特征图;

  • (2)将特征图输入到Transformer模型中,输出个包含物体的区域集合(一个固定数量的集合);

  • (3)对输出的区域集合真实的标签,计算两者间的集合相似度(二分图匹配损失);

  • (4)利用计算的损失,反向更新卷积神经网络(CNN)和Transformer模型的参数。

图2

这里,有两块内容最值得关注,分别为:

  • (1)DETR模型的结构以及一些其实现细节(如解码器的输入,即object queries)

  • (2)这个集合相似度如何计算

接下来我们就这两个点进行详细说明。

2.2 DETR模型

图3

上面提到, DETR模型主要分为两块,即卷积神经网络Transformer模型,其中Transformer模型由分为编码器解码器预测头

那么具体的DETR模型的结构如图3所示。其具体的运算流程可以归纳为:

  • (1)将图像(维度为)输入至卷积神经网络(比如说ResNet50),经过五次尺寸上的缩减(每次降为原来1/2)后,输出维度为的特征图;

  • (2)利用1x1卷积层将CNN输出的特征图的维度降低至256,记做输入特征图,其维度为;

  • (3)生成一个大小为的位置编码,将其与输入特征图按位相加,其相加后维度依旧是(仍记做输入特征图);

  • (4)将输入特征图reshape成 大小喂给Transformer编码器,输出同大小的特征图(记做编码器输出特征),其维度依旧是;

  • (5)构建N个(N=100),维度为256的object queries,其为可学习的embeddings,这里的100是希望模型产生至多100个物体区域。通过学习后,object queries和起到和anchor相似的作用,大致就是告诉解码器哪些区域可能会有物体;

  • (6)将object queries编码器输出特征图喂给Transformer解码器,产生大小的特征输出。其中,object queries为第一层多头自注意力(MSA)的输入,而MSA的输出以及编码器输出的特征将作为解码器中编码-解码多头自注意力的输入,如下图4所示;

  • (7)利用两个并行、不共享权重的全连接层,将Transformer解码器的输出(维度为)映射成两个输出,一个用以分类(维度为),一个用于位置回归(维度为)。其中,回归的目标就是检测框归一化的中心坐标和宽高

图4

说到这儿,基本上DETR的模型结构就解析的差不多啦。

这里我们对两块内容展开来说,其一是这个位置编码,其二是object queries,以加深大家的理解。

(1)位置编码

大家在学习Transformer的时候应该了解过Transformer对各个输入的位置是不敏感的

一般来说,为了让Transformer了解各输入间的位置关系,在输入Transformer之前需要为每个特征嵌入相应的位置信息

这种位置信息嵌入方式主要有两种,其一是为每个输入手动计算相应的位置编码,其二是设置可学习的位置编码

在DETR的代码实现中,作者提供了两种选择。下面我们先展示手动计算的位置编码的代码实现:

class PositionEmbeddingSine(nn.Module):
    """
    This is a more standard version of the position embedding, very similar to the one
    used by the Attention is all you need paper, generalized to work on images.
    """
    def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
        super().__init__()
        self.num_pos_feats = num_pos_feats
        self.temperature = temperature
        self.normalize = normalize
        if scale is not None and normalize is False:
            raise ValueError("normalize should be True if scale is passed")
        if scale is None:
            scale = 2 * math.pi
        self.scale = scale

    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  

事实上,这种设置方法和《Attention is all your need》那篇的设置方式完全一致,感兴趣的小伙伴们可以看我之前的文章。

另外,作者还设置了一种自动学习的位置编码方式,其代码实现如下所示:

class PositionEmbeddingLearned(nn.Module):
    """
    Absolute pos embedding, learned.
    """
    def __init__(self, num_pos_feats=256):
        super().__init__()
        self.row_embed = nn.Embedding(50, num_pos_feats)
        self.col_embed = nn.Embedding(50, num_pos_feats)
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.uniform_(self.row_embed.weight)
        nn.init.uniform_(self.col_embed.weight)

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors
        h, w = x.shape[-2:]
        i = torch.arange(w, device=x.device)
        j = torch.arange(h, device=x.device)
        x_emb = self.col_embed(i)
        y_emb = self.row_embed(j)
        pos = torch.cat([
            x_emb.unsqueeze(0).repeat(h, 1, 1),
            y_emb.unsqueeze(1).repeat(1, w, 1),
        ], dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(x.shape[0], 1, 1, 1)
        return pos

笔者感觉这里的位置编码方式好像与ViT中设置的位置编码方式有些差异

在ViT那篇文章中,作者设置的可学习的位置编码仅一行代码实现,如下:

self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim)) # 可学习的参数,长度为图像块数量+1,这里的1是class token

(2)object queries

这个不得不再提一下这个object queries,因为在DETR这篇文章中,笔者认为其和**集合相似度计算(二分图匹配损失)**都是DETR的核心所在。

前面提到,这个object queries的维度为,其为可学习的embeddings,训练刚开始的时候可以是随机初始化的。

这里的100是希望模型产生至多100个物体区域,在大多数检测任务中,图像中的目标数量远远小于这个值。通过学习后,object queries和起到和anchor相似的作用,大致就是告诉解码器哪些区域可能会有物体。

如下图5所示,作者在100个object queries中选了20个object queries,展示了这20个object queries在COCO(val)数据集上的可视化,也就是将这个数据集上所有的框在其对应的object query 的方块中分别进行了可视化。图中每个点表示一个框,其中点的位置为框的中心点颜色表示大小(绿色为小块,红色为大的水平框,蓝色为大的竖直框)。

从图中可以看出,每个object query负责检测的框的位置和大小都有一些区别,确实有点anchor的味道,只不过anchor是预先设置好的,而object queries是学习出来的

图5

另外一点需要注意的是,这里的100个物体区域Transformer解码器同时输出的,而非像应用于机器翻译的Transformer那样自回归地、逐个输出,这样也极大提高了输出的效率。

2.3 集合相似度计算(二分图匹配损失)

在对DETR的模型结构讨论完毕后,大家可能会有两个疑惑。

  • DETR最后输出个物体区域,但是一般图像中哪有这么多物体呢?

  • 还有就是,网络输出为固定数量的检测框,而其对应的标签里面的物体数量是变化(假设数量为m<<N)的,这个损失如何进行计算呢?

本文中,作者人为构造一个新的物体类别 ϕ (即非物体的背景类),将其补充到图像的标签中去,使得。这就构建了两个等容量的集合了,一个记做为预测集,一个为标签集

有了预测集标签集后,如何计算两者之间的相似度,进而求解两者之间的损失呢?

这里,作者在计算相似度之前执行了最优二分图匹配,来找到预测集标签集间各要素的最佳匹配。让我们能够耳熟能详的最优二分图匹配就是匈牙利算法啦。

匈牙利算法的计算和实现其实很简单,就是先计算两个集合间(二分图)的代价矩阵(cost matrix)。然后调用from scipy.optimize import linear_sum_assignment就可以找到最佳的二分图匹配。

总言之,要实现最终用于DETR训练的损失计算,需要的步骤就是1. 求解代价矩阵—>2. 匈牙利算法求最优匹配—>3. 根据匹配计算损失

(1)求解代价矩阵

匹配的核心就在于如何去计算两个集合间各要素的代价(cost)。具体地,本文作者利用以下公式计算预测集标签集中各要素间的代价(cost),具体如下:

cost计算

这里的第一项类别上的匹配代价,也就是类别相同,代价越小,反之越大;第二项位置上的匹配代价,如果两个框越重合,该代价就越小。

(2)匈牙利算法求最优匹配

这个最简单,利用from scipy.optimize import linear_sum_assignment就可以找到最佳的二分图匹配。其代码实现如下所示:

class HungarianMatcher(nn.Module):
    """This class computes an assignment between the targets and the predictions of the network

    For efficiency reasons, the targets don't include the no_object. Because of this, in general,
    there are more predictions than targets. In this case, we do a 1-to-1 matching of the best predictions,
    while the others are un-matched (and thus treated as non-objects).
    """

    def __init__(self, cost_class: float = 1, cost_bbox: float = 1, cost_giou: float = 1):
        """Creates the matcher

        Params:
            cost_class: This is the relative weight of the classification error in the matching cost
            cost_bbox: This is the relative weight of the L1 error of the bounding box coordinates in the matching cost
            cost_giou: This is the relative weight of the giou loss of the bounding box in the matching cost
        """
        super().__init__()
        self.cost_class = cost_class
        self.cost_bbox = cost_bbox
        self.cost_giou = cost_giou
        assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, "all costs cant be 0"

    @torch.no_grad()
    def forward(self, outputs, targets):
        """ Performs the matching

        Params:
            outputs: This is a dict that contains at least these entries:
                 "pred_logits": Tensor of dim [batch_size, num_queries, num_classes] with the classification logits
                 "pred_boxes": Tensor of dim [batch_size, num_queries, 4] with the predicted box coordinates

            targets: This is a list of targets (len(targets) = batch_size), where each target is a dict containing:
                 "labels": Tensor of dim [num_target_boxes] (where num_target_boxes is the number of ground-truth
                           objects in the target) containing the class labels
                 "boxes": Tensor of dim [num_target_boxes, 4] containing the target box coordinates

        Returns:
            A list of size batch_size, containing tuples of (index_i, index_j) where:
                - index_i is the indices of the selected predictions (in order)
                - index_j is the indices of the corresponding selected targets (in order)
            For each batch element, it holds:
                len(index_i) = len(index_j) = min(num_queries, num_target_boxes)
        """
        bs, num_queries = outputs["pred_logits"].shape[:2]

        # We flatten to compute the cost matrices in a batch
        out_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1)  # [batch_size * num_queries, num_classes]
        out_bbox = outputs["pred_boxes"].flatten(0, 1)  # [batch_size * num_queries, 4]

        # Also concat the target labels and boxes
        tgt_ids = torch.cat([v["labels"] for v in targets])
        tgt_bbox = torch.cat([v["boxes"] for v in targets])

        # Compute the classification cost. Contrary to the loss, we don't use the NLL,
        # but approximate it in 1 - proba[target class].
        # The 1 is a constant that doesn't change the matching, it can be ommitted.
        cost_class = -out_prob[:, tgt_ids]

        # Compute the L1 cost between boxes
        cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)

        # Compute the giou cost betwen boxes
        cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))

        # 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]

(3)根据匹配计算损失

上面实现了最优匹配后,那么这里就需要根据匹配的结果计算损失了。具体的计算如下公式所示:

二分图匹配损失

上面这个公式怎么和代价矩阵的代价计算公式差不多呢?因为两者都是从类别位置上计算框间的相似度。

但是也有不同,较计算cost时,这里在计算类别损失时,作者在前面放了个log,也就是经典的交叉熵损失。而在计算cost的时候是没有这个log的。

这个再提一嘴,这里的的定义如下:

检测框损失

可见该损失函数由两块内容构成,其一是G-IOU损失,其二是常见的L1损失了。至于为什么多引入一个G-IOU损失,其原因是因为G-IOU损失的计算与检测框的大小是无关的,而L1损失对大目标和小目标的敏感程度存在显著差异的。

3 总结

写到这里,关于DETR的基本流程和网络结构都讲解完毕了。如果大家熟悉Transformer和VIT的话,应该会觉得DETR其实比较容易理解。当然,如果大家觉得看起来比较吃力的话,这里非常建议大家可以自学/温习一下Transformer和VIT后再来看一遍!

如何学习AI大模型?

大模型时代,火爆出圈的LLM大模型让程序员们开始重新评估自己的本领。 “AI会取代那些行业?”“谁的饭碗又将不保了?”等问题热议不断。

不如成为「掌握AI工具的技术人」,毕竟AI时代,谁先尝试,谁就能占得先机!

但是LLM相关的内容很多,现在网上的老课程老教材关于LLM又太少。所以现在小白入门就只能靠自学,学习成本和门槛很高

针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料 分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓

👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈

学习路线

在这里插入图片描述

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

在这里插入图片描述

👉学会后的收获:👈

• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

在这里插入图片描述

1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集

👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值