半歩ずつ進める機械学習　～scikit-learn SVR～

##前回
[前回の投稿][1]でscikit-learnに実装されている主な線形回帰クラス
Ridge/Lasso/ElasticNetについて大まかに纏めました。
今回は、SVRとEnsembleRegressionをやろうと思いましたが
長くなったので、SVRのみで、Ensembleは次回にします。

##御注意！
現在自分も勉強しながら書いてますんで
最大限気を付けているつもりですが間違った記述がある可能性が大いにありますので
御注意ください

##[SVR][2]
前回はRidge/Lasso/ElasticNet(以下線形回帰）を学びましたが
SVRと線形回帰では明確に違うのが、個別の説明変数の影響が目的変数に対して**非線形**である事です。

例えば、カキ氷屋の売り上げを予測するモデルを作る時に、説明変数のひとつが気温だったとしましょう

売上と気温の関係だけを単純な一次式で表現すれば

```math
y = 売上\\
x = 気温\\
b = その他の要素\\
y = nx + b\\
```

これは以下の様に直線的なモデルになります。

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316551/c0a58633-3a95-09b3-de32-e6e7e9b31da9.png)


しかし、例えば気温が高すぎると、外を出歩く人が減って以下の様に売り上げが落ちるとしましょう

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316551/fba8dd89-5c1f-af32-b74d-a9eb57926649.png)

上記では説明変数である気温は、目的変数である売上に対して非線形な関係を持っています。

なので、理想的なモデルは**売上**という目的変数に対して**気温**という説明変数の影響が**非線形**である事です。

しかし**気温**という、説明変数1個だけでは、そこにつく回帰係数（パラメータ）もまた1個なので、線形な表現しか出来ません

そこで以下のように**気温^2**という新たな説明変数作ってモデルを作れば**気温**に対して非線形な出力をするモデルが作れます。

```math
x = 気温\\
y = \theta_1x + \theta_2x^2 + b
```

以上の事から分かるように、非線形なモデルを作ろうと思うと、既存の説明変数の次数を増やしたりして

入力される説明変数を非線形化する必要があります。

SVRでは入力された訓練データをもとに、他の訓練データとの距離という新たな説明変数を作り

そこにパラメータを付けてモデルを作ります。

##カーネル関数
scikit-learnには以下のカーネルが実装されており、任意に選択する事が可能です

+ linear
+ poly
+ rbf
+ sigmoid
+ precomputed 

SVRについて解説されているサイト等を見ると、ガウスカーネル（RBF)を使っとけば概ね大丈夫という記述が良く見られます

scikit-learnでもカーネルを指定しなければ、デフォルトでrbfカーネル（以下ガウスカーネル）が選択されます。

##ガウスカーネル
ガウスカーネルは以下のような形をしています

```math
K(x,x') = exp(-\gamma(x-x')^2)
```

この関数は、引数で与えられた値が一致している時に1を出力し、引数同士の差が広がるにつれて0に近い数値を出力します。

