default eye-catch image.

勾配降下法

[mathjax] 各地点において関数の値を最大にするベクトル((frac{partial f}{partial x_0},frac{partial f}{partial x_1}))を全地点に対して計算したものを勾配とかいう。 ある地点において、このベクトルの方向に向かうことにより最も関数の値を大きくする。 で、今後のために正負を反転して関数の値を最小にするベクトルを考えることにした。 関数の値を小さくする操作を繰り返していけば、いずれ\"最小値\"が見つかるはず。 というモチベを続けるのが勾配降下法。学習率(eta)を使って以下みたいに書ける。。 begin{eqnarray} x_0 = x_0 - eta frac{partial f}{partial x_0} \\ x_1 = x_1 - eta frac{partial f}{partial x_1} end{eqnarray} ということで(f(x_0,x_1)=x_0^2+x_1^2)の最小値を初期値((3.0,4.0))、 学習率(eta=0.1)に設定して計算してみる。 import numpy as np def numerical_gradient(f, x): h = 1e-4 grad = np.zeros_like(x) for idx in range(x.size): tmp_val = x[idx] x[idx] = tmp_val + h fxh1 = f(x) x[idx] = tmp_val - h fxh2 = f(x) grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val return grad def gradient_descent(f, init_x, lr=0.01, step_num=100): x = init_x for i in range(step_num): grad = numerical_gradient(f,x) x -= lr * grad return x def function2(x): return x[0]**2 + x[1]**2 init_x = np.array([-3.0, 4.0]) v = gradient_descent(function2, init_x=init_x, lr=0.1, step_num=100) v # array([-6.11110793e-10, 8.14814391e-10]) ((0,0))に収束した。 ニューラルネットワークの勾配 損失関数を重みパラメータで微分する。以下みたいな感じ。 損失関数の大小を見るとして、例えば(w_{11})以外の重みを固定したとして(w_{11})をわずかに 増やしたときに損失関数の値がどれだけ大きくなるか。 損失関数の値はパラメータ(W)と入力(x)から決まるベクトルだけれども、それぞれ乱数と入力値が設定されている。 begin{eqnarray} W= begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} end{pmatrix}, frac{partial L}{partial W}= begin{pmatrix} frac{partial L}{partial w_{11}} & frac{partial L}{partial w_{12}} & frac{partial L}{partial w_{13}} \\ frac{partial L}{partial w_{21}} & frac{partial L}{partial w_{22}} & frac{partial L}{partial w_{23}} end{pmatrix} end{eqnarray} 重み(W)が乱数で決まるネットワークがあるとする。このネットワークは入力と重みの積を出力 として返す。出力はSoftmaxを経由するとする。 ネットワークの出力と教師データのクロスエントロピー誤差を誤差として使う。 その前に、数値微分関数を多次元対応する。 普通、配列の次元が(n)個になると(n)重ループが必要になるけれども、 Numpy.nditer()を使うと(n)乗ループを1回のループにまとめることができる。 下のmulti_indexが((0,0),(0,1),(0,2),(1,0),(1,1),(1,2))みたいに イテレータが(n)次のタプルを返す。反復回数はタプルの要素数の直積。 Numpy配列にそのタプルでアクセスすることで晴れて全ての要素にアクセスできる。 def numerical_gradient_md(f, x): h = 1e-4 grad = np.zeros_like(x) it = np.nditer(x, flags=[\'multi_index\'], op_flags=[\'readwrite\']) while not it.finished: idx = it.multi_index tmp_val = x[idx] x[idx] = tmp_val + h fxh1 = f(x) # f(x+h) x[idx] = tmp_val - h fxh2 = f(x) # f(x-h) grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 値を元に戻す it.iternext() return grad 初期値(x=(0.6,0.9))、教師データ(t=(0,0,1))をネットワークに入力する。 predict()は(1 times 3)を返す。 それをSoftmax()を通して、(t)とのクロスエントロピー誤差を求めたものが以下。 import numpy as np def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) y = y.reshape(1,y.size) batch_size = y.shape[0] delta = 1e-7 return -np.sum( t * np.log( y + delta)) / batch_size def softmax(x): c = np.max(x) return np.exp(x-c) / np.sum(np.exp(x-c)) import sys, os sys.path.append(os.pardir) import numpy as np class simpleNet: def __init__(self): self.W = np.random.randn(2,3) def predict(self, x): return np.dot(x, self.W) def loss(self, x, t): z = self.predict(x) y = softmax(z) loss = cross_entropy_error(y, t) return loss net = simpleNet() x = np.array([0.6, 0.9]) p = net.predict(x) t = np.array([0, 0, 1]) net.loss(x, t) # 0.9463818740797788 このlossを(W)で微分したのが以下。 あえてパラメータ(W)を引数にとり損失関数の値を計算する(f(W))を定義することで、 数値微分が何と何の演算なのかをわかりやすくしている。 実際は(f(W))は(W)とは関係なく(x)と(t)だけから結果を返すけれども、 損失関数(f(W))を(W)で微分するという操作が自明になるようにコードを合わせている。 def f(W): return net.loss(x, t) dW = numerical_gradient_md(f, net.W) dW # array([[ 0.07627371, 0.49923236, -0.57550607], # [ 0.11441057, 0.74884853, -0.8632591 ]]) 結果の解釈 上記の(w),(W),(t)から(frac{partial L}{partial W})が求まった。 損失関数が何か複雑な形をしているという状況で、 (frac{partial L}{partial w_{11}})は(w_{11})がわずかに動いたときに損失関数の値が変化する量を表している。 それが(w_{11})から(w_{32})まで6個分存在する。 begin{eqnarray} frac{partial L}{partial W} = begin{pmatrix} frac{partial L}{partial w_{11}} & frac{partial L}{partial w_{21}} & frac{partial L}{partial w_{31}} \\ frac{partial L}{partial w_{12}} & frac{partial L}{partial w_{22}} & frac{partial L}{partial w_{32}} end{pmatrix} = begin{pmatrix} 0.07627371 & 0.49923236 & -0.57550607 \\ 0.11441057 & 0.74884853 & -0.8632591 end{pmatrix} end{eqnarray}

