AutoGluonのTabularPredictorをscikit-learn互換にしてみた

# はじめに

**2021/08/17: ライブラリ名を変更したことに伴い、内容を更新**

本記事は、[前回の記事](https://qiita.com/kzkymn/items/e40e18eda5e85be76d95)の続きです。ただ、このまま読み進めても理解できるようにしています。

前回の記事では、有力なAutoMLツールであるAutoGluonのテーブルデータ学習用クラス、`TabularPredictor`の話をしました。

そのとき、「このクラスの`fit`メソッドはscikit-learnのものとは異なり、説明変数と目的変数の両者を同じデータフレームにまとめて渡す作りになっている」ということを書きました。また、「簡単なラッパクラスを書けば(scikit-learnとの)互換性を実現できそうに見える」とも書きました。

今回は、**scikit-learn互換のための`TabularPredictor`のラッパクラスを実際に作ってみました**ので、その紹介をしていきます。これがあれば`TabularPredictor`の持つAutoMLの機能を、既存のコードのscikit-learnの学習処理と差し替えることが容易になります。

# TabularPredictorのラッパクラス

TabularPredictorのラッパクラスは、サンプルを含めて「skautogluon」という名前で公開しています。MITライセンスです。
https://github.com/kzkymn/skautogluon/

## インストール方法

お使いのLinuxにある、Python3環境で以下のコマンドを実行してインストールできるようにしています。Python3.7以降であれば問題ないと思います。
Macでも動くかもしれませんが、WindowsはAutoGluon自体が非対応のためおそらく動かないと思います。

```sh
pip install git+https://github.com/kzkymn/skautogluon.git#egg=skautogluon
```

## 使い方

AutoGluonの`TabularPredictor`のかわりに、先ほどインストールしたskautogluonにある`TabularPredictorWrapper`を使います。下記のコードでインポートができます。

```python
from skautogluon import TabularPredictorWrapper
```

### 学習機能の使い方

`TabularPredictorWrapper`はscikit-learnのものと同じく、`fit`に説明変数`X`と目的変数`y`を渡すことで学習ができるようになっています。

```python
save_path = 'agModels-predictClass'
predictor = TabularPredictorWrapper(path=save_path).fit(X=X_train, y=y_train)
```

なお、`TabularPredictorWrapper`のコンストラクタに渡している`path`はオリジナルの`TabularPredictor`にもあったもので、AutoGluonの学習結果やモデルファイルを保存する先のパスです。

このように、オリジナルの`TabularPredictor`の持つコンストラクタ引数は基本的に、`TabularPredictorWrapper`でもサポートしています。

ちなみに、本記事の例として使っているデータは`sklearn.datasets.fetch_openml`関数で`data_id=179`を指定して取得したものです。これはちょうど公式チュートリアルの[tabular-quickstart](https://auto.gluon.ai/stable/tutorials/tabular_prediction/tabular-quickstart.html)で使っているデータとほぼ等価[^1]なものにあたります。

具体的には下記のようなコードでデータを取得し、学習用・テスト用にデータを分けていると考えて下さい。

```python
X, y = sklearn.datasets.fetch_openml(data_id=179, return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=42)
```

[^1]: 下記でプレビューしている範囲で言えば、`age`列の値が異なっているように見えるのですが、今回の本題ではないので詳しい調査はしていません。

説明変数・目的変数の外観は、それぞれ以下のとおりです。

- 説明変数5行プレビュー

|  |   age | workclass        |   fnlwgt | education   |   education-num | marital-status     | occupation        | relationship   | race   | sex    |   capitalgain |   capitalloss |   hoursperweek | native-country   |
|---:|------:|:-----------------|---------:|:------------|----------------:|:-------------------|:------------------|:---------------|:-------|:-------|--------------:|--------------:|---------------:|:-----------------|
|  0 |     2 | State-gov        |    77516 | Bachelors   |              13 | Never-married      | Adm-clerical      | Not-in-family  | White  | Male   |             1 |             0 |              2 | United-States    |
|  1 |     3 | Self-emp-not-inc |    83311 | Bachelors   |              13 | Married-civ-spouse | Exec-managerial   | Husband        | White  | Male   |             0 |             0 |              0 | United-States    |
|  2 |     2 | Private          |   215646 | HS-grad     |               9 | Divorced           | Handlers-cleaners | Not-in-family  | White  | Male   |             0 |             0 |              2 | United-States    |
|  3 |     3 | Private          |   234721 | 11th        |               7 | Married-civ-spouse | Handlers-cleaners | Husband        | Black  | Male   |             0 |             0 |              2 | United-States    |
|  4 |     1 | Private          |   338409 | Bachelors   |              13 | Married-civ-spouse | Prof-specialty    | Wife           | Black  | Female |             0 |             0 |              2 | Cuba         |



- 目的変数5行プレビュー[^2]

```
0    <=50K
1    <=50K
2    <=50K
3    <=50K
4    <=50K
Name: class, dtype: category
Categories (2, object): ['>50K', '<=50K']
```

[^2]: カテゴリ値が`['>50K', '<=50K']`であることからわかるように、今回のデータは二値分類ができるものになっています。

学習が始まると、以下のようなメッセージが出力されていきます。

```
Beginning AutoGluon training ...
AutoGluon will save models to "agModels-predictClass/"
AutoGluon Version:  0.2.0
Train Data Rows:    32724
Train Data Columns: 14
Preprocessing data ...
AutoGluon infers your prediction problem is: 'binary' (because only two unique label-values observed).
	2 unique label values:  ['<=50K', '>50K']
	If 'binary' is not the correct problem_type, please manually specify the problem_type argument in fit() (You may specify problem_type as one of: ['binary', 'multiclass', 'regression'])
Selected class <--> label mapping:  class 1 = >50K, class 0 = <=50K
(後略)
```

### 推論やその他機能の使い方

`fit`以外のメソッドの使い方は、特段`TabularPredictor`のものとは変わりません。
例えば`predict`なら、説明変数のみが入ったデータフレームを渡せば推論結果を得ることができます。

```python
y_pred = predictor.predict(X_test)
print("Predictions:  \n", y_pred)
```

上のコードであれば、以下のような出力が得られるはずです。

```
Predictions:  
 7762     <=50K
23881    <=50K
30507    <=50K
28911    <=50K
19484    <=50K
         ...  
15968     >50K
37984     >50K
31513     >50K
46024    <=50K
2946     <=50K
Name: y, Length: 16118, dtype: object
```

オリジナルの`TabularPredictor`には`leaderboard`メソッドがあり、学習時に作ったモデルを精度順に表示することができます。

`TabularPredictorWrapper`にも`leaderboard`メソッドがあります。
ただし上で説明した`fit`メソッドと仕様を合わせるため、オリジナルのものとは違って、説明変数と目的変数を別々の引数で`leaderboard`に渡す仕様にしています。

```python
predictor.leaderboard(X_test, y_test, silent=True)
```

こちらの実行結果は以下のような表になります。今回のデータではブースティング木やニューラルネットが上位を占めていることがわかります。

|    | model               |   score_test |   score_val |   pred_time_test |   pred_time_val |   fit_time |   pred_time_test_marginal |   pred_time_val_marginal |   fit_time_marginal |   stack_level | can_infer   |   fit_order |
|---:|:--------------------|-------------:|------------:|-----------------:|----------------:|-----------:|--------------------------:|-------------------------:|--------------------:|--------------:|:------------|------------:|
|  0 | LightGBMXT          |     0.856744 |      0.86   |        0.0326574 |       0.0175004 |  0.327207  |                0.0326574  |               0.0175004  |           0.327207  |             1 | True        |           3 |
|  1 | LightGBM            |     0.856682 |      0.8596 |        0.0928633 |       0.0175211 |  0.314495  |                0.0928633  |               0.0175211  |           0.314495  |             1 | True        |           4 |
|  2 | XGBoost             |     0.856062 |      0.8536 |        0.143337  |       0.0195582 |  1.18695   |                0.143337   |               0.0195582  |           1.18695   |             1 | True        |          11 |
|  3 | NeuralNetMXNet      |     0.855813 |      0.858  |        2.01173   |       0.307559  | 56.0177    |                2.01173    |               0.307559   |          56.0177    |             1 | True        |          12 |
|  4 | NeuralNetFastAI     |     0.854821 |      0.8564 |        0.202868  |       0.0504737 | 22.9124    |                0.202868   |               0.0504737  |          22.9124    |             1 | True        |          10 |
|  5 | CatBoost            |     0.85451  |      0.8536 |        0.0263336 |       0.0155103 |  2.28564   |                0.0263336  |               0.0155103  |           2.28564   |             1 | True        |           7 |
|  6 | WeightedEnsemble_L2 |     0.854014 |      0.8636 |        3.74805   |       1.71425   | 90.8635    |                0.00588965 |               0.00274515 |           0.953759  |             2 | True        |          14 |
|  7 | LightGBMLarge       |     0.851594 |      0.8528 |        0.0429571 |       0.0317025 |  3.88792   |                0.0429571  |               0.0317025  |           3.88792   |             1 | True        |          13 |
|  8 | RandomForestEntr    |     0.833664 |      0.8352 |        0.264694  |       0.227056  |  1.31782   |                0.264694   |               0.227056   |           1.31782   |             1 | True        |           6 |
|  9 | RandomForestGini    |     0.833478 |      0.8348 |        0.27859   |       0.112005  |  1.09688   |                0.27859    |               0.112005   |           1.09688   |             1 | True        |           5 |
| 10 | ExtraTreesEntr      |     0.831679 |      0.8348 |        0.319609  |       0.459777  |  0.928697  |                0.319609   |               0.459777   |           0.928697  |             1 | True        |           9 |
| 11 | ExtraTreesGini      |     0.830996 |      0.8344 |        0.354856  |       0.458674  |  0.898111  |                0.354856   |               0.458674   |           0.898111  |             1 | True        |           8 |
| 12 | KNeighborsUnif      |     0.726765 |      0.7328 |        0.114152  |       0.108575  |  0.0209086 |                0.114152   |               0.108575   |           0.0209086 |             1 | True        |           1 |
| 13 | KNeighborsDist      |     0.717769 |      0.7164 |        0.122203  |       0.112647  |  0.0328131 |                0.122203   |               0.112647   |           0.0328131 |             1 | True        |           2 |

## ちょっと進んだfitの使い方

`TabularPredictorWrapper`の`fit`の引数には、説明変数や目的変数に加えて`TabularPredictor`のものと同じオプションを渡すことができます。
以下の例では、学習の制限時間にあたる`time_limit`を渡しています。

```python
time_limit = 60
predictor.fit(X_train, y_train, time_limit=time_limit)
```

もし、オリジナルの`TabularPredictor`における`tuning_data`にあたる引数を`fit`に渡したい場合は、`X`や`y`と同様に、チューニング用のバリデーションデータを説明変数と目的変数に分けたまま、それぞれ`X_tuning`と`y_tuning`に渡します。
具体例は以下のとおりです。

```python
# load datasets same as train.csv and test.csv in https://autogluon.s3.amazonaws.com/datasets/Inc/
X, y = sklearn.datasets.fetch_openml(data_id=179, return_X_y=True)

X_train, X_test_base, y_train, y_test_base = train_test_split(
    X, y, test_size=0.33, random_state=42)

X_val = X_test_base[:int(len(X_test_base)/2)]
y_val = y_test_base[:int(len(X_test_base)/2)]
X_test = X_test_base[int(len(X_test_base)/2):]
y_test = y_test_base[int(len(y_test_base)/2):]

predictor = TabularPredictorWrapper(eval_metric=metric).fit(
    X=X_train, y=y_train,
    X_tuning=X_val, y_tuning=y_val,
    time_limit=time_limit)
```

# おわりに

本記事で紹介した`TabularPredictorWrapper`があれば、冒頭の説明どおりに`TabularPredictor`の機能を既存のコードのscikit-learnの学習処理と差し替えることができ、すぐさまAutoGluonの持つ豊富なAutoMLの機能を試すことができます。また、Pipelineに組み込んで実行することもできるようになります。

`TabularPredictorWrapper`により、AutoGluonがより使いやすく・活用されやすくなることを期待しています。