计算机视觉/视频分析

为什么自动增强很重要

深度学习模型需要数百 GB 的数据才能在看不见的样本上很好地泛化。数据扩充有助于增加数据集中示例的可变性。

传统的数据扩充方法可以追溯到统计学习,当时扩充的选择依赖于建立模型训练的工程师的领域知识、技能和直觉。

自动增强出现了减少对手动数据预处理的依赖。它结合了应用自动调整和根据概率分布随机选择增强的思想。

事实证明,使用 AutoAugment 和 RandAugment 等自动数据增强方法可以通过使模型在训练中看到的样本多样化来提高模型的准确性。自动扩充使数据预处理更加复杂,因为一批中的每个样本都可以用不同的随机扩充进行处理。

在这篇文章中,我们介绍了如何使用 NVIDIA DALI 实现和使用 GPU 加速自动增强来训练,然后使用条件执行。

自动数据扩充方法

自动增强是基于标准的图像变换,如旋转、剪切、模糊或亮度调整。大多数操作都接受一个称为幅值的控制参数。幅度越大,操作对图像的影响就越大。

传统上,扩充策略是由工程师手工编写的固定操作序列。自动增强策略与传统策略的区别在于,增强和参数的选择不是固定的,而是概率的。

AutoAugment采用强化学习从数据中学习最佳概率增强策略,将目标模型的泛化视为奖励信号。使用 AutoAugment ,我们发现了图像数据集的新策略,例如ImageNet,CIFAR-10SVHN,超过了最先进的精度。

AutoAugment 策略是一组增强对。每个增强都用应用或跳过操作的幅度和概率进行参数化。运行策略时,随机选择并应用其中一对,独立于每个样本。

学习策略意味着搜索最佳的增强对、它们的大小和概率。在策略搜索过程中,必须对目标模型进行多次再培训。这使得策略搜索的计算成本巨大。

为了避免计算成本高昂的搜索步骤,您可以重用在类似任务中找到的现有策略。或者,您可以使用其他自动数据扩充方法,这些方法旨在将搜索步骤保持在最低限度。

RandAugment将策略搜索步骤减少到只调整两个数字:NM.N是要在序列中应用的随机选择的操作数,以及M是所有操作共享的大小。尽管 RandAugment 很简单,但我们发现,当与相同的增强集一起使用时,这种数据增强方法优于 AutoAugment 的策略。

TrivialAgument通过移除这两个超参数来构建 RandAugment 。我们建议对每个样本随机选择一个增量。 TrivialAugment 和 RandAugment 之间的区别在于,幅度不是固定的,而是随机均匀采样的。

结果表明,在训练过程中随机采样增强对于模型泛化可能比广泛搜索仔细调整的策略更重要。

从开始1.24 版本发布, DALI 提供了AutoAugment,RandAugmentTrivialAugment在这篇文章中,我们向您展示了如何使用所有这些最先进的实现,并讨论了 DALI 中新的条件执行功能,这是它们实现的支柱。

DALI 和有条件执行

现代 GPU 架构显著加快了深度学习模型训练。然而,为了实现最大的端到端性能,必须快速预处理模型消耗的数据批次,以避免 CPU 出现瓶颈。

NVIDIA DALI 通过异步执行、预取、专用加载程序、一组丰富的面向批处理的扩充以及与流行的 DL 框架(如PyTorch,TensorFlow,PaddlePaddleMXNet.

为了创建一个数据处理管道,我们在 Python 函数中组合了所需的操作,并用@pipeline_def出于性能原因,该函数只定义 DALI 的执行计划,然后由 DALI 执行器异步运行。

下面的代码示例显示了一个管道定义,该定义加载、解码并将随机噪声增强应用于图像。

from nvidia.dali import pipeline_def, fn, types 
 
@pipeline_def(batch_size=8, num_threads=4, device_id=0) 
def pipeline(): 
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True) 
    image = fn.decoders.image(encoded, device="mixed") 
    prob = fn.random.uniform(range=[0, 0.15]) 
    distorted = fn.noise.salt_and_pepper(image, prob=prob) 
    return distorted 
