【写経】ニューラルネットワークによる画像識別(MNIST)
先の記事で書いたように、まずは基本からということで、今回はニューラルネットワークによる画像識別問題を扱います.最初くらいは手を動かしたほうがいいか、ということで以下のサイトを参考に写経します.口が裂けてももコーディングとは言えない.
概要
- 目標:2層ニューラルネットによる画像識別を行う
- 使用するデータ:MNIST(0~9の数値データ)
- 環境:Google Colaboratory
ひたすら写経しているだけでなのですが、ベースとなっているオライリーの本がある方が間違いなく理解しやすいかと思います.
また今回、Google Colaboratory上で作業するため、全てコードはColaboのノート上で行いました.そのため、ところどころ不要になったところを消したりしています.
スクリプトファイルをColaboratory上にアップロードしてもいいとは思うのですが、12時間立つと全部消えてしまうので、今回はそのようにいたしました. では早速、実装もとい写経に入っていきます.
Colaboratory上のnoteはコチラ
必要なライブラリのインポート
ここでは、いつもと同じようにnumpy、matplotlibなどをインポートしています.
OrderedDIctは、辞書なんですが、作成する際の順番が維持されたまま格納されます.一方で、標準のdictでは維持されるかどうかは環境に依存するようです.確実に順序を保持したいのであればOrderdDictを使いましょう.
# 必要なライブラリのインポート: import numpy as np import matplotlib.pylab as plt import sys, os from collections import OrderedDict
MNISTデータのダウンロードおよび展開
MNISTデータはurl_baseでURLからダウンロードしています.kery_fileで指定したファイルがダウンロードされます.
MNISTデータの読み込みは、load_mnist関数を定義して行っています.ここで、読み込む際のオプションは以下のような意味を持ちます.
- normalize : 入力画像を0.0~1.0の値に正規化するかどうかの設定。Falseの場合は、ピクセルは元の0~255のまま。
- flatten : 入力画像を平らにする(1次元配列にする)かどうかを設定する。Falseに設定すると、1x28x28の3次元配列、Trueにすると784個の要素からなる1次元配列として格納される。これをFalseにしても問題なく動くのですが、何でなんでしょう.ニューラルネットの入力層のノード数784と合わせて、28x28のデータを784の一次元配列にしないといけないのではないのだろうか.
- one_hot_label : [0,0,1,0,0,0...] などのように正解となるラベルだけが1、それ以外が0の配列として表記される。
抜粋:
url_base = "http://yann.lecun.com/exdb/mnist/" key_file = { "train_img":"train-images-idx3-ubyte.gz", "train_label":"train-labels-idx1-ubyte.gz", "test_img":"t10k-images-idx3-ubyte.gz", "test_label":"t10k-labels-idx1-ubyte.gz" } : 省略 : # ここで入れてるのは、デフォルト値(引数として入力されなかった場合の値) def load_mnist(normalize=True, flatten=True, one_hot_label=True): : 省略 : if normalize: for key in ("train_img", "test_img"): dataset[key] = dataset[key].astype(np.float32) dataset[key] /= 255.0 if one_hot_label: dataset["train_label"] = _change_one_hot_label(dataset["train_label"]) dataset["test_label"] = _change_one_hot_label(dataset["test_label"]) if not flatten: for key in ("train_img', 'test_img"): dataset[key] = dataset[key].reshape(-1, 1, 28, 28) return (dataset["train_img"], dataset["train_label"]), (dataset["test_img"], dataset["test_label"]) if __name__ == "__main__": init_mnist()
省略 : # ネットワークの初期化 def init_network(): TwoLayerNet(input_size=784, hidden_size=50, output_size=10) return network
Affine変換
入力と重みを掛け合わせ、それにバイアスを加える(xw + b )をAffine(アフィン)変換というようです.
行列積のフォワード計算、予測値と正解との差分の計算などを行っています
class Affine: def __init__(self, W, b): self.W =W self.b = b self.x = None self.original_x_shape = None # 重み・バイアスパラメータの微分 self.dW = None self.db = None def forward(self, x): # バックワード計算のときに使うため、x.shapeの形状を記録しておく self.original_x_shape = x.shape #-1とした場合、行数だけがx.shape[0]で決まり、列数は元の形状から推測される x = x.reshape(x.shape[0], -1) self.x = x #affine変換 out = np.dot(self.x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) #更新する重みの差分量計算 self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis=0) #直前の層への入力 dx = dx.reshape(*self.original_x_shape) # 入力データの形状に戻す(テンソル対応) return dx
Sigmoid
活性化関数の1つ目、Sigmoid関数を定義するクラスです.
forward計算では、入力値xを使って以下の式でまでの値を出力します.
\begin{align}y = \frac{1}{1+e^{-x}}\end{align}
backward計算時は、Sigmoid関数自体は重みと関係ないため、直前の層への入力の計算だけになります.直前の層への入力は以下のようになります.
\begin{align}\frac{\partial L}{\partial y}y(1-y)\end{align}
# シグモイド関数のクラス class Sigmoid: def __init__(self): self.out = None #シグモイド関数 def sigmoid(self, x): return 1 / (1 + np.exp(-x)) #順伝播の場合 def forward(self, x): out = self.sigmoid(x) self.out = out return out #逆伝播 def backward(self, dout): return dout*(1.0 - self.out) * self.out
SoftMax
分類問題の順伝播で出力層では、確率分布を算出するため、 今回は簡単な方法としてSoftmax関数を用います.
Softmax関数では出力の総和が1となる.つまり各出力は、その分類となる確率と捉えることができます.
Softmax関数の式は以下のようになります \begin{align} \frac{e^{a_k}}{\sum_{i=1}^{n}}e^{a_i}\end{align}
SoftMax関数では、入力値xの値が大きすぎるとべき乗計算をする関係で計算が簡単にオーバーフローします。 そのため、実装ではオーバーフローしないように工夫してあります。
次に、損失関数を計算しています。今回は 損失関数として、クロスエントロピー関数を使用しています。
最後に2つの関数(softmaxおよびcross_entorpy)を使用して、forward計算を行っています.
backward計算においては、Sigmoid関数と同様に、直前の層に対する入力を計算するだけです。 計算する値は下のようになります。
\begin{align} \frac{\partial L}{\partial y}y(1-y)\end{align}
class Softmax: #ソフトマックス関数 def softmax(self, x): if x.ndim == 2: x =x.T x = x - np.max(x, axis=0) # オーバーフロー対策① y = np.exp(x) / np.sum(np.exp(x), axis=0) return y.T x = x - np.max(x) # オーバーフロー対策② return np.exp(x) / np.sum(np.exp(x)) : 省略 : def forward(self, x, t): self.t = t # 正解 self.y = self.softmax(x) # 予測値 self.loss = self.cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] if self.t.size == self.y.size : dx = (self.y - self.t) / batch_size else : dx = self.y.copy() dx[np.arange(batch_size), self.t] -= 1 dx = dx / batch_size return dx
2層ニューラルネットの定義
省略します
履歴を描画するグラフの定義
iters_numはトータルで行う重みの更新回数を示し、パラメータで与えます. 今回使用したMNISTデータの場合、訓練データの数は60000であるため、60000/バッチサイズが1エポックで行う更新回数となります. トレーニング中のデータはlistにappendで追加しています.
# グラフ描画用変数(ヒストリーデータを格納していく) train_loss_list = [] train_acc_list = [] test_acc_list = [] : 省略 : def plot(): # グラフの描画 markers = {'train': 'o', 'test': 's'} x = np.arange(len(train_acc_list)) plt.plot(x, train_acc_list, label='train acc') plt.plot(x, test_acc_list, label='test acc', linestyle='--') plt.xlabel("epochs") plt.ylabel("accuracy") plt.ylim(0, 1.0) plt.legend(loc='lower right') plt.show()
学習実行
訓練データの数は60000、バッチサイズを100とするとき、60000/100=600回の計算で1epochとなる. 私の環境では、16epochで精度は95%弱となりました.
中途半端な知識の人間が写経とは言え、コードを書いていくことは余計に自分の首を絞めることになります.はい、なりました.
写経だからなんとかなると思ったのですが、甘かった.分かってないことがわんさか沸いてきて手がつかなかったです.
使うだけでいいなら、そう割りきって深入りしないのもありだと思います.
次回は、引き続きCNNによる画像識別(MNIST)に取り組んでみたいと思います.