Keras(TensorFlow)のImageDataGeneratorでデータを水増しする

2022.06.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんちには。

データアナリティクス事業本部機械学習チームの中村です。

今回は、KerasのImageDataGeneratorで、画像データの水増し(Data Augmentation)に使用できそうな変換をピックアップしてご紹介します。

Data Augmentationについて

画像を入力データに扱うニューラルネットワークの学習では、元画像に対して色々な変換を施すことで、入力画像のパターンを増加させることが良く行われます。具体的には、実際にデータそのものを水増しするのではなく、ある変換定義し、その変換の度合をランダムに実行するブロックを定義する形で実行することが多いです。

ランダムに変換を実行することで、同じデータであってもエポックが異なれば異なる度合の変換が適用されます。これにより、データのバリエーションを増やすようなイメージとなります。

Keras(TensorFlow)では、これを実施するためのImageDataGeneratorというものが準備されています。これを用いることで、簡単な変換であればすぐに導入できます。また、自身でImageDataGeneratorをカスタマイズすることで使いたい変換を実装することが可能です。

今回は、ImageDataGeneratorにデフォルトで準備されているものの中から、データ拡張に使えそうな変換をピックアップしてご紹介します。

変換の実装

前準備

モジュールのインポートやデータ取得を行います。

import tensorflow as tf
import tensorflow.keras as keras
import numpy as np
import matplotlib.pyplot as plt

カラーの方が分かりやすいため、データはCIFAR-10というRGB画像のデータセットを使います。

dataset = keras.datasets.cifar10
(train_images, train_labels), (test_images, test_labels) = dataset.load_data()

CIFAR-10は10分類の32x32画像のデータセットです。

参考までに、10分類は、0:飛行機、1:自動車、2:鳥、3:猫、4:鹿、5:犬、6:カエル、7:馬、8:船、9:トラックという構成になっています。

今回はまず分かりやすさのため、1つの画像に対してどのような変換が行われるかを見ていきます。

ですのでデータセットから1つの画像を抽出します。

sample_index = 7
train_sample = train_images[sample_index]

この画像を描画しておきます。

plt.figure(figsize=(3,3))
plt.imshow(train_sample)
plt.tick_params(labelbottom='off')
plt.tick_params(labelleft='off')

描画用関数の準備

入力画像を複製して変換し、その結果を描画する関数を定義します。

def plot_augmentation_image(train_sample, params):

    # 同じ画像を16個複製する
    train_samples = np.repeat(train_sample.reshape((1, *train.shape)), 16, axis=0)

    # 16個に対してparamsで与えられた変換を実施
    data_generator = keras.preprocessing.image.ImageDataGenerator(**params)
    generator = data_generator.flow(train_samples, batch_size=16)

    # 変換後のデータを取得
    batch_x = generator.next()

    # 変換後はfloat32となっているため、uint8に変換
    batch_x = batch_x.astype(np.uint8)

    # 描画処理
    plt.figure(figsize=(10,10))
    for i in range(16):
        plt.subplot(4,4,i+1)
        plt.imshow(batch_x[i])
        plt.tick_params(labelbottom='off')
        plt.tick_params(labelleft='off')

拡大縮小: zoom_range

  • 拡大縮小処理を実施します。
  • アスペクト比は維持されず、縦横独立してランダムで拡大縮小率が決定されるのでご注意ください。
  • paramsでは[lower,upper]という形でズームの下限・上限を指定します。
  • スカラーで指定した場合、lower1-aupper1+aという扱いとなります。
params = {
    'zoom_range': [0.5, 2.0]
}
plot_augmentation_image(train_sample, params)

回転: rotation_range

  • 度数法で最大変化時の角度を指定します。
params = {
    'rotation_range': 45
}
plot_augmentation_image(train_sample, params)

反転: vertical_flip, horizontal_flip

  • vetricalは上下を反転します。
  • horizontalは左右を反転します。
  • 反転するか否かはランダムとなります。
