Search Tech Journey

Find topics, journeys and posts

6-month learning plan91 / 130
back to blog
mlintermediate 55m read

S091 · Gradient Boosting — XGBoost, LightGBM

The algorithm that has won more Kaggle competitions than everything else combined — sequentially fit trees to residuals, then wrap it in modern tricks (histograms, GPU, DART) to make it fly.

🤖Machine LearningM11 · Classical ML· Session 091 of 130 90 min

🎯 Understand gradient boosting from first principles, then wield XGBoost and LightGBM like a Kaggle grandmaster — with the right hyperparameters and early stopping.

Why this session exists

Between 2015 and 2024, gradient-boosted trees won ~70 % of tabular Kaggle competitions. In industry, they're the default choice for fraud detection, credit scoring, ad ranking, click prediction, and every ‘score this table’ problem you can name. Unlike random forest, which reduces variance by parallel averaging, gradient boosting reduces bias by sequential correction — each new tree fits the errors of the previous ensemble. Understanding this session unlocks the model family that pays the salary of most ML engineers on Earth.

You will be able to
  • Explain gradient boosting as ‘fit a tree to the residuals, add it with a learning rate, repeat.’
  • Derive the update rule from a Taylor expansion of the loss.
  • Wield XGBoost with sane defaults + early stopping in ~20 lines.
  • Choose between XGBoost, LightGBM, and CatBoost based on data characteristics.
  • Diagnose the three ways boosting fails (learning rate too high, no early stopping, unbalanced classes).

Prerequisites

  • S089 · Decision trees — single tree is the building block.
  • S090 · Random forest — you understand ensembling.
  • S087 · Regularization — boosting relies on it heavily.


(a) Intuition · 5 min

Golfing to the pin — many small corrections beat one big swing
🌍 Real world

You're playing golf. First shot lands 50 yards short and left. Next shot corrects: aim 50 yards further, a bit right. Still short. Next shot corrects again. Each swing is a small correction to the previous miss. Eventually you're in the hole.

You could try to sink it in one shot, but a mishit is catastrophic. Many small corrections are safer AND more accurate.

💻 Code world

