Chainerや、TensorFlowといった、深層学習のフレームワークを使用せず、NumpyとMatplotlibのみでニューラルネットワークを実装した。
参考にさせていただいた記事
大変お世話になりました。ありがとうございます。
(1)ライブラリーを使わずにPythonでニューラルネットワークを構築してみる - Qiita
(2)PythonによるDeep Learningの実装(Dropout + ReLU 編) - Yusuke Sugomori's Blog
(3)scikit-learnを用いたサンプルデータ生成 - RとPythonによるデータマイニング
やったこと
3クラス分類を行うニューラルネットワークをPythonで実装した。
(ついでに2クラス分類を行ったり、XORを解いたりしたのでリポジトリに公開しておく)
データの準備
先ほどドヤ顔で深層学習のフレームワークを使用しないと宣言した手前情けないのだが、データの作成に関しては、自分で1から作成せず、scikit-learnのmake_classificationという機能を使って作成した。
import sklearn.datasets x , label = sklearn.datasets.make_classification( n_features=2, n_samples=300, n_redundant=0, n_informative=2,n_clusters_per_class=1, n_classes=3)
これでxに入力データが、labelに教師データが格納される。
make_classification関数の引数は(3)の方の記事に詳しく書いてある。
import matplotlib.pyplot as plt plt.scatter(x[:, 0], x[:, 1],c=label,cmap=plt.cm.jet) plt.show()
このような感じでデータが生成された。
xは2つの入力値(座標)を持つサンプルを300個含んだ配列であり、
labelは各サンプルのクラス(0,1,2)を含んだ配列である。
print x[:10] array([[ 0.52358787, -1.15423424], [-0.52633513, -1.76794057], [ 0.55418671, -0.46817551], [-1.17471257, -0.44251872], [-1.34866206, -0.54916791], [ 0.67537883, -0.53149583], [ 1.61055345, 0.0481599 ], [-0.09616488, -1.51676462], [ 0.22409607, 1.40511859], [ 2.24986887, 1.18862095]])
print label[:10] array([2, 0, 0, 0, 0, 2, 1, 0, 1, 1])
ニューラルネットワークの構造
以下のようにニューラルネットワークを設計した。
層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,3)とし、
隠れ層の活性化関数にはTanh関数、
出力層の活性化関数にはSoftmax関数を使用した。
class Function(object): def tanh(self, x): return np.tanh(x) def dtanh(self, x): return 1. - x * x def softmax(self, x): e = np.exp(x - np.max(x)) # オーバーフローを防ぐ if e.ndim == 1: return e / np.sum(e, axis=0) else: return e / np.array([np.sum(e, axis=1)]).T # サンプル数が1の時
実装
※これ以降定義する関数は全てNewral_Networkクラスの中のメソッドである。
コンストラクタ
コンストラクタとは、新たにオブジェクトが生成される際に呼び出されて内容の初期化などを行なうメソッドのことであり、メッソド名は__init__とする。
ここでニューラルネットワークの構築を行う。
class Newral_Network(object): def __init__(self, unit): print "Number of layer = ",len(unit) print unit print "-------------------------" self.F = Function() self.unit = unit self.W = [] self.B = [] self.dW = [] for i in range(len(self.unit) - 1): w = np.random.rand(self.unit[i], self.unit[i + 1]) self.W.append(w) dw = np.random.rand(self.unit[i], self.unit[i + 1]) self.dW.append(dw) b = np.random.rand(self.unit[i + 1]) self.B.append(b)
unitは各層のユニット数が格納された配列である(今回の場合、unit=[2,3,3])。
unitの情報をもとに、学習するパラメータ(W, B)をランダムに初期化している。
dwは重みの修正量を保持する配列であり、モメンタムでの計算に使う。
順伝搬
構築したネットワークに入力値を与え、出力を得るforwardメソッドを定義する。
配列Zを用意し、そこに各層の出力値を加えていく。
# 順伝搬 def forward(self, _inputs): self.Z = [] self.Z.append(_inputs) for i in range(len(self.unit) - 1): u = self.U(self.Z[i], self.W[i], self.B[i]) if(i != len(self.unit) - 2): z = np.tanh(u) else: z = self.F.softmax(u) self.Z.append(z) return np.argmax(z, axis=1) # ユニットへの総入力を返す関数 def U(self, x, w, b): return np.dot(x, w) + b
forwardメソッドの返り値だが、
出力層の各ユニットの出力値の中で最も高い値を出力したユニットの番号、つまり入力値の属するクラスの予測値である。
誤差の算出
ニューラルネットワークの出力値と目標出力値(label)から誤差を算出する。
誤差関数には交差エントロピーを用いている。
※出力値はforwardメソッド内で算出された配列Zの中の1番末尾にある配列であり、forwardメソッドの返り値ではない。
# 誤差の計算 def calc_loss(self, label): error = np.sum(label * np.log(self.Z[-1]), axis=1) return -np.mean(error)
誤差逆伝搬
今回のプログラムの肝となる誤差逆伝搬法によるパラメータの学習を行う部分。
# 誤差逆伝搬 def backPropagate(self, _label, eta, M): # calculate output_delta and error terms W_grad = [] B_grad = [] for i in range(len(self.W)): w_grad = np.zeros_like(self.W[i]) W_grad.append(w_grad) b_grad = np.zeros_like(self.W[i]) B_grad.append(b_grad) output = True delta = np.zeros_like(self.Z[-1]) for i in range(len(self.W)): delta = self.calc_delta( delta, self.W[-(i)], self.Z[-(i + 1)], _label, output) W_grad[-(i + 1)], B_grad[-(i + 1)] = self.calc_grad(self.W[-(i + 1)], self.B[-(i + 1)], self.Z[-(i + 2)], delta) output = False # パラメータのチューニング for i in range(len(self.W)): self.W[i] = self.W[i] - eta * W_grad[i] + M * self.dW[i] self.B[i] = self.B[i] - eta * B_grad[i] # モメンタムの計算 self.dW[i] = -eta * W_grad[i] + M * self.dW[i] # デルタの計算 def calc_delta(self, delta_dash, w, z, label, output): # delta_dash : 1つ先の層のデルタ # w : pre_deltaとdeltaを繋ぐネットの重み # z : wへ向かう出力 if(output): delta = z - label else: delta = np.dot(delta_dash, w.T) * self.F.dtanh(z) return delta
デルタの計算方法は出力層とそれ以外の層では異なるため、bool変数であるoutputを定義し、その値によってデルタの算出方法を変えている。
変数Mはモメンタムでの加算の割合を制御するハイパーパラメータ。
学習
これまでに定義したメソッドを駆使し、ニューラルネットワークの出力が目標出力(label)に近づいていくようパラメータをチューニングしていく。
def train(self, dataset, N, iterations=1000, minibatch=4, eta=0.5, M=0.1): print "-----Train-----" # 入力データ inputs = dataset[:, :self.unit[0]] # 訓練データ label = dataset[:, self.unit[0]:] errors = [] for val in range(iterations): minibatch_errors = [] for index in range(0, N, minibatch): _inputs = inputs[index: index + minibatch] _label = label[index: index + minibatch] self.forward(_inputs) self.backPropagate(_label, eta, M) loss = self.calc_loss(_label) minibatch_errors.append(loss) En = sum(minibatch_errors) / len(minibatch_errors) print "epoch", val + 1, " : Loss = ", En errors.append(En) print "\n" errors = np.asarray(errors) plt.plot(errors)
うーん...なんかコードが汚い...
実際に3000回学習させた時の誤差の変動を示したグラフがこちら
結果
学習させたパラメータを引数に取るdraw_testメソッドを定義
def draw_test(self, x, label, W, B): self.W = W self.B = B x1_max = max(x[:, 0]) + 0.5 x2_max = max(x[:, 1]) + 0.5 x1_min = min(x[:, 0]) - 0.5 x2_min = min(x[:, 1]) - 0.5 xx, yy = np.meshgrid(np.arange(x1_min, x1_max, 0.01), np.arange(x2_min, x2_max, 0.01)) Z = self.forward(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) plt.contourf(xx, yy, Z, cmap=plt.cm.jet) plt.scatter(x[:, 0], x[:, 1], c=label, cmap=plt.cm.jet) plt.show()
認識できてる...!!
2クラス分類
scikit-learnのmake_moonsという機能を使って2クラスのデータも作成し、
クラスの分類を行うニューラルネットワークを構築し学習させた。
層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,2)とした。
ちなみに、隠れ層のユニット数を20にした場合
多少オーバーフィットを起こしている恐れがある。