AI Knowledge Tree

XGBoost and LightGBM

Add second-order information, regularized objectives, and histogram splits to make boosting fast and accurate.

⏱ 28 minPrerequisites:Gradient boosting

Plain gradient boosting is already the best tabular learner; XGBoost, LightGBM, and CatBoost are the production implementations that made it fast, regularized, and easy to tune. They share four key innovations: second-order Newton steps, an explicit regularization term in the split criterion[1], histogram-based splits, and leaf-wise (best-first) or level-wise (depth-first) tree growth[2].

Newton boosting (second-order)

Plain gradient boosting uses the first-order gradient gi=∂ℓ/∂Fg_i = \partial \ell/\partial F. Newton boosting also uses the second derivative hi=∂2ℓ/∂F2h_i = \partial^2 \ell / \partial F^2. Locally, the loss is a quadratic in the prediction increment ff:

ℓ(yi,Ft−1(xi)+f)≈ℓ(yi,Ft−1(xi))+gif+12hif2.\ell(y_i, F_{t-1}(x_i) + f) \approx \ell(y_i, F_{t-1}(x_i)) + g_i f + \tfrac{1}{2} h_i f^2.

XGBoost adds an L2L_2 regularizer on leaf weights, so the per-leaf objective is ∑i∈S(gif+12hif2)+12λf2\sum_{i \in S}(g_i f + \tfrac{1}{2}h_i f^2) + \tfrac{1}{2}\lambda f^2. Setting the derivative in ff to zero gives the regularized Newton step:

f⋆=−∑i∈Sgi∑i∈Shi+λ,f^\star = -\frac{\sum_{i \in S} g_i}{\sum_{i \in S} h_i + \lambda},

with the unregularized f⋆=−G/Hf^\star = -G/H as the λ=0\lambda = 0 special case. For squared loss hi=1h_i = 1 and λ=0\lambda = 0 recover the residual mean; for log-loss the local curvature hi=pi(1−pi)h_i = p_i(1 - p_i) adjusts the step, and the +λ+\lambda shrinks leaf weights of small or noisy leaves. Second-order information makes training converge in fewer rounds.

What does that curvature actually weight? hi=pi(1−pi)h_i = p_i(1-p_i) is largest where the model is unsure and smallest where it is confident:

  • An uncertain example at pi=0.5p_i = 0.5 contributes hi=0.25h_i = 0.25, the maximum.
  • A fairly confident one at pi=0.9p_i = 0.9 contributes hi=0.09h_i = 0.09.
  • A near-certain one at pi=0.99p_i = 0.99 contributes hi=0.0099h_i = 0.0099, about 25×25\times less than the uncertain example.

Since hih_i sits in the denominator of f⋆=−G/(H+λ)f^\star = -G/(H + \lambda), a leaf full of near-certain examples (tiny HH) takes a larger step per unit gradient, while a leaf of uncertain examples (large HH) is held back. This is the same hh the code below computes as p_t * (1 - p_t): first-order boosting treats every example's curvature as 11 and misses this; Newton boosting reads it off the second derivative.

Regularized split criterion

XGBoost adds an L2L_2 penalty on leaf weights and a per-leaf cost γ\gamma. The gain of a split with left and right groups SL,SRS_L, S_R is

Gain=12 ⁣(GL2HL+λ+GR2HR+λ−(GL+GR)2HL+HR+λ)−γ,\text{Gain} = \tfrac{1}{2}\!\left(\frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L + G_R)^2}{H_L + H_R + \lambda}\right) - \gamma,

with GS=∑i∈SgiG_S = \sum_{i \in S} g_i and HS=∑i∈ShiH_S = \sum_{i \in S} h_i. A split is taken only if Gain > 0. λ\lambda shrinks leaf weights (smoother predictions); γ\gamma prunes splits that barely help, replacing a separate pruning step.

Histogram-based splits

Naive split finding sorts each feature at every node, costing O(ndlog⁡n)O(n d \log n) per tree. Histogram methods (LightGBM's default, XGBoost's hist mode) bucket each feature into a fixed number of bins (typically 256) once. Each node then iterates over bins, computing GG and HH accumulators in O(d⋅bins)O(d \cdot \text{bins}). The result: orders of magnitude faster on large datasets, with negligible accuracy loss.