Eight images distorted with varying levels of salt-and-pepper noise.
图 1 。带有随机噪声的管道输出

管道的代码是面向样本的,而输出是一批图像。在指定运算符时不需要处理批处理,因为 DALI 在内部进行管理。

然而,到目前为止,还不可能表达对一批样本子集进行操作的操作。这阻止了使用 DALI 实现自动扩增,因为它为每个样本随机选择不同的操作。

DALI 中引入的条件执行使您能够使用正则 Python 语义为批处理中的每个样本选择单独的操作: if 语句。下面的代码示例随机应用两个增强中的一个。

@pipeline_def(batch_size=4, num_threads=4, device_id=0,
              enable_conditionals=True)
def pipeline():
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True)
    image = fn.decoders.image(encoded, device="mixed")
    change_stauration = fn.random.coin_flip(dtype=types.BOOL)
    if change_stauration:
        distorted = fn.saturation(image, saturation=2)
    else:
        edges = fn.laplacian(image, window_size=5)
        distorted = fn.cast_like(0.5 * image + 0.5 * edges, image)
    return distorted

在图 2 中,我们增加了一些样本的饱和度,并在其他样本中使用拉普拉斯算子检测边缘,基于fn.random.coin_flip后果 DALI 翻译if-else语句转换为执行计划,该执行计划根据 if 条件将批处理拆分为两个批处理。通过这种方式,部分批次分别并行处理,而样本则属于同一批次if-else分支仍然受益于批处理的 CUDA 内核。

Four images, one with sharpened edges and the other three with saturated color.
图 2 :随机选择的管道输出

您可以很容易地扩展该示例,以使用从任意集合中随机选择的扩充。在下面的代码示例中,我们定义了三个扩充,并实现了一个选择运算符,该运算符根据随机选择的整数选择正确的一个。

def edges(image):
    edges = fn.laplacian(image, window_size=5)
    return fn.cast_like(0.5 * image + 0.5 * edges, image)

def rotation(image):
    angle = fn.random.uniform(range=[-45, 45])
    return fn.rotate(image, angle=angle, fill_value=0)

def salt_and_pepper(image):
    return fn.noise.salt_and_pepper(image, prob=0.15)


def select(image, operation_idx, operations, i=0):
    if i >= len(operations):
        return image
    if operation_idx == i:
        return operations[i](image)
    return select(image, operation_idx, operations, i + 1)

在下面的代码示例中,我们选择了一个随机整数,并在 DALI 管道内使用 select 运算符运行相应的操作。

@pipeline_def(batch_size=6, num_threads=4, device_id=0,
              enable_conditionals=True)
def pipeline():
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True)
    image = fn.decoders.image(encoded, device="mixed")
    operations = [edges, rotation, salt_and_pepper]
    operation_idx = fn.random.uniform(values=list(range(len(operations))))
    distorted = select(image, operation_idx, operations)
    return distorted

因此,我们得到了一批图像,其中每个图像都通过一个随机选择的操作进行变换:边缘检测、旋转和椒盐噪声失真。

Six images: four rotated at different angles, one distorted with salt-and-pepper noise, and one with embossed edges.
图 3 。具有随机增强的管道输出

在图 3 中,管道将随机选择的增强应用于每个图像:旋转、边缘检测或椒盐失真。

DALI 自动增强

通过按样本选择运算符,您可以实现自动扩充。为了便于使用, NVIDIA 推出了auto_augDALI 中的模块,具有流行的自动增强的现成实现:auto_aug.auto_augment,auto_aug.rand_augmentauto_aug.trivial_augment它们可以开箱即用,也可以通过调整增强幅度或构建 DALI 基元的用户定义的增强来定制。

