学生による学生のためのデータサイエンス勉強会

【ゼロから作るディープラーニング#7】数値微分と勾配法【p97-112】

historoid
historoid

こんにちは。清野(@historoid)です。

今回は数値微分と勾配法についてまとめました。

誤差逆伝播法のアルゴリズムは、損失関数に対する重みでの偏微分です。概念としては理解していても、実装はよくわからないという方も多いでしょう。

そこで今回は、ステップバイステップでじっくり解説していきます。

かなり詳細に数値微分と勾配法について解説していますので、じっくり読めばニューラルネットワークの相当深いところまで理解できるようになります。

JDLAのE資格受験を狙う方は必見ですよ〜。

数値微分と勾配法

今回は長くなります!!

1ページ目は数値微分について、2ページ目は勾配についてまとめました。

微分とは何か

historoid
historoid

先に言っておきますが、数学的な厳密性は求めないでください。

私にそんな素養はありません(・_・;)

微分とは、瞬間的な変化量を表したものです。

例えば、物体が落下するとき、1.0秒から1.1秒の間にどれだけ速さが増えたのかを示すことができます。

落下速度そのものではなく、落下速度の変化量を見ているわけですね。

数式では以下のように定義されています。

$$\frac{df(x)}{dx} = \lim_{h \to 0}\frac{f(x+h)-f(x)}{h}$$

数式を読み飛ばしたくなる気持ちはわかりますが、今回数式を読み飛ばすとわけがわからなくなるので、ゆっくりでもいいので頑張って読みましょう。

まず\(\frac{df(x)}{dx}\)は、「\(f(x)\)の\(x\)についての微分」という意味です。

そして\(h\)は、\(x\)の「微小な変化」を表します。これを\(\lim_{h \to 0}\)とすることで、「限りなく0に近づけ」ます

すると右辺は、「限りなく\(0\)である変化量に対する、\(f(x)\)の変化量」という意味になります。

数値微分の実装

定義式をそのまま実装してみましょう。

母関数として、\(f(x)=x^2\)を使ってみます。

# 適当な関数を作る
def my_function(x):
    return x**2


# 微分を関数として定義
# 微小変化量としてhを設定
def numerical_diff(function, val):
    h = 10e-50
    return (function(val+h) - function(val)) / h

numerical_diff(my_function, 1)
# 0.0

結果は\(0\)になってしまいました。

でも\(x^2\)の微分だから\(2x\)ですよね?

それで、\(x=1\)のときだから、答えは\(2\)のはずです。どうして\(0\)になってしまったのでしょう?

丸め誤差による計算不能

それはパソコンが抱える問題点にあります。あまりに小さい数字(ここでは\(10^{-50}\))は内部的に\(0\)として処理されてしまうのです。

これを丸め誤差(rounding error)といいます。数字を丸めることで、誤差が生じてしまうわけですね。

つまり、小さすぎる値は使えないということです。

というわけで、\(10^{-4}\)程度の小さな数で、もう一度試してみます。

def my_function(x):
    return x**2

# h = 10e-4に変更
def numerical_diff(function, val):
    h = 10e-4
    return (function(val+h) - function(val)) / h

numerical_diff(my_function, 1)
#2.0009999999996975

はい! うまくいきました。答えが\(2.0009999999996975\)になり、ほぼ\(2\)になりましたね。

前方差分と後方差分による調整

さてここで、\(f(x+h)-f(x)\)について少し考えたいと思います。

$$\begin{align}
たとえばx=1のとき\\
f(x) &= f(1)\\ &=1^2\\ &=1であり、\\
f(x+h) &= f(1+h)\\ &= (1+h)^2 \\ &= h^2+2h+1\\
となります。
\end{align}$$

ここで\(h \to 0\)とするので、\(f(1+h) \fallingdotseq 1\)となるわけです。

しかし実装では、座標でいうと\(\Bigl(1, f(1)\Bigr)\)と\(\Bigl(1+10^{-4},f(1+10^{-4})\Bigr)\)の傾きを求めているわけです。

def numerical_diff(function, val):
    h = 10e-4
    return (function(val+h) - function(val)) / h

つまり、正確に書くと\(h \to 0\)ではなくて、\(h \to +0\)になってるわけです。

これを前方差分といいます。というわけで、後方差分もしっかりと計算してみましょう

# 後方差分を計算
def numerical_diff(function, val):
    h = 10e-4
    return (function(val) - function(val-h)) / h

