- スクラッチを通してニューラルネットワークの基礎を理解する
- 画像データの簡単な扱い方を知る
NumPyなど最低限のライブラリのみを使いアルゴリズムを実装していきます。
今回は多クラス分類を行う3層のニューラルネットワークを作成します。層の数などは固定した上でニューラルネットワークの基本を確認しましょう。次のSprintで層を自由に変えられる設計にしていきます。
MNISTデータセットを使用します。以下のコードを実行すればKerasによりデータセットをダウンロードし、展開まで行えます。
データセットをダウンロードするコード
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
MNISTとは?
画像分類のための定番データセットで、手書き数字認識を行います。このデータセットには学習用6万枚、テスト用1万枚の28×28ピクセルの白黒画像、およびそれらが0〜9のどの数字であるかが含まれています。
画像データとは?
デジタル画像は点の集合で、これをピクセルと呼びます。一般的に白黒画像であればピクセルには0〜255の値が含まれます。一方、カラー画像であればR(赤)、G(緑)、B(青)それぞれに対応する0〜255の値が含まれます。機械学習をする上では、この0〜255の値一つひとつが特徴量として扱われます。0〜255は符号なしの8ビット整数で表せる範囲になるため、NumPyであれば「uint8」型の変数として保持できます。
まず、どういったデータなのかを見てみます。
サンプルコード
print(X_train.shape) # (60000, 28, 28)
print(X_test.shape) # (10000, 28, 28)
print(X_train[0].dtype) # uint8
print(X_train[0])
各データは28×28ピクセルの白黒画像です。
(1, 28, 28)の各画像を、(1, 784)に変換します。これまで学んできた機械学習手法や、今回扱う全結合層のみのニューラルネットワークではこの形で扱います。全てのピクセルが一列になっていることを、平滑化(flatten)
してあるという風に表現します。
サンプルコード
X_train = X_train.reshape(-1, 784)
X_test = X_test.reshape(-1, 784)
補足
ここまで機械学習を学んでくる中で、特徴量の数を「次元」と呼んできました。その視点ではMNISTは784次元のデータです。一方で、NumPyのshapeが(784,)の状態を1次元配列とも呼びます。画像としての縦横の情報を持つ(28, 28)の状態であれば、2次元配列です。この視点では2次元のデータです。さらに、もしもカラー画像であれば(28, 28, 3)ということになり、3次元配列です。先ほどの視点では3次元のデータになります。しかし、白黒でもカラーでも平面画像であり、立体データではないという視点で、2次元のデータです。画像データを扱う際にはこのように「次元」という言葉が複数の意味合いで使われることに注意してください。
画像データを可視化します。plt.imshow
に渡します。
サンプルコード
import matplotlib.pyplot as plt
%matplotlib inline
index = 0
image = X_train[index].reshape(28,28)
# X_train[index]: (784,)
# image: (28, 28)
plt.imshow(image, 'gray')
plt.title('label : {}'.format(y_train[index]))
plt.show()
numpy.reshape — NumPy v1.15 Manual
matplotlib.pyplot.imshow — Matplotlib 3.0.2 documentation
発展的話題
画像データは符号なし8ビット整数のuint8型で保持されることが一般的ですが、plt.imshow
はより自由な配列を画像として表示することが可能です。例えば、以下のようにマイナスの値を持ったfloat64型の浮動小数点であってもエラーにはならないし、先ほどと全く同じ風に表示されます。
index = 0
image = X_train[index].reshape(28,28)
image = image.astype(np.float) # float型に変換
image -= 105.35 # 意図的に負の小数値を作り出してみる
plt.imshow(image, 'gray')
plt.title('label : {}'.format(y_train[index]))
plt.show()
print(image) # 値を確認
これは、自動的に値を0〜255の整数に変換して処理するように作られているからです。uint8型であっても最小値が0、最大値が255でない場合には色合いがおかしくなります。それを防ぐためには次のように引数を入れてください。
plt.imshow(image, 'gray', vmin = 0, vmax = 255)
画像関係のライブラリではこの自動的なスケーリングが思わぬ結果を生むことがあるので、新しいメソッドを使うときには確認しておきましょう。
画像は0から255のuint8型で表されますが、機械学習をする上では0から1のfloat型で扱うことになります。色は理想的には連続値であり、それを特徴量とするからです。以下のコードで変換可能です。
サンプルコード
X_train = X_train.astype(np.float)
X_test = X_test.astype(np.float)
X_train /= 255
X_test /= 255
print(X_train.max()) # 1.0
print(X_train.min()) # 0.0
また、正解ラベルは0から9の整数ですが、ニューラルネットワークで多クラス分類を行う際にはone-hot表現
に変換します。scikit-learnのOneHotEncoder
を使用したコードが以下です。このone-hot表現による値はそのラベルである確率を示していることになるため、float型で扱います。
サンプルコード
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown='ignore', sparse=False)
y_train_one_hot = enc.fit_transform(y_train[:, np.newaxis])
y_test_one_hot = enc.transform(y_test[:, np.newaxis])
print(y_train.shape) # (60000,)
print(y_train_one_hot.shape) # (60000, 10)
print(y_train_one_hot.dtype) # float64
sklearn.preprocessing.OneHotEncoder — scikit-learn 0.20.0 documentation
さらに、学習用データ6万枚の内2割を検証用データとして分割してください。学習用データが48000枚、検証用データが12000枚となります。
サンプルコード
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2)
print(X_train.shape) # (48000, 784)
print(X_val.shape) # (12000, 784)
ニューラルネットワーク分類器のクラスScratchSimpleNeuralNetrowkClassifier
を作成してください。
以下が雛形です。基本的な構成は機械学習編の線形回帰やロジスティック回帰などと同様です。
雛形
class ScratchSimpleNeuralNetrowkClassifier():
"""
シンプルな三層ニューラルネットワーク分類器
Parameters
----------
Attributes
----------
"""
def __init__(self, verbose = True):
self.verbose = verbose
pass
def fit(self, X, y, X_val=None, y_val=None):
"""
ニューラルネットワーク分類器を学習する。
Parameters
----------
X : 次の形のndarray, shape (n_samples, n_features)
学習用データの特徴量
y : 次の形のndarray, shape (n_samples, )
学習用データの正解値
X_val : 次の形のndarray, shape (n_samples, n_features)
検証用データの特徴量
y_val : 次の形のndarray, shape (n_samples, )
検証用データの正解値
"""
if self.verbose:
#verboseをTrueにした際は学習過程などを出力する
print()
pass
def predict(self, X):
"""
ニューラルネットワーク分類器を使い推定する。
Parameters
----------
X : 次の形のndarray, shape (n_samples, n_features)
サンプル
Returns
-------
次の形のndarray, shape (n_samples, 1)
推定結果
"""
pass
return
これまでの機械学習スクラッチでは、全てのサンプルを一度に計算していました。しかし、ニューラルネットワークではデータを分割して入力する確率的勾配降下法
が一般的です。分割した際のひとかたまりをミニバッチ
、そのサンプル数をバッチサイズ
と呼びます。
今回はバッチサイズを20とします。今回使う学習用データは48000枚ですから、48000/20
で2400回の更新を繰り返すことになります。ニューラルネットワークではこれを2400回イテレーション(iteration)
すると呼びます。学習データを一度全て見ると1回のエポック(epoch)
が終わったことになります。このエポックを複数回繰り返し、学習が完了します。
これを実現するための簡素なイテレータを用意しました。for文で呼び出すと、ミニバッチを取得できます。
コード
class GetMiniBatch:
"""
ミニバッチを取得するイテレータ
Parameters
----------
X : 次の形のndarray, shape (n_samples, n_features)
学習データ
y : 次の形のndarray, shape (n_samples, 1)
正解値
batch_size : int
バッチサイズ
seed : int
NumPyの乱数のシード
"""
def __init__(self, X, y, batch_size = 20, seed=0):
self.batch_size = batch_size
np.random.seed(seed)
shuffle_index = np.random.permutation(np.arange(X.shape[0]))
self._X = X[shuffle_index]
self._y = y[shuffle_index]
self._stop = np.ceil(X.shape[0]/self.batch_size).astype(np.int)
def __len__(self):
return self._stop
def __getitem__(self,item):
p0 = item*self.batch_size
p1 = item*self.batch_size + self.batch_size
return self._X[p0:p1], self._y[p0:p1]
def __iter__(self):
self._counter = 0
return self
def __next__(self):
if self._counter >= self._stop:
raise StopIteration()
p0 = self._counter*self.batch_size
p1 = self._counter*self.batch_size + self.batch_size
self._counter += 1
return self._X[p0:p1], self._y[p0:p1]
このクラスをニューラルネットワークのクラス内でインスタンス化し、for文を使うことでミニバッチが取り出せます。
# 以下をニューラルネットワークのクラス内で呼び出す
get_mini_batch = GetMiniBatch(X_train, y_train, batch_size=20)
print(len(get_mini_batch)) # 2400
print(get_mini_batch[5]) # 5番目のミニバッチが取得できる
for mini_X_train, mini_y_train in get_mini_batch:
# このfor文内でミニバッチが使える
pass
__getitem__
や__next__
は__init__
などと同じ特殊メソッドの一種です。
三層のニューラルネットワークのフォワードプロパゲーションを作成します。以下の説明ではノード数は1層目は400、2層目は200としますが、変更しても構いません。
各層の数式を以下に示します。今回はそれぞれの記号が表す配列が、実装上どのようなndarrayのshapeになるかを併記してあります。
batch_size = 20 # バッチサイズ
n_features = 784 # 特徴量の数
n_nodes1 = 400 # 1層目のノード数
n_nodes2 = 200 # 2層目のノード数
n_output = 10 # 出力のクラス数(3層目のノード数)
「1層目」
「1層目の活性化関数」
「2層目」
「2層目の活性化関数」
「3層目(出力層)」
「3層目の活性化関数」
ニューラルネットワークにおいては重みの初期値は重要な要素です。様々な方法が提案されていますが、今回はガウス分布による単純な初期化を行います。バイアスに関しても同様です。
以下のコードを参考にしてください。標準偏差の値sigmaはハイパーパラメータです。発展的な重みの初期化方法については次のSprintで扱います。
サンプルコード
n_features = 784
n_nodes1 = 400
sigma = 0.01 # ガウス分布の標準偏差
W1 = sigma * np.random.randn(n_features, n_nodes1)
# W1: (784, 400)
numpy.random.randn — NumPy v1.15 Manual
活性化関数を作成し、フォワードプロパゲーションの中で使用します。切り替えられるように実装することを推奨しますが、片方でも構いません。
「シグモイド関数」
指数関数 np.exp
を使用してください。
numpy.exp — NumPy v1.15 Manual
「ハイパボリックタンジェント関数」
次の数式で表されますが、np.tanh
ひとつで実現できます。
numpy.tanh — NumPy v1.15 Manual
*現在ではこれらの代わりにReLUと呼ばれる活性化関数が一般的です。次のSprintで扱います。
ソフトマックス関数を作成し、フォワードプロパゲーションの中で使用します。これも活性化関数の一種ですが、多クラス分類の出力層で使われる特性上、区別して扱われることが多いです。
次の数式です。
分母は全てのクラスに相当する値を指数関数に通した上で足し合わせたものです。その中で、分子に
これを10クラス分計算し、合わせたものが
目的関数(損失関数)を作成します。
多クラス分類の目的関数である交差エントロピー誤差
サンプル1つあたりの誤差が求まります。
三層のニューラルネットワークのバックプロパゲーションを作成します。確率的勾配降下法を行う部分です。
数式を以下に示します。
まず、i層目の重みとバイアスの更新式です。
$$ W_i^{\prime} = W_i - \alpha \frac{\partial L}{\partial W_i} \
B_i^{\prime} = B_i - \alpha \frac{\partial L}{\partial B_i} $$
この更新方法はSprint3線形回帰やsprint4ロジスティック回帰における最急降下法と同様です。より効果的な更新方法が知られており、それは次のSprintで扱います。
勾配
「3層目」
$$ \frac{\partial L}{\partial A_3} = \frac{1}{n_b}\sum_{j}^{n_b}(Z_{3_j} - Y_{j})\
\frac{\partial L}{\partial B_3} = \frac{\partial L}{\partial A_3}\
\frac{\partial L}{\partial W_3} = (\frac{1}{n_b}\sum_{j}^{n_b}Z_{2_j}^T) \cdot \frac{\partial L}{\partial A_3}\
\frac{\partial L}{\partial Z_2} = \frac{\partial L}{\partial A_3} \cdot W_3^T $$
「2層目」
$$ \frac{\partial L}{\partial A_2} = \frac{\partial L}{\partial Z_2} × {1-tanh^2(\frac{1}{n_b}\sum_{j}^{n_b}A_{2_j})}\
\frac{\partial L}{\partial B_2} = \frac{\partial L}{\partial A_2}\
\frac{\partial L}{\partial W_2} = (\frac{1}{n_b}\sum_{j}^{n_b}Z_{1_j}^T) \cdot \frac{\partial L}{\partial A_2}\
\frac{\partial L}{\partial Z_1} = \frac{\partial L}{\partial A_2} \cdot W_2^T $$
「1層目」
$$ \frac{\partial L}{\partial A_1} = \frac{\partial L}{\partial Z_1} × {1-tanh^2(\frac{1}{n_b}\sum_{j}^{n_b}A_{1_j})}\
\frac{\partial L}{\partial B_1} = \frac{\partial L}{\partial A_1}\
\frac{\partial L}{\partial W_1} = (\frac{1}{n_b}\sum_{j}^{n_b}X_{j}^T) \cdot \frac{\partial L}{\partial A_1}\ $$
補足
活性化関数にシグモイド関数を使用した場合は、次のようになります。
推定を行うメソッドを作成します。
フォワードプロパゲーションによって出力された10個の確率の中で、最も高いものはどれかを判定します。
numpy.argmax — NumPy v1.15 Manual
学習曲線をプロットしてください。
ニューラルネットワークは過学習が発生しやすいため、学習曲線の確認が重要です。trainデータとvalデータに対するエポックごとの損失(交差エントロピー誤差)を記録できるようにする必要があります。
分類に関する指標値で精度を確認してください。
誤分類した画像はどのようなものだったかを見てみましょう。推定値を用意し、以下のコードを実行してください。
コード
"""
語分類結果を並べて表示する。画像の上の表示は「推定結果/正解」である。
Parameters:
----------
y_pred : 推定値のndarray (n_samples,)
y_val : 検証用データの正解ラベル(n_samples,)
X_val : 検証用データの特徴量(n_samples, n_features)
"""
import numpy as np
import matplotlib.pyplot as plt
num = 36 # いくつ表示するか
true_false = y_pred==y_val
false_list = np.where(true_false==False)[0].astype(np.int)
if false_list.shape[0] < num:
num = false_list.shape[0]
fig = plt.figure(figsize=(6, 6))
fig.subplots_adjust(left=0, right=0.8, bottom=0, top=0.8, hspace=1, wspace=0.5)
for i in range(num):
ax = fig.add_subplot(6, 6, i + 1, xticks=[], yticks=[])
ax.set_title("{} / {}".format(y_pred[false_list[i]],y_val[false_list[i]]))
ax.imshow(X_val.reshape(-1,28,28)[false_list[i]], cmap='gray')