前回、日本の古文書で機械学習を試す(8) で、人文学オープンデータ共同利用センターの日本古典籍くずし字データセットから複数作品のデータをダウンロードしたので、文字認識以外にも使ってみよう、ということで、どの作品から切り出した文字かを当てるモデルを作成してみる。
###学習、テスト用データを作る
まずは、前回までにダウンロードした「好色一代男」「雨月物語」「おらが春」「養蚕秘録」「物類称呼」の5作品のデータから、学習、テスト用データを作る。前回使った作品のうち「当世料理」はデータ数が少ないので今回は除外する。
学習データとテストデータは、前半後半で分けると文字種が片寄ってしまうので、画像を順番に読み込みながら画像数をカウントし、画像数が10で割り切れるときにテストデータとした。(つまり、10個に1個の割合でテストデータとした。)
# 画像を読み込んで、行列に変換する関数を定義
from keras.preprocessing.image import load_img, img_to_array
def img_to_traindata(file, img_rows, img_cols):
img = load_img(file, color_mode = "grayscale", target_size=(img_rows,img_cols)) # <- Warningが出たので grayscale=True から修正
# x = img.convert('L') # PIL/Pillow グレイスケールに変換
x = img_to_array(img)
x = x.astype('float32')
x /= 255
return x
# 1つの作品のデータから学習用データとテストデータを生成する関数を定義
import glob, os
from keras.utils import np_utils
import numpy as np
def get_train_test_data(root, y_value, nb_classes, img_rows, img_cols):
X_train = []
Y_train = []
X_test = []
Y_test = []
count = 0
chars = glob.glob(root) # 画像のルートディレクトリ内のファイル/ディレクトリ一覧
for char in chars:
if os.path.isdir(char): # ディレクトリなら
img_files = glob.glob(char+"/*.jpg")
for img_file in img_files: # ディレクトリ(文字種)ごとのファイル一覧取得
x = img_to_traindata(img_file, img_rows, img_cols) # 各画像ファイルを読み込んで行列に変換
if count % 10 == 0: # 10個おきにテストデータにする
X_test.append(x) # 画像データ
Y_test.append(y_value) # 正解データ=作品ごとに振った番号
else: # のこりは学習用データにする
X_train.append(x) # 画像データ
Y_train.append(y_value) # 正解データ=作品ごとに振った番号
count = count + 1
# listからnumpy.ndarrayに変換
X_train = np.array(X_train, dtype=float)
Y_train = np.array(Y_train, dtype=float)
Y_train = np_utils.to_categorical(Y_train, nb_classes)
X_test = np.array(X_test, dtype=float)
Y_test = np.array(Y_test, dtype=float)
Y_test = np_utils.to_categorical(Y_test, nb_classes)
return [X_train, Y_train, X_test, Y_test]
# 作品ごとにデータを読み込んで学習、テストデータ生成
nb_classes = 5
img_rows = 28
img_cols =28
# 好色一代男 正解=0
kosyoku_data = get_train_test_data("../200003076/characters/*", 0, nb_classes, img_rows, img_cols)
# 雨月物語 正解=1
ugetsu_data = get_train_test_data("../200014740/characters/*", 1, nb_classes, img_rows, img_cols)
# おらが春 正解=2
oraga_data = get_train_test_data("../200003967/characters/*", 2, nb_classes, img_rows, img_cols)
# 養蚕秘録 正解=3
yosan_data = get_train_test_data("../200021660/characters/*", 3, nb_classes, img_rows, img_cols)
# 物類称呼 正解=4
butsu_data = get_train_test_data("../brsk00000/characters/*", 4, nb_classes, img_rows, img_cols)
# 複数作品の学習データ、テストデータを連結(軸0でデータを連結)
X_train = np.concatenate([kosyoku_data[0],ugetsu_data[0],oraga_data[0],yosan_data[0],butsu_data[0]], 0)
Y_train = np.concatenate([kosyoku_data[1],ugetsu_data[1],oraga_data[1],yosan_data[1],butsu_data[1]], 0)
X_test = np.concatenate([kosyoku_data[2],ugetsu_data[2],oraga_data[2],yosan_data[2],butsu_data[2]], 0)
Y_test = np.concatenate([kosyoku_data[3],ugetsu_data[3],oraga_data[3],yosan_data[3],butsu_data[3]], 0)
# 学習データ、テストデータの型を表示
print(X_train.shape)
print(Y_train.shape)
print(X_test.shape)
print(Y_test.shape)
学習データは205175個、テストデータは22800個生成された。今までの文字認識に比べたら、かなり多いデータ数だ。
###モデルを作って学習
次にモデルを定義して学習させてみる。前回までで一番良かった畳み込み3層、プーリング2層のモデルを使い、パラメータと最適化関数も前回までにチューニングしたものを使う。やろうとしていることが違うので、前回までと同じ条件が最適かどうかは分からないが。
# 再現性を得るための設定をする関数を定義
# 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)
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)
# 畳み込み3層のモデルを使って学習
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
# 【パラメータ設定】
batch_size = 32
epochs = 40
input_shape = (img_rows, img_cols, 1)
nb_filters = 32
# size of pooling area for max pooling
pool_size = (2, 2)
# convolution kernel size
kernel_size = (3, 3)
# 【モデル定義】
model = Sequential()
model.add(Conv2D(nb_filters, kernel_size, # 畳み込み層
padding='valid',
activation='relu',
input_shape=input_shape))
model.add(Conv2D(nb_filters, kernel_size, activation='relu')) # 畳み込み層
model.add(MaxPooling2D(pool_size=pool_size)) # プーリング層
model.add(Conv2D(nb_filters, kernel_size, activation='relu')) # 畳み込み層
model.add(MaxPooling2D(pool_size=pool_size)) # プーリング層
model.add(Dropout(0.25)) # ドロップアウト(過学習防止のため、入力と出力の間をランダムに切断)
model.add(Flatten()) # 多次元配列を1次元配列に変換
model.add(Dense(128, activation='relu')) # 全結合層
model.add(Dropout(0.2)) # ドロップアウト
model.add(Dense(nb_classes, activation='softmax')) # 全結合層
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
# 【各エポックごとの学習結果を生成するためのコールバックを定義(前回より精度が良い時だけ保存)】
from keras.callbacks import ModelCheckpoint
import os
model_checkpoint = ModelCheckpoint(
filepath=os.path.join('add_models','model_kantei_{epoch:02d}_{val_acc:.3f}.h5'),
monitor='val_acc',
mode='max',
save_best_only=True,
verbose=1)
# 【学習】
set_reproducible()
result = model.fit(X_train, Y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(X_test, Y_test),
callbacks=[model_checkpoint])
###結果
データ量が多いため、1エポック当たり950~1000秒 × 40エポックで、前回までより学習にかなり時間がかかった。
いつものように結果をプロットしてみる。日本の古文書で機械学習を試す(6)の最後に、学習データのプロットはあまり意味がないかも?ということが判明したのだが、とりあえず、確認のためプロットしてみる。
# 【学習データとテストデータに対する正解率をプロット】
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(range(1, epochs+1), result.history['acc'], label="Training")
plt.plot(range(1, epochs+1), result.history['val_acc'], label="Validation")
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.ylim([0.8,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()
途中で一度ガクッと精度が下がっているのが謎だが、だいたい0.975あたりで安定してきている。テストデータに対する精度の最大値は0.97794 (30エポック目)だった。
学習データが約20万で、5クラス分類(0~4のどれかをあてる)問題なので、前回までの文字認識と比べるとかなり精度は高めだ。パラメータをいじったり、RNNと組み合わせたりするともっと良くなるのだろうか?
データの一部を表示してみる
確認のため、データの一部を表示してみた。各作品のテストデータから、ひらがなの「あ」を表示させた。
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure(figsize=(10,5)) # 全体の表示領域のサイズ(横, 縦)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.5, wspace=0.05)
# ↑サブ画像余白(左と下は0、右と上は1で余白なし) サブ画像間隔 縦,横
# 好色一代男
for i in range(10):
ax = fig.add_subplot(5, 10, i+1, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text("[0]") # グラフタイトルとして正解番号を表示
ax.imshow(kosyoku_data[2][i+50].reshape((img_rows, img_cols)), cmap='gray')
# 雨月物語
for i in range(10):
ax = fig.add_subplot(5, 10, i+11, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text("[1]") # グラフタイトルとして正解番号を表示
ax.imshow(ugetsu_data[2][i+40].reshape((img_rows, img_cols)), cmap='gray')
# おらが春
for i in range(10):
ax = fig.add_subplot(5, 10, i+21, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text("[2]") # グラフタイトルとして正解番号を表示
ax.imshow(oraga_data[2][i+12].reshape((img_rows, img_cols)), cmap='gray')
# 養蚕秘録
for i in range(10):
ax = fig.add_subplot(5, 10, i+31, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text("[3]") # グラフタイトルとして正解番号を表示
ax.imshow(yosan_data[2][i+30].reshape((img_rows, img_cols)), cmap='gray')
# 物類称呼
for i in range(10):
ax = fig.add_subplot(5, 10, i+41, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text("[4]") # グラフタイトルとして正解番号を表示
ax.imshow(butsu_data[2][i+100].reshape((img_rows, img_cols)), cmap='gray')
ひらがなの「あ」を選ぶのは、原始的だが、データの何番目を表示するかの数字(ソースコード中のax.imshowの最初の引数内の[i+]のの数字部分)を、表示データを見ながら手動で調整した。3列目の「おらが春」(作品番号[2])だけ、「あ」の数が9個しかなく、最後の文字は「い」になっている。
今回は全部の文字種を混ぜて学習させたので、字形の違いはあまり関係ないはずだが、人間の目で見ると、作品ごとの特徴としては、筆遣いよりも、背景(紙の特徴とか、画像化のしかたとか?)の影響の方が大きいように見える。
畳み込みフィルタをかけた結果を表示してみる
学習時にどんな特徴を抽出したのかを知るため、モデルの中間層の出力を見てみる。
まず、学習させたモデルの層構造を表示させてみる。
print(model.layers)
結果はこうなった。出力が見づらかったので、カンマで改行したら少し見やすくなった。<>で囲まれた要素が、モデル定義の際に model.add で追加した要素に対応しているっぽい。
[<keras.layers.convolutional.Conv2D object at 0x00000219D5ACAA20>,
<keras.layers.convolutional.Conv2D object at 0x00000219D5ACAA58>,
<keras.layers.pooling.MaxPooling2D object at 0x00000219D5ACAE80>,
<keras.layers.convolutional.Conv2D object at 0x00000219D5967F28>,
<keras.layers.pooling.MaxPooling2D object at 0x00000219D5ACAC50>,
<keras.layers.core.Dropout object at 0x00000219D5982208>,
<keras.layers.core.Flatten object at 0x0000021939311320>,
<keras.layers.core.Dense object at 0x0000021939311A58>,
<keras.layers.core.Dropout object at 0x000002193932FFD0>,
<keras.layers.core.Dense object at 0x0000021914DD3C50>]
1層目の畳み込みフィルタ model.layers[0] を「好色一代男」の50番目のテストデータ(上の画像の左上端の画像、kosyoku_data[2][50])に適用した結果を表示してみる。途中の層のフィルタを取り出すには、新しく中間層のみのモデルを作れば良いらしい。
from keras.models import Model
#中間層のmodelを作成
intermediate_layer_model = Model(inputs=model.input,
outputs=model.layers[0].output)
#1つのテストデータの画像に対して、フィルタをかけた結果を出力
intermediate_output = intermediate_layer_model.predict([[kosyoku_data[2][50]]]) # 好色一代男
print(intermediate_output.shape)
出力されたデータ型を表示してみると、(26, 26, 32)となっている。最初の26x26が画素数、最後の32がフィルタの数だろう。このままだと扱いにくいので、(32, 26, 26)に変換してから表示させてみる。
# (26, 26, 32) -> (32, 26, 26)
data = intermediate_output[0].transpose(2, 0, 1)
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure(figsize=(8,4)) # 全体の表示領域のサイズ(横, 縦)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.5, wspace=0.05)
for i in range(32):
ax = fig.add_subplot(4,8, i+1, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.imshow(data[i], cmap='gray')
これが、「好色一代男」の50番目のテストデータに32種類の畳み込みフィルタをかけた結果です。結局どんな特徴を抽出したのかは、私の目にはサッパリ分からない。