Python 機械学習 LightGBM入門|仕組みから実装・チューニングまで整理

AI

機械学習を勉強していると、Kaggleの上位解法にやたら「LightGBM」という名前が出てくるのに気づく。最初は「なんか強そうな名前だな」くらいに思ってたんですが、ちゃんと使えるようになると確かに便利で、しかも思ったより難しくなかった。

この記事では、LightGBMの仕組みをざっくり理解したうえで、Pythonでの実装・主要パラメータ・特徴量重要度の確認まで、自分が学んだことを整理しています。

  • LightGBMがなぜ速いのか(Leaf-wise・GOSS・EFBの概要)
  • scikit-learn APIとネイティブAPIの両方での実装例
  • 過学習対策を含む主要パラメータの整理
  • カテゴリ変数の扱い方
  • 特徴量重要度・SHAPによる可視化
  • OptunaによるハイパーパラメータのAuto Tuning

LightGBMってそもそも何者か

LightGBMは、Microsoftが開発した勾配ブースティング(Gradient Boosting)フレームワークです。GBDT(勾配ブースティング決定木)系の中でも、学習を高速化するための工夫がいろいろ入っています。

ベースにある考え方は「勾配ブースティング決定木(GBDT)」。複数の決定木を直列に積み重ねて、前の木が出した誤差を次の木が補正していく……という仕組みです。XGBoostも同じGBDTの一種なんですが、LightGBMとはツリーの成長(分割)のさせ方が違います。

Level-wise vs Leaf-wise

XGBoostは一般に「Level-wise(depth-wise)」と呼ばれる、同じ深さのノードを横に広げていく成長のさせ方として紹介されることが多いです。バランスの取れた木になりやすい反面、同じ深さを一気に評価するので計算が重くなりがち、という説明で語られます。

一方LightGBMは「Leaf-wise(葉優先)」を採用しています。損失を最も大きく減らせる葉を優先的に分割していく方式で、効率よく損失を下げられる反面、木が非対称に成長しやすく、過学習しやすい(特にデータが小さいとき)点には注意が必要です。

余談ですが、このLeaf-wiseって最初に聞いたとき「それ大丈夫なの?」ってなったんですが、過学習を抑える方向では num_leavesmax_depth あたりで複雑さを縛るのが基本っぽいです。

LightGBMが速い理由:GOSSとEFB

LightGBM独自の技術として、以下の2つがよく挙げられます。

  • GOSS(Gradient-based One-Side Sampling):勾配の大きいサンプルを優先的に残し、勾配の小さいサンプルは一部をサンプリングして学習を進める考え方。
  • EFB(Exclusive Feature Bundling):同時に非ゼロになりにくい(排他的になりやすい)特徴量をまとめて扱い、実質的な次元を圧縮する考え方。

正直このあたりの内部実装はまだちゃんと追えていないんですが、「高速化のための理由がちゃんとある」という方向性は合ってるはず。

インストールと基本的な実装

まずインストール。普通に pip で入ります。

pip install lightgbm

LightGBMにはAPIの使い方が大きく2つあります。

  • scikit-learn APILGBMClassifier / LGBMRegressor を使う。sklearn と同じ感覚で書ける。
  • ネイティブAPIlgb.Dataset を使って lgb.train() で学習する。より細かい制御ができる。

個人的には scikit-learn API のほうが使いやすいので、まずそっちから。

scikit-learn APIでの分類タスク

breast_cancer データセットを使った二値分類の例です。

import lightgbm as lgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = lgb.LGBMClassifier(
    num_leaves=31,
    learning_rate=0.05,
    n_estimators=300,
    random_state=42,
    verbose=-1
)

model.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
)

pred = model.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, pred):.4f}")

verbose=-1 を入れないと大量のログが出るので、普段は黙らせておくのがおすすめです。これは地味に最初ハマりました。

ネイティブAPIでの回帰タスク

California housing データセットを使った回帰の例。ネイティブAPIでは lgb.Dataset にデータを渡して lgb.train() で学習します。early_stopping を入れておくと、検証データのスコアが改善しなくなったタイミングで自動で学習が止まるので便利です。

import lightgbm as lgb
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

train_set = lgb.Dataset(X_train, label=y_train)
valid_set = lgb.Dataset(X_test, label=y_test)

params = {
    "objective": "regression",
    "metric": "rmse",
    "num_leaves": 63,
    "learning_rate": 0.05,
    "feature_fraction": 0.8,
    "bagging_fraction": 0.8,
    "bagging_freq": 1,
    "verbosity": -1,
}

model = lgb.train(
    params=params,
    train_set=train_set,
    num_boost_round=500,
    valid_sets=[valid_set],
    callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)],
)

主要なパラメータを整理する

LightGBMはパラメータが本当に多くて、最初は何を触ればいいかわからなくなります。とりあえず覚えておくべきものだけピックアップします。

精度に直結するパラメータ

  • num_leaves(デフォルト: 31):1本の木が持てる最大の葉の数。大きいほど複雑なモデルになるが過学習しやすい。Leaf-wise を採用しているLightGBMでは重要な複雑度のつまみです。
  • learning_rate(デフォルト: 0.1):学習率。小さくすると安定するが、そのぶん木の数(n_estimators / num_iterations)を増やす必要が出てきます。
  • n_estimators / num_boost_round(デフォルト: 100):木の数(反復回数)。early stopping と組み合わせて、ちょっと大きめにしておくのがやりやすいです。
  • max_depth(デフォルト: -1、制限なし):木の最大深さ。num_leaves と合わせて過学習を抑えるために使います。