params = {
    'vertical_flip': True,
    'horizontal_flip': True
}
plot_augmentation_image(train_sample, params)

位置変更: width_shift_range, height_shift_range

  • 中心位置を相対的にずらします。
  • 例えば、height_shift_range=0.1の場合、元画像の高さ x 0.1ピクセルの範囲内で±垂直方向にずらされます。
params = {
    'height_shift_range': 0.1,
    'width_shift_range': 0.1
}
plot_augmentation_image(train_sample, params)

せん断変形: shear_range

  • せん断変形は、四角形を平行四辺形に変換します。
  • 角度は度数法で指定し、最大変化時の角度を指定します。
  • ちなみに90度が完全につぶれる変換となります。
params = {
    'shear_range': 90
}
plot_augmentation_image(train_sample, params)

画素変化: channel_shift_range

  • 各画素値の値を加算・減算します。パラメータとしては変化の最大量を指定します。
  • 1ピクチャ内では全チャンネル、全画素で同じ値が加算・減算されます。
  • そのため全体の明るさが変化する変換となります。
params = {
    'channel_shift_range': 127
}
plot_augmentation_image(train_sample, params)

外挿: fill_mode

  • 拡大縮小などで画素がなくなる部分のpadding(外挿)をどのようにするか決めます。
  • fill_mode=constantの場合、cvalで定数を指定する必要があります。
  • 以下は0(つまり黒)で埋める例となります。
params = {
    'zoom_range': [0.5, 2.0],
    'fill_mode': 'constant',
    'cval': 0
}
plot_augmentation_image(train_sample, params)

組み合わせ

  • 組み合わせて使用したい場合は、paramsに組み合わせたいパラメータをすべての設定します。
params = {
    'zoom_range': [0.5, 2.0],
    'rotation_range': 45,
    'vertical_flip': True,
    'horizontal_flip': True,
    'height_shift_range': 0.1,
    'width_shift_range': 0.1,
    'shear_range': 90,
    'channel_shift_range': 127,
    'zoom_range': [0.5, 2.0],
    'fill_mode': 'constant',
    'cval': 0
}
plot_augmentation_image(train_sample, params)

  • 今回は分かりやすさのため変化を大きめに設定しています。
  • 実際には、変化は性能に対する効果を見ながら少しずつ変化させていきます。

モデル学習プロセスへの組み込み

  • 学習プロセスに組み込むためには、以下のようにgeneratorを定義します。
params = {
    'zoom_range': [0.5, 2.0],
    'rotation_range': 45,
    'vertical_flip': True,
    'horizontal_flip': True,
    'height_shift_range': 0.1,
    'width_shift_range': 0.1,
    'shear_range': 90,
    'channel_shift_range': 127
}
generator = keras.preprocessing.image.ImageDataGenerator(**params)

train_iter = generator.flow(x=train_images, y=train_labels)
  • さらにサンプルのモデルを定義します。
model = keras.Sequential([
    keras.layers.Conv2D(32, (3, 3), input_shape=(32, 32, 3), padding='same'),
    keras.layers.ReLU(),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Conv2D(32, (3, 3), padding='same'),
    keras.layers.ReLU(),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.GlobalAveragePooling2D(),
    keras.layers.Dense(10, activation='softmax'),
])
  • 学習時に作成したgeneratorであるtrain_iterを引数に与えます。
model.compile(
    optimizer='Adam', 
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# model.fit(train_images, train_labels, epochs=10) # こちらがよく見る形
model.fit(train_iter, epochs=10) # 代わりにtrain_iterを引数に与える。

まとめ

いかがでしたでしょうか?画像のデータ拡張はモデル精度を向上させるための重要な工夫の一つとなりますので、本記事がお役に立てば幸いです。 機会があれば、今後カスタマイズ方法についても紹介したいと思います。