バックエンドエンジニアがscikit-learn入門しました【精度を高めるための前処理編】

# はじめに
機械学習ライブラリのデファクトスタンダード的存在であるscikit-learnの"精度を高めるための前処理"をまとめる。
数学レベルでの理解は途中で挫折したため、まずはライブラリから概要を理解しようと思いまとめました。

教師あり学習編は[こちら](https://qiita.com/gold-kou/items/96f7b6da1fba5d96c12d)
教師なし学習編は[こちら](https://qiita.com/gold-kou/items/2c2a73f5222f35ca1a62)

# 前処理とは
本記事で対象とする"前処理"は、scikit-learnに入力データを食わせるためにデータをNumPy配列(多次元配列)に変換する方法ではなく、NumPy配列データに対して精度を向上させるための処理のことを指す（一部one-hot-encodingは除く）。したがって、本記事でまとめる前処理を実施しなくてもひとまずは機械学習アルゴリズムを動かせる。しかし、実際の業務や研究では精度を求めることは必須なので、そのステージにあれば本記事は役立つかと思います。
前処理は、データの全ての特徴量に実施するのではなく一部だけに実施するのが一般的である。
前処理の手法として以下をまとめます。

- スケール変換
- one-hot-encoding
- ビニング
- 非線形特徴量の追加
- 数学関数による変換
- 自動特徴量選択

# スケール変換
スケール変換とは、データの全ての特徴量がだいたい同じスケール(値の大きさ)になるように変換する手法である。
生データを単にベクトル化してNumPy配列に変換しただけのものでは、特徴量ごとに値の大きさが大きく異なる場合が多い。機械学習アルゴリズムによっては（SVMなど）これが精度に大きく影響を及ぼすため、機械学習にデータを入力する前にスケール変換を行う必要がある。
学習データのみに対してスケール変換器を作成（fit関数実行）する。スケール変換器を作成する際にテストデータは使用してはいけない。
ここでは`StandardScaler`、`RobustScaler`、`MinMaxScaler`、`Normalizer`の4つのスケール変換アルゴリズムをまとめる。全て、パラメータの調整は必要としない。
どのスケール変換アルゴリズムが適切かは機械学習モデルとデータ次第である。例えば、SVMではMinMaxScalerが一般的に使用される。

## StandardScaler
個々の特徴量の平均が0で分散が1になるようにスケール変換するアルゴリズム。
最大値と最小値がある一定の範囲に収まることを保証しない。

### 実行例
```py
# インスタンス生成
ss = StandardScaler()

# 学習データに対してスケール変換器の適合
ss.fit(X_train)

# 学習データとテストデータをスケール変換
X_train_scaled = ss.transform(X_train)
X_test_scaled = ss.transform(X_test)
```

## RobustScaler
StandardSclerと似ているが、外れ値を無視するアルゴリズム。

### 実行例
```py
# インスタンス生成
rs = RobustScaler()

# 学習データに対してスケール変換器の適合
rs.fit(X_train)

# 学習データとテストデータをスケール変換
X_train_scaled = rs.transform(X_train)
X_test_scaled = rs.transform(X_test)
```

## MinMaxScaler
個々の特徴量の値が0から1に収まるようにスケール変換するアルゴリズム。

### 実行例
```py
# インスタンス生成
mms = MinMaxScaler()

# 学習データに対してスケール変換器の適合
mms.fit(X_train)

# 学習データとテストデータをスケール変換
X_train_scaled = mms.transform(X_train)
X_test_scaled = mms.transform(X_test)
```

## Normalizer
各データの特徴量がユークリッド長1になるようにスケール変換するアルゴリズム。
各データにそれぞれ異なるスケール変換処理が実施される。
一般的に、ベクトルの方向だけが問題になる場合に使用される。

### 実行例
```py
# インスタンス生成
n = Normalizer()

# 学習データに対してスケール変換器の適合
n.fit(X_train)

# 学習データとテストデータをスケール変換
X_train_scaled = n.transform(X_train)
X_test_scaled = n.transform(X_test)
```

# one-hot-encoding
one-hot-encodingとは、カテゴリ変数を2値（0か1）の新しい特徴量に置き換える手法である。

## カテゴリー変数の値が文字列の場合
例えば、カテゴリ変数が"sports"で、その値が"baseball"と"basketball"と"tennis"の場合、通常、値が文字列なので、機械学習モデルにこのカテゴリ変数の値を入力として与えることができない。しかし、one-hot-encodingにより、以下のようにカテゴリー変数を数値ベクトルに変換できる。

```py
(sports) = ("baseball") ←one-hot-encoding前
(sports_baseball, sports_basketball, sports_tennis) = (1, 0, 0) ←one-hot-encoding後
```

### 実行例
```py:pandasのget_dummies関数でone-hot-encoding
# one-hot-encoding実行。data中に含まれるカテゴリ変数のみが自動でエンコードされる。
encoded_data = pandas.get_dummies(data)

# DataFrameのvlues属性でNumPy配列に変換
X = encoded_data.values
```

## カテゴリー変数の値が数値の場合
例えば、"baseball"の値が1、"basketball"の値が2、"tennis"の値が3として入力を促されるマークシートのデータなど、カテゴリー変数の値が文字列でなく数値によりエンコードされている場合がある。この場合も、値は数値であるが、カテゴリー変数はone-hot-encodingする必要がある。

```py
(sports) = (1) ←one-hot-encoding前
(sports_baseball, sports_basketball, sports_tennis) = (1, 0, 0) ←one-hot-encoding後
```

get_dummies関数は、カテゴリ変数の値が整数値の場合は使えない。なぜならば、get_dummies関数は整数値を離散値でなく全て連続値として認識してしまうから。
カテゴリー変数の値が整数値の場合は、値を自力で文字列に変換するか、下記のようにscikit-learnのOneHotEncoderを用いる。

### 実行例

```py:OneHotEncoderでone-hot-encoding
# インスタンス生成
ohe=OneHotEncoder(sparse=False)

# データに適合。整数値のバリエーションを確認。
ohe.fit(data)

# one-hot-encoding実行
encoded_data = ohe.transform(data)

# DataFrameのvlues属性でNumPy配列に変換
X = encoded_data.values
```

## one-hot-encodigの注意点
get_dummies関数はカテゴリー変数の値が文字列の場合は処理できるが、整数値にエンコードされたデータを適切に処理できない。
OneHotEncoderはカテゴリー変数の値が整数値の場合のみ処理できるが、文字列の場合は処理できない。（現状の実装では）
学習データとテストデータに分割する前のデータに対してone-hot-encodingを実行する必要がある。あるいは、学習データとテストデータのそれぞれにone-hot-encodingを実行した後に学習データとテストデータがそれぞれ同じ特徴量を持っているかを確認する必要がある。なぜならば、学習データには存在するがテストデータには存在しないカテゴリが存在すると適切な処理ができないため。

生データは表記揺れやスペルミスなどのミスがありえるため、データを疑う必要がある。one-hot-encodingを正確に完了するためには、それらの前処理も地味だが必要である。

# ビニング
ビニングとは、ある特徴量を複数の値の範囲(ビン)に分割した複数の特徴量に変換する手法である。
scikit-learnの機械学習モデルに入力させるためには、ビニング処理したものはone-hot-encodingする必要があるため、ビニングはone-hot-encodingとセットで考える必要がある。
離散値なのでone-hot-encodingはOneHotEncoderを使用する。
線形モデルとナイーブベイズに有効である。
ビニングを実行するためにNumPyで関数が用意されている。

## 実行例
```py
# ビンの定義。ここでは値を-5~5の範囲で10等分。
bins = np.linspace(-5, 5, 11)

# ビニング実行
binned_data = np.digitize(data, bins=bins)

# ここからone-hot-encoding
# インスタンス生成
ohe=OneHotEncoder(sparse=False)

# データに適合。整数値のバリエーションを確認。
ohe.fit(binned_data)

# one-hot-encoding実行
encoded_data = ohe.transform(binned_data)

# DataFrameのvlues属性でNumPy配列に変換
X = encoded_data.values
```

# 非線形特徴量の追加
交互作用特徴量(各特徴量の値の掛け合わせなど)や多項式などの"非線形特徴量"を基の特徴量に追加することで特徴量表現を豊かにする方法がある。
線形モデルとナイーブベイズに有効である。

## 実行例
```py
# インスタンス生成。今回は多項式の次数は5で、5つの値をお互いに掛け合わせた値を特徴量に追加する。
pf = PolynomialFeatures(degree=5)

# 学習データへの適合
pf.fit(X_train)

# 学習データとテストデータに多項式特徴量と交互作用特徴量の追加
X_train_poly = pf.transform(X_train)
X_test_poly = pf.transform(X_test)
```

## 主なパラメータ
- degree : 多項式の次数と基の特徴量をいくつ交互に掛け合わせるか。

# 数学関数による変換
特徴量の値がポアソン分布に従うような場合、データをlog関数やexp関数やsin関数やcos関数などで変換させて、ガウス分布に近づけさせれば、精度が上がる場合がある。
線形モデルとナイーブベイズに有効である。
数学関数はNumPyで用意されている。

## 実行例
```py
# 学習データとテストデータに対数関数を適用。対数は0に対して実行できないので、X+1を取る。
X_train_log = np.log(X_train + 1)
X_test_log = np.log(X_test + 1)
```

# 自動特徴量選択
重要な特徴量のみを抽出する方法。
特徴量の数が多すぎるとoverfitingやノイズにより精度の低下の恐れがある(いわゆる次元の呪い)。
自動特徴量選択は学習データのみに対して実行する。
実データで精度が大幅に向上することは稀だが、試す価値はある。
`単変量統計`と`モデルベース特徴量選択`と`反復特徴量選択`の3つの手法をここではまとめる。

## 単変量統計
単変量統計は、各特徴量とターゲットの間で高い確信度で関連している特徴量を選択する手法である。
特徴量を個別に考慮するので、他の特徴量と組み合わさって意味を持つような特徴量は捨てられる。
高速で動作する。
p-値(特徴量とターゲットとの関連性の低さを示す指標)が高い特徴量を捨てる際の閾値をパラメータで指定できる。

### 実行例
```py
# インスタンス生成
sp = SelectPercentile(percentile=50)

# 学習データに対して特徴量選択
sp.fit(X_train, Y_train)

# 次元数削減
X_train_selected = sp.transform(X_train)
X_test_selected = sp.transform(X_test)
```

### 主なパラメータ
- percentile : 残す特徴量の割合

## モデルベース特徴量選択
モデルベース特徴量選択は、決定木ベースモデルやL1正則化を用いた線形モデルなどの自動特徴量選択部分のアルゴリズムを利用する手法である。
特徴量選択に用いる教師あり機械学習モデルとタスクに用いる機械学習アルゴリズムは同じものである必要はない。
単変量統計とは異なり、他の特徴量と組み合わさって意味を持つような特徴量も無視しない。

### 実行例
```py
# インスタンス生成
sfm = SelectFromModel(
RandomForestClassifier(n_estimators=100, random_state=42), 
threshold = "median")

# 学習データに対して特徴量選択
sfm.fit(X_train, y_train)

# 次元数削減
X_train_selected = sfm.transform(X_train)
X_test_selected = sfm.transform(X_test)
```

### 主なパラメータ
- 第一引数 : 特徴量選択に用いる教師あり機械学習モデルのインスタンス。
- threshold : 閾値。"median"で半分。

## 反復特徴量選択
反復特徴量選択は、すべての特徴量を使う状態からあらかじめ定めた数まで1つずつ重要度が低い特徴量を減らしていく方法。
1つずつ減らす度に機械学習モデルを作り直すことになるので実行時間がかかる。
モデルベース特徴量選択と同様に特徴量選択に用いる教師ありモデルのインスタンスを指定する。

### 実行例
```py
# インスタンス生成
rfe = RFE(
RandomForestClassifier(n_estimators=100, random_state=42), 
n_features_to_select = 10)

# 学習データに対して特徴量選択
rfe.fit(X_train, Y_train)

# 次元数削減
X_train_selected = rfe.transform(X_train)
X_test_selected = rfe.transform(X_test)
```

### 主なパラメータ
- 第一引数 : 特徴量選択に用いる教師あり機械学習モデルのインスタンス。
- n_features_to_select : いくつまで特徴量を減らすか。

## 教師なし学習の次元削減による自動特徴量選択
モデルベース特徴量選択や反復特徴量選択のような教師あり学習モデルの機構を利用した方法があるように、教師なし学習モデルを利用した方法（次元削減）もある。
教師なし学習は[別記事](https://qiita.com/gold-kou/items/2c2a73f5222f35ca1a62#%E6%AC%A1%E5%85%83%E5%89%8A%E6%B8%9B)にまとめました。

# 前処理に関する俺的（初心者的）結論
- とりあえずスケール変換すべし。どのスケール変換手法が適切かは機械学習アルゴリズムに大きく依存するので、そこはぐぐる。
- one-hot-encodingとビニングと非線形特徴量の追加と数学関数による変換はデータによって必要そうなものがあればやる。まずはそこらへんは考えずにやってもいいかも。
- 特徴量の数が多すぎる場合は数を減らすために自動特徴量選択を試してみる。