備忘録_勾配

備忘録_勾配

すべての変数の偏微分をベクトルとして纏めたものを勾配という。

Pythonで実装すると・・・

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]
        x[idx] = tmp_val + h
        fxh1 = f(x)

        x[idx] = tmp_val - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = tmp_val

    return grad
  • np.zeros_like(x)
    xと同じ形状の配列で、その要素が全て0の配列

今回の例で使用する関数

     \[ f(x^{}_{0},x^{}_{1})=x^{2}_{0}+x^{2}_{1} \]

Pythonで実装した場合

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

(3, 4)、(0, 2)、(3, 0)での勾配

>>> numerical_gradient(function_2, np.array([3.0, 4.0]))
array([6., 8.]) #※
>>>  numerical_gradient(function_2, np.array([0.0, 2.0]))
array([0., 4.])
>>> numerical_gradient(function_2, np.array([3.0, 0.0]))
array([6., 0.])

※実際は[6.00000000000378, 7.999999999999119]という値が得られるが、[6.,  8.]として出力される。NumPy配列を出力する時には、数値が見やすいように整形して出力される為である。

f(x^{}_{0},x^{}_{1})=x^{2}_{0}+x^{2}_{1}の勾配を図で表すと・・・

  • 向きを持ったベクトルで図示される
  • 勾配は、関数f(x^{}_{0},x^{}_{1})の一番低い場所を指しているようである
  • 一番低い場所から遠く離れれば離れるほど、矢印の大きさも大きくなる

勾配が示す方向は、各場所において関数の値を最も減らす方向である

勾配法

勾配方向へ進むことを繰り返すことで、関数の値を徐々に減らす。機械学習の最適化問題でよく使われる手法であり、ニューラルネットワークの学習では勾配法が用いられることが多い。機械学習の問題の多くは、学習の際に最適なパラメータを探索する。最適なパラメータとは損失関数が最小値を取る時のパラメータの値である。勾配をうまく利用して関数の最小値を探そうというのが勾配法である。

数式で表すと・・・

     \begin{eqnarray*} x^{}_{0}=x^{}_{0} - \eta \frac{\partial f}{x^{}_{0}} \\ x^{}_{1}=x^{}_{1} - \eta \frac{\partial f}{x^{}_{1}} \end{eqnarray*}

\eta は更新の量を表す。ニューラルネットワークの学習においては学習率と呼ぶ。1回の学習でどれだけ学習すべきか、どれだけパラメータを更新するかということを決めるのが学習率である。上記の式は1回の更新式を示しており、そのステップを何度か繰り返すことによって徐々に関数の値を減らしていく。

学習率の値は0.01や0.001など、前もって何らかの値に決める必要がある。この値は、一般的に、大きすぎても小さすぎても、「良い場所」にたどりつくことができない。学習率の値を変更しながら、正しく学習できているかどうか、確認作業を行うことが一般的である。

Pythonで実装すると…

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad # x = x -(lr * grad) と同じ
    return x
  • f
    最適化したい関数
  • init_x
    初期値
  • lr
    学習率(learning rate)
  • step_num
    勾配法による繰り返しの数

f(x^{}_{0},x^{}_{1})=x^{2}_{0}+x^{2}_{1}の最小値を勾配法で求める

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

>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x =init_x, lr =0.1, step_num=100)
array([-6.11110793e-10,  8.14814391e-10])

勾配法による更新のプロセスを図で表すと・・・

学習率が大きすぎる、小さすぎる例

#学習率が大きすぎる例:lr=10.0
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x =init_x, lr =10, step_num=100)
array([-2.58983747e+13, -1.29524862e+12])

#学習率が小さすぎる例:lr=1e-10
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x =init_x, lr =1e-10, step_num=100)
array([-2.99999994,  3.99999992])

学習率が大きすぎると、大きな値へと発散してしまう。逆に、学習率が小さすぎると、ほとんど更新されずに終わってしまう。

適切な学習率を設定するということが重要

重みパラメータに関する損失関数の勾配

形状が2×3の重みWだけを持ち、損失関数Lで表すニューラルネットワーク

数式で表すと・・・

     \[ W=\left( \begin{array}{ccc} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{array} \right) \] \[ \frac{\partial L}{\partial W}=\left( \begin{array}{ccc} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} & \frac{\partial L}{\partial w_{13}} \\ \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} & \frac{\partial L}{\partial w_{23}} \end{array} \right) \]

\frac{\partial L}{\partial W}はそれおぞれの要素に関する偏微分から構成される

例えば、1行1列の要素である\frac{\partial L}{\partial w_{11}} w_{11} を少し変化させると損失関数Lがどれだけ変化するかということを表す

\frac{\partial L}{\partial W}の形状はWと同じである

今回使用するニューラルネットワークの実装

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

softmaxメソッドcross_entropy_errorメソッドnumerical_gradientメソッドゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装から提供頂いたものを使用しています。

  • predict(x)
    予測するためのメソッド
  • loss(x, t)
    損失関数の値を求めるためのメソッド
  • x
    入力データを想定
  • t
    正解ラベルを想定

simpleNetクラスの使用すると・・・

>>> net = simpleNet()
>>> print(net.W) #重みパラメータ
[[-0.29140667  0.31285577  0.58119469]
 [-0.8501255  -0.4970748  -0.12180229]]
>>> x = np.array([0.6, 0.9]) 
>>> p = net.predict(x)
>>> print(p)
[-0.93995695 -0.25965386  0.23909476]
>>> np.argmax(p) #最大値のインデックス
2
>>> t = np.array([0,0,1]) #正解ラベル
>>> net.loss(x, t)
0.6496445327621232

勾配を求めると・・・

>>> def f (X):
    return net.loss(x, t)
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[ 0.09638823  0.19028201 -0.28667024]
 [ 0.14458235  0.28542301 -0.43000536]]

f(W)は、ダミーとして設けたものである。これは、numerical_gradient(f, x)が内部でf(x)を実行するため、それと整合性がとれるようにf(W)を定義した。

  • numerical_gradient(f, x)
    引数f・・・関数
    引数x・・・関数fへの引数
  • dW
    形状が2×3の2次元配列になる。
  • \frac{\partial L}{\partial W}\frac{\partial L}{\partial w_{11}}
    およそ0.1のため、w_{11}をhだけ増やすと損失関数の値は0.1hだけ増加するということを意味する
    損失関数を減らすという観点からはマイナス方向へ更新するのがよい
  • \frac{\partial L}{\partial W}\frac{\partial L}{\partial w_{23}}
    およそ-0.4のため、w_{23}をhだけ増やすと損失関数の値は0.4hだけ減少するということを意味する
    損失関数を減らすという観点からはプラス方向へ更新するのがよい