4.4 数据增广

🏷sec0404_Datasets_DataAugumentation

4.4.1 数据增广概述

在前面的内容中我们介绍过,大规模数据集是成功应用深度神经网络的前提。一种普遍的观点认为,神经网络模型是用数据喂出来的,也就是说神经网络是一类数据驱动型的模型。只要能够提供足够的训练数据,它就能够有效地避免模型的过拟合问题,从而提高算法最终的准确率。特别是当模型深度和数据量同时获得提升的时候,这种收益会更明显。然而,获取真实的数据总是非常困难和昂贵的。因此,当训练数据有限时,数据增广就变成一种最有效的扩充数据的方法。在计算机视觉中,数据增广(Data Augmentation) 技术主要指的是通过对训练图像做一系列随机改变,来产生相似但又不同的训练样本,从而扩大训练数据集的规模。

那么,为什么要使用数据增广呢?这种技术到底有什么用,它解决了什么问题?我们以 7.1节 将要介绍的AlexNet模型为例。在AlexNet模型中,总共有6100万的参数和65万的神经元,这个量非常庞大。那么数据样本有多少呢?以大规模的ImageNet数据集为例,它包含128万的训练样本。一百多万的训练数据听起来还是挺多的,但是与AlexNet的参数量比起来,还是远远不够多。假设我们使用AlexNet去解决前面两小节所介绍的比ImageNet更小的Zodiac数据集的分类问题,那么数据量就显得更少了。此时,由于参数数量远多于样本数量,模型就会更加致力于去拟合仅有的训练数据,而不会去关注数据类本来应该具有的共有模式。这就是我们常说的过拟合问题。所以,图像增广的另一种解释是,它能够随机改变训练样本以降低模型对某些属性的依赖,从而提高模型的泛化能力。例如,我们可以对图像进行不同方式的裁剪,使感兴趣的物体出现在样本的不同像素位置,从而减轻模型对物体出现位置的依赖性。我们也可以调整亮度、色彩等因素来降低模型对颜色的敏感度。也就是说,数据增广技术能够降低模型对样本特定模式的依赖性。换句话说,越复杂的模型,越容易产生过拟合问题。特别是当数据样本比较少,但是难度相对较大时,模型很难从训练样本中学到特定类别的普遍属性,而是更关注这些训练样本的细节问题。这就导致了,当新的测试样本出现的时候,模型很难去抓住新样本的类别特征,从而导致错判。可以说,当年的深度神经网络AlexNet之所以能成功,数据增广技术功不可没。本节我们将讨论这个在计算机视觉里被广泛应用的技术。

首先,我们导入本小节所需要的包和模块。

程序清单4-17 数据增广需要的包和模块
# codes04017_data_augumentation_default_parameters
import cv2
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import os
import random
import math
import paddle.vision as vision
import paddle.vision.transforms as transforms
from paddle.vision.transforms import functional as F
import sys
sys.path.append(r'D:\WorkSpace\DeepLearning\WebsiteV2')   # 定义自定义模块保存位置
from codes.paddle import common
plt.rcParams['font.sans-serif'] = ['simhei']
plt.rcParams['font.size'] = 14
vision.set_image_backend('cv2')

4.4.2 图像的载入与显示

在介绍面向图像的数据增广技术之前,让我们先来看看图像的载入和显示。在python中,最常用的图像处理和操作库有两个,分别是Python图形库(Python Imaging Library, PIL)和开源计算机视觉库(OpenSource Computer Vision, OpenCV)。这两个库都可以实现大多数的图像变换操作,但它们也有一些区别。

相较而言对于简单的图像操作PIL是不错的选择,因为其代码简单,易于实现;而当需要同时处理图像、视频,或者使用更高级功能时,OpenCV是不二的选择。此外在大多数相同操作中,OpenCV的执行效率要更高一些,因此在批量处理时,OpenCV速度也更快。本书将主要使用OpenCV库作为默认的第三方图像开发处理库。

4.4.2.1 图像载入

本书所使用的Paddle机器学习开发包同时支持PIL库和OpenCV库两种图像处理背板。下面,我们简单介绍一下这两种库的区别。

此外,在Paddle开发工具包中还提供了一个数据预处理库。在使用Paddle进行编程时,可以直接调用paddle.vision.transforms库来对图像进行数据预处理和图像增广操作。该库提供了简易的数据接口,可以大大提高编程的效率。通过设置 paddle.vision 视觉库中的 set_image_backend() 参数,可以切换其默认启用 PILcv2 两种数据结构中的一种。不做设置时,默认的处理背板为PIL库。

