Python初心者の備忘録 #16 ~機械学習入門編02~

# はじめに
今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました！  
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。  
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。  
この記事が少しでも誰かの糧になることを願っております！
**※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。**
最初の記事：[Python初心者の備忘録 #01](https://qiita.com/Yushin-Tati/items/961dc47d6163f944f7e9)
前の記事：[Python初心者の備忘録 #15 ~機械学習入門編01~](https://qiita.com/Yushin-Tati/items/c8a9ad09ae9ff1778ca1)
次の記事：[Python初心者の備忘録 #17 ~機械学習入門編03~](https://qiita.com/Yushin-Tati/items/662d56690fadeeeb1b86)

今回は**汎化性能と過学習**や**回帰モデルの評価指標**についてまとめております。
## ■学習に使用している資料
Udemy：[【前編】米国データサイエンティストがやさしく教える機械学習超入門【Pythonで実践】
](https://www.udemy.com/course/mlpython-1/)
## ■汎化性能と過学習
- 機械学習は「学習したら終わり」ではなく、正しく精度を測り、未知のデータに対して最も精度が高いモデルを採用する必要がある

汎化性能：未知のデータに対して正しく予測できる性能
過学習：学習に使用したデータにフィットしすぎて汎化性能が低くなること
→ **過学習を避け、汎化性能を高くすることが大事**
#### 汎化性能の測り方
- 学習に使うデータ(学習データ)と評価に使うデータ(テストデータ)を区別する
→ 学習データでモデルの学習を行い、その制度をテストデータで測る
\- hold-out
\- LOOCV
\- k-Fold CV  など
### ▶hold-out
- 手元にあるデータをランダムに学習データとテストデータに分割する
- 7：3や5：5に分けるのが一般的
- 学習データではなくテストデータで精度(汎化性能)の高いモデルを選択する必要がある
#### 検証データ(Validation data)
- 汎化性能を高めることだけを目的としてしまうとテストデータに対して過学習してしまう
- 上記を避けるためにハイパーパラメータ調整(チューニング)用のもう一つのデータセットを用意する必要がある
- 学習データで学習→検証データで繰り返し調整→最終的にテストデータで評価

:::note info
**ハイパーパラメータとは**
学習の結果割り出されるパラメータ$\theta$とは異なり、学習前にこちら側で決め打ちで設定する値$\alpha$のこと
:::
### ▶Pythonでhold-out
#### hold-out
- `sklearn.model_selection.train_test_split`モジュールを使用する
- `train_test_split(X, y)`でそれぞれの学習データとテストデータを返してくれる
`X_train, X_test, y_train, y_test=train_test_split(X, y)`で受け取るのが通例
- `test_size=xx`でテストデータの割合を指定することができる(デフォルトで0.25)
#### MSE
- 回帰モデルの評価指標の代表例(残差の二乗の平均値)
- `sklearn.metrics.mean_squared_error`モジュールを使用する
- `mean_squared_error(y_true, y_pred)`のように正解値と予測値とのリストを渡す
`y_true`：正解値、`y_pred`：予測値

```python
# import
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.model_selection import train_test_split

# データ取得
df = sns.load_dataset('tips')

y_col = 'tip'
X = df.drop(columns=[y_col])
y = df[y_col]
# 標準化のために数値カラムのリストを取得
numeric_cols = X.select_dtypes(include=np.number).columns.to_list()

# カテゴリ変数のダミー変数作成
X = pd.get_dummies(X, drop_first=True)

# hold-out(train_sizeは0.3が一般的、random_stateはシード値の固定)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
```
```python
# 標準化
# 標準化は，データ分割の後に実施する
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = X_train.copy()

# 数値カラムのみ標準化
X_train_scaled[numeric_cols] = scaler.fit_transform(X_train[numeric_cols])
X_test_scaled = X_test.copy()
# testデータでは学習データで.fitしたモデルで標準化を行う
X_test_scaled[numeric_cols] = scaler.transform(X_test[numeric_cols])
```
:::note info
**なぜテストデータは標準化の際に.fitしないのか**
[特徴量スケーリングの注意点](https://qiita.com/Yushin-Tati/items/c8a9ad09ae9ff1778ca1#%E7%89%B9%E5%BE%B4%E9%87%8F%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9)参照
:::
```python
# 線形回帰モデル学習
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
```
```python
# モデルの評価 (MSE)
from sklearn.metrics import mean_squared_error
mean_squared_error(y_test, y_pred) # np.mean(np.square(y_test - y_pred))と同じ
# -> 0.955080898861715
```
上記の結果から、作成した線形回帰モデルは平均して1のズレで予測可能ということがわかる。
もともとの数値が小さいので何とも言えないが、上記が一般的な汎化性能の検証の流れとなる。
### ▶LOOCV(Leave-One-Out Cross Validation)
- 1つのデータをテストデータ、その他を学習データとして学習し、そのすべてがテストデータとして扱われるまで繰り返し、平均を取る方法(交差検証：Cross Validation)

![Cross_Validation.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/c4365ed4-6366-9ed0-3e2c-48ced30b018f.jpeg)
利点：ランダム性がなく(常に同じ結果となる)、ほぼすべてのデータを学習に使用することができる
欠点：非常にコストがかかる
### ▶PythonでLOOCV
#### LOO
- `sklearn.model_selection.LeaveOneOut`を使用する
1．インスタンス生成
2．`.split(X)`メソッドでイテレーション → train_indexとtest_indexを生成
```python
# import
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import LeaveOneOut

# データ準備
X = df['total_bill'].values.reshape(-1, 1)
y = df['tip']

# インスタンス生成
loo = LeaveOneOut()
model = LinearRegression()

mse_list = []

# LOOの実施
for train_index, test_index in loo.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    
    # モデル学習
    model.fit(X_train, y_train)
    # テストデータの予測
    y_pred = model.predict(X_test)
    # MSE
    mse = mean_squared_error(y_test, y_pred)
    mse_list.append(mse)
    
print(f"MSE(LOOCV):{np.mean(mse_list)}") # -> MSE(LOOCV):1.0675673489857438
print(f"std:{np.std(mse_list)}") # -> std:2.0997944551776313
```
:::note warn
**実際は下記メソッドで一発で求められるのでほとんど使用しない**
:::
#### Cross Validation
- `sklearn.model_selection.cross_val_score`を使用する
\- `cross_val_score(model, X, y, cv=cv)`で一発で交差検証(CV)を行ってくれる
\- `cv`にはLOOなどのcvオブジェクトを渡す
\- `n_jobs`には使用するCPUのコア数を指定する
\- `scoring`には評価指数を指定
```python
# cross_val_score関数を使えば簡単にCVを実行可能
from sklearn.model_selection import cross_val_score
cv = LeaveOneOut()
scores = cross_val_score(model, X, y, cv=cv, scoring='neg_mean_squared_error')

print(f"MSE(LOOCV):{-np.mean(scores)}") # -> MSE(LOOCV):1.0675673489857438
print(f"std:{np.std(scores)}") # -> std:2.0997944551776313
```
### ▶k-Fold CV
- データをk個に分割して、交差検証を行う方法(k=5やk=10が一般的)
- LOOCVよりコストが低く、最もよく使われる評価手法
### ▶Pythonでk-Fold CV
#### k-Fold CV
- `sklearn.model_selection.KFopd`クラスを使用する
1．`KFold(n_splits=5, shuffle=False)`インスタンスを生成
※`n_splits`：分割する個数の指定、`shuffle`：分割する前にデータをシャッフルするかどうか
2．`.split(X)`メソッドでイテレーション -> `train_index`と`test_index`を生成
- `cross_val_score()`を使って一発で実行可能
```python
# import
from sklearn.model_selection import KFold, RepeatedKFold

k = 5
cv = KFold(n_splits=k, shuffle=True, random_state=0)
model = LinearRegression()
mse_list = []

# 評価
for train_index, test_index in cv.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    # 標準化(今回は特徴量が1種なので必要なし)
    # モデル学習
    model.fit(X_train, y_train)
    # テストデータ予測
    y_pred = model.predict(X_test)
    # MSE
    mse = mean_squared_error(y_test, y_pred)
    mse_list.append(mse)
```
```python
print(f"MSE({k}FoldCV): {np.mean(mse_list)}") # -> MSE(5FoldCV): 1.080211088394392
print(f"std: {np.std(mse_list)}") # -> std: 0.1617010050703952
```
```python
# 同様にcross_val_scoreで簡単に実施可能
scores = cross_val_score(model, X, y, cv=cv, scoring='neg_mean_squared_error')
print(f"MSE({k}FoldCV): {-np.mean(scores)}") # -> MSE(5FoldCV): 1.080211088394392
print(f"std: {np.std(scores)}") # -> std: 0.1617010050703952
```
#### Repeated k-Fold CV
- k-Foldを複数回実行する
- `sklearn.model_selection.RepeatedKFold`クラスを使用
- `n_repeats`引数で回数を指定
```python
k = 5
n_repeats = 3
# k-Foldを複数回実行
cv = RepeatedKFold(n_splits=k, n_repeats=n_repeats, random_state=0)
model = LinearRegression()
mse_list = []

# 評価
for train_index, test_index in cv.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    # 標準化
    # モデル学習
    model.fit(X_train, y_train)
    # テストデータ予測
    y_pred = model.predict(X_test)
    # MSE
    mse = mean_squared_error(y_test, y_pred)
    mse_list.append(mse)

print(f"MSE({k}FoldCV): {-np.mean(scores)}") # -> MSE(5FoldCV): 1.0746387233165984
print(f"std: {np.std(scores)}") # -> std: 0.26517178540898434
```
### ▶Pipeline(k-Fold + 標準化)
- Pipelineオブジェクトを使用することで、複数の処理をまとめることが可能
→ 標準化とCVの処理をまとめることで、都度特徴量スケーリングを行うことができる
:::note info
**CVでは毎回学習データが変わるので、その都度特徴量スケーリングを行う必要がある**
:::
#### pipeline
- `sklearn.pipeline.Pipeline`クラスを使用する
\- インスタンスを生成し、modelオブジェクトやStandardScalerのように`.fit`や`.predict`、`.transform`メソッドを利用できる
\- `steps`引数に処理をタプルのリストで指定する
→`[('処理名1', クラス1()), ('処理名2', クラス2()), ...]`の形で記述
※それぞれのクラスは`.fit`などのメソッドを持っている必要がある
- `pipeline`オブジェクトをmodelのように扱うことも可能
→ `cross_val_scoring()`にmodelの代わりにpipelineを入れるなど
```python
# import
from sklearn.pipeline import Pipeline

# pipelineに標準化とMSEのmodelを追加
pipeline = Pipeline(steps=[('scaler', StandardScaler()), ('model', LinearRegression())])

# k-Fold + 評価
cv = KFold(n_splits=5, shuffle=True, random_state=0)
# modelの代わりにpipelineを指定
scores = cross_val_score(pipeline, X, y, scoring='neg_mean_squared_error', cv=cv)
# -> array([-0.82130906, -1.07458421, -1.08801239, -1.33238677, -1.084763  ])
```
#### Pipeline有り、無しの比較
```python
## Pipelineなし
# 標準化 + 線形回帰
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
model = LinearRegression()
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
```
```python
## Pipelineあり
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
pipeline = Pipeline(steps=[('scaler', StandardScaler()), ('model', LinearRegression())])
pipeline.fit(X_train, y_train)
y_pred_p = pipeline.predict(X_test)
```
```python
# どちらも同じ結果になる
(y_pred_p == y_pred).all() # -> True
```
### ▶Bias-Variance Tradeoff
- モデルを複雑にしすぎると、学習データにfitしすぎる(過学習しやすい)
- モデルが単純すぎると精度が出ない
→ 上記二つのちょうど間のモデルを構築することが重要

![bias-variance.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/a9c89bad-7c8c-87ad-32e3-45dd21762358.jpeg)
→赤だと単純すぎるし、緑だと汎化性能が低い、青のようなモデルが最適
※左図は学習後のモデル、右図のx軸の▲は左図のモデルの色と対応している
#### Bias(偏り)
- 簡略化したことによる誤差

![bias.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/7f8c484f-2dc1-f83f-7151-c3a28cbad35d.jpeg)
#### Variance(分散)
- $\hat{f}(x)$の分散(ばらつき)、異なる学習データで学習した際の$\hat{f}(x)$の変動

![variance.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/cfa5cb75-e6af-c14d-8c30-cec10aece252.jpeg)
- 機械学習には「削減可能な誤差」と「削減不可な誤差」があり、「削減可能な誤差」を小さくするにはBiasとVarianceの両方を小さくする必要がある
#### Biasが原因か、Varianceが原因か
- test errorもtrain errorも高い
→ Biasの問題：次元をあげたり特徴量を増やす、データを増やしても効果が薄い
- test errorは高いがtrain errorは低い
→ Varianceの問題：次元を下げたり特徴量を減らす、データを増やすことで解決することもある
### ▶LOOCVとk-Fold CVのBiasとVariance
- LOOCVはbiasはほとんどないが、k-Fold CVよりもvarianceが高い
#### LOOCV
- m-1個のデータを使用しているので、ほぼ求めているモデルと同じ -> low bias
- それぞれのモデルが非常に高い相関がある(テストデータが1個しかないので、使用している学習データはほぼ同じ)ので、平均をとってもあまり均せていない -> high variance
#### k-Fold CV
- m(k-1)/k個のデータを学習に使っているので、モデルを簡略化している(kが小さいとより) -> high bias
- モデル毎に差ができ(kが小さいとより)、その平均を取ることで均すことができる -> low variance

:::note warn
**CV内でのモデルのbias/varianceではなく、プロセス全体でのbias/varianceであることに注意**
:::
- k=5もあればhigh bias、high varianceを回避することができる
- 特にk-Fold CVを繰り返すRepeated f-Fold CVが良いとされている
## ■回帰モデルの評価指標
### ▶MSEとRMSEとMAE
#### MSE(Mean Squared Error)
- 最もよく使われる評価指標で、残価の二乗の平均
- 二乗するので単位(尺度)がわかりにくいのがデメリット
#### RMSE(Root Mean Squared Error)
- MSEの平方根で、目的変数の単位(尺度)と一致するので判断しやすい
#### MAE(Mean Absolute Error)
- シンプルに残差の絶対値の平均
- 直感的でわかりやすく、よく使用される
### ▶PythonでMSEとRMSEとMAE
#### MSEおよびRMSE
- `sklean.metrics.mean_squared_error`クラスを使用
- `mean_squared_error(y_true, y_pred)`でMSEを求められる
→ `squared`引数にFalseを渡すことでRMSEを返す
#### MAE
- `sklean.metrics.mean_absolute_error`クラスを使用
- 使い方は`mean_absolute_error(y_true, y_pred)`
```python
# データの準備
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
import seaborn as sns
# データの準備
df = sns.load_dataset('tips')
X = df['total_bill'].values.reshape(-1, 1)
y = df['tip'].values

# hold-out
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

# train
model = LinearRegression()
model.fit(X_train, y_train)

# predict
y_pred = model.predict(X_test)

# MSE
from sklearn.metrics import mean_squared_error
mean_squared_error(y_test, y_pred) # -> 0.871184553753995

# RMSE
mean_squared_error(y_test, y_pred, squared=False) # -> 0.9333726767770711

# MAE
from sklearn.metrics import mean_absolute_error
mean_absolute_error(y_test, y_pred) # -> 0.6903119067790222
```
### ▶R-Squared(決定係数)
- $R^2$：統計学では決定係数とも呼ばれる
- データに対して回帰の当てはまりの良さを0~1で表す
- 特徴量がどれだけ目的変数の値を説明(決定)しているかを表す
-> 大体0.8以上だと高い(当てはまりがよい)とされている

![R-squared_graph.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/dd6a8db9-2b80-4253-aeea-fb34b6ec19d6.jpeg)
#### 例：身長と体重
- 体重が重ければその分身長も高くなる(身長と体重に相関がある)
-> 体重によって身長の高さをある程度"説明"できそう
- 体重に関係なく、身長は同じ(身長と体重に相関なし)
-> 体重の大小では身長の説明はできない(決定係数0)

$\frac{1}{m}\sum_{i=1}^m(y_i-\bar{y})^2$ <- この一部はきっとxの差異によるもの(その割合が**決定係数**)
#### 決定係数の求め方
- 「xによって説明できるyの分散」=「yの分散」-「xによって説明できないyの分散」

![R-squared_culcurator.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/1cd3ca44-1911-d2a3-540a-9d0cf8fedd35.jpeg)
![R-squared_RSS_and_TSSjpg.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3448232/0aead254-7103-5a32-d7bb-555ea57e6b43.jpeg)
### ▶PythonでR-squared
```python
# R-Squared
from sklearn.metrics import r2_score
r2_score(y_test, y_pred) # -> 0.49515102188632765
```
```python
# 特徴量がひとつの場合は，学習データR-Squaredは相関rの二乗になる
df.corr()
```
||total_bill|tip|size|
|-:|:-:|:-:|:-:|
|total_bill|1.000000|0.675734|0.598315|
|tip|0.675734|1.000000|0.489299|
|size|0.598315|0.489299|1.000000|
### ▶補足：adjusted R-Squared(調整済みR²)
:::note info
機械学習の回帰モデルの評価指標というよりかは統計学の分析でよく使用されるので、**あくまで補足**
:::
- 特徴量の寄与率(目的変数をどれだけ説明しているか)を確認するケースでは学習データに対しての$R^2$を見ることがある
- $R^2$は特徴量を追加すると1に近づく性質がある(※学習データに対してのみ)
→ 特徴量を追加すると$\hat{y_i}$が$y_i$に近づき、$\sum_{i=1}^m(y_i-\hat{y_i})^2$が小さくなるため

★$adjustedR^2=1-\frac{\frac{RSS}{m-n-1}}{\frac{TSS}{m-1}}$ (m：データ数、n：特徴量数)
#### 次の記事
[Python初心者の備忘録 #17 ~機械学習入門編03~](https://qiita.com/Yushin-Tati/items/662d56690fadeeeb1b86)