はじめに
「機械学習・深層学習」初学者のための『ゼロから作るDeep Learning』の攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
ニューラルネットワーク内部の計算について、数学的背景の解説や計算式の導出を行い、また実際の計算結果やグラフで確認していきます。
この記事は、4.3節「数値微分」の前半の内容です。微分の定義を説明をして、数値微分をPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
ニューラルネットワークでは、パラメータの学習を行う際に勾配を利用します。この節では、勾配を求めるのに必要になる微分について解説します。また、微小な差分によって微分を近似計算する数値微分を実装します。
利用するライブラリを読み込みます。
# 4.3節で利用するライブラリ import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation
解説にアニメーションを使います。アニメーションはanimation
モジュールのFuncAnimation()
を使って作成します。アニメーションの作成自体は目的ではないので、不要であれば省略してください。
4.3.1 微分
この項では、微分の定義を確認します。
・数式の確認
変数$x$についての関数$f(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分経過時点を含む一定時間の平均速度ではなく、ある瞬間の速度を知りたいとします。分速でも秒速でもなく、ある瞬間の$f(x)$の変化量が知りたいわけです。
式でいうと、$h$を限りなく小さい値にしたいわけです(ただし0では割れません)。この「0に限りなく近い値」を$\lim_{h \rightarrow 0}$で表しています。
つまり式(4.4)は、$x$の微小な変化に対する$f(x)$の変化の割合を表します。この変化量$\frac{d f(x)}{d x}$を求めることを微分すると言います。また、$\frac{d f(x)}{d x}$を導関数とも呼びます。
・グラフで確認
微分の定義式(4.4)の意図をグラフを使って確認します。
微小な値$h$と近似接線の関係をグラフで確認しましょう。処理については次項で解説するので、グラフを確認して次に進みましょう。
・コード(クリックで展開)
2乗の関数$f(x) = x^2$を例とします。$x^2$の計算を行う関数f()
と、前方差分による数値微分を行う関数numerical_diff_h()
を作成します。
# 2乗の関数を作成 def f(x): # 2乗を計算 return x**2 # 前方差分による数値微分を作成 def numerical_diff_h(f, x, h): # 数値微分を計算:(前方差分) return (f(x + h) - f(x)) / h
微小な値を関数の外から変更できるように、引数にh
を指定できるようにしています。
接点$(x, f(x))$の$x$の値を指定して、真の接線の傾きa
と切片b
を計算します。
# 接点のx軸の値を指定 x = 5 # 解析的に接線の傾きと切片を計算 a_true = 2 * x b_true = f(x) - a_true * x print(a_true) print(b_true)
10
-25
接点のy軸の値は、f(x)
で計算できます。
作図用のx軸の値を作成します。
# x軸の値を作成 x_vals = np.arange(-20.0, 20.1, 0.1)
作図に用いる$h$の値をリストにまとめてh_list
とします。h_list
の値ごとに近似接線を求めて描画します。
# 微小な値を指定 h_list = [15, 10, 5, 1, 0.5] # 対象の関数と近似接線を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.plot(x_vals, a_true * x_vals + b_true, label="f '(" + str(x) + ")") # 解析的に求めた微分:(真の接線) for h in h_list: # 近似接線の傾きと切片を計算 a = numerical_diff_h(f, x, h) b = f(x) - a * x plt.plot(x_vals, a * x_vals + b, linestyle='--', label='h=' + str(h)) # 近似接線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Numerical Differentiation', fontsize=20) # 全体のタイトル plt.title('(x, f(x))=(' + str(x) + ', ' + str(np.round(f(x), 1)) + ')' + ', a=' + str(np.round(a_true, 3)), loc='left') # 接点に関する値 plt.legend() # 凡例 plt.grid() # グリッド線 plt.ylim(-50, 400) # y軸の表示範囲 plt.show()
$h$の値と近似接線の関係をアニメーションで確認します。
# (降順の)微小な値を作成 h_list = np.arange(1e-4, 15, 0.1)[::-1] # 画像サイズを指定 fig = plt.figure(figsize=(8, 6)) # 作図処理を関数として定義 def update(i): # i回目のhを取得 h = h_list[i] # i回目のhのときの近似接線の傾きと切片を計算 a = numerical_diff_h(f, x, h) b = f(x) - a * x # 前フレームのグラフを初期化 plt.cla() # i回目のhのときの近似接線を作図 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.plot(x_vals, a_true*x_vals+b_true, label="f '(x)") # 解析的に求めた微分:(真の接線) plt.plot(x_vals, a*x_vals+b, linestyle='--') # 近似接線 plt.scatter(x+h, f(x+h)) # x+hの点 plt.scatter(x, f(x)) # 接点 plt.vlines(x=x+h, ymin=-50, ymax=f(x+h), color='black', linestyle=':', linewidth=1.5) # x+hの点からの垂線 plt.vlines(x=x, ymin=-50, ymax=f(x), color='black', linestyle=':', linewidth=1.5) # 接点からの垂線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Numerical Differentiation', fontsize=20) # 全体のタイトル plt.title('h=' + str(np.round(h, 2)) + ', a=' + str(np.round(a, 2)), loc='left') # 近似接線に関する値 plt.grid() # グリッド線 plt.legend() # 凡例 plt.ylim(-50, 400) # y軸の表示範囲 # gif画像を作成 diff_anime = FuncAnimation(fig, update, frames=len(h_list), interval=100) # gif画像を保存 diff_anime.save('ch4_3_numerical_diff.gif')
$h$が小さくなるほど近似接線(緑の破線)が真の接線(オレンジの実線)に近づくのを確認できます。以降は、数値微分によって求めた近似接線のことも接線と呼びます。
4.3.2 数値微分の実装
この項では、数値微分を実装します。数値微分とは、微小な差分によって微分を求める方法です。数式を展開して微分を求める場合は、「解析的」と表現します。解析的に求める微分については5章を参照してください。
・数式の確認
まずは、中心差分による微分の計算式を確認します。
微分の定義式(4.4)は、「ある値$x$のときの関数の値$f(x)$」と「限りなく0に近い値$h$を加えた$x + h$のときの関数の値$f(x + h)$」の差$f(x + h) - f(x)$に注目します。この方法を前方差分と呼びます。しかし、プログラム上では限りなく0に近い値を再現できないため、誤差が生じてしまいます。また、$h = 0$だと割り算を計算できません。
そこで、誤差の影響を緩和するために、次の式で計算します。
ある値$x$に対して「$h$を足した値$x + h$のときの関数の値$f(x + h)$」と「引いた値$x - h$のときの関数の値$f(x- h)$」の差$f(x + h) - f(x - h)$に注目します。つまり、$x$を中心として前後$h$だけ変化した($x - h$から$x + h$に変化した)ときの関数$f(x)$の変化量を求めます。このとき、変数は$h$の2倍変化しているので、分母は$2h$になります。この方法を中心差分と呼びます。
数式上では、前方差分の式と中心差分の式は同じことです。
・処理の確認
次に、中心差分による数値微分で行う処理を確認します。
この例では、$f(x) = x^2$の微分を考えます。$f(x)$の計算を行う関数を作成します。
# 2乗の関数を作成 def f(x): # 2乗を計算 return x**2
ちなみに、解析的に求めた微分は$\frac{d f(x)}{d x} = 2 x$です。
作成した関数$f(x)$をグラフで確認しましょう。
# x軸の値を作成 x_vals = np.arange(-20.0, 20.1, 0.1) # y軸の値を計算 y_vals = f(x_vals) # f(x)のグラフを作成 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, y_vals, label='$y = x^2$') # 折れ線グラフ plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.title('$f(x) = x^2$', fontsize=20) # タイトル plt.grid() # グリッド線 plt.show()
中心差分による数値微分を行う前に、前方差分と中心差分のそれぞれで生じる誤差の違いを確認しておきましょう。
$x = 3$における前方差分と中心差分による微分(接線の傾き)をグラフで確認します。誤差が分かりやすいように、$h$を微小ではない値にしておきます。ちなみに、$x = 3$における真の微分(解析的に求めた微分)は$\frac{d f(x)}{d x} = 2 * 3 = 6$です。
この図の作図処理は本筋ではないので解説を省略します。
・コード(クリックで展開)
# 接点のx軸の値を指定 x = 3 # 微小な値を指定 h = 1 # 解析的に接線の傾きと切片を計算 a = 2 * x b = f(x) - a * x # 作図 plt.figure(figsize=(8, 8)) # 図の設定 plt.plot(x_vals, y_vals, label='$f(x) = x^2$') # 対象の関数 plt.plot(x_vals, a*x_vals+b, label='真の接線') # 解析的求めた微分:(真の接線) plt.plot([x-h, x+h], [f(x-h), f(x+h)], color='blue', linewidth=3, label='中心差分') # 中心差分 plt.plot([x, x+h], [f(x), f(x+h)], color='red', linewidth=3, label='前方差分') # 前方差分 plt.scatter([x-h, x+h], [f(x-h), f(x+h)], color='blue') # 中心差分に用いる点 plt.scatter([x, x+h], [f(x), f(x+h)], color='red') # 前方差分に用いる点 plt.vlines(x=x, ymin=-50, ymax=f(x), color='black', linestyle=':', linewidth=1.5) # 接点からの垂線 plt.vlines(x=x-h, ymin=-50, ymax=f(x-h), color='black', linestyle=':', linewidth=1.5) # x-hの点からの垂線 plt.vlines(x=x+h, ymin=-50, ymax=f(x+h), color='black', linestyle=':', linewidth=1.5) # x+hの点からの垂線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.legend(prop={'family':'MS Gothic'}) # 凡例 plt.grid() # グリッド線 plt.xlim(1, 5) # x軸の表示範囲 plt.ylim(0, 20) # y軸の表示範囲 plt.show()
prop={'family':'MS Gothic'}
は、日本語を文字化けせずに表示するためのおまじないです。MacユーザーであればMS Gothic
ではなくAppleGothic
を設定するといいかも(?)
前方差分で求められる近似接線(赤線)の傾きよりも中心差分で求められる近似接線(青線)の傾きの方が、解析的に求めた接線(オレンジ線)の傾きに近いのが分かります。その分、中心誤差の方が接点との誤差が大きくなっていますが、ニューラルネットワークにおいて利用するのは接線ではなく接線の傾き(微分)です。
では、中心差分による数値微分の処理を確認していきます。
この例では、微小な値を$h = 0.0001$とします。
# 微小な値を設定 h = 1e-4
変数$x$の値を指定します。これは、接点$(x, f(x))$のx軸の値です。
# 変数の値を指定 x = 3
この例では、$x = 3$とします。
中心差分による数値微分を計算します。
# 中心差分による数値微分を計算 dx = (f(x + h) - f(x - h)) / (2 * h) print(dx)
6.000000000012662
$\frac{d f(x)}{d x} = 2 * 3 = 6$が求まりました。ただし、誤差を含んでいます。
以上が数値微分で行う処理です。
・実装
処理の確認ができたので、数値微分を関数として実装します。
# 数値微分の実装 def numerical_diff(f, x): # 微小な値を設定 h = 1e-4 # 0.0001 # 数値微分を計算:(中心差分) return (f(x + h) - f(x - h)) / (2 * h)
対象となる関数(数式)の計算を行う関数(プログラム)の関数名を第1引数に受け取ります。
実装した関数を試してみましょう。
最初に作成した$x^2$を計算する関数f()
の関数名f
を第1引数に指定します。
# 変数の値を指定 x = -5 # 数値微分を計算 dx = numerical_diff(f, x) print(dx)
-9.999999999976694
$x = -5$における微分は$\frac{d f(x)}{d x} = 2 * (- 5) = -10$です。
# 変数の値を指定 x = 3 # 数値微分を計算 dx = numerical_diff(f, x) print(dx)
6.000000000012662
関数$f(x)$の微分$\frac{d f(x)}{d x}$は、関数$f(x)$上の点$(x, f(x))$における接線の傾き$a$でもあるのでした。ついでに、接線の切片$b$も求めてみましょう。
接線は直線なので$y = a x + b$で表現できます。この式を接線$b$に関して整理すると、$b = y - a x$で求められることが分かります。ここに$x = 3,\ y = f(x) = 9$と$a = \frac{d f(x)}{d x} = 6$を代入すると、$b = -9$が得られます。
# xにおける接線の切片を計算 b = f(x) - dx * x print(b)
-9.000000000037986
切片も求まりました。これで接線を描画できます。ちなみに、ニューラルネットワークの計算に接線や切片は登場しません。
・グラフで確認
最後に、関数と微分(接線の傾き)の関係をグラフで確認します。
接点のx軸の値を指定して、関数$f(x)$と接点$(x, f(x))$における接線を作図します。
# 接点のx軸の値を指定 x = 3 # 接線の傾きと切片を計算 dx = numerical_diff(f, x) b = f(x) - dx * x # x軸の値を作成 x_vals = np.arange(-10.0, 10.1, 0.1) # 対象の関数と接線を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.scatter(x, f(x)) # 接点 plt.plot(x_vals, dx * x_vals + b, label="f '(" + str(x) + ")") # 接線 plt.suptitle('Numerical Differentiation', fontsize=20) # 全体のタイトル plt.title('(x, f(x))=(' + str(np.round(x, 1)) + ', ' + str(np.round(f(x), 2)) + ')' + ', dx=' + str(np.round(dx, 3)), loc='left') # 接点に関する値 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.grid() # グリッド線 plt.legend() # 凡例 plt.ylim(-10, 100) # y軸の表示範囲 plt.show()
左の図は$x = 3$のとき、右の図は$x = -2.5$のときの接線です。
接点と接線の傾き(微分)の関係をアニメーションで確認します。
・コード(クリックで展開)
# x軸の値を作成 x_vals = np.arange(-10.0, 10.1, 0.1) # 画像サイズを指定 fig = plt.figure(figsize=(8, 6)) # 作図処理を関数として定義 def update(i): # i回目のxの値を取得 x = x_vals[i] # i回目のxにおける接線の傾きと切片を計算 dx = numerical_diff(f, x) b = f(x) - dx * x # 前フレームのグラフを初期化 plt.cla() # i回目のxにおける接線を作図 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.scatter(x, f(x)) # 接点 plt.plot(x_vals, dx * x_vals + b, label="f '(x)") # 接線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Numerical Differentiation', fontsize=20) # 全体のタイトル plt.title('(x, f(x))=(' + str(np.round(x, 1)) + ', ' + str(np.round(f(x), 2)) + ')' + ', dx=' + str(np.round(dx, 3)), loc='left') # 接点に関する値 plt.grid() # グリッド線 plt.legend() # 凡例 plt.ylim(-10, 100) # y軸の表示範囲 # gif画像を作成 diff_anime = FuncAnimation(fig, update, frames=len(x_vals), interval=100) # gif画像を保存 diff_anime.save('ch4_3_f_diff.gif')
点$(x, f(x))$によって傾きが変化しているのが分かります。また、関数$f(x)$がマイナス方向に変化する局面では傾きも負の値であり、プラス方向に変化するときは傾きも正の値なのが分かります。
関数$f(x)$をシグモイド関数として、同じことをしてみましょう。
・コード(クリックで展開)
# シグモイド関数を定義 def f(x): # シグモイド関数の計算:式(3.6) return 1 / (1 + np.exp(-x))
3.5.2項で実装したsigmoid()
と同じ処理です。変数名をf
にしておくと、作図コードを(概ね)そのまま利用できます。
y軸の表示範囲などを変更する必要があります。
# 接点のx軸の値を指定 x = -2.5 # 接線の傾きと切片を計算 dx = numerical_diff(f, x) b = f(x) - dx * x # x軸の値を作成 x_vals = np.arange(-10.0, 10.1, 0.1) # 対象の関数と接線を作図 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.scatter(x, f(x)) # 接点 plt.plot(x_vals, dx * x_vals + b, label="f '(" + str(x) + ")") # 接線 plt.suptitle('Sigmoid Function', fontsize=20) # 全体のタイトル plt.title('(x, f(x))=(' + str(np.round(x, 1)) + ', ' + str(np.round(f(x), 2)) + ')' + ', dx=' + str(np.round(dx, 3)), loc='left') # 接点に関する値 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.grid() # グリッド線 plt.legend() # 凡例 plt.ylim(-0.1, 1.1) # y軸の表示範囲 plt.show()
# x軸の値を作成 x_vals = np.arange(-10.0, 10.1, 0.1) # 画像サイズを指定 fig = plt.figure(figsize=(8, 6)) # 作図処理を関数として定義 def update(i): # i回目のxの値を取得 x = x_vals[i] # i回目のxにおける接線の傾きと切片を計算 dx = numerical_diff(f, x) b = f(x) - dx * x # 前フレームのグラフを初期化 plt.cla() # i回目xにおける接線を作図 plt.plot(x_vals, f(x_vals), label='f(x)') # 対象の関数 plt.scatter(x, f(x)) # 接点 plt.plot(x_vals, dx * x_vals + b, label="f '(x)") # 接線 plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.suptitle('Sigmoid Function', fontsize=20) # 全体のタイトル plt.title('(x, f(x))=(' + str(np.round(x, 1)) + ', ' + str(np.round(f(x), 2)) + ')' + ', dx=' + str(np.round(dx, 3)), loc='left') # 接点に関する値 plt.grid() # グリッド線 plt.legend() # 凡例 plt.ylim(-0.1, 1.1) # y軸の表示範囲 # gif画像を作成 diff_anime = FuncAnimation(fig, update, frames=len(x_vals), interval=100) # gif画像を保存 diff_anime.save('ch4_3_sigmoid_diff.gif')
関数$f(x)$の変化が大きいところでは、傾きも大きくなるのが分かります。
以上で、数値微分を実装できました。この項では、1変数の関数について扱いました。次項では、多変数の関数の微分を考えます。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
おわりに
この節を読んで初めて「解析的に微分する」という言葉の意味を知りました。
- 2021.08.15:加筆修正しました。その際に記事を分割しました。
数学ネタを書くのはやっぱり厳しい。
【次節の内容】