小贴士:OpenCV为什么使用BRG格式的色彩通道

OpenCV由Intel公司于1999年创建,旨在为其处理器提供特定的优化,并于2000年6月发布了第一个版本OpenCV alpha 3。在当时,主流的相机厂商和软件供应商所提供的相机采集图像的色彩通道顺序为BRG。此外,作为当年Windows操作系统最简单、最基本的位图图像格式BMP所支持的默认色彩通道也是BGR。因此,早期OpenCV的开发人员也将图像的默认通道设定为BGR格式,即按照蓝色(Blue)、绿色(Green)和红色(Red)的顺序组合三原色)。该标准一直延续至今。
由于Paddle和Caffe等深度学习开发包底层的图像处理引擎使用的都是OpenCV,因此在使用这些开发包时,我们也必须将输入图像的色彩通道设置为OpenCV默认的BRG格式。

下面我们读取一张分辨率为1299×563(高和宽分别为1299像素和563像素)的图像作为实验的样例,我们将分别使用Paddle、PIL和cv2三种模式对图像进行读取,并显示它们转换为RGB前后的样子。其中PIL图像默认就是RGB色彩通道,因此不需要再手动对其进行色彩通道转换。

程序清单4-18 使用Paddle、PIL和cv2三种模式进行图像载入
# codes04018_load_image
# 1. 设置图片路径
img_dir = r'..\..\Images\Materials\chapter04Datasets'
img_name = 'chapter04008AugmentationExampleRIO.jpg'
img_path = os.path.join(img_dir, img_name)

# 2. 图像载入
img_paddle = paddle.vision.image_load(img_path)  # Paddle内置模式
img_PIL = Image.open(img_path)                   # PIL模式
img_cv2 = cv2.imread(img_path, 1)                # cv2模式

# 2. 图像通道转换
img_paddle_rgb = cv2.cvtColor(img_paddle, cv2.COLOR_BGR2RGB)
img_cv2_rgb = cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB) 

4.4.2.2 图像显示

python的图像显示包含两种模式,一种是基于Notebook的内嵌式显示,另一种是独立显示模式。前者会在Notebook页面内生成一个内嵌框,然后在该框体内显示图形图像;后者会在操作系统中打开一个新的窗体用于显示图像。下面我们分别使用这两种模式来进行显示。

  1. 基于Notebook的内嵌式显示
程序清单4-19 以Notebook内嵌模式显示三种接口转换成RGB通道前后的图像
# codes04019_show_image_notebook
title = ['img_PIL', 'img_paddle', 'img_cv2', 'img_paddle_rgb', 'img_cv2_rgb']
plt.figure(figsize=(24, 10)) 
for i in range(len(title)):
    ax = plt.subplot(1,5,i+1)
    ax.set_title(title[i])
    plt.imshow(eval(title[i]))
显示三种接口模式下的图像
  1. 独立显示模式
程序清单4-20 以非Notebook内嵌模式显示图像
# codes04020_show_image_os
img_PIL.show()
cv2.imshow('img_paddle', img_paddle)
cv2.imshow('img_paddle_rgb', img_paddle_rgb)
cv2.imshow('img_cv2', img_cv2)
cv2.imshow('img_cv2_rgb', img_cv2_rgb)
cv2.waitKey(0)

在独立显示模式下,系统会自动打开一个操作系统本地的图片播放器来显示图像。在这种模式下图片会以默认的尺度来进行显示,不会进行尺度压缩。而在Notebook内嵌模式下,程序一般会根据页面的情况进行自动压缩。此外,我们也可以使用 plt.figure(figsize=()) 函数来指定显示窗口的尺度。在本书中,为了方便展示,后面的内容都以Notebook内嵌模式来显示图片。

从上面的实验结果,不难看出。当我们对Paddle使用 cv2 背板时,无论是Paddle模式还是cv2模式,其显示结果都是完全一样。它们的数据结构都是默认的Numpy数组,其色彩空间为BGR模式,这种形式更有利于大多数的图像操作。而当我们需要进行可视化的时候,将其转换为RGB模式会更加符合视觉习惯一些,否则会因为通道解释错误而产生一定的色偏。

4.4.3 图像的基本变换

图像变换是数字图像处理的常见操作,在深度学习的任务中,主要包含:水平翻转、垂直翻转、缩放、旋转、色彩变换、随机裁剪等。为了简化教程,本书后面的内容仅使用cv2库和paddle接口对图像进行处理。在探索常用的图像图像变换方法时,我们依然使用前一小节中的图片作为示例,注意观察下面可视化结果的坐标轴,其分辨率为默认的1299×563。

