2017/09/09

Kerasのオプティマイザの比較

Kerasのオプティマイザを比較します。データはMNIST、モデルは、フォントの学習時に使った2層のCNN+2層のFCです。 10エポックのみですので、もっと長く学習させると異なる結果となるかもしれません。

比較結果は下表の通りです。

Optimizer Decay Learning
rate
Momentum Epsilon Nesterov Rho Beta_1 Beta_2 Schedule
decay
Train
Validation
Loss Error Loss Error
SGD 0 0.01 0 n/a False n/a n/a n/a n/a 0.03682 1.12%
0.04107 1.31%
SGD 0 0.01 0.9 n/a False n/a n/a n/a n/a 0.01186 0.40%
0.03228 1.04%
SGD 1e-4 0.01 0 n/a False n/a n/a n/a n/a 0.04970 1.50%
0.05264 1.69%
SGD 1e-4 0.01 0.9 n/a False n/a n/a n/a n/a 0.00687 0.16%
0.02503 0.91%
SGD 0 0.01 0 n/a True n/a n/a n/a n/a 0.03496 1.06%
0.04069 1.30%
SGD 0 0.01 0.9 n/a True n/a n/a n/a n/a 0.01203 0.40%
0.03281 0.97%
SGD 1e-4 0.01 0 n/a True n/a n/a n/a n/a 0.05123 1.53%
0.05515 1.74%
SGD 1e-4 0.01 0.9 n/a True n/a n/a n/a n/a 0.00730 0.20%
0.02470 0.84%
RMSprop 0 0.001 n/a 1e-8 n/a 0.9 n/a n/a n/a 0.02124 0.62%
0.06075 1.51%
RMSprop 1e-4 0.001 n/a 1e-8 n/a 0.9 n/a n/a n/a 0.00596 0.18%
0.04909 1.11%
Adagrad 0 0.01 n/a 1e-8 n/a n/a n/a n/a n/a 0.00734 0.17%
0.02752 0.93%
Adagrad 1e-4 0.01 n/a 1e-8 n/a n/a n/a n/a n/a 0.01216 0.31%
0.02688 0.98%
Adadelta 0 1 n/a 1e-8 n/a 0.95 n/a n/a n/a 0.01282 0.41%
0.03215 1.10%
Adadelta 1e-4 1 n/a 1e-8 n/a 0.95 n/a n/a n/a 0.01258 0.33%
0.02489 0.88%
Adam 0 0.001 n/a 1e-8 n/a n/a 0.9 0.999 n/a 0.02548 0.65%
0.06531 1.35%
Adam 1e-4 0.001 n/a 1e-8 n/a n/a 0.9 0.999 n/a 0.00425 0.14%
0.04176 0.96%
Adamax 0 0.002 n/a 1e-8 n/a n/a 0.9 0.999 n/a 0.00696 0.21%
0.02920 0.91%
Adamax 1e-4 0.002 n/a 1e-8 n/a n/a 0.9 0.999 n/a 0.00435 0.09%
0.02528 0.88%
Nadam n/a 0.002 n/a 1e-8 n/a n/a 0.9 0.999 0.004 4.64530 28.83%
4.52680 28.09%

各オプティマイザのパラメータを調整すれば、もっと良い結果が得られるかもしれません。 今回の結果から、10エポック目のモデルで一番良いのはSGDのdecayとmomentumをそれぞれ1e-4と0.9に設定したもので、エラーは0.84%でした。 Decayを1e-4に設定したAdadeltaとAdamaxがその次に良く、エラーは0.88%でした。

エポックごとのValidation errorの変化を下図に示します。

エラーの低下の過程を見ると、decay=1e-4のAdamaxが収束の速さの観点では一番良いようです。 理由は不明ですが、Nadamはなぜか学習を進めるにつれてエラーが増えていくという不思議な挙動をしています。

評価に使ったコードは以下の通りです。

  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
# This script trains a neural network model using MNIST.
import gc, keras, os, re, datetime
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

TRAIN_EPOCH = 10

