14
4

More than 3 years have passed since last update.

ゼロから作るDeepLearning 4.4.2 ニューラルネットワークに対する勾配 numerical_gradient関数についての疑問が解決しました。

Last updated at Posted at 2020-05-08

本記事の内容

内容は「ゼロから作るDeepLearning」の4.4.2 ニューラルネットワークに対する勾配 (p.110あたり)についてです。
疑問が解決したので記事にします。

疑問

私が疑問に感じていたのはp.111の下の方のコードです。(下記)

>>> def f(W):
... return net.loss(x, t)
...
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[ 0.21924763 0.14356247 -0.36281009]
[ 0.32887144 0.2153437 -0.54421514]]

関数fを定義して、それを本書の少し前で定義したnumerical_gradient関数の引数として渡しています。
このnumerical_gradient関数の第二引数を適当な値に変えてみると、dWの値が変わりました。(下記)

# 第二引数にnet.Wを指定。(net.Wについては本書のp.110からの解説を参照してください。)
>>> dW = numerical_gradient(f, net.W) 
>>> print(dW)
[[ 0.06281915  0.46086202 -0.52368118]
 [ 0.09422873  0.69129304 -0.78552177]]

# aにnumpy配列の格納し、第二引数に指定。
>>> a = np.array([[0.2, 0.1, -0.3],
         [0.12, -0.17, 0.088]])
>>> dW = numerical_gradient(f, a)
>>> print(dW)
[[0. 0. 0.]
 [0. 0. 0.]]

なぜdWの値が変わったのか分かりませんでした。
この記事はこの疑問の答えを書いたものです。

なぜ疑問に思ったか

なぜdWの値が変化したことに疑問を持ったかを説明していきます。

ポイント1

まず押さえておくこととして、このf関数は返り値が引数Wの値に全く関係しません。
なぜならf関数内のreturn以降にWが登場していないからです。
なのでf関数の引数Wをどんな値に変更しても返り値は全く変化しません。(下記参照)

# f関数の引数に3を指定。
>>> f(3) 
2.0620146712373737


# f関数にnet.Wを指定。
>>> f(net.W) 
2.0620146712373737


# numpy配列を定義し、aに代入。f関数にaを渡した時と3を渡した時を比較。
>>> a = np.array([[0.2, 0.1, -0.3], 
         [0.12, -0.17, 0.088]]) 
>>> f(a) == f(3) 
True

ポイント2

もう一つのポイントとして、numerical_gradient関数を提示しておきます。(下記)

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x) # f(x+h)

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

        x[idx] = tmp_val # 値を元に戻す
        it.iternext()   

    return grad

この関数は関数内で定義しているgradを返り値とします。
このgradがどのように導き出されるかをコードの下から順に追っていくと、
grad[idx] = (fxh1 - fxh2) / (2*h)と言うコードを見つけることができます。
ではfxh1fxh2が何なのかと言うと、
fxh1 = f(x) fxh2 = f(x)と言うコードを見つけることができます。

ポイント1,2のまとめ

ポイント2より、numerical_gradient関数の返り値gradf(x)の値によるものだと考えることができます。
ポイント1より、f関数は引数の値に関わらず一定の値を返します。
ポイント1,2より、numerical_gradient関数の第二引数xにどんな値を代入しようとも、
numerical_gradient関数の返り値が変化するのはおかしい、と私は考えた訳です。

解決方法

まずはnumerical_gradient関数について詳しく見ていきます。
そして、関数fについてもう少し詳しく説明をします。

numerical_gradient関数について詳しく

numerical_gradientのコードに少し手を加えます。
具体的にはfxh1 = f(x) fxh2 = f(x)の下にそれぞれ
print(fxh1) print(fxh2)を入力します。(下記)

def numerical_gradient(f, x):
    h = 1e-4 
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        print('idx:', idx)
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x) # f(x+h)
        print('fxh1:', fxh1) # print(fxh1)と入力

        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        print('fxh2:', fxh2) # print(fxh2)と入力

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

        x[idx] = tmp_val 
        it.iternext()   

    return grad

では、第二引数を変えてコードを動かしてみましょう。

第二引数にnet.Wを代入