程序清单4-21 读取图像文件
# codes04021_load_image
img_dir = r'..\..\Images\Materials\chapter04Datasets'
img_name = 'chapter04008AugmentationExampleRIO.jpg'
img_path = os.path.join(img_dir, img_name)

# 2. 图像载入
img = cv2.imread(img_path, 1)         # cv2模式
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
原始图像

由于图像增广方法通常都是使用随机的超参数,为了便于观察图像增广的效果,我们定义了一个辅助的可视化函数 draw_aug_images。借助该函数,我们在输入图像img上进行多次运行以显示图像增广算法的不同随机效果。在函数中,我们定义了一个methods参数,用于对比显示基于OpenCV和基于paddle的变换效果差异。methods参数所关联函数名称,对应于后面toolkits参数中的值,默认为cv2和paddle。该方法写入本书的通用函数库中,可以通过 common.draw_aug_images() 进行直接调用。

程序清单4-22 显示图像增广后的图像
#@save common.draw_aug_images @TODO_Codes04022
def draw_aug_images(img=None, methods=None, type_name=None, toolkits=['cv2', 'paddle'], num_out=4, scale=5):
    """绘制数据增广的示例图,其中第一个图例为原图,第一行第二个图例开始为第一种方法的变换图,第二行第二个图例为第二种方法的变换图"""
    plt.figure(figsize=(5*scale, 1.5*scale))
    toolkit = ['cv2', 'paddle']
    list_id = [1]
    for i in range(1, len(methods)+1):
        list_id = list_id + list(range((i-1)*num_out+(i+1), (i-1)*num_out+(i+1)+num_out))
    for i in list_id:
        ax = plt.subplot(2, num_out+1, i)
        if i == 1:
            ax.set_title('原图')
            img_ori = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            plt.imshow(img_ori)
        elif i in list(range(2, num_out+2)):
            ax.set_title(type_name + toolkits[0])
            fake_img = methods[0](img)
            fake_img = cv2.cvtColor(fake_img, cv2.COLOR_BGR2RGB)
            plt.imshow(fake_img)
        elif i in list(range(num_out+3, 2*(num_out+1)+1)):
            ax.set_title(type_name + toolkits[1])
            fake_img = methods[1](img)
            fake_img = cv2.cvtColor(fake_img, cv2.COLOR_BGR2RGB)
            plt.imshow(fake_img)

4.4.2.1 随机水平翻转图片

图像翻转包括水平翻转和垂直翻转,其中水平翻转图像通常不会改变图像的类别,但是垂直翻转在实际应用中可能会影响上下文的真实性。接下来,我们使用OpenCV手工定义了一个水平翻转的函数 horizontal_flip_image,同时通过调用transforms模块创建了一个内置的水平翻转实例RandomHorizontalFlip。两种方法都按照50%的概率使图像向左或向右进行翻转。注意观察坐标轴,水平翻转并不改变图像的尺度。

程序清单4-23 随机水平翻转
# codes04023_RandomHorizontalFlip
# 1. CV2接口:按照50%的概率执行左右翻转
def horizontal_flip_image(img, prob=0.5):
    p = np.random.random()
    if p < prob:
        img = cv2.flip(img, 1)
    return img

# 2. Paddle接口:调用Paddle内置随机水平翻转接口
transform = transforms.RandomHorizontalFlip(prob=0.5)

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[horizontal_flip_image, transform], 
                    type_name='随机水平翻转', 
                    toolkits=['cv2', 'paddle'])
随机水平翻转

小贴士

注意数据增广的概率是一个基于大数据的统计值,并不能保证在输入固定样本数量时,完全按照指定的概率进行执行。例如,在以上的可视化图中,50%的概率并不表示执行水平翻转的图片数量永远都是2张。每次执行时,数量可能大于也可能小于2张。

4.4.2.2 随机垂直翻转图片

垂直翻转图像不像水平翻转那么常用,主要是因为垂直翻转可能产生失真的问题。例如一幅包含站立的人的图片,如果经过垂直翻转后,将会头朝下,这显然是不符合实际的。不过,做为示例图像,垂直翻转也是一种有效的增广方法。接下来,我们依然分别使用OpenCV和Paddle定义两个同样功能的垂直翻转函数来实现50%的概率向上或向下翻转。

程序清单4-24 随机垂直翻转
# codes04024_RandomVerticalFlip
# 1. CV2接口:按照50%的概率执行垂直翻转
def vertical_flip_image(img, prob=0.5):
    p = np.random.random()
    if p < prob:
        img = cv2.flip(img, 0)
    return img

