からっぽのしょこ

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

4.4:勾配【ゼロつく1のノート(数学)】

はじめに

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

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

 この記事は、4.4節「勾配」の内容になります。勾配の計算と勾配降下法の説明をしたあとにそれぞれPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.4 勾配

 4.3.3項では式(4.6)のような複数の変数からなる関数を、1つの変数について微分する偏微分について学びました。

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

 偏微分した値を微分係数と呼び$\frac{\partial f}{\partial x_0}$のように書くのでした。また微分係数$\frac{\partial f}{\partial x_0}$は曲面$f(x_0, x_1)$の$x_0$軸方向の傾きになるでした。

 全ての変数の偏微分をベクトル(1次元配列)や行列(2次元配列)にまとめたものを勾配と言います。(一般的な単語としても山(面)の傾斜を勾配と呼びますよね。)

$$ \mathbf{x} = \begin{pmatrix} x_0 & x_1 \end{pmatrix} ,\ \frac{\partial f}{\partial \mathbf{x}} = \begin{pmatrix} \frac{\partial f}{\partial x_0} & \frac{\partial f}{\partial x_1} \end{pmatrix} $$


 勾配の計算を実装して、可視化してみましょう。

# 4.4節で利用するライブラリを読み込む
import numpy as np
import matplotlib.pyplot as plt


# 式(4.6)を定義
def function_2(x):
    return np.sum(x ** 2)

A = np.array([1, 2])
function_2(A)
5


 まずは勾配の受け皿を作成します。勾配の形状は変数$x$と同じであるため、np.zeros_like()で、引数に指定した変数xと同じ形状の値が全て0の配列を作成します。それをgradとします。

# 仮の変数を指定
x = np.array([3.0, 4.0])
print(x)

# 勾配の受け皿を作成
grad = np.zeros_like(x)
print(grad)
[3. 4.]
[0. 0.]


 数値微分とは変数の微小な変化に対する関数の変化を見るものでした。今から$x_0$について微分します。

 そこで微小な値hを用意します。また、微分する変数$x_0$の値を取り出しておきます。これは一時的に利用する値なので、変数名をtmp_valとしておきます

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

# x0の値を取り出す
tmp_val = x[0]
print(tmp_val)
3.0


 中心差分を求めるため、微小な値hを加えたときと引いたときの関数の値を保存します。$x_0$の微小な変化に対する$f(x_0, x_1)$の変化をみるため、hを加えるのはx[0]のみです。

# x0に微小な値を加える
x[0] = tmp_val + h
print(x)

# f(x0+h, x1)を計算
fxh1 = function_2(x)
print(fxh1)
[3.0001 4.    ]
25.00060001


# x0から微小な値を引く
x[0] = tmp_val - h
print(x)

# f(x0-h, x1)を計算
fxh2 = function_2(x)
print(fxh2)
[2.9999 4.    ]
24.99940001


 中心差分で数値微分を行います。また最後に、取り出していた値を元に戻します。これで微小な値の加減も元の値に戻せます。

# 数値微分(中心差分)
grad[0] = (fxh1 - fxh2) / (2 * h)
print(grad)

# 元の値を戻す
x[0] = tmp_val
print(x)
[6. 0.]
[3. 4.]

 これで$\frac{\partial f}{\partial x_0} = 6$が求まりました!

 次は$x_1$にも同じ処理を・・・ということでforを使って、xの各要素について(xの要素数回繰り返し)この処理を行うようにしましょう。

# 勾配を計算
for idx in range(x.size):
    
    # 微分する変数の値を取り出す
    tmp_val = x[idx]
    
    # f(x+h)を計算
    x[idx] = tmp_val + h
    fxh1 = function_2(x)
    
    # f(x-h)を計算
    x[idx] = tmp_val - h
    fxh2 = function_2(x)
    
    # 数値微分(中心差分)
    grad[idx] = (fxh1 - fxh2) / (2 * h)
    
    # 値を元に戻す
    x[idx] = tmp_val
    
print(grad)
[6. 8.]

 これで勾配

