Greedy Function Approximation: A Gradient Boosting Machine, annotated
How to read this page
- Any dotted word explains itself when you hover it, tab to it, or tap it, and so does every symbol in every equation.
- The diagrams are live: hover or tap a part. The playground in §5 boosts small trees in your browser: switch the loss, drag the learning rate and the number of trees, and watch three bad labels pull the fit off course, or not.
Each idea climbs the same ladder: an everyday picture, a tiny example you can check by hand, a diagram, the math, and why it matters today. The running example is the one the trees and boosting lesson uses: four houses of size 1, 2, 3 and 4 that sold for 1, 2, 6 and 7. The lesson's GradientBoosting is this paper's Algorithm 1 in about thirty lines of NumPy.
Abstract · original
“Function estimation/approximation is viewed from the perspective of numerical optimization in function space, rather than parameter space.”Friedman (2001), Abstract
Everyday picture
A sculptor does not carve a statue in one blow. Each tap removes a little of what is still wrong, guided by where the stone most differs from the figure in mind. This paper says boosting is exactly that: every new small tree is a tap aimed at the direction in which the current model's mistakes shrink fastest. That direction is the gradient of the loss, and because every loss has one, the same recipe works for prices, for yes/no labels, for data full of outliers.
What the paper claims
- Adding one small model at a time, each chosen to reduce the loss, is gradient descent carried out on the predictions rather than on weights (§2 and §3).
- A single generic algorithm, Gradient_Boost, works for any differentiable loss: fit a tree by least squares to the negative gradient, then choose the step size for the actual loss (Algorithm 1).
- With trees as the small models, each leaf can get its own best step, giving the “TreeBoost” algorithms for squared error, absolute error, the Huber loss and logistic classification (§4).
- Shrinking every step by a learning rate ν makes the result markedly more accurate, at the price of more trees (§5).
- Boosted trees can be read through relative importance and partial dependence plots (§8), and inherit trees' robustness to messy data (§10).
Why it matters today
Gradient-boosted trees are the most common winning model on tabular data. XGBoost, LightGBM, CatBoost and scikit-learn's boosting all run this paper's loop, with refinements. See the XGBoost companion for the most influential of those refinements.
1 Function estimation · original
Everyday picture
Learning from examples means finding the rule F that turns inputs x (a house's size) into guesses for the output y (its price) so that, on average over all houses the world could produce, a chosen penalty for bad guesses is as small as possible. The penalty is the loss function L(y, F). Pick a different penalty and a different rule is best.
Tiny example
Suppose the rule may only be a single constant guess c for every house. With the squared penalty (y − c)2, the best constant for prices 1, 2, 6 and 7 is their average, 4. With the absolute penalty |y − c|, any value between the two middle prices is best, and the median 4 is the usual pick. For these four prices the two agree; §4.2 shows a case where they do not.
In words: “the best rule is the one whose average penalty, over every input and output the world could produce, is smallest.”
With the numbers: with only constants allowed and the four prices as the whole world, the average squared penalty at c = 4 is (9 + 4 + 4 + 9)/4 = 6.5, lower than at c = 3 (7.5) or c = 5 (7.5). The average absolute penalty at c = 4 is (3 + 2 + 2 + 3)/4 = 2.5.
In Python:
y = [1, 2, 6, 7]
# E L(y, c): the average penalty of the constant guess c
def avg_squared(c):
return sum((y_i - c) ** 2 for y_i in y) / len(y)
[avg_squared(c) for c in (3, 4, 5)] # → [7.5, 6.5, 7.5]
# the absolute penalty at c = 4
sum(abs(y_i - 4) for y_i in y) / len(y) # → 2.5
The paper then restricts F to an additive expansion: a sum of M simple pieces h(x; am), each with its own parameters am and weight βm. Neural networks, splines and wavelets all have this shape; the paper's case of interest is where each piece is a small regression tree, whose parameters are its splitting variables, split points and leaf values.
In words: “the model is a weighted sum of M simple functions of the input, each shaped by its own parameters.”
With the numbers: one stump, h(x) = −2.5 if size ≤ 2.5 and +2.5 otherwise, with weight β = 1, added to a constant 4: F(4) = 4 + 1 × 2.5 = 6.5 and F(1) = 4 − 2.5 = 1.5.
In Python:
# h(x; a): a stump that splits size at 2.5
def h(x):
return -2.5 if x <= 2.5 else 2.5
beta = 1.0
# F(x) = 4 + β h(x)
[4 + beta * h(x) for x in (1, 4)] # → [1.5, 6.5]
1.1 and 1.2: optimizing parameters, one step at a time
With the shape fixed, learning becomes choosing parameters. Most methods do that by building the answer as a sum of steps: start from a guess, then repeatedly move the parameters a little in the direction that lowers the loss fastest, which is the negative gradient, by an amount found by a line search. That is steepest descent, the ancestor of every optimizer in the optimizers lesson.
Why it matters
Separating the loss L from the model F is the move that makes the rest of the paper general: every algorithm that follows is the same loop with a different L plugged in.
2 Numerical optimization in function space · original
Everyday picture
Instead of turning the knobs of a machine and watching all its outputs move at once, imagine reaching in and moving each output directly. If the machine predicts four house prices, treat those four predictions themselves as the four knobs. The slope of the loss with respect to each prediction says which way that one prediction should move.
Tiny example
Start from F = (4, 4, 4, 4) for the four houses. With the squared loss ½(y − F)2, the slope for each house is F − y = (3, 2, −2, −3). Stepping the other way by a full unit moves the predictions to (1, 2, 6, 7): every price hit exactly. That is suspiciously perfect, and useless for a house of size 2.5, which is not one of the four knobs. §3 fixes this.
In words: “at every input, the next increment moves the prediction against the average slope of the loss there, by a step size ρ.”
With the numbers: for house 4, y = 7 and F0 = 4, so the slope of ½(7 − F)2 at F = 4 is 4 − 7 = −3, and with ρ = 1 the increment is +3: the prediction moves from 4 to 7.
In Python:
y, F, eps = 7.0, 4.0, 1e-6
# ∂L/∂F for L = ½(y − F)², measured by nudging F both ways
g = (0.5 * (y - (F + eps)) ** 2 - 0.5 * (y - (F - eps)) ** 2) / (2 * eps)
round(g, 6) # → -3.0
rho = 1.0
# f_m = −ρ g_m
round(-rho * g, 6) # → 3.0
Why it matters
This is the paper's central reframing. In ordinary training the gradient is taken with respect to weights; here it is taken with respect to the predictions. The same idea explains why, in the trees and boosting lesson, the residual y − F is called a negative gradient.
3 Finite data · original
“This permits the replacement of the difficult function minimization problem (9) by least-squares function minimization (11), followed by only a single parameter optimization based on the original criterion (12).”Friedman (2001), §3
Everyday picture
The gradient from §2 exists only at the houses in the training data: four arrows saying “this one up by 3, that one down by 2”. To predict for a house nobody has seen, the paper looks for the small tree whose predictions point most nearly in the same direction as those arrows. The tree is a smooth-enough stand-in for the arrows that also has an answer everywhere else.
Tiny example
The arrows (the negative gradient, called the pseudo-responses ỹ) for the four houses are −3, −2, +2, +3. Among all stumps, the one splitting at size 2.5 and predicting −2.5 on the left and +2.5 on the right fits them best by least squares (leftover squared error 1, against 14 for either other split). The line search then asks how far to move along that stump; for squared loss the answer is ρ = 1.
Hover or tap an arrow or a flat bar.
Reading it: each arrow starts at zero and shows how far, and which way, one house's prediction should move to reduce its squared error fastest: that is the negative gradient, −g. The two flat bars are the stump's answer: one value for every house left of the dashed split, one for every house right of it. The bars cannot match every arrow (they miss houses 1 and 4 by 0.5 each way), but they point the same way everywhere, and unlike the arrows they give an answer for any size at all. Fitting the bars to the arrows is equation (11); deciding how far to move along the bars is equation (12).
The math: Algorithm 1, line by line
Line 3 of Algorithm 1 computes the pseudo-responses:
In words: “for every training example i = 1 … N, work out how the loss would change if its prediction were nudged, and flip the sign: that is the target the next tree is trained on.”
With the numbers: squared loss ½(y − F)2 at F0 = 4: ỹ = y − 4 = (−3, −2, 2, 3).
In Python:
y = [1, 2, 6, 7]
F0 = sum(y) / len(y)
# ỹ_i = −∂L/∂F = y_i − F_0 for squared loss
y_tilde = [y_i - F0 for y_i in y]
y_tilde # → [-3.0, -2.0, 2.0, 3.0]
Line 4 fits the base learner to them by least squares, whatever the loss:
In words: “choose the tree whose predictions, suitably scaled, come closest to the pseudo-responses in squared distance.”
With the numbers: the three possible stumps split after house 1, 2 or 3. Their leftover squared errors on (−3, −2, 2, 3) are 14, 1 and 14, so the split at 2.5 wins, with leaf values −2.5 and +2.5.
In Python:
y_tilde = [-3.0, -2.0, 2.0, 3.0]
def sse(v):
mean = sum(v) / len(v)
return sum((a - mean) ** 2 for a in v)
# leftover squared error after splitting after house t
[sse(y_tilde[:t]) + sse(y_tilde[t:]) for t in (1, 2, 3)] # → [14.0, 1.0, 14.0]
Line 5 then goes back to the real loss to decide the step size:
In words: “slide along the new tree's direction and stop where the actual loss on the training data is lowest.”
With the numbers: for squared loss the best ρ is Σ ỹi hi / Σ hi2 = (7.5 + 5 + 5 + 7.5) / 25 = 1.0.
In Python:
y_tilde = [-3.0, -2.0, 2.0, 3.0]
h = [-2.5, -2.5, 2.5, 2.5]
# for squared loss the line search has a closed form: Σ ỹ h / Σ h²
sum(a * b for a, b in zip(y_tilde, h)) / sum(b * b for b in h) # → 1.0
Hover or tap a step. Start at the top with the constant.
Reading it: start at the top: the model begins as the single best constant. Everything inside the dashed frame repeats once per tree. Only the first and third steps know which loss you chose: the pseudo-responses are that loss's negative gradient, and the step size is chosen on that loss. The middle step is always the same least-squares tree fit, which is why one tree-growing routine serves every loss. The arrow on the left carries the updated model back to the top for the next round; after M rounds the model is the constant plus M shrunken trees.
Why it matters
“Least squares to find the direction, the real loss to find the step” is the paper's engineering insight. It means one fast tree-growing routine can boost any differentiable loss. The lesson's GradientBoosting.negative_gradient is line 3; its call to DecisionTree with the squared criterion is line 4.
4 Applications: additive modeling · original
Everyday picture
Algorithm 1 is a machine with a slot for the loss. This section feeds it four different losses and writes out what comes out: least squares (a sanity check), least absolute deviation (robust), Huber (a compromise) and the logistic likelihood (classification).
4.1 Least-squares regression · original
Tiny example
With L = ½(y − F)2 the pseudo-responses are the plain residuals y − F, the tree fits them, and the line search just returns the tree's own scale. This is “fit the residuals, add, repeat”, the loop the lesson teaches: after one round with a step of 0.5, the four houses are predicted 2.75, 2.75, 5.25 and 5.25, and the mean squared error falls from 6.5 to 1.8125.
Why it matters
The paper calls this a reality check: the general machine reproduces the familiar method. Algorithm 2 (LS_Boost) is exactly the lesson's boosting_worked_example.
4.2 Least-absolute-deviation (LAD) regression · original
Everyday picture
With the absolute loss |y − F|, the slope is the same size however badly a prediction misses; only its sign matters. So each tree is trained on “too low” or “too high”, never on “by how much”. A wildly wrong label cannot shout louder than any other.
Tiny example
A typo turns house 4's price from 7 into 70. Least squares starts from the mean, 19.75, and its residuals are −18.75, −17.75, −13.75 and +50.25: the whole next tree bends towards the typo. LAD starts from the median, 4, and its pseudo-responses are −1, −1, +1, +1, exactly what they were before the typo.
In words: “under absolute loss, each example asks only to be moved up or down, never by how much.”
With the numbers: prices (1, 2, 6, 70) and F0 = median = 4: signs of (−3, −2, 2, 66) are (−1, −1, +1, +1).
In Python:
import statistics
y = [1, 2, 6, 70]
# least squares starts at the mean, and the typo dominates the residuals
F0_ls = sum(y) / len(y)
[round(y_i - F0_ls, 2) for y_i in y] # → [-18.75, -17.75, -13.75, 50.25]
# LAD starts at the median; its pseudo-responses are signs
F0 = statistics.median(y)
[(y_i > F0) - (y_i < F0) for y_i in y] # → [-1, -1, 1, 1]
The line search for this loss becomes a weighted median (equation 14).
Why it matters
Real targets have typos, sensor glitches and one-off mansions. Choosing the loss is choosing what the model is allowed to be influenced by, and gradient boosting makes that choice a one-line change.
4.3 Regression trees · original
Everyday picture
A tree is not one direction but J separate little ones: one flat value per leaf. So instead of a single step size for the whole tree, give each leaf its own best step. Each leaf becomes a tiny problem: “for the examples that land here, what single number, added to their current predictions, lowers the loss most?”
Tiny example
For the left leaf (houses 1 and 2, residuals −3 and −2), the best addition under squared loss is their average, −2.5. Under absolute loss it is their median. The tree's structure comes from the least-squares fit of §3; only the leaf values are re-chosen for the real loss.
In words: “each leaf's value is the constant that, added to the current predictions of the examples in that leaf, minimizes their loss.”
With the numbers: left leaf, squared loss: trying γ = −3, −2.5 and −2 gives summed losses 0.5, 0.25 and 0.5 (halved squares), so γ = −2.5.
In Python:
# the left leaf: houses 1 and 2, current prediction 4
y_leaf, F_prev = [1, 2], 4.0
def leaf_loss(gamma):
return sum(0.5 * (y_i - (F_prev + gamma)) ** 2 for y_i in y_leaf)
[leaf_loss(g) for g in (-3.0, -2.5, -2.0)] # → [0.5, 0.25, 0.5]
For LAD this gives Algorithm 3 (LAD_TreeBoost): trees are grown on the signs, and each leaf adds the median of its residuals. The paper notes it is highly robust: the trees see only the order of each input, the pseudo-responses take only two values, and the leaf updates are medians.
Why it matters
Per-leaf steps are what “TreeBoost” means, and XGBoost keeps them. It goes one step further and solves (18) approximately for any loss with a second-order formula; see the XGBoost companion.
4.4 M-regression · original
Everyday picture
Squared error is ideal for well-behaved noise but panics at outliers; absolute error is calm about outliers but wastes information on ordinary points. The Huber loss is squared for small misses and absolute for large ones: listen closely to small complaints, and cap the volume of large ones.
Tiny example
With the typo prices (1, 2, 6, 70), starting from 4, the residuals are −3, −2, 2 and 66. With δ = 5, the first three are small and pass through unchanged; the typo is clipped to 5.
In words: “penalize a miss by half its square while it is within δ, and only in proportion to its size beyond that” (equation 19); its negative gradient is the residual, clipped to ±δ.
With the numbers: residuals (−3, −2, 2, 66) with δ = 5 give pseudo-responses (−3, −2, 2, 5). The paper sets δ each round to the α-quantile of the absolute residuals, with α = 0.9 by default, so the largest 10% are treated as outliers.
In Python:
residuals = [-3.0, -2.0, 2.0, 66.0]
delta = 5.0
# −∂L/∂F: the residual, clipped to ±δ
[max(-delta, min(delta, r)) for r in residuals] # → [-3.0, -2.0, 2.0, 5.0]
Algorithm 4 (M_TreeBoost) approximates each leaf's value with one step of Huber's iterative method, starting from the leaf's median residual.
Why it matters
§6.2 finds M_TreeBoost within about 1% of the best method under both perfectly normal noise and extremely heavy-tailed noise. Matching the loss to the noise is still the first modelling choice in boosting, and here it costs nothing but a different pseudo-response.
4.5 Two-class logistic regression and classification · original
Everyday picture
For yes/no labels, the model's output F is a score, and the loss punishes confident wrong scores heavily and confident right ones barely at all. The pseudo-response is then “how surprised were we”: large for examples the model gets wrong, near zero for those it already gets right.
Tiny example
Four emails, labelled y = +1 (spam), +1, +1 and −1. The paper's F is half the log-odds. The starting constant is ½ log(1.5 / 0.5) = ½ log 3 = 0.549, which corresponds to a 75% chance of spam, the base rate. The three spam emails get pseudo-response 0.5 each (a mild “more spam, please”), the one legitimate email −1.5 (a loud “much less”).
In words: “the loss is the logistic penalty on the score times the label; its negative gradient is twice the label, shrunk by how confidently right the model already is.”
With the numbers: with F0 = 0.549, for y = +1: 2 / (1 + e1.099) = 2 / 4 = 0.5. For y = −1: −2 / (1 + e−1.099) = −2 / 1.333 = −1.5. (That is 2(y′ − p) with y′ the 0/1 label and p = 0.75: the lesson's y − p, doubled because F is half the log-odds.)
In Python:
import math
y = [1, 1, 1, -1]
y_bar = sum(y) / len(y)
# F_0 = ½ log((1 + ȳ) / (1 − ȳ)): half the log-odds of the base rate
F0 = 0.5 * math.log((1 + y_bar) / (1 - y_bar))
round(F0, 3) # → 0.549
# ỹ_i = 2 y_i / (1 + exp(2 y_i F_0))
[round(2 * y_i / (1 + math.exp(2 * y_i * F0)), 3) for y_i in y] # → [0.5, 0.5, 0.5, -1.5]
The per-leaf problem (23) has no closed form, so the paper takes a single Newton-Raphson step: divide the leaf's summed slope by its summed curvature.
In words: “a leaf's step is its total pull divided by its total curvature: confident examples have small curvature and barely move the denominator.”
With the numbers: a leaf holding two spam emails: (0.5 + 0.5) / (0.75 + 0.75) = 0.667, so their score rises to 0.549 + 0.667 = 1.216, a spam probability of 1 / (1 + e−2.432) = 0.919. A leaf holding the legitimate email alone: −1.5 / 0.75 = −2.0, so its score falls to −1.451 and its spam probability to 0.052.
In Python:
import math
F0 = 0.5 * math.log(3)
def newton_step(leaf):
# Σ ỹ / Σ |ỹ|(2 − |ỹ|)
return sum(leaf) / sum(abs(t) * (2 - abs(t)) for t in leaf)
round(newton_step([0.5, 0.5]), 3) # → 0.667
round(newton_step([-1.5]), 3) # → -2.0
# probabilities from F: p = 1 / (1 + e^(−2F))
[round(1 / (1 + math.exp(-2 * (F0 + g))), 3) for g in (newton_step([0.5, 0.5]), newton_step([-1.5]))] # → [0.919, 0.052]
4.5.1 Influence trimming
The curvature |ỹi|(2 − |ỹi|) doubles as a measure of how much example i can still affect the fit. An email the model already scores confidently right has a pseudo-response near 0, and so a weight near 0. The paper drops, each round, the lowest-weight examples that together carry only a fraction α (0.05 to 0.2) of the total weight. It reports that 90% to 95% of the examples were often dropped without hurting accuracy, cutting computation 10 to 20 times.
In words: “an example's influence is the curvature of its loss: large while the model is unsure about it, near zero once it is confidently right.”
With the numbers: at the start every email has |ỹ| of 0.5 or 1.5 and weight 0.75. Much later, a spam email the model scores confidently right might have ỹ = 0.02 and weight 0.0396: nearly nothing to learn from it this round.
In Python:
# w_i = |ỹ_i| (2 − |ỹ_i|)
[round(abs(t) * (2 - abs(t)), 4) for t in (0.5, -1.5, 0.02)] # → [0.75, 0.75, 0.0396]
Why it matters
This is the lesson's log-loss boosting with Newton leaves, and the ancestor of XGBoost's second-order objective, where a second derivative hi plays exactly the role this curvature plays here, for every loss (see the XGBoost companion).
4.6 Multi-class logistic regression and classification · original
Everyday picture
With K classes, the model keeps K scores per input, turns them into probabilities with a softmax, and grows K trees per round, one per class, each pushing its class's score up where that class is under-predicted.
Tiny example
Three classes, all scores 0 at the start, so every class gets probability 1/3. For an example whose true class is the first, the pseudo-responses are (1 − 1/3, 0 − 1/3, 0 − 1/3) = (0.667, −0.333, −0.333).
In words: “for each class, the next tree's target is the gap between the true 0/1 label and the probability the model currently gives that class.”
With the numbers: y = (1, 0, 0) and p = (1/3, 1/3, 1/3) give (0.667, −0.333, −0.333).
In Python:
import math
F = [0.0, 0.0, 0.0]
# p_k = exp(F_k) / Σ_l exp(F_l)
p = [math.exp(F_k) / sum(math.exp(F_l) for F_l in F) for F_k in F]
y = [1, 0, 0]
# ỹ_k = y_k − p_k
[round(y_k - p_k, 3) for y_k, p_k in zip(y, p)] # → [0.667, -0.333, -0.333]
Each leaf again takes one Newton step (equation 32, with a factor (K − 1)/K). The paper compares this LK_TreeBoost with LogitBoost: the leaf updates agree, and they differ only in how the trees are split. It also finds its version more numerically stable, because LogitBoost divides by p(1 − p), which is near zero for any confidently classified example.
Why it matters
The target y − p is the same gradient of cross-entropy that trains the last layer of a neural classifier (see the losses lesson).
5 Regularization · original
“As illustrated here decreasing the learning rate clearly improves performance, usually dramatically. The reason for this is less clear.”Friedman (2001), §5
Everyday picture
Every tree is fitted to the training data, noise included. Taking each tree's full correction lets the first few trees commit hard to whatever they saw. Taking only a fraction of each correction means many trees must agree before the model moves far, which averages some of the noise away. The price is more trees.
Tiny example
With a learning rate ν = 0.1, house 4 moves from 4 to 4 + 0.1 × 2.5 = 4.25 after the first tree, instead of all the way to 6.5. The remaining gap is left for later trees, which will see a slightly different picture.
In words: “each round adds only a fraction ν of the best step the new tree offers.”
With the numbers: house 4: 4 + 0.1 × 1.0 × 2.5 = 4.25.
In Python:
F_prev, nu, rho, h = 4.0, 0.1, 1.0, 2.5
# F_m = F_{m−1} + ν ρ_m h
round(F_prev + nu * rho * h, 2) # → 4.25
Try it: boost in your browser
Forty noisy points around a sine curve, three of them pushed up by a labelling mistake (hollow circles). Each round adds one small tree with two levels of questions (up to four leaves). Choose the loss, drag ν and the number of trees M, and watch the fit (solid line) against the true curve (dashed). The chart below the picture tracks how far the fit is from the true curve after every round.
Hover or tap the chart to read the distance from the true curve after any number of trees.
Simulated for this page with a fixed random seed; the data are not from the paper. The distance is the average absolute gap between the fit and the true sine over 121 evenly spaced points.
Reading it: in the picture, dots are training points and the solid line is the model after M trees; it is always a staircase, because every tree adds flat steps. The chart below plots the fit's distance from the true curve against the number of trees, so its lowest point is the best M for your settings. With least squares, the three hollow outliers pull a bump into the fit near them; switch to least absolute deviation or Huber and the bump mostly disappears, because those losses cap how loudly one point can pull. Now drag ν: at 1.0 the curve bottoms out within a few trees and then worsens as trees chase noise; at 0.1 it falls slowly, reaches a lower minimum, and stays near it much longer. That is the paper's ν-M trade-off in miniature.
What the paper measured
The paper boosts 11-leaf trees on 5,000 simulated examples and scores each model with an error measure relative to the best constant, its equation (37):
In words: “the model's average distance from the true function, as a fraction of the distance a constant guess (the median) would have.” 0 is perfect; 1 is no better than a constant.
With the numbers: treat the four prices as the true function and the one-round fit (2.75, 2.75, 5.25, 5.25) as F̂: the top is (1.75 + 0.75 + 0.75 + 1.75)/4 = 1.25, the bottom (3 + 2 + 2 + 3)/4 = 2.5, so A = 0.5: the model has closed half the gap a constant leaves.
In Python:
import statistics
F_star = [1, 2, 6, 7]
F_hat = [2.75, 2.75, 5.25, 5.25]
top = sum(abs(a - b) for a, b in zip(F_star, F_hat)) / 4
bottom = sum(abs(a - statistics.median(F_star)) for a in F_star) / 4
top, bottom, top / bottom # → (1.25, 2.5, 0.5)
Hover or tap the chart to read the best number of trees at each learning rate.
Reading it: each line is one of the paper's methods from its Table 1: least squares and LAD scored by A, and the logistic model scored by its deviance (−2 log-likelihood). The horizontal axis is the learning rate ν on a log scale, and the vertical axis is the number of trees at which the error was lowest. Every line climbs steeply to the left: for least squares, halving ν roughly doubles the trees needed (15, 43, 77, 146, 326, 855). The table below shows what that buys; the logistic model's deviance falls in the same way, from 0.60 at ν = 1 to 0.45 at ν = 0.125.
| ν | LS: best M | LS: A | LAD: best M | LAD: A |
|---|---|---|---|---|
| 1.0 | 15 | 0.48 | 19 | 0.57 |
| 0.25 | 77 | 0.34 | 84 | 0.38 |
| 0.125 | 146 | 0.32 | 307 | 0.35 |
| 0.03 | 855 | 0.32 | 937 | 0.35 |
Going from ν = 1 to ν = 0.125 cuts the least-squares error by a third (0.48 to 0.32); going further buys nothing but trees. The paper also notes that the misclassification rate kept improving after the likelihood had started to overfit, since a class decision depends only on the sign of F, while the likelihood also cares about its size. And it records a mystery it could not explain: shrinking each step during training (36) works far better than shrinking the finished model towards the mean (38).
Why it matters
The paper's advice survives unchanged: set M as large as you can afford, then pick ν so the validation error reaches its minimum near the end, and stop there (early stopping). The lesson measures the same trade-off with boosting_curves.
6 Simulation studies · original
Everyday picture
One benchmark dataset flatters whichever method happens to suit it. The paper instead generates 100 random target functions of 10 inputs (§6.1: sums of 20 random bumps, each depending on a few inputs), and asks how each method does across all of them.
What it found
- Loss versus noise (§6.2). With normal noise, least squares was best on 73 of the 100 targets and Huber (M_TreeBoost) on the other 27; on average least squares was 0.2% worse than the best method, Huber 0.9% and LAD 7.4%. With the heavy-tailed “slash” noise the picture flips: least squares was 364.6% worse than the best on average, LAD 4.1% and Huber 1.0%. The paper's verdict: Huber is the method of choice, never far from the best.
- Steps versus smooth curves (§6.3). Boosted trees are staircases; MARS fits smooth curves. On the 100 smooth targets the two were comparable by average absolute error. But for MARS the root-mean-squared error was typically about 30% higher than its average absolute error, while for the boosted trees the two were close: MARS tends to be either very close or far off, and the boosted trees' errors are more even.
- Classification (§6.4). On five-class problems, LK_TreeBoost was best on 78 of 100 targets, 0.6% above the best on average, against 3.5% for LogitBoost and 15% for AdaBoost.MH. Once LogitBoost and AdaBoost.MH were also given shrinkage (ν = 0.1), all three came within a few percent of each other, which suggests the learning rate matters more than the structural differences.
Why it matters
Two lessons that still hold: the loss should match the noise, and shrinkage helps every boosting method, not just this one.
7 Tree boosting · original
Everyday picture
A house's price may depend on its size and its location separately (each adds its own amount), or on how they combine (a big house matters more downtown). A tree with J leaves can express combinations of at most J − 1 inputs. Stumps (J = 2) can only add up separate effects; bigger trees can capture combinations.
Tiny example
Write any function as a sum of parts that each depend on one input, parts that depend on pairs, parts on triples, and so on:
In words: “a function is its one-input effects, plus its two-input interactions, plus higher ones”; a tree with J leaves reaches at most order min(J − 1, n).
With the numbers: the paper's simulations have n = 10 inputs. Trees with J = 2, 3, 6, 11 and 21 leaves reach interactions of order 1, 2, 5, 10 and 10.
In Python:
n = 10
# highest interaction order a J-leaf tree can express: min(J − 1, n)
[min(J - 1, n) for J in (2, 3, 6, 11, 21)] # → [1, 2, 5, 10, 10]
Reading it: each bar is one tree size J from the paper's §7 (the numbers behind its Figure 6), over the 100 random targets: its length is how much larger, on average, that tree size's error was than the best tree size's on the same target. The number on the right is how many of the 100 targets that tree size won. Stumps (J = 2) won 8 times but were 23.2% worse on average: superb when the target happens to be additive, poor otherwise. From J = 6 up, the sizes are nearly tied and each wins about 30 targets.
Why it matters
This is why boosting uses small trees: a handful of leaves captures the interactions most real targets have, and the paper finds it unlikely that large trees would ever be necessary or desirable. The lesson boosts depth-2 trees (four leaves) in boosting_curves.
8 Interpretation · original
Everyday picture
A model made of hundreds of trees cannot be read tree by tree. The paper offers two summaries: which inputs matter most (relative importance), and how the prediction moves as one or two inputs change with everything else averaged out (partial dependence).
8.1 Relative importance of input variables · original
Tiny example
Each split in a tree removes some squared error. Credit that improvement to the input the split used, add up per input, and average over all trees. In the four-house stump, the split at size 2.5 removed 25 of the 26 units of squared error in the residuals.
In words: “the improvement from a split is the gap between the two sides' averages, squared, weighted by how balanced the split is.”
With the numbers: two houses a side (wl = wr = 2), averages −2.5 and 2.5: (2 × 2)/(2 + 2) × 52 = 25.
In Python:
w_l, w_r, y_l, y_r = 2, 2, -2.5, 2.5
# i² = w_l w_r / (w_l + w_r) · (ȳ_l − ȳ_r)²
w_l * w_r / (w_l + w_r) * (y_l - y_r) ** 2 # → 25.0
In words: “an input's squared importance is the total improvement of every split that used it, averaged over all the trees”; take the square root, and scale so the top input scores 100.
With the numbers: a tree splits on x1 twice (improvements 4.0 and 0.5) and on x2 once (1.0). Then Î1 = √4.5 = 2.12 and Î2 = 1.0, so on the 0 to 100 scale x1 scores 100 and x2 scores 47.1.
In Python:
import math
# (splitting variable v_t, improvement î_t²) for the three splits
splits = [(1, 4.0), (2, 1.0), (1, 0.5)]
I = {j: math.sqrt(sum(i2 for v_t, i2 in splits if v_t == j)) for j in (1, 2)}
round(I[1], 2) # → 2.12
# relative importance, top input = 100
round(100 * I[2] / I[1], 1) # → 47.1
Reading it: the paper checks the measure on a linear target whose true importances are known: input j's coefficient has size j, so on the 0 to 100 scale input 10 should score 100, input 9 score 90, and so on down to 10. Blue bars are those true values and striped bars the paper's estimates from its Table 2 (mean of ten runs, selected rows reproduced with attribution). The estimates track the truth closely, ranking the inputs correctly in every run, with the least important inputs slightly overstated (13.0 for a true 10).
Why it matters
Summing split improvements is the same idea as the impurity importance of a single tree, and it shares its weakness: it is computed on training data, so like the impurity importance in the trees and boosting lesson (DecisionTree.feature_importances) it can credit inputs that only help fit noise; permutation importance on held-out data (see the random forests companion) is the more honest check.
8.2 Partial dependence plots · original
Everyday picture
To see how a model's predicted price depends on size alone, take every real house in the data, pretend each had this particular size, ask the model, and average the answers. Do that for each size and plot the averages: a partial dependence plot.
Tiny example
A model F̂(x1, x2) = 2x1 + x1x2, and three rows whose x2 values are 0, 1 and 2. At x1 = 1 the model gives 2, 3 and 4 for the three rows, averaging 3. At x1 = 2 it gives 4, 6 and 8, averaging 6.
In words: “the partial dependence on the chosen inputs is the model's prediction with those inputs set to z, averaged over the other inputs as they occur in the data.”
With the numbers: F̄(1) = (2 + 3 + 4)/3 = 3.0 and F̄(2) = (4 + 6 + 8)/3 = 6.0.
In Python:
def F_hat(x1, x2):
return 2 * x1 + x1 * x2
# z_{i,\l}: the other input's value in each data row
x2_rows = [0, 1, 2]
# F̄(z) = (1/N) Σ F̂(z, x2_i)
[sum(F_hat(z, x2) for x2 in x2_rows) / len(x2_rows) for z in (1, 2)] # → [3.0, 6.0]
The paper stresses averaging over the other inputs' marginal distribution (51), not their distribution given z (56): the second would mix in effects that belong to correlated inputs. For a single tree it gives a fast recipe that needs no data at all: walk down the tree, follow the branch at nodes that split on the chosen inputs, and send weight down both branches, in proportion to their training counts, at nodes that split on anything else.
Why it matters
The paper points out that partial dependence works for any black-box model (neural networks, nearest neighbours, support vector machines), while its tree-walking shortcut makes it especially cheap for tree ensembles. It also notes the plot tells the whole story only when the model's dependence on the chosen inputs is roughly additive or multiplicative with the rest (equations 54 and 55); otherwise it is a useful summary, not a complete one.
8.3 A randomly generated function
Applied to the first of the §6 random targets, the importances show no small dominant subset of inputs, the one-input plots show smooth trends built from fine steps, and the two-input plots show interactions of varying strength, all consistent with how the target was generated.
9 Real data · original
Garnet data (§9.1)
13,317 garnets from rocks around the world; the task is to predict titanium dioxide concentration from 10 other chemical concentrations and a three-valued tectonic setting. The data are skewed with many outliers.
| Leaves J | LS_TreeBoost | LAD_TreeBoost | M_TreeBoost |
|---|---|---|---|
| 2 | 0.58 | 0.57 | 0.57 |
| 3 | 0.48 | 0.47 | 0.46 |
| 6 | 0.48 | 0.44 | 0.43 |
| 21 | 0.46 | 0.43 | 0.43 |
Stumps are clearly worse, so the inputs interact; six leaves are enough; and the robust losses beat least squares on this outlier-heavy data. Two inputs, gallium and zirconium, dominate the importances, and the two-input partial dependence shows a strong interaction between them: the prediction barely depends on either while the other is at its lowest values.
Demographic data (§9.2)
9,409 shopping-mall questionnaires; the task is to predict income from 13 other answers, most categorical with a few values, many missing. Here tree size hardly matters (errors 0.57 to 0.63 across all sizes and losses), so stumps suffice: the relationship is additive, and the partial dependence plots then describe the model completely. Income rises with education and peaks at age 45 to 54.
Why it matters
The same diagnostic still works: compare stumps with larger trees on validation data. If stumps are as good, the problem is additive and every effect can be plotted one input at a time.
10 Data mining · original
“All TreeBoost procedures are invariant under all (strictly) monotone transformations of the individual input variables.”Friedman (2001), §10
Everyday picture
Real data arrive messy: skewed incomes, outliers, irrelevant columns, missing answers. Boosted trees shrug off most of it, because a tree only ever asks “is this input above that threshold?”.
What the paper lists
- No input transformations needed. Using x, log x, ex or xa gives the same result, since the order of values is all a split sees; long-tailed inputs and input outliers stop mattering. LAD_TreeBoost is also fully robust to outliers in the output, and M_TreeBoost largely so.
- Built-in feature selection and robustness to irrelevant inputs, and trees' handling of missing values.
- The weaknesses of single trees are softened. Coarse staircases become fine ones, instability is averaged away, and the interaction order is controlled by the tree size.
- Speed. After sorting the inputs, the cost grows linearly with the number of examples, inputs and trees. M_TreeBoost on the garnet data (500 trees of 6 leaves) took 20 seconds on a 933 MHz Pentium III.
- Incremental use. Good-enough models often appear by about 100 trees, and boosting can be resumed later, or continued on new data, from where it stopped.
Why it matters
This list is, almost word for word, why boosted trees became the default on tables: the same reasons the trees and boosting lesson gives for trees beating neural networks there (units don't matter, thresholds are native, irrelevant columns are ignored).
What changed since 2001
The loop is unchanged: pseudo-responses, a least-squares tree, per-leaf steps, shrinkage. The refinements around it:
| In the paper | Common today | Why | Where |
|---|---|---|---|
| Newton leaf values only for the logistic losses | A second-order (Newton) step for every loss, with penalties on leaf values and tree size in the split score itself | One formula for any twice-differentiable loss, and built-in regularization | XGBoost companion |
| Exact split search over sorted inputs | Histogram or quantile-sketch split search | Scales to billions of rows | XGBoost companion |
| Influence trimming of confident examples | Random row and column subsampling per tree | Faster, and decorrelates the trees as in a random forest | random forests companion |
| Choose M on a left-out test set | Early stopping on a validation set | Same idea: stop adding trees when held-out error stops falling | trees lesson |
Glossary
Every term with hover guidance on this page, in one place.