>>> dW = numerical_gradient(f, net.W)
fxh1: 2.062020953321506
fxh2: 2.0620083894906935
fxh1: 2.062060757760379
fxh2: 2.061968585355599
fxh1: 2.061962303319411
fxh2: 2.062067039554999
fxh1: 2.062024094490122
fxh2: 2.062005248743893
fxh1: 2.062083801262337
fxh2: 2.0619455426551796
fxh1: 2.061936119510309
fxh2: 2.06209322386368

第二引数に自作のnumpy配列aを代入

>>> a = np.array([[0.2, 0.1, -0.3],
         [0.12, -0.17, 0.088]])
>>> dW = numerical_gradient(f, a)
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737
fxh1: 2.0620146712373737
fxh2: 2.0620146712373737

第二引数にnet.Wを代入した方は、fxh1fxh2で値が微妙に違っています。
対して、自作のnumpy配列aを代入した時はfxh1fxh2は同じ値です。
なぜでしょうか?
これ以降は第二引数にnet.Wを入れた場合を考えて解説を行います。

numerical_gradient関数をもう一度詳しくみてみましょう。
中程に下記のコードがあります。
このコードではidxのインデックス番号を変化させて、そのインデックス番号のxを取り出し、
取り出したxに微小なhを足して、f関数に代入しています。

it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index # idxのインデックス番号を変化させて
        tmp_val = x[idx]     # そのインデックス番号のxを取り出し
        x[idx] = tmp_val + h # 取り出したxに微小なhを足して
        fxh1 = f(x) # f(x+h) # f関数に代入しています。

xに微小なhが足されたことにより、f関数の返り値が変化してしまったのでしょうか?
しかし、引数の変化によりf関数の返り値が変化しないことは前述の「なぜ疑問に思ったのか」のポイント1で示しています。

実はxに微小なhを足すことで変化した部分があるのです。
ここで言うxとは、numerical_gradient関数の第二引数に代入したnet.Wです。
net.Wに微小なhが足された後に、f関数の引数に渡されるのです。
先程示したnumerical_gradient関数の下記の部分です。

x[idx] = tmp_val + h # 取り出したxに微小なhを足して
fxh1 = f(x)          # f関数に代入しています。

ここで重要なことはnet.Wが変化した後にf関数が呼び出されていると言う順番です。
net.Wの変化がf関数にどんな影響を与えるのでしょうか?

f関数をもう少し詳しく説明

net.Wが変化することによってf関数にどんな影響があるのかみていきます。
f関数を下記に示します。

def f(W):
    return net.loss(x, t)

f関数に出てくるloss関数は本書のp.110で定義しているsimpleNetクラス内にで定義しています。
simpleNetクラスを下記に示します。

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

simpleNetの下の方にloss関数が出てきますね。
loss関数の中にpredict関数があります。
predict関数はloss関数のすぐ上で定義されています。
predict関数をよく見てみると重みパラメータであるWが登場しています。

numerical_gradient関数について詳しく」の最後で、net.Wの変化がf関数にどんな影響を与えるのでしょうか?と記述しましたがその答えがここです。
net.Wが変化することで、f関数内のloss関数で呼び出されるpredict関数の重みパラメータWが変化してしまっていたのです。
すると当然loss関数の返り値は変化しますよね。

まとめ

いよいよ説明も終盤です。

話をnumerical_gradient関数に戻します。numerical_gradient関数を再度下記に示します。

def numerical_gradient(f, x):
    h = 1e-4 
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        print('idx:', idx)
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        print('fxh2:', fxh2)

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

        x[idx] = tmp_val 
        it.iternext()   

    return grad

前述の通り、net.Wの変化により、f関数内のloss関数の返り値が変化します。
このコードで言うならx(net.W)に微小なhを足すことで関数fが変化し、fxh1の値が変化していたのでした。
そのあとのfxh2も同様です。
そしてその後のコードに渡されて、numerical_gradient関数は返り値を出力します。
これで最初に示した疑問は解決しました。

重要なポイントは、
numerical_gradient関数の第二引数であるxnet.Wであることを抑えること。
net.Wが変化することによりf関数の返り値が変化することでした。

では引き続き「ゼロから作るDeepLearning」を読み進めていきましょう!

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4