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

# 本記事の内容
内容は[「ゼロから作るDeepLearning」](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8BDeep-Learning-%E2%80%95Python%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%AE%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%A3%85-%E6%96%8E%E8%97%A4-%E5%BA%B7%E6%AF%85/dp/4873117585)の4.4.2 ニューラルネットワークに対する勾配 (p.110あたり)についてです。
疑問が解決したので記事にします。


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

```python
>>> 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`の値が変わりました。(下記)

```python
# 第二引数に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`をどんな値に変更しても返り値は全く変化しません。(下記参照)

```python
# 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`関数を提示しておきます。(下記)

```python
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)`を入力します。(下記)

```python
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`を代入

```python
>>> 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`を代入

```python
>>> 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`関数に代入しています。

```python
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`関数の下記の部分です。

```python
x[idx] = tmp_val + h # 取り出したxに微小なhを足して
fxh1 = f(x)          # f関数に代入しています。
```
ここで重要なことは`net.W`が変化した後に`f`関数が呼び出されていると言う順番です。
`net.W`の変化が`f`関数にどんな影響を与えるのでしょうか？

### `f`関数をもう少し詳しく説明
`net.W`が変化することによって`f`関数にどんな影響があるのかみていきます。
`f`関数を下記に示します。

```python
def f(W):
    return net.loss(x, t)
```
`f`関数に出てくる`loss`関数は本書のp.110で定義している`simpleNet`クラス内にで定義しています。
`simpleNet`クラスを下記に示します。

```python
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`関数を再度下記に示します。

```python
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」を読み進めていきましょう！