atmaCupで初手が爆速になるフレームワークVividに敗北したのでVividに入門する


はじめに

今回はタイトルの通りatmaCupで が作っているML用のフレームワークVividによるSubmitに勝てなかったので、Vividに入門して行こうかと思っています。そこで、Vividの勉強のためにもブログにまとめました。
また、あまりドキュメントを書いている人もいなかったので、気になっているけどドキュメントがなくて取っ付きづらくて触れていないという方の助けになればと思っています。

Vividとは

大まかに行ってしまうと簡単な特徴量の生成に始まり、モデルの学習・保存などを一気通貫にできるML用のフレームワークです。

コンペは本当にざっくり書いてしまうと以下のような流れでそれぞれのフローごとに実装しなければいけない部分がいろいろあります。

フロー図

この辺を毎回手でどうするかとかを考えていると短期的なコンペでは大幅な時間ロスにつながってしまいます。気軽に時間を浪費できる人ならそうじゃない人はこの辺の無駄は省きたいですよね?
なんと、上記の図に書いている内容はVividだと勝手にやってくれたり、圧倒的に少ないコード量で効率的に書くことができたりします。

具体的には以下のことがVividではできます。

  • 特徴量生成
    • 指定カラムのCount Encoding
    • 指定カラムのOneHot Encoding
    • 集計系の特徴量
  • モデル
    • Single Model(二値分類と回帰)
      • XGBoost
      • LightGBM
      • Random Forest
      • K-Neighbors
      • LogisticRegression
      • Ridge
      • Neural Network
      • SVM
    • Optunaでのパラメータチューニング
    • Seed Averaging
    • Single Modelのアンサンブル・スタッキング
    • CVの仕方も変えられる

これらの機能により、分析者は本質的な手法選択などに注力して実験を爆速で進めることができます。

Vividのコンセプト

Vividのコンセプトは「分析コンペLT会でLTをさせてもらいました!!」には以下のように書いており、主にコンペでもっと楽して、コーディングよりももっと本質的なことに注力したいというモチベーションなのかなと思いました。

  • 必要なことだけを書くので良い
    • 基本は勝手にやってくれる
    • k-fold の split をして oof を計算するコード、みたいな定形処理は全部 vivid にお願いして、プロジェクト固有のコードに集中できる用に
    • ログの出力やモデルの保存なんかも勝手にやってくれる用に
  • テンプレート的な特徴作成の提供
    • 毎回 count encoding のコードを書くのは良くないのでそれもやってもらう
    • 特徴量をある粒度 (atom とよんでいます) とその集合体 molecule で管理して versioning する機能とかもあります
  • スタッキング・アンサンブル対応
    • 対応というよりは、一気通貫に出来るというのが売りです。
    • (全部一気につながって作るので、前の run で作った特徴で学習していて予測するとバージョン違いで精度が出ない・カラムの数が違う、といった悲しいミスを防ぐ)
      立ち位置としては sickit-learn よりも更に盛ったフレームワークという感じでしょうか。(webのDjango的な)

私も同じことを感じているので、非常に共感できるコンセプトです。count encodingなどの簡単な特徴量でも毎回書いてるとバグったりして時間食ったり、モデルの管理とかログの出力とか面倒臭くて自分ではやりたくないですし、スタッキング・アンサンブルは自分でやってバグが混入してスコアが全然上がらなかった経験もあったので特に…。
私も面倒くさがりなので、スタッキング・アンサンブル機能のない似たようなオレオレフレームワークを作ってatmaCup参加してたくらいには課題を感じている部分です。(とはいえ設計が悪かったので最終的に一部機能以外使わずに終わりましたが)

環境構築

以下のコマンドを実行するだけでインストールできます。すごい楽です。

pip install git+https://gitlab.com/nyker510/vivid

poetryなら以下のコマンドでインストールできます。

poetry add git+https://gitlab.com/nyker510/vivid

使い方

Simple Model

回帰の場合

シンプルモデルでの回帰の例は公式でsampleとして公開されていました。

import pandas as pd
from sklearn.datasets import load_boston

from vivid.core import AbstractFeature
from vivid.metrics import regression_metrics
from vivid.out_of_fold.boosting import XGBoostRegressorOutOfFold


class BostonProcessFeature(AbstractFeature):
    def call(self, df_source: pd.DataFrame, y=None, test=False):
        return df_source


def main():
    X, y = load_boston(return_X_y=True)
    df_x = pd.DataFrame(X)

    entry = BostonProcessFeature(name='boston_base', root_dir='./boston_simple')  # output to `./boston_simple`

    basic_xgb_feature = XGBoostRegressorOutOfFold(name='xgb_simple', parent=entry)  # normal XGBoost Model
    df = basic_xgb_feature.fit(df_x, y, force=True)  # fit

    for i, cols in df.T.iterrows():
        score = regression_metrics(y, cols.values)  # calculate regression metrics
        print(cols.name, score)


