콘텐츠로 이동

LightGBM (Light Gradient Boosting Machine)

논문 정보

항목 내용
제목 LightGBM: A Highly Efficient Gradient Boosting Decision Tree
저자 Guolin Ke, Qi Meng, Thomas Finley, Taifeng Wang, Wei Chen, Weidong Ma, Qiwei Ye, Tie-Yan Liu
학회/저널 NeurIPS (NIPS)
연도 2017
링크 https://proceedings.neurips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html

개요

문제 정의

기존 GBDT (Gradient Boosting Decision Tree)의 효율성 문제:

  • 대규모 데이터에서 학습 시간이 너무 김
  • 모든 데이터/특성을 탐색하는 비효율
  • 메모리 사용량 과다

핵심 아이디어

두 가지 핵심 기법:

  1. GOSS (Gradient-based One-Side Sampling):
  2. Gradient가 큰 샘플은 모두 유지
  3. Gradient가 작은 샘플은 무작위 샘플링
  4. 정보량 손실 최소화하며 데이터 감소

  5. EFB (Exclusive Feature Bundling):

  6. 상호 배타적인 특성들을 하나로 묶음
  7. 희소 데이터에서 특성 수 대폭 감소
  8. 그래프 컬러링 문제로 해결

추가 최적화:

  • Leaf-wise 트리 성장 (vs. Level-wise)
  • Histogram-based 분할 탐색
  • 병렬/분산 학습 최적화

알고리즘/수식

GOSS (Gradient-based One-Side Sampling)

Algorithm: GOSS

Input: 학습 데이터, 상위 gradient 비율 a, 하위 샘플링 비율 b
Output: 샘플링된 데이터

1. Sort samples by |gradient| in descending order

2. Select top a * 100% samples (high gradient) -> set A

3. Randomly sample b * 100% from remaining samples -> set B

4. Amplify weight of samples in B by (1-a)/b
   (보정 계수: 언더샘플링된 작은 gradient 보상)

5. Return A union B with adjusted weights

왜 작동하는가: - Gradient가 큰 샘플: 학습에 더 많은 정보 기여 - Gradient가 작은 샘플: 이미 잘 학습됨, 일부만 필요 - 가중치 보정으로 편향 최소화

EFB (Exclusive Feature Bundling)

배경: 희소 특성들은 동시에 0이 아닌 값을 갖는 경우가 드묾

Algorithm: EFB

1. 특성 간 충돌 그래프 구성
   - 노드: 각 특성
   - 엣지: 동시에 non-zero인 샘플 수 (충돌)

2. 그래프 컬러링으로 번들 생성
   - 충돌이 적은 특성들을 같은 번들로
   - NP-hard -> 탐욕적 근사

3. 번들 내 특성들을 하나로 병합
   - 값 범위를 offset으로 구분
   - feature_1: [0, 10], feature_2: [0, 5]
   - bundle: feature_1 + (feature_2 + 10)

Leaf-wise vs Level-wise 트리 성장

Level-wise (XGBoost 등):
    레벨 1:    [    root    ]
    레벨 2:  [  L  ]    [  R  ]
    레벨 3: [a][b]     [c][d]
    -> 균형 잡힌 트리, 불필요한 분할 가능

Leaf-wise (LightGBM):
    1. 최대 손실 감소 리프 선택
    2. 해당 리프만 분할
    3. 반복
    -> 비대칭 트리, 더 적은 분할로 같은 손실 감소
    -> 과적합 주의 (max_depth 또는 num_leaves 제한)

Histogram-based 분할

1. 연속 특성을 K개 bin으로 이산화 (기본 255)
2. Histogram 누적으로 O(n) -> O(K)
3. Histogram subtraction: 
   child_hist = parent_hist - sibling_hist
   -> 더 작은 자식만 계산, 큰 자식은 차감으로

시간 복잡도

