博客

Mapillary 研究:无缝场景分割和原地激活 BatchNorm

作者: 2019年7月23日2024年11月16日暂无评论

随着美国等发达国家的道路每年发生高达 15% 的变化,Mapillary 通过将来自任何相机的图像组合成世界的 3D 可视化效果,解决了保持地图更新的需求。Mapillary 的独立协作式方法使任何人都可以收集、共享和使用街道级图像,从而改进地图、建设城市并推动汽车行业的发展。

如今,世界各地的人们和组织已为 Mapillary 的使命贡献了超过 6 亿张图像,旨在通过图像帮助人们了解世界各地,并使这些数据可供使用。其客户和合作伙伴包括世界银行、HERE 和丰田研究院。

Mapillary 的计算机视觉技术以前所未有的方式为地图带来了智能,增强了我们对世界的整体理解。Mapillary 对其所有图像进行大规模的最先进语义图像分析和基于图像的 3D 建模。在本文中,我们将讨论 Mapillary 研究的两个近期工作及其在 PyTorch 中的实现——无缝场景分割(Seamless Scene Segmentation)[1] 和原地激活批量归一化(In-Place Activated BatchNorm)[2]——它们分别能够生成全景分割结果,并在训练期间节省高达 50% 的 GPU 显存。

无缝场景分割

Github 项目页面:https://github.com/mapillary/seamseg/

无缝场景分割的目标是从图像中预测“全景”分割 [3],即一种完整的标注,其中每个像素都被分配一个类别 ID,并在可能的情况下分配一个实例 ID。像许多处理实例检测和分割的现代 CNN 一样,我们采用了 Mask R-CNN 框架 [4],并使用 ResNet50 + FPN [5] 作为主干网络(Backbone)。该架构分两个阶段工作:首先,“提议头”(Proposal Head)在图像上选择一组可能包含对象的候选边界框(即提议);然后,“掩码头”(Mask Head)专注于每个提议,预测其类别和分割掩码。此过程的输出是“稀疏”的实例分割,仅覆盖图像中包含可数对象(如汽车和行人)的部分。

为了完成我们称为无缝场景分割的全景方法,我们在 Mask R-CNN 中增加了第三个阶段。源自相同的主干网络,“语义头”(Semantic Head)在整个图像上预测密集的语义分割,同时也考虑不可数或无定形的类别(如道路和天空)。最后,使用简单的非极大值抑制算法融合掩码头和语义头的输出,生成最终的全景预测。关于实际网络架构、所用损失函数和底层数学原理的所有详细信息,请访问我们 CVPR 2019 论文 [1]项目网站

虽然 Mask R-CNN 有多个公开版本,包括一个用 Caffe2 编写的官方实现,但在 Mapillary,我们决定使用 PyTorch 从头构建无缝场景分割,以便完全控制和理解整个流程。在这样做时,我们遇到了一些主要的障碍,并不得不提出一些将在下面描述的创造性权宜之计。

处理可变大小的张量

使全景分割网络区别于传统 CNN 的一点是可变大小数据的普遍存在。事实上,我们处理的许多数量无法轻易用固定大小的张量表示:每张图像包含的对象数量不同,提议头为每张图像产生的提议数量不同,并且图像本身可以有不同的大小。虽然这本身不是问题(人们可以一次处理一张图像),但我们仍然希望尽可能利用批处理级别的并行性。此外,在使用多个 GPU 进行分布式训练时,DistributedDataParallel 要求其输入是成批的、大小统一的张量。

我们解决这些问题的方案是将每批可变大小的张量包装在 PackedSequence 中。PackedSequence 实际上只是张量的一个高级列表类,它将其内容标记为“相关”,确保它们共享相同的类型,并提供诸如将所有张量移动到特定设备等有用方法。在执行不通过批处理并行性就能明显加快速度的轻量级操作时,我们只需在一个 for 循环中遍历 PackedSequence 的内容。当性能至关重要时(例如在网络主体中),我们只需连接 PackedSequence 的内容,根据需要添加零填充(就像处理可变长度输入的 RNN 一样),并跟踪每个张量的原始维度。

PackedSequence 也有助于我们解决上面提到的第二个问题。我们稍微修改了 DistributedDataParallel 以识别 PackedSequence 输入,将它们拆分为大小相等的块,并将它们的内容分发到各个 GPU 上。

分布式数据并行下的非对称计算图

我们网络的另一个(也许更微妙的)特性是它可以在 GPU 之间生成非对称的计算图。实际上,构成网络的一些模块是“可选的”,即它们并不总是针对所有图像进行计算。例如,当提议头没有输出任何提议时,根本不会遍历掩码头。如果我们正在使用 DistributedDataParallel 在多个 GPU 上进行训练,这将导致其中一个副本不计算掩码头参数的梯度。

在 PyTorch 1.1 之前,这会导致崩溃,因此我们不得不开发一种权宜之计。我们简单但有效的解决方案是在不需要实际前向传播时计算一个“伪前向传播”,即类似这样的操作:

def fake_forward():
    fake_input = get_correctly_shaped_fake_input()
    fake_output = mask_head(fake_input)
    fake_loss = fake_output.sum() * 0
    return fake_loss

在这里,我们生成一批虚假数据,将其通过掩码头,并返回一个总是向所有参数反向传播零的损失函数。

从 PyTorch 1.1 开始,不再需要这种权宜之计:通过在构造函数中设置 find_unused_parameters=TrueDistributedDataParallel 会被告知识别那些未被所有副本计算梯度的参数并正确处理它们。这导致我们的代码库得到了实质性的简化!

