CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: LightGBM を使ってみる

LightGBM は Microsoft が開発した勾配ブースティング (Gradient Boosting) アルゴリズムを扱うためのフレームワーク。 勾配ブースティングは決定木 (Decision Tree) から派生したアルゴリズムで、複数の決定木を逐次的に構築したアンサンブル学習をするらしい。 勾配ブースティングのフレームワークとしては、他にも XGBoost なんかがよく使われているみたい。 調べようとしたきっかけは、データ分析コンペサイトの Kaggle で大流行しているのを見たため。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V        
Python 3.6.5

インストール

LightGBM は並列計算処理に OpenMP を採用しているので、まずはそれに必要なパッケージを入れておく。

$ brew install cmake gcc@7

あとは pip を使ってソースコードから LightGBM をビルドする。

$ export CXX=g++-7 CC=gcc-7
$ pip install --no-binary lightgbm lightgbm
$ pip list --format=columns | grep -i lightgbm
lightgbm        2.1.1  

多値分類問題 (Iris データセット)

それでは早速 LightGBM を使ってみる。

次のサンプルコードでは Iris データセットを LightGBM で分類している。 ポイントとしては LightGBM に渡すパラメータの目的 (objective) に multiclass (多値分類) を指定するところ。 そして、具体的なクラス数として num_class3 を指定する。 scikit-learn や numpy は LightGBM の依存パッケージとして自動的にインストールされるはず。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split

import numpy as np

"""LightGBM を使った多値分類のサンプルコード"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # データセットを生成する
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 多値分類問題
        'objective': 'multiclass',
        # クラス数は 3
        'num_class': 3,
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval)

    # テストデータを予測する
    y_pred = model.predict(X_test, num_iteration=model.best_iteration)
    y_pred_max = np.argmax(y_pred, axis=1)  # 最尤と判断したクラスの値にする

    # 精度 (Accuracy) を計算する
    accuracy = sum(y_test == y_pred_max) / len(y_test)
    print(accuracy)


if __name__ == '__main__':
    main()

細かい精度の検証が目的ではないので、交差検証はざっくり訓練データとテストデータに分けるだけに留めた。

上記に適当な名前をつけて実行してみよう。

$ python mc.py | tail -n 1
0.9736842105263158

今回の実行では精度 (Accuracy) として 97.36% が得られた。 ちなみに、この値は訓練データとテストデータの分けられ方に依存するので毎回異なったものになる。

scikit-learn インターフェース

LightGBM には scikit-learn に準拠したインターフェースも用意されている。 ネイティブな API と好みに合わせて使い分けられるのは嬉しい。

次のサンプルコードでは、先ほどと同じコードを scikit-learn インターフェースを使って書いてみる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split

import numpy as np

"""LightGBM を使った多値分類のサンプルコード (scikit-learn interface)"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # 上記のパラメータでモデルを学習する
    model = lgb.LGBMClassifier()
    model.fit(X_train, y_train)

    # テストデータを予測する
    y_pred = model.predict_proba(X_test)
    y_pred_max = np.argmax(y_pred, axis=1)  # 最尤と判断したクラスの値にする

    # 精度 (Accuracy) を計算する
    accuracy = sum(y_test == y_pred_max) / len(y_test)
    print(accuracy)


if __name__ == '__main__':
    main()

適当な名前でファイルに保存して実行してみよう。

$ python mcs.py | tail -n 1
0.9473684210526315

ちゃんと動いているようだ。

交差検証 (Cross Validation)

また、LightGBM にはブーストラウンドごとの評価関数の状況を交差検証で確認できる機能もある。

次のサンプルコードでは、先ほどと同じ Iris データセットを使った多値分類問題において、どのように学習が進むのかを可視化している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets

import numpy as np

from matplotlib import pyplot as plt

"""LightGBM を使った多値分類のサンプルコード (CV)"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # データセットを生成する
    lgb_train = lgb.Dataset(X, y)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 多値分類問題
        'objective': 'multiclass',
        # クラス数は 3
        'num_class': 3,
    }

    # 上記のパラメータでモデルを学習〜交差検証までする
    cv_results = lgb.cv(lgbm_params, lgb_train, nfold=10)
    cv_logloss = cv_results['multi_logloss-mean']
    round_n = np.arange(len(cv_logloss))

    plt.xlabel('round')
    plt.ylabel('logloss')
    plt.plot(round_n, cv_logloss)
    plt.show()


if __name__ == '__main__':
    main()

上記に適当な名前をつけたら実行してみよう。

$ python mcv.py

すると、次のようなグラフが得られる。 f:id:momijiame:20180501065447p:plain

上記は各ブーストラウンドごとの評価関数の値を折れ線グラフでプロットしている。 ブーストラウンド数はデフォルトで 100 になっており評価関数は LogLoss 損失関数になっている。 グラフを見ると損失が最も小さいのはラウンド数が 40 付近であり、そこを過ぎるとむしろ増えていることが分かる。

つまり、汎化性能を求めるにはブーストラウンド数を 40 あたりで止めたモデルにするのが望ましいことが分かる。 次のサンプルコードでは lightgbm.train() 関数のオプションとして num_boost_round に 40 を指定している。 これによって最適なブーストラウンド数で学習を終えている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets

"""LightGBM を使った多値分類のサンプルコード"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # データセットを生成する
    lgb_train = lgb.Dataset(X, y)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 多値分類問題
        'objective': 'multiclass',
        # クラス数は 3
        'num_class': 3,
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train, num_boost_round=40)


