グリッドサーチでPCAをするか決める

## 環境
- Windows 10
- Python 3.7.1
- scikit-learn 0.20.0

## この記事でやろうとしていること
機械学習においてメジャーな次元削減法に、PCAがあります。
例えば、PCAで次元を減らしてからSVR（Support Vector Machine Regression）にかける、なんて使い方をするかと思います。

この場合、PCAのパラメーター（成分数: n_componentsとか）とSVRのパラメーター（CとかEpsilonとか）を決める必要があります。
scikit-learnの`Pipeline`と`GridSearchCV`を使えば、これらのパラメーターをいっぺんにグリッドサーチしてくれます。

しかし、`GridSearchCV`が検証してくれるのは、全パラメーターを組み合わせたパターンのみです。
**PCAをかけないパターン**も含めていっぺんに検証してほしいのです。

例えば、こんな例を見てください。

### データ準備
例えば、ボストンの住宅価格のデータを使って回帰予測をします。

```python
from sklearn.datasets import load_boston
housing = load_boston()
```
データの内容については、[このあたりの記事](https://momonoki2017.blogspot.com/2018/01/scikit-learn_28.html)をご覧ください。
`train_test_split`でトレーニングデータとテストデータに分けます。

```python
from sklearn.model_selction import train_test_split
X_train, X_test, y_train, y_test = train_test_split(housing.data, housing.target,
                                                    train_size=0.75, test_size=0.25)
```

### 例1) PCA -> SVRのPipelineで解析する
まず、Pipelineに含めるEstimatorを設定します。

```python
from sklearn.svm import SVR
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
pl1 = Pipeline([['pca', PCA(random_state=2018)],
                ['svr', SVR(kernel='linear')]])
```

続いて、パラメーターの候補を与えます。

```python
prms1 = {'pca__n_components': [0.1, 0.5, 0.9],
         'svr__C': [0.1, 0.5, 1.],
         'svr__epsilon': [0.05, 0.10, 0.20]}
```

PipelineとパラメーターをGrid Searchにかけます。

```python
from sklearn.model_selection import GridSearchCV
gs1 = GridSearchCV(pl1, prms1, n_jobs=-1, return_train_score=True, cv=5, verbose=10)
gs1.fit(X_train, y_train)
```

テストデータで精度を検証します。
![スクリーンショット (30).png](https://qiita-image-store.s3.amazonaws.com/0/304493/d507c93f-9e27-b59b-f803-fb39bfa91e6c.png)
Pearsonの相関係数で.527。
ふむ、こんなものか。

### 例2) PCAなしでSVRをする
さっきはPCAをSVRに併用して.527の精度を出してましたが、PCAなしだとどうなるでしょう。

```python
prms2 = {'C': [0.1, 0.5, 1.],
        'epsilon': [0.05, 0.10, 0.20]}
gs2 = GridSearchCV(SVR(kernel='linear'), prms2, n_jobs=-1, return_train_score=True, cv=5, verbose=10)
gs2.fit(X_train, y_train)
```
テストデータで精度検証
![スクリーンショット (29).png](https://qiita-image-store.s3.amazonaws.com/0/304493/9099e085-09fe-efd7-29ad-0fcea21cd176.png)
PCA使わない方が精度がいい！

こんなことがあるので、**PCAをかけないパターン**も一緒に検証してほしいのです。

## 都合のいいPCAクラス
``GridSearchCV``は、パラメーターの全組み合わせしか検証してくれません。
逆に、PCA側がパラメーター次第で「なにもせず」データを返してくれればよいのです。
そんな都合のいいPCAクラスを作成します。

### クラスを作る
scikit-learnは、PipelineやGridSearchCVに投げるクラスを自作する方法を用意してくれています。
[このあたりの記事](https://qiita.com/roronya/items/fdf35d4f69ea62e1dd91)を参照しつつ、都合のいいクラスを作ってみました ↓

```python
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.decomposition import PCA
class myPCA(BaseEstimator, TransformerMixin):
    def __init__(self, n_components=0, random_state=None):
        self.n_components = n_components
        self.random_state = random_state

    def fit(self, X, y=None):
        if self.n_components==0:
            return self
        self.pca = PCA(n_components=self.n_components, random_state=self.random_state)
        self.pca.fit(X)
        return self

    def transform(self, X, y=None):
        if self.n_components==0:
            return X        
        return self.pca.transform(X)
```
`n_components`が0の場合は、PCAを行わず、データ`X`をそのまま返すようにしています。
0以外の時は、普通にscikit-learnのPCAをかけて返します。

### クラスをインポートする
この都合のいいクラス`myPCA`を、別の`.py`ファイルに書いて保存し、`import`で呼び出します。
こうしないと、並列演算をしたときにエラーが出ます。

```python
from mypca import myPCA
```

### クラスを試す
先ほどの、例1と同じ設定で`myPCA`を試してみましょう。

```python
prms3 = prms1
pl3 = Pipeline([['pca', myPCA(random_state=2018)],
                ['svr', SVR(kernel='linear')]])
gs3 = GridSearchCV(pl3, prms3, n_jobs=-1, return_train_score=True, cv=5, verbose=10)
gs3.fit(X_train, y_train)
```
![スクリーンショット (32).png](https://qiita-image-store.s3.amazonaws.com/0/304493/b1900ea5-153b-73ab-798b-173a1d35f28a.png)
ちゃんとさっきと同じ結果が出ました。

では、`n_components`の候補に0を加えてやってみます。

```python
prms4 = {'pca__n_components': [0, 0.1, 0.5, 0.9],
         'svr__C': [0.1, 0.5, 1.],
         'svr__epsilon': [0.05, 0.10, 0.20]}
pl4 = pl3
gs4 = GridSearchCV(pl4, prms4, n_jobs=-1, return_train_score=True, cv=5, verbose=10)
gs4.fit(X_train, y_train)
```

![スクリーンショット (33).png](https://qiita-image-store.s3.amazonaws.com/0/304493/70cc49c7-3ba5-6432-c721-d04e629016df.png)

例2（PCAなしでSVRのみ）の時と同じ結果が出ました。
PCAを使わないパターンも試して、最良の組み合わせを返してくれたわけです。

## まとめ
自分でクラス関数を作ることによって、「そのEstimatorを使わないパターン」も含めて`GridSearchCV`が検証してくれました。
今回はPCAでやりましたが、ほかの前処理についても同じように適用できるのではないかと思います。

**注）**
この記事には色々と間違いがあるかもしれないので、何かに使う場合はご注意ください。
もっと楽にやる方法があったら教えてください。
　
[今回使ったコードのipynbファイル](https://issakuss.github.io/tech/article_7/conveni_pca.ipynb)