##### Copyright 2020 The TensorFlow Authors.
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

数据增强#

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看 下载笔记本

概述#

本教程演示了数据增强:一种通过应用随机(但真实)的变换(例如图像旋转)来增加训练集多样性的技术。

您将学习如何通过两种方式应用数据增强:

  • 使用 Keras 预处理层,例如 tf.keras.layers.Resizingtf.keras.layers.Rescalingtf.keras.layers.RandomFliptf.keras.layers.RandomRotation

  • 使用 tf.image 方法,例如 tf.image.flip_left_righttf.image.rgb_to_grayscaletf.image.adjust_brightnesstf.image.central_croptf.image.stateless_random*

设置#

import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds

from tensorflow.keras import layers

下载数据集#

本教程使用 tf_flowers 数据集。为了方便起见,请使用 TensorFlow Datasets 下载数据集。如果您想了解导入数据的其他方式,请参阅加载图像教程。

(train_ds, val_ds, test_ds), metadata = tfds.load(
    'tf_flowers',
    split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
    with_info=True,
    as_supervised=True,
)

花卉数据集有五个类。

num_classes = metadata.features['label'].num_classes
print(num_classes)

我们从数据集中检索一个图像,然后使用它来演示数据增强。

get_label_name = metadata.features['label'].int2str

image, label = next(iter(train_ds))
_ = plt.imshow(image)
_ = plt.title(get_label_name(label))

使用 Keras 预处理层#

调整大小和重新缩放#

您可以使用 Keras 预处理层将图像大小调整为一致的形状(使用 tf.keras.layers.Resizing),并重新调整像素值(使用 tf.keras.layers.Rescaling)。

IMG_SIZE = 180

resize_and_rescale = tf.keras.Sequential([
  layers.Resizing(IMG_SIZE, IMG_SIZE),
  layers.Rescaling(1./255)
])

注:上面的重新缩放层将像素值标准化到 [0,1] 范围。如果想要 [-1,1],可以编写 tf.keras.layers.Rescaling(1./127.5, offset=-1)

您可以看到将这些层应用于图像的结果。

result = resize_and_rescale(image)
_ = plt.imshow(result)

验证像素是否在 [0, 1] 范围内:

print("Min and max pixel values:", result.numpy().min(), result.numpy().max())

数据增强#

您也可以使用 Keras 预处理层进行数据增强,例如 tf.keras.layers.RandomFliptf.keras.layers.RandomRotation

我们来创建一些预处理层,然后将它们重复应用于同一图像。

data_augmentation = tf.keras.Sequential([
  layers.RandomFlip("horizontal_and_vertical"),
  layers.RandomRotation(0.2),
])
# Add the image to a batch.
image = tf.cast(tf.expand_dims(image, 0), tf.float32)
plt.figure(figsize=(10, 10))
for i in range(9):
  augmented_image = data_augmentation(image)
  ax = plt.subplot(3, 3, i + 1)
  plt.imshow(augmented_image[0])
  plt.axis("off")

有多种预处理层可用于数据增强,包括 tf.keras.layers.RandomContrasttf.keras.layers.RandomCroptf.keras.layers.RandomZoom 等。

使用 Keras 预处理层的两个选项#

您可以通过两种方式使用这些预处理层,但需进行重要的权衡。

选项 1:使预处理层成为模型的一部分#

model = tf.keras.Sequential([
  # Add the preprocessing layers you created earlier.
  resize_and_rescale,
  data_augmentation,
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  # Rest of your model.
])

在这种情况下,需要注意两个要点:

  • 数据增强将与其他层在设备端同步运行,并受益于 GPU 加速。

  • 当您使用 model.save 导出模型时,预处理层将与模型的其他部分一起保存。如果您稍后部署此模型,它将自动标准化图像(根据您的层配置)。这可以省去在服务器端重新实现该逻辑的工作。

注:数据增强在测试时处于停用状态,因此只有在调用 Model.fit(而非 Model.evaluateModel.predict)期间才会对输入图像进行增强。

选项 2:将预处理层应用于数据集#

aug_ds = train_ds.map(
  lambda x, y: (resize_and_rescale(x, training=True), y))

