からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

4.3:数値微分【ゼロつく1のノート(数学)】

はじめに

 「機械学習・深層学習」学習初手『ゼロから作るDeep Learning』民のための数学攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 NumPy関数を使って実装できてしまう計算について、数学的背景を1つずつ確認していきます。

 この記事は、4.3節「数値微分」の内容になります。数値微分と偏微分の説明をしたあとにそれぞれPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.3.1 微分

 関数$f(x)$の変数$x$について微分することを$\frac{d f(x)}{d x}$と表記し、これを微分係数と呼びます。そして微分係数$\frac{d f(x)}{d x}$は、次の式で定義されます。

$$ \frac{d f(x)}{d x} = \lim_{h \rightarrow 0} \frac{ f(x + h) - f(x) }{ h } \tag{4.4} $$

 マラソンの例を使ってこの式を説明します。

 スタートから10分が経過しました:$x = 10$。この時、ちょうど2㎞地点にいたとします:$f(x) = f(10) = 2$。10分間で2㎞進んだので、平均すると1分当たり0.2㎞の速さだったことが計算できます:$\frac{f(x)}{x} = \frac{2}{10} = 0.2$。
 そこから更に5分走り続けました:$h = 5$。スタートから15分間で進んだ距離は3.1㎞でした:$f(x + h) = f(15) = 3.1$。10分経過時からの5分間で1.1㎞進んだことになります:$f(x + h) - f(x) = f(15) - f(10) = 1.1$。つまり、この5分間の平均速度(分速)は0.22㎞となります:$\frac{f(x + h) - f(x)}{h} = \frac{f(15) - f(10)}{5} = 0.22$。

 では、ちょうど10分経過時点での速度が知りたいときどうすればいいでしょうか。10分時点を含む一定時間の平均速度ではなく、ある瞬間の速度を知りたいとします。分速でも秒速でもなく、あるほんの瞬間の変化量が知りたい訳です。つまり式でいうと、$h$を限りなく小さい値にしたい訳です(ただし0では割れない)。それを$\lim_{h \rightarrow 0}$で表現します。

 つまり式(4.4)は、$x$の小さな変化に対する$f(x)$の変化量を表します。またこの変化量を求めることを微分すると言い、微分係数$\frac{d f(x)}{d x}$で表現します。

 では数値微分を実装します。数値微分とは、微小な差分によって微分を求めることを言います。

 ここでは関数を$f(x) = x^2$とします。まずはこの関数を定義します。

# x^2を定義
def function_1(x):
    return x ** 2

 この関数を微分します。

 式(4.4)は、変数がある値$x$のときと$x$に限りなく小さい値を加えた$x + h$のときの関数の値の差$f(x + h) - f(x)$に注目するものでした。しかし限りなく小さい値$h$をプログラム上で再現できないため、誤差が生じてします。
 そこでその誤差の影響を少しでも緩和するために、少し式を加工します。$x$を中心として前後に$h$変化させた(つまり$x$から$h$を引いた値と足した値の)ときの関数の値の差$f(x + h) - f(x - h)$を使います。これは変数が$x - h$から$x + h$に変化したときの関数$f(x)$の変化量をみる訳です。このとき変数は$h$の2倍変化しているので、分母は$2h$になります。
 このような方法を中心差分と言います。

 この例では微小な値を$1e-4 = 0.0001$とします。$x = 3$のときの$f(x)$の変化量を求めます。

# 微小な値を設定
h = 1e-4 # 0.0001

# xを指定
x = 3

# 数値微分:中心差分
(function_1(x + h) - function_1(x - h)) / (2 * h)
6.000000000012662

 $\frac{d f(x)}{d x} = 6$が求まりました。

 このように数値微分には対象となる関数を用いるため、関数を引数として受け取れるように定義します。

# 数値微分を定義
def numerical_diff(f, x):
    
    # 微小な値
    h = 1e-4 # 0.0001
    
    # 数値微分
    return (f(x + h) - f(x - h)) / (2 * h)


 先ほどの関数を値に対して、作成した関数でも計算します。関数名を引数に指定します。

# 数値微分
numerical_diff(function_1, x)
6.000000000012662


 以上で数値微分を実装できました。続いて、グラフを使って微分についての理解を深めます。

4.3.2 数値微分の例

 関数$f(x) = x^2$の微分について考えます。まずはグラフを描画します。

# Matplotlibを読み込む
import matplotlib.pyplot as plt

# x軸の値を生成
x = np.arange(-20.0, 20.0, 0.1)

# x^2を計算
y = function_1(x)

# 作図
plt.plot(x, y)
plt.xlabel("x") # x軸ラベル
plt.ylabel("f(x)") # y軸ラベル
plt.show()

f:id:anemptyarchive:20200617000005p:plain
$f(x) = x^2$のグラフ


 関数を微分した値(微分係数)は、その関数のグラフの傾きとなります。

 注目する変数の値x_tangentを指定して、数値微分の計算をします。

