TPOTで自動機械学習（回帰）を試した

## 更新履歴
- 2018年12月17日　[もっとTPOTで自動機械学習を試した](https://qiita.com/issakuss/items/d8fd83f204becaea4cb3)という記事を書いたので、それに関する追記をしました。
- 2018年12月14日　「TPOTでは特徴選択までは自動化してくれない」なんて大嘘をついていたのを修正しました。

## この記事の目的

[その機械学習プロセス、自動化できませんか？](https://qiita.com/Hironsan/items/30fe09c85da8a28ebd63)という記事を読みました。
機械学習モデルの選定やパラメータチューニングなどを自動で行おうという試みがあるそうです。
この記事の中で、`TPOT`というパッケージが紹介されていました。
遺伝的プログラミングによって、機械学習を最適化してくれるもののようです。

さっそくTPOTで回帰学習を試したのですが、ちょっと躓いたりもしたので、記録を残します。

[TPOTのHP](https://epistasislab.github.io/tpot/)に沿ってやっていきます。

## 環境
- Windows 10
- Python 3.7.2 (64bit)
- TPOT 0.9.5

## 下準備
### パッケージのインストール
[HP](https://epistasislab.github.io/tpot/installing/)を見ながら。

いくつか必要なパッケージがあるようです。
`pip`でサクッとインストールします。
最後の`pywin32`はWindows環境のみ必要です。

```console
pip install numpy scipy scikit-learn pandas deap update_checker tqdm stopit pywin32
```

さらに、`xgboost`というパッケージを入れると、[XGBoost](https://github.com/dmlc/xgboost)というものに対応させることもできるとか。
これは、Distributed Gradient Boostingというアルゴリズムを提供するものです。
いわゆるアンサンブル学習の一種ですね。
Windows未対応のようなので、今回は見送ります。
他にも、`dask`だとか`scikit-mdr`とか`skrebate`とかを入れてもいいよとありますが、見送ります。
そして、満を持して

```console
pip install tpot
```
### データの準備
回帰学習を試してみたいです。
[HPのExample](https://epistasislab.github.io/tpot/examples/#boston-housing-prices-modeling)を見ると、ボストンの住宅価格のデータで試してますね。
これでやってみます。

```python
from sklearn.datasets import load_boston
housing = load_boston()
```

これで、住宅価格のデータセットがロードされました。
データの内容については、[このあたりの記事](https://momonoki2017.blogspot.com/2018/01/scikit-learn_28.html)をご覧ください。
housing.dataには、地域の犯罪発生率や大きな道路へのアクセスしやすさなどが入っています。
housing.targetには、教師信号となる住宅価格があります。

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,
                                                    random_state=2018)
```

`random_state`を設定して、結果が再現されるようにします。

## TPOT回帰を実践
### モジュールのインポート
まず、モジュールをimportします

```python
from tpot import TPOTRegressor
```
…が、エラーが出てしまいました。

```console
ImportWarning: Falling back to the python version of hypervolume module. Expect this to be very slow.
  "module. Expect this to be very slow.", ImportWarning)
```
ググってみるといくつかディスカッションがされており、deapの？コンパイルの？問題らしいです。

[こちら](https://github.com/EpistasisLab/tpot/issues/670)で、古いバージョンのdeapを入れてみろとありましたのでやってみます
（今のバージョンは1.2.2です）。

```console
pip uninstall deap
pip install deap==1.0.2.post2
```

あらためて試したところ、エラーがなくなりました！

### 引数を入れてインスタンス生成
tpotは、sklearnと同じように使えます。

まず、tpotのインスタンスを作ります。

```python
from tpot import TPOTRegressor
tpot = TPOTRegressor(generations=5, population_size=50, verbosity=2)
```

関数の詳しい使い方は、[API Document](https://epistasislab.github.io/tpot/api/)にあります。
が、理解するには遺伝的プログラミングについての基礎知識が必要そうですね…。
遺伝的プログラミングは遺伝的アルゴリズムを拡張したものだそうです。
遺伝的アルゴリズムについては、[このページ](http://www.sist.ac.jp/~kanakubo/research/evolutionary_computing/genetic_algorithms.html)を見たらなんとなくわかったような気になりました。

`generations`は、パイプライン最適化の繰り返し回数だそうです。
遺伝的アルゴリズムにおける「世代」ですね。
`population size`は、[上記ページ](http://www.sist.ac.jp/~kanakubo/research/evolutionary_computing/genetic_algorithms.html)における個体群のことかと思います。
`verbosity`は、進捗報告のためのオプションです。

- 0: なにもprintしない
- 1: 最低限の情報をprintする
- 2: プログレスバーなんかも表示する
- 3: なんでもかんでもprintする


### フィッティング実行
後は、このインスタンスにデータを与えてフィッティングするだけ。
せっかくなので実行時間を`time`で測ります。

```python
import time
t0 = time.time()
tpot.fit(X_train, y_train)
print(time.time() - t0)
```
実行。
![スクリーンショット (21).png](https://qiita-image-store.s3.amazonaws.com/0/304493/eea00c32-2155-9262-b01f-06b3193a52ea.png)
進捗状況がtqdmで表示されます。
私の環境では、126秒で終わりました。

### テストデータに適用
スコアを見てみます。
**注）**再現性はありません（再現性を持たせる方法は[こちら](https://qiita.com/issakuss/items/d8fd83f204becaea4cb3)）。
![スクリーンショット (36).png](https://qiita-image-store.s3.amazonaws.com/0/304493/6a407bb9-ac1f-d1dc-4c7d-a77957ba212c.png)
MSEだそうです。
平均**二乗**誤差なのに負数？とも思いますが、scikit-learnの仕様上、そういうもののようです（[参照](https://stackoverflow.com/questions/21050110/sklearn-gridsearchcv-with-pipeline)）。

`.fitted_pipeline_`で、最終的に吐き出されたpipelineの情報を返してくれます。
![スクリーンショット (37).png](https://qiita-image-store.s3.amazonaws.com/0/304493/a22c3dc9-0416-cd35-374e-2c206f6a8e40.png)

`.evaluated_individuals_`で、検討したモデルについての情報を返してくれます。
![スクリーンショット (38).png](https://qiita-image-store.s3.amazonaws.com/0/304493/ade2be17-4001-f81d-e975-86ec6c3c4f1f.png)

テストデータに適用して精度を見てみましょう。
精度は、わかりやすくPearsonの相関係数で見ます。
フィットされたpipelineから予測を行う場合は、`.predict`を使います。
![スクリーンショット (35).png](https://qiita-image-store.s3.amazonaws.com/0/304493/41d6a961-3f49-d72f-87f0-15a5d4a5a09c.png)
えげつない精度です。


決定係数R<sup>2</sup>も見てみましょう。
![スクリーンショット (39).png](https://qiita-image-store.s3.amazonaws.com/0/304493/e6879672-a9c7-47a7-5d54-b62257d21701.png)
ちなみに、[同じデータを使った記事](https://momonoki2017.blogspot.com/2018/01/scikit-learn_28.html)では、重回帰分析によりR<sup>2</sup>=.635を示しています。

## 引数を変えて試す
先ほどの例では、公式ドキュメントのExampleにならい、`generations=5`, `population_size=50`でやっていました。
しかし、デフォルトの値は`generations=100`, `population_size=100`だったようです。
簡易的に試したということですね。

デフォルトのままの設定で動かしてみようと思います。
また、`n_jobs`を設定することで、並列演算してくれるようなので、これも試します。

### 並列演算してみる
`generations`と`populations`は先ほどと同じで、`n_jobs=-1`にして動かしてみます。
-1を設定すると、CPUのコアを最大限フル活動して並列演算してくれます。

```python
tpot = TPOTRegressor(generations=5, population_size=50, verbosity=2, n_jobs=-1)
t0 = time.time()
tpot.fit(X_train, y_train)
print(time.time() - t0)
```
先ほどは126秒かかってたのが、45秒で済みました。
計算量が多くなれば、さらに効果が期待されそうですね。

### 世代と人口を増やしてみる
というわけで、`generations`と`populations`をデフォルトでやってみました。
結果…
![スクリーンショット (41).png](https://qiita-image-store.s3.amazonaws.com/0/304493/6fdbba62-b4a5-1a9b-ffe8-c65deef1807b.png)
精度が上がってますね。
もともと高かったので、それほどの改善ではありませんが。

世代数を増やしたことが改善の主な要因かと思われます。
ではいったいどのくらいの世代数が求められるものでしょうか。
![スクリーンショット (40).png](https://qiita-image-store.s3.amazonaws.com/0/304493/2861f019-2212-4a7b-b24b-32ab357791f7.png)
`verbosity`の設定により、世代ごとのスコアが出力されています。
こいつをプロットしてみましょう。

…と思ったのですが、世代ごとのCVを取得する方法が見つかりませんでした。
仕方ないので、上の出力をエクセルにコピペするというダッセェ方法でプロットしてみました
（[こちらの記事](https://qiita.com/issakuss/items/d8fd83f204becaea4cb3)に、もう少しまともな方法を載せました）。
![スクリーンショット (42).png](https://qiita-image-store.s3.amazonaws.com/0/304493/39666dea-64da-8511-5438-4e712e4c39d0.png)
横軸が世代で、縦軸が精度です。
これをみると、53世代目くらいから精度が改善していませんね。

ちなみに、`TPOTRegressor`の`early_stop`という引数があります。
これを指定すると、連続で～世代改善が見られなかったときに探索を打ち切ってくれるようです。
プロットを見ると、10世代くらい改善がないことがあるようです。
なので、指定するとしたら20くらいですかね（あくまでこのデータでは）。

**（2018年12月18日 追記）**
手持ちのデータで500世代ほど回したところ、階段状に精度が改善する様子が見られました。
遺伝的プログラミングというものの特性を（わからないなりに）考えると、当然かもしれませんね。
ですのでこのデータでも、もうしばらく世代を経るとまたグンと改善するかもしれません。


## パイプラインの出力

```python
tpot.export('tpot_boston_pipeline.py')
```

とやると、最適化されたpipelineを出力させることができるそうです。
出力された.pyファイルの中身を見てみましょう。

```python
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# NOTE: Make sure that the class is labeled 'target' in the data file
tpot_data = pd.read_csv('PATH/TO/DATA/FILE', sep='COLUMN_SEPARATOR', dtype=np.float64)
features = tpot_data.drop('target', axis=1).values
training_features, testing_features, training_target, testing_target = \
            train_test_split(features, tpot_data['target'].values, random_state=None)

# Average CV score on the training set was:-9.496544723555056
exported_pipeline = RandomForestRegressor(bootstrap=False, max_features=0.45, min_samples_leaf=1, min_samples_split=6, n_estimators=100)

exported_pipeline.fit(training_features, training_target)
results = exported_pipeline.predict(testing_features)
```
これを実行すると、与えたデータに最適化されたPipelineを適用して、交差検証で精度を返してくれるということかと思います。
`'PATH/TO/DATA/FILE'`と`'COLUMN_SEPARATOR'`に自分のデータ（.csv）のあるパスやセパレーターを入れる必要があります。
ちょっと.csvファイル用意するのが面倒なので、ここでは試しません。

## まとめ
とっても簡単に使えました。
scikit-learnに慣れていれば、同じ要領で使えるのが嬉しいです。
今回は回帰をするために`TPOTRegressor`を使いましたが、`TPOTClassifier`による分類も同じようにできると思います。

~~ただ、TPOTでは特徴選択までは自動化してくれないようですね。
その点は、グリッドサーチなどと組み合わせる必要があるかもしれません。~~
↑大嘘です。大変失礼しました。
[TPOTのHP](https://epistasislab.github.io/tpot/using/#what-to-expect-from-automl-software)を読むと、特徴選択や次元削減のような前処理も含めてやってくれるとあります。


[今回使った.ipynbファイル](https://issakuss.github.io/tech/article_8/tpot_boston.ipynb)