通过这种方式,您可以使用 Dataset.map 创建产生增强图像批次的数据集。在本例中:

  • 数据增强将在 CPU 上异步进行,且为非阻塞性。您可以使用 Dataset.prefetch 将 GPU 上的模型训练与数据数据预处理重叠,如下所示。

  • 在本例中,当您调用 Model.save 时,预处理层将不会随模型一起导出。在保存模型或在服务器端重新实现它们之前,您需要将它们附加到模型上。训练后,您可以在导出之前附加预处理层。

您可以在图像分类教程中找到第一个选项的示例。我们在这里演示一下第二个选项。

将预处理层应用于数据集#

使用上面创建的 Keras 预处理层配置训练数据集、验证数据集和测试数据集。您还将配置数据集以提高性能,具体方式是使用并行读取和缓冲预提取从磁盘产生批次,这样不会阻塞 I/O。(您可以通过使用 tf.data API 提高性能指南详细了解数据集性能)。

注:应仅对训练集应用数据增强。

batch_size = 32
AUTOTUNE = tf.data.AUTOTUNE

def prepare(ds, shuffle=False, augment=False):
  # Resize and rescale all datasets.
  ds = ds.map(lambda x, y: (resize_and_rescale(x), y), 
              num_parallel_calls=AUTOTUNE)

  if shuffle:
    ds = ds.shuffle(1000)

  # Batch all datasets.
  ds = ds.batch(batch_size)

  # Use data augmentation only on the training set.
  if augment:
    ds = ds.map(lambda x, y: (data_augmentation(x, training=True), y), 
                num_parallel_calls=AUTOTUNE)

  # Use buffered prefetching on all datasets.
  return ds.prefetch(buffer_size=AUTOTUNE)
train_ds = prepare(train_ds, shuffle=True, augment=True)
val_ds = prepare(val_ds)
test_ds = prepare(test_ds)

训练模型#

为了完整起见,您现在将使用刚刚准备的数据集训练模型。

序贯模型由三个卷积块 (tf.keras.layers.Conv2D) 组成,每个卷积块都有一个最大池化层 (tf.keras.layers.MaxPooling2D)。有一个全连接层 (tf.keras.layers.Dense),上面有 128 个单元,由 ReLU 激活函数 ('relu') 激活。此模型尚未针对准确率进行调整(目标是展示机制)。

model = tf.keras.Sequential([
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(num_classes)
])

选择 tf.keras.optimizers.Adam 优化器和 tf.keras.losses.SparseCategoricalCrossentropy 损失函数。要查看每个训练周期的训练和验证准确率,请将 metrics 参数传递给 Model.compile

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

训练几个周期:

epochs=5
history = model.fit(
  train_ds,
  validation_data=val_ds,
  epochs=epochs
)
loss, acc = model.evaluate(test_ds)
print("Accuracy", acc)

自定义数据增强#

您还可以创建自定义数据增强层。

教程的这一部分展示了两种操作方式:

  • 首先,您将创建一个 tf.keras.layers.Lambda 层。这是编写简洁代码的好方式。

  • 接下来,您将通过子类化编写一个新层,这会给您更多的控制。

两个层都会根据某种概率随机反转图像中的颜色。

def random_invert_img(x, p=0.5):
  if  tf.random.uniform([]) < p:
    x = (255-x)
  else:
    x
  return x
def random_invert(factor=0.5):
  return layers.Lambda(lambda x: random_invert_img(x, factor))

random_invert = random_invert()
plt.figure(figsize=(10, 10))
for i in range(9):
  augmented_image = random_invert(image)
  ax = plt.subplot(3, 3, i + 1)
  plt.imshow(augmented_image[0].numpy().astype("uint8"))
  plt.axis("off")

接下来,通过子类化实现自定义层:

class RandomInvert(layers.Layer):
  def __init__(self, factor=0.5, **kwargs):
    super().__init__(**kwargs)
    self.factor = factor

  def call(self, x):
    return random_invert_img(x)
_ = plt.imshow(RandomInvert()(image)[0])

可以按照上述选项 1 和 2 中的描述使用这两个层。

使用 tf.image#