$$ \frac{\partial f}{\partial \mathbf{x}} = \begin{pmatrix} \frac{\partial f}{\partial x_0} & \frac{\partial f}{\partial x_1} \end{pmatrix} = \begin{pmatrix} 6 & 8 \end{pmatrix} $$

が求まりました。

 以上が勾配を求める処理の内容になります。

 では実装しましょう。数値微分のときと同様に、対象となる関数を引数として指定できるようにします。

# 勾配を定義
def numerical_gradient(f, x):
    
    # 微小な値
    h = 1e-4 # 0.0001
    
    # 勾配を初期化(受け皿を作成)
    grad = np.zeros_like(x)
    
    # 勾配を計算
    for idx in range(x.size):
        
        # 微分する変数の値を取り出す
        tmp_val = x[idx]
        
        # f(x+h)を計算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h)を計算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        # 数値微分(中心差分)
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        
        # 値を元に戻す
        x[idx] = tmp_val
    
    return grad

 numerical_gradient()の使用時に、勾配を求めたい関数を括弧()なしで第1引数に指定することで、その関数による計算が行われます。その計算結果がfxh1fxh2に格納されることで、数値微分を行います。

 同じ値を使って作成した関数でも勾配を計算してみましょう。

# 勾配を計算
grad = numerical_gradient(function_2, x)
print(grad)
[6. 8.]

 同じ結果になりましたね。

 続いてこの関数を使って、勾配を可視化します。2変数のプロットについては【Pythonノート】をご確認ください。

 まずはプロットする値を生成します。

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

# 格子状に変換
X0, X1 = np.meshgrid(x0, x1)
print(X0[0:5, 0:5])
print(X1[0:5, 0:5])
[[-3.   -2.75 -2.5  -2.25 -2.  ]
 [-3.   -2.75 -2.5  -2.25 -2.  ]
 [-3.   -2.75 -2.5  -2.25 -2.  ]
 [-3.   -2.75 -2.5  -2.25 -2.  ]
 [-3.   -2.75 -2.5  -2.25 -2.  ]]
[[-3.   -3.   -3.   -3.   -3.  ]
 [-2.75 -2.75 -2.75 -2.75 -2.75]
 [-2.5  -2.5  -2.5  -2.5  -2.5 ]
 [-2.25 -2.25 -2.25 -2.25 -2.25]
 [-2.   -2.   -2.   -2.   -2.  ]]

 満遍なくプロットするためのペアの値を生成できました。

 作成した関数や作図用の関数で処理するには、このペアの値を2列とする2次元配列に変換する必要があります。

 X0, X1.flatten()メソッドで、要素を横一列に並べ替えます(1次元配列に変換します)。それをnp.array()で、2行の配列にします。最後に転置メソッド.Tで、2列の配列に変換します。

# 作図用に変形
X = np.array([X0.flatten(), X1.flatten()]).T
print(X0.flatten()[0:5])
print(X1.flatten()[0:5])
print(X[0:5, :])
[-3.   -2.75 -2.5  -2.25 -2.  ]
[-3. -3. -3. -3. -3.]
[[-3.   -3.  ]
 [-2.75 -3.  ]
 [-2.5  -3.  ]
 [-2.25 -3.  ]
 [-2.   -3.  ]]


 処理できる形状に整形できたので、1行ずつ勾配を計算していきます。

# 勾配の受け皿を作成
grad = np.zeros_like(X)

# 勾配を計算
for i in range(X.shape[0]):
    
    # 1行ずつ勾配を計算
    grad[i, :] = numerical_gradient(function_2, X[i, :])

print(grad[0:5])
[[-6.  -6. ]
 [-5.5 -6. ]
 [-5.  -6. ]
 [-4.5 -6. ]
 [-4.  -6. ]]


 全ての勾配が得られたので、可視化します。

# 作図
plt.figure() # 図を準備
plt.quiver(X[:, 0], X[:, 1], -grad[:, 0], -grad[:, 1]) # 矢印プロット
plt.xlabel("x0") # x軸ラベル
plt.ylabel("x1") # y軸ラベル
plt.grid() # グリッド線
plt.title("Gradient", fontsize=20) # タイトル
plt.show() # 表示

