今回は LightGBM の cv() 関数から得られる複数の学習済み Booster から特徴量の重要度を取り出して可視化してみる。 それぞれの Booster 毎のバラつきなどから各特徴量の傾向などが確認できるかもしれない。
使った環境は次のとおり。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.6 BuildVersion: 19G2021 $ python -V Python 3.8.5
下準備
あらかじめ必要なパッケージをインストールしておく。 なお、LightGBM のバージョン 3.0 以降 (2020-08-22 現在 RC1) をインストールしておくと学習済みモデルを取り出すのが楽になる。
$ pip install "lightgbm>=3.0.0rc1" scikit-learn seaborn
バージョン 3.0 以前の場合には次の記事を参照のこと。 ちなみに、以下のコールバックを使うやり方はバージョン 3.0 以降でも利用できる。
複数の学習済み Booster の特徴量の重要度を可視化する
早速だけど以下にサンプルコードを示す。 このサンプルコードでは、擬似的に生成した二値分類のデータセットを使っている。 状況としては、特徴量は 100 次元あるものの、その中で本当に有益なものは先頭の 5 次元しかない。 複数の学習済み Booster から得られる特徴量の重要度を可視化するには箱ひげ図を使った。 箱ひげ図の項目は、重要度の平均値を使ってソートしている。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import lightgbm as lgb from matplotlib import pyplot as plt import seaborn as sns import pandas as pd from sklearn.datasets import make_classification from sklearn.model_selection import StratifiedKFold def main(): # 疑似的な教師データを作るためのパラメータ dist_args = { # データ点数 'n_samples': 10_000, # 次元数 'n_features': 100, # その中で意味のあるもの 'n_informative': 5, # 重複や繰り返しはなし 'n_redundant': 0, 'n_repeated': 0, # タスクの難易度 'class_sep': 0.65, # 二値分類問題 'n_classes': 2, # 生成に用いる乱数 'random_state': 42, # 特徴の順序をシャッフルしない (先頭の次元が informative になる) 'shuffle': False, } # 教師データを作る train_x, train_y = make_classification(**dist_args) # 擬似的な特徴量の名前 feature_names = [f'col{i}' for i in range(dist_args['n_features'])] # LightGBM が扱うデータセットの形式に直す lgb_train = lgb.Dataset(train_x, train_y, feature_name=feature_names) # 学習用のパラメータ lgbm_params = { 'objective': 'binary', 'metric': 'binary_logloss', 'first_metric_only': True, 'verbose': -1, } # データの分割方法 folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42, ) # 交差検証 cv_result = lgb.cv(lgbm_params, lgb_train, folds=folds, num_boost_round=1_000, early_stopping_rounds=100, verbose_eval=100, # 学習済みモデルを取り出す (v3.0 以降) return_cvbooster=True, ) # 学習済みモデルから特徴量の重要度を取り出す cvbooster = cv_result['cvbooster'] raw_importances = cvbooster.feature_importance(importance_type='gain') feature_name = cvbooster.boosters[0].feature_name() importance_df = pd.DataFrame(data=raw_importances, columns=feature_name) # 平均値でソートする sorted_indices = importance_df.mean(axis=0).sort_values(ascending=False).index sorted_importance_df = importance_df.loc[:, sorted_indices] # 上位をプロットする PLOT_TOP_N = 20 plot_cols = sorted_importance_df.columns[:PLOT_TOP_N] _, ax = plt.subplots(figsize=(8, 8)) ax.grid() ax.set_xscale('log') ax.set_ylabel('Feature') ax.set_xlabel('Importance') sns.boxplot(data=sorted_importance_df[plot_cols], orient='h', ax=ax) plt.show() if __name__ == '__main__': main()
上記を実行してみよう。
$ python lgbcvimp.py [100] cv_agg's binary_logloss: 0.280906 + 0.0141782 [200] cv_agg's binary_logloss: 0.280829 + 0.018406
すると、次のようなグラフが得られる。 重要度の平均値を上位 20 件について表示している。
やはり、先頭の 5 次元は特徴量の重要度が高い。 一方で、それ以外の特徴量も多少なりとも重要であるとモデルが考えていることが見て取れる。 ここらへんは Null Importance などを使うことで判断できるだろう。
補足
元々は棒グラフとエラーバーを使って可視化することを考えていた。 しかし、エラーバーに表示する内容に何を使うのが適切か悩んで Twitter につぶやいたところ、以下のような助言をいただくことができた。
そうですね.
— HoxoMaxwell!😷 (@Maxwell_110) 2020年7月9日
箱ひげ図のよいところは,確率分布を仮定しておらず,サンプル数が多かろうが少なかろうが関係がないところだと思います.
データ量もどんどん増えている世の中ですから,1 fold 当たりの計算コストもそれなりにあると思います.N が少ない前提で考えるのが妥当かなと私も思いました.
ありがとうございます。 全人類は Kaggle 本を買おう。