上述 Keras 预训练实用工具十分方便。但为了更精细的控制,您可以使用 tf.datatf.image 编写自己的数据增强流水线或数据增强层。您还可以查看 TensorFlow Addons 图像:运算TensorFlow I/O:色彩空间转换

由于花卉数据集之前已经配置了数据增强,因此我们将其重新导入以重新开始。

(train_ds, val_ds, test_ds), metadata = tfds.load(
    'tf_flowers',
    split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
    with_info=True,
    as_supervised=True,
)

检索一个图像以供使用:

image, label = next(iter(train_ds))
_ = plt.imshow(image)
_ = plt.title(get_label_name(label))

我们来使用以下函数呈现原始图像和增强图像,然后并排比较。

def visualize(original, augmented):
  fig = plt.figure()
  plt.subplot(1,2,1)
  plt.title('Original image')
  plt.imshow(original)

  plt.subplot(1,2,2)
  plt.title('Augmented image')
  plt.imshow(augmented)

数据增强#

翻转图像#

使用 tf.image.flip_left_right 垂直或水平翻转图像:

flipped = tf.image.flip_left_right(image)
visualize(image, flipped)

对图像进行灰度处理#

您可以使用 tf.image.rgb_to_grayscale 对图像进行灰度处理:

grayscaled = tf.image.rgb_to_grayscale(image)
visualize(image, tf.squeeze(grayscaled))
_ = plt.colorbar()

调整图像饱和度#

使用 tf.image.adjust_saturation,通过提供饱和度系数来调整图像饱和度:

saturated = tf.image.adjust_saturation(image, 3)
visualize(image, saturated)

更改图像亮度#

使用 tf.image.adjust_brightness,通过提供亮度系数来更改图像的亮度:

bright = tf.image.adjust_brightness(image, 0.4)
visualize(image, bright)

对图像进行中心裁剪#

使用 tf.image.central_crop 将图像从中心裁剪到所需部分:

cropped = tf.image.central_crop(image, central_fraction=0.5)
visualize(image, cropped)

旋转图像#

使用 tf.image.rot90 将图像旋转 90 度:

rotated = tf.image.rot90(image)
visualize(image, rotated)

随机变换#

警告:有两组随机图像运算:tf.image.random*tf.image.stateless_random*。强烈不建议使用 tf.image.random* 运算,因为它们使用的是 TF 1.x 中的旧 RNG。请改用本教程中介绍的随机图像运算。有关详情,请参阅随机数生成

对图像应用随机变换可以进一步帮助泛化和扩展数据集。当前的 tf.image API 提供了 8 个这样的随机图像运算 (op):

这些随机图像运算纯粹是功能性的:输出仅取决于输入。这使得它们易于在高性能、确定性的输入流水线中使用。它们要求每一步都输入一个 seed 值。给定相同的 seed,无论被调用多少次,它们都会返回相同的结果。

注:seed 是形状为 (2,)Tensor,其值为任意整数。

在以下部分中,您将:

  1. 回顾使用随机图像运算来变换图像的示例。

  2. 演示如何将随机变换应用于训练数据集。

随机更改图像亮度#

通过提供亮度系数和 seed,使用 tf.image.stateless_random_brightness 随机更改 image 的亮度。亮度系数在 [-max_delta, max_delta) 范围内随机选择,并与给定的 seed 相关联。

for i in range(3):
  seed = (i, 0)  # tuple of size (2,)
  stateless_random_brightness = tf.image.stateless_random_brightness(
      image, max_delta=0.95, seed=seed)
  visualize(image, stateless_random_brightness)

随机更改图像对比度#

通过提供对比度范围和 seed,使用 tf.image.stateless_random_contrast 随机更改 image 的对比度。对比度范围在区间 [lower, upper] 中随机选择,并与给定的 seed 相关联。

for i in range(3):
  seed = (i, 0)  # tuple of size (2,)
  stateless_random_contrast = tf.image.stateless_random_contrast(
      image, lower=0.1, upper=0.9, seed=seed)
  visualize(image, stateless_random_contrast)

随机裁剪图像#

通过提供目标 sizeseed,使用 tf.image.stateless_random_crop 随机裁剪 image。从 image 中裁剪出来的部分位于随机选择的偏移处,并与给定的 seed 相关联。

