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

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

More than 1 year has passed since last update.

前回は,CNNで49種類のひらがなのくずし字を識別させました。今回は性能の向上のために,次の3つの手法を試してみたいと思います。
1. DenseNet
2. アンサンブル学習(Ensemble Learning)
3. データ拡張(Data Augmentation)

データ拡張については,他の2つよりも少し詳しく書いています。
コード全文はこちらにまとめてあります。

前提:Google Drive内の構造

前回に引き続き,Google Driveにデータを置いて,Google Colaboratoryで実行していきます。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
Kuzushiji-49_preprocessing_augmentation.ipynb
main_path = 'drive/My Drive/Colab Notebooks/Kuzushiji/Kuzushiji-49'
n_class = 49

1. DenseNet

前回よりも高い精度を出すために,代表的なCNNのアーキテクチャであるDenseNetの構造で学習を行いました。DenseNetの構造については,以下の記事を参考にしました。
参考:DenseNetの論文を読んで自分で実装してみる

Kuzushiji-49_train_DenseNet.ipynb
def dense_block(x, k, n_block):
  for i in range(n_block):
    main = x
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    #1x1
    x = Conv2D(filters = 64, kernel_size = (1, 1), padding = 'valid')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    #3x3
    x = Conv2D(filters = k, kernel_size = (3, 3), padding = 'same')(x)
    #concatenate
    x = Concatenate()([main, x])

  return x

def transition_layer(inputs, compression = 0.5):
  n_channel = int(inputs.shape[3])
  filters = int(n_channel * compression)
  x = Conv2D(filters = filters, kernel_size = (1, 1))(inputs)
  outputs = AveragePooling2D(pool_size = (2, 2))(x)

  return outputs

def DenseNet():
  inputs = Input(shape = (28, 28, 1))

  x = dense_block(inputs, k = 16, n_block = 1)
  x = transition_layer(x, compression = 0.5)
  x = dense_block(x, k = 16, n_block = 2)
  x = transition_layer(x, compression = 0.5)
  x = dense_block(x, k = 16, n_block = 4)
  x = transition_layer(x, compression = 0.5)
  x = dense_block(x, k = 16, n_block = 3)

  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)

学習中の訓練データ(train)と検証データ(validation)の様子は次のようになりました。

train_DenseNet_1.png
val_DenseNet_1.png

このモデルではパラメータ数が200,000程ですが,100エポック後の学習結果は,lossが0.302,accuracyが95.7%となりました。100エポックの学習に9時間以上かかりました。
前回のモデルでは,パラメータ数が560,000程で,accuracyが94.4%だったため,1%以上精度が向上したことになります。

2. アンサンブル学習(Ensemble Learning)

アンサンブル学習とは,複数の識別器の結果を利用して,最終的な予測を決定する方法です。前回100エポック学習させたCNN_model_1というモデルと,今回100エポック学習させたDenseNet_1の2つの予測確率の平均をとることにします。

Kuzushiji-49_test_ensemble.ipynb
base_model_names = ('CNN_model_1', 'CNN_model_1_100.h5')
DenseNet_names = ('DenseNet_1', 'DenseNet_1_100.h5')
save_path = os.path.join(main_path, 'model')

sub_models = []

for CNN_name, model_name in [base_model_names, DenseNet_names]:
  sub_model = load_model(os.path.join(save_path, CNN_name, model_name))
  sub_models.append(sub_model)

この後,sub_modelsoutputの平均をとっていくのですが,このままでは「同じ名前のレイヤーが複数存在する」と怒られる(All layer names should be unique.とエラーが出る)ので,レイヤーの名前を変えておきます。

Kuzushiji-49_test_ensemble.ipynb
for i, sub_model in enumerate(sub_models):
  for index, layer in enumerate(sub_model.layers):
    sub_model.layers[index].name = layer.name + '_{}'.format(i)

sub_modelsinputを入力として,sub_modelsoutputの平均を出力とするモデルを構築していきます。

Kuzushiji-49_test_ensemble.ipynb
sub_model_inputs = [
  sub_model.input
    for sub_model in sub_models
]

sub_model_outputs = [
  sub_model.output
    for sub_model in sub_models
]

outputs = Average()(sub_model_outputs)
# 重複するレイヤーの名前がある場合,エラー

model = Model(sub_model_inputs, outputs)

アンサンブル「学習」と言っても,今回は学習済みの2つのモデルを使って確率を出力するだけです。lossは0.232,accuracyは96.1%となりました。
CNN_model_1単体のaccuracyは94.4%,DenseNet_1単体のaccuracyは95.7%なので,さらに精度が向上しました。

3. データ拡張(Data Augmentation)

データ拡張とは,元のデータに識別が可能な程度の変化を加えてデータ数を増加させる手法です。データのサンプル数が少ない場合の識別性能の向上に役立ちます(今回は49種類の識別で計230,000以上データがあるので少ないとは言えないと思いますが)。

例えば,次の二つの画像は,左が元の画像,右がそれを10度回転させたものです。どちらも「く」と読めます1

sample_rotate_10.png

今度は,元の画像を90度回転させてみましょう。(左:元の画像,右:90度回転させた画像)

sample_rotate_90.png

くずし字に詳しくなくとも,「く」ではなく「へ」と認識するのではないでしょうか。データを拡張する場合,どの程度までの変化を加えるのかを考えることも重要です。

3.1 データ拡張処理