f:id:anemptyarchive:20200617002040p:plain
勾配

 これは式(4.6)の3Dグラフを真上から見たときの勾配のイメージです。各点の勾配の値の大きさに応じて矢印が長くなり、傾きの度合いを表します。
 勾配(矢印)が示す方向が、各場所において関数$f(\mathbf{x})$の値を最も減らす方向になります。ただし窪みが1つとは限らないため、矢印の先が必ず関数の最小値になるという訳はありません。

 これで勾配の説明は終わりです。続いてこの勾配に従って最小点を探索する勾配法について説明します。

4.4.1 勾配法

 まずは簡単な例として変数が1つの場合で考えてみましょう。

$$ f(x) = x^2 $$


 関数を作成して、グラフにします。

# 関数を定義
def function_1(x):
    return x ** 2
# x軸の値を生成
x = np.arange(-20.0, 20.0, 0.1)

# f(x)を計算
y = function_1(x)

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

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

 何度も見たグラフですね。この関数(グラフ)$f(x)$が最小となる変数$x$を見つけたい訳です。勿論グラフにできてしまえば、$x = 0$のとき$f(x) = f(0) = 0$が最小値だとすぐに分かりますよね。しかしそれは、たまたま表示した範囲に最小値が含まれていたり、単純な関数なのでグラフにするのが簡単だったりといった特別な場合に限ります。

 そこで、次のような流れで機械的に最小値を探します。

 $x = 10$の微分係数(傾き)を見てみましょう。数値微分numerical_diff()は4.3.1項で作成したものです。

# 接点を指定
x_tangent = 10

# 傾きを求める(数値微分)
a = numerical_diff(function_1, x_tangent)
print(a)

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

 $\frac{d f}{d x} = 20 = a$が求まりました。

 グラフでも確認しましょう。

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

f:id:anemptyarchive:20200617002233p:plain
$x = 10$のとき

 微分係数(傾き)が正の値のときは、$x$を今よりマイナス方向に動かすことで$f(x)$が小さくなることが分かります。

 同様に$x = -5$のときも見てみましょう。

# 接点を指定
x_tangent = -5

# 傾きを求める(数値微分)
a = numerical_diff(function_1, x_tangent)
print(a)

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

 微分係数(傾き)は-10でした。

 グラフでも確認しましょう。

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

f:id:anemptyarchive:20200617002315p:plain
$x = -5$のとき

 傾き(微分係数)が負の値のときは、$x$を今よりプラス方向に動かすと$f(x)$が小さくなることが分かります。

 また2つの結果から、極小値から遠いほど微分係数が大きく、近いほど微分係数が小さくなる傾向があることも分かります。

 このことから、変数$x$から微分係数$\frac{d f}{d x}$を引いた値は、元の$x$よりも$f(x)$を小さくする(可能性のある)値になります。

$$ x^{(\mathrm{new})} = x - \frac{d f}{d x} $$

 この式に従うと、微分係数が正の値のときは$x$をマイナス方向に変化させ、負の値のときはプラス方向に変化させることができます。また、微分係数が大きいほど大きく変化し、小さいほど小さく変化します。

 しかし$x$を動かしすぎると、$f(x)$を最小とする値を飛び越える可能性があります。この例だと、$x = -5$のとき微分係数-10を引いた値は5なので、$f(-5) = f(5) = 25$となり変化しません。更に$x = 5$のときの微分係数は10なので・・・

 そこで、変化量を調整する項を付けることにします。これを学習率と言い、$\eta$を使って表します。(学習率を0から1の値とすることで変化量を割り引きます。)

$$ x^{(\mathrm{new})} = x - \eta \frac{d f}{d x} $$

 これにより、微分係数の$\eta$倍$x$が変化します。この計算結果を更新値$x^{(\mathrm{new})}$とします。

 ただし、この方法によって必ず最小値に辿り着ける訳ではありません。次のような窪みが複数ある関数だと、最小値ではな極小値で微分係数が0になり、それ以上更新できなくなってしまうことがあります。

# 関数を定義
def function_2(x):
    return (x + 4) * (x + 1) * (x - 1) * (x - 3)

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

# 接点を指定
x_tangent = 2.222

# 傾きを求める(数値微分)
a = numerical_diff(function_2, x_tangent)
print(a)