default eye-catch image.

勾配の可視化

[mathjax] 2変数関数(f(x_0,x_1))を各変数で偏微分する。 地点((i,j))におけるベクトル((frac{partial f(x_0,j)}{partial x_0},frac{partial f(i,x_1)}{partial x_1}))を全地点で記録していき、ベクトル場を得る。 このベクトル場が勾配(gradient)。 (f(x_0,x_1)=x_0^2+x_1^2)について、(-4.0 le x_0 le 4.0)、(-4.0 le x_1 le 4.0)の範囲で、 勾配を求めてみる。また、勾配を可視化してみる。 まず、2変数関数(f(x_0,x_1))の偏微分係数を求める関数の定義。 ((3.0,3.0))の偏微分係数は((6.00..,6.00..))。 def numerical_gradient(f, x): h = 10e-4 grad = np.zeros_like(x) for idx in range(x.size): tmp_val = x[idx] x[idx] = tmp_val + h fxh1 = f(x) x[idx] = tmp_val - h fxh2 = f(x) grad[idx] = (fxh1 - fxh2) / 2*h x[idx] = tmp_val return grad def function2(x): return x[0]**2 + x[1]**2 p = np.array([3.0,3.0]) v = numerical_gradient(function2, p) v # array([6.e-06, 6.e-06]) (-4.0 le x_0 le 4.0)、(-4.0 le x_1 le 4.0)の範囲((0.5)刻み)で偏微分係数を求めて、 ベクトル場っぽく表示してみる。matplotlibのquiver()は便利。 各地点において関数の値を最も増やす方向が表示されている。 w_range = 4 dw = 0.5 w0 = np.arange(-w_range, w_range, dw) w1 = np.arange(-w_range, w_range, dw) wn = w0.shape[0] diff_w0 = np.zeros((len(w0), len(w1))) diff_w1 = np.zeros((len(w0), len(w1))) for i0 in range(wn): for i1 in range(wn): d = numerical_gradient(function2, np.array([ w0[i0], w1[i1] ])) diff_w0[i1, i0], diff_w1[i1, i0] = (d[0], d[1]) plt.xlabel(\'$x_0$\',fontsize=14) #x軸のラベル plt.ylabel(\'$x_1$\',fontsize=14) #y軸のラベル plt.xticks(range(-w_range,w_range+1,1)) #x軸に表示する値 plt.yticks(range(-w_range,w_range+1,1)) #y軸に表示する値 plt.quiver(w0, w1, diff_w0, diff_w1) plt.show() 値が大きい方向に矢印が向いている。例えば((-3.0,3.0))における偏微分係数は((-6.0,6.0))。 左上方向へのベクトル。 参考にしている本にはことわりが書いてあり、勾配にマイナスをつけたものを図にしている。 その場合、関数の値を最も減らす方向が表示されることになる。 各地点において、この勾配を参照することで、どちらに移動すれば関数の値を最も小さくできるかがわかる。