Gradient boosting is the same. Fit a small model that captures some signal. Look at the residuals (what's left unexplained). Fit ANOTHER small model to those residuals. Add it, scaled by a small learning rate. Repeat 100-1000 times.

Each new tree is a small correction. The learning rate ensures no single tree overcorrects. The ensemble sum is a very expressive model with controllable capacity.

The three ideas to own before we code

Gradient boosting in three sentences
  • Sequential — each tree fits the residual (or gradient of loss) of the current ensemble. Bagging is parallel; boosting is serial.
  • Small learning rate — each tree is added with weight η ≈ 0.05-0.1. Many small steps, not few big ones. Trades compute for accuracy.
  • Regularize hard — max_depth 4-8, L1/L2 on leaf values, min_child_weight, subsample rows and columns per tree. Boosting overfits fast without these.

A brief history

  1. 1997
    AdaBoost · Freund & Schapire
    First practical boosting algorithm — reweight misclassified examples. Won the 2003 Gödel Prize. Still used in face detection (Viola-Jones).
  2. 1999
    Gradient Boosting · Friedman
    The general framework. AdaBoost is a special case with exponential loss. Works with any differentiable loss.
  3. 2001
    GBM in R + sklearn
    Slow, single-threaded, but widely available. Ruled academic benchmarks for a decade.
  4. 2014
    XGBoost · Tianqi Chen
    Regularized objective, sparse-aware split finding, distributed training. Dominates Kaggle immediately.
  5. 2017
    LightGBM · Microsoft
    Histogram-based algorithm, leaf-wise growth. Often 10× faster than XGBoost, similar accuracy.
  6. 2018
    CatBoost · Yandex
    Native handling of categorical features via ordered target encoding. Great when you have many categorical columns.
  7. 2024
    Still SOTA for tabular
    Grinsztajn et al 2022 paper: trees beat deep learning on 45 medium-sized tabular datasets. Gradient boosting isn't going anywhere.

(b) Visual walkthrough · 15 min

The boosting loop

For regression with MSE loss, residuals ARE the gradients. For classification (log loss), residuals are the gradients of the loss — which is why it's called "gradient" boosting, not "residual" boosting.

The math — Taylor expansion of the loss

Deriving the update rule (XGBoost objective)

Objective at step t
L⁽ᵗ⁾ = Σᵢ ℓ(yᵢ, F_{t-1}(xᵢ) + hₜ(xᵢ)) + Ω(hₜ) — loss on the new ensemble + regularization on the new tree.
step 1
Second-order Taylor expansion
ℓ(y, F + h) ≈ ℓ(y, F) + gₜ·h + (1/2)·hₜ·h² where gₜ = ∂ℓ/∂F and hₜ = ∂²ℓ/∂F² at F=F_{t-1}.
step 2
Drop constant, minimise
L⁽ᵗ⁾ ≈ Σᵢ [gᵢ·hₜ(xᵢ) + (1/2)·hᵢ·hₜ(xᵢ)²] + Ω(hₜ). Now minimise wrt hₜ.
step 3
Optimal leaf value
For samples I falling in a leaf: w* = -Σg / (Σh + λ). λ is L2 regularization. This is a closed-form leaf update.
step 4
Split gain formula
Gain = (1/2)·[(Σg_L)²/(Σh_L + λ) + (Σg_R)²/(Σh_R + λ) - (Σg)²/(Σh + λ)] - γ. Pick splits with maximum gain. γ = pruning penalty.
step 5

Bagging vs Boosting — the great tabular tradeoff

Random Forest (bagging)

Parallel · variance killer

  • Independent trees, parallel training.
  • Reduces variance, not bias.
  • Robust to hyperparameters — hard to break.
  • Free OOB score, no validation set needed.
  • Great baseline, moderate ceiling.
Gradient Boosting

Sequential · bias killer

  • Trees added one by one to fix residuals.
  • Reduces bias, controls variance via regularization.
  • Very sensitive to hyperparameters (LR, depth, rounds).
  • REQUIRES early stopping on val set.
  • Higher ceiling; wins Kaggle.

Which boosting library to use?

XGBoost

The battle-tested standard

  • Great single-node performance.
  • Best distributed training (multi-GPU, Spark).
  • Extensive documentation, mature ecosystem.
  • Slightly slower than LightGBM on single machine.
  • First choice if you're unsure.
LightGBM

Fastest single-node

  • Histogram-based split finding — 5-10× faster.
  • Leaf-wise growth (deeper on strong branches).
  • Slightly more prone to overfitting → tune num_leaves + min_data_in_leaf.
  • Best for large single-node datasets.
  • Default for many Kaggle winners since 2018.
CatBoost

Categorical-first

  • Native categorical encoding (no manual one-hot).
  • Ordered boosting reduces prediction bias.
  • Great defaults — often works with zero tuning.
  • Slower to train than LGBM.
  • Best when data is category-heavy (e-commerce, ad-tech).

Hyperparameters that matter (and their sane defaults)

1η
learning_rate = 0.05

Small step size per tree. Lower = more trees needed but better generalisation. Grid: 0.01, 0.05, 0.1.

2depth
max_depth = 6

Depth of each tree. Deeper = more capacity, more overfit risk. XGBoost default 6; LGBM controlled via num_leaves.

3N
n_estimators = 1000 + early_stopping_rounds = 50

Never tune n_estimators manually. Set high, let early stopping find the right round using val AUC/RMSE.

4bag
subsample = 0.8 · colsample_bytree = 0.8

Bagging on top of boosting. Reduces variance. Standard defaults 0.7-0.9.

5reg
min_child_weight / min_data_in_leaf

Regularization: refuse to split leaves below this weight. Prevents overfitting to noise.

6L1/L2
reg_lambda = 1.0 · reg_alpha = 0

L2 (lambda) and L1 (alpha) on leaf weights. Add lambda if overfit; add alpha for sparsity.


Common misconception
✗ What most people think

"Gradient boosting is just boosting with gradients — each tree fits the errors of the previous one. So it's basically the same idea as Random Forest, only sequential instead of parallel."

✓ What is actually true

They are opposites in what they attack. Random Forest averages many low-bias, high-variance deep trees to kill variance. Boosting sums many high-bias, low-variance shallow trees to kill bias, and it adds variance as it goes. That is why forests cannot overfit by adding trees while boosters absolutely can, why forest trees are deep and booster trees are stumps, and why one needs early stopping and the other does not.

Why the myth is so sticky

The myth is sticky because both are "ensembles of trees" and both appear in the same chapter, so they get filed together. The sequential-versus-parallel difference is the visible one, and it seems sufficient. But that is a description of the implementation; the real difference is which term of the error decomposition each one targets, and everything else — tree depth, tree count behaviour, sensitivity to noise, parallelisability — is downstream of that single choice.

Prove it to yourself

Watch validation error in a booster turn upward as trees accumulate — the thing a forest never does:

from sklearn.ensemble import GradientBoostingClassifier
import numpy as np

m = GradientBoostingClassifier(n_estimators=1000, learning_rate=0.1,
                               max_depth=3).fit(Xtr, ytr)

val = np.array([s for s in m.staged_score(Xte, yte)])
print(val.argmax(), val.max(), val[-1])

# best score arrives at some middle iteration, then DECLINES.
# argmax is the tree count early stopping would have chosen.
# repeat with RandomForest and the curve only ever flattens.
From first principles
Start with the question

Why is it called gradient boosting when the trees appear to just fit residuals? Where is the gradient, and why does that reframing matter enough to name the algorithm after it?

  1. 1
    Think of the ensemble prediction F(x) not as a formula but as a point in function space: the vector of predictions on your n training points. Training is a search for the vector that minimises total loss.
    forced by · the loss depends on the model only through its predictions, so predictions are the true optimisation variable
  2. 2
    Gradient descent says: to reduce the loss, step in the direction of the negative gradient of the loss with respect to that vector — one component per training point.
    forced by · the negative gradient is the direction of steepest local decrease, in any space where a gradient is defined
  3. 3
    Compute that gradient for squared loss L = ½(y − F)². The derivative with respect to F is −(y − F), so the negative gradient is exactly (y − F): the residual. Residual-fitting is not a heuristic — it is the special case of gradient descent under squared loss.
    forced by · the residual literally is the negative gradient for that particular loss
  4. 4
    But the negative gradient is only defined on the n training points. To take a step you can apply to new data, you need a function that approximates it everywhere — so you fit a shallow tree to those gradient values. The tree is the generalisable version of the step direction.
    forced by · a step must be a function, not n numbers, if the model is to predict on unseen inputs
  5. 5
    Then take a small step: F ← F + η·h(x), with η the learning rate. Small because the tree only approximates the gradient, and because the gradient is only locally valid — the same reason learning rates exist anywhere else.
    forced by · an approximate direction is trustworthy only over a short distance
⇒ Therefore

Therefore gradient boosting is gradient descent performed in function space, where each "step" is a tree and the learning rate is the step size. The gradient framing is not decoration — it is what generalises the algorithm.

And note what this predicts: since nothing in the derivation required squared loss, the algorithm must work for any differentiable loss — you simply plug in a different derivative. Log-loss gives (y − p) for classification; absolute error gives sign(y − F), which is why boosting with MAE fits the sign of the residual and is therefore robust to outliers; pinball loss gives quantile regression. One algorithm, and the loss function is a parameter. That is why boosting libraries expose a custom-objective hook at all.

Mental modelCompound interest on corrections

Start with a constant prediction — the mean, or the base log-odds. Then repeatedly ask: "where am I currently wrong, and in which direction?" Fit a deliberately weak tree that answers only that question, and add a small fraction of its answer to the running total. Repeat a thousand times.

No individual tree is trying to solve the problem. Each one is a small correction to the accumulated total, and the strength comes from compounding many tiny, focused adjustments. The learning rate is how much of each correction you accept — take too much and you overshoot into the noise; take too little and you need far more trees to arrive.

  • Learning rate and tree count trade off directly: halving η roughly doubles the trees needed. Lower η with more trees generalises better, so set η small and let early stopping pick the count.
  • Base learners must be weak — depth 3 to 8, or a fixed leaf budget. Deep trees fit the residuals too well and the ensemble overfits within a handful of iterations.
  • Early stopping on a validation set is not optional; it is the regularisation. Without it, extra trees eventually make the model worse.
  • Boosting concentrates capacity on the examples it currently gets wrong, so mislabelled rows receive escalating attention. It is materially more sensitive to label noise than bagging.
🔔 Fires when you see

Fire this the moment you see: a booster trained to a fixed tree count with no validation curve · learning_rate left at default while n_estimators is tuned · deep trees inside a boosting model · boosting applied to data with known label noise · a booster's probabilities used directly in an expected-value calculation without calibration.

The tradeoff

You are shipping gradient boosting. Do you pick a low learning rate with many trees, a higher learning rate with fewer, or push regularisation and subsampling instead?

Low learning rate (~0.01–0.05), many trees
+ you gain each tree contributes little, so the ensemble averages out noise in individual fits and generalisation is reliably better; the result is also less sensitive to the exact stopping point, so early stopping does not need to be precise
− you pay training time scales roughly inversely with η — the same fit may need 5–10× the trees, which multiplies both training cost and the final model size; inference latency and memory grow with tree count, which matters when serving at scale
pick when you have training compute available, accuracy is the objective, and inference cost is not the binding constraint — the default for offline scoring and batch pipelines
Higher learning rate (~0.1–0.3), fewer trees
+ you gain fast to train, so you can iterate on features and hyperparameters many times a day; smaller final model, so lower memory and faster inference — often the difference between meeting a latency SLO and not
− you pay each tree makes a large, coarse correction, so the model is more likely to overshoot and lock in noise; performance becomes sensitive to exactly where you stop, and results vary more across seeds
pick when the iteration loop is your bottleneck (early experimentation), or a hard inference-latency budget caps model size
Aggressive regularisation and subsampling
+ you gain row subsampling (stochastic boosting) and column subsampling per tree inject the same decorrelation that helps forests, plus explicit L1/L2 on leaf weights and a minimum-gain threshold to refuse marginal splits; together these let you keep a moderate learning rate without overfitting, and subsampling makes each tree cheaper to fit
− you pay a much larger hyperparameter space, so honest tuning costs real compute and you risk overfitting the validation set through the search itself; and the interactions between these knobs are not intuitive, so the search is hard to reason about
pick when you have limited data relative to feature count, or noisy labels, where plain early stopping alone leaves the model fragile
What a senior engineer actually does

Fix a small learning rate, set the tree count deliberately high, and let early stopping on a proper validation set choose the actual number. That converts two coupled hyperparameters into one, and it is the single most reliable configuration decision in boosting. Tuning n_estimators by grid search is wasted compute — the validation curve already hands you the answer.

Then spend the remaining tuning budget on tree complexity (depth or leaf count) and subsampling, in that order, because those move the score most. And before shipping, check calibration: boosters optimise ranking loss well but their raw probabilities are frequently over-confident, so if anything downstream multiplies your output by a cost, fit a calibration layer on held-out data rather than trusting the number.


(c) Hands-on · 25 min

We're going to fit XGBoost on a real dataset with early stopping, tune the essentials, and compare against LightGBM.

# gradient_boosting.py — XGBoost + LightGBM in production-ready form, ~130 lines.
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import xgboost as xgb
 
# If LightGBM installed, uncomment:
# import lightgbm as lgb
 
np.random.seed(42)
 
# --- Data ---
data = fetch_california_housing(as_frame=True)
X: pd.DataFrame = data.frame.drop(columns=["MedHouseVal"])
y: pd.Series = data.frame["MedHouseVal"]
 
X_trainval, X_test, y_trainval, y_test = train_test_split(X, y, test_size=0.15, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_trainval, y_trainval, test_size=0.20, random_state=42)
print(f"train={len(X_train)}  val={len(X_val)}  test={len(X_test)}")
 
# =====================================================
# 1. XGBoost with sane defaults + early stopping
# =====================================================
model = xgb.XGBRegressor(
    n_estimators=2000,           # generous upper bound; early stopping decides
    learning_rate=0.05,
    max_depth=6,
    subsample=0.8,
    colsample_bytree=0.8,
    reg_alpha=0.0,
    reg_lambda=1.0,
    min_child_weight=5,
    random_state=42,
    n_jobs=-1,
    early_stopping_rounds=50,    # stop if val doesn't improve for 50 rounds
    eval_metric="rmse",
)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
print(f"[XGBoost]      stopped at round {model.best_iteration}")
print(f"  train RMSE = {mean_squared_error(y_train, model.predict(X_train), squared=False):.4f}")
print(f"  val   RMSE = {mean_squared_error(y_val,   model.predict(X_val),   squared=False):.4f}")
print(f"  test  RMSE = {mean_squared_error(y_test,  model.predict(X_test),  squared=False):.4f}")
 
# =====================================================
# 2. Manual boosting from scratch — for understanding
# =====================================================
from sklearn.tree import DecisionTreeRegressor
 
def gbm_scratch(X_train, y_train, X_test, n_estimators=200, learning_rate=0.05, max_depth=4):
    """Very simple gradient boosting for regression with MSE loss."""
    # Init prediction = mean
    F_train = np.full(len(y_train), y_train.mean())
    F_test = np.full(X_test.shape[0], y_train.mean())
    trees = []
    for m in range(n_estimators):
        residual = y_train - F_train    # for MSE, negative gradient = residual
        tree = DecisionTreeRegressor(max_depth=max_depth, random_state=m)
        tree.fit(X_train, residual)
        F_train += learning_rate * tree.predict(X_train)
        F_test  += learning_rate * tree.predict(X_test)
        trees.append(tree)
    return F_test
 
pred_scratch = gbm_scratch(X_train.values, y_train.values, X_test.values, n_estimators=200)
print(f"\n[scratch GBM]  test RMSE = {mean_squared_error(y_test, pred_scratch, squared=False):.4f}")
 
# =====================================================
# 3. Learning-rate sensitivity (why boosting needs care)
# =====================================================
print("\n=== Learning-rate sweep (XGBoost) ===")
for lr in [0.01, 0.05, 0.1, 0.3, 1.0]:
    m = xgb.XGBRegressor(
        n_estimators=1500, learning_rate=lr, max_depth=6,
        subsample=0.8, colsample_bytree=0.8,
        random_state=42, n_jobs=-1,
        early_stopping_rounds=50, eval_metric="rmse",
    )
    m.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
    rmse = mean_squared_error(y_test, m.predict(X_test), squared=False)
    print(f"  lr={lr:>5}: rounds={m.best_iteration:>4}, test RMSE={rmse:.4f}")
 
# =====================================================
# 4. Feature importance (gain-based, XGBoost specific)
# =====================================================
print("\n=== Feature importance (gain) ===")
importances = model.get_booster().get_score(importance_type="gain")
for feat, imp in sorted(importances.items(), key=lambda x: -x[1])[:8]:
    print(f"  {feat:<20} {imp:>10.2f}")
 
# =====================================================
# 5. Uncomment for LightGBM comparison
# =====================================================
# lgb_model = lgb.LGBMRegressor(
#     n_estimators=2000, learning_rate=0.05, num_leaves=31, max_depth=-1,
#     subsample=0.8, colsample_bytree=0.8, min_child_samples=10,
#     random_state=42, n_jobs=-1,
# )
# lgb_model.fit(X_train, y_train,
#               eval_set=[(X_val, y_val)],
#               callbacks=[lgb.early_stopping(stopping_rounds=50)])
# rmse = mean_squared_error(y_test, lgb_model.predict(X_test), squared=False)
# print(f"[LightGBM]     test RMSE = {rmse:.4f}")

What each block does

Anatomy of the script

3-way split
Train / val / test. Val used for early stopping (you touch it often). Test used once at the end (untouched during tuning).
hygiene
XGBoost with early stopping
Set n_estimators generously (2000). early_stopping_rounds=50 halts training if val RMSE hasn't improved in 50 rounds. This is the #1 hyperparameter you should always use.
prod
Scratch GBM
The whole idea in ~15 lines. Init F=mean, fit tree to residual, add scaled prediction, repeat. This IS gradient boosting for MSE loss.
understand
LR sweep
lr=1.0 overshoots (worse test RMSE). lr=0.01 needs many trees (more compute but better generalisation). lr=0.05 is the sweet spot for most tabular problems.
tuning
Gain-based importance
Sum the loss reduction each feature contributes across all splits. XGBoost-specific, more meaningful than sklearn's default MDI.
explain
LightGBM alternative
5-10× faster on this dataset. Slightly different defaults (num_leaves instead of max_depth). Same conceptual model.
alt
Try itWatch what happens without early stopping

Remove early_stopping_rounds and set n_estimators=3000. After training, plot the train + val RMSE per round using model.evals_result_.

You'll see:

  • Train RMSE drops smoothly to near zero.
  • Val RMSE drops, plateaus, and starts RISING after some round.

Overfitting is not subtle in boosting — it's obvious. Always use early stopping.

💡 Hint · You'll see val RMSE start rising after ~800 rounds while train RMSE keeps dropping. That's the exact signature of overfitting — early stopping caught it automatically.

(d) Production reality · 15 min

War story Kaggle · nearly every tabular competition · 2015-2024thousands of competitions, ~70 % won by GBT
🔥 What broke

Between XGBoost's release (2014) and today, gradient-boosted trees have won roughly 70 % of Kaggle tabular competitions. The 2022 Grinsztajn et al paper formalised this: on 45 medium-sized tabular datasets, trees beat neural networks on almost every one.

🧯 The fix
Modern winning stack: LightGBM + XGBoost + CatBoost + a linear model, all with early stopping, blended by an out-of-fold stack or simple averaging. Deep learning enters only when tabular data + unstructured data (text, images) are combined.
🎓 Lesson to steal
Gradient-boosted trees are the answer for tabular data. Deep learning is for images, text, audio, and truly enormous tabular datasets (~100M+ rows with clear feature interactions). Choose the right tool for the shape of your data.
Post-mortem
War story Uber Michelangelo · ETA and pricing models· 2018millions of rides per day
🔥 What broke

Uber's Michelangelo ML platform was originally XGBoost-heavy for ETAs, pricing, and fraud detection. These models had to be retrained daily on billions of rows and served with <10 ms latency.

🧯 The fix
Uber's team invested heavily in distributed XGBoost + Spark integration, GPU inference, and model versioning. Today many Uber production models are boosted trees running on GPU-accelerated inference servers.
🎓 Lesson to steal
Gradient-boosted trees scale to production ML at hundreds-of-millions-of-users companies. They're not just a competition trick — they're the load-bearing model in most industrial ML systems.
Post-mortem
War story Airbnb · search ranking · 2016-2020· 2018millions of searches per day
🔥 What broke

Airbnb's initial neural network attempt to replace their GBT-based ranking model underperformed. It took two years of feature engineering + architecture iteration before a deep model beat the GBT baseline.

🧯 The fix
The eventual replacement was a hybrid — deep model on top of engineered features that had been developed for the GBT era. The GBT baseline informed which features actually mattered.
🎓 Lesson to steal
‘Replace GBT with deep learning’ is a multi-year project even for companies with unlimited resources. Start with GBT, iterate, and only move to deep when you have (a) so much data that deep beats GBT and (b) unstructured features that GBT can't leverage.
Post-mortem

Where this shows up in the rest of the plan

Gradient boosting is the industry-standard tabular ML approach
S089 · Decision trees
The atomic unit. GBT stacks hundreds of shallow trees.
S090 · Random forest
Parallel counterpart. Blends often stack RF + GBT for decorrelation.
S087 · Regularization
GBT lives on regularization — L1/L2 on leaves, subsample, colsample, early stopping.
S096 · Feature engineering
Even GBT benefits from good features — target encoding, aggregations, time-lag features.
S099 · SHAP interpretability
Tree-based SHAP is fast (TreeSHAP) and canonical for GBT models.
S123 · System design · fraud detection
GBT + rules is the reference architecture for fraud/AML in production.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three without notes, redo the session:

  1. What is gradient boosting in one sentence? (fit trees sequentially to residuals with a small learning rate)
  2. Why is early stopping non-negotiable? (boosting overfits fast; val curve is the honest signal)
  3. When would you pick GBT over deep learning? (tabular data, moderate sample size, interpretability matters)

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.