一方、学習に用いる画像枚数が十分ではない場合、過学習の問題が発生することがあります。 学習データの不足問題への対応策として、データ拡張(Data Augumentation) という手段がしばしば用いられます。 データ拡張とは、学習データの不足を補うために、 限られた実際の学習データに回転や平行移動などの変換を施すことで、 新たなデータを多数生成することです。 学習目的の知的レベルが上がる中でネットワークも複雑化しており、 学習データの質と量がますます重要になってきています。 Kerasでは、画像データを拡張する手段として、ImageDataGeneratorという クラスオブジェクトが提供されています。'Generator'の名前が示すように、 pythonに導入されているジェネレータの性質を有しています。 次にImageDataGeneratorクラスについて概観します。
ImageDataGeneratorクラスのインスタンスオブジェクトを生成するためには、 一連の引数を指定してうえで、クラス名を関数とするコンストラクタを呼び出します。 呼び出し形式の例を以下に示します。 このインスタンスは拡張の際の画像変換を規定するオブジェクトであり、 学習問題に応じた画像拡張となるようにパラメータ設定を行う必要があります。
from keras.preprocessing.image import ImageDataGenerator
image_gen = ImageDataGenerator(
featurewise_center = False,
samplewise_center = False,
featurewise_std_normalization = False,
samplewise_std_normalization = False,
horizontal_flip = True,
vertical_flip = False)
ImageDataGeneratorオブジェクトを生成する際の代表的な引数の種類と意味、
そして各引数の形式をまとめたものを表4-1に示します。
| パラメータ | 意味 | 値 |
|---|---|---|
| featurewise_center | データ全体の入力平均を0にする | 真理値 |
| featurewise_std_normalization | データ全体の標準偏差で正規化 | 真理値 | samplewise_center | 各サンプルの平均を0にする | 真理値 |
| samplewise_std_normalization | 各サンプルの標準偏差で正規化 | 真理値 |
| rotation_range | 画像回転の範囲 | 整数 |
| width_shift_range | 画像横幅に対する水平シフト割合 | 浮動小数点 |
| height_shift_range | 画像縦幅に対する垂直シフト割合 | 浮動小数点 |
| zoom_range | 拡大縮小範囲 | 浮動小数点、または[lower,upper] |
| shear_range | シアー強度 | 浮動小数点(反時計回りシアー角度) |
| horizontal_flip | 水平方向反転 | 真理値 |
| vertical_flip | 垂直方向反転 | 真理値 |
スペースの関係で、表には示していない幾つかの引数について補足をしておきます。 最初に、'fill_mode','data_format'について触れます。
'fill_mode'は、回転等によって入力画像の境界周りに発生する空白領域の埋め方を
選択するための引数です。
引数に指定可能な値としては、{"constant", "nearest", "reflect", "wrap"}
のいずれかで、デフォルト値は"nearest"です。
今、入力画像の変換によって画素領域abcd(a,b,c,dは画素値を表す)の4画素の両側に空白領域ができたとします。
すなわち、'******|abcd|******'のようにabcdの4画素に隣接する値の定まらない
*領域が生じたとします。
このとき、'fill_mode'引数に指定した値によって以下のように空白領域を埋めていきます。
- fill_mode="constant"の場合: 'kkkkkk|abcd|kkkkkk' (cval=kが指定されているとき)}
- fill_mode="nearest"の場合: 'aaaaaa|abcd|dddddd'
- fill_mode="reflect"の場合: 'cddcba|abcd|dcbaab'
- fill_mode="wrap"の場合: 'abcdab|abcd|abcdab'
'data_format'は、学習用データセットをnumpy型配列の構造として、 (samples, height, width, channels)と表現するか、あるいは (samples, channels, height, width)と表現するかを選択するための引数です。 引数の値としては、{"channels_first", " channels_last"}のいずれかとなります。 デフォルトでは"channels_last"です。
他にも、ZCA白色化処理の選択に関係する'zca_whitening','zca_epsilon'など、 画像拡張の前に入力画像に対する処理指定のための引数'preprocessing_function'、 検証のために予約する画像の割合を指定する'validation_split'引数などがあります。
関数呼び出しの際に安全にそれぞれの仮引数に実引数を渡すためには、 関数定義で指定された全ての仮引数の順序に従って、対応する実引数を指定することです。しかし、 引数の数が多い場合、全ての引数を指定するのは煩わしい作業です。 一般に、関数定義の際には引数に対してデフォルト値を設定することがしばしば行われます。 従って、設定した引数のデフォルト値を積極的に用いることが有効です。 関数呼び出しにおいて、引数のデフォルト値を選択するということは、 その実引数を省略することに相当します。もし、実引数を省略した場合は、 関数定義における引数の順序関係が崩れます。そのため、 以降の実引数は省略によるデフォルト値選択か、 引数キーワードを使ったキーワード渡しを行わなければなりません。 もしキーワード渡しを選択した場合は、引数の順序は問いません。 引数の数が多い場合は引数キーワード渡しが一般に用いられますが、 引数キーワードとその値を辞書型データとして表現した上で、 このデータを一括して関数に引き渡すことも可能です。 ImageDataGeneratorオブジェクトの生成を辞書型データ表現の一括引数渡しで行う例を示します。
from keras.preprocessing.image import ImageDataGenerator
data_gen_args = { #引数キーと値を辞書型データとして表現
"featurewise_center" : True,
"featurewise_std_normalization" : True,
"rotation_range" : 90.0,
"width_shift_range" : 0.1,
"height_shift_range" : 0.1,
"zoom_range" : 0.2 }
image_gen = ImageDataGenerator(**data_gen_args)
ImageDataGenerator()の実引数として辞書型データに'**'を接頭語として付けることで、
辞書型データは引数キーワード渡しの形式に展開されます。
辞書型表現の部分は、直接dict()関数によって以下のようにも表すことができます。
data_gen_args = dict(
featurewise_center=True,
featurewise_std_normalization=True,
rotation_range=90.0,
width_shift_range=0.1,
height_shift_range=0.1,
zoom_range=0.2 )
上記の表現に含まれていない引数については、デフォルト値が採用されます。
このように、関数の引数としてプログラム中に埋没しがちなパラメータをまとめて
記述しておくことで、プログラムメンテナンスの効率化が期待されます。
次に、ImageDataGeneratorクラスに定義されているメソッドfit()とflow()について述べます。
fit()メソッドは、与えられたサンプルデータに基づいて、データに依存する幾つかの統計量を計算します。
これらの統計量は、学習を開始する前にデータの正規化を行う場合に用いられます。
関数の呼び出し、および関数の引数は以下のように表されます。
fit(x,augment=False, rounds=1, seed=None)
引数
- x : サンプルデータでnumpy型4次元配列。白黒画像はチャネル数1、カラー画像はチャネル数3。 'data_format'が"channels_last"であるとき、(samples, height, width, channels)の4次元
- augment: ランダムにサンプル拡張するかを決定する真理値。デフォルト値は'False'。
- rounds: augmentが与えられたときに利用データを拡張する回数デフォルトは1。
- seed: 乱数のシードで整数。デフォルトはNone。
もう一つのflow()メソッドは、ImageDataGeneratorオブジェクトの変換情報に基づいて、拡張画像の イテレータを作成します。使い方、および引数の内容は以下のようになります。
flow(x, y=None, batch_size=32, shuffle=True, seed=None, save_to_dir=None,
save_prefix='', save_format="png")
引数
- x: 4次元numpy型配列サンプルデータ。白黒画像はチャネル数1、カラー画像はチャネル数3
- y: サンプルデータのラベル
- batch_size: 1バッチ当たりのデータ数。整数でデフォルト値は32。
- shuffle:サンプルデータを拡張前にシャッフルするかを指定。真理値でデフォルトはTrue。
- seed: 乱数のシード。整数でデフォルト値はNone.
- save_to_dir: 生成された拡張画像を保存するディレクトリを指示。Noneまたは文字列。
- save_prefix: 画像を保存する際にファイルのプリフィックス。文字列。
- save_format: "png"か"jpeg"。save_to_dirに値が設定されているときのみ有効。
flow()メソッドの他にも、 外部記憶上の特定ディレクトリーに置かれた画像の拡張によってバッチを生成する 'flow_from_directory'メソッドや、 Pandasデータフレームからの画像を拡張してバッチを生成する 'flow_from_dataframe'メソッドが定義されています。 詳細はKerasドキュメントを参照してください。 ここでは、numpy型配列に格納されたサンプル画像を拡張する幾つかの例について触れていきます。 始めに、画像拡張の実験で使用した8枚の建物サンプル画像を図4-1に示します。
# coding: shift_jis
import os
import numpy as np
import matplotlib.pyplot as plt
from skimage import io
from keras.preprocessing.image import ImageDataGenerator
############## サンプル画像の読み込みとnumpy配列への格納 ##############
img_path = '../sampleImages/buildings' #個別の環境に依存
files = os.listdir(img_path)
images = []
for file in files:
img = io.imread(img_path + '/' + file).astype('float32')/255.0
img = img.reshape((1,) + img.shape)
images.append(img)
img_array = np.vstack(images) #numpy型4次元配列縦方向積算化
visualize_images(img_array, 8, 1, 8) #オリジナル画像表示(1行8列)
################ ImageDataGeneratorオブジェクトの生成 #################
my_datagen = ImageDataGenerator(
rotation_range = 10.0,
width_shift_range = 0.2,
height_shift_range = 0.2,
fill_mode = "constant",
shear_range = 0,
zoom_range = 0,
horizontal_flip = False,
vertical_flip = False)
############ ImageDataGeneratorオブジェクトによる画像拡張 #############
max_img_num = 40
augmented_imgs = []
for d in my_datagen.flow(img_array, batch_size=4, seed=0, shuffle=False):
augmented_imgs.append(d)
# datagen.flowは無限にループするため必要な枚数取得時点でループを抜ける
if (len(augmented_imgs) >= max_img_num) :
break
aug_imgs = np.vstack(augmented_imgs)
visualize_images(aug_imgs, 8, 5, 40) #拡張画像の表示(5行8列)
########### 画像拡張に使われたImageDataGeneratorのパラメータ ##########
for key, value in my_datagen.__dict__.items():
print(key,':', value)
一方、numpy型4次元配列として表されたサンプル画像とそのラベル配列を受け取り、
my_datagenオブジェクトの拡張規定に従って画像データのバッチを生成するのが、
メソッドflow()です。ここでは、flow()に対して、
'batch_size'キーの値を4としていますので、サンプル画像8枚はバッチ数2に相当します。
ただし、flow()関数には4次元化された画像配列のみが指定され、そのラベル配列は
指定されていませんので、for文中のイテレータからfor文変数'd'に与えられるのは
1バッチ分のnumpy型(4,240,320,3)の画像配列のみとなります。すなわち、
for文の繰り返しのたびに4枚の拡張画像が変数'd'に代入されています。
プログラムでは、flow()メソッドで生成される拡張画像の総数
が40を超えた時点で、for文による無限ループを抜け出します。
ループを抜けるまでに生成された40枚の画像、すなわち10バッチ分の画像をまとめて
1つのnumpy型配列として表示をしています。
その結果を示したのが図4-2です。
flow()メソッドでは、図4-1の8枚のサンプル画像を2つのバッチに分けて拡張画像の生成
が行われていますが、
'shuffle'キーにFalseが指示されているため、イテレータから生成される拡張画像の順番
はサンプル画像の順番と同じです。もし、'shuffle'キーにTrueを指定すれば、イテレータ
から生成される拡張画像の順番も変わってきます。
プログラムの最後では、実際の画像拡張に使用されたパラメータを、 確認のためにプリントしています。以下のような結果がプリントされます。
featurewise_center : False
samplewise_center : False
featurewise_std_normalization : False
samplewise_std_normalization : False
zca_whitening : False
zca_epsilon : 1e-06
rotation_range : 10.0
width_shift_range : 0.2
height_shift_range : 0.2
shear_range : 0
zoom_range : [1, 1]
channel_shift_range : 0.0
fill_mode : nearest
cval : 0.0
horizontal_flip : False
vertical_flip : False
rescale : None
preprocessing_function : None
dtype : float32
interpolation_order : 1
data_format : channels_last
channel_axis : 3
row_axis : 1
col_axis : 2
_validation_split : 0.0
mean : None
std : None
principal_components : None
brightness_range : None
次に、このデータ拡張処理を実際のネットワークの学習に用いるケースについて考えます。 以下は、mnist手書き数字データの読込処理と、 mnistデータの認識を行う最も基本的な階層型ニューラルネットワークをコード化 したものです。ネットワークの定義はKerasのSequentialクラスを用いて行っています。 ネットワークのメタパラメータは経験的に選択したもので、最適化等の検討は行って いません。
# coding: shift_jis
######################################################################
## 全結合3層ニューラルネットワークによる手書き数字認識 ##
## file_name: keras_mnist_dense_mode.py ##
######################################################################
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers.core import Dense,Dropout,Activation
from keras.optimizers import Adam
from keras.utils import np_utils
####################### 分類器のモデル生成 ###########################
model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dropout(0.3))
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.3))
model.add(Dense(10))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',optimizer=Adam(),
metrics=['accuracy'])
################## MNISTデータの読込とデータ変換 #####################
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(-1,784).astype('float32')/255
X_test = X_test. reshape(-1,784).astype('float32')/255
y_train = np_utils.to_categorical(y_train,10)
y_test = np_utils.to_categorical(y_test, 10)
################### 学習に使用するデータの選択 #######################
x_train0 = X_train[0:1000]
y_train0 = y_train[0:1000]
mnistのデータセットには学習用に60,000種、テスト用に10,000種のパターンが
収納されています。通常、学習用のパターン全てを用いて分類器の学習を行いますが、
ここでは、学習用データが十分ではない状況を想定しますので、学習用全データセット
の中の先頭から1000パターンを選択し、それを以降の学習データとして利用します。
学習モデルは上記で定義した階層型全結合モデルとし、選択した1000データのみで
学習する方式、および、1000データにデータ拡張を行う学習方式の比較を行ってみる。
固定データで学習するコードを以下に示す。学習データ数が少ないためバッチサイズを
50としていますが、最適化等による選択ではありません。エポック数についても
5箇所のみで評価していますが、学習の飽和状況の判断ができればと考えての選択です。
############# データ拡張無しでのネットワークの学習と評価 #############
import time
epochs_opt = [20,30,40,50,80]
batch_size = 50
start = time.time()
for epochs in epochs_opt:
model.fit(x_train0, y_train0, epochs=epochs, batch_size=batch_size,verbose=0)
score= model.evaluate(X_test, y_test,verbose=1)
print(f'epochs='{epochs} accuracy={score[1]:.5f}');
elapsed_time = time.time() - start
print(f'elapsed_time:{elapsed_time:.3f} seconds')
一方、データ拡張有の学習に関しては、もともとmnistデータは位置の正規化が行われて
いることから、大きな位置ずれはないであろうと考え、
上下左右方向の移動量を最大1割程度と想定しました。回転角度についても
10度程度までを許容範囲としています。また、
上下・左右の反転は拡張データの中には含めていません。このような条件を考慮の上で
ImageDataGeneratorオブジェクトを生成したのが以下の部分です。
############### ImageDataGeneratorオブジェクトの生成 #################
from keras.preprocessing.image import ImageDataGenerator
my_datagen = ImageDataGenerator(
rotation_range = 10.0,
width_shift_range = 0.1,
height_shift_range = 0.1,
fill_mode = "nearest",
shear_range = 0,
zoom_range = 0,
horizontal_flip = False,
vertical_flip = False)
ImageDataGeneratorクラスのインスタンスオブジェクト'my_datagen'と
flow()メソッドから成るイテレータは、既に述べたように無限のバッチデータ
を生成します。基本的にこのバッチデータをネットワークのモデルオブジェクト
modelのfit()関数に引き渡すことで学習が行われていきますが、ここで注意すべき
点として、イテレータが生成するバッチデータの構造とネットワークモデルが
受け取る入力データの構造の整合性があります。すなわち、イテレータから
生成されるバッチデータは、(batch_size, height, width, channels)の構造を持つ
numpy型4次元配列であるのに対して、ネットワークの入力層にインプットされる
データの構造は(batch_size, height*width, channels)の3次元配列でなければなりません。
したがって、イテレータからネットワークモデルへバッチデータを渡す段階で、
データのリサイズが必要になります。mnistデータセットから読み込んだ時点で
numpy画像データ(4次元)がベクトルデータ(3次元)になっていることから、
ジェネレータには4次元化して渡したうえで、イテレータから出力される4次元
データをmodelのfit()関数に渡す段階で3次元への変換を行っています。
バッチサイズは、データ拡張無しの場合と同じに設定しています。以下は、その
コードです。
############# データ拡張ありでのネットワークの学習と評価 #############
import time
x_train0 = x_train0.reshape(-1,28,28,1)
epochs_opt = [20,30,40,50,80]
batch_size = 50
start = time.time()
for epochs in epochs_opt:
for epoch in range(epochs):
batches = 0
for x_batch, y_batch in my_datagen.flow(x_train0, y_train0,
batch_size=batch_size):
model.fit(x_batch.reshape(-1,784), y_batch, verbose=0)
batches += 1
if batches >= len(x_train0)/batch_size:
break
score = model.evaluate(X_test, y_test,verbose=0)
print(f'epochs={epochs} accuracy= {score[1]:.5f}');
elapsed_time = time.time() - start
print(f'elapsed_time:{elapsed_time:.2f} seconds')
両プログラムともテスト用には10,000個の未学習データを使用し、エポック数を
20,30,40,50,80と増やして学習を行った場合の確信度を求めてみました。また、
両実験とも、すべての計算を終えるまでの時間を計測しました。
その結果を以下の表4-2および表4-3に示します。
| epoch数 | 確信度 |
|---|---|
| epochs=20 | acc=0.89850 |
| epochs=30 | acc=0.90100 |
| epochs=40 | acc=0.90860 |
| epochs=50 | acc=0.89900 |
| epochs=80 | acc=0.91360 |
| epoch数 | 確信度 |
|---|---|
| epochs=20 | acc=0.91450 |
| epochs=30 | acc=0.93580 |
| epochs=40 | acc=0.93980 |
| epochs=50 | acc=0.94590 |
| epochs=80 | acc=0.94650 |
データ拡張無しの場合は、学習回数を増やしても認識率 の向上が見られないことから、明らかに学習データが不足していると言えます。 一方、データ拡張有りの場合は、学習回数の増加に応じて認識率の向上が見られます。 バッチサイズおよびバッチ数が同じであっても、データ拡張によって学習パターンの 多様性が広がったことが理由であると考えられます。 しかし、処理時間については、画像の拡張処理やサイズ変換等の処理が増えるため、 結果として、2倍強の処理時間を要しています。