2017年9月10日日曜日

入力データ加工の効果

MNISTで学習するときの、入力画像の加工について実験をしてみます。
加工方法は2種類準備しました。1つは変形、もう1つは一部の消去です。

画像の変形については、Kerasに付属のImageDataGeneratorクラスを利用します。 パラメータは次のように設定しました。

datagen = ImageDataGenerator(
        rotation_range=20,
        width_shift_range=0.2,
        height_shift_range=0.2,
        shear_range=math.pi/4, # 45 degree
        zoom_range=0.3,
        fill_mode="constant",
        cval=-1, # constant value for fill_mode
        )
これにより、MNISTの学習用画像を回転・移動・シアー・拡大縮小させます。

画像の一部の消去については、ランダムに8x8の四角形の領域を背景色に塗りつぶすようにしました。数字の一部が薄かったり、線の一部が見えない場合の画像の認識に対してロバストになることを期待しています。 この処理を行えるKerasに付属のクラスがなかったため、MaskedImageGeneratorというクラスをImageDataGeneratorを参考にして作成しました。詳細は、最後に示すコードで確認してください。

オプティマイザは、

Adamax(lr=0.002, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=1e-4)
を利用しました。今回利用している2層CNN+2層DenseのモデルでMNISTの学習をするときに、性能が良い傾向があるためです(以前の実験を参照)。

それでは、まず、学習時のLossの変化を下図に示します。

加工なしの場合(No augmentation)、Lossが非常に小さくなります。一方、変形(ImageDataGenerator)・一部消去(MaskedImageGenerator)については学習用データがランダムに変化するため、なかなか低くはなりません。

次に、Validation errorの経過を下図に示します。

変形については加工なしに比べてエラーが大きく、今回の実験に関しては、学習用画像の変形は有効な手法ではないことが分かります。 一方、画像の一部消去については、加工なしと同じか、若干エラーが低いように見えます。統計的に有意な差があるかまでは調べていません。

加工する場合のコードを以下に示します。加工なしの場合については、冗長なので省略しますが、画像の加工がない以外の差はありません。違いは、fit_generatorではなくfitを利用するようにする部分のみです。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# This script trains a neural network model using MNIST with image augmentation
import gc, keras, os, re, datetime, math
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, Reshape
from keras.layers.convolutional import Conv2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers import Flatten, Dropout
from keras.datasets import mnist
from keras.preprocessing.image import ImageDataGenerator, NumpyArrayIterator

TRAIN_EPOCH = 100
BATCH_SIZE = 32
MASK_WIDTH = 8  # Width of the region to be erased
MASK_HEIGHT = 8 # Height of the region to be erased
USE_MASKED_IMAGE_GENERATOR = True # If False, use ImageDataGenerator

# Image size of MNIST
IMAGE_WIDTH = 28
IMAGE_HEIGHT = 28

# Define a discriminator model for numbers
def num_discriminator_model():
    model = Sequential()
    model.add(Conv2D(64, (5, 5), strides=(2, 2), padding='same',
                     input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, 1),
                     data_format='channels_last'))
    model.add(LeakyReLU(0.2))
    model.add(Conv2D(128, (5, 5), strides=(2, 2), data_format='channels_last'))
    model.add(LeakyReLU(0.2))
    model.add(Flatten())
    model.add(Dense(256))
    model.add(LeakyReLU(0.2))
    model.add(Dropout(0.5))
    model.add(Dense(10))
    model.add(Activation('softmax'))
    print(model.summary())
    return model

class MaskIterator(NumpyArrayIterator):
    def __init__(self, x, y, masked_image_generator,
                 batch_size=32, shuffle=False, seed=None,
                 data_format='channels_last',
                 save_to_dir=None, save_prefix='', save_format='png'):
        super(MaskIterator, self).__init__(x, y, masked_image_generator,
                                           batch_size, shuffle, seed,
                                           data_format, save_to_dir,
                                           save_prefix, save_format)

