もっとTPOTで自動機械学習を試した

## 更新履歴
2019年1月18日：リンクが一部間違っていたのを修正しました。

## この記事の目的
[TPOTで自動機械学習（回帰）を試した](https://qiita.com/issakuss/items/f05d90cc5893ecce8b1a)という記事を書きました。
この時は簡単に使い方を確かめた程度だったのですが、いろいろ試すと、いろいろ便利な使い方が見つかりました。

前回は基本編ということで、今回は応用編をお届けします
（ほとんどはただ、オプション引数を使っただけですが）。

今回も基本的に、[TPOTのHP](https://epistasislab.github.io/tpot/)を参考にしています。

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

## データの準備
今回も回帰学習をしますので、またボストン住宅価格をDLしてきます。
そして、`train_test_split`でトレーニングデータとテストデータを分けます。

```python
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
housing = load_boston()
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)
```

## 結果に再現性をもたせる
[前回の記事](https://qiita.com/Hironsan/items/30fe09c85da8a28ebd63)で、推定の結果などを載せました。
しかしあれ、再現性がなく、実行するたびに結果が変わります。
再現性をもたせるためには、`random_state`の引数を入れましょう。

```python
from tpot import TPOTRegressor
tpot = TPOTRegressor(generations=5, population_size=50,
                     verbosity=2, n_jobs=-1, random_state=2018)
```

何度か実行するとわかりますが、同じ結果を返してくれます。

## 同じTransformを省略させる
機械学習では、Transformer -> Estimator というPipelineで解析をすることが多々あるかと思います。

- Transformer: PCAなどによりデータを変換（Transform）して、次元削減・特徴選択をする
- Estimator: SVMなどのモデルで、分類や回帰（Estimate）をする

TPOTはこれらの組み合わせをあれこれ試すわけです。
ここで例えば、PCA->SVR というPipelineを試したあと、PCA->LassoというPipelineを試すとします。
この場合、データにPCAをかけるという、まったく同じ計算を2回やることになります。

これは計算コスト的にもったいない。
そこで、データをTransformした結果を保存して、使いまわしてしまいましょう。

`memory='auto'`にするだけです。

```python
tpot = TPOTRegressor(generations=5, population_size=50,
                     verbosity=2, random_state=2018, n_jobs=-1, memory='auto')
t0 = time.time()
tpot.fit(X_train, y_train)
print(time.time() - t0)
```

時間を比較したところ、デフォルトでは38秒かかったのに対し、
`memory='auto'`では39秒かかりました。
この程度の試行数では、むしろ遅くなるということでしょうか。

## 途中経過を随時保存する
[前回](https://qiita.com/Hironsan/items/30fe09c85da8a28ebd63)の最後に紹介しましたが、TPOTにはPipelineを.pyファイルとして出力する機能があります。
このファイル出力を、一定間隔で行ってくれる設定があります。
`periodic_checkpoint_folder`という引数にディレクトリを設定します。

試しに`proc_folder`というフォルダを同じディレクトリに作り…

```python
dire = 'proc_folder'
tpot = TPOTRegressor(generations=5, population_size=50,
                     verbosity=2, n_jobs=-1, random_state=2018, periodic_checkpoint_folder=dire)
t0 = time.time()
tpot.fit(X_train, y_train)
print(time.time() - t0)
```

…を走らせます。
すると、フォルダの中に.pyファイルが複数生成されます。
世代が進むたび（ただし経過時間が30秒未満の場合はスキップ）に.pyファイルを作ってくれているようです。

これを設定しておけば、途中でTPOTが死んだとしても結果を残せます。

一定処理ごとにTPOTを.pickleとして保存できたら色々便利だと思うのですが、今のところできないようです（[参照](https://github.com/EpistasisLab/tpot/issues/520)）。
代替策として、こちらの引数を使ってくださいといったことを開発者が言っています。

## 前回の続きからFittingする
`warm_start=True`とすることで、一度`.fit()`をした後のインスタンスを、再度`.fit()`することができます。
この時、Fittingを前回の続きから行ってくれます。

これを使えば、
「とりあえず100世代回してみたけど、まだまだ改善できそうだからもう100世代！」
なんていうおかわりができます。

```python
t0 = time.time()
tpot = TPOTRegressor(generations=5, population_size=50,
                     verbosity=2, n_jobs=-1, random_state=2018, warm_start=True)
tpot.fit(X_train, y_train)
print(time.time() - t0)
```

からの

```python
t0 = time.time()
tpot.fit(X_train, y_train)
print(time.time() - t0)
```

で、合計10世代計算できます。
しかし、やはり合計時間はいっぺんに10世代やった方が早いようです。

## 各世代のCV scoreを出力する
`verbosity=2`にしていると、世代ごとのCV scoreを出力してくれます。
![ss.png](https://qiita-image-store.s3.amazonaws.com/0/304493/92cdb964-e717-669a-4cc8-ae3897f271ac.png)
これをプロットできると、精度の改善が収束しているか一目でわかりますね。
しかしこのスコアを取得する方法がどーーしてもわかりません。

[前回](https://qiita.com/Hironsan/items/30fe09c85da8a28ebd63)は、この出力をエクセルにコピペするというダッッッセェ方法でプロットしました。
これよりは若干スマートな方法でプロットしてみたのでご報告します。


### 出力を.txtへするようにする
Fittingをする前に、`sys.stdout`を使って、出力を.txtファイルに行うようにします。
[この記事](http://d.hatena.ne.jp/alicehimmel/20110213/1297611415)を見ながらやりました。

```python
import sys
sys.stdout = open('temp.txt', 'w')
```

で、いつもと同じように

```python
tpot = TPOTRegressor(generations=30, population_size=50,
                     verbosity=2, n_jobs=-1, random_state=2018)
tpot.fit(X_train, y_train)
```

そのあとは、

```python
sys.stdout.close()
sys.stdout = sys.__stdout__
```

とします。
これで、通常画面に出力される内容が、`temp.txt`に吐き出されました。
ちなみに、`tqdm`によるプログレスバーは通常通り表示されるので安心です。

### 出力を.txtから読み取る

```python
txt = open('temp.txt', 'r').read()
```

とすると、`txt`に.txtファイルの中身が、`str`型として入ります。
![スクリーンショット (44).png](https://qiita-image-store.s3.amazonaws.com/0/304493/99d36180-c3f3-9b36-e056-d1cd1ce4af00.png)
まず、`/n/n`で区切ってやりましょう。


```python
outs = txt.split('\n\n')[1:-1]
```

![スクリーンショット (45).png](https://qiita-image-store.s3.amazonaws.com/0/304493/c8243a0e-1d78-3c48-a445-ad310592ba7d.png)
このリストのうち、ひとつ目と最後を除きましょう。
その上でリストの各要素について、`' '`で`split`して、最後の要素を取ってくればいいですね。

Pythonらしく、内包表記で。

```python
scores = [float(out.split(' ')[-1]) for out in outs[1:-1]]
```

![スクリーンショット (46).png](https://qiita-image-store.s3.amazonaws.com/0/304493/7aca53da-34c2-39d9-aa3d-acdcc8ed84a8.png)
やったぁ！

後から使いやすいように、関数としてまとめておきます。

```python
def fit_tpot(tpot, X_train, y_train):
    import sys
    sys.stdout = open('temp.txt', 'w')
    tpot.fit(X_train, y_train)    
    sys.stdout.close()
    sys.stdout = sys.__stdout__    
    
    outs = open('temp.txt', 'r').read().split('\n\n')
    scores = [float(out.split(' ')[-1]) for out in outs[1:-1]]
    
    return tpot, scores
```

こんな感じで使えます。
![スクリーンショット (47).png](https://qiita-image-store.s3.amazonaws.com/0/304493/9c677b1f-c42f-1f3b-25f7-eb7aea0dcfe3.png)

## 文字列からPipelineを生成
`.evaluated_individuals_`を使うことによって、検討されたPipelineの一覧を取得することができます。
このとき、Pipelineの内容は、文字列で取得されます。
これを、scikit-learnのPipelineオブジェクトに変換する方法が[こちら](https://github.com/EpistasisLab/tpot/issues/516)で紹介されていました。

```python
strs_pipeline = list(tpot.evaluated_individuals_.keys())
```

このようにすることで、使用したPipelineが文字列のリストとして手に入ります。
中身は、こんな感じです。
![スクリーンショット (48).png](https://qiita-image-store.s3.amazonaws.com/0/304493/0192cd8f-419b-ed5f-a2d0-e8aea762e3fe.png)

`deap.creator`を使うことで、これを文字列に変換します。

```python
from deap import creator
pipeline4deap = creator.Individual.from_string(strs_pipeline[1], tpot._pset)
pipeline4sklearn = tpot._toolbox.compile(expr=pipeline4deap)
```

![スクリーンショット (49).png](https://qiita-image-store.s3.amazonaws.com/0/304493/71fb0d4a-3d9e-72db-ddd6-bee3f0f6cc2f.png)

## 自分のデータに適用する
自身の研究のデータにもTPOTを試しました。
詳細は省きますが、189の特徴と120のサンプルからなる、比較的スモールなデータです。
PCAからのSVR(linear)で試すと、テストデータでの精度はr=.49くらいでした。

これをTPOTにかけて、とりあえず100世代回しました。
<img width="395" alt="スクリーンショット 2018-12-20 8.47.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/304493/071e791a-3c0b-82ac-3806-3552307ac0f7.png">
収束しているように見えますね。
しかしテストデータで精度をみてみると、r=.28と、イマイチな結果でした。

もう100世代ほど回します。
<img width="390" alt="スクリーンショット 2018-12-20 8.48.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/304493/746003ae-5434-8846-c0c5-7b283751cc33.png">
やはりこれ以上の改善は望めないか…

と思いきや、合計500世代回すと、
<img width="410" alt="スクリーンショット 2018-12-20 8.48.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/304493/719da6e2-69a7-064f-6a56-f36469cb94c8.png">
またぐんと精度が上がってます。

さらに、合計1,000世代回します。
<img width="402" alt="スクリーンショット 2018-12-20 8.48.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/304493/8b1346e5-d7f7-a07c-e678-100b7a5f39fd.png">
テストデータで精度検証すると、r=.447でした。
自身で組んだPipeline（PCA->SVR）に近い精度です。
もうしばらく回せば、もうちょっと良くなる気もします。

2,000世代やってみました。
<img width="395" alt="スクリーンショット 2018-12-20 8.55.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/304493/6f162a6d-e552-153e-a796-baf42e75d5e1.png">
また大きく改善しました。
精度改善が収束するということは、そうそうないのかもしれません。
遺伝的プログラミングという仕組みを（よく知らないなりに）考えると、今回のように階段状に改善していくのが通常に思えます。
適応的な突然変異がたまたま起きたタイミングで精度が改善するわけですからね。

ちなみに、テストデータで精度検証すると、r=.378でした。
1,000世代のときよりも悪化しています。
過学習が生じているのでしょうか。

## まとめ
100世代くらいの計算ならともかく、丸一日かけて演算するような使い方をするとなると、いろいろ工夫をしたくなりますね。
またなにかいい使い方が見つかったら、こちらの記事に追記します。

TPOTインスタンスをシリアライズして保存できれば、もっと便利な使い方ができそうです。
一定世代計算するごとに　中断→インスタンス保存→スコアプロット→再開　するとか。
まだまだ開発中のパッケージだそうですし、今後バージョンアップで対応してくれたりするかもしれません。

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