방법 복잡도
Pre-sorted (XGBoost Exact) O(n * d) per split
Histogram (LightGBM) O(bins * d) per split
With GOSS O(an + bn) * bins * d
With EFB O(bins * d') where d' << d

하이퍼파라미터 가이드

핵심 파라미터

파라미터 설명 권장 범위 기본값
num_leaves 리프 수 (트리 복잡도) 20 ~ 150 31
max_depth 최대 깊이 -1 (무제한) 또는 5 ~ 15 -1
learning_rate 학습률 0.01 ~ 0.3 0.1
n_estimators 부스팅 라운드 100 ~ 1000 100
min_child_samples 리프 최소 샘플 10 ~ 100 20
subsample (bagging_fraction) 행 샘플링 0.5 ~ 1.0 1.0
colsample_bytree (feature_fraction) 열 샘플링 0.5 ~ 1.0 1.0

정규화 파라미터

파라미터 설명 권장 범위 기본값
reg_alpha (lambda_l1) L1 정규화 0 ~ 10 0
reg_lambda (lambda_l2) L2 정규화 0 ~ 10 0
min_gain_to_split 분할 최소 gain 0 ~ 1 0
min_child_weight 리프 최소 Hessian 0.001 ~ 10 0.001

GOSS/EFB 파라미터

파라미터 설명 권장값
boosting 부스팅 타입 'gbdt', 'goss', 'dart'
top_rate GOSS 상위 비율 (a) 0.2
other_rate GOSS 하위 샘플링 비율 (b) 0.1
enable_bundle EFB 활성화 True
max_conflict_rate EFB 최대 충돌율 0

num_leaves vs max_depth

관계: num_leaves <= 2^max_depth

추천:
- num_leaves를 주로 조정
- max_depth는 안전장치로 사용
- num_leaves = 2^max_depth 피하기 (균형 트리 = LightGBM 장점 상실)

예시:
- max_depth=7이면 num_leaves는 70~100 정도 (128보다 작게)

Python 코드 예시

기본 사용법

import numpy as np
import pandas as pd
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, roc_auc_score, classification_report

# 데이터 로드
data = load_breast_cancer()
X, y = data.data, data.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# LightGBM 분류기
model = lgb.LGBMClassifier(
    n_estimators=100,
    learning_rate=0.1,
    num_leaves=31,
    max_depth=-1,
    min_child_samples=20,
    subsample=0.8,
    colsample_bytree=0.8,
    reg_alpha=0,
    reg_lambda=1,
    random_state=42,
    n_jobs=-1,
    verbose=-1
)

model.fit(X_train, y_train)

# 평가
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]

print("=== LightGBM Classification ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob):.4f}")

조기 종료

X_tr, X_val, y_tr, y_val = train_test_split(
    X_train, y_train, test_size=0.2, random_state=42
)

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

model_es.fit(
    X_tr, y_tr,
    eval_set=[(X_val, y_val)],
    eval_metric='logloss',
    callbacks=[
        lgb.early_stopping(stopping_rounds=50),
        lgb.log_evaluation(period=0)
    ]
)

print(f"Best iteration: {model_es.best_iteration_}")
print(f"Best score: {model_es.best_score_['valid_0']['binary_logloss']:.4f}")

Native API 사용

# Dataset 생성
train_data = lgb.Dataset(X_tr, label=y_tr)
valid_data = lgb.Dataset(X_val, label=y_val, reference=train_data)

# 파라미터
params = {
    'objective': 'binary',
    'metric': ['binary_logloss', 'auc'],
    'boosting': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.8,
    'bagging_fraction': 0.8,
    'bagging_freq': 5,
    'verbose': -1,
    'seed': 42
}

# 학습
bst = lgb.train(
    params,
    train_data,
    num_boost_round=1000,
    valid_sets=[train_data, valid_data],
    valid_names=['train', 'valid'],
    callbacks=[
        lgb.early_stopping(stopping_rounds=50),
        lgb.log_evaluation(period=100)
    ]
)

print(f"Best iteration: {bst.best_iteration}")

특성 중요도

# 두 가지 중요도
importance_split = model.booster_.feature_importance(importance_type='split')
importance_gain = model.booster_.feature_importance(importance_type='gain')