default eye-catch image.

おっさんが数値微分を復習する

引き続き、ゼロDの写経を続ける。今回は、学生の頃が懐かしい懐ワード、数値微分。 全然Deepに入れないけれどおっさんの復習。解析的な微分と数値微分が一致するところを確認してみる。 昔と違うのは、PythonとJupyterNotebookで超絶簡単に実験できるし、 こうやってWordPressでLaTeXで記事を書いたりできる点。 [mathjax] まず、微分の基本的な考え方は以下の通り。高校数学の数3の範囲。 begin{eqnarray} frac{df(x)}{fx} = lim_{hrightarrow infty} frac{f(x+h)-f(x)}{h} end{eqnarray} 情報系学科に入って最初の方でEuler法とRunge-Kutta法を教わってコードを 書いたりレポート書いたりする。懐すぎる..。 または、基本情報の試験かなんかで、小さい値と小さい値どうしの計算で発生する問題が現れる。 ゼロDにはこの定義を少し改良した方法が載っている。へぇ。 begin{eqnarray} frac{df(x)}{fx} = lim_{hrightarrow infty} frac{f(x+h)-f(x-h)}{2h} end{eqnarray} 写経なので、がんばって数値微分を書いて動かしてみる。 簡単な2次関数(f(x))。 begin{eqnarray} f(x) &=& x^2 - 5x +3 \\ f\'(x) &=& 2x - 5 end{eqnarray} def numerical_diff(f, x): h = 10e-4 return (f(x+h) - f(x-h)) / (2*h) (f(x))と、(x=2.5)のところの(f\'(x))をmatplotlibで書いてみる。懐い... import matplotlib.pyplot as plt import numpy as np def f(x): return x**2 - 5*x + 3 x = np.arange(-10, 10, 0.1) y = f(x) dy = numerical_diff(f,x) plt.plot(x, y) x1 = -2.5 dy1 = numerical_diff(f, x1) y1 = f(x1) # y-y1 = dy1(x-x1) -> y = dy1(x-x1) + y1 j = lambda x: dy1 * (x-x1) + y1 plt.plot(x,j(x)) plt.xlabel(\'x\') plt.ylabel(\'y\') plt.grid() plt.show() 偏微分 2変数以上の関数の数値微分は以下の通り。片方を止める。 数値微分の方法は上記と同じものを使った。 begin{eqnarray} frac{partial f(x_0,x_1)}{partial x_0} &=& lim_{hrightarrow infty} frac{f(x_0 +h,x_1)-f(x_0-h,x_1)}{2h} \\ frac{partial f(x_0,x_1)}{partial x_1} &=& lim_{hrightarrow infty} frac{f(x_0,x_1+h)-f(x_0,x_1-h)}{2h} end{eqnarray} ((x_0,x_1)=(1,1))における(x_0)に対する偏微分(frac{partial f(x_0,x_1)}{x_0})、(x_1)に対する偏微分(frac{partial f(x_0,x_1)}{x_1})を求めてみる。 ちゃんと(frac{partial f(x_0,1.0)}{x_0}=2.00..)、(frac{partial f(1.0,x_1)}{x_1}=2.00..)になった。 import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import Axes3D def f(x): return x[0]**2 + x[1]**2 X = np.meshgrid(np.arange(-5., 5., 0.2),np.arange(-5., 5., 0.2)) Z = f(X) fig = plt.figure(figsize=(6, 6)) axes = fig.add_subplot(111, projection=\'3d\') axes.plot_surface(X[0],X[1], Z) f0 = lambda x: x**2 + 1.0**2 f1 = lambda x: 1.0**2 + x**2 df0 = numerical_diff(f0, 1.0) df1 = numerical_diff(f1, 1.0) print(df0) # 2.0000000000000018 print(df1) # 2.0000000000000018 plt.show()

