Search Tech Journey

Find topics, journeys and posts

back to blog
ai mlintermediate 15m2026-07-06

Gradient Boosting — Intuition, XGBoost, LightGBM

A deep-dive on Intuition, XGBoost, LightGBM — part of a 24-topic evergreen learning series.

Why this session matters

Part of a 24-topic learning series on engineering, ML, and LLM systems. Each session is a 90-minute deep-dive on one topic — designed so anyone can pick it up cold. Every two topics are followed by a revision session with recall prompts and hands-on drills.


Part 1: Gradient Boosted Trees Part 1 — Boosting Intuition, Trees, Loss

Why this session matters

It builds on the rhythm of one focused topic, paced so you have time to actually absorb it rather than rush.

Agenda

  • Why gradient boosting still beats neural nets on tabular data
  • The boosting idea — fit a tree to the residual, repeat, sum the trees
  • Trees as base learners — splits, gain, depth, leaf values
  • Loss functions — MSE for regression, log-loss for classification
  • What goes wrong without regularisation (and what Part 2 will fix)

Pre-read (skim before the session)

Deep dive

1. Why GBDT still wins on tabular data

In 2024–2026, transformer-everything is the headline. But:

  • For tabular data — features, joins, categoricals — XGBoost / LightGBM / CatBoost still beat deep nets in 80%+ of Kaggle competitions and most enterprise ML jobs.
  • They handle missing values, mixed types, and skewed targets without much preprocessing.
  • They train on a CPU in minutes for tens of millions of rows.
  • They give you feature importance and SHAP explanations for free.

If your ML role touches click prediction, ranking, churn, risk scoring, demand forecasting — GBDT is the workhorse. Knowing it cold is non-negotiable.

2. The boosting idea in one line

Fit a tree to the residuals (errors) of the previous model. Add it. Repeat M times. The final prediction is the sum.

F_0(x) = mean(y)                       # initial guess
for m in 1..M:
    r_m = y - F_{m-1}(x)               # residuals
    h_m = tree.fit(x, r_m)             # tree on residuals
    F_m(x) = F_{m-1}(x) + lr * h_m(x)  # add (with learning rate)

Each tree is a correction. You need lots of small corrections (M = 500–2000) more than a few big ones.

3. Why this works — the gradient-descent view

For squared loss L(y, F) = (y - F)²/2, the negative gradient is exactly the residual (y - F). So fitting a tree to the residual IS approximating one step of gradient descent in function space.

For other losses (log-loss for classification, Huber for robust regression, quantile loss for prediction intervals), you fit a tree to the pseudo-residual = negative gradient of the loss w.r.t. the current prediction. Same algorithm, different "residual".

This is the Friedman 2001 insight: boosting is gradient descent where each "step" is a small tree.

4. The base learner — what makes a single tree

A regression tree recursively splits the feature space:

              x_3 < 4.5?
              /        \
           yes          no
           /              \
   x_1 < 2.0?       x_5 in {A, B}?
    /       \          /        \
   …         …       leaf      leaf
                    (val=2.1) (val=-0.7)

For each candidate split (feature × threshold), compute the gain:

gain = MSE(parent) - [w_L · MSE(left) + w_R · MSE(right)]

Choose the split with max gain. Recurse until depth limit or min-gain threshold. Leaf value = mean of y at that leaf (for MSE loss).

XGBoost/LightGBM use a smarter gain that includes regularisation — the next session.

5. Loss functions you'll actually use

TaskLossPseudo-residual
RegressionMSE: ½(y − F)²y − F
Binary classificationLog-loss: -y·log(p) − (1-y)·log(1-p) (p = σ(F))y − p
Robust regressionHuberclipped residual
Quantile (e.g. P90)Pinballα if y > F else α − 1
Ranking (LambdaMART)Pairwise / listwisegradient ∝ swap utility

For classification, you typically operate on logits (raw F), and the link function σ(F) gives probabilities. Same gradient-descent shell.

6. Learning rate, number of trees, depth — the three knobs

KnobTypical rangeEffect
learning_rate0.01 – 0.3Smaller = more trees needed, less overfit, more robust.
n_estimators100 – 5000Higher with smaller LR. Use early stopping.
max_depth3 – 8Deeper trees overfit faster; XGBoost prefers shallow.

Standard recipe:

  • lr = 0.05, n_estimators = 2000, max_depth = 6, early stopping with 50 rounds.
  • Let early stopping pick the actual tree count.

7. A worked example — predict house prices in 30 lines

