本記事の内容
内容は「ゼロから作る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)と言うコードを見つけることができます。
ではfxh1、fxh2が何なのかと言うと、
fxh1 = f(x) fxh2 = f(x)と言うコードを見つけることができます。
ポイント1,2のまとめ
ポイント2より、numerical_gradient関数の返り値gradはf(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を代入した方は、fxh1とfxh2で値が微妙に違っています。
対して、自作のnumpy配列aを代入した時はfxh1とfxh2は同じ値です。
なぜでしょうか?
これ以降は第二引数に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関数の第二引数であるxがnet.Wであることを抑えること。
● net.Wが変化することによりf関数の返り値が変化することでした。
では引き続き「ゼロから作るDeepLearning」を読み進めていきましょう!