Machine Learning Algorithms

Ch.64: Deriving XGBoost's Formulas From Scratch

By Ayush Arora••13 min read

Inspired by: YouTube

The regression and classification walkthroughs used two formulas without justifying either: the similarity score, (∑residuals)2/(n+λ)(\sum \text{residuals})^2 / (n + \lambda) for regression and (∑residuals)2/(∑p(1−p)+λ)(\sum \text{residuals})^2 / (\sum p(1-p) + \lambda) for classification, and the matching leaf output formula, the same numerator without the square. Both were presented as "this is what you compute," with a promise to come back to where they came from. This post is that derivation, starting from XGBoost's actual objective function and ending at the exact formulas used in both earlier posts.

1. The Objective Function, Built Up Stage by Stage

Gradient boosting's additive structure means a model after tt stages is a sum of functions, y^i=∑k=1tfk(xi)\hat y_i = \sum_{k=1}^{t} f_k(x_i), where each fkf_k is one fitted tree (or, for f1f_1, a constant baseline). Every one of those trees makes some amount of error, and the quantity you want to minimize at any stage combines that error with a penalty on model complexity:

L(t)=∑i=1nl(yi,y^i(t))⏟loss+∑k=1tΩ(fk)⏟regularization\mathcal{L}^{(t)} = \underbrace{\sum_{i=1}^{n} l(y_i, \hat y_i^{(t)})}_{\text{loss}} + \underbrace{\sum_{k=1}^{t} \Omega(f_k)}_{\text{regularization}}

The regularization term Ω(f)\Omega(f) only applies to decision trees, not to a constant baseline, since a flat number has no leaves to penalize. For the single-tree case this is:

Ω(f)=γT+12λ∑j=1Twj2\Omega(f) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2

TT is the number of leaves in that tree, wjw_j is leaf jj's output value (its "weight"), and γ\gamma, λ\lambda are two separate regularization knobs: γ\gamma penalizes having too many leaves, λ\lambda penalizes leaves with large output magnitudes. This is the piece the XGBoost overview referred to as "a regularization term baked directly into the loss."

At stage tt, every tree before the current one is already fixed, so y^i(t)=y^i(t−1)+ft(xi)\hat y_i^{(t)} = \hat y_i^{(t-1)} + f_t(x_i) splits the objective into "everything already accounted for" and "the one new tree":

L(t)=∑i=1nl ⁣(yi,y^i(t−1)+ft(xi))+Ω(ft)\mathcal{L}^{(t)} = \sum_{i=1}^{n} l\!\left(y_i, \hat y_i^{(t-1)} + f_t(x_i)\right) + \Omega(f_t)

The goal at every stage is the same: find the ftf_t (equivalently, the leaf weights of the tt-th tree) that minimizes this expression. That makes it an optimization problem, the same flavor as fitting mm and bb in linear regression by minimizing mean squared error, just with a far less convenient function to minimize.

2. Why This Can't Be Differentiated Directly

Ordinary optimization differentiates the objective with respect to the parameter being solved for, sets that derivative to zero, and solves. In linear regression, the loss as a function of mm and bb traces out a smooth parabola, differentiable everywhere, which is exactly what makes that route work.

ft(xi)f_t(x_i) here is a decision tree, and a decision tree's output is piecewise constant: the same output for every row that lands in a given leaf, then a sudden jump for rows in the next leaf over. Fitting the same two curves, a line and a shallow tree, to one dataset makes the shape difference concrete:

Two scatter plots of CGPA against package with a fitted prediction curve overlaid. The left plot shows a straight diagonal line from linear regression. The right plot shows a staircase of flat horizontal segments with sudden jumps between them, from a depth-2 decision tree fit on the same points.

A straight line is differentiable at every point. The staircase is not, at each jump the slope is undefined, there's no single tangent line to speak of. Since y^i(t)\hat y_i^{(t)} is built out of ft(xi)f_t(x_i), plotting the objective function against ft(xi)f_t(x_i) inherits that same jaggedness instead of a clean parabola. Partial differentiation with respect to ft(xi)f_t(x_i) breaks down at exactly the points where it would be needed.

3. Taylor Series: Smoothing a Function Into a Polynomial

