LightGBM

中級

2.4.7

LightGBM

最終更新 2020-04-22 読了時間 3 分
まとめ
  • LightGBMはMicrosoftが開発した勾配ブースティングライブラリで、ヒストグラム近似と葉優先(Leaf-wise)の木構築で大規模データを高速に処理する。
  • カテゴリ変数のネイティブ処理、GPU学習、分散学習に対応し、前処理の手間を削減できる。
  • num_leaveslearning_raten_estimatorsmin_child_samplesが主なチューニング対象。

直感 #

通常の勾配ブースティングは木をレベルごとに均等に成長させるが、LightGBMは損失をもっとも減らせる葉を優先的に分割する(Leaf-wise)。これにより同じ木の数でも精度が出やすく、学習も速い。さらにヒストグラム近似で分割候補を大幅に減らすため、大規模データでも実用的な速度で動作する。

flowchart LR A["学習データ"] --> B["ヒストグラム\n近似で離散化"] B --> C["損失最大の\n葉を分割\n(Leaf-wise)"] C --> D["学習率ηで\n予測に加算"] D --> E{"収束?"} E -->|No| C E -->|Yes| F["最終予測"] style A fill:#2563eb,color:#fff style C fill:#1e40af,color:#fff style F fill:#10b981,color:#fff

アルゴリズムの詳細 #

ヒストグラムベースの分割探索 #

従来の勾配ブースティングでは、すべてのサンプル×すべての特徴量で分割候補を評価するため、計算量は\(O(n \times d)\)です。LightGBMは連続値の特徴量を固定個数のビン(デフォルト255個)に離散化し、ビン単位で勾配統計量を集計します。

$$ \text{計算量}: O(n \times d) \longrightarrow O(B \times d') $$

ここで\(B\)はビン数(\(\ll n\))、\(d’\)はバンドル後の特徴量数(\(\le d\))です。さらに、親ノードと一方の子ノードのヒストグラムの差分からもう一方の子ノードのヒストグラムを求める「ヒストグラム差分トリック」により計算量を半減させます。

GOSS(Gradient-based One-Side Sampling) #

大きな勾配を持つサンプルは情報量が大きく、小さな勾配のサンプルはすでによく学習されています。GOSSはこの性質を利用してサンプリングを行います。

  1. 全サンプルの勾配の絶対値でソートします
  2. 上位\(a \times 100\)%のサンプル(集合\(A\))をすべて保持します
  3. 残りの\((1-a) \times 100\)%からランダムに\(b \times 100\)%をサンプリングします(集合\(B\))
  4. \(B\)のサンプルに重み\(\frac{1-a}{b}\)を掛けてデータ分布を補正します

分割ゲインの近似式は以下のようになります。

$$ \tilde{V}_j(d) = \frac{1}{n}\left(\frac{\left(\sum_{x_i \in A_l} g_i + \frac{1-a}{b}\sum_{x_i \in B_l} g_i\right)^2}{n_l^j(d)} + \frac{\left(\sum_{x_i \in A_r} g_i + \frac{1-a}{b}\sum_{x_i \in B_r} g_i\right)^2}{n_r^j(d)}\right) $$

EFB(Exclusive Feature Bundling) #

高次元でスパースなデータでは、多くの特徴量が互いに排他的(同時に非ゼロにならない)です。EFBはこれらの特徴量をバンドルにまとめて1つの特徴量として扱います。

  1. 特徴量間の非排他性をグラフの辺として表現します
  2. グラフ彩色問題に帰着させ、貪欲法で近似的にバンドルを求めます
  3. 同一バンドル内の特徴量はオフセットを加えて1つの特徴量に統合します

これにより特徴量数が\(d\)から\(d’\)(バンドル数)に削減され、ヒストグラム構築の計算量がさらに減少します。

Leaf-wise成長 vs Level-wise成長 #

Level-wiseLeaf-wise
成長方法同じ深さのノードをすべて分割損失改善が最大の葉のみ分割
メリットバランスの良い木構造同じ葉数でより低い損失を達成
リスク不要な分割が発生しやすい過学習しやすい
制御max_depthnum_leaves(\(\le 2^{\text{max_depth}}\))
flowchart TD A["学習データ\n(N × D)"] --> B["EFB: 排他的特徴量を\nバンドル化 D→D'"] B --> C["ヒストグラム離散化\n(255 bins)"] C --> D["GOSS: 勾配ベースの\nサンプリング N→N'"] D --> E["Leaf-wise\n分割探索"] E --> F["損失改善が\n最大の葉を分割"] F --> G{"収束?"} G -->|No| D G -->|Yes| H["最終モデル"] style A fill:#2563eb,color:#fff style E fill:#1e40af,color:#fff style H fill:#10b981,color:#fff

参考リンク

Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., Ye, Q., & Liu, T.-Y. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. Advances in Neural Information Processing Systems (NeurIPS), 30.

詳細な解説 #

ライブラリと実験データ #

1
2
3
4
5
6
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from lightgbm import LGBMClassifier
1
2
3
4
5
6
7
X, y = make_classification(
    n_samples=1000, n_features=20, n_informative=10,
    n_redundant=5, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

基本的な学習と評価 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
model = LGBMClassifier(
    n_estimators=100,
    num_leaves=31,
    learning_rate=0.1,
    random_state=42,
    verbose=-1,
)
model.fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob):.4f}")

