日本の古文書で機械学習を試す(1)　『日本古典籍字形データセット』のサンプルプログラムを動かす

本業はWebプログラマーだけど、機械学習の勉強をしたいなぁと思っていたところ、[人文学オープンデータ共同利用センター](http://codh.rois.ac.jp/)で日本の古文書の機械学習用データ(日本古典籍字形データセット)を公開しているのに気が付いた。これは趣味でカルチャーセンターの古文書教室に通っている私向けのデータだ！と思って早速遊んでみることにした。

今回は環境設定、サンプルコードをそのまま動かす、Warningの修正、モデルの保存と読み込みまで。

### 動作環境
OS  Windows10
CPU Intel(R) Core(TM) i7-5500U 2.4GHz
RAM 8.00GB
数年前に買ったノートPCで、GPUなし
(pythonとライブラリのバージョン)
python3.6.6、keras2.2.4、tensorflow1.9.0、hdf51.10.2

### 環境設定

まずは[こちら](http://codh.rois.ac.jp/char-shape/book/)からサンプルコード（TAR+GZ 24.57 MB）をダウンロードして解凍。Pythonのプログラムで、Kerasを使っていると書いてある。
このサンプルコードは、Kerasのサンプルコード中の[MNISTの文字認識プログラム（CNN）](https://github.com/fchollet/keras/blob/master/examples/mnist_cnn.py)を流用して、古文書中の10個の文字を識別するもの。

解凍したフォルダの中身は以下の通り

- config.py　　設定ファイル(読み込みファイル名、認識させる文字コードなど)
- config.pyc　　config.pyのコンパイル結果(なくてもOK?合ったら少し早くなる?) 
- load.py　　　データファイルを読み込むためのライブラリ
- load.pyc　　　load.pyのコンパイル結果
- readme.md　　説明ファイル(英語)
- run.py　　　　サンプルプログラム本体
- train_test_file_list.h5　　学習用、テスト用データファイル(HDF5形式)


Anacondaは既にPCにインストール済みなので、Anaconda Navigatorから新しい仮想環境 komonjo を作成して、Anaconda NavigatorからJupyter Notebookをインストール。

その他のライブラリはAnaconda Promptから

```
> activate komonjo
```
で先ほど作った仮想環境に入って、まずは既存のライブラリを全アップデート

```
> conda update --all
```
それから、Kerasのインストール

```
> conda install keras
```
何事もなくすんなりインストールできた。TensorFlow等の必要なライブラリも自動的にインストールされる。

ダウンロードしたサンプルファイルを解凍したフォルダに移動して

```
> python run.py
```
で動かそうとすると、 No module named 'cv2' というエラーが出るので、OpenCVもインストール

```
> conda install -c conda-forge opencv
```

### サンプルコードをそのまま動かす
ここまで設定すると、Anaconda Promptから

```
> python run.py
```
で、サンプルコードが動く。私の環境では約1時間かかった。

```text:サンプルプログラムの出力結果
Using TensorFlow backend.
X_train shape: (19909, 28, 28, 1)
19909 train samples
3514 test samples
run.py:50: UserWarning: Update your `Conv2D` call to the Keras 2 API: `Conv2D(32, (3, 3), input_shape=(28, 28, 1..., padding="valid")`
  input_shape=input_shape))
run.py:52: UserWarning: Update your `Conv2D` call to the Keras 2 API: `Conv2D(32, (3, 3))`
  model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1]))
run.py:69: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
  verbose=1, validation_data=(X_test, Y_test))
Train on 19909 samples, validate on 3514 samples
Epoch 1/12
2018-10-28 20:35:51.951479: I T:\src\github\tensorflow\tensorflow\core\platform\cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
19909/19909 [==============================] - 323s 16ms/step - loss: 1.4118 - acc: 0.5303 - val_loss: 0.7143 - val_acc: 0.8127
Epoch 2/12
19909/19909 [==============================] - 316s 16ms/step - loss: 0.6508 - acc: 0.8024 - val_loss: 0.4540 - val_acc: 0.8640
Epoch 3/12
19909/19909 [==============================] - 315s 16ms/step - loss: 0.4948 - acc: 0.8523 - val_loss: 0.3459 - val_acc: 0.8936
Epoch 4/12
19909/19909 [==============================] - 312s 16ms/step - loss: 0.3992 - acc: 0.8801 - val_loss: 0.2742 - val_acc: 0.9220
Epoch 5/12
19909/19909 [==============================] - 313s 16ms/step - loss: 0.3643 - acc: 0.8925 - val_loss: 0.2513 - val_acc: 0.9260
Epoch 6/12
19909/19909 [==============================] - 326s 16ms/step - loss: 0.3284 - acc: 0.9032 - val_loss: 0.2416 - val_acc: 0.9289
Epoch 7/12
19909/19909 [==============================] - 313s 16ms/step - loss: 0.3031 - acc: 0.9083 - val_loss: 0.2204 - val_acc: 0.9348
Epoch 8/12
19909/19909 [==============================] - 317s 16ms/step - loss: 0.2820 - acc: 0.9151 - val_loss: 0.2036 - val_acc: 0.9380
Epoch 9/12
19909/19909 [==============================] - 316s 16ms/step - loss: 0.2644 - acc: 0.9163 - val_loss: 0.1989 - val_acc: 0.9391
Epoch 10/12
19909/19909 [==============================] - 323s 16ms/step - loss: 0.2593 - acc: 0.9205 - val_loss: 0.2127 - val_acc: 0.9368
Epoch 11/12
19909/19909 [==============================] - 328s 16ms/step - loss: 0.2478 - acc: 0.9245 - val_loss: 0.1936 - val_acc: 0.9459
Epoch 12/12
19909/19909 [==============================] - 326s 16ms/step - loss: 0.2301 - acc: 0.9276 - val_loss: 0.1948 - val_acc: 0.9454
Test score: 0.19476004225171692
Test accuracy: 0.9453614112933251
```

最後のTest accuracyというのは、学習に使うデータとは別に用意したテストデータでの正解率。94.5%という数字が出ている。解凍したデータ中のreadme.md中の数字94.02%と完全に一致はしないが近い数字だ。

### Warningを修正
Warningが気になるので、可能な限り修正する。

最初の2つは同じ内容っぽい。

```
run.py:50: UserWarning: Update your `Conv2D` call to the Keras 2 API: `Conv2D(32, (3, 3), input_shape=(28, 28, 1..., padding="valid")`
  input_shape=input_shape))
run.py:52: UserWarning: Update your `Conv2D` call to the Keras 2 API: `Conv2D(32, (3, 3))`
  model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1]))
```
ソースコードrun.pyの48～50行目を見てみると、

```python:修正前
model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1],
                        border_mode='valid',
                        input_shape=input_shape))
```
となっている。Convolution2Dという関数が、Keras2ではConv2Dという名前になって、引数の2つめと3つめがまとめられた(list? set? tuple?)ということっぽいので、修正してみる。

```python:修正後
model.add(Conv2D(nb_filters, kernel_size,
                        padding='valid',
                        input_shape=input_shape))
```

52行目も同じように修正し、ついでにimport文中のConvolution2DもConv2Dに修正。

次のWarningはもっとシンプル。

```
run.py:69: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
```
69行目のnb_epochをepochsに名前変更したということなので、パラメータ名のnb_epochを変更。(変数名のnb_epochは変更しない)

```python:修正前
model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=nb_epoch,
```
```python:修正後
model.fit(X_train, Y_train, batch_size=batch_size,  epochs=nb_epoch,
```

最後のWarningをチェック。

```
Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
```
これは、CPUがAVX2をサポートしているけど、TensorFlowがAVX2をサポートするようにコンパイルされていないということっぽい。condaからバイナリでTensorFlowをインストールしたためと思われる。TensorFlowをソースからコンパイルするのも面倒なので、このWarningは無視することにする。

ソース修正後、また1時間待つのも面倒なので、nb_epoch = 12をnb_epoch = 1に変更して学習回数を1回だけにして走らせてみたところ、CPUがらみのWarning以外は出なくなった。1回だけでも5分ぐらいかかる。

### モデルの保存と読み込み

次は自分で書いた文字をアップして認識させてみようと思ったのですが、その前に、毎回学習プログラムを走らせるととても時間がかかるので、.pyファイルからJupyter Notebookに移し、学習したモデルの保存、読み込みのコードを追加した。

いろいろなサイトを調べてみるとモデルと重みを別々に保存している例が多かったのだが、意味がよく分からない。モデルだけ保存ということは学習前の状態を保存しているのか？？気になったので動かしてみた。

```python:モデルを保存
# 【モデルをHDF5で保存】
model.save("model_komonjo_mnist.h5")
# 【モデルの重みを保存】
model.save_weights("model_komonjo_mnist_weight.h5")
```

```python:モデル読み込み、テストデータの正解率算出
# 【保存したモデルを読み込んで確認】
from keras.models import load_model
model1 = load_model('model_komonjo_mnist.h5')
score = model1.evaluate(X_test, Y_test, verbose=0)
print('Test score1:', score[0])
print('Test accuracy1:', score[1])

# 【保存したモデルと重みを読み込んで確認】
model2 = load_model('model_komonjo_mnist.h5')
model2.load_weights('model_komonjo_mnist_weight.h5')
score = model2.evaluate(X_test, Y_test, verbose=0)
print('Test score2:', score[0])
print('Test accuracy2:', score[1])
```

```text:出力結果
Test score1: 0.1976389723257986
Test accuracy1: 0.9433693796243597
Test score2: 0.1976389723257986
Test accuracy2: 0.9433693796243597
```

結果は全く同じ。この場合は、モデル保存で学習済みのモデルが保存されているっぽい。重みの保存は何に使うのか、そのうち勉強せねば。

次回は保存したモデルを使って自分で書いた文字の認識をさせたい。

サンプルソースを解読してパラメーターチューニングもしたいけど、別の文字データを使って、10文字じゃなくもっとたくさんのデータを認識させたい。RNNを使って次に来る文字の予測もできたら面白い。そのうち機械翻訳で現代語訳とか、使われている文字、単語、文法で時代判定(偽文書判定)とかできたらいいなぁ。

今回修正したソースコード全文

```python:run.pyのカスタマイズ(日本語のコメントは私が勝手に入れたもの)
# 【ライブラリインポート】
from __future__ import print_function
import config
import load,os,random
import numpy as np
np.random.seed(1337)  # for reproducibility

from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.utils import np_utils
from keras import backend as K

# 【パラメータ設定】
batch_size = 128
nb_classes = 10
nb_epoch = 12
nb_filters = 32
# size of pooling area for max pooling
pool_size = (2, 2)
# convolution kernel size
kernel_size = (3, 3)

# 【学習データとテストデータ作成】
img_rows,img_cols = config.img_row, config.img_col
img_path = config.img_path
X_train, y_train, X_test, y_test  = load.load_sample_dataset()
if K.image_dim_ordering() == 'th':
    X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols)
    X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:
    X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)
    X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_test /= 255
print('X_train shape:', X_train.shape)
print(X_train.shape[0], 'train samples')
print(X_test.shape[0], 'test samples')

# convert class vectors to binary class matrices
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)

# 【モデル定義】
model = Sequential()
model.add(Conv2D(nb_filters, kernel_size,
                        padding='valid',
                        input_shape=input_shape))
model.add(Activation('relu'))
model.add(Conv2D(nb_filters, kernel_size))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=pool_size))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='adadelta',
              metrics=['accuracy'])

# 【学習】
model.fit(X_train, Y_train, batch_size=batch_size, epochs=nb_epoch,
          verbose=1, validation_data=(X_test, Y_test))

# 【テストデータに対して信頼度を表示】
score = model.evaluate(X_test, Y_test, verbose=0)
print('Test score:', score[0])
print('Test accuracy:', score[1])

# 【モデルを保存】
model.save("model_komonjo_mnist.h5")
# 【モデルの重みを保存】
model.save_weights("model_komonjo_mnist_weight.h5")

# 【保存したモデルを読み込んで確認】
from keras.models import load_model
model1 = load_model('model_komonjo_mnist.h5')
score = model1.evaluate(X_test, Y_test, verbose=0)
print('Test score1:', score[0])
print('Test accuracy1:', score[1])

# 【保存したモデルと重みを読み込んで確認】
model2 = load_model('model_komonjo_mnist.h5')
model2.load_weights('model_komonjo_mnist_weight.h5')
score = model2.evaluate(X_test, Y_test, verbose=0)
print('Test score2:', score[0])
print('Test accuracy2:', score[1])

```