if __name__ == '__main__':
    main()

最適なブーストラウンド数を自動で決める

といっても、最適なブーストラウンド数を毎回確認して調整するのは意外と手間だったりもする。 そこで LightGBM には自動で決めるための機能として early_stopping_rounds というものが用意されている。 これは、モデルの評価用データを渡した状態で学習させて、性能が頭打ちになったところで学習を打ち切るというもの。

次のサンプルコードでは LightGBM.train()early_stopping_rounds オプションを渡して機能を有効にしている。 数値として 10 を渡しているので 10 ラウンド進めても性能に改善が見られなかったときは停止することになる。 この数値は、あまり小さいと局所最適解にはまりやすくなってしまう恐れもあるので気をつけよう。 学習ラウンド数は最大で 1000 まで回るように num_boost_round オプションで指定している。 注意点としては、前述した通りこの機能を使う際は学習用データとは別に評価用データを valid_sets オプションで渡す必要がある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

import numpy as np

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

"""LightGBM を使った多値分類のサンプルコード"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # データセットを学習用とテスト用に分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    # テスト用のデータを評価用と検証用に分ける
    X_eval, X_valid, y_eval, y_valid = train_test_split(X_test, y_test, random_state=42)

    # データセットを生成する
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_eval = lgb.Dataset(X_eval, y_eval, reference=lgb_train)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 多値分類問題
        'objective': 'multiclass',
        # クラス数は 3
        'num_class': 3,
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train,
                      # モデルの評価用データを渡す
                      valid_sets=lgb_eval,
                      # 最大で 1000 ラウンドまで学習する
                      num_boost_round=1000,
                      # 10 ラウンド経過しても性能が向上しないときは学習を打ち切る
                      early_stopping_rounds=10)

    # 学習したモデルでホールドアウト検証する
    y_pred_proba = model.predict(X_valid, num_iteration=model.best_iteration)
    # 返り値は確率になっているので最尤に寄せる
    y_pred = np.argmax(y_pred_proba, axis=1)

    # 精度 (Accuracy) を計算する
    accuracy = accuracy_score(y_valid, y_pred)
    print(accuracy)


if __name__ == '__main__':
    main()

また、ホールドアウト検証するときは評価用データとはまた別に検証用データを用意する必要がある点にも注意しよう。 評価用データで得られた精度はパラメータの調整に使ってしまっているので、それで確認しても正しい検証はできない。

上記を実行すると最大 1000 ラウンドまでいくはずが性能が頭打ちになって 55 で停止していることがわかる。

$ python mes.py 
[LightGBM] [Info] Total Bins 89
[LightGBM] [Info] Number of data: 112, number of used features: 4
...(snip)...
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[65]   valid_0's multi_logloss: 0.060976
Early stopping, best iteration is:
[55]  valid_0's multi_logloss: 0.0541825
1.0

最終的にホールドアウト検証で精度が 100% として得られている。 ただ、これはちょっと分割を重ねすぎてデータが少なくなりすぎたせいかも。

特徴量の重要度の可視化

LightGBM では各特徴量がどれくらい予測に寄与したのか数値で確認できる。

次のサンプルコードでは lightgbm.plot_importance() 関数を使って特徴量の重要度を棒グラフでプロットしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split

from matplotlib import pyplot as plt

"""LightGBM を使った特徴量の重要度の可視化"""


def main():
    # Iris データセットを読み込む
    iris = datasets.load_iris()
    X, y = iris.data, iris.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # データセットを生成する
    lgb_train = lgb.Dataset(X_train, y_train, feature_name=iris.feature_names)
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 多値分類問題
        'objective': 'multiclass',
        # クラス数は 3
        'num_class': 3,
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval, num_boost_round=40)

    # 特徴量の重要度をプロットする
    lgb.plot_importance(model, figsize=(12, 6))
    plt.show()


if __name__ == '__main__':
    main()

上記に適当な名前をつけて実行しよう。

$ python mci.py

すると、次のようなグラフが得られる。 f:id:momijiame:20180501073242p:plain

上記を見ると Iris を識別するための特徴量としては Petal Width と Petal Length が重要なことが分かる。

回帰問題 (Boston データセット)

続いては Boston データセットを使った回帰問題に取り組んでみよう。 データセットには住宅価格を予測する Boston データセットを用いた。

早速だけどサンプルコードは次の通り。 多値分類問題とは、学習時に渡すパラメータしか違わない。 具体的には目的 (Objective) に regression を渡して、評価関数に rmse (Root Mean Squared Error) を指定している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

import numpy as np

"""LightGBM を使った回帰のサンプルコード"""


def main():
    # Boston データセットを読み込む
    boston = datasets.load_boston()
    X, y = boston.data, boston.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # データセットを生成する
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 回帰問題
        'objective': 'regression',
        # RMSE (平均二乗誤差平方根) の最小化を目指す
        'metric': 'rmse',
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval)

    # テストデータを予測する
    y_pred = model.predict(X_test, num_iteration=model.best_iteration)

    # RMSE を計算する
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    print(rmse)


if __name__ == '__main__':
    main()

上記に適当な名前をつけて実行しよう。

$ python rb.py | tail -n 1
2.686275348587732

テストデータに対して RMSE が約 2.68 として得られた。 尚、訓練データとテストデータの分けられ方によって毎回出る値が異なる点については最初の問題と同じ。

scikit-learn インターフェース

続いては回帰問題を scikit-learn インターフェースで解いてみる。

サンプルコードは次の通り。 scikit-learn インターフェースにおいて回帰問題を解くときは lightgbm.LGBMRegressor クラスを使う。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

import numpy as np

"""LightGBM を使った回帰のサンプルコード (scikit-learn interface)"""


def main():
    # Boston データセットを読み込む
    boston = datasets.load_boston()
    X, y = boston.data, boston.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # 上記のパラメータでモデルを学習する
    model = lgb.LGBMRegressor()
    model.fit(X_train, y_train)

    # テストデータを予測する
    y_pred = model.predict(X_test)

    # RMSE を計算する
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    print(rmse)


if __name__ == '__main__':
    main()

実行結果の内容については先ほどと変わらないので省略する。

二値分類問題 (Breast Cancer データセット)

続いては Breast Cancer データセットを使った二値分類問題について。

サンプルコードは次の通り。 これまでの内容からも分かる通り、学習において変更すべき点は渡すパラメータ部分のみ。 今度は目的として binary を指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn import metrics

"""LightGBM を使った二値分類のサンプルコード"""


def main():
    # Breast Cancer データセットを読み込む
    bc = datasets.load_breast_cancer()
    X, y = bc.data, bc.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # データセットを生成する
    lgb_train = lgb.Dataset(X_train, y_train)
    lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)

    # LightGBM のハイパーパラメータ
    lgbm_params = {
        # 二値分類問題
        'objective': 'binary',
        # AUC の最大化を目指す
        'metric': 'auc',
    }

    # 上記のパラメータでモデルを学習する
    model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval)

    # テストデータを予測する
    y_pred = model.predict(X_test, num_iteration=model.best_iteration)

    # AUC (Area Under the Curve) を計算する
    fpr, tpr, thresholds = metrics.roc_curve(y_test, y_pred)
    auc = metrics.auc(fpr, tpr)
    print(auc)


if __name__ == '__main__':
    main()

上記に名前をつけて実行しよう。

$ python bc.py | tail -n 1
0.9920212765957447

AUC として 99.2% という結果が得られた。

scikit-learn インターフェース

同様に scikit-learn インターフェースからも使ってみる。 とはいえ、これに関しては最初に紹介した Iris データセットを使った多値分類問題と変わらない。 lightgbm.LGBMClassifier を使えば二値問題も多値問題も同じように扱うことができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import lightgbm as lgb

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn import metrics

"""LightGBM を使った二値分類のサンプルコード (scikit-learn interface)"""


def main():
    # Breast Cancer データセットを読み込む
    bc = datasets.load_breast_cancer()
    X, y = bc.data, bc.target

    # 訓練データとテストデータに分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    # 上記のパラメータでモデルを学習する
    model = lgb.LGBMClassifier()
    model.fit(X_train, y_train)

    # テストデータを予測する
    y_pred = model.predict(X_test)

    # AUC (Area Under the Curve) を計算する
    fpr, tpr, thresholds = metrics.roc_curve(y_test, y_pred)
    auc = metrics.auc(fpr, tpr)
    print(auc)


if __name__ == '__main__':
    main()

実行結果の内容については先ほどと変わらない。

まとめ

今回は勾配ブースティングアルゴリズムを扱うフレームワーク LightGBM を試してみた。 使ってみて、勾配ブースティングによる性能の高さはもちろん、細部まで使いやすさに配慮されている印象を受けた。 同じアルゴリズムを分類にも回帰にも応用できる上、CV や特徴量の重要度まで確認できる。 計算量も Deep Learning ほど大きくないし、最近のコンペで Winning Solution を獲得した実績も多い。 これは Kaggle で流行る理由もうなずけるね。