手書き文字認識でCNNを用いて実装してみた(その2:CNNを実装するだけなら簡単)

手書き文字認識でCNNを用いて実装してみた プログラミング
困ったさん
困ったさん
CNNモデルを用いた解析を見てみたいんだけど

こんな疑問を解決します。

この記事を書いてる人

FUMIPEN blogを運営
本記事の内容

  • CNNを用いた実際に手書き文字の認証プログラム
  • 手書き認証プログラムの解説

本記事では「CNNを用いた手書き文字認証を行い解説

この記事を読むことで、CNNを用いて誰でも簡単にイメージをつけることができるようになります。

結論から話すと、

結論

  • CNNを用いた手書き文字認証プログラムを用いることで全結合からなるプログラムよりも良い結果が得られる

CNNを用いたプログラムの実装

ここからは実装に入っていくのですが、CNNを用いた手書き文字の実装は畳み込みニューラルネットワークの記事で書いてありました。

今回はこれをもとにプログラムを作成します。

まずは流れから説明していき、次の章で詳しく解説をしていこうと思います。

from tensorflow.keras import datasets, layers, models

まずは必要なプログラムの実装です。

(train_images, train_labels), (test_images, test_labels) = datasets.mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
test_images = test_images.reshape((10000, 28, 28, 1))

# ピクセルの値を 0~1 の間に正規化
train_images, test_images = train_images / 255.0, test_images / 255.0

ここでモデルを構築します。

model = models.Sequential()
# 28x28x1(白黒画像を認識)の画像を26x26x32で(26✖︎26枚の画像が32枚用意)
#フィルタは3✖︎3で用意
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))

# 26x26x32 -> 13x13x32(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

# 13x13x32 -> 11x11x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

# 11x11x64 -> 5x5x64(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

# 5x5x64 -> 3x3x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu')) 

model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

#モデルの概要を表示
model.summary()

モデルがどのような形式になっているのかをmodel.summary()で確かめます。

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten (Flatten)            (None, 576)               0         
_________________________________________________________________
dense (Dense)                (None, 64)                36928     
_________________________________________________________________
dense_1 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________さ

最後にモデルをコンパイルして実行するところです。

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(train_images, train_labels, epochs=5) 

最後にモデルを学習させて

Epoch 1/5
1875/1875 [==============================] - 27s 14ms/step - loss: 0.1423 - accuracy: 0.9567
Epoch 2/5
1875/1875 [==============================] - 29s 15ms/step - loss: 0.0449 - accuracy: 0.9856
Epoch 3/5
1875/1875 [==============================] - 27s 14ms/step - loss: 0.0327 - accuracy: 0.9902
Epoch 4/5
1875/1875 [==============================] - 26s 14ms/step - loss: 0.0247 - accuracy: 0.9920
Epoch 5/5
1875/1875 [==============================] - 25s 14ms/step - loss: 0.0194 - accuracy: 0.9937

学習が終了します。

テスト問題を読み取らせます。

test_loss, test_acc = model.evaluate(test_images, test_labels)
313/313 [==============================] - 1s 3ms/step - loss: 0.0269 - accuracy: 0.9917

テスト問題を解き終わったので、どのくらいの精度があるのかを確かめてみます。

print(test_acc)
0.9916999936103821

最終的に99.1%の精度で手書き文字を認識することができるプログラムが作成できました。

それでは詳しくこれから流れを以下の流れに分けて考えていきます。

これから解説する流れ

  • 実際に使われているデータの紹介
  • MNISTデータセットのダウンロードと準備
  • 畳み込みニューラルネットワークのモデルと全結合層の構築
  • モデルのコンパイルと学習
  • モデルの評価

一つづつ解説します。

実際に使われているデータの紹介

こちらが今回使われているデータです。実施に今回使うデータはどこを定義しているかというと

学習データ 畳み込みニューラルネットワーク
ふみペン
ふみペン
読み込ませるときはlabelごとに1だよ!2だよ!とコンピュータに特徴を学習させてるんだね!