num_leavesの影響 #

num_leavesは木の最大葉数を制御します。大きくすると表現力が上がるが、過学習しやすくなります。max_depthよりも直接的にモデルの複雑さを決定するパラメーターです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
leaves_list = [8, 16, 31, 63, 127]
train_scores, test_scores = [], []
for nl in leaves_list:
    m = LGBMClassifier(
        n_estimators=100, num_leaves=nl,
        learning_rate=0.1, random_state=42, verbose=-1,
    )
    m.fit(X_train, y_train)
    train_scores.append(roc_auc_score(y_train, m.predict_proba(X_train)[:, 1]))
    test_scores.append(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]))

plt.figure(figsize=(8, 4))
plt.plot(leaves_list, train_scores, "o-", label="Train")
plt.plot(leaves_list, test_scores, "s--", label="Test")
plt.xlabel("num_leaves")
plt.ylabel("ROC-AUC")
plt.title("num_leaves vs ROC-AUC")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

num_leavesとROC-AUCの関係


learning_rateの影響 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
rates = [0.01, 0.05, 0.1, 0.3, 0.5]
train_scores, test_scores = [], []
for lr in rates:
    m = LGBMClassifier(
        n_estimators=200, num_leaves=31,
        learning_rate=lr, random_state=42, verbose=-1,
    )
    m.fit(X_train, y_train)
    train_scores.append(roc_auc_score(y_train, m.predict_proba(X_train)[:, 1]))
    test_scores.append(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]))

plt.figure(figsize=(8, 4))
plt.plot(rates, train_scores, "o-", label="Train")
plt.plot(rates, test_scores, "s--", label="Test")
plt.xlabel("learning_rate")
plt.ylabel("ROC-AUC")
plt.title("learning_rate vs ROC-AUC")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

learning_rateとROC-AUCの関係


min_child_samplesの影響 #

葉ノードの最小サンプル数を増やすと、過学習を抑制できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
min_samples = [5, 10, 20, 50, 100]
train_scores, test_scores = [], []
for ms in min_samples:
    m = LGBMClassifier(
        n_estimators=100, num_leaves=31,
        learning_rate=0.1, min_child_samples=ms,
        random_state=42, verbose=-1,
    )
    m.fit(X_train, y_train)
    train_scores.append(roc_auc_score(y_train, m.predict_proba(X_train)[:, 1]))
    test_scores.append(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]))

plt.figure(figsize=(8, 4))
plt.plot(min_samples, train_scores, "o-", label="Train")
plt.plot(min_samples, test_scores, "s--", label="Test")
plt.xlabel("min_child_samples")
plt.ylabel("ROC-AUC")
plt.title("min_child_samples vs ROC-AUC")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

min_child_samplesとROC-AUCの関係


特徴量の重要度 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
importances = model.feature_importances_
indices = np.argsort(importances)[::-1]

plt.figure(figsize=(10, 5))
plt.bar(range(len(importances)), importances[indices], color="#2563eb", alpha=0.8)
plt.xlabel("Feature Index")
plt.ylabel("Importance (split)")
plt.title("LightGBM Feature Importance")
plt.xticks(range(len(importances)), [f"f{i}" for i in indices], fontsize=9)
plt.show()

LightGBM特徴量重要度


Level-wiseとLeaf-wiseの違い #

通常の勾配ブースティング(XGBoostのデフォルトなど)はLevel-wise、すなわち深さごとに全ノードを分割します。一方LightGBMはLeaf-wise、つまり損失をもっとも減らせる葉だけを分割します。

  • Level-wise: バランスの取れた木になりやすく、過学習しにくい
  • Leaf-wise: 同じ葉数ならより低い損失を達成できるが、深くなりすぎると過学習する

このため、LightGBMではmax_depthよりもnum_leavesでモデルの複雑さを制御するのが基本です。

Leaf-wise成長のため、num_leavesを大きくしすぎると急速に過学習します。num_leaves2^max_depthより小さく設定するのが目安です。たとえばmax_depth=6ならnum_leaves63以下に抑えてください。

チューニングはnum_leaveslearning_ratemin_child_samplesn_estimatorsの順が効率的です。カテゴリ変数はOne-Hot Encodingせず、データ型をcategoryにしてそのまま渡すとLightGBMが最適な分割を自動で見つけます。

まとめ #

  • LightGBMはLeaf-wise成長とヒストグラム近似により、大規模データでも高速に勾配ブースティングを実行できます。
  • num_leavesがモデルの複雑さをもっとも直接的に制御するパラメーターで、max_depthより優先して調整します。
  • カテゴリ変数のネイティブ処理により、前処理の手間を削減しながら高い精度を実現できます。
  • TrainとTestのスコア乖離を確認しながらmin_child_sampleslearning_rateで過学習を抑制します。
  • Gradient Boosting(回帰) — 勾配ブースティングの基礎
  • XGBoost — 正則化重視の勾配ブースティング
  • CatBoost — カテゴリ変数に強い勾配ブースティング
  • SHAP — モデルの予測を特徴量ごとに分解して解釈
  • PDP / ICE — 特徴量と予測の関係を可視化

推定器数と LightGBM #

推定器数を増やすと LightGBM の予測がどう改善するか確認できます。