这个auto_aug.augmentationsDALI 中的模块提供由自动增强程序共享的默认操作集:

下面的代码示例显示了如何运行 RandAugment 。

import nvidia.dali.auto_aug.rand_augment as ra

@pipeline_def(batch_size=6, num_threads=4, device_id=0,
              enable_conditionals=True)
def pipeline():
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True)
    shape = fn.peek_image_shape(encoded)
    image = fn.decoders.image(encoded, device="mixed")
    distorted = ra.rand_augment(image, n=3, m=15, shape=shape, fill_value=0)
    return distorted

这个rand_augment操作员接受解码后的图像、图像的形状、要在序列中应用的随机增强的数量 (n=3) 以及这些行动应该具有的规模 (m=15,在可定制的0, 30范围)。

Six images with six different distortions.
图 4 。 RandAugment DALI 管道的示例输出

图 4 中的增强分为两类:几何变换和颜色变换。

在某些应用程序中,您可能必须限制已使用的扩充集。例如,如果数据集由数字图片组成,则将数字“ 9 ”旋转 180 度将使相关标签无效。运行以下代码示例rand_augment具有有限的增强集。

from nvidia.dali.auto_aug import augmentations as a

augmentations = [
    a.shear_x.augmentation((0, 0.3), randomly_negate=True),
    a.shear_y.augmentation((0, 0.3), randomly_negate=True),
    a.translate_x.augmentation((0, 0.45), randomly_negate=True),
    a.translate_y.augmentation((0, 0.45), randomly_negate=True),
    a.rotate.augmentation((0, 30), randomly_negate=True),
]

每个增强都可以通过幅度如何映射到变换强度来参数化。例如a.rotate.augmentation((0, 30))指定要将图像旋转不大于 30 度的角度。randomly_negate=True指定角度应随机取反,以便随机顺时针或逆时针旋转图像。

下面的代码示例以类似 RandAugment 的方式应用增强。

@pipeline_def(batch_size=8, num_threads=4, device_id=0,
              enable_conditionals=True)
def pipeline():
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True)
    shape = fn.peek_image_shape(encoded)
    image = fn.decoders.image(encoded, device="mixed")
    distorted = ra.apply_rand_augment(augmentations, image, n=3, m=15, shape=shape, fill_value=0)
    return distorted

前两个管道定义之间的唯一区别是使用了更通用的apply_rand_augment接受附加参数的运算符,即扩充列表。

接下来,将自定义扩充添加到集合中。使用cutout作为一个例子。它使用 DALI 用一个归零的矩形随机覆盖图像的一部分fn.erase作用包fn.erase@augmentation描述如何将幅度映射到cutout矩形。cutout_size是从 0 . 01 到 0 . 4 范围的大小的元组,而不是普通大小。

from nvidia.dali.auto_aug.core import augmentation

def cutout_shape(size):
    # returns the shape of the rectangle
    return [size, size]

@augmentation(mag_range=(0.01, 0.4), mag_to_param=cutout_shape)
def cutout(image, cutout_size, fill_value=None):
    anchor = fn.random.uniform(range=[0, 1], shape=(2,))
    return fn.erase(image, anchor=anchor, shape=cutout_size, normalized=True, centered_anchor=True, fill_value=fill_value)

augmentations += [cutout]

对于更改,运行一组自定义的几何增强,如TrivialAugment,即具有随机幅度。对代码的更改是最小的;您导入并调用trivial_augment而不是rand_augment来自aut_aug单元

import nvidia.dali.auto_aug.trivial_augment as ta

@pipeline_def(batch_size=8, num_threads=4, device_id=0,
              enable_conditionals=True)
def pipeline():
    encoded, _ = fn.readers.file(file_root=data_path, random_shuffle=True)
    shape = fn.peek_image_shape(encoded)
    image = fn.decoders.image(encoded, device="mixed")
    distorted = ta.apply_trivial_augment(augmentations, image, shape=shape, fill_value=0)
    return distorted