MNISTデータセットのダウンロードと準備

(train_images, train_labels), (test_images, test_labels) = datasets.mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
test_images = test_images.reshape((10000, 28, 28, 1))

# ピクセルの値を 0~1 の間に正規化
train_images, test_images = train_images / 255.0, test_images / 255.0

まずは学習用とテスト用のimageを読み込んでいきます。

reshape関数を使用

reshape関数を使用することで、画像を整形することができます。

reshape((60000, 28, 28, 1))で60000枚の画像を28✖︎28のRGBが1つまり白黒画像に変換してください。ということを意味しています。

ふみペン
ふみペン
60,000枚というのは、ダウンロードしてきたデータのサイズが60,000枚だったからだよ!

同じようにtestデータの10,000枚も整形します。

0~1に正規化

RGB(レッド・グリーン・ブルー)は0~255まであることをご存知でしょうか??

ここでは正規化を行うことでRGB値が0~1の正規化を行うようにしています。

なんで正規化を行うの??
困ったさん
困ったさん

これには2つメリットがあります。

正規化のメリット

  • 説明変数(RGB値)の値が小さくなることで、ニューラルネットワークの重み更新の振れ幅が抑えられ、重みが収束しやすくなる
  • 目的変数(0~9)の値が小さくなることで、損失関数の値も小さくなり、誤差が収束しやすくなる

引用:正規化 標準化(機械学習)の理由,必要性,メリットと元に戻す(逆変換)方法をPythonで解説【ディープラーニング,ニューラルネットワーク】

畳み込みニューラルネットワークのモデルと全結合層の構築

model = models.Sequential()
# 28x28x1(白黒画像を認識)の画像を26x26x32で(26✖︎26枚の画像が32枚用意)
#フィルタは3✖︎3で用意
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))

# 26x26x32 -> 13x13x32(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

# 13x13x32 -> 11x11x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

# 11x11x64 -> 5x5x64(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

# 5x5x64 -> 3x3x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu')) 

model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

#モデルの概要を表示
model.summary()

モデルがどのような形式になっているのかをmodel.summary()で確かめます。

ちなみに今回のモデル構築の流れは

  • RGB1(白黒モデル)の画像を用意(縦28✖︎横28)
  • 26✖︎26ピクセルの画像を32枚用意(畳み込み層)
  • 32枚の画像を13✖︎13ピクセルの二次元のプーリング層
  • 11✖︎11の合計64枚の画像を用意(畳み込み層)
  • 5✖︎5の合計64枚のプーリング層を用意
  • 3✖︎3の合計64枚の畳み込み層を用意
  • Flattenで3✖︎3✖︎64の画像を1次元に変換(1次元で576層へ)
  • Denceの全結合層で64個に変換(損失関数はRelu)
  • 出力の際は0~9までの10個(ソフトマックス関数で表現)

このような流れで畳み込みニューラルネットワークを表現しています

一つづつ解説します。

RGB1(白黒モデル)の画像を用意(縦28✖︎横28)

手書き文字認識

まずはSequential()モデルのインスタンスを作成します。

ふみペン
ふみペン
kerasはSequentialモデルの他にFunctional APIと呼ばれるモデルでも作成できる!

Fuctional APIモデルはおまけで最後に載せようと思います。

26✖︎26ピクセルの画像を32枚用意(畳み込み層)

# 28x28x1(白黒画像を認識)の画像を26x26x32で(26✖︎26枚の画像が32枚用意)
#フィルタは3✖︎3で用意
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))

model.add()はsequentialインスタンスの書き方で、どんどん関数を追加していくモデルとなっているので、このように書いてあります。

Conv2Dが畳み込み層を表しています。

畳み込み層
フィルタ??
困ったさん
困ったさん
畳み込みニューラルネットワーク フィルタ

