畳み込みニューラルネットワークの実装

ディープラーニングのブームは画像解析において、ILSVRC のコンペティションで従来の解析手法よりもディープラーニングを用いたモデルが制度を大きく上回った頃から始まったといわれています。そして、それから現在まで 8 年ほど、画像処理、自然言語処理の領域において目覚ましい進展を遂げていることは事実から明らかです。

本章では、その画像解析において目覚ましい発展を遂げている畳み込みニューラルネットワーク (Convolutional Neural Network ; 以下 CNN) の実装方法を学んでいきます。

本章の構成

  • データセットの準備
  • CNN モデルの定義と学習
  • 学習済みモデルの重みの保存と推論

データセットの準備

TensorFlow を用いて、CNN を実装する際の画像のデータセットの形式を確認します。画像や自然言語などの非構造化データを取り扱う際にはまず入力値がどのような形式になっているのかを把握することが重要です。

データセットの読み込みは tf.keras.datasets.mnist クラスを用いて取得します。

33_2

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import tensorflow as tf
      

GPU が使用可能であるか確認しましょう。
name: "/device:GPU:0" の表示があれば GPU が使用可能な状況となっています。

# GPU が使用可能であることを確認
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())
      
[name: "/device:CPU:0" device_type: "CPU" memory_limit: 268435456 locality { } incarnation: 12998763931943164372 , name: "/device:XLA_CPU:0" device_type: "XLA_CPU" memory_limit: 17179869184 locality { } incarnation: 792657384670992162 physical_device_desc: "device: XLA_CPU device" , name: "/device:XLA_GPU:0" device_type: "XLA_GPU" memory_limit: 17179869184 locality { } incarnation: 14253429787293474831 physical_device_desc: "device: XLA_GPU device" , name: "/device:GPU:0" device_type: "GPU" memory_limit: 15701463552 locality { bus_id: 1 links { } } incarnation: 15489940811440588118 physical_device_desc: "device: 0, name: Tesla P100-PCIE-16GB, pci bus id: 0000:00:04.0, compute capability: 6.0" ]
from tensorflow.keras.datasets import mnist
      
# データセットの取得
train, test = mnist.load_data()
      

取得したデータセットはすでに TensorFlow を用いて CNN を実装する際に適したデータ形式となっています。データセットの型、データ型、形などを確認し、どのような形式でデータセットを準備する必要があるのか確認していきます。

len(train)
      
2

変数 train には 2 つの要素が存在することが確認できます。中身を確認しましょう。

# 1 つ目の要素の確認
type(train[0])
      
numpy.ndarray
train[0]
      
