1. LightGBM with huge parquet dataset

1.1 Introduction

LightGBM 아주 좋은 모델입니다.
제가 가장 최애하는 모델중의 하나입니다.
특히 latent vector 를 만들어내거나 그런 작업이 아니고, tabular dataset 을 다룬다면 더더더욱! deep learning을 쓸 필요도 없죠.
아~~ 주 좋은 모델입니다.

문제는 데이터 사이즈가 커질 때 입니다.
하둡에서 만들어진 거대한 parquets 파일들을 다루는 것부터 학습을 시키는 방법까지 해당 문서에서 다루고자 합니다.
알고 보면 매우 쉽습니다.

전체코드는 Continuous Learning Code를 참고해주세요.

1.2 Summary

batch learning 을 하게 되었을때의 인싸이트

  • epoch 반드시 해야 합니다. 즉 같은 데이터를 여러번 학습 시키는 것이 꼭 필요합니다.
  • epoch, num_boost_round 값이 클수록 원래 LGBMClassifier 만큼 수준으로 올라옵니다.
  • 즉 LGBMClassifier 으로 학습하면 장점은 겁나 빠르고, 최적화 시키기가 매우 쉽습니다.
  • batch learning 을 하게 되면, LGBMClassifier <- 이걸 쓴것보다는 결과가 더 좋게 나오기 어렵습니다. 조금 더 떨어지는 경향이 있습니다.
  • 하지만 데이터 사이즈가 크면 선택 권한이 없죠.

2. Code Implementation

2.1 PrAUC

PrAUC 계산은 다음의 함수를 사용하겠습니다.

def calculate_prauc(y_true, y_prob, plot, label, method=[]):
    from collections.abc import Iterable

    def point_optimal_threshold(name):
        # Other Metrics at the max_threshold

        acc_ = accuracy_score(y_true, y_prob >= max_threshold)
        f1_ = f1_score(y_test, y_prob >= max_threshold)

        plot.plot(
            recall[max_idx],
            precision[max_idx],
            marker="o",
            markersize=10,
            label=f"{label} | {name:5} | optimal threshold: {max_threshold}",
        )

    if not isinstance(method, Iterable):
        method = [method]

    precision, recall, thresholds = precision_recall_curve(y_true, y_prob)
    auc_ = auc(recall, precision)
    plot.plot(recall, precision, label=f"{label} | prauc:{auc_:.4f})")

    # Optimize the thesholds
    best_threshold = None
    if "diff" in method:
        max_idx = np.argmax(recall - precision)
        max_threshold = thresholds[max_idx]
        point_optimal_threshold("diff")
        best_threshold = max_threshold

    if "plus" in method:
        max_idx = np.argmax(recall + precision)
        max_threshold = thresholds[max_idx]
        point_optimal_threshold("plus")
        best_threshold = max_threshold

    if "f1" in method:
        fscores = 2 * (precision * recall) / (precision + recall)
        max_idx = np.argmax(fscores)
        max_threshold = thresholds[max_idx]
        point_optimal_threshold("f1")
        best_threshold = max_threshold
    return best_threshold

2.2 LGBMClassifier

일단 continuous learning이 아닐때의 방법입니다.

# Train
model = LGBMClassifier(
    metrics="prauc",
    n_estimators=100,
    scale_pos_weight=scale_pos_weight,
    random_state=32,
)
model.fit(x_train, y_train)

예측은 다음과 같이 합니다.

# Predict
y_pred = model.predict_proba(x_test)[:, 1] >= 0.5
y_prob = model.predict_proba(x_test)[:, 1]


# Model Performance
print("scale_pos_weight:", scale_pos_weight)
print("Accuracy :", accuracy_score(y_test, y_pred))
print("Precision:", precision_score(y_test, y_pred))
print("Recall   :", recall_score(y_test, y_pred))
print("F1 Score :", f1_score(y_test, y_pred))


fig, plot = plt.subplots(1, figsize=(8, 6))
calculate_prauc(y_test, y_prob, plot, "LightGBM", ("diff", "plus", "f1"))

