🐥

xfeatとAutoGluonでベンチマークモデル作成

2021/03/08に公開

この記事ではxfeatとAutoGluonを使って、機械学習モデルの評価で役に立つベンチマークモデルを作ってみようと思います。

はじめに

みなさんは機械学習モデルのベンチマーク作成に悩んだことはないですか?
個人的な経験では、新規プロジェクトのデータサイエンティストが自分一人の時に悩む時がありました(モデルの精度はデータに依存する部分が大きいですが、新しいデータ、タスクで比較対象がないなか、いかに自身のモデルの有用性を伝えるかなどの面で)。

過去には...

  • 素のデータセットでモデルを学習した結果で比較する
  • 実験過程の記録を使用する
    • 複数アルゴリズムを試した時の結果を利用する(後付けでA, B, C...のアルゴリズムを試してAが一番良かった)
    • HPOした時の結果を利用する(後付けでA, B, C...のハイパーパラメータを探索してAが一番良かった)

こういったアプローチをとったことがあります。
しかしこれらのアプローチは自作自演というか出来レース的で、必ず自身のモデルが勝つ(勝てる)状況のなかで、どこまで精度を追求するかというモチベーションの面で悪い影響を与えてしまう部分もあるのではないかなと感じていました。

こんな時に、”機械学習コンペのようにいいライバルがいたらいいなー”というのがこの記事でやりたいことです。

この記事では、テーブルデータに対してxfeatとAutoGluonを使って"いいライバル(ベンチマークモデル)"を作ってみたいと思います。

xfeatとは

https://github.com/pfnet-research/xfeat

xfeatは、PFNが開発したOSSで特徴量エンジニアリングと探索のためのライブラリです。
主な特徴は以下の通りです。

  • 数値、カテゴリ変数に使える処理が一通り揃っている
  • PandasのDataFrameを入力にしており、使いやすい
  • Optunaを使った特徴量選択も可能
  • cuDF(RAPIDS)とCuPyをサポートしており、GPU上で高速に動作

個人的にはAPIが直感的なのでとても使いやすいライブラリです。

AutoGluonとは

Paper: AutoGluon-Tabular: Robust and Accurate AutoML for Structured Data

https://github.com/awslabs/autogluon

AutoGluonは、AWSが開発した画像、テキスト、テーブルデータに対してAutoMLを実行するOSSです(この記事ではテーブルデータに対してAutoMLを実行するAutoGluon Tabularを紹介します)。
主な特徴は以下の通りです。

  • テーブルデータの教師あり学習の「回帰」と「分類(二値、多値)」に対応
  • 簡単なAPIで実行可能
  • 複数モデルのアンサンブル(重み付け平均、スタッキング)に対応
  • テキスト変数から特徴生成が可能

xfeatと組み合わせることで数値、カテゴリ、テキストのデータセットに対応できるので幅が広がります。

ベンチマークモデル作成

それではベンチマークモデルの作成を進めていきたいと思います。

ライブラリのインストール

# AutoGluon
pip install -U pip
pip install -U setuptools wheel
pip install -U "mxnet<2.0.0"
pip install autogluon==0.1.0
pip install pygraphviz

# xfeat
pip install git+https://github.com/pfnet-research/xfeat.git

pygraphvizはモデルの可視化で使いますが、なくてもモデルの学習・推論はできます。
必要な場合はGraphvizと併せてインストールしてください。

データの準備

この記事ではUCIのin-vehicle coupon recommendation Data Setを使いたいと思います。

!wget https://archive.ics.uci.edu/ml/machine-learning-databases/00603/in-vehicle-coupon-recommendation.csv

import pandas as pd

df = pd.read_csv('in-vehicle-coupon-recommendation.csv')
df

このデータセットはドライバーが、目的地や時間、天気などの様々な状況でクーポンを受け入れるかどうか(Y={0, 1})を調査したデータセットです。

target = 'Y'

categorical_columns = [x for x in df.columns if df[x].dtypes == 'object']
numerical_columns = [x for x in df.columns if (df[x].dtypes != 'object') & (x != target)]

入力変数には数値、カテゴリがありますが、ここではカテゴリ変数に対してxfeatで特徴量を作成していきたいと思います(自身のデータで実行する際はnumerical_columnscategorical_columnsが意図したように分けられているか確認して必要であれば修正してください)。

print(categorical_columns)
# ['destination', 'passanger', 'weather', 'time', 'coupon', 'expiration', 'gender', 'age', 'maritalStatus', 'education', 'occupation', 'income', 'car', 'Bar', 'CoffeeHouse', 'CarryAway', 'RestaurantLessThan20', 'Restaurant20To50']
print(numerical_columns)
# ['temperature', 'has_children', 'toCoupon_GEQ5min', 'toCoupon_GEQ15min', 'toCoupon_GEQ25min', 'direction_same', 'direction_opp']

特徴量を作る前に学習用と検証用にデータを分割します。

from sklearn.model_selection import train_test_split

train_df, val_df = train_test_split(df, test_size=0.2, random_state=42)
print(train_df.shape)
print(val_df.shape)
# (10147, 26)
# (2537, 26)

xfeatを使った特徴量エンジニアリング

最初にカテゴリ変数に対してカウントエンコーディングを行います。
学習データを基にエンコーダを作成し、検証データに適用します。

from xfeat import CountEncoder

encoder = CountEncoder(input_cols=categorical_columns)
train_df = encoder.fit_transform(train_df)
val_df = encoder.transform(val_df)

次にカテゴリ変数同士でペアを作り、結合します。

from xfeat import ConcatCombination

encoder = ConcatCombination(input_cols=categorical_columns,
                            drop_origin=False,
                            r=2)