過学習対策パラメータ

  • feature_fraction(デフォルト: 1.0):各反復で使う特徴量の割合。
  • bagging_fraction + bagging_freq:データのサブサンプリング設定。bagging_freq を 0 以外にしないと bagging は有効にならない、という点は忘れがちなので注意。
  • min_child_samples(デフォルト: 20):葉が持てるサンプルの最小数(min_data_in_leaf の別名)。増やすと過学習を抑制しやすいです。
  • reg_alpha / reg_lambda:L1/L2正則化。デフォルトはどちらも 0.0。パラメータ名としては lambda_l1 / lambda_l2 の別名扱いになってます。

正直、最初はデフォルト設定でもそこそこ動くので、まず動かしてから徐々に調整していく方がいいと思っています。全部いじろうとすると沼にはまります。

カテゴリ変数の扱い

LightGBMの地味に便利な機能として、カテゴリ変数をそのまま扱えるというものがあります。categorical_feature を指定したり、pandas のカテゴリ型を使ったりして、カテゴリ特徴量として扱えます。

import pandas as pd
import lightgbm as lgb

# カテゴリ列を category 型にしておく
df["category_col"] = df["category_col"].astype("category")

train_set = lgb.Dataset(X_train, label=y_train)

params = {
    "objective": "binary",
    "metric": "binary_logloss",
    "verbosity": -1,
}

# categorical_feature を指定するか、pandas の category 型で自動判別(データの渡し方次第)
model = lgb.train(params, train_set, categorical_feature=["category_col"])

pandas の category 型を使うと自動判別されるケースがあります(渡すデータが pandas.DataFrame で、カテゴリ列がちゃんと含まれているなど条件次第)。一方で、カテゴリの基数(ユニーク数)が多すぎると効きが悪くなることもあるので、状況によっては手動でエンコードした方がいい場面も出てきます。

特徴量重要度で「何が効いているか」を確認する

学習後にモデルがどの特徴量を重視したかを確認できます。モデルの解釈に使えるし、不要な特徴量を削る手がかりにもなります。

import matplotlib.pyplot as plt

lgb.plot_importance(model, max_num_features=20, importance_type="gain")
plt.tight_layout()
plt.show()

importance_type"split"(分割に使われた回数)と "gain"(分割によって得られた利得の合計)の2種類があります。"gain" のほうが「どれだけ損失を下げたか」寄りの見方になるので、個人的にはこっちをよく使っています。

さらに一歩進んで、SHAPを使うと「特定のサンプルに対してどの特徴量がどう影響したか」まで可視化できます。実務で「なんでこの予測になったの?」と聞かれたときに強い味方になります。

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

shap.summary_plot(shap_values, X_test)

SHAPはまだちゃんと使いこなせていない部分が多いんですが、summary_plot を出すだけでも結構いろんなことがわかるので、とりあえず見てみる価値はあります。

XGBoostとどう使い分けるか

同じGBDTなのでよく比較される2つですが、雑に整理するとこんな感じです。

  • LightGBM:大規模データ・高次元データで速いことが多い。カテゴリ変数の扱いがやりやすい。Leaf-wiseなので過学習に注意が必要。
  • XGBoost:レベル(深さ)ごとに成長させる説明で語られることが多く、比較的バランスの取れた木になりやすい、という整理をよく見ます。

学習時間はデータサイズや設定にもよりますが、傾向としてはLightGBMが速いケースが多いです。どちらが「絶対に強い」というわけではなく、データやタスクによって変わるので、両方試してみるのが結局一番な気がします。

Optunaでハイパーパラメータを自動チューニングする

ハイパーパラメータの探索は手動でやるのが大変なので、Optunaと組み合わせるのが定番です。

import optuna
import lightgbm as lgb
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer

X, y = load_breast_cancer(return_X_y=True)

def objective(trial):
    params = {
        "verbosity": -1,
        "objective": "binary",
        "metric": "binary_logloss",
        "num_leaves": trial.suggest_int("num_leaves", 20, 150),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.1, log=True),
        "feature_fraction": trial.suggest_float("feature_fraction", 0.5, 1.0),
        "bagging_fraction": trial.suggest_float("bagging_fraction", 0.5, 1.0),
        "bagging_freq": 1,
        "min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
    }
    model = lgb.LGBMClassifier(**params, n_estimators=300)
    score = cross_val_score(model, X, y, cv=5, scoring="accuracy").mean()
    return score

optuna.logging.set_verbosity(optuna.logging.WARNING)
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)

print("Best params:", study.best_params)
print("Best score:", study.best_value)

グリッドサーチより賢く探索してくれるので、50〜100 trial もやれば実用的なパラメータが見つかることが多いです。ランダムサーチよりも効率的に解を探してくれる点が地味に助かります。

まだ自分もOptunaを使い始めたばかりで、「どの範囲を探索させればいいか」の感覚がまだあんまりないんですが、とりあえず動かしながら覚えていくのがよさそうです。

まとめ

テーブルデータの予測タスクを始めるとき、LightGBMはとりあえず一番最初に手を出していい選択肢だと思います。デフォルトでもそれなりに動いて、チューニングすればさらに伸びる。

改めて整理すると、この記事では以下をカバーしました。

  • Leaf-wise・GOSS・EFBによる高速化の仕組み
  • scikit-learn API / ネイティブAPI それぞれの実装例
  • 精度・過学習対策の主要パラメータ
  • カテゴリ変数の扱いと特徴量重要度の確認
  • Optunaによるハイパーパラメータ探索

パラメータを全部把握しようとすると頭が爆発するので、まず動かしてみて、気になったパラメータから少しずつ理解していくのが自分には合っていました。Kaggleのnotebookや公式ドキュメントを合わせて読むと、より実践的な使い方が見えてくると思います。

参考になったらクリックしてもらえると嬉しいです!

Blogmura AIAI Ranking
タイトルとURLをコピーしました