# Erase the part of an image randomly
class MaskedImageGenerator(ImageDataGenerator):
    def __init__(self, mask_width=MASK_WIDTH, mask_height=MASK_HEIGHT):
        self.mask_width = mask_width
        self.mask_height = mask_height

        # channels_last
        self.channel_axis = 3
        self.row_axis = 1
        self.col_axis = 2

    def flow(self, x, y=None, batch_size=32, shuffle=True, seed=None,
             save_to_dir=None, save_prefix='', save_format='png'):
        return MaskIterator(
            x, y, self,
            batch_size=batch_size,
            shuffle=shuffle,
            seed=seed,
            data_format='channels_last',
            save_to_dir=save_to_dir,
            save_prefix=save_prefix,
            save_format=save_format)

    def random_transform(self, x, seed=None):
        img_row_axis = self.row_axis - 1
        img_col_axis = self.col_axis - 1
        img_channel_axis = self.channel_axis - 1
        if seed is not None:
            np.random.seed(seed)

        # Select the upper left corner of the box to be erased
        row = np.random.randint(x.shape[img_row_axis] - self.mask_height)
        col = np.random.randint(x.shape[img_col_axis] - self.mask_width)
        for r in range(row, row + self.mask_height):
            for c in range(col, col + self.mask_width):
                x[r][c] = -1 # Fill a pixel by background color
        return x

    def standardize(self, x):
        return x

def format_x(x):
    x = (x.astype(np.float32) - 127.5)/127.5 # [0,255] --> [-1,1]
    x = x.reshape((x.shape[0], x.shape[1], x.shape[2], 1))
    return x

def train_by(model, opt, datagen, fname):
    model.compile(optimizer=opt,
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
    mc = keras.callbacks.ModelCheckpoint(filepath="models/ndisc_"+fname+"-{epoch:02d}.hdf5")
    hist = model.fit_generator(datagen.flow(x_train, y_train, batch_size=BATCH_SIZE),
                               steps_per_epoch=len(x_train)/BATCH_SIZE, epochs=TRAIN_EPOCH,
                               validation_data=(x_val, y_val),
                               callbacks=[mc]).history

    # Evaluate the last model by train and validation data
    score_train = model.evaluate(x_train, y_train, batch_size=32)
    score_val = model.evaluate(x_val, y_val, batch_size=32)

    # Write training history into a file
    with open("history.input-mask.log", mode="a") as f:
        d = datetime.datetime.today()
        f.write("#"+d.strftime("%Y-%m-%d %H:%M:%S")+"\n")
        f.write("#Data gen type : "+str(type(datagen))+"\n")
        f.write("#Data gen params : "+str(vars(datagen))+"\n")
        f.write("#Optimizer :"+str(type(opt))+str(opt.get_config())+"\n")
        f.write("#Data in the last line are calculated by the last model"
                " and not calculated in model.fit()\n")
        f.write("#epoch train-loss train-acc val-loss val-acc\n")
        for v in range(0, TRAIN_EPOCH):
            f.write("{0} {1:10.6f} {2:10.6f} {3:10.6f} {4:10.6f}\n"
                    .format(v, hist["loss"][v], hist["acc"][v],
                            hist["val_loss"][v], hist["val_acc"][v]))
        f.write("{0} {1:10.6f} {2:10.6f} {3:10.6f} {4:10.6f}\n"
                .format(v+1, score_train[0], score_train[1],
                        score_val[0], score_val[1]))
        f.write("\n\n")
    model.save("models/ndisc_"+fname+"-final.hdf5")

if __name__ == '__main__':
    if not os.path.exists("models"):
        os.mkdir("models")

    # Load MNIST data
    (x_train, y_train), (x_val, y_val) = mnist.load_data()
    x_train = format_x(x_train)
    x_val = format_x(x_val)

    # Encode labels into 1-hot vectors
    y_train = keras.utils.to_categorical(y_train, num_classes=10)
    y_val = keras.utils.to_categorical(y_val, num_classes=10)

    # Select MaskedImageGenerator or ImageDataGenerator
    if USE_MASKED_IMAGE_GENERATOR:
        fname = "input-mask-w{0}-h{1}".format(MASK_WIDTH, MASK_HEIGHT)
        datagen = MaskedImageGenerator()
    else:
        fname = "image-gen"
        datagen = ImageDataGenerator(
            rotation_range=20,
            width_shift_range=0.2,
            height_shift_range=0.2,
            shear_range=math.pi/4, # 45 degree
            zoom_range=0.3,
            fill_mode="constant",
            cval=-1, # constant value for fill_mode
            )

    # Make and train a model
    model = num_discriminator_model()
    o = keras.optimizers.Adamax(lr=0.002, beta_1=0.9, beta_2=0.999,
                                epsilon=1e-08, decay=1e-4)
    train_by(model, o, datagen, fname)
    gc.collect() # To suppress error messages of TensorFlow

0 件のコメント :