# 接点を指定
x_tangent = 5

# 数値微分(傾き)
a = numerical_diff(function_1, x_tangent)
print(a)
9.999999999976694

 $x = 5$のとき$\frac{d f(x)}{d x} = 10$であることが分かりました。

 接線は直線なので$y = a x + b$で表現できます。この例では$x = 5$、$y = f(x) = 25$、傾き$a = \frac{d f(x)}{d x} = 10$なので、切片$b$は$b = y - a x$で求められますね。

# 切片を求める
b = function_1(x_tangent) - a * x_tangent
print(b)
-24.99999999988347

 傾きと切片が求まりました。

 では先ほどのグラフに接線も追加します。

# 作図
plt.plot(x, y) # 元の関数
plt.plot(x, a * x + b) # 接線
plt.xlabel("x") # x軸ラベル
plt.ylabel("f(x)") # y軸ラベル
plt.show()

f:id:anemptyarchive:20200617000126p:plain
$f(x) = x^2$のグラフと接線


 一応$h$の変化に対する直線の変化を確認しておきましょうか。以下は作図用のコードなので図だけ見てください。

・コード(クリックで展開)

# 数値微分を定義
def numerical_diff_h(f, x, h):
    return (f(x + h) - f(x)) / (h) # 数値微分

# 接線を定義
def tangent_line(f, x, h):
    
    # 傾き
    a = numerical_diff_h(f, x, h) # 数値微分
    
    # 切片
    b = f(x) - a * x
    
    return a, b
# 接点を指定
x_tangent = 5

# x軸の値を生成
x = np.arange(-20.0, 20.0, 0.1)

# x^2を計算
y_f = function_1(x)

# h=15のとき
a_15, b_15 = tangent_line(function_1, x_tangent, 15)
y_15 = a_15 * x + b_15

# h=10のとき
a_10, b_10 = tangent_line(function_1, x_tangent, 10)
y_10 = a_10 * x + b_10

# h=5のとき
a_5, b_5 = tangent_line(function_1, x_tangent, 5)
y_5 = a_5 * x + b_5

# h=1のとき
a_1, b_1 = tangent_line(function_1, x_tangent, 1)
y_1 = a_1 * x + b_1

# 数値微分
a_d, b_d = tangent_line(function_1, x_tangent, 1e-4)
y_d = a_d * x + b_d
# 作図
plt.plot(x, y_f) # 元の関数
plt.plot(x, y_d, label="h=1e-4") # 接線
plt.plot(x, y_1, linestyle="--", label="h=1")
plt.plot(x, y_5, linestyle="--", label="h=5")
plt.plot(x, y_10, linestyle="--", label="h=10")
plt.plot(x, y_15, linestyle="--", label="h=15")
plt.ylim(-50, 410) # y軸の表示範囲
plt.xlabel("x") # x軸ラベル
plt.ylabel("f(x)") # y軸ラベル
plt.legend() # 凡例
plt.show()

f:id:anemptyarchive:20200617000208p:plain
$h$の変化と傾きの関係

 $h$が小さくなるにしたがって、接線に近づいていることが確認できますね。

4.3.3 偏微分

 続いて、変数が2つの場合を考えます。2つの変数$x_0,\ x_1$の2乗和を扱います。

$$ f(x_0, x_1) = x_0^2 + x_1^2 \tag{4.6} $$

 偏微分とは、複数の変数の内の1つの変数にのみ注目し他の変数については定数として扱い、微分することを言います。例えばこの式の$x_0$についての偏微分は$\frac{\partial f}{\partial x_0}$と書きます。

 まずはこの関数のグラフを描きます。2変数の関数のグラフは3次元のグラフになります。3Dプロットの作図については【Pythonノート】を確認してください。

 2変数分の作図用の値を作成します。

# NumPyを読み込む
import numpy as np

# 作図用の値を生成
x0 = np.arange(-3.0, 3.5, 0.5)
x1 = np.arange(-3.0, 3.5, 0.5)

# 格子状の配列に変換
X0, X1 = np.meshgrid(x0, x1)
print(X0[0:7, 0:7])
print(X1[0:7, 0:7])
[[-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]
 [-3.  -2.5 -2.  -1.5 -1.  -0.5  0. ]]
[[-3.  -3.  -3.  -3.  -3.  -3.  -3. ]
 [-2.5 -2.5 -2.5 -2.5 -2.5 -2.5 -2.5]
 [-2.  -2.  -2.  -2.  -2.  -2.  -2. ]
 [-1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5]
 [-1.  -1.  -1.  -1.  -1.  -1.  -1. ]
 [-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5]
 [ 0.   0.   0.   0.   0.   0.   0. ]]

 np.meshgrid()で、格子状の配列に変換します。X0は行方向に値が変化し、X1は列方向に値が変化します。

 X0, X1の各要素の2乗和を計算します。