(x-x')の前につくガンマはハイパーパラメータで、非負の数値を任意に指定する必要があります。

ガンマが大きくなるほど、入力変数の差に対してガウスカーネルの出力を小さくする力が強く働きます。

##SVRのモデル
SVRでは訓練データ同士をガウスカーネルに入力して、出てくる値にパラメータを付してモデルを作りますので

具体的に言うと、3件の訓練データが存在した場合、最終的に作るモデルは以下の形になると思います。


```math
\theta_1K(x,x_1)+\theta_2K(x,x_2)+\theta_3K(x,x_3)\\
```

通常の線形重回帰であれば、**パラメータの数 = 説明変数の数**でしたが

SVRでは**パラメータの数 = 説明変数の数 × 訓練データ件数**になります

パラメータの数が増えるので、通常の線形重回帰と比較してモデルの表現力は高まります

ただ、このまま計算すると表現力の高さ故に訓練データに対して誤差が全く発生しない**過学習**モデルが出来上がってしまいます

Ridge/Lassoでも出てきた正則化項を加えてモデルを汎化させる必要があります。

##ガウスカーネルでのSVRの主なハイパーパラメータ
ガウスカーネルを使うSVRで調整が必要な主なパラメータは```gamma/C/epsilon```です。
※この辺りの内容は結構怪しいです、間違いにお気づきの方はご指摘頂けましたら幸いです。

まずgammaですが、これはカーネル関数の出力に効いています。

```math
exp(-\gamma(x - x')^2)
```

次にCとepsilonですが、SVRでは損失関数の値＋正則化項を最小化しますが、このバランスを調整するのがCで

誤差の不感帯の大きさを調整するのがepsilonです。

```math
f(x) = \sum_{i=1}^{n}(\theta_iK(x,x_i))\\
h(x,y) = max(0 , |y - f(x)| - \epsilon)\\
min\frac{1}{2}||\boldsymbol{\theta}||^2 + C\sum_{k=1}^{n} h(x_k,y_k)(y_k - f(x_k))^2
```

かなりガチャついてますが、ひとつずつ確認します。

まずCについてなのですが、前半部分が正則化項でCは後半の損失関数についているので

これが大きくなればなるほど、正則化項は相対的に小さくなるので、汎化する力が小さくなります。

次にepsilonについてなのですが、これは不感帯の広さを調整します。

通常の損失関数であれば、観測値ー実測値がそのまま誤差として蓄積されていきますが

SVRでは、誤差がepsilon以内であれば0として、それ以上なら普通に計算します。

##実際に見てみる
実際に見てみましょう

```python3:svr.py
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVR
from sklearn.model_selection import cross_validate
from sklearn.model_selection import KFold
import pprint

svr = SVR(kernel = "rbf",gamma = 1,C=100,epsilon = 0)
kf = KFold(n_splits=5,shuffle=True,random_state=1)

pprint.pprint(cross_validate(svr,train_z,test_z,scoring="r2",cv=kf))
```

※```train_z```と```test_z```にはボストン住宅価格データが入っています。

パラメータを確認します
kernel = "rbf" -> ガウスカーネルを指定
gamma = 1 -> ガンマ値を1に
C = 100 -> デフォルトが3なので、かなり大きい数字（正則化項の影響が小さくなり、過学習する）
epsilon = 0 -> 誤差に不感度帯を設けない、僅かな誤差でもモデルに影響を与える。

これで実際にクロスバリデーションを実行してみた時の出力は以下の通りです。

```
{'fit_time': array([0.04299998, 0.02099991, 0.01900005, 0.01600003, 0.01699996]),
 'score_time': array([0.00099993, 0.00099993, 0.00200009, 0.00200009, 0.00099993]),
 'test_score': array([0.76426838, 0.62257032, 0.53556972, 0.64250845, 0.61426254]),
 'train_score': array([0.99999994, 0.99999995, 0.99999994, 0.99999995, 0.99999994])}
```

指標の```r2```は最大で1なので、train_score(訓練データに対するスコア）がほぼ1に張り付いており過学習が起きていると推測出来ます。

C/gamma/epsilonの値をデフォルトに変更して同様に計算した結果が以下の通りです。

あと、線形回帰のモデルとの比較もしたいので、LinearRegressionで作ったモデルで同じように出したスコアを併記します。

```
#SVRのスコア
{'fit_time': array([0.02399993, 0.00999975, 0.00800014, 0.00800014, 0.0079999 ]),
 'score_time': array([0.00099993, 0.00200009, 0.00199986, 0.00099993, 0.00099993]),
 'test_score': array([0.86738359, 0.81297562, 0.85346089, 0.75874279, 0.82619329]),
 'train_score': array([0.86226595, 0.8910744 , 0.88008554, 0.89111121, 0.89161245])}

#LinearRegressionのスコア
{'fit_time': array([0.02099991, 0.01600003, 0.00200009, 0.00099993, 0.00300002]),
 'score_time': array([0.00100017, 0.        , 0.00199986, 0.00100017, 0.        ]),
 'test_score': array([0.76348092, 0.64664899, 0.79214918, 0.65064254, 0.73524062]),
 'train_score': array([0.72929397, 0.75821711, 0.72620069, 0.75796688, 0.74083143])}
```

SVRのスコアを見ると訓練データに対す値が大分落着いて、テストデータに対しても良い数字が出ていますね

また、通常の線形回帰モデルと比較しても、ハッキリと良い結果が出ている事が分かります。

大規模なデータでなく、またデータについて深い考察や分析が必要ない場合

お手軽に精度の良い回帰モデルが作れるので非常に便利かと思います。


##次回へ
次回はEnsembleRegressionをやります。

##お願い
機械学習の初心者が、学んだ知識の確認と備忘用に投稿しています。
間違っている部分や、何かお気づきの事がありましたらご指摘頂けますと幸いです
あと、独学者なので「私も今勉強中なんです！」みたいな人がいればコメント貰えると喜びます。

[1]:https://qiita.com/Mukomiz/items/450b6821cf31e6a0dbc6
[2]:https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html