SIGMA-SE Math & Tech Library

SIGMA-SE Math & Tech Library


数学と情報技術をテーマに、書籍や教材だけではつかみにくい考え方を具体例とともに簡潔にわかりやすく伝える解説サイトです。
技術の歴史や背景、関連知識の整理、学習のための覚書や要約記事も掲載しています。

Python - ニューラルネットワーク:12/14 勾配降下法で最小値を探す

概要

勾配降下法を使って、関数の値が小さくなる方向へ少しずつ移動する考え方を整理する。

ニューラルネットワークの学習では、損失関数を小さくするために、勾配を手がかりとしてパラメータを更新する。

ここでは、単純な関数を例に、勾配、学習率、更新式を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\) となり初期値とほぼ同じ値が出力され、学習率が小さすぎることを意味している。

      実際のニューラルネットワークでも、学習率を調整しながら勾配に基づく更新を繰り返す。ただし、一般の損失関数では大域的な最小値へ到達する保証はない。

まとめ

  • 勾配降下法は、勾配の逆方向へ少しずつ進み、関数の値を小さくする方法。
  • 学習率は一回の更新量を決め、大きすぎると発散し、小さすぎると収束に時間がかかる。
  • 関数の形によっては全体の最小値ではなく局所的な最小値へ向かう場合がある。

参考文献

この記事を共有
Xで共有 Facebookで共有 LINEで共有



Copyright SIGMA-SE All Rights Reserved.
s-hama@sigma-se.jp