Boosting

ml basics

Boosting is covered in section 8.2.3 of An Introduction to Statistical Learning, and its classic example is AdaBoost. Boosted trees (the XGBoost and LightGBM family) are among the most common models in banking today for tabular data, so it’s worth understanding what they actually do.

Bagging vs boosting in one picture

Boosting

its mistakes

remaining mistakes

data

tree 1

tree 2

tree 3

weighted sum

Bagging and random forests

data

tree 1

tree 2

tree 3

average

Bagging / random forestBoosting
How trees are trainedindependently, in parallelone after another
What each tree seesa random resamplethe previous trees’ mistakes
Typical treesdeep (low bias, high variance)shallow (high bias, low variance)
What it reducesvariancemainly bias
Riskcan’t overfit by adding treescan overfit if you add too many

In the bias and variance terms: bagging takes complex, unstable models and steadies them. Boosting takes simple, stable models and makes them smarter.

AdaBoost: reweight the mistakes

AdaBoost (adaptive boosting, Freund and Schapire, 1997) was the first widely used boosting method.

  1. Give every training example the same weight.
  2. Train a weak model, often a stump: a tree with a single question.
  3. Measure its weighted error ε\risk{\varepsilon}: the total weight of the examples it got wrong.
  4. Give the model a vote α\key{\alpha}: the lower its error, the bigger its say.
  5. Increase the weight of the examples it got wrong, decrease the rest, and repeat from step 2.

The final prediction is a weighted vote of all the models.

The formulas

α, the model's voteε, its errorwrong examplesright examples

The vote of model tt:

αt=12ln⁡ ⁣(1−εtεt)\key{\alpha_t} = \frac{1}{2} \ln\!\left(\frac{1 - \risk{\varepsilon_t}}{\risk{\varepsilon_t}}\right)

If εt=0.5\risk{\varepsilon_t} = 0.5 (a coin flip), αt=0\key{\alpha_t} = 0: no say. The smaller the error, the larger αt\key{\alpha_t}.

The weight update for each example ii, depending on whether model tt got it wrong or right:

wi←wi×{eαtif wronge−αtif rightw_i \leftarrow w_i \times \begin{cases} \bad{e^{\key{\alpha_t}}} & \bad{\text{if wrong}} \\ \good{e^{-\key{\alpha_t}}} & \good{\text{if right}} \end{cases}

Then all the weights are divided by their total, so they add up to 1 again.

Murphy’s Probabilistic Machine Learning: An Introduction (chapter 18, on boosting) writes the same algorithm slightly differently. He calls the vote βm=12ln⁡((1−errm)/errm)\key{\beta_m} = \tfrac{1}{2}\ln\big((1 - \risk{\text{err}_m}) / \risk{\text{err}_m}\big), and only multiplies the wrong examples, by eαm\bad{e^{\key{\alpha_m}}} with αm=2βm\key{\alpha_m} = 2\key{\beta_m}, before normalising. That’s the same result: either way, a wrong example ends up e2βe^{2\key{\beta}} times heavier than a right one.

Worked example: one round, ten trades

Ten trades, each with weight 0.1. The first stump gets 3 of them wrong:

ε=3×0.1=0.3\begin{aligned} \risk{\varepsilon} &= 3 \times 0.1 \\ &= \risk{0.3} \end{aligned} α=12ln⁡ ⁣(0.70.3)=0.4236\begin{aligned} \key{\alpha} &= \tfrac{1}{2} \ln\!\left(\tfrac{0.7}{\risk{0.3}}\right) \\ &= \key{0.4236} \end{aligned}

wrong trades are multiplied by e0.4236=1.5275\bad{e^{\key{0.4236}}} = 1.5275, right ones by e−0.4236=0.6547\good{e^{-\key{0.4236}}} = 0.6547:

Before× factorUnnormalisedAfter normalising (÷ 0.9165)
Each wrong trade (3)0.11.52750.152750.1667
Each right trade (7)0.10.65470.065470.0714

After one round, the 3 wrong trades carry half the total weight (3 × 0.1667 = 0.5), and the 7 right ones the other half. That’s always true after an AdaBoost update: the next model is forced to treat the previous model’s mistakes as half of its whole problem.

A note on scikit-learn’s numbers

scikit-learn’s AdaBoostClassifier (version 1.9) uses the SAMME form of the algorithm. With two classes, the weight it reports is

αt=ln⁡ ⁣(1−εtεt)\key{\alpha_t} = \ln\!\left(\frac{1 - \risk{\varepsilon_t}}{\risk{\varepsilon_t}}\right)

without the 12\tfrac{1}{2}. That’s exactly Murphy’s αm=2βm\key{\alpha_m} = 2\key{\beta_m}. Every vote is twice as big, so the final ranking of predictions is the same; only the printed numbers differ. If you compare a textbook with ada.estimator_weights_, that’s why.

What AdaBoost did on the settlement data

I boosted stumps on the 15,000 made-up OTC trades (5.9% failed). The first three rounds:

RoundStump’s questionWeighted errorscikit-learn weight
1SSI mismatch?0.05892.7706
2SSI mismatch?0.33640.6794
3Hours booked late?0.38820.4548

Round 1’s error, 0.0589, equals the fail rate. That’s no accident: even trades with a mismatch settle more often than they fail (58% versus 42%), so the first stump says “settles” for everything and gets exactly the fails wrong. Then AdaBoost does its job: the fails now carry half the weight, so round 2’s stump on the same question starts flagging mismatched trades as fails. Round 3 moves on to late booking. Each round fixes a part of what the earlier ones missed.

A single stump scores an AUC of 0.669 on new trades. Boosting 25 of them reaches 0.811.

Gradient boosting: fit the leftover error

Gradient boosting (Friedman, 2001) generalises the idea. Instead of reweighting examples, each new tree is trained to predict what the current model still gets wrong.

For regression with squared error, that’s literally the residuals. An Introduction to Statistical Learning gives it as Algorithm 8.2. Start with f^(x)=0\hat{f}(x) = 0 and residuals ri=yir_i = y_i. Then, for each round b=1,2,…,Bb = 1, 2, \dots, B, fit a small tree f^b\hat{f}^b to the residuals and update:

f^(x)←f^(x)+λf^b(x)(8.10)\hat{f}(x) \leftarrow \hat{f}(x) + \lambda \hat{f}^b(x) \qquad \text{(8.10)} ri←ri−λf^b(xi)(8.11)r_i \leftarrow r_i - \lambda \hat{f}^b(x_i) \qquad \text{(8.11)}

The final model is the sum of all the shrunken trees:

f^(x)=∑b=1Bλf^b(x)(8.12)\hat{f}(x) = \sum_{b=1}^{B} \lambda \hat{f}^b(x) \qquad \text{(8.12)}
SymbolMeaning
f^(x)\hat{f}(x)the model so far
rir_ithe residuals: what’s still unexplained for example ii
f^b\hat{f}^bthe small tree added in round bb, trained to predict the residuals
λ\lambdathe shrinkage or learning rate, a small number like 0.1 (ISLP suggests 0.01 or 0.001)
BBthe number of trees

Plain meaning: start with a rough guess. Look at the errors. Train a small tree to predict the errors. Add a fraction of it to the guess. Repeat. For other losses, like the log loss used for classification, “residuals” become the gradient of the loss, which gives the method its name. The idea is the same.

The learning rate λ\lambda matters a lot. Each tree only corrects part of the error, so no single tree can push the model too far:

ModelAfter 10 treesAfter 50After 300Best
AdaBoost, stumps0.8030.8080.8100.811 at 25 trees
Gradient boosting, rate 0.10.8030.8140.8040.814 at 44 trees
Gradient boosting, rate 1.00.8060.7760.7340.806 at 9 trees
Random forest (last post)0.795

Test AUC against number of trees: AdaBoost rises and flattens; gradient boosting at rate 0.1 peaks around 44 trees then slowly declines; at rate 1.0 it peaks after 9 trees and falls steadily.

Two lessons from the chart:

  • Too high a learning rate overfits fast. At rate 1.0, each tree over-corrects. The score peaks after 9 trees, then falls to 0.734 as the model starts fitting noise.
  • Boosting can overfit by adding trees, unlike a random forest. Even at rate 0.1, the score peaks at 44 trees and then slowly declines. In practice you use early stopping: watch a validation score and stop adding trees when it stops improving.

On this data, well-tuned boosting edged out the random forest. That matches common experience on tabular data, but it isn’t a law: with fewer rows or noisier labels, forests often win because they’re harder to overfit.

The code

from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier
from sklearn.tree import DecisionTreeClassifier

ada = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1),   # stumps
                         n_estimators=300, learning_rate=1.0)
ada.fit(X_train, y_train)
ada.estimator_errors_[:3], ada.estimator_weights_[:3]           # epsilon and alpha per round

gb = GradientBoostingClassifier(n_estimators=300, learning_rate=0.1, max_depth=3,
                                validation_fraction=0.1, n_iter_no_change=20)   # early stopping
gb.fit(X_train, y_train)
gb.n_estimators_                                                # trees actually used

For large datasets, scikit-learn’s HistGradientBoostingClassifier, XGBoost and LightGBM are much faster versions of the same idea.

Summary

IdeaFormula / ruleRemember it as
Boostingweak models in sequence, each fixing the lasta team where each member covers the gaps
AdaBoost voteα=12ln⁡((1−ε)/ε)\key{\alpha} = \tfrac{1}{2}\ln\big((1-\risk{\varepsilon}) / \risk{\varepsilon}\big)lower error, bigger say
AdaBoost updatewrong × eα\bad{e^{\key{\alpha}}}, right × e−α\good{e^{-\key{\alpha}}}, normalisemistakes end up with half the weight
scikit-learn AdaBoostα=ln⁡((1−ε)/ε)\key{\alpha} = \ln\big((1-\risk{\varepsilon}) / \risk{\varepsilon}\big) (SAMME)twice the textbook value, same ranking
Gradient boostingf^←f^+λf^b\hat{f} \leftarrow \hat{f} + \lambda \hat{f}^b, f^b\hat{f}^b fits the residualskeep fitting what’s left over
Main dangertoo many trees or too high a rateuse a small rate and early stopping

That completes the ML basics series: what machine learning is, regression versus classification, linear regression, decision trees, bias and variance, bagging, random forests and boosting.

References