# 2. Paddle接口:调用Paddle内置随机水平翻转接口
transform = transforms.RandomVerticalFlip(prob=0.5)

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[vertical_flip_image, transform], 
                    type_name='随机垂直翻转', 
                    toolkits=['cv2', 'paddle'])
随机垂直翻转

4.4.2.3 图像缩放

在训练过程中图像缩放通常和随机位置裁剪混合使用,实现不同位置、不同尺度、不同比例的多样性采样。而在测试过程中,通常又会要求直接将图像缩放到模型规定的尺度。下面我们先实现按照固定尺寸进行缩放。

程序清单4-25 图像缩放
# codes04025_Resize
# 1. CV2接口:执行图像缩放
def resize_image(img, imgSize=(224,224)):
    img = cv2.resize(img, imgSize)
    return img
    
# 2. Paddle接口:调用Paddle内置随机图像缩放接口
transform = transforms.Resize([224,224])

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[resize_image, transform], 
                    type_name='图像缩放', 
                    toolkits=['cv2', 'paddle'])
图像缩放

4.4.2.4 随机裁剪

在前面三个小节的示例图像中,金刚鹦鹉们始终位于图像的中央位置,但并非所有的图像都是这样的。在6.5节中,我们解释了汇聚层可以缓解卷积层对目标位置的敏感性。随机裁剪是另外一种降低模型对目标位置敏感性的有效方法。因为对图像的随机裁剪可以使物体以不同比例出现在图像的不同位置。随机裁剪可以认为是随机缩放和随机位置切割的混合,该方法先对原始图进行一定尺度的缩放,然后在随机位置进行不同长宽比的切割,最后再将切片缩放到统一的尺度,例如:[224,224]。下面的代码将随机裁剪一个面积为原始面积10%到100%的区域,该区域的高宽比从[0.75, 1.5]之间随机取值。最后再将该区域的宽度和高度都被缩放到224像素。在本书中,aabb之间的随机数指的是在区间[a,b][a,b]中通过均匀采样获得的连续值。

程序清单4-26 随机裁剪
# codes04026_RandomResizedCrop
# 1. CV2接口:按照一定的比例和尺度进行随机裁剪
def random_crop_img(img, crop_size=(224,224), scale=[0.1, 1.0], ratio=[0.75, 1.5]):
    aspect_ratio = math.sqrt(np.random.uniform(*ratio))
    w = 1. * aspect_ratio
    h = 1. / aspect_ratio

    bound = min((float(img.shape[1]) / img.shape[0]) / (w**2),
                (float(img.shape[0]) / img.shape[1]) / (h**2))
    scale_max = min(scale[1], bound)
    scale_min = min(scale[0], bound)

    target_area = img.shape[1] * img.shape[0] * np.random.uniform(scale_min, scale_max)
    target_size = math.sqrt(target_area)
    w = int(target_size * w)
    h = int(target_size * h)

    i = np.random.randint(0, img.shape[1] - w + 1)
    j = np.random.randint(0, img.shape[0] - h + 1)

    img = img[i:i+w, j:j+h]
    img = cv2.resize(img, crop_size, Image.BILINEAR)
    return img
    
# 2. Paddle接口:调用Paddle内置随机图像缩放接口
transform = transforms.RandomResizedCrop((224,224))

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[random_crop_img, transform], 
                    type_name='随机裁剪', 
                    toolkits=['cv2', 'paddle'])
随机裁剪

4.4.2.5 随机旋转

随机旋转分为2D旋转和3D旋转,对于常用的平面样本一般只需要使用2D旋转即可。2D旋转包括顺时针或逆时针旋转,并且旋转的幅度一般不能太大,15-20度是一个常用的阈值空间。下面的两个函数分别通过OpenCV和Paddle工具包实现[-15, 15]度之间的随机2D旋转。

程序清单4-27 随机旋转
# codes04027_RandomRotation
# 1. CV2接口:按照50%的概率执行随机旋转
def random_rotate_image(img, angle=15):
    # 将图片随机旋转-14到15之间的某一个角度
    angle = np.random.randint(-angle, angle)

    #获取图像的尺寸
    #旋转中心
    h, w = img.shape[0], img.shape[1]
    cx,cy = w/2,h/2

    #设置旋转矩阵
    M = cv2.getRotationMatrix2D((cx,cy),-angle,1.0)
    cos = np.abs(M[0,0])
    sin = np.abs(M[0,1])

    # 计算图像旋转后的新边界
    nW = int((h*sin)+(w*cos))
    nH = int((h*cos)+(w*sin))

    # 调整旋转矩阵的移动距离(t_{x}, t_{y})
    M[0,2] += (nW/2) - cx
    M[1,2] += (nH/2) - cy

    fake_img = cv2.warpAffine(img,M,(nW,nH))    
    fake_img = cv2.resize(fake_img, (img.shape[1], img.shape[0]))
    return fake_img
    
