目的
この記事では、損失関数と数値微分について簡単な実装サンプルを記載する。
概念の説明と実装サンプル
損失関数と微分の関係
前の記事でも触れた交差エントロピー誤差などの損失関数が取る値は、小さいほど正解に近づいているが、もちろんそこで終わりではなく今の結果より正解に近いパラメータ候補(重み、バイアス)を決めてさらに正解に近づけなければ意味がない。
そこで基準になるのが重みパラメータの微分結果(勾配値)である。
具体的な説明は割愛するが、一つの重みパラメータの微分結果と損失関数の結果は、密接な関係にある。
微分結果が負であれば、正の方向に重みパラメータを変化させることで損失関数の結果も小さくなり、同様に微分結果が正であれば、負の方向に重みパラメータを変化させることで損失関数の結果も小さくなる。
この要領で重みパラメータを変化幅を少しずつ減らしながらこの作業を繰り返すことで、微分結果が \(0\) に収束し、その結果、損失関数の結果も \(0\) に収束することになる。
但し、この概念を理解する上で注意しなければならないのが Python - AI : 活性化関数の実装サンプルまとめ(ステップ、シグモイド、ReLU、恒等関数、ソフトマックス関数)> ステップ関数 のように、\(x = 0\) 以外の値で微分結果が \(0\)(勾配が\(0\))となるような損失関数では、そもそも正解に近づけようがないため、微分する意味がない。
つまり、損失関数は連続(曲線状の変化をしている)でなければ、微分する意味がない。
また、この連続性をもつこととは、損失関数の結果が実数であるということになるが、実数と実数の間に隙間はないので、いくらでも際限なく損失関数の極微量な区間の微分結果を探索することができ、ここに微分の性質が利用されている。
微分のおさらい
微分とは、簡単に言うと極微量な区間である点 \(f(x)\), \(f(x+\Delta x)\) の変化量(傾き、勾配)を表したもの。
そして、厳密に言うとこの極微量な区間 \(f(x)\), \(f(x+\Delta x)\) の差を限りなく \(0\) に近づけた時の変化量を表しており、一般的に下記のように定義される。
※ \(f’(x)\) と \(\frac{df(x)}{dx}\) は、\(f(x)\) を \(x\) で微分した結果(導関数)を表す記号。
また、この記事で解説しているニューラルネットワークでの微分は、この極微量な区間 \((f(x), f(x+\Delta x))\)
の差をプログラム言語の型の認識できる程度の微量なものであることを前提としている。
(プログラム言語の型の認識できるレベルで\(0\)に近づけるため、微量な誤差が発生するため。)
この微分を 数値微分と表現し、下記誤差がない高校数学の解析的な微分(真の微分)と区別する。
高校数学の微分「\(y = x^{n}\) ⇒ \(y’ = nx^{n-1}\)」は、誤差がない解析的な微分(真の微分)であり、上記の定義に従い、区間 \((f(x), f(x+\Delta x))\) の差を限りなく \(0\) に近づけた値を作業的に計算し求めている。
数値微分の関数定義(Python実装サンプル)
上記で触れたが数値微分なのでプログラム言語の型の認識できるレベルでなければならない。
もし、\(h\) を下記のように \(10^{-50}\) とし、NumPy.float32の型が認識できないほどの微量な値である場合、丸め誤差により、\(0.0\) となってしまう。
$ python
>>> def num_dif(f, x):
... h = 10e-50
... return (f(x+h) - f(x)) / h
...
>>>
従って、NumPy.float32型で丸目誤差がでないラインである \(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次関数 \(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('/static/tblog/img/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…\) という結果は、非常に小さい誤差で数値微分できていることになる。
参考文献
- 斎藤 康毅(2018)『ゼロから作るDeep Learning - Pythonで学ぶディープラーニングの理論と実装』株式会社オライリー・ジャパン