default eye-catch image.

誤差, 2乗誤差と交差エントロピー誤差

台風で自宅に篭れるから勉強時間をとれるな..、と見積もってたのだけれども、 近所の多摩川がマジで溢れそうでそれどころではなく...。 時間が空いてしまったがゼロから作るDeepLearningを読んで実際に実装する作業を再開する。 今後、パラメータを更新していくのだが、どういう方針でパラメータを更新するか決めておく必要がある。 教師ありデータを使った学習を扱っている訳で、訓練データと対応する教師データが与えられている前提。 何かの学習をした結果のモデルの出力と教師データの差を「誤差」として、「誤差」が小さくなるように パラメータを決めていこうという方針。 例えば手書き文字認識で言うところの「認識精度」を指標に使ってしまうと、 モデルの出力が微小に変化したところで「認識精度」は微小に変化しない状況が発生する。 「認識精度」が変化するときは一気に変化する。これではパラメータをどの方向にずらして良いかわからない。 ※SVMの解説で非線形分離を行う決定境界を0/1損失で決めることの問題点に通じる。 非線形分離を行う決定境界も損失関数により微小な変化に追従して決めていく。 2乗和誤差,クロスエントロピー誤差 ということで誤差関数を導入する。 (y_k)はモデルの出力で、最終段でSoftMax関数を通してある。 まずは2乗和誤差。まぁ簡単で、正解と出力の差を2乗した値を足す。 [mathjax] begin{eqnarray} E = frac{1}{2} sum_{k=1}^N (y_k - t_k)^2 end{eqnarray} 次にクロスエントロピー誤差。 begin{eqnarray} E = - sum_{k=1}^N t_k log y_k end{eqnarray} どっちでも良いんじゃ..、と思う訳だけれども、非線形分離問題で決定境界を決めるときに、 正解をより正解として、誤りをより誤りとして表現できる誤差がより優秀なので、 クロスエントロピー誤差の方が適切ではある。 クロスエントロピー誤差の方は(t_k)がゼロの項はゼロになるので、(t_k)が1の項だけ計算すれば良い。 つまり、正解が1のケースについてのみ誤差値が発生する。 (-log y_k)は(y_k)がゼロに近いと急激に値が大きくなる。 これにより、(t_k=1)なのにゼロに近い(y_k)が出力されたときに大きなペナルティを与えられる。 クロスエントロピー誤差関数の実装 バッチ(並列実行)対応のクロスエントロピー誤差関数。 def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) y = y.reshape(1,y.size) batch_size = y.shape[0] delta = 10e-7 return -np.sum( t * np.log( y + delta)) / batch_size 1次元のデータを与えてみる。 t1 = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] y1 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] cross_entropy_error(np.array(y1), np.array(t1)) # 0.5108239571007129 2次元のデータを与えてみる。 t2 = [ [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]] y2 = [[0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0], [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]] cross_entropy_error(np.array(y2), np.array(t2)) # 7.163167257532494 バッチ対応が簡単に書けるところがかなり美しい。

default eye-catch image.

MNISTの手書き文字認識用データ取得クラスの作成