# 2. Paddle接口:调用Paddle内置随机旋转接口
transform = transforms.RandomRotation(15)

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[random_rotate_image, transform], 
                    type_name='随机旋转', 
                    toolkits=['cv2', 'paddle'])
随机旋转

4.4.2.6 色彩增强

除了形态变换,色彩变换是另一类常用的数据增广方法。图像的色彩变换一般包括四种:亮度(brightness)、对比度(contrast)、色度(hue)和饱和度(saturation)。在OpenCV中,亮度和对比度可以使用同一个公式进行变换,色度和饱和度也可以使用同一个公式进行变换。下面分别给出基于OpenCV的实现方法和基于Paddle高层接口的实现方法。

1. 基于OpenCV的实现

(1)亮度和对比度

我们可以利用公式 g(x)=αf(x)+βg(x)=\alpha f(x) + \beta 来实现图像对比和亮度的同时调节,其中 α(0,255]\alpha \in (0,255] 表示对比度,β[255,255]\beta \in [-255,255] 表示亮度。其中 α=1,β=0\alpha=1, \beta=0 时,图像保持不变。值得注意的是在进行亮度和对比度调节的时候,要注意像素值不能过曝,如果像素值 “<255” 或 “>255”,则色彩会产生奇怪的现象。

程序清单4-28 使用OpenCV按50%的比例随机调整随机亮度和对比度
# codes04028_random_bright_contrast
# 1. CV2接口:按50%的比例进行随机亮度和对比度调整
def random_bright_contrast(img, brightness_beta = [-10, 10], contrast_alpha = [0, 2]):
    alpha = np.random.uniform(contrast_alpha[0], contrast_alpha[1])
    beta = np.random.uniform(brightness_beta[0], brightness_beta[1])
    img = img.astype('float32')
    img = np.clip((alpha*img + beta), 0, 255)
    img = img.astype('uint8')
    return img

# 2. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[random_bright_contrast], 
                    type_name='随机亮度和对比度调整', 
                    toolkits=['cv2'])
随机亮度和对比度调整
(2)色度和饱和度

HSV(Hue, Saturation, Value)是一种根据颜色的直观特性(由A. R. Smith在1978年创建)建立的颜色空间, 也称六角锥体模型(Hexcone Model)。在这个模型中颜色的参数分别是:色度(H),饱和度(S),明度(V)。

值得注意的是,RGB和CMYK颜色模型都是面向硬件的,而HSV(Hue Saturation Value)颜色模型是面向用户的。

在python中,我们可以使用numpy数组来描述一幅使用HSV颜色空间的图像。对于图像 A[640,480,3] 的三个像素值,其第三个维度分别表示HSV的三个参数 ,其中A[:,:,0]是色度Hue, A[:,:,1]是饱和度Saturation,A[:,:,2]是明度Value。在进行色度和饱和度调整的时候,同样要注意像素值的过曝问题。

值得注意的是,RGB和CMYK颜色模型都是面向硬件的,而HSV(Hue Saturation Value)颜色模型是面向用户的。在python中,我们可以使用numpy数组来描述一幅使用HSV颜色空间表示对图像。对于图像 A[640,480,3]的图像,其第三个维度表示的就是HSV的三个参数 ,其中A[:,:,0]是色度Hue, A[:,:,1]是饱和度Saturation,A[:,:,2]是明度Value。在进行色度和饱和度调整的时候,有两点需要注意。第一,像素值的过曝问题,所以我们需要使用一些办法来检测变换后的像素值是否超过了允许的值。此时,函数 np.where 是一个不错的选择。第二,与标准HSV颜色体系不同是,如果直接使用OpenCV中的cvtColor函数,并且设置参数为CV_BGR2HSV时,H、S、V的值范围是[0,180)、[0,255)、[0,255),而非标准的[0,360]、[0,1]、[0,1]。

以下是对色调和饱和度进行调整的Python代码示例:

程序清单4-29 使用OpenCV按50%的比例随机调整色度和饱和度
# codes04029_random_hue_saturation
# 1. CV2接口:随机色度和饱和度调整
def random_hue_saturation(img, hue_alpha=0.4, saturation_beta=0.4):     
    alpha = np.random.uniform(-hue_alpha, hue_alpha)
    beta = np.random.uniform(-saturation_beta, saturation_beta)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    hue = img[:, :, 0]
    sat = img[:, :, 1]
    hue = np.where(hue*(1+alpha) <= 180, hue*(1+alpha), 180) #  np.where(cond, exp_true, exp_false)
    sat = np.where(sat*(1+beta) <= 255., sat*(1+beta), 255)
    np.clip(hue, 0, 180)
    np.clip(sat, 0, 255)
    img[:, :, 0] = hue
    img[:, :, 1] = sat
    img = cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
    return img

