はじめに
「機械学習・深層学習」学習初手『ゼロから作るDeep Learning』民のための数学攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
NumPy関数を使って実装できてしまう計算について、数学的背景を1つずつ確認していきます。
この記事は、4.3節「数値微分」の内容になります。数値微分と偏微分の説明をしたあとにそれぞれPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
4.3.1 微分
関数$f(x)$の変数$x$について微分することを$\frac{d f(x)}{d x}$と表記し、これを微分係数と呼びます。そして微分係数$\frac{d f(x)}{d x}$は、次の式で定義されます。
マラソンの例を使ってこの式を説明します。
スタートから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()
関数を微分した値(微分係数)は、その関数のグラフの傾きとなります。
注目する変数の値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()
一応$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()
$h$が小さくなるにしたがって、接線に近づいていることが確認できますね。
4.3.3 偏微分
続いて、変数が2つの場合を考えます。2つの変数$x_0,\ x_1$の2乗和を扱います。
偏微分とは、複数の変数の内の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()
式(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()
このグラフを$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()
$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年.
おわりに
この節を読んで初めて「解析的に微分する」という言葉の意味を知りました。
【次節の内容】