CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: LightGBM の cv() 関数から得られるモデルの特徴量の重要度を可視化してみる

今回は 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 以降でも利用できる。

blog.amedama.jp

複数の学習済み 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 件について表示している。

f:id:momijiame:20200822195419p:plain
LightGBM の cv() 関数から得られた複数の Booster から可視化した特徴量の重要度

やはり、先頭の 5 次元は特徴量の重要度が高い。 一方で、それ以外の特徴量も多少なりとも重要であるとモデルが考えていることが見て取れる。 ここらへんは Null Importance などを使うことで判断できるだろう。

blog.amedama.jp

補足

元々は棒グラフとエラーバーを使って可視化することを考えていた。 しかし、エラーバーに表示する内容に何を使うのが適切か悩んで Twitter につぶやいたところ、以下のような助言をいただくことができた。

ありがとうございます。 全人類は Kaggle 本を買おう。