# 2. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[random_hue_saturation], 
                    type_name='随机色度和饱和度调整', 
                    toolkits=['cv2'])
随机色度和饱和度调整

2. 基于Paddle高层接口的实现

在Paddle的高层接口中分别实现了四种图像变换的方法。为了简化代码,也可以使用封装好的高层API接口ColorJitter同时实现这四种色彩变换。其中亮度、对比度和饱和度按照给定参数value后的均匀分布 [max(0, 1-value), 1+value] 中进行随机选择,取值不能为负。色度从区间 [-hue, hue](0<=hue<=0.5)中进行随机选择。

程序清单4-30 使用Paddle的transforms模块实现色彩扰动
# codes04030_ColorJitter
# 1. 调用Paddle内置接口
transform = transforms.ColorJitter(brightness=0.4, 
                                contrast=0.4, 
                                saturation=0.4, 
                                hue=0.4)

# 2. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[transform], 
                    type_name='色彩扰动', 
                    toolkits=['paddle'])
色彩扰动

4.4.3 多种变换混合的图像增广

多种变换混合的图像增广包含两种方式,第一种是每张图片都选用一种图像变换的方法进行预处理;第二种是按照一定的概率同时应用多种图像变换方法。在使用多种方式混合的变换时,每个样本都会随机同时使用多种不同类型的图像变换方法,每种方法也会使用不同的超参数来调节图像变换的程度。在深度学习的数据预处理中,通常使用第二种方法进行随机混合。在下面的代码中,我们采取顺序执行的方法来组合多种图像增强。增强变换的顺序可以任意进行调整,但应注意不同的是,不同的顺序会对变换后的结果产生一定影响。在Paddle中,我们可以通过使用一个Compose实例来综合多种不同的图像增广方法,并将它们应用到每个图像。

程序清单4-31 多种变换混合的图像增广
# codes04031_multi_enhance
# 1. CV2接口:多种变换的混合变换函数
def multi_enhance(img):
    # 顺序可以自己随意调整
    img = random_crop_img(img, (224,224))
    img = horizontal_flip_image(img, prob=0.5)                # 随机左右翻转 
    img = vertical_flip_image(img, prob=0.5)
    img = random_rotate_image(img, angle=15)              # 随机旋转
    img = random_bright_contrast(img, 
                            brightness_alpha = [-10, 10], 
                            contrast_beta = [0, 2])    # 随机亮度对比度调整
    return img
    
# 2. Paddle接口:使用Paddle的tranforms.Compose接口定义多种混合增强函数transform
transform = transforms.Compose([
    transforms.Resize((256, 256)), 
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(prob=0.5),    
    transforms.RandomVerticalFlip(prob=0.5),
    transforms.RandomRotation(15),
    transforms.ColorJitter(brightness=0.4, 
                        contrast=0.4, 
                        saturation=0.4, 
                        hue=0.4)
])

# 3. 调用绘图函数绘制变换图
common.draw_aug_images(img, 
                    methods=[multi_enhance, transform], 
                    type_name='多种混合变换', 
                    toolkits=['cv2', 'paddle'])
多种混合变换

4.4.4 十重切割

为了确保模型预测的稳定性,在测试阶段通常不需要进行数据增广。但在AlexNet的论文中,作者提出了一种可以提高测试准确率的增广方法,这就是十重切割。该算法的基本步骤是:

  1. 将图像压缩成256×256的正方形;
  2. 按照224×224的尺度对压缩后的图像进行切割,分别获取左上角、右上角、左下角、右下角和中央五个位置以及这五个位置的水平翻转的样本;
  3. 将样本组合成一个4D张量[N=10, H, W, C=3]进行输出。

下面给出十重切割的定义函数。