# 切片を求める
b = function_2(x_tangent) - a * x_tangent
-0.07756770910916089
# 作図
plt.plot(x, function_2(x)) # 元の関数
plt.plot(x, a * x + b) # 接線
plt.show()

f:id:anemptyarchive:20200617002343p:plain
極小値が複数ある場合


 ここまでは変数が1つの場合について考えました。変数が2つに増えても(複数になっても)考え方は同じです。

 変数$x_0,\ x_1$について、それぞれ学習率$\eta$を掛けた偏微分との差を更新値$x_0^{(\mathrm{new})},\ x_1^{(\mathrm{new})}$とします。

$$ \begin{align*} x_0^{(\mathrm{new})} &= x_0 - \eta \frac{\partial f}{f x_0} \\ x_1^{(\mathrm{new})} &= x_1 - \eta \frac{\partial f}{f x_1} \tag{4.7} \end{align*} $$

 この式(4.7)をまとめると、次のように書けます。

$$ \mathbf{x} = \begin{pmatrix} x_0 & x_1 \end{pmatrix} ,\ \frac{\partial f}{\partial \mathbf{x}} = \begin{pmatrix} \frac{\partial f}{\partial x_0} & \frac{\partial f}{\partial x_1} \end{pmatrix} $$

 勾配$\frac{\partial f}{\partial \mathbf{x}}$を用いて変数$\mathbf{x}$を更新していき、関数$f(\mathbf{x})$が最小となる変数を求める手法を勾配法と言います。更新式は次の式になります。

