Objectives, metrics and imbalanced data

LightGBM · Advanced · 12 min

Matching the loss to the problem often beats any amount of hyperparameter tuning.

The objective defines the loss. Beyond the defaults (regression = L2, binary, multiclass):

  • regression_l1, huber, fair: robust to outliers,
  • quantile with alpha=0.9: conditional quantiles, for prediction intervals,
  • poisson, gamma, tweedie (tweedie_variance_power): counts, positive skewed amounts, insurance losses with many zeros (log link),
  • cross_entropy (xentropy): targets that are probabilities in [0, 1],
  • multiclassova: one-vs-all multi-class,
  • lambdarank, rank_xendcg: ranking.

metric (or eval_metric in the wrapper) controls what is evaluated: auc, average_precision, binary_logloss, l1, l2, quantile, ndcg, map and more. A custom metric in the scikit-learn API is a function (y_true, y_pred) -> (name, value, is_higher_better) passed as eval_metric.

Imbalanced classes: is_unbalance=True or scale_pos_weight=n_neg/n_pos (use one, not both) reweight the positive class; class_weight="balanced" works in the wrapper, including multi-class. As with any reweighting, probabilities become inflated; tune the decision threshold or recalibrate if you need real probabilities. Monitor average_precision rather than accuracy.

For prediction intervals, train two quantile models (for example alpha 0.05 and 0.95) and check their empirical coverage on validation data.

Example

import lightgbm as lgb
import numpy as np

rng = np.random.default_rng(1)
X = rng.uniform(0, 10, size=(4000, 2))
y = 5 * X[:, 0] + rng.normal(0, 1 + X[:, 0], 4000)        # noise grows with x0
X_tr, X_val, y_tr, y_val = X[:3000], X[3000:], y[:3000], y[3000:]

lo = lgb.LGBMRegressor(objective="quantile", alpha=0.05, verbose=-1).fit(X_tr, y_tr)
hi = lgb.LGBMRegressor(objective="quantile", alpha=0.95, verbose=-1).fit(X_tr, y_tr)
inside = (y_val >= lo.predict(X_val)) & (y_val <= hi.predict(X_val))
print(round(inside.mean(), 2))                          # 0.82 (target 0.90: check coverage, quantile models often undercover)

width = hi.predict([[1.0, 5.0], [9.0, 5.0]]) - lo.predict([[1.0, 5.0], [9.0, 5.0]])
print(width.round(1))                                   # [ 4.7 27. ] (wider where noisier)

def mape(y_true, y_pred):
    return "mape", float(np.mean(np.abs((y_true - y_pred) / y_true))), False
m = lgb.LGBMRegressor(verbose=-1).fit(X_tr, y_tr + 100, eval_X=X_val, eval_y=y_val + 100,
                                       eval_metric=mape)
print(sorted(m.evals_result_["valid_0"]))               # ['l2', 'mape']

Remember

  • Match the objective to the target: quantile, Poisson/Tweedie, robust losses
  • Custom metrics return (name, value, is_higher_better)
  • Use one of is_unbalance or scale_pos_weight, and expect inflated probabilities

Watch out for

  • Setting both is_unbalance and scale_pos_weight, which LightGBM rejects.

Check yourself

  1. How can you get a 90% prediction interval from LightGBM?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: lightgbm.readthedocs.io/en/stable/Parameters.html#objective