LoginSignup
0
0

More than 5 years have passed since last update.

日本の古文書で機械学習を試す(9) 筆跡鑑定?どの作品の文字画像かを当てる

Last updated at Posted at 2019-02-11

前回、日本の古文書で機械学習を試す(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()

筆跡鑑定.png

途中で一度ガクッと精度が下がっているのが謎だが、だいたい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')

各作品のテスト画像.png

ひらがなの「あ」を選ぶのは、原始的だが、データの何番目を表示するかの数字(ソースコード中の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])に適用した結果を表示してみる。途中の層のフィルタを取り出すには、新しく中間層のみのモデルを作れば良いらしい。

1層目の畳み込みフィルタをかけた結果を出力
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)に変換してから表示させてみる。

1層目の畳み込みフィルタをかけた結果を表示
# (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')

フィルタ1層目.png

これが、「好色一代男」の50番目のテストデータに32種類の畳み込みフィルタをかけた結果です。結局どんな特徴を抽出したのかは、私の目にはサッパリ分からない。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0