畳み込みニューラルネットワークの精度向上
本章では、モデルの精度向上に役立つチューニングのテクニックをいくつか紹介していきます。前章までは基礎編として使い方を重視して説明してきました。使い方を理解した次のステップとして、本番環境でも使用できるレベルの予測精度にどのように近づけることができるのか学んでいきましょう。
よく使われるテクニックをいくつか実際に実装しながら、適用前と適用後の予測精度を比較します。
本章の構成
- ベースモデルの作成
- 最適化アルゴリズム
- 過学習対策
- 活性化関数
ベースモデルを作成
はじめに、ベースモデルを作成しましょう。今後色々なテクニックを適用する際に、適用前と適用後の差分を正確に測るためです。それぞれのテクニックを適用する場合には、追加部分以外はベースモデルと同じモデル構造にします。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
データセットの準備
前章で扱った手書き文字である MNIST の分類は簡単なモデルの定義でもある程度の正解率が得られますが、もう少し難しい問題設定で試してみましょう。CIFAR10 と呼ばれる以下のような 10 クラスの分類を行います。CIFAR10 は MNIST のグレースケール画像とは異なり、フルカラー画像です。CIFAR10 も MNIST と同様に、TensorFlow の datasets
にデータセットが用意されています。
(x_train, t_train), (x_test, t_test) = tf.keras.datasets.cifar10.load_data()
それでは、今回扱うデータを 25 枚ランダムに抜粋して表示します。
正解ラベル | 種別 |
---|---|
0 | airplane |
1 | automobile |
2 | bird |
3 | cat |
4 | deer |
5 | dog |
6 | frog |
7 | horse |
8 | ship |
9 | truck |
10 クラス分類となっており、上記の表の種別を分類することが目標です。 と低解像度なところも、CIFAR10 がよく画像の練習問題として扱われる理由のひとつです。
plt.figure(figsize=(12,12))
for i in range(25):
plt.subplot(5, 5, i+1)
plt.imshow(x_train[i])
# 正規化
x_train = x_train / 255.0
x_test = x_test / 255.0
x_train.shape, x_test.shape, t_train.shape, t_test.shape
モデルの定義と学習
import os
import random
def reset_seed(seed=0):
os.environ['PYTHONHASHSEED'] = '0'
random.seed(seed) # random関数のシードを固定
np.random.seed(seed) #numpyのシードを固定
tf.random.set_seed(seed) #tensorflowのシードを固定
from tensorflow.keras import models, layers
# シードの固定
reset_seed(0)
# モデルの構築
model = models.Sequential([
layers.Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=(32, 32, 3)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=1e-3)
# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
model.summary()
# 学習の詳細設定
batch_size = 1024
epochs = 50
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, t_test))
結果の確認
results = pd.DataFrame(history.history)
results[['accuracy', 'val_accuracy']].plot()
results[["loss", "val_loss"]].plot()
results.tail(1)
loss | accuracy | val_loss | val_accuracy | |
---|---|---|---|---|
49 | 0.273878 | 0.9113 | 1.006714 | 0.7262 |
Train | Test | |
---|---|---|
Base Accuracy | 0.911 | 0.726 |
Base Loss | 0.274 | 1.007 |
上記のスコアをベースラインとして、様々なテクニックを適用してどう変化するか確認していきましょう。
最適化アルゴリズム
Basic of Deep Learning の章で、勾配降下法を紹介しました。勾配降下法をベースとして SGD などのさまざまな最適化アルゴリズムがあります。どのアルゴリズムが一番良いのかについてはその時のデータやモデルによって様々であり、色々と試していく必要があるのが現状で、これを使えば間違いないというアルゴリズムはまだ存在しません。今でも活発に世界中のトップリサーチャーによって研究されています。
その中でも現在、代表的な最適化アルゴリズムとしては下記が挙げられます。
- SGD
- Momentum SGD
- RMSprop
- Adam
他にも存在しますが、まずはこちらを把握しておけば問題ありません。最適化アルゴリズムを使った精度向上のポイントとしては、大きく 2 点あります。
- 最適化アルゴリズムの選択
- ハイパーパラメータのチューニング
まずはどの最適化アルゴリズムを選択するかですが、こちらは決まりはありません。Momentum SGD と Adam が初手でよく使われています。
また、各アルゴリズムのハイパーパラメータチューニングも重要なポイントです。一概にどの値にすれば良いのかというのは難しいですが、学習率 (Learning Rate) を 1e-1
〜 1e-5
辺りの値にすることが経験上多いです。手動でチューニングすることも多いですが、Optuna などの探索ツールを使用して、効率的に探していくこともありますのでどちらも頭の中の候補にいれておきましょう。
それでは、上記で紹介した 4 つの最適化アルゴリズムについて、数式を交えて簡単にご紹介します。数式は覚える必要ありませんので、気になる方だけご覧ください。ここでは勾配を (ナブラ)という記号で表現します。
SGD (Stochastic Gradient Descent: 確率的勾配降下法)
SGD (Stochastic Gradient Descent: 確率的勾配降下法) は、勾配降下法をミニバッチ学習(オンライン学習)で行ったものです。勾配降下法では、局所最適解への収束が起こりやすいという問題点をランダムにサンプルを選び出すことで解消しました。勾配を求め、重み を勾配と逆方向に更新します。更新する際の更新幅の調整のために、学習係数 を設けています。
TensorFlow では、tf.keras.optimizers.SGD(lr=1e-2)
として設定できます。lr
が学習係数です。
Momentum SGD
Momentum SGD は SGD に慣性項()を付け足したものです。1 式を 2 式に代入すると となり、前半部分は SGD と同様であることがわかります。
また、ハイパーパラメータとして が追加されており、実装では momentum
引数として指定します。TensorFlow では、tf.keras.optimizers.SGD(lr=1e-2, momentum=0.9)
のように設定できます。
このアルゴリズムはハイパーパラメータが 2 つに増えており、最適化が難しいという問題があります。しかし、初手で使うことが多い最適化アルゴリズムなので、皆さんも迷われたらこちらを使ってみてはいかがでしょうか。
RMSprop
RMSprop は、SGD における学習係数の箇所を学習の収束に合わせて(勾配の大きさに合わせて)変化するように組まれたアルゴリズムです。勾配の 2 乗の指数移動平均を取るように設計されています。
2 式を 3 式に代入すると、
となり、SGDでの学習係数 の箇所が に変わっていることがわかります。
TensorFlow では tf.keras.optimizers.RMSprop(lr=1e-3, rho=0.9, epsilon=1e-8)
として設定でき、それぞれの引数は
- :
lr
- :
rho
- :
eps
に対応しています。こちらもハイパーパラメータの数が増え、最適化が困難なアルゴリズムの一つです。
Adam (Adaptive moment estimation)
Adam は 前述の Momentum の慣性的な動きと、RMSprop の適応的に学習係数を調整する考えを組み合わせたアルゴリズムです。現在、最も評価されているアルゴリズムのひとつですが Adam はハイパーパラメータの数が非常に多いので、それぞれのハイパーパラメータを適切にチューニングすることがポイントになります。
TensorFlow では、 tf.keras.optimizers.Adam(lr=1e-3, beta_1=0.9, beta_2=0.999, epsilon=1e-8)
として設定でき、それぞれの引数は
- :
lr
- :
beta_1
- :
beta_2
- :
epsilon
に対応しています。デフォルトでも良い結果が出やすいアルゴリズムですが、Optuna 等を使用してチューニングすることもおすすめします。
過学習対策
機械学習をおこなう上で、よく遭遇する現象として過学習があります。学習データに対して過度に適合してしまい本来の目的である未知のデータに対する誤差(汎化誤差)が大きくなってしまうことを指します。そのような過学習に対する対策として、いくつかの方法をご紹介していきます。
過学習を防止するための最良の解決策は、より多くの学習用データを使うことです。多くのデータで学習を行えば行うほど、モデルは自然により汎化していく様になります。これが不可能な場合、次善の策は本節で紹介するようなテクニックを使うことです。
過学習への対策として、本章では 4 つのテクニックをご紹介します。
- ドロップアウト (Dropout)
- 正則化(Regularization)
- 早期終了 (Early Stopping)
- バッチノーマリゼーション (Batch Normalization)
ドロップアウト
出典:http://jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf
ドロップアウトでは、ニューラルネットワークを学習する際に、ある更新で層の中のノードのうちのいくつかを無効にして学習を行い、次の更新では別のノードを無効にして学習をおこなうことを繰り返していきます。これにより学習時にネットワークの自由度を強制的に小さくして汎化性能をあげ、過学習を避けることができます。
ドロップアウトが高性能である理由は、アンサンブル学習を近似しているからと言われています。アンサンブル学習とは、複数のモデルに学習させて、予測結果を統合することで汎化性能を高める手法です。
tensorflow.keras.layers.Dropout
で用意されており、引数には入力ユニットをドロップする割合を指定します。もし、Dropout(0.5)
とするならば半分のノードを無効化して学習するということを意味しています。
# シードの固定
reset_seed(0)
# モデルのインスタンス化
model = models.Sequential([
layers.Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=(32, 32, 3)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dropout(0.5),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=1e-3)
# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
# 学習の詳細設定
batch_size = 1024
epochs = 50
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, t_test))
results = pd.DataFrame(history.history)
results[['accuracy', 'val_accuracy']].plot()
results[['loss', 'val_loss']].plot()
results.tail(1)
loss | accuracy | val_loss | val_accuracy | |
---|---|---|---|---|
49 | 0.480941 | 0.83056 | 0.63874 | 0.7846 |
Train | Test | |
---|---|---|
Base Accuracy | 0.911 | 0.726 |
Base Loss | 0.274 | 1.007 |
Dropout Accuracy | 0.83 | 0.785 |
Dropout Loss | 0.481 | 0.639 |
学習用データセットの正解率とテスト用データセットの正解率の乖離が小さくなり、過学習が抑えられたことが確認できました。
正則化 (Regularization)
パラメータの値の大きさに対してモデルの複雑さが増すことに対するペナルティを設け、過学習を抑える方法があります。これを正則化と呼びます。機械学習の基礎 : 回帰の章で詳しく説明しているので、そちらも合わせてご覧ください。
正則化には大きく 2 種類あり、
- L1 正則化 (Lasso):パラメータの絶対値の総和を用い、極端なデータの重みを 0 にする
- L2 正則化 (Ridge):パラメータの二乗の総和を用い、極端なデータの重みを 0 に近づける
ただし注意点として、過学習をしているからといってむやみに正則化をしすぎると逆に学習不足(アンダーフィッティング)となって精度が落ちることもあるので注意が必要です。学習不足の原因は様々であり、
- モデルが十分複雑でない
- 正則化が強すぎる
- 単に学習時間が短すぎる
といった理由が挙げられます。学習不足は、学習用データの中の関連したパターンを学習しきれていないということを意味します。
TensorFlow では各層の kernel_regularizer
引数で指定できます。
from tensorflow.keras import regularizers
# シードの固定
reset_seed(0)
# モデルのインスタンス化
model = models.Sequential([
layers.Conv2D(32, (3, 3), activation='relu', padding='same', kernel_regularizer=regularizers.l2(1e-2), input_shape=(32, 32, 3)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), activation='relu', padding='same', kernel_regularizer=regularizers.l2(1e-2)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), activation='relu', padding='same', kernel_regularizer=regularizers.l2(1e-2)),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=1e-3)
# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
# 学習の詳細設定
batch_size = 1024
epochs = 50
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, t_test))
results = pd.DataFrame(history.history)
results[['accuracy', 'val_accuracy']].plot()
results[['loss', 'val_loss']].plot()
results.tail(1)
loss | accuracy | val_loss | val_accuracy | |
---|---|---|---|---|
49 | 0.964864 | 0.7318 | 1.051897 | 0.6985 |
Train | Test | |
---|---|---|
Base Accuracy | 0.911 | 0.726 |
Base Loss | 0.274 | 1.007 |
Dropout Accuracy | 0.83 | 0.785 |
Dropout Loss | 0.481 | 0.639 |
Regularization Accuracy | 0.732 | 0.699 |
Regularization Loss | 0.965 | 1.052 |
学習データの正解率と検証データの正解率の乖離が小さくなり、過学習が抑えられたことが確認できました。
早期終了
早期終了は Early Stopping と英語で呼ばれる事が多いため、本資料では Early Stopping という言葉を用います。
Early Stopping は学習が進まなくなった場合、途中であっても学習を打ち切ることができる機能です。
モデルの学習時に、callbacks
で tf.keras.callbacks.EarlyStopping
を追加します。
# シードの固定
reset_seed(0)
# モデルのインスタンス化
model = models.Sequential([
layers.Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=(32, 32, 3)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=1e-3)
# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
# 学習の詳細設定
batch_size = 1024
epochs = 50
# Early Stopping
callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3)
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, t_test),
callbacks=[callback])
results = pd.DataFrame(history.history)
results[['accuracy', 'val_accuracy']].plot()
results[['loss', 'val_loss']].plot()
results.tail(1)
loss | accuracy | val_loss | val_accuracy | |
---|---|---|---|---|
17 | 0.765085 | 0.73806 | 0.901713 | 0.6877 |
Train | Test | |
---|---|---|
Base Accuracy | 0.911 | 0.726 |
Base Loss | 0.274 | 1.007 |
Dropout Accuracy | 0.83 | 0.785 |
Dropout Loss | 0.481 | 0.639 |
Regularization Accuracy | 0.732 | 0.699 |
Regularization Loss | 0.965 | 1.052 |
Early Stopping Accuracy | 0.738 | 0.688 |
Early Stopping Loss | 0.765 | 0.902 |
エポック数は最大 50 回までと設定していましたが、val_loss
が下がらなくなるタイミング( 18 エポック目)で学習が打ち切りされました。
注意点として、ある一定期間に値の向上が見られない場合でも、しばらくするとまた向上することがあるということです。効率的に学習するために、大きめのエポック数を準備して学習を実行することもありますが、ケースバイケースという点は抑えておきましょう。
バッチノーマリゼーション
バッチノーマリゼーションは、ミニバッチごとに平均 と 標準偏差 を求め、
のように へと各変数ごとに変換を行います。ここで、 と はハイパーパラメータであり、単純な正規化のように平均 0、標準偏差 1 とするのではなく、平均 、標準偏差 となるように変換を行います。必ずしも平均 0、標準偏差 1 が良いとは限らないためです。
実装としては、各バッチ毎に平均と標準偏差を定めて標準化を行うといった非常に簡単な手法なのですが、こちらを層に加えることで各変数感のスケールによる差を吸収できます。
それでは、バッチノーマリゼーションがある場合で試してみましょう。
# シードの固定
reset_seed(0)
#モデルのインスタンス化
model = models.Sequential([
layers.Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=(32, 32, 3)),
layers.BatchNormalization(),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.BatchNormalization(),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
layers.BatchNormalization(),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# optimizerの設定
optimizer = tf.keras.optimizers.Adam(lr=1e-3)
# モデルのコンパイル
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
# 学習の詳細設定
batch_size = 1024
epochs = 50
# 学習の実行
history = model.fit(x_train, t_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, t_test))
results = pd.DataFrame(history.history)
results[['accuracy', 'val_accuracy']].plot()
results[['loss', 'val_loss']].plot()
results.tail(1)
loss | accuracy | val_loss | val_accuracy | |
---|---|---|---|---|
49 | 0.178049 | 0.93546 | 0.837536 | 0.7784 |
Train | Test | |
---|---|---|
Base Accuracy | 0.911 | 0.726 |
Base Loss | 0.274 | 1.007 |
Dropout Accuracy | 0.83 | 0.785 |
Dropout Loss | 0.481 | 0.639 |
Regularization Accuracy | 0.732 | 0.699 |
Regularization Loss | 0.965 | 1.052 |
Early Stopping Accuracy | 0.738 | 0.688 |
Early Stopping Loss | 0.765 | 0.902 |
Batch Normalization Accuracy | 0.935 | 0.778 |
Batch Normalization Loss | 0.178 | 0.838 |
学習用データセット、テスト用データセット共に正解率の向上が確認できました。ベースモデルほどの乖離もなく、やはりバッチノーマリゼーションは効果的な手法であることが実験からわかりました。
活性化関数
これまで活性化関数として、ReLU 関数を多く使ってきましたが、ニューラルネットワークに活性化関数(非線型変換)を用いる理由は何でしょうか。それは、モデルの表現力を増すためです。
では、なぜ活性化関数を用いるとニューラルネットワークの表現力が増すのでしょうか。線形変換をいくら繋げても、それらは一つの線形変換で表現し直すことができることになり、層を深いディープラーニングの恩恵を預かることができなくなってしまうからです。非線形な関数を表現するには、非線形な関数を活性化関数に入れようというのは直感的に理解できそうです。
また、ReLU 関数の他でよく使われる活性化関数としては、
- sigmoid 関数
- tanh 関数
が代表されるので、まずはこちらを押さえておきましょう。
他にも ReLU 関数がよく使われているのは以下のような理由があります。
- は単純ゆえに計算コストが低い
- の部分では微分値が常に 1 であるため勾配消失の心配が少なくなる
より高度な活性化関数
上記で説明した単純な関数よりも高度な活性化関数を用いたい場合は、tensorflow.keras.layers
にありますので、こちらの公式ドキュメントをご覧ください。
有名な活性化関数を確認しましょう。
def sigmoid(x):
return 1 / (1+np.exp(-x))
def relu(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
def leaky_relu(x):
return np.maximum(x, 0.01*x)
def prelu(x, a):
return np.maximum(x, a*x)
fig = plt.figure(figsize=(10, 6))
x = np.linspace(-10, 10, 1000)
ax = fig.add_subplot(111)
ax.plot(x, sigmoid(x), label='sigmoid')
ax.plot(x, relu(x), label='ReLU')
ax.plot(x, tanh(x), label='tanh')
ax.plot(x, leaky_relu(x), label='leaky_relu')
ax.plot(x, prelu(x, 0.08), label='prelu')
plt.legend()
plt.xlim(-5, 5)
plt.ylim(-1.1, 2)
plt.grid(color='white', linestyle='-')
plt.show();