Pythonで基礎から機械学習 「勾配法」

# はじめに
　この「Pythonで基礎から機械学習」シリーズの目的や、環境構築方法、シリーズの他の記事などは以下まとめページを最初にご覧下さい。

[Pythonで基礎から機械学習まとめ](https://karaage.hatenadiary.jp/machine-learning-study)

　今回は、[Pythonで基礎から機械学習 「単回帰分析」](https://qiita.com/karaage0703/items/701367b6c926552fe505)、[Pythonで基礎から機械学習 「重回帰分析」](https://qiita.com/karaage0703/items/f38d18afc1569fcc0418)を読んだことが前提の内容となっております。

　また、本記事は、初学者が自分の勉強のために個人的なまとめを公開している記事になります。そのため、記事中に誤記・間違いがある可能性が大いにあります。あらかじめご了承下さい。

　より良いものにしていきたいので、もし間違いに気づいた方は、編集リクエストやコメントをいただけましたら幸いです。

# 勾配法とは
　前回の記事で取り上げたロジスティック回帰では、単回帰・重回帰のように最小二乗法をもとに行列計算で解析解を求めることはできず、勾配法等の探索アルゴリズムで、任意の値から逐次計算をしていくことで解空間を探索して近似解を求める必要があるという話をしました。今回は、その勾配法に関して紹介します。

　といっても、勾配法自体を説明したサイトは、数多くあるので、そちらに説明を譲ります。以下記事等参照下さい。

[最適化のための勾配法](https://gihyo.jp/dev/serial/01/machine-learning/0016)

[数学 - 勾配法について可視化して理解する。](http://pynote.hatenablog.com/entry/math-gradient-method)

　この記事では、今までそれぞれの解き方で計算する必要があった単回帰・重回帰の解が、勾配法という一つのアルゴリズムで解けることと、必ずしも正しい解を求められるわけではないことを確認したいと思います。


# 単回帰分析を勾配法で計算
　まずは、単回帰分析を勾配法で行います。

## 単回帰分析の解析解の確認
　[単回帰分析の記事](https://qiita.com/karaage0703/items/701367b6c926552fe505)の復習です。

　使用するデータは、以下コマンドでダウンロードします。

```sh
!wget https://raw.githubusercontent.com/karaage0703/machine-learning-study/master/data/karaage_data.csv
```

　データを可視化した結果は以下です。

![01_data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/43289ff1-5d1e-e7a6-36d6-a8516eeba674.png)

　最小二乗法を元に解いた、単回帰分析の結果は以下となります。

![02_lr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/158fad72-5cf0-2e5f-d240-2b95881ecb14.png)

　可視化と計算の詳細は、[単回帰分析の記事](https://qiita.com/karaage0703/items/701367b6c926552fe505)を参照下さい。

## 勾配法を用いて単回帰分析
　単回帰分析を勾配法を用いて行います。

　まずは、必要なライブラリをインポートします。

```python
import numpy as np
import pandas as pd
```

　続いて、データを読み込んで、計算できる形に前処理しておきます（データのダウンロードに関しては、前の節の冒頭を参照下さい）。

```python
df = pd.read_csv('karaage_data.csv')
df.head()

x = df[['x']].values
x = np.insert(x, 0, 1.0, axis=1)
y = df[['y']].values.T[0]
```

　勾配法の設定値（ハイパーパラメータと呼ばれます）を設定します。

```python
# 繰り返し回数（10より大きい数を入れる）
EPOCHS = 50

# 学習率
LEARNING_RATE = 0.01
```

　さて、ここでハイパーパラメータは、どうやって決めるのでしょうか？実は基本的に人間が経験則で決めます。もちろん決め方の指針に関する研究や自動化に関する研究もあるのですが、問題設定によって大きく変わるので、とりあえず色々試して良かったものを選ぶというのが一般的です。

　そんなんでいいの？と思うかもしれませんが、実はディープラーニングでも状況は同じで、ハイパーパラメータは試行錯誤しながらいまだに人間が調整することが多かったりします。AutoML等、自動化も進んできましたが、膨大な計算資源が必要となります。

　続いて勾配降下法を実装します。

```python
def pred(x, w):
    return x @ w

def gradient_method(x, y, epochs=EPOCHS):
    # 重みベクトル初期化
    w = np.ones(x.shape[1])
    # 結果履歴初期化
    history = np.zeros((0,2))

    # 逐次計算（学習）
    for i in range(epochs):
        yp = pred(x, w)
        yd = yp - y
        w = w - LEARNING_RATE * (x.T @ yd) / x.shape[0]
        if ( i % ((int)(EPOCHS / 10)) == 0):
            loss = np.mean(yd ** 2) / 2
            history = np.vstack((history, np.array([i, loss])))

    return w, history
```

　やっていることは、損失関数の空間で勾配を計算して、損失関数を最小化する方向に重みを更新していくだけですね。

　勾配法による計算を行います。

```python
weights, history = gradient_method(x, y, epochs=EPOCHS)
```

　計算結果を表示します。

```python
import matplotlib.pyplot as plt
import seaborn as sns

xall = x[:,1].ravel()
xl = np.array([[1, xall.min()],[1, xall.max()]])
yl = pred(xl, weights)

plt.figure(figsize=(6,6))
plt.plot(x[:,1], y, 'o')
plt.plot(xl[:,1], yl, linestyle="solid")
plt.show()
```

![03_lr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/b11fcb20-7c34-80fe-075c-4ddf44c9d85c.png)

　むむ、なんか変ですね。

　以下コマンドで学習曲線を表示させます。学習曲線は、損失関数の値（loss）の試行回数ごとの変化具合で、lossが小さいほど正しい解が得られているという指標になります。

```python
plt.plot(history[1:,0], history[1:,1])
plt.show()
```

![04_lr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/d1d15cc8-bc9a-7e71-dbb8-0b59c03e5ccd.png)

　lossがめちゃめちゃ発散していますね。

　学習率が大きすぎたかもしれないので、以下のように学習率を変えてみます。

```python
LEARNING_RATE = 0.001
```

　再度計算します。

```python
weights, history = gradient_method(x, y, epochs=EPOCHS)
```

　結果を先ほどと同じ要領で表示します。

![05_lr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/a9e78e79-809a-9305-026d-a6387b9a8937.png)

　今度は良い感じですね。値自体も、ほぼ解析解と一致しています。

　学習曲線を表示させます。

![06_learn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/7d811b80-bf8d-0745-44c6-ff6e499295d8.png)

　良い感じにlossが下がっていますね。

# 重回帰分析を勾配法で計算

## 重回帰分析の解析解の確認
　重回帰分析の記事の復習です。利用するデータは以下コマンドでダウンロードします。

```
!wget http://pythondatascience.plavox.info/wp-content/uploads/2016/07/winequality-red.csv
```

　データを可視化します。
![07_ml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/a6363ba5-15a4-0cb3-9aa9-897b9595081a.png)

　重回帰分析の解析解を可視化すると以下のようになりました。
![08_mr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/80a2dff9-d2a3-178f-c664-7a9f827e8b3c.png)

　詳細は、重回帰分析の記事を参照下さい。

## 勾配法を用いて重回帰分析
　単回帰と同様に計算していきます。

　以下でデータを読み込み前処理します。

```python
import pandas as pd

df = pd.read_csv('winequality-red.csv', sep=';')

x = df[['density', 'volatile acidity']]
y = df[['alcohol']]

x = x.values
x = np.insert(x, 0, 1.0, axis=1)
y = y.values.T[0]
```

　ハイパーパラメータの設定です。

```
# 繰り返し回数（10より大きい数を入れる）
EPOCHS = 500

# 学習率
LEARNING_RATE = 0.01
```

　勾配法を計算します。ここで勾配法のアルゴリズム（関数）自体は、単回帰と全く同じものを使用します。

```
weights, history = gradient_method(x, y, epochs=EPOCHS)
```

　可視化します。

```
from mpl_toolkits.mplot3d import Axes3D  #3Dplot
import matplotlib.pyplot as plt
import seaborn as sns

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

y = df[['alcohol']]
x1 = df[['density']]
x2 = df[['volatile acidity']]

ax.scatter3D(x1, x2, y)
ax.set_xlabel("x1")
ax.set_ylabel("x2")
ax.set_zlabel("y")

mesh_x1 = np.arange(x1.min()[0], x1.max()[0], (x1.max()[0]-x1.min()[0])/20)
mesh_x2 = np.arange(x2.min()[0], x2.max()[0], (x2.max()[0]-x2.min()[0])/20)
mesh_x1, mesh_x2 = np.meshgrid(mesh_x1, mesh_x2)
mesh_y = weights[1] * mesh_x1 + weights[2] * mesh_x2 + weights[0]
ax.plot_wireframe(mesh_x1, mesh_x2, mesh_y)
plt.show()
```

![09_ml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/d26dedf0-d38e-b6d1-cd8f-59cd37acd71e.png)

　3次元なので、分かりづらいですが、あんまりフィットしていない感じがしますね。

　学習曲線を可視化します。

```
plt.plot(history[:,0], history[:,1])
plt.show()
```


![10_learning.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/91a0818f-0a59-b51f-b289-ea7f70ce0b88.png)

　良い感じにlossが下がっていますが、まだまだ下がっていきそうな雰囲気がありますね。

　繰り返し回数が少なかったのかもしれません。回数を増やしてみましょう。

```python
EPOCHS = 5000
```

　再度計算します。

```python
weights, history = gradient_method(x, y, epochs=EPOCHS)
```

![11_ml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/df93adc5-fd19-bcdf-e2dd-5a2402a1e79e.png)

　それっぽい感じになりました。

![12_learning.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/81919/c8ae7968-a305-d14d-e568-cf442a981109.png)

　学習曲線も良い感じにlossが下がっています。

　ただし、解析解とはかなり異なる値になっていることがわかります。変数が増えるほど、損失関数が最小に見える解（局所解）が増えるので、解析解が得られないことは増えていきます。

# ロジスティック回帰分析を勾配法で計算
　ロジスティック回帰も、もちろん勾配法で同様に解けるはずなのですが、今回は省略します。もし実施したら追記します。

# TensorFlowを使って勾配法を実装
　TensorFlowは、ディープラーニングのフレームワークとして有名ですが、その実は自動微分のライブラリなので、勾配法はお手の物です。この記事では省略しますが、興味ある方は以下サイトなど参照下さい。

https://qiita.com/MahoTakara/items/0d7284774c2adf1f05ec

# まとめ
　勾配法に関して学習しました。単回帰をはじめとして、重回帰、ロジスティック回帰も同じ方法で近似解を計算できてしまいます。一方、ハイパーパラメータを適切に設定しないと、適切な解を得られない場合があることも分かりました。

　実は、ディープラーニングでもニューラルネットワークの膨大な変数の損失関数の解を探索する方法のベースとして勾配法が使われています。そして、ハイパーパラメータによって、適切な解（モデル）が得られないのは、ディープラーニングでも同じだったりします。

　大局的に見れば、結局パラメータが増えただけで、単回帰もディープラーニングもやっていることは一緒と言えないこともないですね。