📚 背景知識(読んでから問題へ)
🎯
Day 062 — Day 061 のXGBoostに続き、現在Kaggleで最もよく使われるGBDTの一つ、LightGBM(Light Gradient Boosting Machine)を学びます。大規模データで圧倒的に速く、カテゴリカル変数をそのまま扱えます。
XGBoost vs LightGBM — 根本的な違い
| 観点 | XGBoost (level-wise) | LightGBM (leaf-wise) |
|---|---|---|
| 木の成長方向 | 深さを均一に広げる | 最も損失を下げる葉から広げる |
| 同じ葉数での精度 | 普通 | 高い(重要分岐に集中) |
| 過学習リスク | 普通 | やや高い(min_child_samplesで制御) |
| 大規模データの速度 | 普通 | 大幅に速い(ヒストグラム最適化) |
| カテゴリカル変数 | ラベルエンコード必須 | 直接処理可能 |
| メモリ効率 | 普通 | 低メモリ(ヒストグラムで圧縮) |
| 主な深さ制御パラメータ | max_depth | num_leaves(葉の最大数) |
なぜLightGBMは速いのか — 3つの技術
| 技術 | 内容 | 効果 |
|---|---|---|
| ヒストグラムアルゴリズム | 連続値をビンに分けて整数化。ソート不要で分割点探索が高速 | 大規模データで10倍以上速い |
| GOSS | 残差の大きいサンプルを優先し小さいものをランダム削除。学習量を削減 | 精度を保ちながらサンプル数を削減 |
| EFB | 同時にゼロでない特徴量をまとめてバンドリング。スパースな特徴量を圧縮 | 高次元データで特徴量数を削減 |
🌳 level-wise vs leaf-wise の成長比較
同じ「4回の分割」でどう木が成長するか — leaf-wiseは重要な分岐に集中
XGBoost: level-wise(深さ2)
LightGBM: leaf-wise(num_leaves=4)
leaf-wise は同じ分割回数でも重要な箇所に集中するため、小さいnum_leavesで高精度を達成できる。 ただし num_leaves が大きすぎると過学習するため min_child_samples とセットで制御する。
⚡ 速度・精度比較(n=100,000 の場合)
n_estimators=200 固定。大規模データほど LightGBM の優位性が大きくなる。
学習時間(小さいほど速い)
CV精度(大きいほど良い)
結論: 大規模データでは LightGBM が約6倍速く、かつ精度も高い。10万行以上のデータではまず LightGBM を選ぶ。
num_leaves と過学習の関係(n_estimators=200 固定)
num_leaves=31(デフォルト)は CV 精度のピーク付近。大きくするほど訓練精度は上がるが CV 精度は下がる(過学習)
🏷️ カテゴリカル変数の直接処理
LightGBMはカテゴリカル変数の最適二分割を内部で学習 — One-Hotより情報量が多い
LightGBMは各カテゴリをターゲット変数に対してソートし、最も情報利得の大きい二分割点を探す。 One-Hotより次元が増えず、かつランダムな分割よりも情報量が多い。
カテゴリカル変数の渡し方(3通り)
| 方法 | コード | 推奨度 |
|---|---|---|
| dtype='category' | df['Sex'] = df['Sex'].astype('category') |
★★★ 最も簡単・推奨 |
| categorical_feature パラメータ | fit(X, y, categorical_feature=['Sex']) |
★★★ fit時に指定 |
| LightGBM Dataset で指定 | lgb.Dataset(X, categorical_feature=['Sex']) |
★★☆ 低レベルAPIでは必須 |
⚙️ 主要パラメータ
num_leaves
木の最大葉数。leaf-wise の中心パラメータ。デフォルト=31。 大きいと過学習。2^max_depthより小さくする。
learning_rate
各木の寄与縮小率。XGBoostと同じ役割。Kaggleでは0.01〜0.05 + n_estimators多めが定石。
min_child_samples
葉ノードに必要な最小サンプル数(min_data_in_leaf)。デフォルト=20。 大きいと過学習抑制。小さいデータでは重要。
subsample / subsample_freq
行サブサンプリング率(bagging_fraction)。subsample_freq=1 も必須。 0.8が多い。
colsample_bytree
各木で使う列の割合。XGBoostと同名。0.7〜0.9が多い。 feature_fractionとも呼ぶ。
max_depth
デフォルト=-1(無制限)。 leaf-wiseでは num_leaves で制御するため通常は設定不要。小さいデータでは設定することも。
reg_alpha / reg_lambda
L1/L2正則化。XGBoostと同名。過学習時に増やす。デフォルトは両方0。
verbose=-1
必ず設定。 デフォルトだと大量のログが出る。sklearn APIでは verbose=-1 でログ非表示。
🗂️ データスキーマ(Titanic 拡張版 n=10,000)
| 列名 | 型 | 値の範囲 | 説明 | LightGBMでの取扱い |
|---|---|---|---|---|
Pclass | int | 1, 2, 3 | 旅客クラス | 数値として使用 |
Sex | str / category | male, female | 性別 | dtype='category' で直接処理 |
Age | float | 1〜80 | 年齢 | NaN自動処理 |
SibSp | int | 0〜8 | 同乗兄弟/配偶者数 | 数値として使用 |
Parch | int | 0〜6 | 同乗親/子供数 | 数値として使用 |
Fare | float | 0〜∞ | 運賃 | 対数変換推奨(外れ値) |
Embarked | str / category | S, C, Q | 乗船港 | dtype='category' で直接処理 |
Survived | int | 0 / 1 | 生存(目的変数) | — |
📝 問題
セットアップコード(最初に実行)
import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
import time
import warnings
warnings.filterwarnings('ignore')
try:
import lightgbm as lgb
print(f"LightGBM version: {lgb.__version__}")
except ImportError:
print("pip install lightgbm")
try:
import xgboost as xgb
print(f"XGBoost version: {xgb.__version__}")
except ImportError:
print("pip install xgboost")
np.random.seed(42)
n = 10000
data = pd.DataFrame({
'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
'Sex': np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
'Age': np.random.normal(29.7, 14.5, n).clip(1, 80),
'SibSp': np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
'Parch': np.random.choice(range(7), n, p=[0.76,0.13,0.09,0.005,0.005,0.005,0.005]),
'Fare': np.abs(np.random.normal(32, 50, n)),
'Embarked': np.random.choice(['S', 'C', 'Q'], n, p=[0.72, 0.19, 0.09]),
})
data['Survived'] = (
(data['Sex'] == 'female') * 0.55 + (data['Pclass'] == 1) * 0.25 +
(data['Age'] < 16) * 0.2 + np.random.normal(0, 0.12, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
data['Embarked_enc'] = LabelEncoder().fit_transform(data['Embarked'].fillna('S'))
features_enc = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked_enc']
X_enc = data[features_enc].values
y = data['Survived'].values
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
問1 — XGBoost vs LightGBM の基本比較
XGBClassifier(n_estimators=200)とLGBMClassifier(n_estimators=200)を同じデータ(エンコード済み)で StratifiedKFold(5) 比較せよ- 学習時間・CV精度・訓練精度の過学習度を記録せよ
- どちらが速く、どちらが精度が高かったか説明せよ
問2 — LightGBMのカテゴリカル変数直接処理
dtype='category'に変換した Sex・Embarked を LGBMClassifier に渡して学習せよ- ラベルエンコード済みバージョンとCV精度を比較せよ
- カテゴリカルを直接渡す際の注意点を説明せよ
問3 — leaf-wise vs level-wise の挙動確認
num_leavesを[8, 31, 64, 128, 256]で変化させながらCV精度と過学習度を記録せよnum_leavesが大きいと何が起こるか説明せよmax_depth=-1がデフォルトである理由を leaf-wise の観点から説明せよ
問4 — 主要パラメータの効果実験
learning_rate:[0.001, 0.01, 0.05, 0.1, 0.3](n_estimators=200固定)でCV精度を確認せよmin_child_samples:[5, 20, 50, 100, 200]で過学習度がどう変わるか確認せよsubsample:[0.5, 0.6, 0.8, 1.0]で変化させよ(subsample_freq=1も忘れずに)
問5 — 特徴量重要度と大規模データでの速度比較
- LightGBMの
feature_importances_(importance_type='gain')を取得・表示せよ - n=100,000 のデータを生成し、XGBoost と LightGBM の学習時間を比較せよ
- 大規模データで LightGBM が速い理由を2点述べよ
💡 ヒント
ヒント1(方向性)
pip install lightgbmでインストール。sklearn API があるのでLGBMClassifierをXGBClassifierと同じように使える- カテゴリカル変数を直接渡す場合、Pandasのカラムを
dtype='category'に変換する(文字列のままはエラー) verbose=-1を必ず指定。デフォルトだと大量のログが出るtime.time()で学習前後を計測して速度差を確認する
ヒント2(アプローチ)
import lightgbm as lgb
# sklearn API(XGBoostと同じ使い方)
clf_lgb = lgb.LGBMClassifier(
n_estimators=200,
random_state=42,
verbose=-1 # ログ非表示
)
# カテゴリカル直接処理
features_cat = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
X_cat = data[features_cat].copy()
X_cat['Sex'] = X_cat['Sex'].astype('category')
X_cat['Embarked'] = X_cat['Embarked'].astype('category')
clf_lgb.fit(X_cat, y)
cv_s = cross_val_score(clf_lgb, X_cat, y, cv=cv, scoring='accuracy').mean()
print(f"カテゴリカル直接: CV={cv_s:.4f}")
ヒント3(コード骨格)
# 問3: num_leaves の効果
for n_leaves in [8, 31, 64, 128, 256]:
clf = lgb.LGBMClassifier(
n_estimators=200, num_leaves=n_leaves,
random_state=42, verbose=-1
)
clf.fit(X_enc, y)
tr = clf.score(X_enc, y)
cv_s = cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
print(f"num_leaves={n_leaves:3d}: 訓練={tr:.4f}, CV={cv_s:.4f}, 差={tr-cv_s:.4f}")
# 問5: 大規模データ速度比較
n_large = 100_000
X_large = np.random.randn(n_large, 7)
y_large = (X_large[:, 0] + X_large[:, 1] > 0).astype(int)
for name, clf in [
("XGBoost ", xgb.XGBClassifier(n_estimators=200, eval_metric='logloss', random_state=42)),
("LightGBM", lgb.LGBMClassifier(n_estimators=200, random_state=42, verbose=-1)),
]:
t0 = time.time()
clf.fit(X_large, y_large)
print(f"[{name}] 学習時間: {time.time()-t0:.2f}s")
✅ 模範解答
import numpy as np
import pandas as pd
import time
import lightgbm as lgb
import xgboost as xgb
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')
# ── データ準備 ──
np.random.seed(42)
n = 10000
data = pd.DataFrame({
'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
'Sex': np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
'Age': np.random.normal(29.7, 14.5, n).clip(1, 80),
'SibSp': np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
'Parch': np.random.choice(range(7), n, p=[0.76,0.13,0.09,0.005,0.005,0.005,0.005]),
'Fare': np.abs(np.random.normal(32, 50, n)),
'Embarked': np.random.choice(['S', 'C', 'Q'], n, p=[0.72, 0.19, 0.09]),
})
data['Survived'] = (
(data['Sex'] == 'female') * 0.55 + (data['Pclass'] == 1) * 0.25 +
(data['Age'] < 16) * 0.2 + np.random.normal(0, 0.12, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
data['Embarked_enc'] = LabelEncoder().fit_transform(data['Embarked'].fillna('S'))
features_enc = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked_enc']
X_enc = data[features_enc].values
y = data['Survived'].values
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# ── 問1: XGBoost vs LightGBM 基本比較 ──
print("=" * 65)
print("問1: XGBoost vs LightGBM 基本比較 (n=10000, n_estimators=200)")
print("=" * 65)
for name, clf in [
("XGBoost ", xgb.XGBClassifier(n_estimators=200, eval_metric='logloss', random_state=42)),
("LightGBM", lgb.LGBMClassifier(n_estimators=200, random_state=42, verbose=-1)),
]:
t0 = time.time()
clf.fit(X_enc, y)
tr = clf.score(X_enc, y)
t_fit = time.time() - t0
cv_s = cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
print(f"[{name}] 訓練:{tr:.4f}, CV:{cv_s:.4f}, 過学習:{tr-cv_s:.4f}, fit:{t_fit:.2f}s")
# ── 問2: カテゴリカル変数の直接処理 ──
print("\n" + "=" * 65)
print("問2: カテゴリカル変数の直接処理")
print("=" * 65)
features_cat = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
X_cat = data[features_cat].copy()
X_cat['Sex'] = X_cat['Sex'].astype('category')
X_cat['Embarked'] = X_cat['Embarked'].astype('category')
cv_cat = cross_val_score(lgb.LGBMClassifier(n_estimators=200, random_state=42, verbose=-1),
X_cat, y, cv=cv, scoring='accuracy').mean()
cv_enc = cross_val_score(lgb.LGBMClassifier(n_estimators=200, random_state=42, verbose=-1),
X_enc, y, cv=cv, scoring='accuracy').mean()
print(f" カテゴリカル直接処理: CV={cv_cat:.4f}")
print(f" ラベルエンコード済み: CV={cv_enc:.4f}")
print(f" 差: {cv_cat - cv_enc:+.4f}")
# ── 問3: num_leaves の効果 ──
print("\n" + "=" * 65)
print("問3: num_leaves の効果(leaf-wise 成長)")
print("=" * 65)
for n_leaves in [8, 31, 64, 128, 256]:
clf = lgb.LGBMClassifier(n_estimators=200, num_leaves=n_leaves,
random_state=42, verbose=-1)
clf.fit(X_enc, y)
tr = clf.score(X_enc, y)
cv_s = cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
star = " ★デフォルト" if n_leaves == 31 else ""
print(f" num_leaves={n_leaves:3d}: 訓練={tr:.4f}, CV={cv_s:.4f}, 過学習={tr-cv_s:.4f}{star}")
# ── 問4: 主要パラメータ実験 ──
print("\n" + "=" * 65)
print("問4: 主要パラメータの効果")
print("=" * 65)
print(" [4-1] learning_rate:")
for lr in [0.001, 0.01, 0.05, 0.1, 0.3]:
cv_s = cross_val_score(
lgb.LGBMClassifier(n_estimators=200, learning_rate=lr, random_state=42, verbose=-1),
X_enc, y, cv=cv, scoring='accuracy').mean()
bar = "█" * int(cv_s * 40)
print(f" lr={lr:.3f}: CV={cv_s:.4f} {bar}")
print(" [4-2] min_child_samples:")
for mcs in [5, 20, 50, 100, 200]:
clf = lgb.LGBMClassifier(n_estimators=200, min_child_samples=mcs,
random_state=42, verbose=-1)
clf.fit(X_enc, y)
tr = clf.score(X_enc, y)
cv_s = cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
print(f" min_child_samples={mcs:3d}: 訓練={tr:.4f}, CV={cv_s:.4f}")
print(" [4-3] subsample:")
for ss in [0.5, 0.6, 0.8, 1.0]:
freq = 1 if ss < 1.0 else 0
cv_s = cross_val_score(
lgb.LGBMClassifier(n_estimators=200, subsample=ss, subsample_freq=freq,
random_state=42, verbose=-1),
X_enc, y, cv=cv, scoring='accuracy').mean()
print(f" subsample={ss}: CV={cv_s:.4f}")
# ── 問5: 特徴量重要度と大規模データ速度比較 ──
print("\n" + "=" * 65)
print("問5: 特徴量重要度 + 大規模データ速度比較")
print("=" * 65)
clf_lgb = lgb.LGBMClassifier(n_estimators=200, random_state=42,
verbose=-1, importance_type='gain')
clf_lgb.fit(X_enc, y)
print(" 特徴量重要度 (gain):")
for f, imp in sorted(zip(features_enc, clf_lgb.feature_importances_), key=lambda x: -x[1]):
bar = "█" * int(imp / max(clf_lgb.feature_importances_) * 30)
print(f" {f:12s}: {imp:8.1f} {bar}")
n_large = 100_000
X_large = np.random.randn(n_large, 7)
y_large = (X_large[:, 0] + X_large[:, 1] > 0).astype(int)
print("\n 大規模データ (n=100,000) 学習時間:")
for name, clf in [
("XGBoost ", xgb.XGBClassifier(n_estimators=200, eval_metric='logloss', random_state=42)),
("LightGBM", lgb.LGBMClassifier(n_estimators=200, random_state=42, verbose=-1)),
]:
t0 = time.time()
clf.fit(X_large, y_large)
print(f" [{name}] {time.time()-t0:.2f}s")
🪜 Step-by-Step 解説
1leaf-wise 成長の直感的理解
# leaf-wise を手動でシミュレーション
# 「最も損失を下げる葉を選んで分割」を確認する
import numpy as np
# 仮想: 3枚の葉の残差(二乗誤差)
leaves = {
'leaf_A': {'n': 100, 'residual_sq': 500.0}, # 損失大
'leaf_B': {'n': 50, 'residual_sq': 80.0}, # 損失中
'leaf_C': {'n': 200, 'residual_sq': 120.0}, # 損失小
}
print("現在の葉の損失:")
for name, leaf in leaves.items():
print(f" {name}: n={leaf['n']}, 損失={leaf['residual_sq']:.1f}")
# leaf-wise: 損失が最大の leaf_A を選ぶ(level-wiseは全部分割する)
best_leaf = max(leaves, key=lambda k: leaves[k]['residual_sq'])
print(f"\n→ leaf-wise が選ぶのは: {best_leaf}(損失が最大)")
print(" level-wise はこのラウンドで3枚全部を分割する(効率が悪い)")
# num_leaves の意味
print("\num_leaves の意味:")
print(" num_leaves=31: 最大31枚の葉まで成長できる")
print(" num_leaves=8: 最大8枚 → 単純なモデル(アンダーフィット気味)")
print(" num_leaves=256: 最大256枚 → 複雑なモデル(オーバーフィット気味)")
2カテゴリカル変数の最適分割を確認
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import cross_val_score, StratifiedKFold
import numpy as np
# LightGBMのカテゴリカル最適分割を確認
# カテゴリカルをターゲットでソートして最良の二分割を探す仕組み
np.random.seed(42)
n = 1000
df = pd.DataFrame({
'Pclass': np.random.choice([1,2,3], n),
'Sex': np.random.choice(['male','female'], n, p=[0.65,0.35]),
'Age': np.random.normal(29.7, 14.5, n).clip(1, 80),
})
df['Survived'] = ((df['Sex'] == 'female') * 0.55 + (df['Pclass'] == 1) * 0.3
+ np.random.normal(0, 0.1, n)) > 0.4
df['Survived'] = df['Survived'].astype(int)
y = df['Survived'].values
# 文字列: エラーになる
# lgb.LGBMClassifier().fit(df[['Sex']], y) # ValueError
# dtype='category': 正常に動く
df_cat = df[['Pclass', 'Sex', 'Age']].copy()
df_cat['Sex'] = df_cat['Sex'].astype('category')
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_s = cross_val_score(
lgb.LGBMClassifier(n_estimators=100, random_state=42, verbose=-1),
df_cat, y, cv=cv, scoring='accuracy').mean()
print(f"カテゴリカル直接処理: CV={cv_s:.4f}")
# category コードの確認
print(f"\nSexのカテゴリ: {df_cat['Sex'].cat.categories.tolist()}")
print(f"内部コード例: {df_cat['Sex'].cat.codes.head(5).tolist()}")
3early_stopping の使い方(LightGBM版)
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X_train, X_val, y_train, y_val = train_test_split(
X_enc, y, test_size=0.2, random_state=42, stratify=y
)
# sklearn API での early_stopping(LightGBM 4.x 以降)
clf = lgb.LGBMClassifier(
n_estimators=1000,
learning_rate=0.05,
num_leaves=31,
callbacks=[
lgb.early_stopping(stopping_rounds=30),
lgb.log_evaluation(period=-1), # ログ非表示
],
random_state=42,
verbose=-1
)
clf.fit(X_train, y_train, eval_set=[(X_val, y_val)])
print(f"最適な n_estimators: {clf.best_iteration_}")
print(f"バリデーション精度: {accuracy_score(y_val, clf.predict(X_val)):.4f}")
4Kaggle標準 LightGBM テンプレート
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import accuracy_score
lgb_params = {
'n_estimators': 1000,
'learning_rate': 0.05,
'num_leaves': 31,
'min_child_samples': 20,
'subsample': 0.8,
'subsample_freq': 1, # subsampleを毎ラウンド適用(必須)
'colsample_bytree': 0.8,
'reg_alpha': 0.1,
'reg_lambda': 1.0,
'random_state': 42,
'verbose': -1,
'n_jobs': -1,
}
clf = lgb.LGBMClassifier(**lgb_params)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof = cross_val_predict(clf, X_enc, y, cv=cv)
print(f"OOF精度: {accuracy_score(y, oof):.4f}")
clf.fit(X_enc, y)
for f, imp in sorted(zip(features_enc, clf.feature_importances_), key=lambda x: -x[1]):
print(f" {f}: {imp:.1f}")
📐 数学・統計の補足(文系向け)
ヒストグラムアルゴリズムを試験で例える
| 概念 | 試験の例え | 機械学習の対応 |
|---|---|---|
| 連続値をビンに分ける | 点数を「60〜69点台」などに区切る | float値を整数ビンに変換(uint8等) |
| ビン単位で分割を探す | 「60点台以上か未満か」で2グループ比較 | ビン境界で分割点探索。ソート不要 |
| 個票不要で集計だけ | 全員を並べなくても「60〜69点台:15人、生存:10人」でOK | O(n)→O(bin数)で大幅高速化 |
| メモリ節約 | 個票データ→集計表に圧縮 | float64→uint8(ビン番号)でメモリ1/8 |
ヒストグラムアルゴリズム: 連続値を256ビンに変換してから分割点を探す
🏆 Kaggleでの実践的な使い方
| 場面 | LightGBMを選ぶ理由 | 補足 |
|---|---|---|
| 大規模データ(>10万行) | 学習速度がXGBoostの2〜10倍速い | GBDTならまずLightGBMを試す |
| カテゴリカル変数が多い | ラベルエンコード不要・最適分割を自動学習 | dtype='category' に変換して渡す |
| メモリが限られる | ヒストグラムで圧縮しメモリ使用量が少ない | Kaggle notebook(16GB RAM)で有利 |
| Optuna チューニング | 速いため試行回数を多くできる | 100trialでも数分で完了 |
| アンサンブル | XGBoost・CatBoostとの相性が良い | 相関が低く組み合わせ効果大 |
| 小規模データ | XGBoostやRFが勝つこともある | 必ず比較する |
# Optuna で LightGBM をチューニングする例
import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'num_leaves': trial.suggest_int('num_leaves', 10, 100),
'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
'subsample': trial.suggest_float('subsample', 0.5, 1.0),
'subsample_freq': 1,
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
'random_state': 42, 'verbose': -1,
}
clf = lgb.LGBMClassifier(**params)
return cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50)
print(f"最良スコア: {study.best_value:.4f}")
print(f"最良パラメータ: {study.best_params}")
🚫 よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| num_leaves を大きくするほど良い | 「多い葉=高精度」という思い込み | 過学習する。小さいデータではデフォルト(31)以下が多い |
| max_depth を XGBoost と同じ感覚で設定する | XGBoostの知識を誤適用 | LightGBMはmax_depth=-1(無制限)がデフォルト。num_leavesで制御する |
| subsample だけ指定してsubsample_freqを忘れる | パラメータ名の違いを知らない | subsample_freq=1 か bagging_freq=1 も必須。ないとsubsampleが無効になる |
| 文字列のままカテゴリカルを渡す | 「直接渡せる」を誤解 | dtype='category' への変換か categorical_feature リストの指定が必要。文字列はエラー |
| LightGBM が常に XGBoost より良い | コンペ実績のイメージ | 小規模データや特定の問題ではXGBoost・CatBoostが勝つ。必ず比較する |
| verbose を設定しない | 初期設定のまま使う | verbose=-1 を必ず設定。デフォルトだと大量ログが出てJupyterが重くなる |
🚀 次のステップ
- 発展: CatBoostを試す(カテゴリカル処理がさらに強力・ordered boosting)。
early_stoppingの callbacks API を使う - 次回予告(Day 063): CatBoost 入門 — 順序付きブースティング・カテゴリカル処理の仕組み・3大GBDTの最終比較(XGBoost / LightGBM / CatBoost)
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: