概要
損失関数の値が入力の変化によってどう変わるかを、数値微分で確認する。
ニューラルネットワークの学習では、損失を小さくする方向を知るために微分の考え方が必要になる。
ここでは、微小な差分を使って傾きを近似し、Pythonで数値微分を実装する。
この記事の構成
- 損失関数と微分の関係
損失関数と微分の関係を整理し、要素同士がどう結び付くかを確認。 - 微分のおさらい
微分の定義と、以降の説明に必要な要点を確認。 - 数値微分の関数定義(Python実装サンプル)
数値微分の関数定義(Python実装サンプル)と、式や用語が表す意味を整理。 - 数値微分の例(Python実装サンプル)
数値微分の例(Python実装サンプル)をコードや具体例で確認。
概念の説明と実装サンプル
損失関数と微分の関係
- 損失を小さくする方向
前の記事でも触れた交差エントロピー誤差などの損失関数が取る値は、小さいほど正解に近づいているが、もちろんそこで終わりではなく今の結果より正解に近いパラメータ候補(重み、バイアス)を決めてさらに正解に近づけなければ意味がない。
そこで基準になるのが重みパラメータの微分結果(勾配値)である。
ある重みに対する損失の微分は、その重みをわずかに増やしたときに損失がどちらへどの程度変化するかを表す。
現在の点の近くでは、微分結果が負なら重みを正の方向へ、微分結果が正なら負の方向へ少し動かすと、損失が小さくなると判断できる。
この更新を繰り返すと、勾配が0に近い停留点へ到達する場合がある。ただし、停留点は局所最小値や鞍点の場合もあり、損失そのものが0になるとは限らない。
ステップ関数は損失関数ではなく活性化関数である。ステップ関数のように不連続で、ほとんどの点で微分が0となる関数を多層ネットワークの活性化関数に使うと、勾配を前の層へ伝えにくい。
通常の勾配法では、損失をパラメータで微分できることが必要になる。連続であるだけでは微分可能とは限らず、ReLUのように一部で微分できない関数では、実装上の微分値や劣勾配を定めて扱う。
微分のおさらい
- 微分と数値微分の考え方
微分とは、簡単に言うと極微量な区間である点 \(f(x)\), \(f(x+\Delta x)\) の変化量(傾き、勾配)を表したもの。
そして、厳密に言うとこの極微量な区間 \(f(x)\), \(f(x+\Delta x)\) の差を限りなく \(0\) に近づけた時の変化量を表しており、一般的に下記のように定義される。
\[ f'(x) = \frac{df(x)}{dx} \]\[ \frac{df(x)}{dx} = \lim_{\Delta x \to 0} \frac{ f(x+\Delta x) - f(x) }{\Delta x} \]※ \(f'(x)\) と \(\frac{df(x)}{dx}\) は、\(f(x)\) を \(x\) で微分した結果(導関数)を表す記号。
また、この記事で解説しているニューラルネットワークでの微分は、この極微量な区間 \((f(x), f(x+\Delta x))\) の差をプログラム言語の型の認識できる程度の微量なものであることを前提としている。
(プログラム言語の型の認識できるレベルで\(0\)に近づけるため、微量な誤差が発生するため。)このように、有限の差分幅を使って導関数を近似する方法を数値微分という。有限差分には、差分幅を \(0\) にできないことによる打切り誤差と、浮動小数点数の丸め誤差が含まれる。
一方、高校数学で扱う「\(y = x^{n}\) ⇒ \(y' = nx^{n-1}\)」のような解析的微分では、微分法則を使って導関数を記号的に求める。数値微分は、その導関数の値をプログラムで近似する方法として区別する。
数値微分の関数定義(Python実装サンプル)
- 中心差分による数値微分
上記で触れたが数値微分なのでプログラム言語の型の認識できるレベルでなければならない。
もし、\(h\) を下記のように \(10^{-50}\) とすると、浮動小数点数の丸めにより
x + hがxと同じ値として扱われたり、差の計算で有効桁を失ったりする。$ python >>> def num_dif(f, x): ... h = 1e-50 ... return (f(x+h) - f(x)) / h ... >>>ここでは、数値微分の例でよく使われる \(10^{-4} = 0.0001\) を採用する。ただし、適切な差分幅は関数とデータ型によって異なる。
$ python >>> def num_dif(f, x): ... h = 1e-4 ... return (f(x+h) - f(x)) / h ... >>>しかし、数学的に見ると \(10^{-4} = 0.0001\) でも十分大きな値なので、誤差を減らす工夫として、\(f(x+\Delta x) - f(x) \) の正の増加分だけでなく \(f(x) - f(x-\Delta x)\) となる負の増加分の変化も加味した中心差分という方法を取り、下記のように実装する。
$ python >>> def num_dif(f, x): ... h = 1e-4 ... return (f(x+h) - f(x-h)) / (2*h) ... >>>
数値微分の例(Python実装サンプル)
- 2次関数の傾きの確認
2次関数 \(y = 0.05x^{2} + 0.5x \) を例にした関数
func_exを定義し、イメージしやすいようグラフ描画もしておく。$ python >>> import numpy as np >>> import matplotlib.pyplot as plt >>> >>> def func_ex(x): ... return 0.05*x**2 + 0.5*x >>> >>> x = np.arange(0.0, 20.0, 0.1) # 区間を0~20まで、描画間隔を0.1刻みに設定 >>> y = func_ex(x) >>> >>> plt.title("y = 0.05x^2+0.5x \n# arange:0, 20, 0.1, xlabel:x, ylabel:f(x)") # グラフタイトルを設定 Text(0.5, 1.0, 'y = 0.05x^2+0.5x \n# arange:0, 20, 0.1, xlabel:x, ylabel:f(x)') >>> plt.xlabel("x") # x軸のラベルを設定 Text(0.5, 0, 'x') >>> plt.ylabel("f(x)") # y軸のラベルを設定 Text(0, 0.5, 'f(x)') >>> plt.plot(x,y) # グラフの描画 [<matplotlib.lines.Line2D object at 0x7f1f0e6f5be0>] >>> plt.savefig('pid24_1.png') >>>
そして上記 数値微分の関数定義(Python実装サンプル)で定義した数値微分の関数
num_difにこの2次関数の \(x = 5 \) の場合と、\(x = 10\) の場合を例に結果を出してみる。$ python >>> num_dif(func_ex, 5) 0.9999999999976694 >>> num_dif(func_ex, 10) 1.4999999999965041 >>>解析的には、\(f(x) = 0.05x^{2} + 0.5x \) ⇒ \(f'(x) = 0.1x + 0.5 \) より、\(x = 5\) の場合は \(1.0 \) 、\(x = 10\) の場合は \(1.5 \) となる。したがって、\(0.9999…\) と \(1.4999…\) という数値微分の結果は、それぞれの導関数の値を小さい誤差で近似している。
まとめ
- 数値微分は、数式から厳密に求める解析的微分とは異なり、微小な差分から関数の傾きを近似する。
- 差分hが大きすぎると近似が粗くなり、小さすぎると丸め誤差の影響を受けやすい。
- 前方差分より中心差分の方が誤差を抑えやすい。
- 損失関数の傾きは、学習で重みをどちらへ動かすかを決める手掛かりになる。