array([[[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], [[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], [[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], ..., [[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], [[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], [[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]]], dtype=uint8)

1 つ目の要素には画像処理の節で確認した、画像のデータセットを NumPy の ndarray に変換した時のような値が入っていることが確認できます。このデータは手書き文字の画像データをあらわしています。

ndarray は形を確認することができました。

# 1 目の要素の形を確認
train[0].shape
      
(60000, 28, 28)

形から画像のデータセットについて以下の事がわかりました。

  • サンプル数 : 60,000
  • 高さ (height) : 28
  • 幅 (width) : 28
  • チャネル数 (channel) : 1

1 つ目の画像データを抽出・描画し、上記の情報と一致しているか確認します。

img = train[0][0] # 画像データセットの 1 サンプル目を抽出
plt.imshow(img, cmap='gray');
      
<Figure size 432x288 with 1 Axes>

要素の 1 つ目が画像のデータセットであることが確認できました。
次に 2 つ目の要素について確認します。

# 2 つ目の要素の確認
type(train[1])
      
numpy.ndarray
train[1]
      
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)
train[1].shape
      
(60000,)

2 つ目の要素の要素も NumPy の ndarray で、数値が格納されています。

形を確認すると 60,000 のサンプルが入っていることが確認できます。この 2 つ目の要素は 1 つ目の要素の画像のデータセット(入力値)に対応する答え(目標値)になります。

TensorFlow で使用できる形式に変換

画像データの形を (height, width) から (height, width, channel) へと変換します。また画像データの値の正規化を行います。形の変換は reshape() に変換後の形をタプル型で引数に指定します。正規化は uint8 形式のデータの最大値である 255 で割ることで 0~1 の間に変換します。

x_train = train[0].reshape(60000, 28, 28, 1) / 255
x_test = test[0].reshape(10000, 28, 28, 1) / 255
      
# チャネルが追加されていることを確認
x_train[0].shape
      
(28, 28, 1)
# 正規化されていることを確認
x_train[0].min(), x_train[0].max()
      
(0.0, 1.0)

目標値も学習用データセットとテスト用データセットに切り分けておきます。

t_train = train[1]
t_test = test[1]
      

最後に入力値は float32 のデータ型に、目標値は int32 のデータ型に変換しておきます。

x_train, x_test = x_train.astype('float32'), x_test.astype('float32')
t_train, t_test = t_train.astype('int32'), t_test.astype('int32')
      

CNN モデルの定義

CNN モデルの定義を行います。まず、CNN のモデルの概要を再度確認しましょう。

33_1

CNN のモデルは上図のように大きく分けて 3 つの要素からなります。説明に記載されている英字はコードと関連します。

  • 特徴抽出 : convolution + pooling
    • 画像データからクラス分類などを行う際に使用する特徴量を抽出を行う。畳み込み (convolution) と縮小 (pooling) を繰り返す。畳み込み層を何層追加するのかなどはハイパーパラメータに該当する
  • ベクトル化 : flatten
    • 特徴抽出後の値をベクトルに変換する
  • 識別 : dense
    • 全結合層、活性化関数を介してクラス分類を行う

全体像を把握したところで、モデルの定義を行いましょう。

import os, random

def reset_seed(seed=0):
    os.environ['PYTHONHASHSEED'] = '0'
    random.seed(seed)
    np.random.seed(seed)
    tf.random.set_seed(seed)
      
from tensorflow.keras import models,layers

# シードの固定
reset_seed(0)

# モデルの構築
model = models.Sequential([
    # 特徴量抽出
    layers.Conv2D(filters=3, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)),
    layers.MaxPool2D(pool_size=(2, 2)),
    # ベクトル化
    layers.Flatten(),
    # 識別
    layers.Dense(100, activation='relu'),
    layers.Dense(10, activation='softmax')
])
      

モデルの定義が完了しました。summary() メソッドでパラメータを確認します。

model.summary()
      
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_1 (Conv2D) (None, 26, 26, 3) 30 _________________________________________________________________ max_pooling2d_1 (MaxPooling2 (None, 13, 13, 3) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 507) 0 _________________________________________________________________ dense_2 (Dense) (None, 100) 50800 _________________________________________________________________ dense_3 (Dense) (None, 10) 1010 ================================================================= Total params: 51,840 Trainable params: 51,840 Non-trainable params: 0 _________________________________________________________________

1 層目の conv2d のパラメータの数が 3030 となっています。何故この値なのか確認します。

  • カーネルのサイズ : 3×33\times3
  • 入力のチャネル数 : 11
  • 出力のチャネル数 : 33
  • 重みの数 : 3×3×1×3=273\times3\times1\times3=27
  • バイアスの数 : 33
  • 合計のパラメータの数 : 27+3=3027+3=30

注意点として、今回入力値の画像は 1 チャネルのものを使用していますが、このチャネル数が 3 になった場合は、重みの数は 3 倍多くなります。

構造のプロットも行います。

from tensorflow.keras.utils import plot_model
plot_model(model)
      
<IPython.core.display.Image object>

今回は非常にシンプルな CNN のモデルを定義しました。精度向上のためには、特徴抽出の部分の convolution 層や pooling 層の数を調整したり、全結合層の層やノードの数を調整します。

目的関数と最適化手法の選択

今回は最適化の手法に Adam を、目的関数は分類の問題設定のため sparse categorical crossentropy を使用します。

# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=0.01)

# モデルのコンパイル
model.compile(optimizer=optimizer, 
              loss='sparse_categorical_crossentropy', 
              metrics=['accuracy'])
      

モデルの学習

バッチサイズ、エポック数を定義して、モデルの学習を実行します。

# モデルの学習
batch_size = 4096
epochs = 30

# 学習の実行
history = model.fit(x_train, t_train, 
                batch_size=batch_size, 
                epochs=epochs, verbose=1, 
                validation_data=(x_test, t_test))
      
Epoch 1/30 15/15 [==============================] - 0s 20ms/step - loss: 0.8872 - accuracy: 0.7389 - val_loss: 0.3773 - val_accuracy: 0.8911 Epoch 2/30 15/15 [==============================] - 0s 15ms/step - loss: 0.3335 - accuracy: 0.9030 - val_loss: 0.2680 - val_accuracy: 0.9166 Epoch 3/30 15/15 [==============================] - 0s 15ms/step - loss: 0.2471 - accuracy: 0.9260 - val_loss: 0.1998 - val_accuracy: 0.9360 Epoch 4/30 15/15 [==============================] - 0s 15ms/step - loss: 0.1983 - accuracy: 0.9413 - val_loss: 0.1720 - val_accuracy: 0.9434 Epoch 5/30 15/15 [==============================] - 0s 23ms/step - loss: 0.1636 - accuracy: 0.9509 - val_loss: 0.1481 - val_accuracy: 0.9531 Epoch 6/30 15/15 [==============================] - 0s 15ms/step - loss: 0.1396 - accuracy: 0.9578 - val_loss: 0.1232 - val_accuracy: 0.9608 Epoch 7/30 15/15 [==============================] - 0s 15ms/step - loss: 0.1204 - accuracy: 0.9636 - val_loss: 0.1132 - val_accuracy: 0.9650 Epoch 8/30 15/15 [==============================] - 0s 15ms/step - loss: 0.1068 - accuracy: 0.9679 - val_loss: 0.1039 - val_accuracy: 0.9665 Epoch 9/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0985 - accuracy: 0.9694 - val_loss: 0.0964 - val_accuracy: 0.9695 Epoch 10/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0861 - accuracy: 0.9740 - val_loss: 0.0956 - val_accuracy: 0.9687 Epoch 11/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0795 - accuracy: 0.9752 - val_loss: 0.0857 - val_accuracy: 0.9728 Epoch 12/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0736 - accuracy: 0.9774 - val_loss: 0.0837 - val_accuracy: 0.9731 Epoch 13/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0664 - accuracy: 0.9798 - val_loss: 0.0783 - val_accuracy: 0.9729 Epoch 14/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0620 - accuracy: 0.9809 - val_loss: 0.0766 - val_accuracy: 0.9747 Epoch 15/30 15/15 [==============================] - 0s 16ms/step - loss: 0.0559 - accuracy: 0.9830 - val_loss: 0.0724 - val_accuracy: 0.9767 Epoch 16/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0518 - accuracy: 0.9841 - val_loss: 0.0753 - val_accuracy: 0.9750 Epoch 17/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0485 - accuracy: 0.9855 - val_loss: 0.0700 - val_accuracy: 0.9775 Epoch 18/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0470 - accuracy: 0.9856 - val_loss: 0.0697 - val_accuracy: 0.9775 Epoch 19/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0476 - accuracy: 0.9851 - val_loss: 0.0763 - val_accuracy: 0.9750 Epoch 20/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0417 - accuracy: 0.9872 - val_loss: 0.0675 - val_accuracy: 0.9775 Epoch 21/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0371 - accuracy: 0.9884 - val_loss: 0.0668 - val_accuracy: 0.9784 Epoch 22/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0356 - accuracy: 0.9892 - val_loss: 0.0650 - val_accuracy: 0.9789 Epoch 23/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0310 - accuracy: 0.9906 - val_loss: 0.0651 - val_accuracy: 0.9787 Epoch 24/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0303 - accuracy: 0.9907 - val_loss: 0.0670 - val_accuracy: 0.9789 Epoch 25/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0267 - accuracy: 0.9921 - val_loss: 0.0624 - val_accuracy: 0.9792 Epoch 26/30 15/15 [==============================] - 0s 14ms/step - loss: 0.0259 - accuracy: 0.9920 - val_loss: 0.0695 - val_accuracy: 0.9788 Epoch 27/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0262 - accuracy: 0.9920 - val_loss: 0.0628 - val_accuracy: 0.9821 Epoch 28/30 15/15 [==============================] - 0s 16ms/step - loss: 0.0227 - accuracy: 0.9930 - val_loss: 0.0669 - val_accuracy: 0.9795 Epoch 29/30 15/15 [==============================] - 0s 16ms/step - loss: 0.0227 - accuracy: 0.9931 - val_loss: 0.0704 - val_accuracy: 0.9792 Epoch 30/30 15/15 [==============================] - 0s 15ms/step - loss: 0.0242 - accuracy: 0.9922 - val_loss: 0.0640 - val_accuracy: 0.9802

今回は GPU を使用して学習を行いました。GPU のメモリの使用率は下記の !nvidia-smi コマンドを実行します。

Memory-Usage の欄を確認すると 1179MiB / 15079MiB のように現在どの程度メモリを専有しているか確認できます。

経験的にバッチサイズはこのメモリを可能な限り使用できる大きさに調整することが多いです。

!nvidia-smi
      
Thu Apr 2 07:58:32 2020 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 440.64.00 Driver Version: 418.67 CUDA Version: 10.1 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | |===============================+======================+======================| | 0 Tesla P100-PCIE... Off | 00000000:00:04.0 Off | 0 | | N/A 37C P0 37W / 250W | 1111MiB / 16280MiB | 0% Default | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: GPU Memory | | GPU PID Type Process name Usage | |=============================================================================| +-----------------------------------------------------------------------------+

予測精度の評価

学習結果を確認します。

results = pd.DataFrame(history.history)
results.tail(3)
      
loss accuracy val_loss val_accuracy
27 0.022713 0.992983 0.066860 0.9795
28 0.022750 0.993083 0.070425 0.9792
29 0.024187 0.992150 0.063995 0.9802
# 損失を可視化
results[['loss', 'val_loss']].plot(title='loss')
plt.xlabel('epochs');
      
<Figure size 432x288 with 1 Axes>
# 正解率を可視化
results[['accuracy', 'val_accuracy']].plot(title='accuracy')
plt.xlabel('epochs');
      
<Figure size 432x288 with 1 Axes>

損失が下がり、正解率も 95% を超えており、予測精度としては悪くない事が確認できます。続いては実装の中身を分解して確認します。

検証用データセットに対する目的関数の値も、精度も期待した値を出すことができました。これは今回の MNIST の難易度が低かったという理由がありますが、それでも CNN を活用すると画像の特徴を上手く掴んでくれることが分かりました。

CNN モデルの順伝播の流れ

構築した CNN モデルの計算の中身を確認していきます。
入力画像が特徴抽出からベクトル化にかけてどのように変化しているのかを簡単に確認します。

# 推論に使用するデータを切り出し + バッチサイズの追加
x_sample = np.array([x_train[0]])
x_sample.shape
      
(1, 28, 28, 1)

学習済みモデルの層は layers 属性から取得することができ、層のインデックス番号を使用すると特定の層の取り出しを行うことが可能です。

model.layers
      
[<tensorflow.python.keras.layers.convolutional.Conv2D at 0x7f2cb408a4a8>, <tensorflow.python.keras.layers.pooling.MaxPooling2D at 0x7f2c820ef828>, <tensorflow.python.keras.layers.core.Flatten at 0x7f2c820ef048>, <tensorflow.python.keras.layers.core.Dense at 0x7f2c820efeb8>, <tensorflow.python.keras.layers.core.Dense at 0x7f2c820ff9e8>]

切り出した重みの取得には get_weights() メソッドを用います。

model.layers[0].get_weights()
      
[array([[[[-0.8406145 , -0.7892956 , 0.34098932]], [[ 0.31748846, -0.3093362 , 0.56136435]], [[ 0.5118114 , 0.5906836 , 0.25882816]]], [[[-0.88637626, -0.33033183, 0.36804056]], [[-0.13935491, 0.08202607, 0.26626545]], [[ 0.65556884, 0.6243123 , 0.29205638]]], [[[-0.1402987 , -0.24992312, -0.32202363]], [[-0.82671976, 0.5432165 , 0.29812136]], [[ 0.3795635 , 0.07497335, 0.39722762]]]], dtype=float32), array([ 0.08681452, 0.00536394, -0.01557611], dtype=float32)]

convolution 層の計算

切り出した層に値を渡すことによって計算を行うことができます。1 層目の convolution 層の計算を実行し、出力データを画像として可視化してみましょう。

output = model.layers[0](x_sample) # convolution 層の計算
output = output[0].numpy() # NumPy の ndarray オブジェクトに変換
      

今回の convolution 層のフィルタの数は 3 でした。そのため、出力されるデータのチャンネル数は 3 になります。それぞれのチャンネル毎に可視化を行います。

output.shape
      
(26, 26, 3)
# 1 つ目の出力
plt.imshow(output[:, :, 0], cmap='gray');
      
<Figure size 432x288 with 1 Axes>
# 2 つ目の出力
plt.imshow(output[:, :, 1], cmap='gray');
      
<Figure size 432x288 with 1 Axes>
# 3 つ目の出力
plt.imshow(output[:, :, 2], cmap='gray');
      
<Figure size 432x288 with 1 Axes>

それぞれ個別のフィルタが適用され、異なる出力が確認できます。この画像から人間側がどのような特徴を抽出しているか理解することは少し困難ですが、前章で学んだ数学の処理が施されている事が確認できます。

pooling 層の計算

pooling 層の計算を確認します。pooling サイズが 2×22 \times 2 だったため、出力のサイズは 1/21/2 になります。

output = model.layers[0](x_sample) # convolution 層の計算
output = model.layers[1](output) # pooling 層の計算(サイズを 1/2 に変換)
output = output[0].numpy()
      
output.shape
      
(13, 13, 3)
# 1 つ目の出力
plt.imshow(output[:, :, 0], cmap='gray');
      
<Figure size 432x288 with 1 Axes>
# 2 つ目の出力
plt.imshow(output[:, :, 1], cmap='gray');
      
<Figure size 432x288 with 1 Axes>
# 3 つ目の出力
plt.imshow(output[:, :, 2], cmap='gray');
      
<Figure size 432x288 with 1 Axes>

ベクトル化

先程の出力の形は (13, 13, 3) になります。全ての値の数の合計は 13×13×3=50713\times13\times3=507 となります。実際に 507507 次元のベクトルに変換されていることを確認しましょう。

output = model.layers[0](x_sample) # convolution 層の計算
output = model.layers[1](output) # pooling 層の計算(サイズを 1/2 に変換)
output = model.layers[2](output) # ベクトル化
output = output[0].numpy()
      
output.shape
      
(507,)

確かに確認することができました。このように順伝播の流れを進んでいき、学習を行っていきます。前章の理論編では少し複雑に感じられた方もいらっしゃるかもしれませんが、ディープラーニングフレームワークを利用するとフレームワーク側でほとんどの処理を吸収してくれるので、比較的簡単に実装することができます。

shareアイコン