Machine Learning Algorithms

Ch.59: Gradient Boosting Regression, the Actual Math

By Ayush Arora••10 min read

Inspired by: YouTube

The previous post covered gradient boosting's regression procedure but deferred every "why": why the first model is the mean, why the target that gets passed forward is called a pseudo-residual instead of a plain residual, and why a fitted tree's leaf values line up so neatly with what the algorithm needs. All three turn out to be the same differentiation, applied three times to the same loss function. This post works through it, following Friedman's original gradient boosting algorithm, and checks every number against a real, tiny dataset along the way.

1. Additive Modeling: Why Add Small Functions at All

Machine learning, stripped down, is finding a function ff that maps inputs to an output: y=f(x)y = f(x). Sometimes that function is simple enough for one model, a straight line, a polynomial. But some relationships are genuinely composite, built out of several simpler functions added together. A toy example: f(x)=x+sin⁡(x)f(x) = x + \sin(x) is not something a single straight line or a single low-degree polynomial captures well, but it's exactly the sum of two functions that are individually easy to reason about.

Line chart showing three curves over x from -10 to 10: a dashed grey straight line y=x, a dashed orange oscillating curve y=sin(x), and a solid blue curve y=x+sin(x) that follows the straight line's overall upward trend while wobbling around it in a wave pattern matching sin(x).

The blue curve is genuinely hard to describe as "one simple shape", it's not a line, not a low-degree polynomial, but it's exactly the straight line plus the sine wave, two shapes that are each trivial on their own. That's the whole idea additive modeling borrows: instead of hunting for one function that matches the blue curve directly, build it by adding simple pieces one at a time.

Additive modeling is the strategy of not trying to find the whole composite function in one shot, and instead building it up piece by piece: fit a small function, see what's left unexplained, fit another small function to explain more of that, and so on. Every boosting algorithm, AdaBoost included, is a specific recipe for doing this. Gradient boosting's version, for regression, is what this post derives.

There's a cautionary reason to prefer "add several simple pieces" over "keep reaching for one increasingly complex function", and it has a name: Runge's phenomenon. If you try to fit a single polynomial through a set of evenly spaced points on a smooth curve, the intuitive expectation is that a higher-degree polynomial, with more terms to work with, should track the curve more closely. In practice it doesn't: past a certain degree, the polynomial starts oscillating wildly near the edges of the interval, and the fitting error near those edges actually gets worse as more terms are added, not better.

Line chart on x from -1 to 1 showing the true function 1/(1+25x²) as a black curve peaking at 1 near x=0 and flattening near the edges. A degree-5 polynomial fit (green) stays close everywhere but underfits the peak. A degree-9 fit (orange) tracks the peak better but wobbles noticeably near the edges. A degree-14 fit (red) fits the middle almost exactly but oscillates wildly near both edges, swinging from about -0.7 to above 1.5, far outside the true function's range.

The degree-14 fit matches the true curve almost exactly through the middle, yet near x=±1x = \pm 1 it swings from roughly −0.7-0.7 up past 1.51.5, wildly overshooting a function that never leaves [0,1][0, 1]. Adding more polynomial terms didn't buy a better fit everywhere, it bought a worse one at the edges. It's a concrete demonstration that "more complex" and "better fit" aren't the same thing for a single function, which is exactly the failure mode additive modeling sidesteps: instead of cranking up the complexity of one function until it breaks, keep every individual piece small and well-behaved, and let the sum of many simple pieces do the work a single complicated one can't do reliably.

2. What Goes In: Data and a Differentiable Loss

