畳み込みニューラルネットワークの実装
ディープラーニングのブームは画像解析において、ILSVRC のコンペティションで従来の解析手法よりもディープラーニングを用いたモデルが制度を大きく上回った頃から始まったといわれています。そして、それから現在まで 8 年ほど、画像処理、自然言語処理の領域において目覚ましい進展を遂げていることは事実から明らかです。
本章では、その画像解析において目覚ましい発展を遂げている畳み込みニューラルネットワーク (Convolutional Neural Network ; 以下 CNN) の実装方法を学んでいきます。
本章の構成
- データセットの準備
- CNN モデルの定義と学習
- 学習済みモデルの重みの保存と推論
データセットの準備
TensorFlow を用いて、CNN を実装する際の画像のデータセットの形式を確認します。画像や自然言語などの非構造化データを取り扱う際にはまず入力値がどのような形式になっているのかを把握することが重要です。
データセットの読み込みは tf.keras.datasets.mnist
クラスを用いて取得します。
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())
from tensorflow.keras.datasets import mnist
# データセットの取得
train, test = mnist.load_data()
取得したデータセットはすでに TensorFlow を用いて CNN を実装する際に適したデータ形式となっています。データセットの型、データ型、形などを確認し、どのような形式でデータセットを準備する必要があるのか確認していきます。
len(train)
変数 train
には 2 つの要素が存在することが確認できます。中身を確認しましょう。
# 1 つ目の要素の確認
type(train[0])
train[0]
1 つ目の要素には画像処理の節で確認した、画像のデータセットを NumPy の ndarray に変換した時のような値が入っていることが確認できます。このデータは手書き文字の画像データをあらわしています。
ndarray は形を確認することができました。
# 1 目の要素の形を確認
train[0].shape
形から画像のデータセットについて以下の事がわかりました。
- サンプル数 : 60,000
- 高さ (height) : 28
- 幅 (width) : 28
- チャネル数 (channel) : 1
1 つ目の画像データを抽出・描画し、上記の情報と一致しているか確認します。
img = train[0][0] # 画像データセットの 1 サンプル目を抽出
plt.imshow(img, cmap='gray');
要素の 1 つ目が画像のデータセットであることが確認できました。
次に 2 つ目の要素について確認します。
# 2 つ目の要素の確認
type(train[1])
train[1]
train[1].shape
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
# 正規化されていることを確認
x_train[0].min(), x_train[0].max()
目標値も学習用データセットとテスト用データセットに切り分けておきます。
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 のモデルの概要を再度確認しましょう。
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()
1 層目の conv2d
のパラメータの数が となっています。何故この値なのか確認します。
- カーネルのサイズ :
- 入力のチャネル数 :
- 出力のチャネル数 :
- 重みの数 :
- バイアスの数 :
- 合計のパラメータの数 :
注意点として、今回入力値の画像は 1 チャネルのものを使用していますが、このチャネル数が 3 になった場合は、重みの数は 3 倍多くなります。
構造のプロットも行います。
from tensorflow.keras.utils import plot_model
plot_model(model)
今回は非常にシンプルな 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))
今回は GPU を使用して学習を行いました。GPU のメモリの使用率は下記の !nvidia-smi
コマンドを実行します。
Memory-Usage
の欄を確認すると 1179MiB / 15079MiB
のように現在どの程度メモリを専有しているか確認できます。
経験的にバッチサイズはこのメモリを可能な限り使用できる大きさに調整することが多いです。
!nvidia-smi
予測精度の評価
学習結果を確認します。
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');
# 正解率を可視化
results[['accuracy', 'val_accuracy']].plot(title='accuracy')
plt.xlabel('epochs');
損失が下がり、正解率も 95% を超えており、予測精度としては悪くない事が確認できます。続いては実装の中身を分解して確認します。
検証用データセットに対する目的関数の値も、精度も期待した値を出すことができました。これは今回の MNIST の難易度が低かったという理由がありますが、それでも CNN を活用すると画像の特徴を上手く掴んでくれることが分かりました。
CNN モデルの順伝播の流れ
構築した CNN モデルの計算の中身を確認していきます。
入力画像が特徴抽出からベクトル化にかけてどのように変化しているのかを簡単に確認します。
# 推論に使用するデータを切り出し + バッチサイズの追加
x_sample = np.array([x_train[0]])
x_sample.shape
学習済みモデルの層は layers
属性から取得することができ、層のインデックス番号を使用すると特定の層の取り出しを行うことが可能です。
model.layers
切り出した重みの取得には get_weights()
メソッドを用います。
model.layers[0].get_weights()
convolution 層の計算
切り出した層に値を渡すことによって計算を行うことができます。1 層目の convolution 層の計算を実行し、出力データを画像として可視化してみましょう。
output = model.layers[0](x_sample) # convolution 層の計算
output = output[0].numpy() # NumPy の ndarray オブジェクトに変換
今回の convolution 層のフィルタの数は 3 でした。そのため、出力されるデータのチャンネル数は 3 になります。それぞれのチャンネル毎に可視化を行います。
output.shape
# 1 つ目の出力
plt.imshow(output[:, :, 0], cmap='gray');
# 2 つ目の出力
plt.imshow(output[:, :, 1], cmap='gray');
# 3 つ目の出力
plt.imshow(output[:, :, 2], cmap='gray');
それぞれ個別のフィルタが適用され、異なる出力が確認できます。この画像から人間側がどのような特徴を抽出しているか理解することは少し困難ですが、前章で学んだ数学の処理が施されている事が確認できます。
pooling 層の計算
pooling 層の計算を確認します。pooling サイズが だったため、出力のサイズは になります。
output = model.layers[0](x_sample) # convolution 層の計算
output = model.layers[1](output) # pooling 層の計算(サイズを 1/2 に変換)
output = output[0].numpy()
output.shape
# 1 つ目の出力
plt.imshow(output[:, :, 0], cmap='gray');
# 2 つ目の出力
plt.imshow(output[:, :, 1], cmap='gray');
# 3 つ目の出力
plt.imshow(output[:, :, 2], cmap='gray');
ベクトル化
先程の出力の形は (13, 13, 3) になります。全ての値の数の合計は となります。実際に 次元のベクトルに変換されていることを確認しましょう。
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
確かに確認することができました。このように順伝播の流れを進んでいき、学習を行っていきます。前章の理論編では少し複雑に感じられた方もいらっしゃるかもしれませんが、ディープラーニングフレームワークを利用するとフレームワーク側でほとんどの処理を吸収してくれるので、比較的簡単に実装することができます。