# 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

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):
    model.compile(optimizer=opt,
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
    hist = model.fit(x_train, y_train, validation_data=(x_val, y_val),
                     epochs=TRAIN_EPOCH, batch_size=32).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.log", mode="a") as f:
        d = datetime.datetime.today()
        f.write("#"+d.strftime("%Y-%m-%d %H:%M:%S")+"\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")

if __name__ == '__main__':
    # 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)

    # Make a model
    model = num_discriminator_model()

    # Save initial weights which will be used to reset weights
    model.save_weights("initial_weights.hdf5") # 

    # Make a list of optimizers
    opt = []
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=False))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=0.0, nesterov=False))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0,   decay=1e-4, nesterov=False))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=1e-4, nesterov=False))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=True))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=0.0, nesterov=True))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0,   decay=1e-4, nesterov=True))
    opt.append(keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=1e-4, nesterov=True))

    opt.append(keras.optimizers.RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0))
    opt.append(keras.optimizers.RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=1e-4))

    opt.append(keras.optimizers.Adagrad(lr=0.01, epsilon=1e-08, decay=0.0))
    opt.append(keras.optimizers.Adagrad(lr=0.01, epsilon=1e-08, decay=1e-4))

    opt.append(keras.optimizers.Adadelta(lr=1.0, rho=0.95, epsilon=1e-08, decay=0.0))
    opt.append(keras.optimizers.Adadelta(lr=1.0, rho=0.95, epsilon=1e-08, decay=1e-4))

    opt.append(keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999,
                                     epsilon=1e-08, decay=0.0))
    opt.append(keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999,
                                     epsilon=1e-08, decay=1e-4))

    opt.append(keras.optimizers.Adamax(lr=0.002, beta_1=0.9, beta_2=0.999,
                                       epsilon=1e-08, decay=0.0))
    opt.append(keras.optimizers.Adamax(lr=0.002, beta_1=0.9, beta_2=0.999,
                                       epsilon=1e-08, decay=1e-4))

    opt.append(keras.optimizers.Nadam(lr=0.002, beta_1=0.9, beta_2=0.999,
                                      epsilon=1e-08, schedule_decay=0.004))
    # Train for each optimizer
    for o in opt:
        model.load_weights("initial_weights.hdf5") # Initialize weights
        train_by(model, o)
    gc.collect() # To suppress error messages of TensorFlow

分類に失敗した画像の出力

MNISTの画像の識別器を学習させていると、精度(accuracy)だけではなく、誤って識別した画像の一覧を見たい場合があります。

以下のコードを使うと、学習済みのKerasのモデルをロードし、識別し、誤った画像を.png形式で出力できます。"mnist-tranied.hdf5" の部分は読み込みたいモデルのファイルパスに書き換えてください。

 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
# This script loads a trained model and outputs error images.
import gc, keras, os
import numpy as np
from keras.models import load_model
from keras.datasets import mnist
from PIL import Image, ImageDraw

IMAGE_PATH = 'error-images'
if not os.path.exists(IMAGE_PATH):
    os.mkdir(IMAGE_PATH)

# Load MNIST data for validation
(_, _), (x_val, y_val) = mnist.load_data()
x_val = (x_val.astype(np.float32) - 127.5)/127.5 # [0,255] --> [-1,1]
x_val = x_val.reshape((x_val.shape[0], x_val.shape[1], x_val.shape[2], 1))

# Predict classes for MNIST validation data
model = load_model("mnist-tranied.hdf5")
pred = model.predict_classes(x_val)

# Write error images
for i, (p, y) in enumerate(zip(pred, y_val)):
    if p != y:
        image = x_val[i]*127.5 + 127.5 # [-1,1] --> [0,255]
        img = Image.fromarray(image.reshape((image.shape[0], image.shape[1])).astype(np.uint8))
        img.save(IMAGE_PATH+"/{0}-c{1}-p{2}.png".format(i, y, p)) # c=correct, p=predict

数字フォントでモデル学習で使ったニューラルネットワークにMNISTの訓練用データを入力して学習したモデルを使って、上記のコードを走らせると、どうして認識できないのだろうという画像と、これは無理だろうという画像などが目で見て分かるようになります。

例えば、

正解は1
正解は7
正解は6

は人間でも難しいでしょう。

2017/09/02

数字画像の水増し

ただ単にフォントで描いた画像で学習したモデルではMNISTの手書き数字はうまく認識できなかったので、フォントで描いた画像を加工して水増しする方法(Data augmentation)を試してみます。

画像の加工には、KerasのImageDataGeneratorを使います。前回のコードとほぼ同じですが、加工部分が異なっています。

  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