MNISTから手書き文字認識用のデータセットをロードするクラスを作ってみた。 ロードしたデータセットをpickleでシリアライズ、デシリアライズする機能付き。 後々改造する予定でここに貼ったのはメンテしない予定。 たったこれだけ書くのに40分も要してしまった...。 Pythonの冗長感が半端ないけども慣れるしかない。 マジックコードだらけだけども、MNISTの手書き文字認識用データ取得専用だから仕方ない。 get_image()関数により訓練データ、訓練ラベル、テストデータ、テストラベルを取れる。 データは1次元になって入る。つまり1ファイルごとに28*28のサイズがある。 import os import urllib.request import numpy as np import gzip import pickle class MnistLoader: def __init__(self, mnistdir): self.url_base = \'http://yann.lecun.com/exdb/mnist/\' self.dataset_dir = mnistdir self.save_path = self.dataset_dir + \'mnist.pkl\' self.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\' } self.dataset = {} if os.path.exists(self.save_path): with open(self.save_path,\'rb\') as f: self.dataset = pickle.load(f) else: self.__load_mnist() self.dataset[\'train_img\'] = self.__load_img(self.key_file[\'train_img\']) self.dataset[\'train_label\'] = self.__load_label(self.key_file[\'train_label\']) self.dataset[\'test_img\'] = self.__load_img(self.key_file[\'test_img\']) self.dataset[\'test_label\'] = self.__load_label(self.key_file[\'test_label\']) with open(self.save_path, \'wb\') as f: pickle.dump(dataset, f, -1) def __load_mnist(self): \'\'\' load mnist data and store to file \'\'\' for v in self.key_file.values(): file_path = self.dataset_dir + v urllib.request.urlretrieve(url_base + v, self.dataset_dir + v) def __load_img(self, file_name): file_path = self.dataset_dir + file_name with gzip.open(file_path, \'rb\') as f: data = np.frombuffer(f.read(), np.uint8, offset=16) data = data.reshape(-1, 784) return data def __load_label(self,file_name): file_path = dataset_dir + \'/\' + file_name with gzip.open(file_path, \'rb\') as f: labels = np.frombuffer(f.read(), np.uint8, offset=8) return labels def get_image(self): return (self.dataset[\'train_img\'] , self.dataset[\'train_label\']), (self.dataset[\'test_img\'], self.dataset[\'test_label\']) mnist_loader = MnistLoader(mnistdir=\'/Users/ikuty/Documents/mnist/\') PILを使って訓練データの1枚目を表示するテスト。 import sys, os sys.path.append(os.pardir) import numpy as np from PIL import Image def img_show(img): pil_img = Image.fromarray(np.uint8(img)) pil_img.show() (x_train, t_train), (x_test, t_test) = mnist_loader.get_image() img = x_train[0] label = t_train[0] bimg = img.reshape(28, 28) img_show(bimg) こんなのが出る。(28x28しかなくて小さすぎなので256x256に引き伸ばして表示。)

default eye-catch image.

SoftMax関数

[mathjax] 出力層の総和を1にするように調整できれば、出力層を確率としてとらえることができるようになる。 入力層に画像を放り込み、出力層でラベルに属する確率を出せば、画像にラベルをつける分類器になる。 入力が(n)ノードあるとして、全てのノードを入力を受けて出力する。 出力側のノードが(m)ノードあるとして、和が(1)になるようにどう操作するか。 分母は入力側の全ノードの和、分子は対応する1ノードだけ。そうやって1にする。 指数関数に入力を食わせて和を取る。指数関数は単調増加関数だから、 入力の大小と出力の大小は一致する。定義上は以下の通り。 begin{eqnarray} y_k &=& frac{e^{a_k}}{sum_{i=1}^n e^{a_i}} end{eqnarray} import numpy as np def softmax1(x): return np.exp(x) / np.sum(np.exp(x)) a = np.array([1010,1000,990]) r1 = softmax1(a) r1 # array([nan, nan, nan]) ただ、この通りにするとオーバーフローを起こすので、 式変形してオーバーフローを回避する。(C)は入力ノードの最大値。 begin{eqnarray} y_k &=& frac{e^{a_k}}{sum_{i=1}^n e^{a_i}} \\ &=& frac{C e^{a_k}}{Csum_{i=1}^n e^{a_i}} \\ &=& frac{e^{a_k+log{C}}}{sum_{i=1}^{n} e^{a_i + log{C}}} \\ &=& frac{e^{a_k+C\'}}{sum_{i=1}^{n} e^{a_i + C\'}} end{eqnarray} import numpy as np def softmax2(x): c = np.max(x) return np.exp(x-c) / np.sum(np.exp(x-c)) a = np.array([1010,1000,990]) r2 = softmax2(a) r2 # array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09]) SoftMax後の和は1 前述の通り、SoftMax後の和は1。 z = np.sum(r2) z # 1.0