importance_df = pd.DataFrame({
    'feature': data.feature_names,
    'split': importance_split,
    'gain': importance_gain
}).sort_values('gain', ascending=False)

print("\nFeature Importance (Top 10):")
print(importance_df.head(10).to_string(index=False))

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

lgb.plot_importance(model, importance_type='split', ax=axes[0], 
                    title='Split Importance', max_num_features=15)
lgb.plot_importance(model, importance_type='gain', ax=axes[1],
                    title='Gain Importance', max_num_features=15)

plt.tight_layout()
plt.savefig('lgbm_importance.png', dpi=150)
plt.show()

GOSS 사용

# GOSS (Gradient-based One-Side Sampling)
model_goss = lgb.LGBMClassifier(
    n_estimators=100,
    boosting_type='goss',  # GOSS 활성화
    top_rate=0.2,          # 상위 20% 유지
    other_rate=0.1,        # 나머지 중 10% 샘플링
    learning_rate=0.1,
    num_leaves=31,
    random_state=42,
    verbose=-1
)

model_goss.fit(X_train, y_train)
y_pred_goss = model_goss.predict(X_test)
print(f"GOSS Accuracy: {accuracy_score(y_test, y_pred_goss):.4f}")

범주형 특성 처리

# 범주형 특성이 있는 경우
# LightGBM은 범주형을 직접 처리 가능 (One-hot 불필요)

# 예시 데이터 생성
df = pd.DataFrame({
    'num_feature': np.random.randn(1000),
    'cat_feature': np.random.choice(['A', 'B', 'C'], 1000)
})
y_cat = np.random.randint(0, 2, 1000)

# 범주형을 category 타입으로 변환
df['cat_feature'] = df['cat_feature'].astype('category')

model_cat = lgb.LGBMClassifier(verbose=-1)
model_cat.fit(
    df, y_cat,
    categorical_feature=['cat_feature']
)

print("Categorical feature handled natively")

하이퍼파라미터 튜닝 (Optuna)

import optuna

def objective(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'max_depth': trial.suggest_int('max_depth', 3, 12),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
        'random_state': 42,
        'verbose': -1,
        'n_jobs': -1
    }

    model = lgb.LGBMClassifier(**params)

    # Early stopping을 위한 split
    X_tr, X_val, y_tr, y_val = train_test_split(
        X_train, y_train, test_size=0.2, random_state=42
    )

    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        callbacks=[
            lgb.early_stopping(50),
            lgb.log_evaluation(0)
        ]
    )

    y_prob = model.predict_proba(X_val)[:, 1]
    return roc_auc_score(y_val, y_prob)

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50, show_progress_bar=True)

print(f"\nBest AUC: {study.best_value:.4f}")
print(f"Best params: {study.best_params}")

언제 쓰나?

적합한 상황: - 대규모 데이터셋 (수백만 행) - 고차원 희소 데이터 (텍스트 특성 등) - 빠른 학습이 필요한 경우 - 범주형 특성이 많은 경우 - 메모리 제약이 있는 환경

부적합한 상황: - 매우 작은 데이터셋 (< 1000) - Leaf-wise로 인한 과적합 위험 - 균형 잡힌 트리가 필요한 경우

LightGBM vs XGBoost

항목 LightGBM XGBoost
트리 성장 Leaf-wise Level-wise
속도 빠름 보통
메모리 적음 많음
범주형 처리 Native 지원 One-hot 필요
작은 데이터 과적합 위험 안정적
대규모 데이터 최적 느림
GPU 지원 있음 있음

장단점

장점 단점
매우 빠른 학습 작은 데이터에서 과적합
낮은 메모리 사용 num_leaves 튜닝 중요
범주형 직접 처리 Level-wise보다 불안정할 수 있음
희소 데이터 효율적
GOSS/EFB 최적화
높은 정확도

관련 논문/참고

  • Ke, G., et al. (2017). "LightGBM: A Highly Efficient Gradient Boosting Decision Tree". NeurIPS.
  • LightGBM Documentation: https://lightgbm.readthedocs.io/
  • LightGBM GitHub: https://github.com/microsoft/LightGBM