$$ \mathbf{x}^{(\mathrm{new})} = \mathbf{x} - \eta \frac{\partial f}{\partial \mathbf{x}} \tag{4.7'} $$

 式(4.7)をまとめて表現しているだけで、式の意味は同じです。また、勾配法によって最小値を探す場合は勾配降下法と呼びます。

 $f(\mathbf{x}) = x_0^2 + x_1^2$を対象の関数として、勾配降下法を実装していきます。

# 関数を定義
def function_2(x):
    
    # 1次元配列の場合は2次元配列に変換
    if x.ndim == 1:
        x = x.reshape(1, x.size)
    
    # 2乗和を計算
    return np.sum(x ** 2, axis=1)

 複数行の配列を処理できるように定義しています。そのため1次元配列が渡された場合は2次元配列に変換してから和をとります。

 またnp.sum()の引数にaxis=1を指定することで、配列の各行の和を計算します。(axis=0列ごに和をとります。)

a = np.array([1, 2])
print(function_2(a))
A = np.array([[1, 2], [3, 4]])
print(function_2(A))
[5]
[ 5 25]


 変数$x$の初期値をinit_xとします。また学習率をlr、試行回数をstep_numとして指定します。

# xの初期値を指定
init_x = np.array([-3.0, 4.0])

# 学習率を指定
lr = 0.1

# 試行回数を指定
step_num = 100


 $x$の初期を使って勾配を計算します。

# 勾配を計算
grad = numerical_gradient(function_2, init_x)
print(grad)
[-6.  8.]


 勾配と学習率の積を初期値から引いた値を更新値xとします。

# xを更新:式(4.7)
x = init_x - lr * grad
print(init_x)
print(x)
[-3.  4.]
[-2.4  3.2]

 以上が1回目の更新の処理になります。

 これをforを使って指定した回数繰り返し行います。

 処理の内容を確認できたので、勾配降下法を実装します。

# 勾配降下法を定義
def gradient_descent(f, init_x, lr=0.01, step_num=10):
    
    # xの初期値
    x = init_x
    
    for i in range(step_num):
        
        # 勾配降下法
        grad = numerical_gradient(f, x) # 勾配を計算
        x -= lr * grad # xを更新:式(4.7)
    
    return x

 引数として渡された初期値init_xxに置き換えます。それにより式(4.7)の計算をx -= lr * gradと書くことができます。(このような方法を代入演算と言います。)

 処理の確認では1回しか更新しませんでしたから、100回行ってみましょう。

# xの初期値を指定
init_x = np.array([-3.0, 4.0])

# 勾配降下法
x = gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
print(x)
print(function_2(x))
[-6.11110793e-10  8.14814391e-10]
[1.03737889e-18]

 どちらも0に近い値になっていますね。

 これをグラフでも確認してみましょう。プロットするために、勾配降下法の定義に少し手を加えて更新する度に値を保存するようにします。

# 勾配降下法を定義
def gradient_descent_histry(f, init_x, lr=0.01, step_num=10):
    
    # xの初期値
    x = init_x
    
    # 更新値の推移を初期化
    x_histry = []
    x_histry.append(x.copy()) # 初期値を保存
    
    for i in range(step_num):
        
        # 勾配降下法
        grad = numerical_gradient(f, x) # 勾配を計算
        x -= lr * grad # xを更新:式(4.7)
        
        # 更新値を保存
        x_histry.append(x.copy())
        
    return x, np.array(x_histry)

 x.copy()xの値を取り出して、x_histry.append()x_histryに追加します。これで、更新値を保存できるようになります。

# xの初期値を指定
init_x = np.array([-3.0, 4.0])

# 勾配降下法
x, x_histry = gradient_descent_histry(function_2, init_x=init_x, lr=0.1, step_num=100)
print(x_histry[0:5])
print(x)
[[-3.      4.    ]
 [-2.4     3.2   ]
 [-1.92    2.56  ]
 [-1.536   2.048 ]
 [-1.2288  1.6384]]
[-6.11110793e-10  8.14814391e-10]

 これで作図用に更新値を記録できました。

 では作図していきます。まずは2乗和のグラフを等高線で表現します。

# 作図用の値を生成
x0 = np.arange(-4.5, 4.5, 0.1)
x1 = np.arange(-4.5, 4.5, 0.1)

# 格子状の配列に変換
X0, X1 = np.meshgrid(x0, x1)

# 2乗和を計算
Z = function_2(np.array([X0.flatten(), X1.flatten()]).T)

# 作図用に形状を整形
Z = Z.reshape(X0.shape[0], X0.shape[1])
print(Z)
[[40.5  39.61 38.74 ... 37.89 38.74 39.61]
 [39.61 38.72 37.85 ... 37.   37.85 38.72]
 [38.74 37.85 36.98 ... 36.13 36.98 37.85]
 ...
 [37.89 37.   36.13 ... 35.28 36.13 37.  ]
 [38.74 37.85 36.98 ... 36.13 36.98 37.85]
 [39.61 38.72 37.85 ... 37.   37.85 38.72]]

 function_2()には2列の2次元配列を渡す必要があります。そこでX0, X1.flatten()メソッドで1次元配列に変換してから、np.array()で2行の2次元配列を作成し、転置メソッド.Tで整形します。

 ただし作図時には、プロット位置を示すX0, X1と計算結果の形状を揃える必要があるため、.shapeメソッドで再度整形します。

 これでグラフを作成する準備が整いました。まずは$f(\mathbf{x})$の等高線をプロットしましょう。

# 作図
fig = plt.figure() # 図を準備
ax = fig.add_subplot() # 図の準備
cs = ax.contour(X0, X1, Z) # 等高線
cs.clabel(fmt="%.1f") # 等高線のラベル
plt.xlabel("x0") # x軸ラベル
plt.ylabel("x1") # y軸ラベル
plt.show()

f:id:anemptyarchive:20200617002433p:plain
$f(\mathbf{x}) = x_0^2 + x_1^2$の等高線グラフ

 このグラフは図4-8を上から見た図で、$f(\mathbf{x})$の値を等高線で表しています。このグラフを見ると(当然ですが)図4-8や図4-9と同じく、円の中心$(x_0 = 0, x_1 = 0)$に近づくほど値が小さくなっていることが分かります。

 ここに更新値の推移を重ねます。

# 作図
fig = plt.figure() # 図を準備
ax = fig.add_subplot() # 図を準備
cs = ax.contour(X0, X1, Z) # 等高線
cs.clabel(fmt="%.1f") # 等高線のラベル
plt.plot(x_histry[:, 0], x_histry[:, 1], 'o') # 簡易散布図
plt.xlabel("x0") # x軸ラベル
plt.ylabel("x1") # y軸ラベル
plt.title("Gradient Method", fontsize=20) # タイトル
plt.show()

f:id:anemptyarchive:20200617002556p:plain
勾配降下法

 勾配降下法によって$x_0,\ x_1$の更新を繰り返すことで、$f(x)$が最小となる点に近づいていることが確認できました!

 学習率を10としたときの更新値の推移を見てみましょう。

# xの初期値を指定
init_x = np.array([-3.0, 4.0])

# 勾配降下法:学習率が大きすぎる例
x, x_histry = gradient_descent_histry(function_2, init_x=init_x, lr=10.0, step_num=100)
print(x_histry[0:5])
print(x)
[[-3.00000000e+00  4.00000000e+00]
 [ 5.70000000e+01 -7.60000000e+01]
 [-1.08300000e+03  1.44400000e+03]
 [ 2.05770000e+04 -2.74360000e+04]
 [-3.90963008e+05  5.21284002e+05]]
[-2.58983747e+13 -1.29524862e+12]
# 作図
fig = plt.figure() # 図を準備
ax = fig.add_subplot() # 図を準備
cs = ax.contour(X0, X1, Z) # 等高線
cs.clabel(fmt="%.1f") # 等高線のラベル
plt.plot(x_histry[0:2, 0], x_histry[0:2, 1], 'o') # 簡易散布図
plt.xlabel("x0") # x軸ラベル
plt.ylabel("x1") # y軸ラベル
plt.title("Gradient Method", fontsize=20) # タイトル
plt.show()

f:id:anemptyarchive:20200617002639p:plain
学習率が大きい場合

 えーと、左上にあるごちゃっとしているのが先ほどの例では図いっぱいに表示されていた部分です(空白部分は作図用に生成した値の範囲外なので描画できていないだけで、勿論2乗和のグラフはどこまでも続きます)。そして、右下にある点が「1回目の更新値」です。最小値となる原点に辿り着くどころか1度の更新でこれだけ離れてしましました。(ちなみに、2度目の更新の際にはこの地点の勾配を使います。2乗和の関数なので変数の値が大きくなるほど勾配も大きくなります。従って次は、ここから原点に向かって進みそして原点から更に離れた位置になります。当然その次の更新ではまた逆方向に・・・x_histryを見ても行ったり来たりを繰り返しながら発散することが分かります。)

 次は学習率を小さくした場合を見てみましょう。

# xの初期値を指定
init_x = np.array([-3.0, 4.0])

# 勾配降下法:値が小さすぎる例
x, x_histry = gradient_descent_histry(function_2, init_x=init_x, lr=1e-10, step_num=100)
print(x_histry[0:5])
print(x)
[[-3.  4.]
 [-3.  4.]
 [-3.  4.]
 [-3.  4.]
 [-3.  4.]]
[-2.99999994  3.99999992]
# 作図
fig = plt.figure() # 図を準備
ax = fig.add_subplot() # 図を準備
cs = ax.contour(X0, X1, Z) # 等高線
cs.clabel(fmt="%.1f") # 等高線のラベル
plt.plot(x_histry[:, 0], x_histry[:, 1], 'o') # 簡易散布図
plt.xlabel("x0") # x軸ラベル
plt.ylabel("x1") # y軸ラベル
plt.title("Gradient Method", fontsize=20) # タイトル
plt.show()

f:id:anemptyarchive:20200617002706p:plain
学習率が小さい場合

 これもグラフにするまでもなくx_histryの値を見れば分かりますが、初期値から全くと言っていいほど変化していません。最小値に近づくほど勾配が小さくなることを踏まえると、何度更新を繰り返しても最小値までたどり着けないことが分かります。

 このように学習率には適切な値を指定する必要があります。

 また複雑な関数では、学習率が適切であっても最小値ではない極小値を求めてしまうことで正しく学習できないことがあります。勾配降下法以外の勾配法が6章で登場します。

 次項は、ニューラルネットワークの損失関数を最小にする重みを勾配法を使って求めます。

参考文献

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

おわりに

 相変わらず解説することと伏せることの線引きに悩みます。

【次節の内容】

www.anarchive-beta.com