train_df = encoder.fit_transform(train_df)
val_df = encoder.transform(val_df)

カテゴリ変数に対してターゲットエンコーディングを行います。

from sklearn.model_selection import KFold
from xfeat import TargetEncoder

fold = KFold(n_splits=5, shuffle=True, random_state=42)
encoder = TargetEncoder(input_cols=categorical_columns,
                        target_col=target,
                        fold=fold)
train_df = encoder.fit_transform(train_df)
val_df = encoder.transform(val_df)

ここまで何も考えずにカテゴリ変数に対して特徴量エンジニアリングしてきたので、重複している列、同じ値しか入っていない列、相関が高過ぎる変数のペアなどがあれば削除します。

from xfeat.pipeline import Pipeline
from xfeat.selector import DuplicatedFeatureEliminator
from xfeat.selector import ConstantFeatureEliminator
from xfeat.selector import SpearmanCorrelationEliminator

def get_feature_selector():
    return Pipeline(
        [
            DuplicatedFeatureEliminator(),
            ConstantFeatureEliminator(),
            SpearmanCorrelationEliminator(threshold=0.9),
        ]
    )

input_columns = [x for x in train_df.columns if x != target]

selector = get_feature_selector()
df_reduced = selector.fit_transform(train_df[input_columns])
print("Selected columns: {}".format(df_reduced.columns.tolist()))
train_df = train_df[df_reduced.columns.tolist()+list(target)]
val_df = val_df[df_reduced.columns.tolist()+list(target)]

6つの変数が削除されました。

AutoGluonを使った機械学習モデリング

続いてAutoGluonで機械学習モデルの学習を行います。
AutoGluon==0.1.0ではデフォルトで以下のモデルを実行します。

  • RandomForest(sklearn)
  • ExtraTrees(sklearn)
  • KNearestNeighbors(sklearn)
  • LightGBM
  • CatBoost
  • XGBoost
  • NeuralNet(MXNet, FastAI)

いくつかのモデルはハイパーパラメータを少しいじって複数パターン実行してくれます。
実行時はまずPredictorを定義します。
今回は評価指標にroc_aucを指定しています。

import autogluon as ag
from autogluon.tabular import TabularDataset, TabularPredictor

output_path = 'agModels-predictClass-xfeat'

predictor = TabularPredictor(
    label=target,
    problem_type='binary', # 二値分類
    eval_metric='roc_auc', # 評価指標
    path=output_path, # モデルの格納場所
    verbosity=4, # ログの出力レベル
)

入力となるtrain_df, val_dfにはカテゴリがカテゴリのまま入っている状態ですが、AutoGluonは内部ではアルゴリズムに合わせて変換してくれます(ラベルエンコーディングしてから、sklearn系はone-hot, GBDT系はcategorical, NN系はEmbeddingなど)。欠損処理も同様です。

今回のデータセットでは、1つテキスト認識されてしまうカラムがあったため、テキスト処理をFalseにしています(カテゴリかテキストかの判断は文字列長やカーディナリティを基に判断)。
その他はデフォルトの設定で学習を実行します。

predictor.fit(
    train_data=train_df,
    tuning_data=val_df, # これを渡さない場合はランダムスプリット
    time_limit=None, # おおよその時間制限を設けられる
    presets='medium_quality_faster_train', #デフォルトプリセット
    _feature_generator_kwargs={
        'enable_text_ngram_features': False,
        'enable_text_special_features': False,
        'enable_raw_text_features': False
    },
    #excluded_model_types=['KNN'], # 一部モデルを除外する場合
)

結果が以下となります(xfeatを使わないバージョンは別途実行しています)。

モデル AutoGluonのみ AutoGluon + xfeat
WeightedEnsemble_L2 0.849777 0.857070
LightGBMXT 0.844241 0.848947
CatBoost 0.839667 0.846775
LightGBMLarge 0.837590 0.846599
LightGBM 0.835666 0.841780
XGBoost 0.834714 0.840627
NeuralNetMXNet 0.811182 0.832815
RandomForestEntr 0.812572 0.822937
RandomForestGini 0.815846 0.819350
ExtraTreesEntr 0.797244 0.817992
ExtraTreesGini 0.798302 0.815785
NeuralNetFastAI 0.818770 0.811000
KNeighborsDist 0.539007 0.715612
KNeighborsUnif 0.539007 0.678436

仮にLightGBMで素のデータセットを学習した場合のroc_auc0.835666と捉えると、xfeatで特徴量を作成することで0.841780になり、さらに複数モデルのアンサンブル(WeightedEnsemble_L2:デフォルトは重み付け平均)で0.857070となります。

ライバルは少し強くなりました

学習が終わると.leaderboard()で一覧が取得できます。

predictor.leaderboard()

最終的に使用されたモデル

predictor.plot_ensemble_model()
# 'agModels-predictClass-xfeat/ensemble_model.png'

ライバルは精度を示すだけでなく、ヒント(feature_importance)もくれます。

predictor.feature_importance(val_df,
                             subsample_size=val_df.shape[0], # 全レコード使う場合
                             num_shuffle_sets=1 # シャッフルする回数、ドキュメントは最低10を推奨。今回は無視
                            )

マイナスの特徴量はシャッフルして精度が改善した特徴量です(実際に使用する際は複数回シャッフルしてください)。

最後に

xfeat + AutoGluonの組み合わせはコード量が少なく、且つ良いベンチマークモデルが作れそうだなと思い、この記事を書いてみました。xfeatとAutoGluonはまだまだできることがあるのでさらに強いベンチマークモデルを作ることも可能だと思います。
何かの参考になると嬉しいです。

Discussion