default eye-catch image.

行列の積和と順伝播

[mathjax] 順伝搬(forward)はNumpyの積和計算を使って超絶簡単に記述できる。 行列の積和計算 何はともあれNumpyで行列の積を計算する方法について。 この仕組みがあるから、forなどの制御構造を使わずに行列の積を実現できる。 begin{eqnarray} A &=& begin{pmatrix} a_{11} & a_{21} \\ a_{12} & a_{22} end{pmatrix} \\ B &=& begin{pmatrix} b_{11} & b_{21} \\ b_{12} & b_{22} end{pmatrix} \\ A cdot B &=& begin{pmatrix} a_{11} b_{11} + a_{21} b_{12} && a_{11} b_{21} + a_{21} b_{22} \\ a_{12} b_{11} + a_{22} b_{12} && a_{12} b_{21} + a_{22} b_{22} end{pmatrix} end{eqnarray} 積はNumpyのdot関数で一発。 左行列と右行列の\"対応する行\"は一致している必要がある。 つまり(N_1 times M_1 cdot N_2 times M_2)という積計算において(M_1 = N_2)である必要がある。 a11,a21,a12,a22 = (1,2,3,4) b11,b21,b12,b22 = (1,2,3,4) a = np.array([[a11,a21],[a12,a22]]) b = np.array([[b11,b21],[b12,b22]]) np.dot(a,b) # array([[ 7, 10], # [15, 22]]) 和はNumpyの+オペレータで一発。 begin{eqnarray} A &=& begin{pmatrix} a_{11} & a_{21} \\ a_{12} & a_{22} end{pmatrix} C &=& begin{pmatrix} c_{1} \\ c_{2} end{pmatrix} end{eqnarray} begin{eqnarray} A+C &=& begin{pmatrix} a_{11} + c_1 & a_{21} + c_1 \\ a_{12} + c_1 & a_{22} + c_2 end{pmatrix} end{eqnarray} c1, c2 = (5,5) c = np.array([[c1],[c2]]) a + c # array([[6, 7], # [8, 9]]) 各層における信号伝達の実装 Numpyで積和が簡単にかける。 すなわち、各層における伝達が積和で表されるとすると、 入力層から各層における信号伝達の実装を簡単にかける。 左側の層が(X=begin{pmatrix}x_1 x_2end{pmatrix})、重みが(W=begin{pmatrix}w_{11} & w_{21} \\ w_{12} & w_{22} end{pmatrix})、 右側の層が(A=begin{pmatrix} a_{11} & a_{21} end{pmatrix})とする。 すると、右層と左層の関係は以下の通り。 begin{eqnarray} A &=& XW \\ begin{pmatrix} a_{11} & a_{21} end{pmatrix} &=& begin{pmatrix}x_1 x_2end{pmatrix} begin{pmatrix}w_{11} & w_{21} \\ w_{12} & w_{22} end{pmatrix} \\ &=& begin{pmatrix} w_{11}x_1 + w_{21} x_2 \\ w_{12}x_1 + w_{22}x_2end{pmatrix} end{eqnarray} 左層にバイアス(B=begin{pmatrix}b_1 \\ b_1 end{pmatrix})があった場合、線形和で表現できる。 行と列がどちらの方向に対応するかに注意。 1つの層に(N)個のノードが存在することを(Ntimes 1)の行列で表現する。 begin{eqnarray} A &=& XW + B \\ begin{pmatrix} a_{11} & a_{21} end{pmatrix} &=& begin{pmatrix}x_1 x_2end{pmatrix} begin{pmatrix}w_{11} & w_{21} \\ w_{12} & w_{22} end{pmatrix} + begin{pmatrix}b_1 \\b_1 end{pmatrix} \\ &=& begin{pmatrix} w_{11}x_1 + w_{21} x_2 + b_1 \\ w_{12}x_1 + w_{22}x_2 + b_1end{pmatrix} end{eqnarray} x1,x2 = (0.4,0.9) w11,w21,w12,w22 = (0.8,0.5,0.1,0.3) b1 = 0.1 x = np.array([x1,x2]) w = np.array([[w11,w21],[w12,w22]]) b = np.array([b1,b1]) # これで一発 a = np.dot(x,w) + b # array([0.51, 0.57]) # 活性化関数としてsigmoid関数をかます z = sigmoid(a) # array([0.62480647, 0.63876318]) 順伝播 入力層から出力層まで上記のような計算を繰り返していく。これを\"順伝播\"とか言う。 順伝\"搬\"ではなく、順伝\"播\"。ネットワークが多層になったとしても全く同様。 以下みたいにNumpyの積和計算を繰り返すだけで出来る。 def init_network(): network = {} network[\'W1\'] = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]]) network[\'b1\'] = np.array([0.1,0.2,0.3]) network[\'W2\'] = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]]) network[\'b2\'] = np.array([0.1,0.2]) network[\'W3\'] = np.array([[0.1,0.3],[0.2,0.4]]) network[\'b3\'] = np.array([0.1,0.2]) return network def forward(network, x): W1, W2, W3 = network[\'W1\'], network[\'W2\'], network[\'W3\'] b1,b2,b3 = network[\'b1\'], network[\'b2\'], network[\'b3\'] a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) a3 = np.dot(z2, W3) + b3 y = a3 return y network = init_network() x = np.array([1.0,0.5]) y = forward(network,x) # array([0.31682708, 0.69627909])

