图片增广

“深度卷积神经网络:AlexNet”小节里我们提到过,大规模数据集是成功使用深度网络的前提。图片增广(image augmentation)技术通过对训练图片做一系列随机变化,来产生相似但又有不同的训练样本,从而扩大训练数据集规模。图片增广的另一种解释是,通过对训练样本做一些随机变形,可以降低模型对某些属性的依赖,从而提高泛化能力。例如我们可以对图片进行不同的裁剪,使得感兴趣的物体出现在不同的位置中,从而使得模型减小对物体出现位置的依赖性。也可以调整亮度色彩等因素来降低模型对色彩的敏感度。在 AlexNet 的成功中,图片增广技术功不可没。本小节我们将讨论这个在计算机视觉里被广泛使用的技术。

首先,导入本节实验所需的包或模块。

In [1]:
import sys
sys.path.insert(0, '..')

%matplotlib inline
import gluonbook as gb
import mxnet as mx
from mxnet import autograd, gluon, image, init, nd
from mxnet.gluon import data as gdata, loss as gloss, utils as gutils
import sys
from time import time

常用增广方法

我们先读取一张 \(400\times 500\) 的图片作为样例。

In [2]:
gb.set_figsize()
img = image.imread('../img/cat1.jpg')
gb.plt.imshow(img.asnumpy())
Out[2]:
<matplotlib.image.AxesImage at 0x7fa9829e0160>
../_images/chapter_computer-vision_image-augmentation_3_1.svg

下面定义绘图函数show_images。该函数也被定义在gluonbook包中供后面章节调用。

In [3]:
def show_images(imgs, num_rows, num_cols, scale=2):
    """Plot a list of images."""
    figsize = (num_cols * scale, num_rows * scale)
    _, axes = gb.plt.subplots(num_rows, num_cols, figsize=figsize)
    for i in range(num_rows):
        for j in range(num_cols):
            axes[i][j].imshow(imgs[i * num_cols + j].asnumpy())
            axes[i][j].axes.get_xaxis().set_visible(False)
            axes[i][j].axes.get_yaxis().set_visible(False)
    return axes

因为大部分的增广方法都有一定的随机性。接下来我们定义一个辅助函数,它对输入图片img运行多次增广方法aug并显示所有结果。

In [4]:
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
    Y = [aug(img) for _ in range(num_rows * num_cols)]
    show_images(Y, num_rows, num_cols, scale)

变形

左右翻转图片通常不改变物体的类别,它是最早也是最广泛使用的一种增广。下面我们使用 transform 模块里的RandomFlipLeftRight类来实现按 0.5 的概率左右翻转图片:

In [5]:
apply(img, gdata.vision.transforms.RandomFlipLeftRight())
../_images/chapter_computer-vision_image-augmentation_9_0.svg

上下翻转不如水平翻转通用,但是至少对于样例图片,上下翻转不会造成识别障碍。

In [6]:
apply(img, gdata.vision.transforms.RandomFlipTopBottom())
../_images/chapter_computer-vision_image-augmentation_11_0.svg

我们使用的样例图片里,猫在图片正中间,但一般情况下可能不是这样。“池化层”一节里我们解释了池化层能弱化卷积层对目标位置的敏感度,另一方面我们可以通过对图片随机剪裁来让物体以不同的比例出现在不同位置。

下面代码里我们每次随机裁剪一片面积为原面积 10% 到 100% 的区域,其宽和高的比例在 0.5 和 2 之间,然后再将高宽缩放到 200 像素大小。