The fix is to approximate the non-differentiable objective with something that is differentiable, close enough to be useful, close enough to be trustworthy. The Taylor series is the standard tool for exactly this: given a complicated function f(x)f(x) and a nearby point aa, it builds a polynomial approximation out of ff's derivatives at aa:

f(x)≈f(a)+f′(a)1!(x−a)+f′′(a)2!(x−a)2+f′′′(a)3!(x−a)3+⋯f(x) \approx f(a) + \frac{f'(a)}{1!}(x-a) + \frac{f''(a)}{2!}(x-a)^2 + \frac{f'''(a)}{3!}(x-a)^3 + \cdots

Each additional term tightens the approximation around aa. Expanding exe^x around a=0a=0 term by term:

ex≈1⏟f(a)+x⏟f′(a)1!(x−a)+x22⏟f′′(a)2!(x−a)2+x36⏟f′′′(a)3!(x−a)3e^x \approx \underbrace{1}_{f(a)} + \underbrace{x}_{\frac{f'(a)}{1!}(x-a)} + \underbrace{\frac{x^2}{2}}_{\frac{f''(a)}{2!}(x-a)^2} + \underbrace{\frac{x^3}{6}}_{\frac{f'''(a)}{3!}(x-a)^3} A line chart comparing e^x against its Taylor series approximations using 1, 2, and 3 terms, expanded around x=0. The true e^x curve is a solid black exponential. Each approximation curve hugs it more closely near x=0 and over a wider range as more terms are added, with the 3-term approximation visibly closest across the plotted range.

With one term the fit only holds very close to a=0a=0; with three terms it tracks the true curve over a visibly wider range. That's the general trade: more terms buys more accuracy, farther from aa. XGBoost's objective function stops at two terms, a second-order (quadratic) approximation, which turns out to be exactly enough.

4. Applying the Taylor Expansion to the Objective

Treat l(yi,y^i(t−1)+ft(xi))l(y_i, \hat y_i^{(t-1)} + f_t(x_i)) as the function being approximated, with x=y^i(t−1)+ft(xi)x = \hat y_i^{(t-1)} + f_t(x_i) and the expansion point a=y^i(t−1)a = \hat y_i^{(t-1)}, the prediction already locked in before this tree gets added. x−ax - a is then just ft(xi)f_t(x_i), so the second-order expansion is:

l ⁣(yi,y^i(t−1)+ft(xi))≈l ⁣(yi,y^i(t−1))+gift(xi)+12hift(xi)2l\!\left(y_i, \hat y_i^{(t-1)} + f_t(x_i)\right) \approx l\!\left(y_i, \hat y_i^{(t-1)}\right) + g_i f_t(x_i) + \frac{1}{2} h_i f_t(x_i)^2

where gig_i and hih_i are the first and second derivatives of the loss with respect to the previous prediction, evaluated at y^i(t−1)\hat y_i^{(t-1)}:

gi=∂l(yi,y^i(t−1))∂y^i(t−1),hi=∂2l(yi,y^i(t−1))∂(y^i(t−1))2g_i = \frac{\partial l(y_i, \hat y_i^{(t-1)})}{\partial \hat y_i^{(t-1)}}, \qquad h_i = \frac{\partial^2 l(y_i, \hat y_i^{(t-1)})}{\partial (\hat y_i^{(t-1)})^2}

gig_i is called the gradient, hih_i the Hessian (the standard name for a second derivative in optimization). Substituting this approximation into the objective:

L(t)≈∑i=1n[l ⁣(yi,y^i(t−1))+gift(xi)+12hift(xi)2]+Ω(ft)\mathcal{L}^{(t)} \approx \sum_{i=1}^{n} \left[ l\!\left(y_i, \hat y_i^{(t-1)}\right) + g_i f_t(x_i) + \frac{1}{2} h_i f_t(x_i)^2 \right] + \Omega(f_t)

The next move is to notice that l(yi,y^i(t−1))l(y_i, \hat y_i^{(t-1)}) doesn't involve ft(xi)f_t(x_i) at all, it's a fixed number by the time this tree is being fit. Since the entire point is to differentiate with respect to ft(xi)f_t(x_i) and that term vanishes under differentiation anyway, it can be dropped without changing the answer:

L(t)≈∑i=1n[gift(xi)+12hift(xi)2]+Ω(ft)\mathcal{L}^{(t)} \approx \sum_{i=1}^{n} \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t(x_i)^2 \right] + \Omega(f_t)

Checking this against the paper confirms it line for line: a gradient term, a Hessian term, and the regularization term, nothing else.

5. From Rows to Leaves

Ω(ft)\Omega(f_t) is still sitting unexpanded. Writing it out:

L(t)≈∑i=1n[gift(xi)+12hift(xi)2]+γT+12λ∑j=1Twj2\mathcal{L}^{(t)} \approx \sum_{i=1}^{n} \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t(x_i)^2 \right] + \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2

There are now two different summations, one over rows (i=1i = 1 to nn), one over leaves (j=1j = 1 to TT), and they need to become one. The bridge is that ft(xi)f_t(x_i), the tree's output for row ii, is just the output of whichever leaf row ii lands in. If IjI_j denotes the set of row indices that fall into leaf jj, then summing over all rows one at a time is equivalent to grouping rows by leaf first and summing within each group:

∑i=1n[⋯]=∑j=1T∑i∈Ij[⋯]\sum_{i=1}^{n} \big[\cdots\big] = \sum_{j=1}^{T} \sum_{i \in I_j} \big[\cdots\big]

Reusing the regression walkthrough's finished tree makes this concrete. That tree has three leaves: I1={CGPA≥8.25}I_1 = \{\text{CGPA} \geq 8.25\} (row with CGPA 9.0), I2={5.85≤CGPA<8.25}I_2 = \{5.85 \leq \text{CGPA} < 8.25\} (rows with CGPA 6.7 and 7.5), I3={CGPA<5.85}I_3 = \{\text{CGPA} < 5.85\} (row with CGPA 5.0). Summing gift(xi)g_i f_t(x_i) over all four rows directly, or summing it leaf by leaf and then adding the three leaf totals together, reach the identical number, the rows are just visited in a different order.

Within a single leaf jj, every row shares the same tree output, ft(xi)=wjf_t(x_i) = w_j for every i∈Iji \in I_j, by definition of what a leaf is. That lets wjw_j be pulled out of the inner sum entirely:

L(t)≈∑j=1T[(∑i∈Ijgi)wj+12(∑i∈Ijhi+λ)wj2]+γT\mathcal{L}^{(t)} \approx \sum_{j=1}^{T} \left[ \left(\sum_{i \in I_j} g_i\right) w_j + \frac{1}{2}\left(\sum_{i \in I_j} h_i + \lambda\right) w_j^2 \right] + \gamma T

This used the fact that the λ∑wj2/2\lambda \sum w_j^2 / 2 term already sums over jj, so its jj-th piece, 12λwj2\frac{1}{2}\lambda w_j^2, folds directly into the jj-th leaf's bracket. Writing Gj=∑i∈IjgiG_j = \sum_{i \in I_j} g_i and Hj=∑i∈IjhiH_j = \sum_{i \in I_j} h_i for brevity:

L(t)≈∑j=1T[Gjwj+12(Hj+λ)wj2]+γT\mathcal{L}^{(t)} \approx \sum_{j=1}^{T} \left[ G_j w_j + \frac{1}{2}(H_j + \lambda) w_j^2 \right] + \gamma T

Every term here is a function of a single leaf's weight wjw_j, nothing couples one leaf's weight to another's. That's what makes the next step possible.

6. Solving for the Optimal Leaf Weight

Each leaf's contribution, Gjwj+12(Hj+λ)wj2G_j w_j + \frac{1}{2}(H_j + \lambda) w_j^2, is a plain quadratic in wjw_j, exactly the shape a parabola takes, so it can be minimized the ordinary way: differentiate with respect to wjw_j, set to zero, solve.

∂∂wj[Gjwj+12(Hj+λ)wj2]=Gj+(Hj+λ)wj=0\frac{\partial}{\partial w_j}\left[G_j w_j + \frac{1}{2}(H_j+\lambda)w_j^2\right] = G_j + (H_j + \lambda)w_j = 0 wj∗=−GjHj+λ\boxed{w_j^* = -\frac{G_j}{H_j + \lambda}}