程序清单4-32 定义十重切割函数
# @save train.TenCrop @TODO_Codes04032, project004
def TenCrop(img, crop_size=224):
    """AlexNet模型种定义的十重切割法,用于分类预测的测试过程"""
    # 实现从256像素的输入图像中切割出10个224×224的patch
    # input_data: Height x Width x Channel 
    img_size = 256
    img = F.resize(img, (img_size, img_size))
    blob = np.zeros([10, crop_size, crop_size, 3], dtype=np.uint8)
    
    # 获取左上、右上、左下、右下、中央,及其对应的翻转,共计10个样本
    blob[0] = F.crop(img,0,0,crop_size,crop_size)
    blob[1] = F.crop(img,0,img_size-crop_size,crop_size,crop_size)
    blob[2] = F.crop(img,img_size-crop_size,0,crop_size,crop_size)
    blob[3] = F.crop(img,img_size-crop_size,img_size-crop_size,crop_size,crop_size)
    blob[4] = F.center_crop(img, crop_size)
    blob[5] = F.hflip(blob[0, :, :, :])
    blob[6] = F.hflip(blob[1, :, :, :])
    blob[7] = F.hflip(blob[2, :, :, :])
    blob[8] = F.hflip(blob[3, :, :, :])
    blob[9] = F.hflip(blob[4, :, :, :])

    return blob

接下来,我们调用这个十重切割函数来看看它的增广效果。

程序清单4-33 显示十重切割后的图片
# codes04033_show_TenCrop
# 1. 调用十重切割函数
fake_blob = train.TenCrop(img)

# 2. 显示十重切割后生成的4D数组
print('十重切割后的数据块形态:{}.'.format(fake_blob.shape))

# 3. 在Notebook中显示数据增广前后的图像
title = ['左上', '右上', '左下', '右下', '中央', '左上翻转', '右上翻转', '左下翻转', '右下翻转', '中央翻转']
plt.figure(figsize=(18, 8))
for i in range(10):
    ax = plt.subplot(2, 5, i+1)
    ax.set_title(title[i])
    fake_img = cv2.cvtColor(fake_blob[i], cv2.COLOR_BGR2RGB)
    plt.imshow(fake_img)
十冲切割后的数据块形态:(10, 224, 224, 3).
十重切割结果

在分类模型中,十重切割对于最终测试性能会有一定的性能提升。在分类任务的测试阶段,我们可以将需要进行推理的样本按照十重切割的方法分成10个不同的patch块,然后分别对这10个patch块进行前向推理,最后将它们通过模型所获得特征值进行合并计算,最后再计算合并后的特征值,并输出最终的分类概率。

4.4.5 基于数据增广的模型训练

接下来,让我们使用数据增广后的数据来对模性进行训练。这里我们依然使用本章中的范例数据集Zodiac。

为了简化代码,我们使用Paddle内置的高阶训练API来实现模型的训练。为了在预测过程中得到确切的结果,我们通常只对训练数据进行数据增广操作,而在预测过程中不使用带随机操作的图像增广。当然,上一小节中所介绍的十重切割是一种不错的测试数据增广策略。下面,我们对 4.3节 中的 程序清单4-8 做一些扩展。如 程序清单4-9 所示,下面的代码清单中,我们对 isTransforms 属性所关联的条件分支进行了一定增补。针对训练数据分支,增加了若干数据增广的配置以实现对训练样本的随机裁剪、旋转、水平翻转变换和色彩扰动变换。此外,对于所有数据我们依然都使用ToTensor实例将图像转换为深度学习框架所要求的格式,即形状为[批次大小、通道数、高度、宽度]的32位浮点数,取值范围位0~1。同时,均值消除和方差标准化也被应用到所有的数据中。

程序清单4-34 定义带数据增广属性的Zodiac数据集
#@save datasets.DatasetZodiac @TODO:4.5
class DatasetZodiac(paddle.io.Dataset):
    """定义十二生肖Zodiac数据集"""
    ......
        # 对训练数据和验证、测试数据采用不同的数据预处理方法
        # 1) train和trainval:执行随机裁剪,并完成标准化预处理
        # 2) train和trainval:直接执行尺度缩放,并完成标准化预处理
        inputSize = self.args['input_size'][1:3] if len(self.args['input_size'])==3 else self.args['input_size']
        if self.isTransforms == 0:
            self.transforms = T.Compose([                      # 0) 必要数据规约
                T.Resize(inputSize),                           # 直接尺度缩放
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
            ])
        elif self.isTransforms == 1 or (self.isTransforms == 2 and mode in ['val', 'test']):
            self.transforms = T.Compose([                      # 1) 基本数据预处理,不含数据增广
                T.Resize(inputSize),                           # 直接尺度缩放
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
                T.Normalize(mean=self.args['mean_value'],      # 均值方差归一化
                            std=self.args['std_value'])    
            ])
        elif self.isTransforms == 2 and mode in ['train', 'trainval']:
            self.transforms = T.Compose([                      # 2) 训练数据预处理,包含数据增广
                T.Resize((256, 256)),                          # 直接尺度缩放
                T.RandomResizedCrop(inputSize),                # 随机裁剪
                T.RandomHorizontalFlip(prob=0.5),              # 水平翻转
                T.RandomRotation(15),                          # 随机旋转
                T.ColorJitter(brightness=0.4,                  # 色彩扰动:亮度、对比度、饱和度和色度
                                contrast=0.4, 
                                saturation=0.4, 
                                hue=0.4),
                T.ToTensor(),                                  # 转换成Paddle规定的Tensor格式
                T.Normalize(mean=self.args['mean_value'],      # 均值方差归一化
                            std=self.args['std_value'])      
            ])