if __name__ == '__main__':
    main()

VividではAbstractFeatureを継承したクラス(BostonProcessFeature)を作成し、クラス変数のuse_columns に使用するカラム(この例にはないですが)を書いて、加工本体を call に実装するという流れで特徴量をOutOfFoldと後ろについているモデルクラスに渡します。

ちなみにモデルにBostonProcessFeatureクラスを渡さなくても動作はしますが、モデルの保存や評価結果を残したりの諸々の処理が行われなくなるので、極力BostonProcessFeatureクラスは作成すべきです。

fitすることでOut-of-Foldによる回帰の結果がpandasのデータフレームの形で出てくるという流れになっています。
printで得られるスコアの出力は、以下になりました。

xgb_simple__boston_base                              score
rmse                      2.970294
mean_squared_log_error    0.017799
median_absolute_error     1.463511
mean_squared_error        8.822643
r2_score                  0.895491
explained_variance_score  0.895524
mean_absolute_error       2.016873

また、結構foldごとの学習ログやモデル保存したよなどの動作ログも出てきて便利なのですが、文字数が多くなりすぎてもよくないのでこの記事では記載しないことにしますが、ぜひ実際に動かしてみて体験してみてください。

学習ログやモデルを保存してくれるだけでなく、変数重要度も以下のような感じでかなり綺麗にファイル出力してくれますし、プロットに使った変数重要度のデータもcsvで残してくれます。

変数重要度プロット

二値分類の場合

こちらは特にsampleが公開されてなかったので、私の方でirisで試してみました。

import pandas as pd
from sklearn.datasets import load_iris

from vivid.core import AbstractFeature
from vivid.metrics import binary_metrics
from vivid.out_of_fold.boosting import XGBoostClassifierOutOfFold


class IrisProcessFeature(AbstractFeature):
    def call(self, df_source: pd.DataFrame, y=None, test=False):
        return df_source

data = load_iris()
df_x = pd.DataFrame(data.data, columns=data.feature_names)
# 多クラス分類はサポートしていないっぽいため、'setosa', 'versicolor'の二値分類にする
df_x['target'] = data.target
df_tmp = df_x.query('target in [0, 1]')
df_train = df_tmp.drop('target', axis=1)
y = df_tmp['target'].values

entry = IrisProcessFeature(name='iris_base', root_dir='./iris_simple')

basic_xgb_feature = XGBoostClassifierOutOfFold(name='xgb_simple', parent=entry)
df = basic_xgb_feature.fit(df_train, y, force=True)  # fit

for i, cols in df.T.iterrows():
    score = binary_metrics(y, cols.values)  # calculate regression metrics
    print(cols.name, score)

多クラス分類には対応していないっぽいので、二値分類になるようにクラスを削っています。
こちらのprintで得られるスコアの出力は、以下になりました。

xgb_simple__iris_base                             score
accuracy_score           1.000000
f1_score                 1.000000
precision_score          1.000000
recall_score             1.000000
roc_auc_score            1.000000
log_loss                 0.030827
average_precision_score  1.000000

アンサンブルによる回帰

おそらくVividの目玉機能かなと思っているのですが、アンサンブルによる回帰は以下のように非常に簡単にできます。

import pandas as pd
from sklearn.datasets import load_boston

from vivid.core import AbstractFeature, EnsembleFeature
from vivid.metrics import regression_metrics
from vivid.out_of_fold.boosting import XGBoostRegressorOutOfFold, OptunaXGBRegressionOutOfFold


class BostonProcessFeature(AbstractFeature):
    def call(self, df_source: pd.DataFrame, y=None, test=False):
        return df_source


def main():
    X, y = load_boston(return_X_y=True)
    df_x = pd.DataFrame(X)

    entry = BostonProcessFeature(name='boston_base', root_dir='./boston_ens')

    basic_xgb_feature = XGBoostRegressorOutOfFold(name='xgb_simple', parent=entry)
    optuna_xgb = OptunaXGBRegressionOutOfFold(name='xgb_optuna', n_trials=10, parent=entry)
    ens = EnsembleFeature([optuna_xgb, basic_xgb_feature], name='ensumble', root_dir=entry.root_dir)

    df = ens.fit(df_x, y)

    for i, cols in df.T.iterrows():
        score = regression_metrics(y, cols.values)
        print(cols.name, score)


if __name__ == '__main__':
    main()

