自分なりのAMLを作ってみた

# 誰にでもAIを気軽に作れる世界

1995年、Windows95 OSが発売され、一般大衆にもハード製品が普及したことによってインターネットは誰でも気軽に使えるツールとなりました。これは「インターネットのインフラ整備」と表現されると思います。

これと同じようなことが機械学習技術にも起きようとしています。[DataRobot](https://www.datarobot.com/jp/)や[Azure Machine Learning](https://azure.microsoft.com/ja-jp/services/machine-learning/)のようなサービスはその典型的な例です。従来、機械学習によるデータ分析は、エンジニアやデータサイエンティストなどの専門的な職業のみの**専売特許**であったと思います。しかし、このようなAuto MLの出現により「機械学習の民主化」の波が始まっています。

今回は、それを作ってしまおうぜ(シンプルなもの)ということが目的です。

# MLとは

AMLのお話の前に機械学習(Machine Learning 以下ML)とは？からお話しします。

英語版wikipediaには以下の記載がありました。


> Machine learning (ML) is the study of computer algorithms that improve automatically through experience. It is seen as a subset of artificial intelligence. Machine learning algorithms build a mathematical model based on sample data, known as "training data", in order to make predictions or decisions without being explicitly programmed to do so.

つまり「過去の経験(データ)から、人の介在なしで未来を **予測** するもの」と言えます。
下の図をご覧ください。生データ(row data)を準備し、それを「前処理」→「特徴量エンジニアリング」→「学習」→「モデル選択・スコアリング」といったフロー全体に通して「ターゲット」を予測することを**機械学習**と言います。

たとえば、明日の天気を予測したいときには、昨日や今日の天気、気温、湿度、風向きなどの情報から予測できそうですね。このとき「明日の天気」を「ターゲット」、「昨日や今日の情報」などの過去の情報を「生データ」と表現しています。(なお、この場合、時系列データなので他にも考慮しなければいけないことはたくさんありますが)

![c2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429781/8e42bb8b-8bfd-3360-f19a-d86a9ffc0b06.jpeg)

機械学習によって解くタスクには「分類問題」と「回帰問題」の２つがあります。今回は、「分類問題」に絞ったお話になります。

## Auto ML

では、Auto MLとはどういうものでしょうか。上記の説明においての「前処理」→「特徴量エンジニアリング」を自動化してくれる機械学習のことをさします。

一口にAuto MLと言ってもその方法は多岐に渡りますが、今回目指すのは以下の機能を兼ね備えて、かつそれぞれのモデルの精度を比較するAuto MLを開発することです。なお、本来であればパラメータチューニングなどの作業も自動化する必要もありますが、今回は許してください ><

- データパスからデータをロード
- onehotエンコーディング
- 欠損値を「平均値」「中央値」「最頻値」のいずれかで補完
- 特徴量選択
- グリッドリサーチ
- ランダムリサーチ
- 混合行列
- ROC曲線
- スコアリング

# コード全体

今回のコードは[github](https://github.com/bigshiny/AutomatedML)上においてあります

# データ用意

今回は、みなさんお馴染みの[titanic dataset](https://www.kaggle.com/c/titanic/data)を使用します。

### ディレクトリ構成

~~~
aml
|----data
|      |---train.csv
|      |---test.csv
|
|----model
|      |---こちらにモデルが保存される
|
|----myaml.inpynb
~~~

# 前処理と特徴量エンジニアリングと学習

お先に使い方のコードをお示しします。APIのexampleに対応するものです。

~~~python
model_data = 'data/train.csv'
scoring_data = 'data/test.csv'

aml = MyAML(model_data, scoring_data, onehot_columns=None)
aml.drop_cols(['Name', 'Ticket', 'Cabin'])  # NameとTicketとCabinの情報は使わない
# 前処理と特徴量エンジニアリング(特徴量選択)
aml.preprocessing(target_col='Survived', index_col='PassengerId', feature_selection=False)
# 学習とモデル比較結果表示(ホールドアウト法を採用)
aml.holdout_method(pipelines=pipelines_pca, scoring='auc')
~~~

|          |     test | train    |
|---------:|---------:|----------|
|       gb | 0.754200 | 0.930761 |
|      knn | 0.751615 | 0.851893 |
| logistic | 0.780693 | 0.779796 |
|       rf | 0.710520 | 0.981014 |
|     rsvc | 0.766994 | 0.837220 |
|     tree | 0.688162 | 1.000000 |

### 前処理

ここでの前処理は次の２つをさします。なお、以降のコードは大切なところのみ掻い摘んで説明できたらと思います

- onehotエンコーディング
- 欠損値を「平均値」「中央値」「最頻値」のいずれかで補完

#### onehotエンコーディング

~~~python
    def _one_hot_encoding(self, X: pd.DataFrame) -> pd.DataFrame:
        ...

        # one_hot_encoding
        if self.ohe_columns is None:  # obejct型またはcategory型の列のみone_hot_encoding
            X_ohe = pd.get_dummies(X,
                                   dummy_na=True,  # NULLもダミー変数化
                                   drop_first=True)  # 最初のカテゴリーを除外
        
        else:  # self.ohe_columnsで指定された列のみone_hot_encoding
            X_ohe = pd.get_dummies(X,
                                   dummy_na=True,  # NULLもダミー変数化
                                   drop_first=True,  # 最初のカテゴリーを除外
                                   columns=self.ohe_columns)
        ...
~~~

MyAMLクラスの初期化で、インスタンス変数に格納された `onehot_columns` は、「onehotエンコーディングさせたいカラム名」をリストで受け取ります。何も指定しなければ、受け取ったデータフレームのカラムのうちobejct型またはcategory型であるものをonehotエンコーディングします。

#### 欠損値補完

~~~python
    def _impute_null(self, impute_null_strategy):
        """
        欠損値をimpute_null_strategyで補完する
        impute_null_strategyの種類
        mean...平均値で補完
        median...中央値で補完
        most_frequent...最頻値で補完
        """
        self.imp = SimpleImputer(strategy=impute_null_strategy)
        self.X_model_columns = self.X_model.columns.values
        self.X_model = pd.DataFrame(self.imp.fit_transform(self.X_model),
                                    columns=self.X_model_columns)
~~~

`scikit-learn`の`SimpleImputer`クラスを用いて欠損値補完を行います。
`impute_null_strategy`は「何」で補完するのかを表す引数です。対応する補完方法は次の通りです。

- `mean`...平均値で補完
- `median`...中央値で補完
- `most_frequent`...最頻値で補完

### 特徴量エンジニアリング

特徴量エンジニアリングも奥が深いですが、今回は単純化して「**ランダムフォレスト**による特徴量選択」を考えます。

~~~python
    def _feature_selection(self, estimator=RandomForestClassifier(n_estimators=100, random_state=0), cv=5):
        """
        特徴量選択
        @param estimator: 特徴量選択を実施するための学習器
        """
        self.selector = RFECV(estimator=estimator, step=.05, cv=cv)
        self.X_model = pd.DataFrame(self.selector.fit_transform(self.X_model, self.y_model),
                              columns=self.X_model_columns[self.selector.support_])
        self.selected_columns = self.X_model_columns[self.selector.support_]
~~~

最初の行で、`RFECV`クラスを初期化します。この際の推定器は`RandomForestClassifier`をデフォルトとして指定しています。
次の行で、特徴量として重要性が高いものを選び出します。
最後に、**選ばれしものたち**をインスタンス変数`selected_columns `に格納します。

### 学習

ホールドアウト法により、モデルとデータの相性を比較します。
ホールドアウト法とは、訓練データ(モデルの学習に使われるデータ)とテストデータ(学習には使われない検証のためのデータ)に分ける方法です。この方法では、訓練データはずっと訓練データであり、テストデータはずっとテストデータであります。

モデルとデータの相性を比較する別の方法として、クロスバリデーションも実装していますが説明は割愛します。

~~~python
    def holdout_method(self, pipelines=pipelines_pca, scoring='acc'):
        """
        ホールドアウト法によりモデルの精度を確認する
        @param piplines: パイプライン(試すモデルの辞書)
        @param scoring: 評価指標
                  acc: 正解率
                  auc: ROC曲線の面積
        """
        X_train, X_test, y_train, y_test = train_test_split(self.X_model,
                                                            self.y_model,
                                                            test_size=.2,
                                                            random_state=1)
        y_train=np.reshape(y_train,(-1))
        y_test=np.reshape(y_test,(-1))

        scores={}
        for pipe_name, pipeline in pipelines.items():
            pipeline.fit(X_train, y_train)
            joblib.dump(pipeline, './model/'+ pipe_name + '.pkl')
            if scoring == 'acc':
                scoring_method = accuracy_score
            elif scoring == 'auc':
                scoring_method = roc_auc_score
            scores[(pipe_name, 'train')] = scoring_method(y_train, pipeline.predict(X_train))
            scores[(pipe_name, 'test')] = scoring_method(y_test, pipeline.predict(X_test))
        display(pd.Series(scores).unstack())
~~~

ここで変数 `piplines` は以下のような形式をしています。

~~~python
# make pipelines for PCA
pipelines_pca={
    """
    'モデル名': Pipeline([('scl', 標準化を行うクラス)
                          , ('pca', 主成分分析を行うクラス)
                          , ('est', モデル)]) 
    """
    'knn': Pipeline([('scl', StandardScaler())
                      , ('pca', PCA(random_state=1))
                      , ('est', KNeighborsClassifier())]),
    
    'logistic': Pipeline([('scl', StandardScaler())
                            , ('pca', PCA(random_state=1))
                            , ('est', LogisticRegression(random_state=1))]), 
    
     ...
}
~~~

`Pipeline`クラスに閉じ込められた３つのクラスはそれぞれ次のような機能を果たします。

- 'scl': 標準化を行う
- 'pca': 主成分分析を行う
- 'est': モデル

ゆえに、`pipeline.fit(X_train, y_train)`の際に「標準化」→「主成分分析による特徴量解析」→「学習」の一連の流れをします。

# My Dream

自分には夢があります。
「インターネットが誰にでも使えるように、機械学習や深層学習のモデルが誰にでも簡単に作れる社会の実現です」
そのAIインフラの整備の第一歩として、今回は「パスを通すだけで機械学習の一連のプロセスが動くシステム」を実装いたしました。まだまだ、至らないところだらけですがこれからも頑張っていきます。