概要
勾配降下法を使って、関数の値が小さくなる方向へ少しずつ移動する考え方を整理する。
ニューラルネットワークの学習では、損失関数を小さくするために、勾配を手がかりとしてパラメータを更新する。
ここでは、単純な関数を例に、勾配、学習率、更新式をPython実装で確認する。
この記事の構成
- 勾配法とは
勾配法の意味と基本的な考え方を整理。 - 勾配降下法のPython実装サンプル
勾配降下法のPython実装サンプルをコードや具体例で確認。 - 勾配降下法の実装サンプル実行例
勾配降下法の実装サンプル実行例をコードや具体例で確認。
概念の説明と実装サンプル
勾配法とは
- 勾配法と学習率
下記の記事で数値微分による勾配と偏微分による勾配について解説したが、これらの勾配では、ある点を基準に実施した勾配が示す方向が分かってもどの程度その方向に進むと最小値(損失関数の結果が最小)に近づくかまで分からない。
そこで、現在の点から勾配の逆方向へ少しずつ移動し、関数値を小さくする点を反復して探す。
これを勾配法といい、最小値を探す勾配降下法と最大値を探す勾配上昇法に分類される。
※ ニューラルネットワークの学習では、勾配降下法が多く用いられている。ニューラルネットワークでは、勾配法を用いて損失を小さくする重みやバイアスを探す。ただし、初期値、学習率、関数の形によっては局所最小値や鞍点に近づいたり、発散したりするため、大域的な最小値への到達は保証されない。
変数が \(x_{0}\) と \(x_{1}\) の2つである場合を例に勾配降下法を数式で表現すると
\[ x_{0}^{(r+1)} = x_{0}^{(r)} - μ\frac{\partial f}{\partial x_{0}}\left(x^{(r)}\right)\hspace{5mm}・・・(A) \]\[ x_{1}^{(r+1)} = x_{1}^{(r)} - μ\frac{\partial f}{\partial x_{1}}\left(x^{(r)}\right)\hspace{5mm}・・・(B) \]と表せ、\(μ\) を学習率といい、複数回の勾配を実施するにあたって前回の勾配が示す方向へどのくらい進めるかの基準値となる。
(勾配1回に対し、\((A)\)と\((B)\)をそれぞれ一回実施する。)※ \(\displaystyle \frac{∂ f}{∂x_{0}}\) と \(\displaystyle \frac{∂ f}{∂x_{1}}\) の偏微分については、Python - ニューラルネットワーク: 偏微分と勾配の実装サンプル > 勾配のPython実装サンプル を参考のこと。
- 学習率についての補足
下記の記事で触れている重みは、機械学習アルゴリズムにより自動で変化していくが、学習率は、あらかじめ人の手で設定するパラメータとなり、深層学習では勾配法によって、最適化できない(しない)パラメータになる。
(このようなパラメータをハイパーパラメータという。)
- 学習率についての補足
勾配降下法のPython実装サンプル
- 勾配降下法の関数定義
Python - ニューラルネットワーク: 偏微分と勾配の実装サンプル > 勾配の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)\)の実装サンプル。>>> # 上記対話モードの続き >>> def gradient_descent(f, init_x, lr=0.01, step_num=100): ... x = init_x.copy() # 呼び出し元の初期値を変更しない ... ... for i in range(step_num): ... grad = num_gradient(f, x) # 上記 勾配関数「num_gradient」をコール ... x -= lr * grad # (A)、(B) の実装箇所 ... ... return x ... >>>※
gradient_descentの引数- 第1引数「f」:最適化したい関数
- 第2引数「init_x」:初期値
- 第3引数「lr」:学習率
- 第4引数「step_num」:勾配を繰り返す回数
-
勾配降下法の実装サンプル実行例
- 学習率による収束の違い
最後に実行サンプル。
-
勾配法の実行例
関数 \(f(x_{0}, x_{1}) = x^2_{0} + x^2_{1}\) で \(x_{0} = -15.0\) 、\(x_{1} = 20\) とした時の勾配法gradient_descentの実行例。>>> # ↑↑↑ 上記対話モードの続き >>> 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]) >>>※
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
初期値 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([ 4.78115669e+12, -8.05260147e+12]) >>> >>> gradient_descent(func_ex, init_x=init_x, lr=1e-10, step_num=100) array([-14.9999997, 19.9999996]) >>>学習率を \(10\) とした場合、値の絶対値が大きくなって発散している。
これは、学習率が大きすぎることを意味しており、反対に学習率を \(1e-10\) と \(0\) に近づけすぎると \(x_{0} = -14.9999997\) 、\(x_{1} = 19.9999996\) となり初期値とほぼ同じ値が出力され、学習率が小さすぎることを意味している。
実際のニューラルネットワークでも、学習率を調整しながら勾配に基づく更新を繰り返す。ただし、一般の損失関数では大域的な最小値へ到達する保証はない。
-
まとめ
- 勾配降下法は、勾配の逆方向へ少しずつ進み、関数の値を小さくする方法。
- 学習率は一回の更新量を決め、大きすぎると発散し、小さすぎると収束に時間がかかる。
- 関数の形によっては全体の最小値ではなく局所的な最小値へ向かう場合がある。