原地激活批量归一化

Github 项目页面:https://github.com/mapillary/inplace_abn/

大多数研究人员可能会同意,无论研究实验室是拥有少量还是数千个 GPU,在可用 GPU 资源方面总是存在限制。在 Mapillary 尚在使用较少且主要是 12GB Titan X 级别的高端消费级 GPU 时,我们正在寻找一种能够虚拟增强训练过程中可用内存的解决方案,以便能够在语义分割等密集标注任务上获得并推动最先进的结果。原地激活批量归一化(In-place Activated BatchNorm)使我们能够使用多达 50% 的额外内存(且计算开销很小),因此它已深入集成到我们所有的当前项目中(包括上面描述的无缝场景分割)。

在处理前向传播中的 BN-激活-卷积序列时,大多数深度学习框架(包括 PyTorch)需要存储两个大的缓冲区,即 BN 的输入 x 和卷积的输入 z。这是必要的,因为 BN 和卷积的标准反向传播实现依赖于它们的输入来计算梯度。使用 InPlace-ABN 替换 BN-激活序列,我们可以安全地丢弃 x,从而在训练时节省高达 50% 的 GPU 显存。为了实现这一点,我们根据输出 y 重写了 BN 的反向传播,而 y 又通过反转激活函数从 z 重构出来。

InPlace-ABN 的唯一限制是它需要使用可逆激活函数,例如 Leaky ReLU 或 ELU。除此之外,它可以直接作为任何网络中 BN+激活模块的即插即用替代品。我们的原生 CUDA 实现与 PyTorch 的标准 BN 相比计算开销极小,任何人都可以从此处使用:https://github.com/mapillary/inplace_abn/

具有非对称图和不平衡批次的同步 BN

在使用多个 GPU 和/或多个节点对网络进行同步 SGD 训练时,通常的做法是在每个设备上分别计算 BatchNorm 统计信息。然而,根据我们在语义和全景分割网络方面的工作经验,我们发现累积所有工作节点的均值和方差可以显著提高准确性。在处理小批量数据时尤其如此,例如在无缝场景分割中,我们每个 GPU 只训练一张超高分辨率的图像。

InPlace-ABN 支持跨多个 GPU 和多个节点的同步操作,并且自 1.1 版本起,这也可以在标准 PyTorch 库中使用 SyncBatchNorm 实现。然而,与 SyncBatchNorm 相比,我们支持一些对无缝场景分割特别重要的额外功能:不平衡的批次和非对称图。

如前所述,类似 Mask R-CNN 的网络会自然地产生可变大小的张量。因此,在 InPlace-ABN 中,我们使用此处描述的并行算法的变体来计算同步统计信息,该变体正确考虑了每个 GPU 可能持有不同数量样本的事实。PyTorch 的 SyncBatchNorm 目前正在修改以支持这一点,改进后的功能将在未来的版本中提供。

非对称图(以上述意义而言)是创建同步 BatchNorm 实现时必须处理的另一个复杂因素。幸运的是,PyTorch 的分布式组功能允许我们将分布式通信限制在工作节点的子集中,从而轻松排除那些当前不活跃的节点。唯一缺失的一点是,为了创建一个分布式组,每个进程都需要知道将参与该组的所有进程的 ID,即使不是该组成员的进程也需要调用 new_group() 函数。在 InPlace-ABN 中,我们用这样的函数来处理它:

import torch
import torch.distributed as distributed

def active_group(active):
    """Initialize a distributed group where each process can independently decide whether to participate or not"""
    world_size = distributed.get_world_size()
    rank = distributed.get_rank()

    # Gather active status from all workers
    active = torch.tensor(rank if active else -1, dtype=torch.long, device=torch.cuda.current_device())
    active_workers = torch.empty(world_size, dtype=torch.long, device=torch.cuda.current_device())
    distributed.all_gather(list(active_workers.unbind(0)), active)

    # Create group
    active_workers = [int(i) for i in active_workers.tolist() if i != -1]
    group = distributed.new_group(active_workers)
    return group

首先,每个进程(包括不活跃的进程)通过 all_gather 调用将其状态传达给所有其他进程,然后使用共享信息创建分布式组。在实际实现中,我们还包含了一个组缓存机制,因为 new_group() 通常在每个批次调用时开销太大。

参考文献

[1] Seamless Scene Segmentation; Lorenzo Porzi, Samuel Rota Bulò, Aleksander Colovic, Peter Kontschieder; Computer Vision and Pattern Recognition (CVPR), 2019

[2] In-place Activated BatchNorm for Memory-Optimized Training of DNNs; Samuel Rota Bulò, Lorenzo Porzi, Peter Kontschieder; Computer Vision and Pattern Recognition (CVPR), 2018

[3] Panoptic Segmentation; Alexander Kirillov, Kaiming He, Ross Girshick, Carsten Rother, Piotr Dollar; Computer Vision and Pattern Recognition (CVPR), 2019

[4] Mask R-CNN; Kaiming He, Georgia Gkioxari, Piotr Dollar, Ross Girshick; International Conference on Computer Vision (ICCV), 2017

[5] Feature Pyramid Networks for Object Detection; Tsung-Yi Lin, Piotr Dollar, Ross Girshick, Kaiming He, Bharath Hariharan, Serge Belongie; Computer Vision and Pattern Recognition (CVPR), 2017