日本の古文書で機械学習を試す(11) 転移学習を試す

Kerasから使える学習済みモデルVGG16を使って、転移学習を試してみる。VGG16は大規模画像データセットImageNet(約120万枚)を学習させた学習済みCNNモデルだ。

###モデル生成

今回は、最後の結合層のみを、古文書の文字画像データを使って再学習させる。それ以外の層は変更しない。まず、VGG16のモデルをダウンロードして、入力画像のサイズを設定する。

今まで28x28ピクセルの画像を使ってきたが、VGG16では最低32x32ピクセルにしないとエラーが出るので、32x32とする。また Inputの3つめの引数を1(グレイスケール)にするとエラーが出たので、3(RGB)に設定する。

```python:VGG16モデル生成
from keras.applications.vgg16 import VGG16
from keras.layers import Input

img_rows = 32
img_cols = 32
input_tensor = Input(shape=(img_rows, img_cols, 3))
base_model_v = VGG16(include_top=False,  # 最後の全結合層は読み込まない
                     weights='imagenet',
                     input_tensor=input_tensor)
```

初回のみ import VGG16 のところでモデルがダウンロードされるので、少し時間がかかる。2回目以降はローカルから読み込まれるので速くなる。

次に、最後の全結合層を定義する。判別するクラス数は、前回[日本の古文書で機械学習を試す(10)](https://qiita.com/ariera/items/02516b84a57b65a17894)と同じ学習データを使う予定なので、前回数えた3578とする。結合層Denseの次元数とDropputの係数は、参考にした複数のサイトの中で一番多かったものを適当に選択した。

```python:全結合層定義
from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Activation, Flatten

nb_classes = 3578
fc_model = Sequential()
fc_model.add(Flatten(input_shape=base_model_v.output_shape[1:]))
fc_model.add(Dense(256))
fc_model.add(Activation("relu"))
fc_model.add(Dropout(0.5))
fc_model.add(Dense(nb_classes))
fc_model.add(Activation("softmax"))
```

最後に、読み込んだVGG16のモデルと、作成した全結合層を組み合わせてモデルを作る

```python:VGG16と全結合層を組み合わせ
# 読み込んだVGG16と、定義した全結合層を連結
model_t = Model(inputs=base_model_v.input, outputs=fc_model(base_model_v.output))

# base_model_vの各層の重みを固定する(VGG15)
for layer in base_model_v.layers:
    layer.trainable = False

# モデルのコンパイル
model_t.compile(loss='categorical_crossentropy', # 多クラス分類なので損失関数は categorical_crossentropy とする
              optimizer='adam',                  # 最適化関数はadam
              metrics=['accuracy'])              # accuracyを最適化(最大化)する
```

できたモデルをmodel_t.summary()で表示させるとこうなった。前回よりかなり複雑なモデルになっている。

```
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 32, 32, 3)         0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 32, 32, 64)        1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 32, 32, 64)        36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 16, 16, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 16, 16, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 16, 16, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 8, 8, 128)         0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 8, 8, 256)         295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 8, 8, 256)         590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 8, 8, 256)         590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 4, 4, 256)         0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 4, 4, 512)         1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 4, 4, 512)         2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 4, 4, 512)         2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 2, 2, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 1, 1, 512)         0         
_________________________________________________________________
sequential_1 (Sequential)    (None, 3578)              1050874   
=================================================================
Total params: 15,765,562
Trainable params: 1,050,874
Non-trainable params: 14,714,688
_________________________________________________________________
```


###学習データ、テストデータ生成

学習データ、テストデータは前回[日本の古文書で機械学習を試す(10)](https://qiita.com/ariera/items/02516b84a57b65a17894)のものをそのまま使おうと思ったのだが、画像サイズとグレイスケール／RGBが違うので、再度作成し直すことにした。

データサイズが28x28→32x32で1.3倍、グレイスケール→RGBで3倍になったためか、前回までのコードをそのまま32x32、RGBに変えただけでは、listからnumpy.ndarrayに変換するところでメモリエラーが出た。そのため、numpy.ndarrayに変換するときのデータ型をfloat(デフォルト64bit)から、float32に変更し、さらに1作品ずつ読み込んでnumpy.ndarrayに変換し、最後に5作品分を連結した。

前回までのコードを活かして修正したので、グローバル変数を使ったちょっと気持ち悪いコードになってしまった…

```python:学習データ、テストデータ作成
# 画像を読み込んで、行列に変換する関数を定義
from keras.preprocessing.image import load_img, img_to_array
def img_to_traindata(file, img_rows, img_cols, rgb):
    if rgb == 0: # グレイスケールのとき
        img = load_img(file, color_mode = "grayscale", target_size=(img_rows,img_cols)) # grayscaleで読み込み
    else: # RGBのとき
        img = load_img(file, color_mode = "rgb", target_size=(img_rows,img_cols)) # RGBで読み込み
    x = img_to_array(img)
    x = x.astype('float32')
    x /= 255
    return x

# 5作品の文字画像データのルートディレクトリ
img_roots = ["../200003076/characters/*", "../200014740/characters/*", "../200003967/characters/*", "../200021660/characters/*",  "../brsk00000/characters/*"]
char_index_dict = {} # 文字コード　-> 画像番号の辞書
nb_classes = 0       # 文字種数

# 1作品中の画像データを読み込む関数を定義
import glob, os
import numpy as np
def get_image_files(img_root):
    global char_index_dict
    global nb_classes
    X_ret = []
    Y_ret = []
    chars = glob.glob(img_root) # 画像のルートディレクトリ内のファイル／ディレクトリ一覧
    for char in chars:
        if os.path.isdir(char): # ディレクトリなら(ディレクトリ名はUTF-8文字コード)
            
            char_code =os.path.basename(char) # 文字コード = ディレクトリ名
            if(char_code in char_index_dict.keys()): # 既に辞書にある文字コードなら
                y = char_index_dict[char_code]
            else: # 辞書にない文字コードなら
                y = nb_classes
                char_index_dict[char_code] = y # 文字種から番号を調べるためのdict
                nb_classes = nb_classes + 1 # 判別文字種数をインクリメント
             
            img_files = glob.glob(char+"/*.jpg") # ディレクトリ内の画像ファイルを全部読み込む
            for img_file in img_files:           # ディレクトリ(文字種)内の全ファイルに対して
                x = img_to_traindata(img_file, img_rows, img_cols, 1) # 各画像ファイルを読み込んで行列に変換
                X_ret.append(x) # 学習用データ(入力)に画像を変換した行列を追加
                Y_ret.append(y) # 学習用データ(出力)に正解の文字種番号を追加

    # 学習データをlistからnumpy.ndarrayに変換
    X_ret = np.array(X_ret, dtype='float32')
    Y_ret = np.array(Y_ret, dtype='int16')
    return [X_ret, Y_ret]

# 5作品分のデータを連結 
[X_train, Y_train] = get_image_files(img_roots[0])
for i in range(len(img_roots)-1):
    d = get_image_files(img_roots[i+1])
    X_train = np.concatenate([X_train, d[0]], 0)
    Y_train = np.concatenate([Y_train, d[1]], 0)

# テストデータ作成
# 「当世料理」のデータのうち、学習データにある文字種のみをテストデータとする
X_test = []
Y_test = []
img_root = "../200021637/characters/*"
chars = glob.glob(img_root)
for char in chars:
    if os.path.isdir(char) and os.path.basename(char) in char_index_dict:  # 学習データにある文字コード
        img_files = glob.glob(char+"/*.jpg")
        for img_file in img_files:
            x = img_to_traindata(img_file, img_rows, img_cols, 1)        # 各画像ファイルを読み込んで行列に変換
            X_test.append(x)                                          # テストデータ(入力)＝画像データ
            Y_test.append(char_index_dict[os.path.basename(char)])    # テストデータ(出力)＝正解の文字種番号

# テストデータをlistからnumpy.ndarrayに変換
X_test = np.array(X_test, dtype='float32')
Y_test = np.array(Y_test, dtype='int16')

# 文字種を表す数字をカテゴリカルデータ(ベクトル)に変換
from keras.utils import np_utils
Y_train = np_utils.to_categorical(Y_train, nb_classes)
Y_test = np_utils.to_categorical(Y_test, nb_classes)

# 作成した学習データ、テストデータをファイル保存(後で使う可能性を考えて)
np.save('X_train_rgb_float32.npy', X_train)
np.save('X_test_rgb_float32.npy', X_test)
np.save('tY_train_rgb.npy', Y_train)
np.save('Y_test_rgb.npy', Y_test)

# Y_trainの型を表示
print(Y_train.shape)
```

最後のprintの結果は、(227975, 3578)、学習データ数は227975個、判別する文字数は3578種類 となった。

###学習とFailedPreconditionErrorでハマった話

モデルとデータの準備ができたので、学習させてみる。

前回までと同じように、再現性を得るために乱数の種を固定する関数を定義し、学習結果のモデルを保存するコールバックを定義して、学習させてみた。

が、ここでドツボにはまった。
model_t.fitのところで、学習をスタートしようとして、
FailedPreconditionError: Attempting to use uninitialized value ...
という謎のエラーが出た。

googleで調べてみると、tensorflowのグローバル変数が初期化されていない、ということらしい。対策としては、tf.global_variables_initializerで変数を初期化するということが書かれていたが、そもそもtensorflowを直接いじってはいないので、何を初期化すべきなのか分からない。

tensorflowの関数を直接呼び出しているのは、set_reproducible関数定義内の、session_conf = tf.ConfigProto以下だけなので、関数定義の最後の行 K.set_session(sess) の手前に、
sess.run(tf.global_variables_initializer()) 
の1行を入れてみた。

その結果、エラーが出なくなって、学習がはじまったが、10エポック進んでも学習データのaccが0.15、テストデータのaccが0.06と、おそろしく精度が悪い。何か初期化してはいけないものを初期化してしまったのだろうか。

次に、initializeのコードは削除し、set_reproducible関数を呼ぶのをやめて動かしてみたら、エラーは出ずに学習が始まって、1エポック目のテストデータのaccが0.14となった。(このとき、jupyterのnotebookを一度閉じて、再度開かないと色々変なことになった。)

set_reproducible関数の中でエラーが出ていることが分かったので、1行ずつコメントアウトして試したところ、tf.session関係を全部コメントアウトすると、エラーは出なくなった。

実は、set_reproducible関数の中身は、[KerasのFAQ](ttps://keras.io/ja/getting-started/faq/#how-can-i-obtain-reproducible-results-using-keras-during-development)をコピーしただけで、中身をよく理解していない。今のところ、厳密に再現性を確保する必要はないので、tf.session関係をコメントアウトした状態で学習させることにする。

```python:学習
# 【乱数の種を固定する関数を定義】
# https://keras.io/ja/getting-started/faq/#how-can-i-obtain-reproducible-results-using-keras-during-development
import tensorflow as tf
import random
import os
from keras import backend as K
def set_reproducible():
    os.environ['PYTHONHASHSEED'] = '0'
    np.random.seed(1337)
    random.seed(1337)

    ## 転移学習のときにエラーになるのでtfのsession関係はコメントアウト
    #session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)
    tf.set_random_seed(1337)
    #sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
    #K.set_session(sess)

# 【各エポックごとの学習結果を生成するためのコールバックを定義(前回より精度が良い時だけ保存)】
from keras.callbacks import ModelCheckpoint
import os
model_checkpoint_t = ModelCheckpoint(
    filepath=os.path.join('transfer_models','model_transfer_{epoch:02d}_{val_acc:.3f}'),
    monitor='val_acc',
    mode='max',
    save_best_only=True,
    verbose=1)

# 【パラメータ設定】
batch_size = 32
epochs = 20

# 【学習】
set_reproducible()
result_t = model_t.fit(X_train, Y_train,
                   batch_size=batch_size,
                   epochs=epochs,
                   verbose=1,
                   validation_data=(X_test, Y_test),
                   callbacks=[model_checkpoint_t])
```

###学習結果

学習結果を表示させてみた。

```python:学習結果表示
# 学習データに対する精度チェック
score = model_t.evaluate(X_train, Y_train, verbose=1)
print('Train score:', score[0])
print('Train accuracy:', score[1])

# テストデータに対する精度チェック
score = model_t.evaluate(X_test, Y_test, verbose=1)
print('Test score:', score[0])
print('Test accuracy:', score[1])

# 【学習データとテストデータに対する正解率をプロット】
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(range(1, epochs+1), result_t.history['acc'], label="Training")
plt.plot(range(1, epochs+1), result_t.history['val_acc'], label="Validation")
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.ylim([0,1])# y軸の最小値、最大値
plt.grid(True) # グリッドを表示
plt.xticks(np.arange(0, epochs+1, 10))
plt.legend(bbox_to_anchor=(1.8, 0), loc='lower right', borderaxespad=1, fontsize=15)
plt.show()
```
結果はこうなった。途中でテストデータに対する精度が一番良かったのは13/20エポック目で0.313だった。

227975/227975 [==============================] - 4432s 19ms/step
Train score: 1.6769424402629973
Train accuracy: 0.6915802171290711
4813/4813 [==============================] - 94s 20ms/step
Test score: 4.3911824926823755
Test accuracy: 0.29648867650114274

![転移学習VGG16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306431/6594b4f8-71a1-7621-4202-9b01b8c0a9d2.png)

前回普通に学習させたモデルで、テストデータに対する精度は0.605だったので、前回の約半分ぐらいの精度になっている。
学習にかかる時間は1エポック当たり70～80分で、前回の3～4倍時間がかかっている。モデルが複雑になっているためだろうか。

学習データに対しても精度が頭打ちになっているので、エポックを増やしてもこれ以上良くならないだろう。VGG16のモデルは結構精度が高いという話だったのでちょっと意外。

被写体を識別するために必要な特徴量と、何の文字かを識別するために必要な特徴量は違うということだろうか。(RGBの色のデータは全く役に立たないはずだし)