こんにちは。投資エンジニアの三年坊主(@SannenBouzu)です。
今回は、機械学習のモデル精度向上に役立つ技術:データ拡張(Data Augmentation)における画像操作について紹介します。
大学の研究・自分の趣味・業務で、合わせて5年以上Pythonを使ってきました。
画像処理に関しては専門外なのですが(間違いがあれば是非コメント等で指摘いただけると幸いです)、最近興味が湧いてきたので、自身の知識定着も兼ねて整理したいと思います。
KerasのImageDataGeneratorでのパラメータ指定方法に加えて、一部NumPyでの実装例も紹介しているので、ぜひご覧ください。
- 機械学習のモデル精度向上に役立つ?データ拡張(Data Augmentation)って何?概要とモチベーションを知りたい方
- データ拡張で実際に使われる画像操作を、サンプル画像で確認したい方
- preprocess_input関数による前処理の効果を、サンプル画像で確認したい方
データ拡張(Data Augmentation)の概要とモチベーション
機械学習、特に深層学習(Deep Learning)の複雑なモデルを構築する際には、解決したい課題に適した大量のトレーニングデータが必要ですが、データ収集は必ずしも簡単ではありません。
一方で、学習データが少ないと、過学習(Overfitting)が起きてしまい汎化性能(未知のデータに対する性能)が出ないという問題があると言われます。
データ拡張(Data Augmentation)は、元のトレーニングデータに変換処理を加える、いわばデータの「水増し」によってトータルのデータ量を増やし、データのバリエーションを増やすことでモデルの精度向上を目指すテクニックです。
今回は、画像処理分野におけるCNN(Convolutional Neural Network)への入力データを想定して、データ拡張(Data Augmentation)における画像操作について具体的に確認します。
サンプル画像データの準備
サンプル画像データを準備します。
ライブラリをインストール。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import numpy as np from PIL import Image import matplotlib.pyplot as plt try: import scipy # scipy.ndimage cannot be accessed until explicitly imported from scipy import ndimage except ImportError: scipy = None #from keras.preprocessing.image import load_img, ImageDataGenerator from keras_preprocessing import image |
深層学習(Deep Learning)系ライブラリのバージョンを確認します。
- tf : 1.14.0
- (tf.keras : 2.2.4-tf)
- keras : 2.2.5
- keras_applications : 1.0.8
- keras_preprocessing : 1.1.0
1 2 3 4 5 6 7 8 9 10 | import tensorflow as tf import keras import keras_applications import keras_preprocessing print('tf :', tf.VERSION) print('tf.keras :', tf.keras.__version__) print('keras :', keras.__version__) print('keras_applications :', keras_applications.__version__) print('keras_preprocessing :', keras_preprocessing.__version__) |
Kerasの訓練済みモデル定義や画像の前処理についてはそれぞれkeras_applicationsとkeras_preprocessingというライブラリに具体的に記述されていて、kerasライブラリがそれらをインポートしている状況のようです。
- Keras公式ドキュメントで使われている load_img -> img_to_array という流れを参考に、float32の(0,255.)のndarrayを用意します
- load_imgは内部でPIL.Image.resize()を使っていて、縦横比が崩れてしまうのが嫌だったので、自分でPIL.Image.thumbnail()+中央寄せしています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fname = '/path/to/deer.jpg' # ndarray img = Image.open(fname).convert('RGB') img.thumbnail((224, 224), Image.ANTIALIAS) img = np.array(img) # 中央寄せ w_, h_, *_ = img.shape out_img = np.ones((224, 224, 3), dtype=np.float32) * 255 i, j = map(lambda x: 224 // 2 - x // 2, [w_, h_]) #out_img[i: i+w_, j: j+h_, :] = img / 255. out_img[i: i+w_, j: j+h_, :] = img |
以下、中央寄せしたこの画像に対してデータ拡張を行っていきます。
データ拡張(Data Augmentation)における画像操作
近年、機械学習のモデル精度向上に有効なデータ拡張方法が次々と提案されています。
ここではその全てに触れることは避け、いくつかの基本的な例を紹介するに留めます。
Horizontal Flip, Vertical Flip(画像の左右反転・上下反転)
1 2 3 4 5 6 7 8 9 10 11 12 13 | datagen = image.ImageDataGenerator(horizontal_flip=True) x = out_img[np.newaxis] # (Height, Width, Channels) -> (1, Height, Width, Channels) gen = datagen.flow(x, batch_size=1) f, axarr = plt.subplots(3, 3, figsize=(10,10)) for i in range(9): batches = next(gen) gen_img = batches[0] q, mod = divmod(i, 3) # datagen.flow は numpy.ndarray のfloat型で返してくるので、pyplotで表示する際にはint型に変換 axarr[q, mod].imshow(gen_img.astype(np.uint8)) |
NumPyでの実装は、例えばこのようになります。(Kerasのコードを参考にしています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # "channels_last" backend ex. TensorFlow channel_axis = 3 row_axis = 1 col_axis = 2 img_row_axis = row_axis - 1 img_col_axis = col_axis - 1 def flip_axis(x, axis): x = np.asarray(x).swapaxes(axis, 0) x = x[::-1, ...] x = x.swapaxes(0, axis) return x def horizontal_flip(image, rate=0.5): if np.random.rand() < rate: image = flip_axis(image, img_col_axis) return image def vertical_flip(image, rate=0.5): if np.random.rand() < rate: image = flip_axis(image, img_row_axis) return image |
- スライス(::-1)を使って、配列の値を逆順にしています
- x = x[::-1, …] の … はPythonの組み込み定数Ellipsis。途中の次元を省略して指定できます
1 2 3 4 | print(...) # 出力結果 Ellipsis |
Random Rotation(画像の回転)
1 | datagen = image.ImageDataGenerator(rotation_range=20) |
Random Shift(画像の平行移動)
1 | datagen = image.ImageDataGenerator(width_shift_range=0.3) |
Random Shear(画像のシアー変換)
1 | datagen = image.ImageDataGenerator(shear_range=15) |
Random Zoom(画像の拡大・縮小)
1 | datagen = image.ImageDataGenerator(zoom_range=[0.5, 2.0]) |
Random Channel Shift(画像のチャネルシフト)
1 | datagen = image.ImageDataGenerator(channel_shift_range=50.) |
Random Brightness(画像の明るさ調整)
1 | datagen = image.ImageDataGenerator(brightness_range=[0.3, 1.0]) |
その他の画像操作
- Random Crop(画像のトリミング)
- Scale Augmentation(画像のリサイズ+トリミング)
- Cutout(画像のマスク)
- Random Erasing
- …
機械学習モデルにとって意味のある画像操作をすることが大切だと思います。
- 被写体の向きや光の加減が少し違ったり、一部分が見えなかったりすることはたしかに十分起こりうる
- 被写体があらぬ方向に引き伸ばされるようなシチュエーションはありうるのか・・・?
トレーニングデータをいくら大量に集めても絶対に実際に出現しないような画像を水増ししても、おそらく実際のモデルの性能向上にはあまり役立たないでしょう。
preprocess_input関数とImageDataGeneratorの関係を読み解く【コードリーディング】
preprocess_input関数とは、Kerasの訓練済みモデルごとに個別に定義されている画像前処理用の関数のことです。
上のリンクからサンプルコードを確認すると、Keras model に画像を与える前にこのpreprocess_inputを適用しているのが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 | from keras.applications.resnet50 import ResNet50 from keras.preprocessing import image from keras.applications.resnet50 import preprocess_input, decode_predictions import numpy as np model = ResNet50(weights='imagenet') img_path = 'elephant.jpg' img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) ... |
この preprocess_input は実質的には _preprocess_numpy_input という関数を呼んでいて、それぞれの訓練済みモデルが元々どのように訓練されたかに応じて、画素値が[-1,1]の範囲に収まるように正規化をしたり、’RGB’->’BGR’の変換をしたりなどの前処理をしているようです。
ImageDataGeneratorクラスでこの処理を行わせるためには、引数preprocessing_function に preprocess_input を指定できます。
1 2 3 | datagen = image.ImageDataGenerator(..., preprocessing_function=preprocess_input, ...) |
少しだけコードリーディングをすると、
- 指定したpreprocess_inputは、ImageDataGeneratorのstandardizeという関数の中で呼ばれます。
- このstandardize関数は、class BatchFromFilesMixin() の _get_batches_of_transformed_samples 関数で呼ばれていて、
- ImageDataGeneratorからflowを作るNumpyArrayIterator (flow) 、DirectoryIterator (flow_from_directory) や DataFrameIterator (flow_from_dataframe) などが、このBatchFromFilesMixinを継承している
という関係になっているようです。
最後に、いくつかの訓練済みモデルを例に、preprocess_inputによる前処理の効果を確認してみます。
元々Caffeで訓練されているResNet50の場合、
1 2 3 4 5 | from keras.applications.resnet50 import preprocess_input as preprocess_input_resnet out_img_processed_resnet = preprocess_input_resnet(out_img) # converted 'RGB'->'BGR' in preprocess_input, so revert 'BGR'->'RGB' check_img_preprocess_input(out_img_processed_resnet[..., ::-1]) |
より最近になって提案された、軽量・高精度のMobileNetでは、
1 2 3 4 | from keras.applications.mobilenet import preprocess_input as preprocess_input_mobilenet out_img_processed_mobilenet = preprocess_input_mobilenet(out_img) check_img_preprocess_input(out_img_processed_mobilenet) |
このように、モデルごとに適切な前処理を行うための関数 preprocess_input が定義されているので、keras.applications以下の空間からモデルに合ったものをインポートして使いましょう。
今後の課題
- 白い部分を補完する
- 縦横比が崩れるのが嫌で(PIL.Image.resizeではなく)PIL.Image.thumbnailを使いましたが、データによっては気にせずresizeで縦横比を壊してしまってもいいかもしれません
- NumPy(SciPy)での実装を確認する
- 今回はHorizontal Flip, Vertical FlipのNumPy実装を確認しましたが、他の画像操作についても実装を検討して追加できればと思います
関連記事:こちらも読まれています
【2019年版】Pythonインストール・Mac編【長く安全に使える環境構築】
Pythonを快適に使いこなすMac環境【現役エンジニアおすすめはPro 13インチ】
【pandas】重複したDataFrameの行を確認・削除【逆引きデータ分析】