本記事の内容
内容は「ゼロから作る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」を読み進めていきましょう!