default eye-catch image.

活性化関数の実装。Step,Sigmoid,ReLU

[mathjax] 深層学習入門。Python,Numpyにも少し慣れてきたので、 Numpyだけで伝搬,逆伝搬を計算することで深層学習に慣れていく。 単に自分の理解のためだけの記事なので、誤りがあっても気にしない。 活性化関数 まず活性化関数。activation function。 3つ(x_1,x_2,x_3)の入力があったとする。それぞれに重み(w_1,w_2,w_3)がかかるとする。 ノードは入力に重みをかけた和、つまり(w_1x_1 + w_2x_2 + w_3x_3)を受けるものとする。 ノードは受けた値の大きさに応じて出力を返す機能を持つ。 受けた値と出力の関係(まさに関数)を活性化関数と言って、 ステップ関数,Sigmoid関数,ReLU,Softmax関数などいくつか種類がある。 Step関数,Sigmoid関数,ReLUをNumpyだけで実装してみる。 import numpy as np import matplotlib.pyplot as plt # sigmoid function def sigmoid(x): return 1 / (1+np.exp(-x)) # step function def step(x): return np.array(x > 0, dtype=int) # ReLU def relu(x): return np.maximum(0,x) / 5 x = np.arange(-5.0,5.0,0.1) y = sigmoid(x) x = np.arange(-5.0,5.0,0.1) y1 = sigmoid(x) y2 = step(x) y3 = relu(x) plt.plot(x,y1) plt.plot(x,y2) plt.plot(x,y3) plt.ylim(-0.1,1.1) plt.show() どんなに入力信号が大きくても、出力を0から1の間に押し込める。 入力が大きければ出力が大きいという意図はあるものの、 入力と出力の関係が非線形になっているものが多い。 活性化関数が線形関数だと、ネットワークを重ねていったとしても、 ネットワークの最初の入り口と最後の出口を見たとして、1つの線形関数を通しただけ... ということになる。つまり加算と定数倍は何回実行したとしても、 一つの定数倍、加算の計算にまとめることができる。 これだと、層を重ねる意味がなくなってしまう。 活性化関数が非線形だと、重ねた活性化関数をまとめることはできず、 複雑な入出力を表現できるため、活性化関数として非線形関数を使用する。