for i in range(3):
  seed = (i, 0)  # tuple of size (2,)
  stateless_random_crop = tf.image.stateless_random_crop(
      image, size=[210, 300, 3], seed=seed)
  visualize(image, stateless_random_crop)

对数据集应用增强#

我们首先再次下载图像数据集,以防它们在之前的部分中被修改。

(train_datasets, val_ds, test_ds), metadata = tfds.load(
    'tf_flowers',
    split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
    with_info=True,
    as_supervised=True,
)

接下来,定义一个用于调整图像大小和重新缩放图像的效用函数。此函数将用于统一数据集中图像的大小和比例:

def resize_and_rescale(image, label):
  image = tf.cast(image, tf.float32)
  image = tf.image.resize(image, [IMG_SIZE, IMG_SIZE])
  image = (image / 255.0)
  return image, label

我们同时定义 augment 函数,该函数可以将随机变换应用于图像。此函数将在下一步中用于数据集。

def augment(image_label, seed):
  image, label = image_label
  image, label = resize_and_rescale(image, label)
  image = tf.image.resize_with_crop_or_pad(image, IMG_SIZE + 6, IMG_SIZE + 6)
  # Make a new seed.
  new_seed = tf.random.split(seed, num=1)[0, :]
  # Random crop back to the original size.
  image = tf.image.stateless_random_crop(
      image, size=[IMG_SIZE, IMG_SIZE, 3], seed=seed)
  # Random brightness.
  image = tf.image.stateless_random_brightness(
      image, max_delta=0.5, seed=new_seed)
  image = tf.clip_by_value(image, 0, 1)
  return image, label

选项 1:使用 tf.data.experimental.Counter#

创建一个 tf.data.experimental.Counter() 对象(我们称之为 counter),并使用 (counter, counter) Dataset.zip 数据集。这将确保数据集中的每个图像都与一个基于 counter 的唯一值(形状为 (2,))相关联,稍后可以将其传递到 augment 函数,作为随机变换的 seed 值。

# Create a `Counter` object and `Dataset.zip` it together with the training set.
counter = tf.data.experimental.Counter()
train_ds = tf.data.Dataset.zip((train_datasets, (counter, counter)))

augment 函数映射到训练数据集:

train_ds = (
    train_ds
    .shuffle(1000)
    .map(augment, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)
val_ds = (
    val_ds
    .map(resize_and_rescale, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)
test_ds = (
    test_ds
    .map(resize_and_rescale, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)

选项 2:使用 tf.random.Generator#

  • 创建一个具有初始 seed 值的 tf.random.Generator 对象。在同一个生成器对象上调用 make_seeds 函数会始终返回一个新的、唯一的 seed 值。

  • 定义一个封装容器函数:1) 调用 make_seeds 函数;2) 将新生成的 seed 值传递给 augment 函数进行随机变换。

注:tf.random.Generator 对象会将 RNG 状态存储在 tf.Variable 中,这意味着它可以保存为检查点或以 SavedModel 格式保存。有关详情,请参阅随机数生成

# Create a generator.
rng = tf.random.Generator.from_seed(123, alg='philox')
# Create a wrapper function for updating seeds.
def f(x, y):
  seed = rng.make_seeds(2)[0]
  image, label = augment((x, y), seed)
  return image, label

将封装容器函数 f 映射到训练数据集,并将 resize_and_rescale 函数映射到验证集和测试集:

train_ds = (
    train_datasets
    .shuffle(1000)
    .map(f, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)
val_ds = (
    val_ds
    .map(resize_and_rescale, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)
test_ds = (
    test_ds
    .map(resize_and_rescale, num_parallel_calls=AUTOTUNE)
    .batch(batch_size)
    .prefetch(AUTOTUNE)
)

这些数据集现在可以用于训练模型了,如前文所述。

后续步骤#

本教程演示了使用 Keras 预处理层和 tf.image 进行数据增强。

  • 要了解如何在模型中包含预处理层,请参阅图像分类教程。

  • 您可能也有兴趣了解预处理层如何帮助您对文本进行分类,请参阅基本文本分类教程。

  • 您可以在此指南中了解有关 tf.data 的更多信息,并且可以在这里了解如何配置输入流水线以提高性能。