# 2乗和を計算
Z = X0 ** 2 + X1 ** 2
print(Z[0:7, 0:7])
[[18.   15.25 13.   11.25 10.    9.25  9.  ]
 [15.25 12.5  10.25  8.5   7.25  6.5   6.25]
 [13.   10.25  8.    6.25  5.    4.25  4.  ]
 [11.25  8.5   6.25  4.5   3.25  2.5   2.25]
 [10.    7.25  5.    3.25  2.    1.25  1.  ]
 [ 9.25  6.5   4.25  2.5   1.25  0.5   0.25]
 [ 9.    6.25  4.    2.25  1.    0.25  0.  ]]


 関数の値が求まったので、3Dグラフを作図します。

# 作図用のライブラリを読み込む
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 作図
fig = plt.figure() # 図を準備
ax = Axes3D(fig) # 3Dグラフの準備
ax.plot_wireframe(X0, X1, Z) # ワイヤーフレーム図
ax.set_xlabel("x0") # x軸ラベル
ax.set_ylabel("x1") # y軸ラベル
ax.set_zlabel("f(x)") # z軸ラベル
plt.show()

f:id:anemptyarchive:20200617000303p:plain
$f(x_0, x_1) = x_0^2 + x_1^2$のグラフ

 式(4.7)のグラフを描けました。4.3.2項の$f(x) = x^2$のグラフのy軸をを360°回転させたようなイメージです。

 $x_0 = 2,\ x_1 = 1$のときの$x_1$についての偏微分$\frac{\partial f}{\partial x_1}$を求めます。

 まずは、$x_0$を固定した式(4.6)の関数を作成します。

# 接点(x0, x1, f(x))のx0を指定
x0_tangent = 2.0

# 接点(x0, x1, f(x))のx1を指定
x1_tangent = 1.0

# x0を固定した2乗和関数を定義
def function_tmp1(x1):
    return x0_tangent ** 2.0 + x1 ** 2.0

 この関数は$x_0,\ x_1$の2乗和を返しますが、$x_0$は固定されているため、$x_1$の関数と言えます。

 つまり4.3.2項と同様に、数値微分を行うことで$\frac{\partial f}{\partial x_1}$を求められます。

# x_1についての偏微分(x_0を固定した数値微分)(x1軸方向の接点の傾きを求める)
a = numerical_diff(function_tmp1, x1_tangent)
print(a)
1.9999999999997797

 この値は、$x_1$軸方向の接線の傾きになります

 では切片も求めて、接線を引いてみましょう。

# 切片を求める
b = function_tmp1(x1_tangent) - a * x1_tangent
print(b)

# 作図用の値(格子状の配列)を生成
X0_tangent, X1_tangent = np.meshgrid(x0_tangent, x1)

# 接線のf(x)軸の値を計算
Z_tangent = a * X1_tangent + b
print(Z_tangent[0:7, :])
3.0000000000002203
[[-3.0000000e+00]
 [-2.0000000e+00]
 [-1.0000000e+00]
 [ 5.5067062e-13]
 [ 1.0000000e+00]
 [ 2.0000000e+00]
 [ 3.0000000e+00]]


 先ほどのグラフに接線を重ねます。

# 作図
fig = plt.figure() # 図を準備
ax = Axes3D(fig) # 3Dグラフの準備
ax.plot_wireframe(X0, X1, Z) # f(x_0, x_1)
ax.plot_wireframe(X0_tangent, X1_tangent, Z_tangent, color='orange') # 接線
ax.set_xlabel("x0") # x軸ラベル
ax.set_ylabel("x1") # y軸ラベル
ax.set_zlabel("f(x)") # z軸ラベル
ax.set_zlim(-5, 20) # z軸の範囲
plt.show()

f:id:anemptyarchive:20200617000421p:plain
$f(x_0, x_1) = x_0^2 + x_1^2$のグラフと接線


 このグラフを$x_0 = 2$で切断した断面図は次のようなグラフになります。

z = function_tmp1(x1)
z_tangent = a * x1 + b

plt.plot(x1, z, label="f(x0=2, x1)")
plt.plot(x1, z_tangent, label="tangent line")
plt.xlabel("x1") # x軸ラベル
plt.ylabel("f(x)") # y軸ラベル
plt.legend() # 凡例
plt.show()

f:id:anemptyarchive:20200617000439p:plain
$f(x_0, x_1) = x_0^2 + x_1^2$の断面図と接線

 $x_0 = 2,\ x_1 = 1$のときの$f(x_0, x_1)$の偏微分$\frac{\partial f}{\partial x_1}$は、接点$(x_0, x_1, f(x_0, x_1))$の$x_1$軸方向の傾きとなることが確認できました。

 この節では微分の説明を行いました。次節では偏微分を用いた勾配について説明します。  

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 この節を読んで初めて「解析的に微分する」という言葉の意味を知りました。

【次節の内容】

www.anarchive-beta.com