SIGMA-SE Tech Blog

SIGMA-SE Tech Blog

Python - AI : 勾配降下法の実装サンプル

目的

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

概念の説明と実装サンプル

勾配法とは

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

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

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

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

変数が \(x_{0}\) と \(x_{1}\) の2つである場合を例に勾配降下法を数式で表現すると

\[ x_{0} = x_{0} - μ\frac{∂\ (x_{0}, x_{1})}{∂x_{0}}\hspace{5mm}・・・(A) \]
\[ x_{1} = x_{1} - μ\frac{∂\ (x_{0}, x_{1})}{∂x_{1}}\hspace{5mm}・・・(B) \]

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

※ \(\displaystyle \frac{∂\ (x_{0}, x_{1})}{∂x_{0}}\) と \(\displaystyle \frac{∂\ (x_{0}, x_{1})}{∂x_{1}}\) の偏微分については、Python - AI : 偏微分と勾配の実装サンプル > 勾配のPython実装サンプル を参考のこと。

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

Python - AI : 偏微分と勾配の実装サンプル > 勾配の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
        ...
        ...     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([ 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 SIGMA-SE All Rights Reserved.
s-hama@sigma-se.jp