# This script trains a neural network model from font-based number images.
# Image data are augmented by changing their shape.
# The trained model is evaluated by validation data of MNIST.
import gc, keras, math, os, re
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, Reshape
from keras.layers.convolutional import Conv2D
from keras.preprocessing.image import ImageDataGenerator
from keras.layers.advanced_activations import LeakyReLU
from keras.layers import Flatten, Dropout
from keras.datasets import mnist
from PIL import Image, ImageDraw, ImageFont # For drawing font images

FONT_DIR = "C:/Windows/Fonts"
TRAIN_EPOCH = 30
BATCH_SIZE = 32

# Image size is the same as MNIST
IMAGE_WIDTH = 28
IMAGE_HEIGHT = 28

# Make images and labels of 0-9 by using the font specified by font_name
def make_image(font_name):
    font = ImageFont.truetype(FONT_DIR+"/{}".format(font_name), 25)
    images = np.empty((0, IMAGE_WIDTH, IMAGE_HEIGHT))
    labels = np.empty((0, 1))
    for i in range(10): # Draw 0-9 as image
        image = Image.new('RGB', (IMAGE_WIDTH, IMAGE_HEIGHT), (0, 0, 0))
        draw = ImageDraw.Draw(image)
        font_width, font_height = font.getsize(str(i))
        draw.text(((IMAGE_WIDTH-font_width)/2, (IMAGE_HEIGHT-font_height)/2),
                  str(i), font=font, fill=(255, 255, 255))
        ni = np.delete(np.asarray(image), [1, 2], 2) # Remove green and blue
        ni = ni.reshape(1, ni.shape[0], ni.shape[1]) # Convert to 1x28x28 matrix
        images = np.append(images, ni, axis=0)
        labels = np.append(labels, np.array([i]).reshape(1, 1), axis=0)
    return images, labels

# Collect images of numbers for all fonts
def make_images():
    # Collect only true type fonts including numbers
    rp = re.compile(".*ttf")
    font_list = [f for f in os.listdir(FONT_DIR) if rp.match(f)
                 and (f != "webdings.ttf" and f != "wingding.ttf" and 
                      f != "marlett.ttf" and f != "opens___.ttf" and
                      f != "symbol.ttf")]
    images = np.empty((0, IMAGE_WIDTH, IMAGE_HEIGHT))
    labels = np.empty((0, 1))
    for font_name in font_list:
        fimages, flabels = make_image(font_name)
        images = np.append(images, fimages, axis=0)
        labels = np.append(labels, flabels, axis=0)
    return images, labels

# 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

if __name__ == '__main__':
    # Make images of numbers from fonts
    x_train, y_train = make_images()
    x_train = (x_train.astype(np.float32) - 127.5)/127.5 # [0,255] --> [-1,1]
    x_train = x_train.reshape((x_train.shape[0], x_train.shape[1], x_train.shape[2], 1))

    # Load MNIST data for validation
    (_, _), (x_val, y_val) = mnist.load_data()
    x_val = (x_val.astype(np.float32) - 127.5)/127.5 # [0,255] --> [-1,1]
    x_val = x_val.reshape((x_val.shape[0], x_val.shape[1], x_val.shape[2], 1))

    # 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)

    # For data augmentation
    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, compile and train a model
    model = num_discriminator_model()
    model.compile(optimizer='rmsprop',
                loss='categorical_crossentropy',
                metrics=['accuracy'])
    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)).history

    # Evaluate the model by train and validation data
    score = model.evaluate(x_train, y_train, batch_size=32)
    print("\ntrain acc : ", score)
    score = model.evaluate(x_val, y_val, batch_size=32)
    print("\nmnist val acc : ", score)

    # Write training history into a file
    f = open("history.dat", mode="w")
    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.close()
    gc.collect() # To suppress error messages of TensorFlow
30エポック学習させたモデルを使ったときのLossとAccuracyは次のようになりました。
AugmentationLossAccuracy
TrainOff0.002670.99852
TrainOn0.091110.97811
MNISTOff6.101930.52960
MNISTOn1.019090.71590
学習データを加工しない場合に比べて、ずいぶんMNISTの結果がよくなりました。一方で、学習データに対する性能は低下しています。

では、学習中のLossの変化を見てみましょう。

学習データの加工なしの場合に比べて、MNISTのLossもある程度は低下しています。

学習中のAccuracyの変化は次のようになりました。

MNISTに関しては、ざっくり70%くらいの精度でしょうか。データ加工なしの場合が約50%だったので、結構改善しました。