前回までの学んだことはこちら
ミニバッチ学習
機械学習は、膨大がデータセットがないと行うことはできません。
しかし、その全てのデータにおいて損失関数の計算を行うのは時間がかかります。
そこで、データの中から一部を選び出しその一部のデータを全体の「近似」として利用したりします。
このような学習方法をミニバッチ学習というようです。
訓練データの中から指定された個数のデータをランダムに選び出すコードを書いてみます。
import sys, os sys.path.append(os.pardir) import numpy as np from dataset.mnist import load_mnist (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) print(x_train.shape) # (60000, 784) print(t_train.shape) # (60000, 10) # ランダムに10枚だけ抜き出す train_size = x_train.shape[0] batch_size = 10 batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # この出力は実行の度に変化する # ex)[42046 53515 8543 48925 23975 16930 51302 58674 14433 35769] # 0~60000までのインデックス中からランダムで出力される print(batch_mask)
1に微分、2に微分、3に(ry
機械学習といえば微分。
どんな書籍、サイトをみても微分と記載していないものはないだろう。
微分が必要な理由は損失関数にあります。
NNの学習では、最適なパラメータ(重み&バイアス)を探索する際に、損失関数の値をできるだけ小さくなるようにパラメータを探します。
ここで、できるだけ小さな損失関数の場所を探すために、パラメータの微分(正確には勾配)を計算し、その微分の値を手がかりにパラメータの値を徐々に更新して、最適なパラメータを探索します。
あるニューラルネットワークがあると仮定し、その中のひとつの重みパラメータに注目してみると、ひとつの重みパラメータの損失関数に対する微分は
その重みパラメータの値を少しだけ変化させた時に、損失関数がどのように変化するか
ということを表します。
この微分値がマイナスだった場合(右肩下がりの接線の場合)、その重みパラメータを正の方向へ変化させることで、損失関数を減少させることができます。
逆に微分値がプラスだった場合(右肩上がりの接線の場合)、その重みパラメータを負の方向へ変化させることで、損失関数を減少させることができます。
微分の復習
微分とは、ある瞬間における変化の量を表したものでしたね。
(高校の時にやってる・・・はず・・・)
import numpy as np import matplotlib.pylab as plt # 数値微分 def numerical_diff(f, x): h = 1e-4 # 0.0001 限りなく0に近い数 return (f(x+h) - f(x-h)) / (2*h) # 微分対象関数 def function_1(x): return 0.01*x**2 + 0.1*x def tangent_line(f, x): d = numerical_diff(f, x) print(d) # 0.1999999999990898 y = f(x) - d*x return lambda t: d*t + y x = np.arange(0.0, 20.0, 0.1) # 0から20まで0.1刻みの配列作成 y = function_1(x) # 今回微分したい関数 plt.xlabel("x") plt.ylabel("f(x)") tf = tangent_line(function_1, 5) # x = 5で微分 y2 = tf(x) plt.plot(x, y) plt.plot(x, y2) plt.show()
画像をみて分かる通り、x = 5の部分で微分されていますね。
さて、微分には偏微分というものがあったのを覚えていますでしょうか。
微分対象の関数は上記のように変数が1つだけではなく、2つ、3つの場合もあると思います。
そういった時に、偏微分を利用することで、どの変数に対する微分かを区別することができます。
偏微分について記憶が曖昧な場合はググってみてください。
勾配
偏微分は、複数の変数がある場合に、特定の変数にのみ注目して微分することでした。
例えば、x1とx2という変数があった場合は、それそれについて微分するので、計算が2回必要です。
せっかくですから、まとめて計算を行いたいですよね。そこで登場するのが勾配です。
勾配とは、すべての変数の偏微分をベクトルとしてまとめたものです。
下に実装例を紹介します。
# coding: utf-8 # 全ての変数を一度に偏微分します # すべての変数の偏微分をベクトルとしてまとめたもの:勾配 import numpy as np import matplotlib.pylab as plt from mpl_toolkits.mplot3d import Axes3D %matplotlib inline def _numerical_gradient_no_batch(f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x) # xと同じ形状の配列を作成(全要素が0) for idx in range(x.size): tmp_val = x[idx] # f(x+h)の計算 x[idx] = float(tmp_val) + h fxh1 = f(x) # f(x+h) # f(x-h)の計算 x[idx] = tmp_val - h fxh2 = f(x) # f(x-h) # 中心差分をとって微分 grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 値を元に戻す return grad # 勾配を求める関数 def numerical_gradient(f, X): # 1次元にも対応 if X.ndim == 1: return _numerical_gradient_no_batch(f, X) else: grad = np.zeros_like(X) # xと同じ形状の配列を作成(全要素が0) # enumerate関数を用いると、[インデックス番号, 要素]が取得できる。 for idx, x in enumerate(X): grad[idx] = _numerical_gradient_no_batch(f, x) return grad # y = x0**2 + x1**2 の関数 def function_2(x): # 1次元の配列の場合とそうでない場合で場合分け(ndimで次元数を取得) if x.ndim == 1: return np.sum(x**2) else: return np.sum(x**2, axis=1) # メイン処理 if __name__ == '__main__': # [-2.0, -1.75, -1.5, -1.25, -1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25] x0 = np.arange(-2, 2.5, 0.25) x1 = np.arange(-2, 2.5, 0.25) # 格子点を生成 X, Y = np.meshgrid(x0, x1) # ネストされた配列を1次元配列にフラット化 X = X.flatten() Y = Y.flatten() # 勾配(gradient)を求める grad = numerical_gradient(function_2, np.array([X, Y])) # グラフの描画 plt.figure() plt.quiver(X, Y, -grad[0], -grad[1], angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444") plt.xlim([-2, 2]) plt.ylim([-2, 2]) plt.xlabel('x0') plt.ylabel('x1') plt.grid() plt.legend() plt.draw() plt.show()
この結果をみてみると...
向きを持ったベクトルとして表示されます。
勾配は、関数f(x)の「一番低い場所(最小値)」を指しているように見えますね。
正確に述べると、各場所において関数の値を最も減らす方向を向いています。
この勾配が指し示す方向に進むことで、関数(損失関数)の値を減らすことができます。
勾配法
勾配法とは、現在の場所から勾配方向に一定の距離だけ進み、移動した先でも同様に勾配を求め、その勾配方向へ進む、ということを繰り返して勾配方向へ移動し、関数の値を徐々に減らすことを指します。
勾配法では、1回の学習でどれだけ学習すべきか、どれだけパラメータを更新するべきか、ということを決めるのに学習率という変数を用います。
下記で実装してみます。
import numpy as np import matplotlib.pylab as plt from mine_gradient_2d import numerical_gradient %matplotlib inline # 勾配降下法 # 引数fは最適化したい関数、init_xは初期値、lrは学習率、step_numは勾配法の繰り返し回数 def gradient_descent(f, init_x, lr = 0.01, step_num = 100): x = init_x # 0~100までループ for i in range(step_num): # 関数の勾配を求める grad = numerical_gradient(f, x) # 勾配法の数式に当てはめる(xを勾配の方向へ移動) x -= lr * grad return x def function_2(x): return x[0]**2 + x[1]**2 init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100) # array([-6.11110793e-10, 8.14814391e-10])
結果が[-6.11110793e-10, 8.14814391e-10]なので、ほとんど(0,0)まで近づいていることが分かると思います。
ちなみに、更新のプロセスはこんな感じです。
また長くなりそうなので、続きは別記事で。
最後に参考にさせて頂いたサイトのリンクを掲載しておきます。
参考サイト
NumPyの軸と次元数はこちらのブログに詳しく載っていました。
また、3Dグラフに描画については、こちらのブログが参考になります。