UNISIA-SE Tech Blog

気まぐれお勉強日記

[Python] [12] 勾配降下法の実装サンプル

1. 前提条件


このページでは、勾配降下法についての実装サンプルを記載する。

以下、必要な前提知識。

▼ 下記ページを理解していること。
[Python] [10] 損失関数と数値微分の実装サンプル
[Python] [11] 偏微分と勾配の実装サンプル

▼ Python3.6、NumPyがインストールされていること。
このページでは、venvの仮想環境(Python3.6)上にNumPyをインストールした環境で、Python対話モード(Pythonインタプリタ)にて実装サンプルを記載している。

※ Python対話モード、NumPyについては下記を参考。
Python対話モード:[Python] 対話モード (インタプリタ) の使用方法
NumPy:[Python] [NumPy] インストールとnumpy.ndarrayの使用方法

2. 勾配法とは


下記ページで 数値微分による勾配偏微分による勾配 について解説したが、これらの 勾配 では、ある点を基準に実施した 勾配が示す方向 が分かっても どの程度その方向に 進むと 最小値(損失関数の結果が最小) に近づくかまで分からない。


そこでより正確な 最小値 を求めるために、この 勾配 を複数回実施して その勾配が示す方向に基準をずらし ながら 損失関数の結果が最小となる値 を探していく。

これを 勾配法 といい、最小値を探す 勾配降下法 と最大値を探す勾配上昇法 に分類される。
※ ニューラルネットワークの学習では、勾配降下法が多く用いられている。

ニューラルネットワークでも最適な 重みバイアス を学習時に決定するため 勾配法 を用いて損失関数の結果が最小となる値 を算出していくことになる。

変数が \(x_{0}\) と \(x_{1}\) の2つである場合を例に 勾配降下法 を数式で表現すると
\[ x_{0} = x_{0} - μ\frac{∂\ (x_{0}, x_{1})}{∂x_{0}} … (A) \]
\[ x_{1} = x_{1} - μ\frac{∂\ (x_{0}, x_{1})}{∂x_{1}} … (B) \]

と表せ、\(μ\)学習率といい、複数回の勾配を実施するにあたって前回の勾配が示す方向へどのくらい進めるかの 基準値となる。
(勾配1回に対し、\((A)\) と \((B)\) をそれぞれ一回実施する。)

※ \(\displaystyle \frac{∂\ (x_{0}, x_{1})}{∂x_{0}}\) と \(\displaystyle \frac{∂\ (x_{0}, x_{1})}{∂x_{1}}\) の 偏微分 については、下記を参考。

学習率 についての補足
下記ページで触れている 重み は、機械学習アルゴリズムにより自動で変化していくが、学習率 は、あらかじめ人の手で設定するパラメータとなり、深層学習では勾配法によって、最適化できない(しない) パラメータになる。
(このようなパラメータを ハイパーパラメータという。)

3. 勾配降下法のPython実装サンプル


下記ページで解説した勾配関数(num_gradient) を使用する。

勾配関数(num_gradient)

$ python
 >>> import numpy as np
 >>>
 >>> def num_gradient(f,x):    # 勾配関数
 ...     h = 1e-4
 ...     grad = np.zeros_like(x)    # xと同じ形状の配列で値がすべて 0
 ...
 ...     for idx in range(x.size):    # x の次元分ループする。 (下記例は、5.0, 10.0 の 2 周ループ)
 ...         idx_val = x[idx]
 ...         x[idx] = idx_val + h    # f(x + h)の算出。
 ...         fxh1 = f(x)
 ...
 ...         x[idx] = idx_val - h    # f(x - h)の算出。
 ...         fxh2 = f(x)
 ...
 ...         grad[idx] = (fxh1 - fxh2) / (2 * h)
 ...         x[idx] = idx_val    # 値をループ先頭の状態に戻す。
 ...     return grad
 ...
 >>>

▼ 勾配法の実装サンプル
この 勾配関数(num_gradient) をラップした形の 勾配法 となる上記 \((A)\) と \((B)\) の実装サンプル。
gradient_descent の引数
・第1引数「f」:最適化したい関数。
・第2引数「init_x」:初期値。
・第3引数「lr」:学習率。
・第4引数「step_num」:勾配を繰り返す回数。


 >>> # 上記対話モードの続き
 >>> def gradient_descent(f, init_x, lr=0.01, step_num=100):
 ...     x = init_x
 ...
 ...     for i in range(step_num):
 ...         grad = num_gradient(f, x)    # 上記 勾配関数「num_gradient」をコール
 ...         x -= lr * grad    # (A)、(B) の実装箇所
 ...
 ...     return x
 ...
 >>>

4. 勾配降下法の実装サンプル実行例


最後に実行サンプル。

▼ 勾配法の実行例
関数 \(f(x_{0}, x_{1}) = x^2_{0} + x^2_{1}\) で \(x_{0} = -15.0\) 、\(x_{1} = 20\) とした時の勾配法 gradient_descent の実行例。
gradient_descent の引数
・関数「f」:\(f(x_{0}, x_{1}) = x^2_{0} + x^2_{1}\)
・初期値「init_x」: \(x_{0} = -15.0\) 、\(x_{1} = 20\)
・学習率「lr」:0.1
・勾配繰返「step_num」:100


 >>> # ↑↑↑ 上記対話モードの続き
 >>> def func_ex(x):
 ...     return x[0]**2 + x[1]**2
 ...
 >>> init_x = np.array([-15.0, 20.0])
 >>>
 >>> gradient_descent(func_ex, init_x=init_x, lr=0.1, step_num=100)
 array([-3.05555396e-09,  4.07407195e-09])
 >>>

初期値 init_x: \(x_{0} = -15.0\) 、\(x_{1} = 20\) の実行結果は、 \(x_{0} = -3.05555396e-09\) 、\(x_{1} = 4.07407195e-09\) となり、真の最小値が \(x_{0} = 0\) 、\(x_{1} = 0\) なのでほぼ正しい結果となっていることが分かる。

e について
-3.05555396e-09 ≒ -3.1 * 10のマイナス9乗 ≒ -0.0000000031
4.07407195e-09 ≒ 4.1 * 10のマイナス9乗 ≒ 0.0000000041

試しに、学習率を 極端に増減 させてみると

 >>> # 上記対話モードの続き
 >>> gradient_descent(func_ex, init_x=init_x, lr=10, step_num=100)
 array([ 3.10343509e+12, -8.05258867e+12])
 >>>
 >>> gradient_descent(func_ex, init_x=init_x, lr=1e-10, step_num=100)
 array([-14.9999997,  19.9999996])
 >>>

学習率を \(10\) とした場合、\(x_{0} = 3.10343509e+12\) 、\(x_{1} = -8.05258867e+12\) となりプラス方向に大きく発散している。

これは、学習率が大きすぎることを意味しており、反対に学習率を \(1e-10\) と \(0\) に近づけすぎると \(x_{0} = -14.9999997\) 、\(x_{1} = 19.9999996\) となり初期値とほぼ同じ値が出力され、学習率が小さすぎることを意味している。

実際のニューラルネットワークの学習においても、以上のように妥当な 学習率 を事前に決め、勾配 を繰り返し、真の最小値を求めていくことになる。

以上。


【参考文献】
斎藤 康毅 (2018) 『ゼロから作るDeep Learning - Pythonで学ぶディープラーニングの理論と実装』株式会社オライリー・ジャパン


Copyright UNISIA-SE All Rights Reserved.
s-hama@unisia-se.jp