アンサンブルってなんかよくわからないですが、難しそうですよね。
しかし、なんとSingle Modelの時との違いは以下の2点だけです。

  • 新たにモデルのインスタタンスを作成(OptunaXGBRegressionOutOfFold)
  • EnsembleFeatureにモデルのインスタタンスを渡す→EnsembleFeatureのインスタンスでfitする

なので、誰でも簡単にアンサンブルを行うことができます。やったね。
こちらのprintで得られるスコアの出力は、以下になりました。

ensumble                              score
rmse                      2.981058
mean_squared_log_error    0.018178
median_absolute_error     1.363933
mean_squared_error        8.886704
r2_score                  0.894732
explained_variance_score  0.894743
mean_absolute_error       2.023016

分類については省略しますが、同じようにやればできます。

スタッキングによる回帰

スタッキングは以下のように実装できます。

import os

import pandas as pd
from sklearn.datasets import load_boston

from vivid.core import AbstractFeature, EnsembleFeature, MergeFeature
from vivid.featureset import AbstractAtom
from vivid.featureset.encodings import CountEncodingAtom
from vivid.metrics import regression_metrics
from vivid.out_of_fold.boosting import XGBoostRegressorOutOfFold, OptunaXGBRegressionOutOfFold, LGBMRegressorOutOfFold
from vivid.out_of_fold.boosting.block import create_boosting_seed_blocks
from vivid.out_of_fold.ensumble import RFRegressorFeatureOutOfFold
from vivid.out_of_fold.kneighbor import KNeighborRegressorOutOfFold
from vivid.out_of_fold.linear import RidgeOutOfFold


class BostonBasicAtom(AbstractAtom):
    def call(self, df_input, y=None):
        return df_input.copy()


class BostonCountEncoding(CountEncodingAtom):
    use_columns = ['']


class BostonProcessFeature(AbstractFeature):
    def call(self, df_source: pd.DataFrame, y=None, test=False):
        return df_source


def main():
    X, y = load_boston(return_X_y=True)
    df_x = pd.DataFrame(X)

    entry = BostonProcessFeature(name='boston_base', root_dir='./boston_stacking')

    singles = [
        XGBoostRegressorOutOfFold(name='xgb_simple', parent=entry),
        RFRegressorFeatureOutOfFold(name='rf', parent=entry),
        KNeighborRegressorOutOfFold(name='kneighbor', parent=entry),
        OptunaXGBRegressionOutOfFold(name='xgb_optuna', n_trials=200, parent=entry),
        *create_boosting_seed_blocks(feature_class=XGBoostRegressorOutOfFold, prefix='xgb_',
                                     parent=entry),  # seed averaging models
        *create_boosting_seed_blocks(feature_class=LGBMRegressorOutOfFold, prefix='lgbm_', parent=entry)
        # lgbm averaging models
    ]
    mreged_feature = MergeFeature([*singles, entry], root_dir=entry.root_dir,
                                  name='signle_merge')  # 一段目のモデル + もとのデータを merge した特徴量
    stackings = [
        RidgeOutOfFold(name='stacking_ridge', parent=mreged_feature, n_trials=10),
        OptunaXGBRegressionOutOfFold(name='stacking_xgb', parent=mreged_feature, n_trials=100),
    ]
    ens = EnsembleFeature(stackings[:], name='ensumble', root_dir=entry.root_dir)  # stacking のアンサンブル
    stackings.append(ens)

    df = pd.DataFrame()
    for feat in [*stackings, *singles]:
        f_df = feat.fit(df_x, y)
        df = pd.concat([df, f_df], axis=1)

    score_df = pd.DataFrame()
    for i, cols in df.T.iterrows():
        df_i = regression_metrics(y, cols.values)
        score_df[i] = df_i['score']

    score_df = score_df.T.sort_values('rmse')
    score_df.to_csv(os.path.join(entry.root_dir, 'score.csv'))
    print(score_df)


if __name__ == '__main__':
    main()

スタッキングモデルを作成する場合は最初の引数としてinput_featuresを持つMergeFeatureを親に設定します。
*create_boosting_seed_blocksはSeed Averagingを行うための関数です。
この例だと、EnsembleFeatureを使ってスタッキングモデルをさらにアンサンブルしています。
と言った形で、スタッキングを行い、さらにその結果をアンサンブルということもできるので、めちゃくちゃ便利です。

出力結果はいっぱい出るので、省略します。

おわりに

Vividはコンペ目的では非常に強力なフレームワークであると感じました。
何よりコードが全体的に綺麗で読みやすいという印象でした。

参考文献

Vivid
分析コンペLT会でLTをさせてもらいました!!