In [7]:
shape_aug = gdata.vision.transforms.RandomResizedCrop(
    (200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
../_images/chapter_computer-vision_image-augmentation_13_0.svg

颜色变化

另一类增广方法是变化颜色。我们可以从四个维度改变图片的颜色:亮度、对比、饱和度和色相。在下面的例子里,我们将随机亮度改为原图的 50% 到 150%。

In [8]:
apply(img, gdata.vision.transforms.RandomBrightness(0.5))
../_images/chapter_computer-vision_image-augmentation_15_0.svg

类似的,我们可以修改色相。

In [9]:
apply(img, gdata.vision.transforms.RandomHue(0.5))
../_images/chapter_computer-vision_image-augmentation_17_0.svg

或者用使用RandomColorJitter来一起使用。

In [10]:
color_aug = gdata.vision.transforms.RandomColorJitter(
    brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
../_images/chapter_computer-vision_image-augmentation_19_0.svg

使用多个增广

实际应用中我们会将多个增广叠加使用。Compose类可以将多个增广串联起来。

In [11]:
augs = gdata.vision.transforms.Compose([
    gdata.vision.transforms.RandomFlipLeftRight(), color_aug, shape_aug])
apply(img, augs)
../_images/chapter_computer-vision_image-augmentation_21_0.svg

使用图片增广来训练

接下来我们来看一个将图片增广应用在实际训练中的例子,并比较其与不使用时的区别。这里我们使用 CIFAR-10 数据集,而不是之前我们一直使用的 Fashion-MNIST。原因在于 Fashion-MNIST 中物体位置和尺寸都已经归一化了,而 CIFAR-10 中物体颜色和大小区别更加显著。下面我们展示 CIFAR-10 中的前 32 张训练图片。

In [12]:
show_images(gdata.vision.CIFAR10(train=True)[0:32][0], 4, 8, scale=0.8);
../_images/chapter_computer-vision_image-augmentation_23_0.svg

我们通常将图片增广用在训练样本上,但是在预测的时候并不使用随机增广。这里我们仅仅使用最简单的随机水平翻转。此外,我们使用ToTensor变换来将图片转成 MXNet 需要的格式,即格式为(批量,通道,高,宽)以及类型为 32 位浮点数。

In [13]:
train_augs = gdata.vision.transforms.Compose([
    gdata.vision.transforms.RandomFlipLeftRight(),
    gdata.vision.transforms.ToTensor(),
])

test_augs = gdata.vision.transforms.Compose([
    gdata.vision.transforms.ToTensor(),
])

接下来我们定义一个辅助函数来方便读取图片并应用增广。Gluon 的数据集提供transform_first函数来对数据里面的第一项(数据一般有图片和标签两项)来应用增广。另外图片增广将增加计算复杂度,这里使用 4 个进程来加速读取(暂不支持 Windows 操作系统)。

In [14]:
num_workers = 0 if sys.platform.startswith('win32') else 4
def load_cifar10(is_train, augs, batch_size):
    return gdata.DataLoader(
        gdata.vision.CIFAR10(train=is_train).transform_first(augs),
        batch_size=batch_size, shuffle=is_train, num_workers=num_workers)

使用多 GPU 训练模型

我们在 CIFAR-10 数据集上训练“残差网络:ResNet”一节介绍的 ResNet-18 模型。我们将应用“多 GPU 计算的 Gluon 实现”一节中介绍的方法,使用多 GPU 训练模型。

首先,我们定义try_all_gpus函数,从而能够使用所有可用的 GPU。

In [15]:
def try_all_gpus():
    ctxes = []
    try:
        for i in range(16):
            ctx = mx.gpu(i)
            _ = nd.array([0], ctx=ctx)
            ctxes.append(ctx)
    except:
        pass
    if not ctxes:
        ctxes = [mx.cpu()]
    return ctxes

然后,我们定义evaluate_accuracy函数评价模型的分类准确率。与“Softmax 回归的从零开始实现”“卷积神经网络(LeNet)”两节中描述的evaluate_accuracy函数不同,当ctx包含多个 GPU 时,这里定义的函数通过辅助函数_get_batch将小批量数据样本划分并复制到各个 GPU 上。

In [16]:
def _get_batch(batch, ctx):
    features, labels = batch
    if labels.dtype != features.dtype:
        labels = labels.astype(features.dtype)
    # 当 ctx 包含多个 GPU 时,划分小批量数据样本并复制到各个 GPU 上。
    return (gutils.split_and_load(features, ctx),
            gutils.split_and_load(labels, ctx),
            features.shape[0])

def evaluate_accuracy(data_iter, net, ctx=[mx.cpu()]):
    if isinstance(ctx, mx.Context):
        ctx = [ctx]
    acc = nd.array([0])
    n = 0
    for batch in data_iter:
        features, labels, _ = _get_batch(batch, ctx)
        for X, y in zip(features, labels):
            y = y.astype('float32')
            acc += (net(X).argmax(axis=1)==y).sum().copyto(mx.cpu())
            n += y.size
        acc.wait_to_read()
    return acc.asscalar() / n

接下来,我们定义train函数使用多 GPU 训练并评价模型。

In [17]:
def train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs):
    print('training on', ctx)
    if isinstance(ctx, mx.Context):
        ctx = [ctx]
    for epoch in range(1, num_epochs + 1):
        train_l_sum, train_acc_sum, n, m = 0.0, 0.0, 0.0, 0.0
        start = time()
        for i, batch in enumerate(train_iter):
            Xs, ys, batch_size = _get_batch(batch, ctx)
            ls = []
            with autograd.record():
                y_hats = [net(X) for X in Xs]
                ls = [loss(y_hat, y) for y_hat, y in zip(y_hats, ys)]
            for l in ls:
                l.backward()
            train_acc_sum += sum([(y_hat.argmax(axis=1) == y).sum().asscalar()
                                 for y_hat, y in zip(y_hats, ys)])
            train_l_sum += sum([l.sum().asscalar() for l in ls])
            trainer.step(batch_size)
            n += batch_size
            m += sum([y.size for y in ys])
        test_acc = evaluate_accuracy(test_iter, net, ctx)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
              'time %.1f sec'
              % (epoch, train_l_sum / n, train_acc_sum / m, test_acc,
                 time() - start))

现在,我们可以定义函数使用图片增广来训练模型了。

In [18]:
def train_with_data_aug(train_augs, test_augs, lr=0.001):
    batch_size = 256
    ctx = try_all_gpus()
    net = gb.resnet18(10)
    net.initialize(ctx=ctx, init=init.Xavier())
    # 这里使用了 Adam 优化算法。
    trainer = gluon.Trainer(net.collect_params(), 'adam',
                            {'learning_rate': lr})
    loss = gloss.SoftmaxCrossEntropyLoss()
    train_iter = load_cifar10(True, train_augs, batch_size)
    test_iter = load_cifar10(False, test_augs, batch_size)
    train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs=15)