This is the general leaf weight formula, valid for any differentiable loss function, with the specific loss only entering through GjG_j and HjH_j. To check it reproduces the regression and classification formulas used earlier, it just needs gig_i and hih_i for each loss:

Regression, squared error l(y,y^)=12(y−y^)2l(y, \hat y) = \frac{1}{2}(y-\hat y)^2:

gi=∂l∂y^=y^i(t−1)−yi=−residuali,hi=∂2l∂y^2=1g_i = \frac{\partial l}{\partial \hat y} = \hat y_i^{(t-1)} - y_i = -\text{residual}_i, \qquad h_i = \frac{\partial^2 l}{\partial \hat y^2} = 1

Classification, log loss with y^\hat y in log-odds space and pi=sigmoid(y^i(t−1))p_i = \text{sigmoid}(\hat y_i^{(t-1)}) (the same conversion used in gradient boosting for classification):

gi=pi−yi=−residuali,hi=pi(1−pi)g_i = p_i - y_i = -\text{residual}_i, \qquad h_i = p_i(1 - p_i)

Both of these are checkable with symbolic differentiation rather than by hand:

import sympy as sp
 
y, yhat, z = sp.symbols('y yhat z')
 
# regression: L = 1/2 (y - yhat)^2
L_reg = sp.Rational(1, 2) * (y - yhat)**2
print("g_reg:", sp.diff(L_reg, yhat))        # yhat - y
print("h_reg:", sp.diff(L_reg, yhat, 2))     # 1
 
# classification: L = log loss, z is log-odds, p = sigmoid(z)
p = 1 / (1 + sp.exp(-z))
L_cls = -(y * sp.log(p) + (1 - y) * sp.log(1 - p))
g_cls = sp.simplify(sp.diff(L_cls, z))
h_cls = sp.simplify(sp.diff(L_cls, z, 2))
print("g_cls:", g_cls)
print("h_cls:", h_cls)
g_reg: -y + yhat
h_reg: 1
g_cls: (-y + (1 - y)*exp(z))/(exp(z) + 1)
h_cls: exp(z)/(exp(2*z) + 2*exp(z) + 1)

h_cls simplifies to p(1−p)p(1-p) (since ez/(1+ez)2=p(1−p)e^z/(1+e^z)^2 = p(1-p) once p=ez/(1+ez)p = e^z/(1+e^z)), and g_cls simplifies to p−yp - y by the same substitution, confirming both by hand matches what sympy returns.

Plugging these into wj∗=−Gj/(Hj+λ)w_j^* = -G_j/(H_j+\lambda):

wj∗,reg=−∑i∈Ij(y^i(t−1)−yi)∑i∈Ij1+λ=∑i∈Ijresidualinj+λw_j^{*,\text{reg}} = -\frac{\sum_{i \in I_j}(\hat y_i^{(t-1)} - y_i)}{\sum_{i \in I_j} 1 + \lambda} = \frac{\sum_{i \in I_j} \text{residual}_i}{n_j + \lambda} wj∗,cls=−∑i∈Ij(pi−yi)∑i∈Ijpi(1−pi)+λ=∑i∈Ijresiduali∑i∈Ijpi(1−pi)+λw_j^{*,\text{cls}} = -\frac{\sum_{i \in I_j}(p_i - y_i)}{\sum_{i \in I_j} p_i(1-p_i) + \lambda} = \frac{\sum_{i \in I_j} \text{residual}_i}{\sum_{i \in I_j} p_i(1-p_i) + \lambda}

Both are exactly the leaf output formulas used without proof in the regression and classification posts. The "sum of residuals" in both earlier posts was always −Gj-G_j, and the "nn" or "∑p(1−p)\sum p(1-p)" in the denominator was always HjH_j.

7. The Minimum Loss, and Where the Similarity Score Comes From

wj∗w_j^* is the weight that minimizes each leaf's contribution to the objective, but it doesn't yet say what that minimum value is. Substituting wj∗w_j^* back into Gjwj+12(Hj+λ)wj2G_j w_j + \frac{1}{2}(H_j+\lambda)w_j^2:

Gj(−GjHj+λ)+12(Hj+λ)(GjHj+λ)2=−Gj2Hj+λ+12Gj2Hj+λ=−12Gj2Hj+λG_j \left(-\frac{G_j}{H_j+\lambda}\right) + \frac{1}{2}(H_j+\lambda)\left(\frac{G_j}{H_j+\lambda}\right)^2 = -\frac{G_j^2}{H_j+\lambda} + \frac{1}{2}\frac{G_j^2}{H_j+\lambda} = -\frac{1}{2}\frac{G_j^2}{H_j+\lambda}

Summed across all TT leaves, the minimum achievable objective for a given tree structure is:

L∗=−12∑j=1TGj2Hj+λ+γT\mathcal{L}^{*} = -\frac{1}{2}\sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T

The paper calls this a structure score, a single number that says how good a candidate tree shape is, lower is better since it's a loss. Flip the sign and drop the constant −12-\frac{1}{2} and γT\gamma T (neither affects which of several candidate splits scores highest, since a positive constant multiplier and an additive term that's identical across candidates can't change which one wins), and what's left is:

Similarity=∑j=1TGj2Hj+λ\text{Similarity} = \sum_{j=1}^{T} \frac{G_j^2}{H_j+\lambda}

For a single leaf (T=1), this is (∑gi)2/(∑hi+λ)(\sum g_i)^2 / (\sum h_i + \lambda). Substituting gi=−residualig_i = -\text{residual}_i for regression gives (∑residuali)2/(n+λ)(\sum \text{residual}_i)^2/(n+\lambda), exactly the regression similarity score. Substituting the classification gi,hig_i, h_i gives (∑residuali)2/(∑p(1−p)+λ)(\sum \text{residual}_i)^2/(\sum p(1-p)+\lambda), exactly the classification version. Both formulas trace back to the same two-line substitution, the only thing that ever changes between regression and classification is what loss function gig_i and hih_i come from.

8. From Similarity Score to Split Gain

A split takes one leaf (the parent) and replaces it with two (left and right child). Using L∗\mathcal{L}^* from Section 7, the loss before splitting (one leaf, T=1T=1) is:

Lparent∗=−12GP2HP+λ+γ\mathcal{L}^*_{\text{parent}} = -\frac{1}{2}\frac{G_P^2}{H_P+\lambda} + \gamma

and the loss after splitting (two leaves, T=2T=2) is:

Lchildren∗=−12[GL2HL+λ+GR2HR+λ]+2γ\mathcal{L}^*_{\text{children}} = -\frac{1}{2}\left[\frac{G_L^2}{H_L+\lambda} + \frac{G_R^2}{H_R+\lambda}\right] + 2\gamma

A split is worth making exactly when it reduces the loss, so the quantity that matters is Lparent∗−Lchildren∗\mathcal{L}^*_{\text{parent}} - \mathcal{L}^*_{\text{children}}:

Gain=12[GL2HL+λ+GR2HR+λ−GP2HP+λ]−γ\text{Gain} = \frac{1}{2}\left[\frac{G_L^2}{H_L+\lambda} + \frac{G_R^2}{H_R+\lambda} - \frac{G_P^2}{H_P+\lambda}\right] - \gamma

This is exactly the split criterion the regression and classification posts used, SSL+SSR−Similarityparent\text{SSL} + \text{SSR} - \text{Similarity}_{\text{parent}}, with two differences made explicit here: an overall factor of 12\frac{1}{2}, which doesn't change which candidate split wins since every candidate gets scaled the same way, and a −γ-\gamma term, set to zero in both earlier posts' worked examples. That −γ-\gamma is not cosmetic in general: it's the same γ\gamma hyperparameter that sets a minimum required gain before a split is allowed to happen at all. A candidate split whose raw 12[SSL+SSR−SSP]\frac{1}{2}[\text{SSL}+\text{SSR}-\text{SS}_P] doesn't clear γ\gamma gets rejected outright, regardless of how it compares to other candidates, which is the exact mechanism behind XGBoost's pruning behavior.

9. What to Take Away