TensorFlow/Kerasを用いて、CNN(Convolutional Neural Network:畳み込みニューラルネットワーク)による画像分類を行う方法について解説します。一例としてMNIST(エムニスト)という手書き画像データセットの分類実装を紹介します。
Contents
CNN(畳み込みニューラルネットワーク)
ディープラーニングは、2011年以降コンピュータービジョン領域を最初のきっかけとして発展してきました。CNN(Convolutional Neural Network:畳み込みニューラルネットワーク)はディープラーニングに関するモデルで、2011年頃に画像分類のコンテストでそれまでにない精度の成果を上げ、ディープラーニングの発展のきっかけになったものです。現在利用されているモデルの多くはCNNの考え方を色々と拡張したものとなっています。
本記事では、このCNNの概要について紹介するとともに、簡単な例であるMNIST(エムニスト)の手書き文字認識を例にTensorFlow/Kerasを用いた実装例を紹介します。
CNNの構成概要
CNN(Convolutional Neural Network:畳み込みニューラルネットワーク)は、「畳み込み層」と「プーリング層」という層を使って、特徴抽出を行っていくニューラルネットワークです。
以下の図は、この後に紹介するMNIST(エムニスト)という手書き画像データ分類の例におけるモデルを記載してみたものです。
CNNでは、この図のように畳み込み層、プーリング層というものを繰り返し適用していきます。畳み込み層は「入力データの一部分に注目して、その部分の特徴を調べる層」となっています。一方、プーリング層は「畳み込み層から得た情報を縮約する層」となっています。図の例で、最後に全結合層につないでいるのは分類をするためで、この部分は解決したい問題によって変わります。
CNNでは、畳み込み層で特徴を抽出するフィルター(カーネル)の重みを学習していくことになります。例えば、顔画像認識の例だと、入力に近い層の畳み込み層では線や点といった細かな概念の特徴を抽出するフィルターとなり、出力層に近い層のフィルターでは、目や鼻のようなより大きな概念の特徴を抽出するものとなります。
次からは、畳み込み、プーリング、パディングといったCNNの要素についてもう少し具体的に紹介していきます。
以下の論文で学習済みフィルターの例が分かりやすく画像で表示されているのでイメージをつかむ際に参考にしてみてください。
Convolutional Deep Belief Networks
for Scalable Unsupervised Learning of Hierarchical Representations
畳み込み
畳み込みについて具体的に見ていきます。畳み込みとはフィルター(カーネル)という重みを入力にかけて、その部分に対する特徴を抽出します。フィルタを数画素ずつずらしながら入力の各部分に対する特徴の値を出力するわけです。なお、このフィルタをずらす幅をストライドと言います。
上記の図を使ってもう少し説明します。この例は、フィルタサイズが3の場合になっています。
一般的には画像は幅や高さの他に深さがあります。例えば、カラー画像の場合にはR,G,Bそれぞれの画素値というものを持っています。この時には深さは3となります。一方で、MNISTのようなグレースケール画像は深さは1です。
上記のような入力画像で濃い黒枠の画素に対してフィルタをかける場合を考えてみます。この時、フィルタの各重みと各画素の値のそれぞれ積をとって、すべてを足し合わせたものが出力値になります。
ここで、出力にも深さがある点にお気づきでしょうか。出力の深さというのは適用するフィルタの数を表します。このフィルタ数はモデルの設計時に指定するものです。ある画像のある部分に複数フィルタを書けたらそれぞれのフィルタに対する出力値が出ます。その値が上の図での出力における濃い黒枠の部分ということになります。
畳み込みのフィルタサイズやストライドの選択基準としては以下のような考え方があります。
- 畳み込みのフィルタサイズは、3や5のような小さい奇数がよい
- ストライドは小さい方がよい
プーリング
プーリングについて具体的に見ていきます。プーリング層は畳み込み層の出力を縮約することでデータ量を削減している層になります。
一般的に使用される方法はMaxプーリングと言います。Maxプーリングでは、フィルタをあてた中で最大値となる値を出力に採用する方法です。例えば、以下例の緑の部分に対してMaxプーリングを適用する場合、最大値は6なので出力は6になります。
他にも、フィルタをあてたときの画素の平均をとるAverageプーリングなどもありますが、Maxプーリングが最もよく使われる方法かと思います。
畳み込みで得られる特徴は、画像のある部分を見たときには同じような特徴が集まっているため無駄があります。プーリングにより部分的な情報の損失を抑えつつデータを圧縮することができるわけです。また、プーリングには元画像の平行移動でも影響を受けないようにするという役割も果たします。
プーリングのフィルタサイズやストライドの選択基準としては以下のような考え方があります。
- プーリングのフィルタサイズは、2にすることが多い。
- ストライドはプールサイズと同じにすることが多い
パディング
これまで見てきたように、CNNでフィルタを適用すると画像が縮小されます。フィルタを適用した時の画像の縮小を抑えるために、入力画像の周囲に画素を追加することをパディングと言います。
また、一般的には追加するピクセルの画素数は0であるためゼロパディングとも呼ばれます。
パディングにより、画像の端のデータ値の特徴も考慮されるようになりますが、畳み込みの演算回数は増えることになります。
TensorFlow/Kerasを用いたCNNによる画像分類の実装
以降では、TensorFlow/Kerasを用いたCNNによる画像分類の実装例を紹介していきます。上記の説明でも用いてきた以下のモデルを実装してみます。
モデルの構造をまとめてみると以下のようになります。
畳み込み層設定
- フィルターサイズ:3
- ストライド:1
- パディング:なし
- 活性化関数:ReLU
プーリング層設定
- フィルターサイズ:2
- ストライド:2
- パディング:なし
ネットワーク設計
- フィルタ数は32, 64, 128と適用
- 最終的に128のプーリング層の出力を全結合層につないで10個の分類に使用
- 分類の活性化関数としてはsoftmaxを使い、各文字に対する分類結果を確率値で出力
- 全結合層につなぐ前に、過学習防止のためにドロップアウトを0.5で適用
- オプティマイザはAdam
- 損失関数はクロスエントロピー(sparse_categorical_crossentropy)
- 指標は正解率(accuracy)
なお、分類のためのモデルの構築方法が上記だけしかないということではありません。ベースとなる実装方法として参考にしていただけるとよいかと思っています。
層の数や、フィルターサイズ、ストライド、フィルタ数、最適化手法(オプティマイザ)、損失関数等、色々と変えられるところがありますので、どういったモデル設定が性能が良いのか色々と試して比較してみてもらえるとよいかと思います。
実装例
以下に、上記で説明したCNNのモデルについてTensorFlow/Kerasを用いて実装した例を示します。
import matplotlib.pyplot as plt import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.datasets import mnist def main(): """メイン関数""" # ===== MNIST(エムニスト)データの読込 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28, 28, 1)) test_imgs = test_imgs.reshape((10000, 28, 28, 1)) # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx] # ===== CNNモデルの構築 # MNIST画像は28×28でチャンネルは1 inputs = keras.Input(shape=(28, 28, 1)) # 前処理0~1へ正規化 x = layers.Rescaling(1.0 / 255)(inputs) # 畳み込み層とプーリング層の定義 x = layers.Conv2D(32, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(64, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(128, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) # 平坦化する x = layers.Flatten()(x) # ドロップアウトを設定 x = layers.Dropout(0.5)(x) # 分類のために10のノードに接続 outputs = layers.Dense(10, activation="softmax")(x) # モデルの作成 model = keras.Model(inputs=inputs, outputs=outputs) # モデル構成の表示&画像保存 print(model.summary()) keras.utils.plot_model(model, "mnist_cnn_classifier.png", show_shapes=True) # ===== オプティマイザ、損失関数、指標を設定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) # ===== fitを使ったモデルの訓練 num_epochs = 5 history = model.fit( train_imgs, train_labels, epochs=num_epochs, batch_size=32, validation_data=(val_imgs, val_labels), ) # ===== history情報の可視化 # 損失関数(loss)の履歴 loss = history.history["loss"] val_loss = history.history["val_loss"] # 正解率(accuracy)の履歴 acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] # 損失関数の履歴描画 x_epoch = range(1, num_epochs + 1) plt.plot(x_epoch, loss, "r", label="training loss") plt.plot(x_epoch, val_loss, "b", label="validation loss") plt.xlabel("Epochs") plt.ylabel("Loss") plt.legend() # 正解率の履歴描画 plt.figure() plt.plot(x_epoch, acc, "r", label="training acc") plt.plot(x_epoch, val_acc, "b", label="validation acc") plt.xlabel("Epochs") plt.ylabel("Accuracy") plt.legend() plt.show() # ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result) # ===== predictを使って予測結果を表示 preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}") if __name__ == "__main__": main()
【実行結果例】 Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 28, 28, 1)] 0 rescaling (Rescaling) (None, 28, 28, 1) 0 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 (MaxPooling (None, 5, 5, 64) 0 2D) conv2d_2 (Conv2D) (None, 3, 3, 128) 73856 max_pooling2d_2 (MaxPooling (None, 1, 1, 128) 0 2D) flatten (Flatten) (None, 128) 0 dropout (Dropout) (None, 128) 0 dense (Dense) (None, 10) 1290 ================================================================= Total params: 93,962 Trainable params: 93,962 Non-trainable params: 0 _________________________________________________________________ None Epoch 1/5 1500/1500 [==============================] - 9s 4ms/step - loss: 0.3419 - accuracy: 0.8942 - val_loss: 0.1142 - val_accuracy: 0.9676 Epoch 2/5 1500/1500 [==============================] - 6s 4ms/step - loss: 0.1294 - accuracy: 0.9620 - val_loss: 0.0752 - val_accuracy: 0.9772 Epoch 3/5 1500/1500 [==============================] - 6s 4ms/step - loss: 0.0971 - accuracy: 0.9715 - val_loss: 0.0670 - val_accuracy: 0.9812 Epoch 4/5 1500/1500 [==============================] - 6s 4ms/step - loss: 0.0815 - accuracy: 0.9753 - val_loss: 0.0561 - val_accuracy: 0.9843 Epoch 5/5 1500/1500 [==============================] - 6s 4ms/step - loss: 0.0701 - accuracy: 0.9786 - val_loss: 0.0513 - val_accuracy: 0.9866 313/313 [==============================] - 1s 3ms/step - loss: 0.0455 - accuracy: 0.9868 [0.04547674208879471, 0.9868000149726868] 313/313 [==============================] - 0s 1ms/step 予測: 7, 正解: 7
実装内容の解説
上記で紹介した実装例の各部分ごとに内容を説明していきます。
必要モジュールのインポート
import matplotlib.pyplot as plt import numpy as np from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.datasets import mnist
まずは、必要なモジュール類をインポートします。今回はTensorFlowのKerasを使用するので、tensorflowからkerasやlayersをインポートしています。データセットとしてはKeras内にMNISTデータセットがありますのであわせてインポートします。
データセットの用意
# ===== MNIST(エムニスト)データの読込 (train_imgs, train_labels), (test_imgs, test_labels) = mnist.load_data() train_imgs = train_imgs.reshape((60000, 28, 28, 1)) test_imgs = test_imgs.reshape((10000, 28, 28, 1)) # 訓練データの一部(20%)を評価データとして使う idx = int(train_imgs.shape[0] * 0.2) train_imgs, val_imgs = train_imgs[idx:], train_imgs[:idx] train_labels, val_labels = train_labels[idx:], train_labels[:idx]
今回使用するMNISTデータセットを準備しています。MNISTデータセットは、訓練データ60,000、テストデータ10,000というデータセットです。読み込みはload_data()で実行できます。また、訓練データのうち20%を評価データとして抜き出しています。
【検証データとテストデータを分ける理由】
ディープラーニングの場合には、検証データとテストデータを明確に分けます。というのも、検証データを使ってパラメータチューニングを行ううちに検証データセットを過学習する結果になることがあるためです。これは、情報の漏れと言われます。そのため、最終的には訓練に全く関わっていないテストデータセットで評価します。
CNNモデルの構築
# ===== CNNモデルの構築 # MNIST画像は28×28でチャンネルは1 inputs = keras.Input(shape=(28, 28, 1)) # 前処理0~1へ正規化 x = layers.Rescaling(1.0 / 255)(inputs) # 畳み込み層とプーリング層の定義 x = layers.Conv2D(32, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(64, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) x = layers.Conv2D(128, kernel_size=3, activation="relu")(x) x = layers.MaxPooling2D(pool_size=2)(x) # 平坦化する x = layers.Flatten()(x) # ドロップアウトを設定 x = layers.Dropout(0.5)(x) # 分類のために10のノードに接続 outputs = layers.Dense(10, activation="softmax")(x) # モデルの作成 model = keras.Model(inputs=inputs, outputs=outputs) # モデル構成の表示&画像保存 print(model.summary()) keras.utils.plot_model(model, "mnist_cnn_classifier.png", show_shapes=True)
さて、ここがモデル構築の中心ともいえる部分です。今回はFunctional APIを用いた実装を行っています。KerasのAPIの実装方法の違いについて気になる方は「Keras APIでモデルを構築する色々な方法と違い(Sequential API, Functional API, Subclassing API)」も参考にしてください。
まず、データの前処理としてRescalling層を使ってデータを0~1になるように正規化しています。
畳み込み層では、Conv2D層を使用します。最初の引数がフィルタ数(filters=で指定しても構いません)で、kernel_sizeがフィルタサイズ、activationで”relu”を指定しています。
プーリング層ではMaxプーリングのMaxPooling2D層を使用します。pool_sizeを2としています。
畳み込み層とプーリング層をモデルに従って積み上げた後に、分類のための10の全結合層に接続しますが、その前にFlattenで平坦化してからDropout層を追加しています。
モデルの作成は、keras.Modelにインプットとアウトプットを指定することで実行できます。summary()でモデルの構造を表示すると共に、モデル構造を以下のように画像で出力しています。このように可視化するとモデルのつながり関係が分かりやすくて便利です。
モデルのコンパイル
# ===== オプティマイザ、損失関数、指標を設定してコンパイル model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] )
モデルのコンパイルはcompileを使用します。optimizer、loss、metricsでそれぞれ今回の設定の値を指定しています。
モデルの訓練(学習)
# ===== fitを使ったモデルの訓練 num_epochs = 5 history = model.fit( train_imgs, train_labels, epochs=num_epochs, batch_size=32, validation_data=(val_imgs, val_labels), )
モデルの訓練はfitで実行します。今回は手短に実行の確認ができるようにエポックは5としましたが、数字は増やしたりして試してみてください。評価用のデータとしてvalidation_dataを指定することで、各エポックでvalidation_dataを使った評価も行います。
訓練(学習)状況の可視化
# ===== history情報の可視化 # 損失関数(loss)の履歴 loss = history.history["loss"] val_loss = history.history["val_loss"] # 正解率(accuracy)の履歴 acc = history.history["accuracy"] val_acc = history.history["val_accuracy"] # 損失関数の履歴描画 x_epoch = range(1, num_epochs + 1) plt.plot(x_epoch, loss, "r", label="training loss") plt.plot(x_epoch, val_loss, "b", label="validation loss") plt.xlabel("Epochs") plt.ylabel("Loss") plt.legend() # 正解率の履歴描画 plt.figure() plt.plot(x_epoch, acc, "r", label="training acc") plt.plot(x_epoch, val_acc, "b", label="validation acc") plt.xlabel("Epochs") plt.ylabel("Accuracy") plt.legend() plt.show()
fitは返却値として、損失関数や指標の推移(history)を返却します。上記部分では、historyの中に保存されている損失関数(loss)の履歴と正解率(accuracy)の履歴を取得してきて、matplotlibのplotで描画しています。なお、val_xxxとなっているものについては、評価データに対する値になります。
テストデータでの評価及び予測
# ===== evaluateを使ったテストデータでの評価 result = model.evaluate(test_imgs, test_labels) print(result) # ===== predictを使って予測結果を表示 preds = model.predict(test_imgs) print(f"予測: {np.argmax(preds[0])}, 正解: {test_labels[0]}")
上記部分はテストデータを使った評価と予測に関する部分です。テストデータを使った評価はevaluateを用いて実行できます。結果は、[損失(loss), 正解率(accuracy)]という形で返ってきます。今回の例では、98.7%の正解率であったことが分かります。
また、予測結果はpredictを用いて実行できます。今回のモデルは各文字に対する確率値が計算されます。そのため、予測結果としては確率が最も高い値と判断できるため、argmaxで取得しています。
以上が、TensorFlow/Kerasを用いたMNIST画像分類の実装例でした。結構少ないステップ数で、かつ精度がよいモデルが構築できていることが分かると思います。
まとめ
ディープラーニングの画像認識で中心となるCNN(Convolutional Neural Network:畳み込みニューラルネットワーク)について概要を説明しました。また、MNIST(エムニスト)の手書き文字認識を例にTensorFlow/Kerasを用いた実装例を紹介しました。
CNNを理解するには、畳み込み、プーリングといった層とそれぞれハイパーパラメータとなるフィルター数やフィルター(カーネル)サイズ等について理解する必要があります。CNNを用いた色々な手法がありますが、本記事の内容はそれらのベースとなる基礎的な内容です。
基本的な内容を理解した上で、発展的なモデル(ResNet等)について勉強していくとより理解が深まるのではないかと思います。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。