我们先观察使用了图片增广的结果。

In [19]:
train_with_data_aug(train_augs, test_augs)
training on [gpu(0), gpu(1)]
epoch 1, loss 1.4407, train acc 0.494, test acc 0.539, time 37.0 sec
epoch 2, loss 0.8541, train acc 0.698, test acc 0.651, time 35.0 sec
epoch 3, loss 0.6333, train acc 0.778, test acc 0.767, time 35.1 sec
epoch 4, loss 0.5039, train acc 0.825, test acc 0.798, time 35.0 sec
epoch 5, loss 0.4173, train acc 0.858, test acc 0.807, time 35.0 sec
epoch 6, loss 0.3537, train acc 0.878, test acc 0.803, time 35.2 sec
epoch 7, loss 0.2953, train acc 0.896, test acc 0.819, time 35.4 sec
epoch 8, loss 0.2480, train acc 0.914, test acc 0.833, time 35.2 sec
epoch 9, loss 0.2125, train acc 0.926, test acc 0.838, time 35.3 sec
epoch 10, loss 0.1822, train acc 0.937, test acc 0.836, time 35.1 sec
epoch 11, loss 0.1541, train acc 0.948, test acc 0.846, time 35.0 sec
epoch 12, loss 0.1276, train acc 0.957, test acc 0.841, time 35.0 sec
epoch 13, loss 0.1164, train acc 0.959, test acc 0.837, time 35.1 sec
epoch 14, loss 0.0969, train acc 0.967, test acc 0.845, time 35.2 sec
epoch 15, loss 0.0864, train acc 0.969, test acc 0.838, time 35.1 sec

作为对比,下面我们尝试不使用图片增广。

In [20]:
train_with_data_aug(test_augs, test_augs)
training on [gpu(0), gpu(1)]
epoch 1, loss 1.4530, train acc 0.486, test acc 0.502, time 35.5 sec
epoch 2, loss 0.8570, train acc 0.695, test acc 0.709, time 35.0 sec
epoch 3, loss 0.6114, train acc 0.785, test acc 0.655, time 35.1 sec
epoch 4, loss 0.4536, train acc 0.843, test acc 0.772, time 35.1 sec
epoch 5, loss 0.3385, train acc 0.882, test acc 0.776, time 35.1 sec
epoch 6, loss 0.2476, train acc 0.912, test acc 0.802, time 35.0 sec
epoch 7, loss 0.1785, train acc 0.937, test acc 0.767, time 35.1 sec
epoch 8, loss 0.1237, train acc 0.958, test acc 0.790, time 35.1 sec
epoch 9, loss 0.0988, train acc 0.965, test acc 0.809, time 35.3 sec
epoch 10, loss 0.0751, train acc 0.973, test acc 0.797, time 35.2 sec
epoch 11, loss 0.0775, train acc 0.973, test acc 0.806, time 35.0 sec
epoch 12, loss 0.0601, train acc 0.979, test acc 0.808, time 35.1 sec
epoch 13, loss 0.0463, train acc 0.984, test acc 0.826, time 35.0 sec
epoch 14, loss 0.0465, train acc 0.984, test acc 0.823, time 35.3 sec
epoch 15, loss 0.0432, train acc 0.985, test acc 0.822, time 35.0 sec

可以看到,即使添加了简单的随机翻转也会对训练产生一定的影响。图片增广通常会使训练准确率变低,但有可能提高测试准确率。

本节中描述的try_all_gpusevaluate_accuracytrain函数被定义在gluonbook包中供后面章节调用。

小结

  • 图片增广基于现有训练数据生成大量随机图片来有效避免过拟合。

练习

  • 尝试在 CIFAR-10 训练中增加不同的增广方法。

扫码直达讨论区