今はこれが3✖︎3で手書き文字の特徴量を捉えていこうとしてるわけです。

活性化関数??
困ったさん
困ったさん
ふみペン
ふみペン
困った君は本当に疑問が多いなぁ笑

活性化関数とは簡単に言えば「より欲しい特徴量を抽出できるようになる方法」です。

今回はrelu(ランプ関数)を用いて畳み込み層から次のプーリング層へ出力しています。

畳み込み層 プーリング層

イメージにするとこのような感じですね。

32枚の画像を13✖︎13ピクセルの二次元のプーリング層

# 26x26x32 -> 13x13x32(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

マックスプーリングと呼ばれる方法を使います。

その名の通り検査領域内の特徴量を一番示す値を取り出すことです。

今回は2✖︎2の4領域を一つの空間として、その中で最大の値を取り出す方法を行っています。

11✖︎11の合計64枚の画像を用意(畳み込み層)

# 13x13x32 -> 11x11x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

先ほどの同じように畳み込み層を用意して今度は64枚の画像を用意します。

フィルタは3、活性化関数はReluを用いています。

5✖︎5の合計64枚のプーリング層を用意

# 11x11x64 -> 5x5x64(2次元のプーリング層)
model.add(layers.MaxPooling2D((2, 2)))

さらにマックスプーリング で2✖︎2の領域を作成し、その中で最大を抽出します。

3✖︎3の合計64枚の畳み込み層を用意

# 5x5x64 -> 3x3x64(二次元の畳み込み層)
model.add(layers.Conv2D(64, (3, 3), activation='relu')) 

その後同じように64枚の画像を3✖︎3のフィルタを通します。

Flattenで3✖︎3✖︎64の画像を1次元に変換(1次元で576層へ)

model.add(layers.Flatten())

値を抽出するには一次元に直す必要があるため、Flattenを用いて3✖︎3の64枚の画像一次元で表現します。

Denceの全結合層で64個に変換(損失関数はRelu)

model.add(layers.Dense(64, activation='relu'))

576個の入力を64個の全結合層に変換します。

なんで64??と思うかもしれませんが、これはいろいろ試してみて一番精度が良かったものだから使用しております。

出力の際は0~9までの10個(ソフトマックス関数で表現)

model.add(layers.Dense(10, activation='softmax'))

最後は0~9までの数字を出力するために10層に直してモデルの結果を出力します。

ここではソフトマックス関すを使用します。

ソフトマックス関数
困ったさん
困ったさん

これも活性化関数の一つです。各出力値を0~1の範囲で変換してくれる活性化関数となっています。

モデルのコンパイルと学習

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(train_images, train_labels, epochs=5) 

optimizer=adamは最適化手法のうちの一つである。

損失関数はsparse_categorical_crossentropyを用いています。

metricsはaccuracyを指定します。

最後はfitを用いてモデルを学習させます。train_imagesとtrain_labelsを読み込ませて、epochsは学習回数です。

モデルの評価

test_loss, test_acc = model.evaluate(test_images, test_labels) 

test_imagesとtest_labelsを読み込ませます。

 print(test_acc)
0.9916999936103821

最終結果は99.1%の正答率でした。

100文字数字を読み取らせたら、99回は正解を示すような結果となりました。

CNNを用いたディープラーニングを実装した感想

今回は実装を行ってみたのですが、正直たくさんの実装の例があるため、実装するだけなら簡単です。

ただし、そこから改良してより良いモデルを作ることを考えると難しくなっていくと感じます。

特に統計や線形代数学の力が必要不可欠になる場面もたくさんあります。

今後は数式も合わせて解説するようなブログも書いていきますが、心が折れたら、途中でブログも雑になると思います。

この他にも畳み込みニューラルネットワーク(CNN)について解説(その1:人間はどうやって物を認識?)を脳の働きと結びつけて解説してます。

ぜひお時間あったら参考にしていただけたらと思います。

最後まで読んでいただきありがとうございました。