Help us understand the problem. What is going on with this article?

Kerasで機械学習〜くずし字の識別〜(1)

More than 1 year has passed since last update.

導入

以下を目的として,機械学習入門者がCNNで識別を行なってみました。

  1. Kerasを用いた機械学習の実装
  2. Google Colaboratory(以下,Colab)の利用方法の理解

※本記事では,理論的な話はしません。「とりあえず動かす」ことを目的としています。

MNISTを用いた手書き数字の識別は多くの方が行なっているため,今回私は,CNNを利用して,くずし字の識別を行なってみました。躓いた部分を中心に書いていきます。
なお,コードはこちらにまとめておきます。

データセットについて

データセットは,人文学データセンターから提供されている『KMNISTデータセット』(doi:10.20676/00000341)のうち『Kuzushiji-49』を用いました[1]
『Kuzushiji-49』には,49種類のひらがなのくずし字が収録されています。人文学データセンターからは,他にも漢字のくずし字や古典作品ごとのくずし字のデータなどが提供されています。

Kuzushiji-49データセットの例

提供されている訓練データのうち,先頭から10個を示します。どれくらい読めますか。
sample_dataset.png

解答はこちら
左から順に,「ま」「と」「な」「ま」「く」「お」「や」「な」「の」「わ」。
私は10個のうち6個しか読めませんでした。

実装

前提:Google Drive内の構造

データセットは,ダウンロード後,Google Driveにおいておきます。自分のGoogle Driveのファイル構造が,以下のようになっているものとします。

  • Colab Notebooks
    • Kuzushiji
      • Kuzushiji-49
        • k49_classmap.csv
        • k49-train-imgs.npz
        • k49-train-labels.npz
        • k49-test-imgs.npz
        • k49-test-imgs.npz

では,順を追って動かしていきます。

1. Google Driveのマウント

GPUを用いるための設定を,まず行います。Colabの左上の「編集」->「ノートブックの設定」から「ハードウェア アクセラレータ」の設定を「GPU」にします。
次に,ColabからGoogle Driveのデータの読み書きを行うための設定を行います。

Kuzushiji-49_train.ipynb
from google.colab import drive
drive.mount('/content/drive')
# Go to this URL in a browser: https://*****
# Enter your authorization code:
# .......
# Mounted at /content/drive

Google Driveへのアクセスを許可するためのリンクが表示されるため,各種データが置いてあるアカウントを選択し,「許可」を選択します。Colabに入力するための認証コードが表示されるため,Colabに入力しエンターキーを押すことで,マウントができます。
実際,以下の文を実行すると,マウントされていることが確認できます。

Kuzushiji-49_train.ipynb
!ls 'drive/My Drive'
# 'Colab Notebooks'

2. データのロード

データの読み込みを行います。データはnpz形式のため,np.loadでデータの読み込みを行うことができます。

Kuzushiji-49_train.ipynb
import os
import numpy as np
import keras
from keras.utils import np_utils
from keras.models import Model, load_model
from keras.layers import Input, Conv2D, BatchNormalization, Activation, GlobalAveragePooling2D, Dense
from keras.callbacks import ModelCheckpoint, CSVLogger

main_path = 'drive/My Drive/Colab Notebooks/Kuzushiji/Kuzushiji-49'
n_class = 49
img_shape = (28, 28, 1)

train_X_file = np.load(os.path.join(main_path, 'k49-train-imgs.npz'))
train_Y_file = np.load(os.path.join(main_path, 'k49-train-labels.npz'))

train_X = train_X_file['arr_0']
train_Y = train_Y_file['arr_0']

train_Xtrain_Yの形状を確認してみます。

Kuzushiji-49_train.ipynb
train_X.shape
# (232365, 28, 28)
train_Y.shape
# (232365)

サンプル数が232,365個で,高さ28ピクセル,幅28ピクセルです。明示されているわけではありませんが,モノクロのためチャネル数は1です。
CNNに入力することを考えて,train_Xの次元を拡張しておきます。また,0~255の符号なし整数を0.0~1.0の浮動小数点数に変換します。
train_Yは,one-hot1表現に直します。

Kuzushiji-49_train.ipynb
train_X = np.expand_dims(train_X, axis = 3) / 255.0
train_X.shape
# (232365, 28, 28, 1)
train_Y = np_utils.to_categorical(train_Y, n_class)
train_Y.shape
# (232365, 49)

3. CNNの構造

画像を識別するためのCNN2のモデルを構築していきます。今回は,CNNの構造については深入りしません。以下のような構造を考えてみました。

Kuzushiji-49_train.ipynb
def cba(inputs, filters, kernel_size, strides):
  x = Conv2D(filters = filters, kernel_size = kernel_size, strides = strides, padding = 'same')(inputs)
  x = BatchNormalization()(x)
  outputs = Activation('relu')(x)

  return outputs