plot.plot([0, 1], [1, 0], "k--", label=f"Baseline  (AUC=0.5)")
plot.set_xlabel("Recall")
plot.set_ylabel("Precision")
plot.set_title(f"Precision Recall Curve")
plot.legend(loc="lower left")
scale_pos_weight: 0.8961671428571428
Accuracy : 0.92689
Precision: 0.8745577356063042
Recall   : 0.34804313738039616
F1 Score : 0.49792835069245733

2.3 Training with Huge Parquet Files

parquet 데이터는 일단 압도적으로 사이즈가 큽니다.
데이터름 모두 올려 놓는 순간 그 자체로 바로 OOM이 뜹니다.
따라서 데이터를 분할해서 학습을 시켜야 합니다.

Parquet 데이터를 다루는 코드

def iter_data(data_path) -> pd.DataFrame:
    parquet_dataset = pq.ParquetDataset(data_path, use_legacy_dataset=False)
    for frag in parquet_dataset.fragments:
        for batch in frag.to_batches():
            yield batch.to_pandas()


def split_dataset(df):
    y_data = df["y"]
    df.drop("y", axis=1, inplace=True)
    return df, y_data

학습시키는 코드

import lightgbm as lgb

params = {
    "boosting_type": "gbdt",
    "objective": "binary",
    "metric": "prauc",
    "num_leaves": 31,
    "num_iterations": 1,
    "max_bin": 2000,
    "num_threads": 12,
    "force_col_wise": True,
    "verbose": 0,
}

model = None
for epoch in tqdm(range(5), desc='epoch'):
    for data in iter_data(train_path):
        x_train, y_train = split_dataset(data)
        scale_pos_weight = sum(y_train == 0) / len(y_train)
    
        if model is None:
            model = lgb.train(params, 
                              lgb.Dataset(x_train, y_train), 
                              num_boost_round=10)
        else:
            model = lgb.train(params, 
                              lgb.Dataset(x_train, y_train), 
                              num_boost_round=10, 
                              init_model=model, 
                              keep_training_booster=True)

저장된 모델 불러오기

model_path = 'model.txt'
model = lgb.Booster(model_file=model_path)

Evaluation

x_test = test_data.drop("y", axis=1)
y_test = test_data["y"]

# Predict
y_pred = model.predict(x_test) >= 0.5
y_prob = model.predict(x_test)


# Model Performance
print("scale_pos_weight:", scale_pos_weight)
print("Accuracy :", accuracy_score(y_test, y_pred))
print("Precision:", precision_score(y_test, y_pred))
print("Recall   :", recall_score(y_test, y_pred))
print("F1 Score :", f1_score(y_test, y_pred))


fig, plot = plt.subplots(1, figsize=(8, 6))
calculate_prauc(y_test, y_prob, plot, "LightGBM", ("diff", "plus", "f1"))

plot.plot([0, 1], [1, 0], "k--", label=f"Baseline  (AUC=0.5)")
plot.set_xlabel("Recall")
plot.set_ylabel("Precision")
plot.set_title(f"Precision Recall Curve")
plot.legend(loc="lower left")
Accuracy : 0.9231666666666667
Precision: 0.8784270285239546
Recall   : 0.30452174469583027
F1 Score : 0.45225987358015307

2.4 Feature Importance

  • importance_type
    • auto: default value
    • gain: 전체 gain을 얼마나 얻었는지로 판단
    • split: 몇번이나 features가 모델에서 사용되었는지로 판단
  • ignore_zero: 기본값은 True이고, False 로 하게 되면 전혀 사용안된 features 들 까지도 뽑을 수 있습니다.
lgb.plot_importance(model, importance_type='gain', figsize=(7, 5), ignore_zero=False, title='gain feature importance')

또는 따로 값을 얻으려면..

model.feature_importance(importance_type='gain')
# array([3.77903369e+04, 8.43276426e+03, 2.00370975e+04, 1.38318949e+01,
#        2.34696655e+04, 2.82261864e+04, 2.89943968e+04, 0.00000000e+00])

model.feature_name()
# ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']