The algorithm needs two things: training data {(xi,yi)}i=1n\{(x_i, y_i)\}_{i=1}^n, and a loss function L(y,F(x))L(y, F(x)) that must be differentiable, since the entire derivation below leans on taking derivatives and setting them to zero. This post uses the simplest one, squared error, with a 12\frac{1}{2} tacked on purely for algebraic convenience (it cancels a factor of 2 that the chain rule would otherwise introduce, and multiplying every candidate model's loss by the same constant 12\frac{1}{2} never changes which model has the lowest loss):

L(y,F(x))=12(y−F(x))2L(y, F(x)) = \frac{1}{2}(y - F(x))^2

The worked example throughout is a tiny 3-row slice of a real startups profit dataset (R&D spend, admin spend, marketing spend as inputs, profit as output, values in thousands): profit is 192, 144, and 91 for the three rows.

3. Step 1: Why F0(x)F_0(x) Is the Mean

The algorithm's first step defines the starting model as:

F0(x)=arg⁡min⁡γ∑i=1nL(yi,γ)F_0(x) = \arg\min_\gamma \sum_{i=1}^n L(y_i, \gamma)

Read in words: find the single constant γ\gamma that minimizes the total loss across every training row, if that constant were the prediction for all of them. To find it, differentiate with respect to γ\gamma and set to zero:

ddγ[12∑i=1n(yi−γ)2]=∑i=1n−(yi−γ)=0  ⟹  ∑i=1nγ=∑i=1nyi  ⟹  γ=1n∑i=1nyi\frac{d}{d\gamma}\left[\frac{1}{2}\sum_{i=1}^n (y_i - \gamma)^2\right] = \sum_{i=1}^n -(y_i - \gamma) = 0 \implies \sum_{i=1}^n \gamma = \sum_{i=1}^n y_i \implies \gamma = \frac{1}{n}\sum_{i=1}^n y_i

That's the mean, derived, not assumed. Plugging in the three profit values:

F0(x)=192+144+913=4273≈142.33F_0(x) = \frac{192 + 144 + 91}{3} = \frac{427}{3} \approx 142.33

(worth flagging: if you round that to a flat 142 before carrying it forward, as is easy to do by hand, every downstream number below drifts slightly, leaf outputs end up about 0.3 off from what a decision tree would compute precisely. Carrying the full 142.33142.33 avoids that drift.)

This result is specific to squared-error loss. A different differentiable loss function would produce a different formula in place of "take the mean", the shape of the algorithm (differentiate, set to zero, solve for γ\gamma) stays the same regardless of which loss is plugged in.

4. Step 2a: The Pseudo-Residual Is a Gradient, Not Just a Subtraction

The next stage needs a target to fit a tree on. The algorithm's formula for it is:

ri,m=−[∂L(yi,F(xi))∂F(xi)]F=Fm−1r_{i,m} = -\left[\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right]_{F = F_{m-1}}

This looks intimidating mostly because of notation. Read it as: take the loss function, differentiate it with respect to the model's own output F(xi)F(x_i) (not with respect to yiy_i, which is fixed data), negate it, and evaluate that at the current model Fm−1F_{m-1}. For squared-error loss:

∂∂F(xi)[12(yi−F(xi))2]=−(yi−F(xi))\frac{\partial}{\partial F(x_i)}\left[\frac{1}{2}(y_i - F(x_i))^2\right] = -(y_i - F(x_i))

so the negative of that is:

ri,m=yi−Fm−1(xi)r_{i,m} = y_i - F_{m-1}(x_i)

Actual minus predicted, exactly the familiar residual, but now it's clear why it's called pseudo-residual: it isn't defined as "actual minus predicted" by assumption, it's defined as the negative gradient of the loss, and it only happens to simplify to that familiar subtraction because squared-error loss is the one being used. Swap in a different loss function (as later gradient boosting posts on classification will), and the pseudo-residual becomes some other expression, still the negative gradient, no longer literally y−y^y - \hat{y}.

For the first stage (m=1m=1), using the precise F0(x)=142.33F_0(x) = 142.33:

r1,1=192−142.33=49.67,r2,1=144−142.33=1.67,r3,1=91−142.33=−51.33r_{1,1} = 192 - 142.33 = 49.67, \quad r_{2,1} = 144 - 142.33 = 1.67, \quad r_{3,1} = 91 - 142.33 = -51.33

5. Step 2b: Fit a Tree to the Residuals

A regression tree is fit with the original inputs as XX and these pseudo-residuals as the target, not the original profit column. Whatever split the tree lands on divides the training rows into terminal regions Rj,mR_{j,m} (a fancy name for "which leaf a row falls into"). With three rows and a single-split, depth-1 tree, this produces two terminal regions, say R1,1R_{1,1} (the row with residual −51.33-51.33 alone) and R2,1R_{2,1} (the other two rows together).

6. Step 2c: Why a Leaf's Value Is Also a Mean

Every terminal region needs its own output value γj,m\gamma_{j,m}, computed by the same optimization pattern as Section 3, but now restricted to only the rows that fell into that particular region:

γj,m=arg⁡min⁡γ∑xi∈Rj,mL(yi,Fm−1(xi)+γ)\gamma_{j,m} = \arg\min_\gamma \sum_{x_i \in R_{j,m}} L\left(y_i, F_{m-1}(x_i) + \gamma\right)

For the single-row region R1,1R_{1,1} (the row with yi=91y_i = 91), differentiating with respect to γ\gamma and setting to zero, following exactly the same steps as Section 3, gives:

γ1,1=yi−F0(xi)=91−142.33=−51.33\gamma_{1,1} = y_i - F_0(x_i) = 91 - 142.33 = -51.33

That's precisely the pseudo-residual for that row, no surprise, since with one row in the region, "minimize the loss over the region" and "minimize the loss for that one row" are the same problem.

For the two-row region R2,1R_{2,1} (rows with yi=192y_i = 192 and yi=144y_i = 144), the same differentiation now sums over both rows:

∑i=12[yi−F0(xi)−γ]=0  ⟹  (192−142.33)+(144−142.33)=2γ  ⟹  γ2,1=49.67+1.672=25.67\sum_{i=1}^{2}\left[y_i - F_0(x_i) - \gamma\right] = 0 \implies (192 - 142.33) + (144 - 142.33) = 2\gamma \implies \gamma_{2,1} = \frac{49.67 + 1.67}{2} = 25.67

This is the mean of the residuals in that region. And that's exactly what a fitted decision tree already does at every leaf: average the target values of whatever rows land there. That's why, back in the previous post's code, nobody ever computes γj,m\gamma_{j,m} by hand, the tree already handed it over as the leaf's prediction. It isn't a coincidence that they match, it's a consequence of the loss function: squared-error loss's optimal constant is always a mean, whether you're solving for the whole dataset's baseline (Section 3) or one leaf's correction (this section). A different loss function would make the tree's raw leaf output and the algorithm's actual required γj,m\gamma_{j,m} diverge, at which point you would need to explicitly recompute the leaf values rather than trust the tree's own averaging.

7. Step 2d: Updating the Model

The last piece per stage is almost anticlimactic after the derivation above. Written precisely, using an indicator function I(x∈Rj,m)I(x \in R_{j,m}) that's 1 when xx falls in that region and 0 otherwise (so that only the matching leaf's γ\gamma ever gets added for a given row):

Fm(x)=Fm−1(x)+∑j=1Jmγj,m I(x∈Rj,m)F_m(x) = F_{m-1}(x) + \sum_{j=1}^{J_m} \gamma_{j,m} \, I(x \in R_{j,m})

In plain terms: whichever leaf a row lands in, add that leaf's γ\gamma to the running total, every other leaf contributes 0 for that row. Unrolled across stages, F1(x)=F0(x)+tree1(x)F_1(x) = F_0(x) + \text{tree}_1(x), F2(x)=F1(x)+tree2(x)F_2(x) = F_1(x) + \text{tree}_2(x), and so on, which is exactly the additive model from Section 1, now with every term concretely defined. After MM stages, the final output is simply F^(x)=FM(x)\hat{F}(x) = F_M(x), the fully unrolled sum of the mean plus every tree's contribution.

8. What to Take Away