AutoMLの自動化イメージを具体例から掴んでみる

# はじめに
この記事はFUJITSU Advent Calendar 2022の18日目の記事です。
お約束ごとですが、記事は全て個人の見解です。会社・組織を代表するものではありません。
本記事ではAutoMLについて自動化のイメージを簡単な具体例を用いて書いてみます。

# AutoML(Automated Machine Learning)
機械学習のステップは下記の様にそれなりに大きなものです。自動化できたら嬉しい、と自然に思えるのではないかと思います。
![Screen Shot 2022-12-17 at 22.16.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93677/985173ae-60fd-c93a-978d-afd1af77969a.png)
（引用: https://www.esri.com/arcgis-blog/products/arcgis-pro/analytics/automl-in-arcgis-pro-3-0/）

# どうやって自動化するのか？
仕組みとしては種々ありますがここでは２パターン取り上げてみます。
1. ルールベース
2. 学習＆推論ベース

### 1. ルールベース
`if ... then ...`のやつです。ここではAutogluonのTabularPredicorのタスク設定を例として取り上げます。凄く局所的な部分ですがコードを読んでいて想定よりも少なく書かれていて驚きだったのでこちらを取り上げます。
TabularPredictorはタスクとして、分類２種（バイナリ、マルチ）と回帰１種の３パターンを取り得ますが、`problem_type`引数に`None`を指定すると自動で推測してくれます。
```python
'''
problem_type : str, default = None
    Type of prediction problem, i.e. is this a binary/multiclass classification or regression problem (options: 'binary', 'multiclass', 'regression', 'quantile').
    If `problem_type = None`, the prediction problem type is inferred based on the label-values in provided dataset.
'''
```
https://github.com/autogluon/autogluon/blob/cebe3b53915ead4d4f880af4bdf9ef7a620a224c/tabular/src/autogluon/tabular/predictor/predictor.py#L56

この自動推測がどう行われているか、ソースを追ってみましょう。
いろいろ端折ると下記の関数でタスクの推測をしてくれることが分かります。
```python
def infer_problem_type(y: Series, silent=False) ->str:
```

https://github.com/autogluon/autogluon/blob/cebe3b53915ead4d4f880af4bdf9ef7a620a224c/core/src/autogluon/core/utils/utils.py#L571-L640

70行程度のルールと学習セットの正解ラベルから推測しており、一番分かりやすいルールは以下です。「正解ラベルが２値しか取らない場合はバイナリ分類だろう」という推測。まさにルールベースだと思います。
```python
unique_values = y.unique()
,,,
unique_count = len(unique_values)
if unique_count == 2:
	problem_type = BINARY
	reason = "only two unique label-values observed"
```
https://github.com/autogluon/autogluon/blob/cebe3b53915ead4d4f880af4bdf9ef7a620a224c/core/src/autogluon/core/utils/utils.py#L588-L591

### 2. 学習＆推論ベース
1.ルールベースで取り上げたタスクの自動推測を機械学習ベースで考えてみましょう。
Autogluonの内部コードでは、`unique_count`, `unique_ratio`, `subdtype`, `can_convert_to_int`という変数を使って判定していたので、これらを特徴量として３値の正解ラベル（`binary`, `multi`, `regression`）を推論するモデルを考えます。データセットを用意するのも大変なのでsklearnを使って擬似データセットを作り、学習と評価をしてみます。

#### 2-1. まずは関数定義。
```python
from typing import NamedTuple

import numpy as np
import numpy.typing as npt
import pandas as pd
from sklearn.datasets import make_classification, make_regression
from sklearn.metrics import accuracy_score
import category_encoders as ce
from lightgbm import LGBMClassifier

class Dataset(NamedTuple):
    type: str # ['binary', 'multi', 'regression']
    X: npt.NDArray
    y: npt.NDArray

TASK_TYPES = ['binary', 'multi', 'regression']

class DatasetCreator():
    def __init__(self, rand=20221218):
        self.rng = np.random.default_rng(rand)
    
    def create_datasets(self, n_sets: int):
        return [self._create() for i in range(n_sets)]
    
    def _create(self):
        tt = self._pick_task_type()
        rs = self.rng.integers(3, 100)
        if tt == 'binary':
            X,y= make_classification(n_classes=2, random_state=rs)
        elif tt == 'multi':
            X,y = make_classification(n_classes=rs, n_features=rs, n_informative=rs, n_redundant=0, n_clusters_per_class=1, random_state=rs)
        elif tt == 'regression':
            X,y = make_regression(random_state=rs)
        else:
            print('unexpected', tt)

        return Dataset(type=tt, X=X, y=y)
    
    def _pick_task_type(self):
        return self.rng.choice(TASK_TYPES, 1)[0]

def generate_features(y: np.ndarray):
    s = pd.Series(y)
    unique_count = s.nunique()
    unique_ratio = unique_count / float(len(y))
    subdtype = y.dtype
    can_convert_to_int = False
    try:
        can_convert_to_int = np.array_equal(s, s.astype(int))
    except:
        pass
    
    return pd.DataFrame({'unique_count': [unique_count], 'unique_ratio': [unique_ratio], 'subdtype': [subdtype], 'can_convert_to_int': can_convert_to_int})
```

#### 2-2. 次に学習。
`subdtype`は値として`int64`と`float64`を取りますがこのままだとモデルに食わせられないのでOneHotEncoderを挟んでいます。
```python
n_trains = 1000

dc = DatasetCreator()
train_sets = dc.create_datasets(n_trains)
train_Xs = pd.concat([generate_features(set.y) for set in train_sets])
ohe = ce.OneHotEncoder(cols=['subdtype'])
train_Xs = ohe.fit_transform(train_Xs)
train_ys = pd.Series([set.type for set in train_sets])

cls = LGBMClassifier()
cls.fit(train_Xs, train_ys)

## なんとなく確認。
train_pred = cls.predict(train_Xs)
print(accuracy_score(train_ys, train_pred))
> 1.0
```

#### 2-3. 最後に評価。
```python
test_sets = dc.create_datasets(100)

test_Xs = pd.concat([generate_features(set.y) for set in test_sets])
test_Xs = ohe.transform(test_Xs)
test_ys = pd.Series([set.type for set in test_sets])

pred = cls.predict(test_Xs)
print(accuracy_score(pred, test_ys))
> 1.0
```

きちんと精度高く推論出来ていますね。ちなみにn_trainsが30程度と少ない場合は、学習セットですら精度100%が出ませんでした。書いたコードの量とデータセットのサイズからして、この程度であればルールベースでやってしまった方が賢そうですね。。

# まとめ
例があまり良くなかったのか特徴量設計が不要なディープラーニングの導入のような流れになってしまいましたが、自動化ってきっとこんなイメージかと思っています。
今回の例では学習データセットの正解ラベルからタスクを自動推論（推測？）する例を取り上げましたが、入力データやラベルの傾向から必要な前処理というのは両手・両足では数えきれないくらいのパターンがあります。必要な前処理の例としては、特徴量に欠損値があったらImputerを挟む、他の特徴量に比べて幅広い値を取りうる場合にはscalingする、等です。
この辺りまとめて全部自動化してくれたらさぞ嬉しいですよね！！