numerical_diff(my_function, 1)
# 1.998999999999973

今度は、また違う値になりましたよね(ほぼ2ですが)。

というわけで、前方差分と後方差分の平均値をとって正確な微分値としましょう。これにあわせて実装を修正します。

def numerical_diff(function, val):
    h = 10e-4
    return (function(val + h) - function(val - h)) / (2*h)

numerical_diff(my_function, 1)
# 1.999999999999835e-06

これでさらに\(2\)に近付きましたね。素晴らしい精度です。

実装した数値微分の検証

では数値微分の関数を動かしてみましょう。

せっかくなので教科書の例とはちょっと母関数を変えてみましょう。

$$f(x)=\frac{1}{1+\exp(-x)}$$

はい。シグモイド関数ですね。

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

これを描画しましょう。

import numpy as np
import matplotlib.pylab as plt
%matplotlib inline

x = np.arange(-7.0, 7.0, 0.1)
y = sigmoid(x)

plt.xlabel('x')
plt.ylabel('y')
plt.plot(x,y)
plt.show()

教科書では、\(x=5, 10\)のときの微分値を出力していますが、同じ\(x\)の値ですべてプロットして、導関数を描画してみます。

x = np.arange(-20.0, 20.0, 0.1)
y = sigmoid(x)
y2 = numerical_diff(sigmoid, x)

plt.xlabel('x')
plt.ylabel('y')
plt.plot(x, y)
plt.plot(x, y2)
plt.show()

これがシグモイド関数の導関数です。

あとあと登場するかもしれないので、形を覚えておくといいでしょう。

偏微分

さあいよいよ偏微分です。

まずは以下の関数について考えます。

$$f(x_0, x_1)=x_0^2+x_1^2$$

Pythonで実装すると以下のようになります。

def function_2(x):
    return x[0]**2 + x[1]**2

Matplolibでグラフを描いてみました。

\(f(x,y)=x^2+y^2\)のグラフ
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

def function_2(x, y):
    return x**2 + y**2
 
x = np.arange(-3.0, 3.0, 0.1)
y = np.arange(-3.0, 3.0, 0.1)
X, Y = np.meshgrid(x, y)
Z = function_2(X, Y)

fig = plt.figure(figsize=(10,10),dpi=72)
ax = Axes3D(fig)

ax.set_xlabel("x0")
ax.set_ylabel("x1")
ax.set_zlabel("f(x0, x1)")
ax.plot_wireframe(X, Y, Z)
plt.show()

どの変数に対する微分なのか

いま変数は、\(x_0\)と\(x_1\)があります。つまり2変数です。

微分するには「どちらの変数に対する微分か」を示す必要があります。

偏微分とは複数個ある変数のうちで1つだけを選んで微分することです。

$$\begin{align}
x_0に対する偏微分は\\
\frac{\partial y}{\partial x_0} &= 2x_0\\
x_1に対する偏微分は\\
\frac{\partial y}{\partial x_1} &= 2x_1\\
と書きます。
\end{align}$$

新しい記号が出てきましたね。\(\partial\)はギリシャ文字のdです。

だから\(df\)と\(\partial f\)は文字としてはあんまり変わらないってことですね。

実際に偏微分値を計算してみよう

まずは数値微分の実装をおさらいします。

def numerical_diff(function, val):
    h = 10e-4
    return (function(val + h) - function(val - h)) / 2*h

ここで\(x_0=3, x_1=4\)のときの\(x_0\)に対する偏微分を考えます。

偏微分ですから、\(x_1=4\)で固定されています。\(x_0\)だけを動かします。

# これがもとの方程式です。
def function_2(x):
    return x[0]**2 + x[1]**2

# x_1 = 4で固定すると
def partial_x0(x0):
    return x0**2 + 16  # 4の2乗ですね。

ここまではOKですよね。これを数値微分の関数に代入します。

numerical_diff(partial_x0, 3)  # x0=3のときの微分値
# 5.999999999998451e-06

だいたい6ですね。

同じように今度は\(x_1\)に対する偏微分をしてみます。

def partial_x1(x1):
    return 9 + x1**2  # 3の2乗で9

 
numerical_diff(partial_x1, 4)  # x1=4のときの微分値
# 8.000000000000896e-06

ほぼ8です。いい感じですね!!

ここから言えるのは、同じ点でも\(x_0\)方向での勾配と、\(x_1\)方向の勾配は違っているということです。

1 2

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です