Leaf-wise vs level-wise

  • Level-wise (depth-first, XGBoost's default): grow every node at depth dd before moving to depth d+1d + 1. Predictable structure, easy to vectorize.
  • Leaf-wise (best-first, LightGBM's default): always grow the leaf with the largest gain next. Trees become unbalanced but achieve lower training loss for the same number of leaves. Risk of overfitting on small datasets; control with max_leaves.

CatBoost's contribution: categoricals

XGBoost and LightGBM require categorical features to be one-hot or target-encoded. CatBoost uses ordered target statistics to encode categoricals without leakage and produces symmetric oblivious trees that make inference vectorized.

Hyperparameter cheat sheet

The knobs that matter most, roughly in order:

  1. learning_rate (eta): smaller is safer; 0.05 with many trees beats 0.3 with few.
  2. n_estimators (num_boost_round): as many as fit budget, paired with early stopping on a validation set.
  3. max_depth or num_leaves: complexity per tree. 6 levels or 31 leaves is a sensible default.
  4. min_child_weight (XGBoost) / min_data_in_leaf (LightGBM): refuse splits that produce tiny leaves.
  5. subsample and colsample_bytree: stochastic boosting (per-round row and column subsampling).
  6. reg_lambda / reg_alpha: leaf-weight regularization.

Cross-validation with early stopping is the standard fitting recipe: add trees while held-out loss improves, stop when it plateaus.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

# Newton boosting from scratch for log-loss, with histogram binning + L2 reg.
n, d = 1500, 4
X = rng.normal(size=(n, d))
true_f = 0.5 * X[:, 0] + 0.3 * X[:, 1] - 0.3 * X[:, 0] * X[:, 1]
p = 1 / (1 + np.exp(-true_f))
y = (rng.random(n) < p).astype(int)

def histogram(x, n_bins=64):
    """Map a 1D float array to integer bins; returns bins and bin edges."""
    qs = np.quantile(x, np.linspace(0, 1, n_bins + 1)[1:-1])
    return np.searchsorted(qs, x), qs

X_bins = np.column_stack([histogram(X[:, j])[0] for j in range(d)])
n_bins = 64

def fit_tree_hist(X_bins, g, h, max_depth=4, lam=1.0, gamma=0.0):
    """Newton tree on binned features, with L2-regularized gain."""
    n, d = X_bins.shape
    G_total, H_total = g.sum(), h.sum()
    indices = np.arange(n)

    class Node:
        def __init__(self): self.leaf = self.f = self.b = self.L = self.R = None

    def split(idx, depth):
        if depth == 0 or len(idx) < 2:
            return None, None, 0.0
        G_par, H_par = g[idx].sum(), h[idx].sum()
        best = (None, None, 0.0)
        for j in range(d):
            for b in range(n_bins - 1):
                mask = X_bins[idx, j] <= b
                Gl = g[idx[mask]].sum(); Hl = h[idx[mask]].sum()
                if mask.sum() < 2 or (~mask).sum() < 2: continue
                Gr, Hr = G_par - Gl, H_par - Hl
                gain = 0.5 * (Gl**2 / (Hl + lam) + Gr**2 / (Hr + lam) - G_par**2 / (H_par + lam)) - gamma
                if gain > best[2]: best = (j, b, gain)
        return best

    def build(idx, depth):
        node = Node()
        j, b, gain = split(idx, depth)
        if j is None or gain <= 0:
            node.leaf = -g[idx].sum() / (h[idx].sum() + lam); return node
        m = X_bins[idx, j] <= b
        node.f, node.b = j, b
        node.L = build(idx[m], depth - 1); node.R = build(idx[~m], depth - 1)
        return node

    return build(indices, max_depth)

def predict(tree, X_bins):
    out = np.zeros(len(X_bins))
    for i, x in enumerate(X_bins):
        node = tree
        while node.leaf is None:
            node = node.L if x[node.f] <= node.b else node.R
        out[i] = node.leaf
    return out

idx_tr = rng.choice(n, size=1100, replace=False)
idx_te = np.setdiff1d(np.arange(n), idx_tr)

# Newton boosting for log-loss. Build each tree on TRAINING rows only (so
# splits and leaf weights are unpolluted by test data), then apply it to the
# full X_bins to extend F to both train and test rows.
F = np.zeros(n)
eta = 0.1
trees = []
X_bins_tr = X_bins[idx_tr]
for t in range(60):
    p_t = 1 / (1 + np.exp(-F[idx_tr]))
    g_tr = p_t - y[idx_tr]
    h_tr = p_t * (1 - p_t) + 1e-3
    tree = fit_tree_hist(X_bins_tr, g_tr, h_tr, max_depth=3, lam=1.0)
    F = F + eta * predict(tree, X_bins)
    trees.append(tree)

p_hat = 1 / (1 + np.exp(-F))
y_hat = (p_hat > 0.5).astype(int)
tr_acc = np.mean(y_hat[idx_tr] == y[idx_tr])
te_acc = np.mean(y_hat[idx_te] == y[idx_te])
log_loss_te = -np.mean(y[idx_te] * np.log(p_hat[idx_te] + 1e-12) + (1 - y[idx_te]) * np.log(1 - p_hat[idx_te] + 1e-12))
print(f"train acc: {tr_acc:.3f}, test acc: {te_acc:.3f}, test log-loss: {log_loss_te:.3f}")
print(f"trees: {len(trees)}, leaf weights via Newton step (g/h + lam)")

The Newton-boosted ensemble reaches strong test accuracy with a few dozen shallow trees, using binned features and an L2-regularized split criterion.

Exercises

Exercise 1. A leaf holds three examples with gradients g=(0.4,−0.2,0.6)g = (0.4, -0.2, 0.6) and Hessians h=(0.25,0.21,0.16)h = (0.25, 0.21, 0.16). Compute the regularized Newton leaf weight f⋆=−∑gi∑hi+λf^\star = -\frac{\sum g_i}{\sum h_i + \lambda} for λ=0\lambda = 0 and for λ=1\lambda = 1, and explain the effect of λ\lambda.

Solution

Step 1. Sum the gradients and Hessians: G=0.4−0.2+0.6=0.8G = 0.4 - 0.2 + 0.6 = 0.8 and H=0.25+0.21+0.16=0.62H = 0.25 + 0.21 + 0.16 = 0.62.

Step 2. For λ=0\lambda = 0:

f⋆=−0.80.62≈−1.290.f^\star = -\frac{0.8}{0.62} \approx -1.290.

For λ=1\lambda = 1:

f⋆=−0.80.62+1=−0.81.62≈−0.494.f^\star = -\frac{0.8}{0.62 + 1} = -\frac{0.8}{1.62} \approx -0.494.

Step 3. Adding λ\lambda to the denominator shrinks the leaf weight toward zero, from −1.29-1.29 to −0.49-0.49 here. The shrinkage is proportionally larger for small-HH leaves (few or confident examples), which is how the L2L_2 term damps noisy leaves that would otherwise take large steps.

Exercise 2. Using the regularized split-gain formula from the lesson, take λ=1\lambda = 1 and a node with GL=3G_L = 3, HL=4H_L = 4 on the left and GR=−1G_R = -1, HR=2H_R = 2 on the right. Compute the gain for γ=0\gamma = 0, then find the smallest γ\gamma that would reject the split.

Solution

Step 1. Compute the three terms with λ=1\lambda = 1:

GL2HL+λ=95=1.8,GR2HR+λ=13≈0.3333,\frac{G_L^2}{H_L+\lambda} = \frac{9}{5} = 1.8, \qquad \frac{G_R^2}{H_R+\lambda} = \frac{1}{3} \approx 0.3333,(GL+GR)2HL+HR+λ=(3−1)24+2+1=47≈0.5714.\frac{(G_L+G_R)^2}{H_L+H_R+\lambda} = \frac{(3-1)^2}{4+2+1} = \frac{4}{7} \approx 0.5714.

Step 2. Combine with the 12\tfrac12 factor and γ=0\gamma = 0:

Gain=12(1.8+0.3333−0.5714)=12(1.5619)≈0.781.\text{Gain} = \tfrac12(1.8 + 0.3333 - 0.5714) = \tfrac12(1.5619) \approx 0.781.

Step 3. A split is taken only if Gain>0\text{Gain} > 0, i.e. if 12(⋯ )−γ>0\tfrac12(\cdots) - \gamma > 0. The split is rejected once γ≥0.781\gamma \ge 0.781. So γ\gamma acts as a minimum-gain threshold, folding pruning into the split decision itself.

Exercise 3 (code). Verify the Newton leaf weight beats the plain gradient-descent leaf weight (which ignores curvature) on log-loss. For a single leaf, compare the loss after one step using f⋆=−G/(H+λ)f^\star = -G/(H+\lambda) against a first-order step −ηG-\eta G, on a batch of examples at varying confidence.

Solution

Step 1. Take a leaf of examples with current logits, compute log-loss gradients gi=pi−yig_i = p_i - y_i and Hessians hi=pi(1−pi)h_i = p_i(1 - p_i), then evaluate the total log-loss after adding the Newton step versus a tuned first-order step.

python · numpy
import numpy as np

rng = np.random.default_rng(0)
m = 200
y = (rng.random(m) < 0.5).astype(float)
F = rng.normal(0, 1.5, size=m)                 # current logits, varied confidence

def logloss(F):
    p = 1 / (1 + np.exp(-F))
    return -np.mean(y*np.log(p+1e-12) + (1-y)*np.log(1-p+1e-12))

p = 1 / (1 + np.exp(-F))
g = p - y
h = p * (1 - p)
lam = 1.0

newton = -g.sum() / (h.sum() + lam)            # second-order leaf weight

# Best first-order step over a grid of learning rates (curvature ignored)
best_fo = min(logloss(F - eta * g.sum() / m) for eta in np.linspace(0, 2, 201))
newton_loss = logloss(F + newton)

assert newton_loss <= best_fo + 1e-9           # Newton step is at least as good
print(f"Newton step loss: {newton_loss:.4f}, best first-order loss: {best_fo:.4f}")

Step 2. The Newton leaf weight reaches a log-loss at least as low as the best tuned first-order step, with no learning-rate search. The Hessian sum in the denominator sets the step size per leaf automatically, which is why second-order boosting converges in fewer rounds than plain gradient boosting.

This closes the C3 tree-based track; the next subsection covers instance- based and probabilistic classifiers, where the inductive bias is very different.


References

  1. [1]Chen, T., & Guestrin, C. (2016). "XGBoost: a scalable tree boosting system". KDD. Second-order Newton boosting with a regularized split objective (doi:10.1145/2939672.2939785).↩
  2. [2]Ke, G., Meng, Q., Finley, T., et al. (2017). "LightGBM: a highly efficient gradient boosting decision tree". NeurIPS. Histogram-based splits and leaf-wise tree growth.↩