機械学習にでてくる勾配降下法/勾配ベクトルなどの整理。ついでにPythonで試してみた。


さてさて [ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装](https://amzn.to/2vfNT85) を絶賛学習中。[前回書いた記事](https://qiita.com/masatomix/items/1ab6aca13b2da96a49fe)では、MNISTの手書き文字をニューラルネットワークで分類する件をちょっと整理したのでした。

## 勾配ベクトルとか、勾配降下法とか、損失関数の最小値とか

そのつづきの 4.4.1節の **勾配法** で「損失関数の最小値」を求めるという節があります。損失関数ってのは「ニューラルネットワークから出力されたAIがだした回答」と「教師データ(正解)」の誤差を算出する多変数関数です。

そして、その損失関数が最小になるようパラメタ(重みとバイアス)を調整することを「ニューラルネットワークの学習」と呼んでいるようですね。


さて **損失関数$f$の最小値を求める方法として勾配降下法(gradient descent method)** ってのが出てきます。勾配降下法ってのは、ある初期値ベクトル $\boldsymbol{x_{0}}$から始まって、下記の漸化式にしたがって$\boldsymbol{x_i}$ を決めて

```math
\boldsymbol{x_{i+1}} = \boldsymbol{x_{i}} - \eta 
\left(
\begin{array}{c}
\frac{\partial f}{\partial x^{0}}\\
\frac{\partial f}{\partial x^{1}}\\
\vdots
\end{array}
\right) \Big|_{\boldsymbol{x}=\boldsymbol{x_{i}}}
```

損失関数 $f(\boldsymbol{x_i})$ の値を順次計算していく、みたいな方法です。


この、ある ${\boldsymbol{x}=\boldsymbol{x_{i}}}$ における

```math
\left(
\begin{array}{c}
\frac{\partial f}{\partial x^{0}}\\
\frac{\partial f}{\partial x^{1}}\\
\vdots
\end{array}
\right) =: \nabla f
```

を「**勾配ベクトル**」と呼びます。で、勾配ベクトルに微小な$\eta$ (これを学習率という) をかけて引くという漸化式を繰り返すことで、**すこしずつ$\nabla f$の逆方向に進んでいく**ことになります。そして、勾配ベクトルの性質「**勾配ベクトルの逆方向にすこし進むと、関数 $f$ の値はすこしだけ小さくなる**」という性質により、$f$ の最小値の場所に収束していく、というのが勾配降下法の仕組みみたいですね。


### 勾配ベクトルってなんだっけ、、、？

勾配ベクトル、、うーん、むかーし学生の頃ならった気もするなーとおもいつつ「勾配ベクトルの逆方向にすこし進むと、関数 $f$ の値がすこしだけ小さくなる」ってのはなんだかすっかり忘れてたので、、、うん復習することにしました。

さて必要な前提知識は、下記の多変数関数のTaylor展開(というか一次近似式):

```math
f(x,y) = f(x_0,y_0) + \frac{\partial \ f(x_0,y_0)}{\partial x}(x-x_0)+ \frac{\partial \ f(x_0,y_0)}{\partial y}(y-y_0)+ R(x,y) 
```

(これは $\boldsymbol{x}$ が二次元の例で、$R$ は二次以上の無限小)。これと、ベクトルの内積の性質について:

```math
\begin{align}
\|\boldsymbol{a}\|\|\boldsymbol{b}\| &\geq 
\boldsymbol{a}\cdot\boldsymbol{b} &(等号は、\boldsymbol{a} \ //\ \boldsymbol{b} のときのみ成立)\\
&= \sum_{i=1}^{n} a_ib_i \\
\end{align}
```
(よーするにCauchy–Schwarzの不等式) を用います。


先の多変数関数を一次近似した式は、下記のように式変形できます。$x-x_0 =:\Delta x,\ y-y_0 =:\Delta y$ として、

```math

\begin{align}
f(x_0+\Delta x,y_0+\Delta y) - f(x_0,y_0) &= \frac{\partial \ f(x_0,y_0)}{\partial x}\Delta x+ \frac{\partial \ f(x_0,y_0)}{\partial y}\Delta y &+ R(x,y) \\
&= \left(
\begin{array}{c}
\frac{\partial f}{\partial x}\\
\frac{\partial f}{\partial y}\\
\end{array}
\right) \cdot \left(
\begin{array}{c}
\Delta x\\
\Delta y\\
\end{array}
\right) &+ R(x,y) \\
&=  \nabla f(x_0,y_0) \cdot \left(
\begin{array}{c}
\Delta x\\
\Delta y\\
\end{array}
\right) &+ R(x,y) 
\end{align}
```

さきの Cauchy–Schwarzの不等式 を用いることで

```math
\begin{align}
f(x_0+\Delta x,y_0+\Delta y) - f(x_0,y_0) &= \nabla f(x_0,y_0) \cdot \left(
\begin{array}{c}
\Delta x\\
\Delta y\\
\end{array}
\right) &+ R(x,y) \\
&\leq \|\nabla f(x_0,y_0)\| \|\boldsymbol{\Delta x} \| &+ R(x,y) 
\end{align}
```

が成立します。等号は $\boldsymbol{\Delta x}$ が $\boldsymbol{\nabla f(x_0,y_0)}$ のスカラ倍のときのみ成立するので、そのときに $f$ の変化量が最大であることが分かりました。


さて $\boldsymbol{\Delta x}$ が $\boldsymbol{\nabla f(x_0,y_0)}$ のスカラ倍つまり $\boldsymbol{\Delta x} := k \boldsymbol{\nabla f(x_0,y_0)}$ のとき


```math
\begin{align}
f(x_0+\Delta x,y_0+\Delta y) - f(x_0,y_0) &= \nabla f(x_0,y_0) \cdot \left(
\begin{array}{c}
\Delta x\\
\Delta y\\
\end{array}
\right) &+ R(x,y) \\
&= k \nabla f(x_0,y_0) \cdot \nabla f(x_0,y_0) &+ R(x,y) \\
&= k\|\nabla f(x_0,y_0)\|^2 &+ R(x,y) 
\end{align}\\

```

したがって $\Delta x, \Delta y$ が限りなく小さければ、つまり$k$が十分に小さければ、$R(x,y)$を無視することで、

```math
f(x_0+\Delta x,y_0+\Delta y) - f(x_0,y_0) \approx  k\|\nabla f(x_0,y_0)\|^2  
```

となることがわかりました。これは、**$\Delta x, \Delta y$ が $\nabla f(x_0,y_0)$  と正方向におなじ向き($k>0$)に進んだときに、$f$ の値の変化量は正となる** ことを示しています。

逆に$\ \boldsymbol{\Delta x} := -\eta \boldsymbol{\nabla f(x_0,y_0)} \ \ \ \ (\eta > 0)$ として $\nabla f(x_0,y_0)$ と平行に逆方向に進めば、


```math
\begin{align}
f(x_0+\Delta x,y_0+\Delta y) - f(x_0,y_0) &= \nabla f(x_0,y_0) \cdot \left(
\begin{array}{c}
\Delta x\\
\Delta y\\
\end{array}
\right) &+ R(x,y) \\
&= -\eta  \nabla f(x_0,y_0) \cdot \nabla f(x_0,y_0) &+ R(x,y) \\
&= -\eta \|\nabla f(x_0,y_0)\|^2 &+ R(x,y) 
\end{align}\\

```

となり、$f$ の値を小さくする方向に進むということがわかります。したがって、**ちびちびと $\eta$倍 づつ勾配ベクトルの逆方向に進んでいくことで、局所的には徐々に値が小さい方に行きそう**だということが分かりました。

参考:

- [勾配ベクトル(∇f)の方向の意味](https://qiita.com/tamanujan/items/96fc2fc4026a67e4f162) 
- [ニューラルネットワークを用いた手書き文字認識](http://nnadl-ja.github.io/nnadl_site_ja/chap1.html#learning_with_gradient_descent)




## Pythonで計算してみる。

さて実際に

```math
f(x,y) := x^2 + y^2
```

の最小値を勾配降下法で求めてみます。ちなみに$f$ はこんな感じの関数です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/51cc3717-2d7e-a2c2-feab-fe4e6e3c9184.png)


式からも見た目からも、、原点(0,0)が最小値っぽいですが、、、。

数値微分と勾配降下法のコードは、MITライセンスで公開してくださっている
https://github.com/oreilly-japan/deep-learning-from-scratch/tree/master/ch04
を参考(ほぼ流用)にさせてもらいました。感謝です！


### Pythonの分離環境作成

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4
BuildVersion:	18E226
$

$ python --version
Python 3.7.1

$ python -m venv ./venv
$ source ./venv/bin/activate
(venv) $

(venv) $ pip install numpy
(venv) $
```

### やってみる

```python:index.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import sys

def main(args):
    lr = 0.10  # 学習率 η
    step_num = 100  # 繰返し回数

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

    init_x = np.array([-3.0, 4.0])  # 初期値は(x,y) = (-3,4)とした
    x1 = gradient_descent(f, init_x, lr=lr, step_num=step_num)

    print(x1)


def numerical_gradient(f, x):
    """
    ベクトル xと、多変数関数fを引数にとって、数値微分の結果をベクトルで返す
    :param f: 多変数関数
    :param x: ベクトル
    :return: 各成分で偏微分したベクトル。つまり勾配ベクトル
    """

    h = 1e-4
    grad = np.zeros_like(x)  # おなじかたちのゼロベクトル作成

    for index in range(x.size):  # ベクトルの各成分で偏微分
        tmp = x[index]  # 待避

        x[index] = tmp + h  # hすすめたf
        fx_plus_h = f(x)

        x[index] = tmp - h  # hもどしたf
        fx_minus_h = f(x)

        grad[index] = (fx_plus_h - fx_minus_h) / (2 * h)
        x[index] = tmp  # 待避してたxをもどす

    return grad


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    """
    勾配降下法を用いて、fの最小値をとるベクトルxを計算して返す
    :param f: 対象の多変数関数
    :param init_x: 初期値ベクトル
    :param lr:  学習率
    :param step_num: 何回繰り返すか
    :return: 最小値となるベクトルx
    """
    x = init_x

    for index in range(step_num):
        grad = numerical_gradient(f, x)
        x = x - lr * grad

    return x


if __name__ == "__main__":
    main(sys.argv)
```

実行します。

```bash
(venv) $ python index.py 
[-6.11110793e-10  8.14814391e-10]

(venv) $
```

やっぱり、ほぼ原点の場所が最小値だって事が分かりました。

コードをすこしだけみてみると、まず、``numerical_gradient``関数は、関数とベクトルを引数にとって、その地点の勾配ベクトルを返しています。
``gradient_descent`` 関数が勾配降下法を使って最小値の地点を求める関数です。``init_x``は初期値のベクトル、lrはいわゆる$\eta$(学習率)、``step_num``は、漸化式の繰返し回数です。


ちなみに、先のグラフを等高線付きで見てみると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/e998b251-572d-d027-9ab5-df4c7ebc7cd0.png)

となってるので、図的にもあってそうですね。


さて勾配降下法で、初期ベクトル$(x_0,y_0)= (-3,4)$ から始まって、原点までたどり着いたようですが、一応その軌跡を確認しておきます。といってもこれも書籍のコードとほぼ同様です。。

```python:index_his.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import sys
import matplotlib.pyplot as plt


def main(args):
    lr = 0.10  # 学習率 η
    step_num = 100  # 繰返し回数

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

    init_x = np.array([-3.0, 4.0])  # 初期値は(x,y) = (-3,4)とした
    x1, x_history = gradient_descent(f, init_x, lr=lr, step_num=step_num)

    print(x1)
    # print(x_history)

    plt.plot([-5, 5], [0, 0], '--b')  # 軸の描写
    plt.plot([0, 0], [-5, 5], '--b')  # 軸の描写
    plt.plot(x_history[:, 0], x_history[:, 1], 'o')

    plt.xlabel("x")
    plt.ylabel("y")
    plt.show()


def numerical_gradient(f, x):
    """
    ベクトル xと、多変数関数fを引数にとって、数値微分の結果をベクトルで返す
    :param f: 多変数関数
    :param x: ベクトル
    :return: 各成分で偏微分したベクトル。つまり勾配ベクトル
    """

    h = 1e-4
    grad = np.zeros_like(x)  # おなじかたちのゼロベクトル作成

    for index in range(x.size):  # ベクトルの各成分で偏微分
        tmp = x[index]  # 待避

        x[index] = tmp + h  # hすすめたf
        fx_plus_h = f(x)

        x[index] = tmp - h  # hもどしたf
        fx_minus_h = f(x)

        grad[index] = (fx_plus_h - fx_minus_h) / (2 * h)
        x[index] = tmp  # 待避してたxをもどす

    return grad


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    """
    勾配降下法を用いて、fの最小値をとるベクトルxを計算して返す
    :param f: 対象の多変数関数
    :param init_x: 初期値ベクトル
    :param lr:  学習率
    :param step_num: 何回繰り返すか
    :return: 最小値となるベクトルx
    """
    x = init_x
    x_history = []

    for index in range(step_num):
        x_history.append(x.copy())
        grad = numerical_gradient(f, x)
        x = x - lr * grad

    return x, np.array(x_history)


if __name__ == "__main__":
    main(sys.argv)
```

実行します

```bash
(venv) $ python index_his.py 
[-6.11110793e-10  8.14814391e-10]
(venv) $
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/c457da15-e71e-0096-2b13-0915aaa0033f.png)

初期値$(-3,4)$から原点に向かって、勾配降下している事が確認出来ましたね！


おつかれさまでした。


## (補足) 等高線付きグラフを出力するコード

蛇足ですが、さきほどの等高線付きグラフを出力するコードはこんな感じです。


```bash 
(venv) $ pip install matplotlib
(venv) $ pip install PyQt5
```

```python:3d.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm


def func2(X, Y):
    return X ** 2 + Y ** 2


def main(args):
    x = np.arange(-5, 5, 0.25)
    y = np.arange(-5, 5, 0.25)

    X, Y = np.meshgrid(x, y)
    Z = func2(X, Y)

    fig = plt.figure()
    ax = Axes3D(fig)

    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_zlabel("f(x, y)")

    contour = ax.contourf(X, Y, Z, levels=20, cmap=cm.coolwarm)
    fig.colorbar(contour, shrink=0.5, aspect=10)
    plt.show()


if __name__ == "__main__":
    main(sys.argv)
```

参考:

- [MacでMatplotlib がエラーが出たりプロットが表示されないときの対応](https://qiita.com/masatomix/items/03419c7ea10262da18f3)



おつかれさまでした。


## 関連リンク

- [勾配ベクトル(∇f)の方向の意味](https://qiita.com/tamanujan/items/96fc2fc4026a67e4f162) 勾配ベクトルに関しては、ココに超詳しく書いてありました。。
- [ベクトルの内積の意味や性質](https://qiita.com/masatomix/items/278a233c39b4e82d0bad#%E5%86%85%E7%A9%8D%E3%81%AE%E5%AE%9A%E7%BE%A9)
- [微分をおさらいしつつ偏微分をつまみ食い！](https://qiita.com/43x2/items/aaff93cc0b978223a8f6)微分・偏微分や勾配降下法について詳しく書いてありました。
- [勾配ベクトルの意味と例題](https://mathtrain.jp/koubai) わかりやすい
- [Chainerで勾配法の基礎の基礎を確認【ニューラルネット入門】](https://www.hellocybernetics.tech/entry/2017/05/17/045055)
- [ニューラルネットワークを用いた手書き文字認識](http://nnadl-ja.github.io/nnadl_site_ja/chap1.html#learning_with_gradient_descent)