Eight images, each distorted with two random affine transformations. Some of the images are partially overlayed with black rectangles.
图 5 。跑步的效果TrivialAugment

图 5 显示了使用自定义几何增强和剪切集运行 TrivialAugment 的效果。

DALI 的自动增强性能

现在,插上 DALI 和AutoAugment进入模型训练并比较吞吐量,使用EfficientNet-b0例如,改编自NIVDIA Deep Learning Examples.AutoAugment是 EfficientNet 系列模型预处理阶段的标准部分。

在链接的示例中AutoAugment策略使用 PyTorch 数据加载器实现,并在 CPU 上运行,而模型训练在 GPU 上进行。当 DALI 管道替换在 CPU 上运行的数据加载器时,吞吐量会增加。 EfficientNet 加 DALI 的源代码可在DALI examples.

Bar plot comparing EfficientNet training speed with different data loaders: PyTorch CPU data loader, DALI GPU pipeline, and a theoretical speed without data loading.
图 6 。高效 Net-b0 训练性能(图像/秒,越多越好)

该模型在自动混合精度模式( AMP )下运行,批量大小: DGX-1 V100 为 128 , DGX A100 为 256 。

我们用两种硬件设置进行了实验: DGX-1 V100 16 GB 和 DGX A100 。我们测量了每秒处理的图像数量(越多越好)。在这两种情况下,速度都有所提高: DGX-1 V100 的速度提高了 33% , DGX A100 的速度增加了 12% 。

图中虚线所示的理论吞吐量是通过单独改进数据预处理可以预期的训练速度的上限。为了测量理论极限,我们使用在每次迭代中重复的一批合成数据而不是真实数据进行训练。这让我们看到了在不需要预处理的情况下,模型处理批次的速度有多快。

合成情况和 CPU 数据加载器情况之间的显著性能差距表明存在预处理瓶颈。为了验证这一假设,请查看训练期间 GPU 的使用情况。

Bar chart compares two variants with different data-preprocessing backends: a CPU PyTorch data loader vs. a GPU DALI pipeline.
图 7 。在 EfficientNet-b0 培训期间, DGX-1V 16GB 的 GPU 利用率增加

(批量大小 128 ,具有 DALI 数据预处理的自动混合精度模式)

Bar chart compares two variants with different data-preprocessing backends: a CPU PyTorch data loader vs. a GPU DALI pipeline.
图 8 。在 EfficientNet-b0 培训期间, DGX-A100 的 GPU 利用率增加

(批量大小 256 ,自动混合精度模式,带 DALI 数据预处理)

这些图显示了在给定的 GPU 利用率下我们花费了多少时间。您可以看到,当使用在 CPU 上运行的数据加载器对数据进行预处理时, GPU 的利用率会反复下降。值得注意的是,在大约 5% 的时间里,利用率下降到 10% 以下。这表明训练定期停滞,等待下一批数据从数据加载程序到达。

如果您将加载和自动增强步骤移动到带有 DALI 的 GPU0, 10条消失,并且整体 GPU 利用率增加。图 6 中显示的使用 DALI 的训练吞吐量的增加证实了我们成功地克服了之前的预处理瓶颈。

有关如何发现和解决数据加载瓶颈的更多信息,请参阅Case Study: ResNet-50 with DALI.

尝试使用 DALI 进行自动增强

您可以下载预构建和测试的最新版本DALI pip packages。您可以发现 DALI 集成为 NVIDIA NGC 容器的一部分,用于TensorFlow,PyTorch,PaddlePaddleNVIDIA Optimized Deep Learning Framework powered by Apache MXNet。 DALI Triton 后端是NVIDIA Triton Inference Server container.

有关 DALI 新功能和增强功能的更多信息,请参阅DALI User Guide examples以及最新的DALI release notes.

 

Tags