画像読み込みとAugmentationが可能で、非常に便利なImageDataGeneratorですが、ノイズ付与やカラー変化などの機能は実装されておりません。
そんな時に自前で機能追加する方法です。
はじめに
ImageDataGenerator とは、
Model.fit_generator()に渡せるジェネレータであり、
データセットや、フォルダ内の画像を読み込んで、
バッチ毎にオーグメンテーションができるクラスです。
学習時に画像を読み込みつつ
カンタンに「毎回違う画像変化」をつけることで、
画像分類での正解率向上にバツグンの効果を発揮します。
ただし、標準のAugmentation機能には、
RandomNoiseとかmixupなどの最新の論文にあるような機能は実装されておらず、実現するには自作する必要があります。
今回はその第一歩のサンプルとして記事にしました。
なお、ImageDataGenerator の持つAugmentation機能の説明は、
Keras公式ドキュメントのほか、
以下のサイトが非常にわかりやすかったです。
ソースコード
こちらにありますので、実際に動作させたい方はこちらを。
以下の説明は、このソースコードの前提で書いています。
ImageDataGeneratorの拡張
ImageDataGeneratorはクラスの一個なので、
クラス継承して機能拡張が可能です。
これにノイズ付与機能を加えてみます。
class ExtImageDataGenerator(ImageDataGenerator): def __init__(self, noise_pb=0.25, *args, **kwargs): super().__init__(*args, **kwargs) # noise parameter assert noise_pb >= 0.0 self.noise_pb = noise_pb
拡張クラス ExtImageDataGenerator では、
ノイズ付与する対象の確率 “noise_pb” を追加し、
例えば 0.25 なら、4つに1つがノイズ付与される仕様にします。
そして、親クラスの引数も使えるように、
super().__init__(*args, **kwargs) をCallしています(重要)。
ノイズ付与メソッドの追加
ノイズ付与メソッドは、
一から作ってもいいですが、手っ取り早く
scikit-imageのutil.random_noiseで実装します。
var=0.005くらいあれば見た目に分かりやすいのですが、
影響も過多なのでとりあえず0.001で。
from skimage import util def add_noise(self, rgb): return util.random_noise(rgb, var=0.001)
flow() もしくはflow_from_directory()のオーバーライド
そして、バッチ実行されるflow() をオーバーライドします。
このメソッドは学習時に随時呼び出されるメソッドなので、
この中から先程のadd_noise()を実行することで、
自作Augmentationが実施される仕組みです。
# Override flow() def flow(self, *args, **kwargs): batches = super().flow(*args, **kwargs) while True: batch_x, batch_y = next(batches) # My Augumentation for i,img in enumerate(batch_x): if np.random.random() < self.noise_pb: batch_x[i] = self.add_noise(img) yield (batch_x, batch_y)
flow() 仕様に基づき、
while~yieldによって、学習時にデータを随時参照させます。
他の機能も追加したい場合、
このテンプレートの # My Augmentation以下に同じようにメソッド追加していけばいいです。
generator定義
拡張した ExtImageDataGenerator でオブジェクト作成。
これをもとに、学習用のジェネレータを作成します。
train_datagen = ExtImageDataGenerator(horizontal_flip = True, channel_shift_range = 0.3, noise_pb=0.5) train_generator = train_datagen.flow( x_train, y_train, batch_size=BATCH_SIZE, shuffle=True, seed = 12345 )
ここで、
horizontal_flip, channel_shift_range は、元々ある親クラスの引数で、
noise_pb が継承クラスの引数です。
同時に使えることの確認として実施しています。
validationデータも同様に作成します。
こちらは、validation用なので特にAugmentation必要無いと思って実施しません。
(やったほうが良いのかどうかよく分かっていません・・・)
valid_datagen = ExtImageDataGenerator() valid_generator = valid_datagen.flow( x_valid, y_valid, batch_size=BATCH_SIZE, shuffle=True, seed = 12345 )
train実行
あとは”fit_generator” に渡して完了です。
# モデル訓練実行 history = model.fit_generator( train_generator, steps_per_epoch=x_train.shape[0]//BATCH_SIZE, epochs=MAX_EPOCH, verbose=1, validation_data=valid_generator, validation_steps=x_test.shape[0]//BATCH_SIZE, callbacks=cb_funcs )
通常Augmentationとの精度比較
CIFAR10のような小さな画像なので、
今回は非常に単純な4層CNNを用いて実施しました。
def Conv_L4(input_shape): model = Sequential() model.add(Conv2D(64, (3, 3), padding='same', input_shape=input_shape, kernel_initializer='random_uniform', activation='relu')) model.add(MaxPooling2D((2,2), strides=(2, 2))) model.add(Conv2D(128, (3, 3), padding='same', kernel_initializer='random_uniform', activation='relu')) model.add(MaxPooling2D((2,2), strides=(2, 2))) model.add(Conv2D(128, (3, 3), padding='same', kernel_initializer='random_uniform', activation='relu')) model.add(MaxPooling2D((2,2), strides=(2, 2))) model.add(Conv2D(256, (3, 3), padding='same', kernel_initializer='random_uniform', activation='relu')) model.add(MaxPooling2D((2,2), strides=(2, 2))) return model
結果
- ExtImageDataGeneratorによる HFlip + ChannelShift + AddNoise実施 学習結果 loss:0.772628215932846
acc :0.7592999935150146
- ImageDataGeneratorによる HFlip + ChannelShift実施 学習結果
loss:0.7938857310771942
acc :0.7570000290870667 - ImageDataGeneratorでAugmentなし学習結果
loss:1.076167423915863
acc :0.7027000188827515
若干ですが、ノイズ付与を追加したことで、
一番精度が良い結果が得られました。
今回のCIFAR10のような小さな画像だとあまり効かなったですが、
ランダムノイズ付与は様々なシーンで有効な結果が出ています。
他にも有効なAugmentation術が日々開発されています。
(「Data Augmentation」でググると色々見つかります RandomErasing, mixup など…)
これらを試したい時も、
ImageDataGeneratorを拡張して実装すると便利だと思います。
最後まで読んでいただき、ありがとうございます!
ブックマーク登録、
ツイッターフォロー、
よろしくお願いいたします!🙇♂️🙇♂️
↓↓↓