import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split

df = pd.read_csv("ames-housing.csv")
y = df.pop("SalePrice")
X = pd.get_dummies(df, drop_first=True)

X_tr, X_v, y_tr, y_v = train_test_split(X, y, test_size=0.2, random_state=0)

model = xgb.XGBRegressor(
    n_estimators=2000,
    learning_rate=0.05,
    max_depth=5,
    subsample=0.9,
    colsample_bytree=0.9,
    eval_metric="rmse",
    early_stopping_rounds=50,
)
model.fit(X_tr, y_tr, eval_set=[(X_v, y_v)], verbose=False)

print("best iter:", model.best_iteration)
print("RMSE:", model.evals_result()["validation_0"]["rmse"][model.best_iteration])

That's a baseline that's very hard to beat without per-feature engineering.

8. The intuition for feature importance

GBDT gives you three importance metrics:

  1. Gain — total reduction in loss across all splits using this feature. The most useful one for "which features moved the needle?"
  2. Cover — number of training rows that fell into splits using this feature.
  3. Weight — count of splits using this feature. Misleading on high-cardinality categoricals.

For deeper explanation use SHAP (the next session will mention; full SHAP is its own rabbit hole). For a first pass, gain is fine.

9. What goes wrong without regularisation (preview of Part 2)

A deep tree on noisy data memorises. Combine 500 deep trees and you get a model that scores 0.01 RMSE on train and 5.0 on test. The classic fix list — covered in detail next session:

  • Shrinkage (= learning rate < 1)
  • Subsample rows per tree (stochastic gradient boosting)
  • Colsample features per tree
  • L1 / L2 leaf penalties (XGBoost's reg_alpha, reg_lambda)
  • Min child weight / min samples leaf
  • Early stopping on a held-out set

10. What's next (the next session — GBDT Part 2)

  • XGBoost — the second-order Newton step, sparsity-aware splits, regularisation in the gain function
  • LightGBM — histogram-based splits, leaf-wise growth, GOSS, EFB
  • CatBoost — ordered boosting, native categorical handling
  • Tuning recipes (Optuna)
  • Production gotchas (feature drift, label leakage, monitoring)

Reading material

Books:

  • The Elements of Statistical Learning — Hastie, Tibshirani, Friedman (ch. 10: Boosting and Additive Trees — the textbook treatment)
  • Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow — Aurélien Géron (ch. 7: Ensemble Learning)
  • An Introduction to Statistical Learning — James, Witten, Hastie, Tibshirani (ch. 8.2 for boosting intuition)

Papers:

Official docs:

Blog posts:

In-depth research material

Videos

LeetCode — Binary Tree Maximum Path Sum

Assignment / Deliverables

Give yourself a clean 2-hour window and complete all of these before moving on:

  1. Read the deep-dive above end-to-end — no skimming. Take notes in your own words.
  2. Solve the LeetCode problem below without help first. Only look at the hint after 15 focused minutes; only look at editorial after 30. Log your time.
  3. Reproduce one code snippet locally. Pick the snippet that felt least obvious and get it running in a scratch file / notebook.
  4. Draw the core diagram from memory. Paper, whiteboard, or tldraw — doesn't matter. If you can't, re-read section 2 and try again.
  5. Write a 3-line takeaway in your prep journal: what surprised you, what you still don't understand, what you'd read next.
  6. Skim one item from the Reading material section. Bookmark the rest for the weekend.
  7. Commit any code + notes to your prep repo with message session-NN: <one-line summary>.

Stretch (optional, +30 min): explain today's topic to a rubber duck / a friend / a voice note. If you can't teach it in 5 minutes, you don't own it yet — flag it and revisit next weekend.

Post-session checklist

By the end of this session you should be able to:

  • Explain the boosting loop in 5 lines of pseudocode.
  • Derive that the negative gradient of MSE is the residual.
  • Pick a loss for: regression, binary classification, quantile prediction.
  • Tune learning_rate, n_estimators, max_depth with early stopping.
  • Read a feature-importance plot (gain) and call out top drivers.

Generated from sessions_data.py + content_part*.py. To edit a video / leetcode / title, edit the data file and re-run write_sessions.py.


Part 2: GBDT Part 2 — XGBoost, LightGBM, Regularisation, In-Practice Tuning

Why this session matters

It builds on the rhythm of one focused topic, paced so you have time to actually absorb it rather than rush.

Agenda

  • XGBoost — second-order Taylor expansion, exact greedy + approximate split
  • LightGBM — histogram-based, GOSS, EFB, leaf-wise growth
  • Regularisation — L1, L2, gamma, min_child_weight, max_depth
  • Hyperparameter tuning — the order that actually matters
  • When NOT to use GBDT — high-cardinality categoricals, sparse text, sequential data

Pre-read (skim before the session)

Deep dive

1. Why XGBoost won Kaggle

the next session covered the math of boosting: fit a tree to the gradient, add it scaled by learning rate, repeat. XGBoost added three things that turned a textbook algorithm into a production weapon:

  1. Second-order objective — uses both gradient and Hessian, so the loss approximation is quadratic, not linear. Splits are chosen with sharper objective improvement.
  2. System engineering — sparse-aware split-finding, column block pre-sort, out-of-core training. Trains on disk-resident data faster than scikit-learn loads it.
  3. Regularisation baked inα (L1), λ (L2) on leaf weights; γ (min-loss-reduction) for pruning. Generalises out of the box.

2. XGBoost's objective

The objective at step t:

Obj^(t) = Σ_i  loss(y_i, ŷ_i^(t-1) + f_t(x_i))  +  Ω(f_t)
        ≈ Σ_i  [ g_i · f_t(x_i)  +  ½ h_i · f_t(x_i)² ]  +  Ω(f_t)

Where g_i = ∂loss/∂ŷ, h_i = ∂²loss/∂ŷ². For a tree with leaves j and weights w_j:

Obj^(t) = Σ_j  [ (Σ_{i∈j} g_i) · w_j  +  ½ (Σ_{i∈j} h_i + λ) · w_j² ]  +  γ T

Closed-form optimal leaf weight:

w_j* = - G_j / (H_j + λ)

And the gain of a candidate split:

Gain = ½ [ G_L² / (H_L + λ)  +  G_R² / (H_R + λ)  -  (G_L + G_R)² / (H_L + H_R + λ) ] - γ

If Gain \< 0, don't split. That's γ pruning. This is the core formula in tree boosting.

3. Exact vs approximate split finding

Exact greedy: for each feature, sort, scan, compute gain at every split point. O(n × d) per node. Great for small data, brutal for billions of rows.

Approximate (histogram): bucket each feature into max_bin (default 256) bins. Split-finding is now O(max_bin × d), independent of n. XGBoost's tree_method='hist' (default since 1.0) and all of LightGBM use this.

You barely lose accuracy (the buckets are quantile-based) and gain 10–100× speedup. There's no reason to use exact-greedy on >100K rows.

4. LightGBM's additions

LightGBM doubles down on histogram + ships two more tricks:

  • GOSS (Gradient-based One-Side Sampling): keep all rows with large gradient (they're hard examples); randomly sample easy ones (small gradient). Cuts training time without losing accuracy.
  • EFB (Exclusive Feature Bundling): bundle sparse features that are rarely non-zero at the same time into a single feature. Saves memory and time on wide sparse data (one-hot encodings).
  • Leaf-wise growth (vs level-wise): always split the leaf with highest loss reduction, not the whole level. Deeper, lopsided trees per iteration; gives more loss reduction per tree but can overfit small data — control with num_leaves and min_data_in_leaf.

Practical: LightGBM is 2–10× faster than XGBoost on the same data, usually within 0.5% AUC.

5. Regularisation — what each knob does

ParamWhat it doesWhen to increase
max_depth / num_leavesTree sizeDecrease if overfitting
min_child_weight / min_data_in_leafMin samples per leafIncrease if overfitting
λ / reg_lambdaL2 on leaf weightsIncrease if overfitting
α / reg_alphaL1 on leaf weightsIncrease to drive weights to zero
γ / min_split_gainMin loss reduction to splitIncrease to prune more
subsampleRow sampling per treeDecrease (e.g., 0.8) to reduce variance
colsample_bytreeColumn sampling per treeDecrease (e.g., 0.8) to reduce variance
learning_rate (η)Step sizeDecrease and increase n_estimators together

6. Tuning order that actually works

Most online tuning guides have it wrong (GridSearchCV over everything). The real order:

  1. Fix learning_rate=0.1, n_estimators=1000 with early stopping on a validation set.
  2. Tune max_depth / num_leaves first — biggest impact on capacity.
  3. Then min_child_weight / min_data_in_leaf — controls overfit.
  4. Then subsample, colsample_bytree — variance reduction.
  5. Then reg_alpha, reg_lambda, gamma — fine-tuning.
  6. Finally drop learning_rate to 0.01 or 0.05, bump n_estimators proportionally for the final model.

Use Optuna with TPE — Bayesian search beats grid for >3 hyperparams.

import optuna, lightgbm as lgb

def objective(trial):
    params = {
        "objective": "binary",
        "metric": "auc",
        "num_leaves": trial.suggest_int("num_leaves", 16, 256),
        "max_depth": trial.suggest_int("max_depth", 4, 12),
        "min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
        "learning_rate": 0.05,
        "feature_fraction": trial.suggest_float("feature_fraction", 0.5, 1.0),
        "bagging_fraction": trial.suggest_float("bagging_fraction", 0.5, 1.0),
        "bagging_freq": 5,
        "lambda_l2": trial.suggest_float("lambda_l2", 1e-3, 10, log=True),
    }
    model = lgb.train(params, train_set=dtrain, valid_sets=[dval],
                      num_boost_round=2000, early_stopping_rounds=50, verbose_eval=False)
    return model.best_score["valid_0"]["auc"]

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)

7. Categorical handling

  • XGBoost: needs one-hot or target encoding. Recently added native categorical support but still rough.
  • LightGBM: native categorical via categorical_feature param. Splits like {a, c, f} vs {b, d, e} instead of one-hot.
  • CatBoost: best-in-class, uses ordered target statistics; built for categoricals.

For high-cardinality categoricals (>1000 levels), use target encoding with leave-one-out or k-fold to avoid leakage.

8. When GBDT is the wrong tool

GBDT dominates tabular. It's the wrong tool for:

  • Text / images / audio — no spatial or sequential prior. Neural nets win.
  • Very high-cardinality sparse features — embeddings handle this better.
  • Online learning — GBDT is batch by nature. Use SGD or factorisation machines.
  • Strict latency budget (<1 ms per inference at high QPS) — logistic regression or a tiny MLP is simpler.

9. Production checklist

  • Monotonic constraints (monotone_constraints) — force the model to be monotonic in features like price or bid — keeps business logic sane.
  • Feature importance — use SHAP, not the built-in gain. SHAP is consistent; built-in importance varies wildly across runs.
  • Calibration — gradient boosting on log-loss is reasonably calibrated, but verify with reliability diagrams. Use isotonic or Platt if needed.
  • Drift monitoring — track feature distributions in production vs training; alert on PSI > 0.2.

Reading material

Books:

  • The Elements of Statistical Learning — Hastie, Tibshirani, Friedman (ch. 10 continued)
  • Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow — Géron (ch. 7 advanced ensembles)
  • Approaching (Almost) Any Machine Learning Problem — Abhishek Thakur (Kaggle GM; XGBoost / LightGBM tuning recipes)

Papers:

Official docs:

Blog posts:

In-depth research material

Videos

LeetCode — Split Array Largest Sum

Assignment / Deliverables

Give yourself a clean 2-hour window and complete all of these before moving on:

  1. Read the deep-dive above end-to-end — no skimming. Take notes in your own words.
  2. Solve the LeetCode problem below without help first. Only look at the hint after 15 focused minutes; only look at editorial after 30. Log your time.
  3. Reproduce one code snippet locally. Pick the snippet that felt least obvious and get it running in a scratch file / notebook.
  4. Draw the core diagram from memory. Paper, whiteboard, or tldraw — doesn't matter. If you can't, re-read section 2 and try again.
  5. Write a 3-line takeaway in your prep journal: what surprised you, what you still don't understand, what you'd read next.
  6. Skim one item from the Reading material section. Bookmark the rest for the weekend.
  7. Commit any code + notes to your prep repo with message session-NN: <one-line summary>.

Stretch (optional, +30 min): explain today's topic to a rubber duck / a friend / a voice note. If you can't teach it in 5 minutes, you don't own it yet — flag it and revisit next weekend.

Post-session checklist

By the end of this session you should be able to:

  • Derive the XGBoost gain formula from the second-order Taylor expansion.
  • Explain leaf-wise vs level-wise growth and when each overfits.
  • List the tuning order (depth → leaf-min → subsample → reg → η).
  • Compare LightGBM, XGBoost, CatBoost on speed and categorical handling.
  • Identify two problem types where GBDT is the wrong choice.
  • Solve split-array-largest-sum — binary-search on the answer; same shape as tuning a split threshold.

Generated from sessions_data.py + content_part*.py. To edit a video / leetcode / title, edit the data file and re-run write_sessions.py.