def base_CNN_model():
  inputs = Input(shape = img_shape)

  x = cba(inputs, filters = 64, kernel_size = (2, 2), strides = (1, 1))
  x = cba(x, filters = 64, kernel_size = (3, 3), strides = (2, 2))
  x = cba(x, filters = 128, kernel_size = (3, 3), strides = (2, 2))
  x = cba(x, filters = 256, kernel_size = (3, 3), strides = (2, 2))

  x = GlobalAveragePooling2D()(x)

  x = Dense(512)(x)
  x = BatchNormalization()(x)
  x = Activation('relu')(x)

  x = Dense(n_class)(x)

  outputs = Activation('softmax')(x)

  return Model(inputs, outputs)

model = base_CNN_model()

では,コンパイルしていきます。
最適化アルゴリズム3は,Adamを用います。Adamのハイパーパラメータの設定はKerasのデフォルトのままとしています。
また,49クラスの識別なので,損失関数4はcategorical cross entropyを用います。
model.summaryで,モデルのパラメータなどを確認することができます。

Keras Documentation: 最適化アルゴリズム損失関数

Kuzushiji-49_train.ipynb
optimizer = keras.optimizers.Adam()

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

# _________________________________________________________________
# Layer (type)                 Output Shape              Param #   
# =================================================================
# input_1 (InputLayer)         (None, 28, 28, 1)         0         
# _________________________________________________________________
# conv2d_1 (Conv2D)            (None, 28, 28, 64)        320       
# _________________________________________________________________
# batch_normalization_1 (Batch (None, 28, 28, 64)        256       
# _________________________________________________________________
# activation_1 (Activation)    (None, 28, 28, 64)        0         
# _________________________________________________________________
# conv2d_2 (Conv2D)            (None, 14, 14, 64)        36928     
# _________________________________________________________________
# batch_normalization_2 (Batch (None, 14, 14, 64)        256       
# _________________________________________________________________
# activation_2 (Activation)    (None, 14, 14, 64)        0         
# _________________________________________________________________
# conv2d_3 (Conv2D)            (None, 7, 7, 128)         73856     
# _________________________________________________________________
# batch_normalization_3 (Batch (None, 7, 7, 128)         512       
# _________________________________________________________________
# activation_3 (Activation)    (None, 7, 7, 128)         0         
# _________________________________________________________________
# conv2d_4 (Conv2D)            (None, 4, 4, 256)         295168    
# _________________________________________________________________
# batch_normalization_4 (Batch (None, 4, 4, 256)         1024      
# _________________________________________________________________
# activation_4 (Activation)    (None, 4, 4, 256)         0         
# _________________________________________________________________
# global_average_pooling2d_1 ( (None, 256)               0         
# _________________________________________________________________
# dense_1 (Dense)              (None, 512)               131584    
# _________________________________________________________________
# batch_normalization_5 (Batch (None, 512)               2048      
# _________________________________________________________________
# activation_5 (Activation)    (None, 512)               0         
# _________________________________________________________________
# dense_2 (Dense)              (None, 49)                25137     
# _________________________________________________________________
# activation_6 (Activation)    (None, 49)                0         
# =================================================================
# Total params: 567,089
# Trainable params: 565,041
# Non-trainable params: 2,048
# _________________________________________________________________

出力されたOutput Shapeの列は,各層の出力後の形状を表しています。例えば,input_1 (InputLayer)では,(None, 28, 28, 1)となっています。後ろ3つの数字は,それぞれ,高さ28ピクセル,幅28ピクセル,チャネル数1に対応しています。また,いずれの層でもタプルの最初がNoneとなっていますが,これは本来バッチサイズ5を表します。バッチサイズは,学習時に決まるためこの時点ではNoneとなっています。

4. モデルの学習

モデルを保存するためのディレクトリを作成していきます。Google DriveのKuzushiji-49ディレクトリ内に,モデルを保存するためのmodelディレクトリを作成しておきます。さらに,複数種類のモデルを作成した場合のために,modelディレクトリ下に(モデル名)ディレクトリを作成します(ここでは,モデル名は'CNN_model_1'としています)。

Kuzushiji-49_train.ipynb
save_path = os.path.join(main_path, 'model')
CNN_name = 'CNN_model_1'

if not os.path.exists(save_path):
  os.mkdir(save_path)

if not os.path.exists(os.path.join(save_path, CNN_name)):
  os.mkdir(os.path.join(save_path, CNN_name))