データ拡張は,KerasのImageDataGeneratorを用いることで,実現できます。ただし,このような使い方は,ImageDataGeneratorの本来の利用方法ではないと思われます2。今回は,元のデータに,水平シフト,垂直シフト,回転の3種類を組み合わせてデータを拡張していきます。この3つの変化がランダムに適用された画像が生成されます。

Kuzushiji-49_preprocessing_augmentation.ipynb
save_path = os.path.join(main_path, 'k49-train_aug-imgs.npz')

if not os.path.exists(save_path):
  #image_data_generator.flowのbatch_sizeについては注釈1を参照
  image_data_generator = ImageDataGenerator(fill_mode = 'constant', rotation_range = 15.0, width_shift_range = 0.1, height_shift_range = 0.1)
  generator = image_data_generator.flow(train_X, shuffle = False, batch_size = len(train_X))

  train_aug_X = generator.next()

  #(232365, 28, 28, 1) -> (232365, 28, 28)
  train_aug_X = np.reshape(train_aug_X, train_aug_X.shape[: 3])

ImageDataGeneratorの引数について[1]

引数 説明
fill_mode 回転などにより生まれる隙間の補完方法。'constant'の場合,一定値で埋められる
rotation_range 画像の回転範囲。15度まで認めた
width_shift_range 幅に対して,シフト可能な割合。0.1とした
height_shift_range 高さに対して,シフト可能な割合。0.1とした

1枚の画像から1枚の変化を加えた画像を生成しているので,単純にデータ数として数えると2倍になります。先頭10個のデータから次のようなデータが生成されました。

元の画像
sample_train_X.png

変化を加えた後の画像
sample_train_aug_X.png

それぞれ若干の変化が加わったものが生成されていることがわかります。
生成されたデータを,npz形式で保存します。np.savezで保存することもできますが,np.savez_compressedを使うと圧縮され,データサイズを小さくすることができます。手元の環境では,np.savezの場合,174MB,np.savez_compressedの場合,73MBとなりました。

Kuzushiji-49_preprocessing_augmentation.ipynb
  #np.savez(save_path, train_aug_X.astype(np.uint8))
  np.savez_compressed(save_path, train_aug_X.astype(np.uint8))

元のデータと生成されたデータの2つのファイルがあるため,これらを結合する必要があります。

Kuzushiji-49_preprocessing_augmentation.ipynb
train_X_file = np.load(os.path.join(main_path, 'k49-train-imgs.npz'))
train_aug_X_file = np.load(os.path.join(main_path, 'k49-train_aug-imgs.npz'))
train_Y_file = np.load(os.path.join(main_path, 'k49-train-labels.npz'))

train_Y = train_Y_file['arr_0']
train_X = np.concatenate([train_X_file['arr_0'], train_aug_X_file['arr_0']], axis = 0)
train_Y = np.concatenate([train_Y, train_Y], axis = 0)

結合後には,データの前半が元のデータ,後半が変化を加えたデータとなってしまうため,ランダム性を確保するために,シャッフルをしていきます。

Kuzushiji-49_preprocessing_augmentation.ipynb
indices = np.array(range(len(train_X)))
np.random.shuffle(indices)

train_X = train_X[indices]
train_Y = train_Y[indices]

np.savez_compressed(os.path.join(main_path, 'k49-train_con-imgs.npz'), train_X)
np.savez_compressed(os.path.join(main_path, 'k49-train_con-labels.npz'), train_Y)

3.2 モデルの学習と評価

CNNの構造に関しては,前回と同様です。100エポック学習します。

Kuzushiji-49_train_augmentation.ipynb
# base_CNN_model: 自作したCNNのモデル
model = base_CNN_model()

model.fit(train_X, train_Y, epochs = 100, callbacks = [checkpoint, csv_logger], validation_split = 0.1)

テストデータで,モデルを評価してみます。

Kuzushiji-49_train.ipynb
loss, acc = model.evaluate(test_X, test_Y)
print('loss: {:f}, acc.: {:f}'.format(loss, acc))
# loss: 0.393653, acc.: 0.951073

accuracyは95.1%と,前回の94.4%より0.7ポイントほど向上しています。当たり前ですが,データ数が2倍になっているので,100エポックの学習にかかった時間も増加して,6時間30分程かかりました。

4. 比較・まとめ

モデル 追加技術 loss accuracy[%]
CNN_model_1(前回のモデル) なし 0.462 94.4
DenseNet_1 なし 0.302 95.7
CNN_model_1 & DenseNet_1 アンサンブル学習 0.232 96.1
CNN_model_1 データ拡張 0.390 95.3

モデル自体の改善や追加技術の導入により,前回の94.4%の精度から96.1%まで向上させることができました。DenseNetでデータ拡張を行なったりすれば,さらにこの数値の上昇を見込めるのではないでしょうか。

参考

[1] Keras Documentation: ImageDataGenerator


  1. 後述するImageDataGeneratorとは異なる方法で生成しています。そのため,回転後に画像がリサイズされてしまっています。 

  2. ImageDataGeneratorは本来,学習中にリアルタイムでデータ拡張を行うことが目的です。しかし,今回は生成されたデータをnpz形式で保存したかったため,バッチサイズを訓練データ数と同じにして拡張したデータを生成しました。巨大なデータに対して同様の操作を行うとメモリに乗り切らない場合があるのでご注意を。 

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