下面,我们使用Paddle内置的高级API函数 model.fit() 来实现模型的训练。在下面的代码中 load_dataset_Zodiac() 函数与 4.3节 中的 程序清单4-15 是相同的,用于实现创建数据集读取器和小批量数据迭代读取器。在后面的训练过程中,我们分别调用训练数据和测试数据的迭代读取器来实训模型的训练和训练过程中的验证。需要注意都是,我们前面介绍过使用高阶API进行训练时,不需要再次创建小批量数据迭代读取器。但由于 model.fit() 函数同时支持数据读取器实例Dataset和数据迭代读取器DataLoader,为了统一代码,且能够重用 4.3节 中已经定义的函数,我们依然将DataLoader所创建的小批量迭代读取器作为训练数据和验证数据送入模型中进行训练。在本小节中,大家不用过于关注训练代码的实现,我们将在后续的章节中进行详细介绍。

程序清单4-35 使用增广后的数据进行模型训练
# codes04034_train_Zodiac_withAugmentation
train_reader, val_reader, train_reader, test_reader = load_dataset_Zodiac(batch_size=64, transformArgs=args, isTransforms=2)
network = paddle.vision.models.alexnet(num_classes=12, pretrained=True)
model = paddle.Model(network)
model.prepare(
    paddle.optimizer.Momentum(learning_rate=0.001, momentum=0.9, parameters=model.parameters()),    # 优化函数Adam
    paddle.nn.CrossEntropyLoss(),                                                 # 交叉熵损失函数
    paddle.metric.Accuracy()                                                      # 精度评价指标
    )
model.fit(train_reader,             # 训练集
        val_reader,                 # 测试集,设置后则在每个周期后进行验证,若不指定则不在验证集上进行验证
        epochs=3,                   # 训练周期数
        batch_size=64,              # 训练的批次大小
        verbose=1)                  # 是否显示训练日志
The loss value printed in the log is the current step, and the metric is the average value of previous steps.
Epoch 1/3
step 123/123 [==============================] - loss: 1.6229 - acc: 0.4767 - 2s/step          
Eval begin...
step 11/11 [==============================] - loss: 0.7070 - acc: 0.7108 - 1s/step          
Eval samples: 650
Epoch 2/3
step 123/123 [==============================] - loss: 1.3363 - acc: 0.5800 - 1s/step          
Eval begin...
step 11/11 [==============================] - loss: 0.5635 - acc: 0.7477 - 1s/step          
Eval samples: 650
Epoch 3/3
step 123/123 [==============================] - loss: 1.1920 - acc: 0.6092 - 1s/step          
Eval begin...
step 11/11 [==============================] - loss: 0.4572 - acc: 0.7908 - 1s/step          
Eval samples: 650

4.4.5 小结

4.4.6 练习

  1. 尝试在Zodiac数据集上使用多种不同的图像增广方法,并进行多种不同超参数的调整来完成模型训练。对比使用和不使用图像增广的训练和测试精度,通过对实验结果的分析,看看是否能够支持图像增广可以减轻过拟合的论点。

  2. 使用AlexNet模型在Zodiac数据集上进行训练,看看哪种图像增广方法及其组合能够提高测试的准确性。

  3. 参考深度学习的相关资料,看看是否还有其他可以使用的图像增广方法。

  4. OpenCV的默认图像形状为()。
    A.(通道,高度,宽度)
    B.(宽度,高度,通道)
    C.(高度,宽度,通道)
    D.(高度,通道,宽度)

  5. HSV是一种将RGB色彩空间中的点在倒圆锥体中的表示方法。HSV色彩空间的三个坐标分别为()。
    A. 色相
    B. 饱和度
    C. 明度
    D. 亮度

  6. Python的OpenCV库提供了颜色空间转换功能,具体函数名是()。
    A. cvtColor()
    B. convertColor()
    C. setColor()
    D. getColor()

  7. 计算机显示器的颜色模型为()。
    A. CMYK
    B. HIS
    C. RGB
    D. YIQ