学習は,model.fitで行うことができます。
ModelCheckpointを用いることで,学習途中でモデルを保存することができます。デフォルトの設定では,1エポック6ごとに保存されます。今回は,modelディレクトリ下に(モデル名)_(エポック数).h5というファイル名で保存します。学習途中のモデルでテストを行いたい場合などに役立ちます。
CSVLoggerを用いることで,学習の過程(lossやaccuracy)をcsvファイルに書き出すことができます。modelディレクトリ下の(モデル名)ディレクトリに(モデル名).csvというファイル名で保存されます。
これらの関数をfitcallbacksにリストとして渡しています。
validation_splitは,訓練データの一部を検証データ7として使う場合に指定します。今回は訓練データの0.1(= 10%)を検証データとして用います。

Kuzushiji-49_train.ipynb
checkpoint = ModelCheckpoint(os.path.join(save_path, CNN_name, CNN_name + '_{epoch:d}.h5'))
csv_logger = CSVLogger(os.path.join(save_path, CNN_name, CNN_name + '.csv'), append = True)
model.fit(train_X, train_Y, epochs = 100, callbacks = [checkpoint, csv_logger], validation_split = 0.1)

100エポック学習させた結果を図に示します。訓練データの場合,accuracyは99.8%程度に,lossは0.008程度に収束しています。
train.png
一方,検証データの場合,accuracyは96.8%程度に付近でうろうろしていますが,lossは,5エポック目あたりで最小値0.150をとった後,増加してしまっていることがわかります。
val.png

5. モデルの評価

テストデータでどの程度の性能が出るか測定してみます。model.evaluateで,テストデータに対するlossとaccuracyを求めることができます(モデルは100エポック学習後のものです)。

Kuzushiji-49_train.ipynb
# テストデータのロードなどについては省略
loss, acc = model.evaluate(test_X, test_Y)
print('loss: {:f}, acc.: {:f}'.format(loss, acc))
# loss: 0.461652, acc.: 0.943835

lossは0.462,accuracyは,94.4%です。
lossが最小であった,5エポック学習後のモデルでも評価を行ってみます。保存した学習済みモデルは,load_modelで読み込むことができます。4項のModelCheckpointは,このように学習途中のモデルを後で評価したい場合などに役立ちます。

Kuzushiji-49_train.ipynb
model_name = 'CNN_model_1_5.h5'

model = load_model(os.path.join(save_path, CNN_name, model_name))

loss, acc = model.evaluate(test_X, test_Y)
print('loss: {:f}, acc.: {:f}'.format(loss, acc))
# loss: 0.301178, acc.: 0.927102

accuracyは92.7%で,100エポック学習後のモデルの方が識別性能が高いようです。エポック数の増加に伴って,過学習してしまっているのではないかと考えたのですが…。

ちなみに,くずし字に詳しくない私が予測をすると,accuracyは60%程度でした。そのため,学習済みモデルの方がだいぶ性能が高いことがわかります。くずし字が読めない人にとっては大きな助けになります。余力があれば,機械も私も性能向上を図るかもしれません。

終わりに

Google Colaboratoryを用いて,CNNを構築し,くずし字の識別を行ないました。目的であった,Kerasでの実装方法とGoogle Colaboratoryの使い方を中心に書いてみました。
ちなみに今回,100エポックの学習に3時間以上かかりました。手元のノートパソコンで同様の学習を行うにはその何倍も時間がかかることを考えると,無料でGPUを利用できるColabは非常に有用です。他の機械学習の際にも使ってみようと思います。

誤り等ありましたら,ご連絡ください。

続き
Kerasで機械学習〜くずし字の識別〜(2)

参考

[1]人文学データセンター『Kuzushiji-49』(doi:10.20676/00000341)


  1. one-hot表現とは, クラスのラベルを[0, 0, 1, 0, 0, 0, ..., 0, 0]のように,該当クラスのみ1で,それ以外を0にしたベクトルの表現とすること。 

  2. CNN(Convolutional Neural Network)とは,脳の神経回路を模したニューラルネットワークに,畳み込み層(Convolution Layer)を加えたもの。画像認識などに用いられる。 

  3. 最適化アルゴリズムとは,モデルの重みを更新するためのアルゴリズムのこと。今回用いたAdamの他に,SGDやRMSPropなどがある。 

  4. 損失関数とは,識別性能を評価するための指標。損失関数に基づいて計算される損失(loss)を低下させるための学習を機械は行う。 

  5. バッチ(batch)とは,データのサンプルのまとまりのこと。例えば,128個のデータを同時に入力する場合,バッチサイズは128だと言える。model.fitでは,このバッチサイズごとにモデルの重みの更新が行われる。 

  6. エポック(epoch)とは,学習回数のこと。model.fitでは,訓練データを1周使い終わるごとに1エポック。 

  7. 訓練データの一部で,重みの更新に寄与せず評価のみに使われるものを検証データ(validation data)という。訓練データのみに対応した,過学習が行われていないかの判断の指標になる。 

tky823
M1, 音響信号処理
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away