rumblr Work in progressWIP

● The AI Primer · Lesson 23 · Part 1: how the model works inside

Trees and boosting

the other workhorse

This lesson covers Decision trees, random forests, gradient boosting, and when they still win

Members · open during launch 51 min14 figures and diagrams
How it works builds the idea from scratch. Math & code adds the formulas and the Python.

At a glance

Key takeaways

  1. A decision tree asks yes/no questions about one column at a time, choosing each question greedily to leave the purest piles (lowest weighted Gini impurity or entropy), until a stopping rule says stop.
  2. Deep trees overfit: a question for every example gives 100% on training data and poor results on new data. Depth is the capacity knob.
  3. Random forests average many deep trees, each grown on a bootstrap sample with random feature subsets. Averaging removes the variance the trees don't share; decorrelating them lowers the part they do.
  4. Gradient boosting adds small trees one at a time, each fit to the residuals (the negative gradient of the loss), shrunk by a learning rate, with the number of rounds set by early stopping.
  5. On tables, trees usually win: no scaling, native thresholds, modest data. Neural networks win on images, audio, text and huge datasets.

Level 2

How it works, from scratch

Most of this primer is about neural networks, because language models are neural networks. But open the models that decide whether a loan is approved, whether a card payment looks like fraud, which ad to show, or how many umbrellas a shop should stock, and very often you find no neural network at all. You find hundreds of small decision trees, added together.

Those problems share a shape: a table. Each row is a customer, a payment or a day; each column is a fact measured in its own units (age in years, income in dollars, number of late payments, a region code). On tables of modest size, ensembles of trees have stayed the model to beat, and they train in seconds on a laptop. This lesson builds them from scratch, so you can see why they work and, just as usefully, recognise when a neural network is the wrong tool.

The plan: one tree (how it chooses its questions), why a single tree overfits, how a random forest fixes that by averaging many trees, how gradient boosting fixes it differently by adding trees one after another, and finally a head-to-head against a small neural network from primer.ml.neural_net on a loan table.

Chapter 1

A decision tree is a game of twenty questions

Everyday picture A nurse at an emergency desk runs a flowchart: "Is the patient breathing normally? No: resuscitation room. Yes: is there chest pain? Yes: see a doctor now. No: take a seat." Each question looks at one fact and sends the patient left or right, and the last box gives the answer. A decision tree is exactly that flowchart, except the questions are chosen by the computer from past examples.

Tiny example Eight emails, each described by two facts: how many links it contains, and whether it comes from a sender you have written to before.

email links known sender spam?
1 0 yes no
2 1 yes no
3 4 yes no
4 0 no no
5 3 no yes
6 5 no yes
7 6 no yes
8 1 no yes

The tree this lesson grows from those eight rows:

Figure 1 · Diagram

Reading it: start at the top diamond with a new email and answer each question until you land in a box. Diamonds are questions (the tree's internal nodes); boxes are answers (its leaves), and each leaf lists the training emails that ended up there. Two questions sort all eight correctly. Notice what the tree did not ask: "more than 2 links?" looks reasonable, but it was worse. The next two sections show how the tree decides that.

In code: spam_emails returns the table above; DecisionTree grows the tree, and DecisionTree.rules prints it as nested if/else lines.

Chapter 2

Measuring a mess: Gini impurity

Everyday picture You are sorting laundry into piles. A pile of only socks is pure: pull out any two items and they match. A pile that is half socks, half shirts is as mixed-up as two kinds of item can be. A good sorting question is one that leaves piles as pure as possible.

Tiny example Before any question, the eight emails are 4 spam and 4 not: a 50/50 pile. Pull out two emails at random (putting the first one back) and they disagree half the time. The pile of unknown senders is 1 "not spam" and 4 spam: purer, so two random picks disagree less often.

Gini impurity is exactly that chance of disagreement:

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the Gini impurity of one pile of examples: 0 when pure, larger when mixed 0.5 for the eight emails
how many different labels there are 2 (spam, not spam)
a counter that walks over the labels 1 = not spam, 2 = spam
the share of the pile carrying label ,
the chance two random picks both have label
"add up over every label": the chance two picks match
turns "chance they match" into "chance they disagree"

In words: "the impurity of a pile is one minus the sum of the squared shares of each label: the chance that two examples drawn at random carry different labels."

With the numbers: all eight emails: $1 - (0.5^2 + 0.5^2) = 1 - 0.5 = 0.5$. The unknown-sender pile, 1 of 5 not spam and 4 of 5 spam: . A pure pile: .

Level 3: in Python
# four spam (1) and four not spam (0)
labels = [1, 1, 1, 1, 0, 0, 0, 0]
# p_k: the share of each label
p = [labels.count(k) / len(labels) for k in (0, 1)]
p  # → [0.5, 0.5]
# G = 1 - Σ p_k²
1 - sum(p_k ** 2 for p_k in p)  # → 0.5
# the unknown-sender pile: 1 not spam, 4 spam
p = [1 / 5, 4 / 5]
round(1 - sum(p_k ** 2 for p_k in p), 2)  # → 0.32

Why it matters the tree needs a single number to compare questions with, and it must be cheap, because a real tree scores millions of candidate questions. Gini needs only counts, one multiply per label, and no logarithm.

In code: gini computes for any list of labels.

Entropy: the same idea, counted in yes/no questions

Everyday picture Entropy asks: if I had to guess an email's label by asking yes/no questions, how many would I need on average? A 50/50 pile needs one full question. A pure pile needs none, because you already know.

Tiny example For the eight emails, one question ("is it spam?") settles it: entropy 1 bit. For the unknown-sender pile, 4 of 5 are spam, so you are usually right before asking and the average cost is about 0.72 of a question.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the entropy of one pile, in bits (yes/no questions) 1 for the eight emails
the share of the pile carrying label , as above 0.2 and 0.8
the logarithm base 2: the power you raise 2 to in order to get . It is negative for shares below 1 ,
add up over the labels, then flip the sign so the total is positive

In words: "entropy is the average, over the labels, of how surprising each label is, weighted by how often it appears." A label with share 0.5 costs bit of surprise.

With the numbers: eight emails: bit. Unknown-sender pile: $-(0.2 \cdot \log_2 0.2 + 0.8 \cdot \log_2 0.8) = -(0.2 \cdot (-2.32) + 0.8 \cdot (-0.32)) = 0.722$ bits.

Level 3: in Python
import math
# H = -Σ p_k log2 p_k
p = [0.5, 0.5]
-sum(p_k * math.log2(p_k) for p_k in p)  # → 1.0
p = [1 / 5, 4 / 5]
round(-sum(p_k * math.log2(p_k) for p_k in p), 3)  # → 0.722

Gini and entropy almost always pick the same question; Gini is a little cheaper. The drop in entropy a question produces is called information gain, the name the early ID3 algorithm used. See primer.notation for logarithms from scratch and primer.ml.losses for entropy's close cousin, cross-entropy.

In code: entropy computes in bits; DecisionTree takes criterion="entropy" to split by it instead of Gini.

Chapter 3

Choosing the best question

Everyday picture A question splits one pile into two. Judge it by how messy the two new piles are, but weigh each by its size: a tiny pure pile of one email is worth less than a big pure pile of five.

Tiny example "Known sender?" makes a pile of 3 (all not spam, Gini 0) and a pile of 5 (1 not spam, 4 spam, Gini 0.32). "More than 2 links?" makes a pile of 4 (3 not spam, 1 spam) and another pile of 4 (1 not spam, 3 spam), Gini 0.375 each.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here For "known sender?"
, the left pile (answer yes) and the right pile (answer no) known / unknown senders
, how many examples land in each pile 3 and 5
how many examples reached this node, 8
, the Gini impurity of each pile (the formula above) 0 and 0.32
the left pile's share of the examples: its weight 3/8
the impurity left after asking: lower is better 0.2

In words: "the mess left by a question is the average of the two piles' impurities, each weighted by the share of examples in it."

With the numbers: known sender: $\frac{3}{8} \cdot 0 + \frac{5}{8} \cdot 0.32 = 0.2\frac{4}{8} \cdot 0.375 + \frac{4}{8} \cdot 0.375 = 0.375$. The tree asks "known sender?" because 0.2 < 0.375. The impurity decrease (the gain) is .

Level 3: in Python
# known senders: 3 emails, all not spam
n_left, g_left = 3, 0.0
# unknown senders: 1 not spam, 4 spam
n_right, g_right = 5, 0.32
n = n_left + n_right
# G_split = (n_L/n)·G(L) + (n_R/n)·G(R)
round(n_left / n * g_left + n_right / n * g_right, 3)  # → 0.2
# the impurity the question removed
round(0.5 - 0.2, 3)  # → 0.3

Where do candidate questions come from? For a number such as "links", only thresholds between neighbouring values in the data can change which emails go where. The links column holds 0, 1, 3, 4, 5 and 6, so there are five thresholds worth trying (halfway points 0.5, 2, 3.5, 4.5, 5.5), plus one for the yes/no column. The tree scores all six and keeps the lowest.

Figure 2 · Chart

0.0 0.1 0.2 0.3 0.4 0.5 0.6 weighted Gini impurity after the split (lower is better) links > 0.5 links > 2 links > 3.5 links > 4.5 links > 5.5 known_sender > 0.5 Six questions the tree could ask first before any split: 0.5 0.333 0.375 0.467 0.333 0.429 0.200

Weighted Gini after each of six candidate first questions: known_sender scores 0.200, the best; the five links thresholds score between 0.333 and 0.467; all are below the 0.5 of the unsplit pile

Reading it: each bar is one question the tree could ask first, and its length is the weighted Gini impurity left afterwards. The dashed line is the impurity before asking anything (0.5). Every question helps a little, since every bar ends left of the line, but "known_sender > 0.5" (blue) leaves the least mess by a clear margin, so it becomes the root of the tree. Two links thresholds tie at 0.333: the greedy search would take the first if known sender did not exist.

In code: split_impurity is ; candidate_splits scores every question in the figure; best_split does the same search fast, by sorting each feature once and keeping running label counts, and returns a Split.

Why it matters this greedy search is the whole learning algorithm. There is no gradient and no learning rate: the tree tries every feature and every threshold and takes the best. That is why trees handle columns in any units, as the last section shows.

Chapter 4

Growing the tree: ask, split, repeat

Everyday picture Sorting a big box of photos: first by year, then each year's pile by who is in them, then each of those by place, until every pile is tidy or too small to bother with.

Tiny example After "known sender?", the left pile (emails 1, 2, 3) is already pure: it becomes a leaf. The right pile (4, 5, 6, 7, 8) is not, so the tree searches again inside that pile only. There, "more than 0 links?" separates email 4 from the four spam emails perfectly. Both new piles are pure, so growth stops at depth 2.

Figure 3 · Diagram

Reading it: the same recipe runs at every node, which is why the code is a function that calls itself (recursion). The dotted arrows are those calls: each child is a fresh node holding only its share of the examples. The diamond is where growth stops; everything else is the search from the previous section. To predict, a new example simply follows the questions from the root down to a leaf.

Each question looks at a single feature and compares it with a threshold, so every question cuts the input space with a straight line parallel to an axis. A tree therefore carves the plane into rectangles:

Figure 4 · Chart

depth 1: 2 leaves class 0 class 1 depth 3: 8 leaves depth unlimited: 51 leaves

A tree on two-moon data at depth 1, 3 and unlimited: one horizontal cut, then a few rectangles, then a patchwork of 51 boxes with thin strips around individual noisy points

Reading it: the dots are 300 training points of two interleaved half-moons with noise (primer.ml.neural_net.make_moons); the shaded background is what the tree would predict at each spot. At depth 1 there is one question, so one straight horizontal cut. At depth 3 there are 8 leaves, and the boundary is a staircase of vertical and horizontal edges that roughly follows the moons. With no limit, the tree keeps cutting until every leaf is pure: 51 boxes, including thin strips that reach into the other class's territory to capture a few noisy points. Every edge is axis-aligned: trees cannot draw a diagonal, only approximate one with steps.

A regression tree, predicting a number instead of a label, is the same recipe with two swaps: the impurity is the variance of the numbers in a pile (how spread out they are), and a leaf predicts their average. Gradient boosting, below, is built from those.

In code: DecisionTree grows by the recipe in the diagram (a depth limit and a minimum leaf size are its stopping rules, and the "squared" criterion makes it a regression tree), and DecisionTree.predict routes each row down to its leaf.

Chapter 5

Overfitting: a question for every example

Everyday picture Ask a friend to write rules for which of their emails are spam, and forbid them from stopping until every past email is sorted correctly. They will end up with rules like "an email from Pat at 9:14 on a Tuesday with 3 links is spam": perfect on the past, useless for the future.

Tiny example On the noisy moons, a tree with no depth limit grows 51 leaves and gets 100% of its 300 training points right. On 200 fresh points it gets 82%. A tree stopped at depth 2 has 4 leaves, gets 88% on training and 89.5% on the fresh points: it learned the shape rather than the noise.

Figure 5 · Chart

1 2 3 4 5 6 8 10 12 none depth limit 0.75 0.80 0.85 0.90 0.95 1.00 accuracy Deeper trees memorise: training climbs, validation falls training validation

Training and validation accuracy against the depth limit: training rises from 78% to 100%; validation peaks at 89.5% for depth 2 and 3 and falls to 82% with no limit

Reading it: the horizontal axis is the depth limit, from one question to no limit at all. The blue training curve only ever rises, because every extra level can only fit the training points better. The orange validation curve peaks early, at depth 2 to 3, then slides down as the tree starts building boxes around individual noisy points. The widening gap between the two curves is overfitting, exactly as in primer.ml.regularization.

A single tree is the textbook high-variance model: change a few training points and the top question can flip, and everything below it changes with it. There are two ways out. Keep trees small (limit the depth, require a minimum number of examples per leaf, or grow fully then prune back the branches that do not help on validation data). Or keep trees big and combine many of them, which is what the rest of this lesson does.

In code: depth_sweep grows one tree per depth limit on moons_split data and measures both accuracies.

Why it matters depth is the tree's capacity knob. Every tree library exposes it (along with a minimum leaf size), and choosing it on validation data is the first thing to tune.

Chapter 6

Random forests: many noisy trees, one steady vote

Everyday picture At a county fair, hundreds of people guess the weight of an ox. Individual guesses are wildly off in both directions, but the average of all of them lands remarkably close, because the errors point in different directions and cancel. A random forest does this with trees: grow many deep trees that each make different mistakes, then average them. The trick is making the mistakes different, since averaging copies of the same tree changes nothing.

Two sources of difference, both from Leo Breiman:

  1. Bagging (bootstrap aggregating): each tree trains on its own bootstrap sample, a resample of the training rows drawn with replacement.
  2. Random feature subsets: at every node, a tree may only consider a random handful of the features (commonly the square root of their number), so different trees are forced to find different questions.

Figure 6 · Diagram

Reading it: the table fans out into B different resamples, each resample grows its own full-depth tree, and the trees' answers are averaged at the end. Nothing flows between trees, so they can be grown in parallel on separate machines. Diversity enters twice: once in which rows each tree sees (the bootstrap boxes) and once in which features each node may use (inside the tree boxes).

Bagging: drawing names from a hat, and putting them back

Tiny example Put the eight email numbers in a hat. Draw one, write it down, put it back, and repeat eight times. One draw might give 3, 1, 3, 7, 5, 1, 8, 2: emails 1 and 3 twice, and emails 4 and 6 not at all. Each tree sees a slightly different dataset, with some rows counted double and about a third missing.

How big is "about a third"? A particular email is missed on one draw with chance , and missed on all draws with:

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the number of rows, and also the number of draws 8
the chance one draw picks this particular row 1/8
the chance one draw misses it 7/8
missing it on every one of the independent draws: multiply the chances
"gets closer and closer to, as grows"
Euler's number, ≈ 2.718 (see primer.notation)

In words: "the chance a row never makes it into a bootstrap sample is the chance of missing it once, multiplied by itself once per draw; for big datasets that settles at about 37%."

With the numbers: for the eight emails; for 100,000 rows, , already equal to to four places.

Level 3: in Python
import math
n = 8
# (1 - 1/n)^n: missed on every one of the n draws
round((1 - 1 / n) ** n, 4)  # → 0.3436
round((1 - 1 / 100_000) ** 100_000, 4)  # → 0.3679
round(1 / math.e, 4)  # → 0.3679

The rows a tree never saw are its out-of-bag rows: a free validation set for that tree, which random forests use to estimate their own accuracy without holding data back.

In code: bootstrap_sample draws the n row indices; out_of_bag_fraction is the formula; RandomForest gives every tree its own sample and its own feature subsets.

Why averaging helps, and what limits it

Tiny example Three trees give a point spam probabilities of 0.9, 0.2 and 0.7. The forest says 0.6. If each tree's error is a coin flip that cancels out on average, three trees already wobble less than one; a hundred wobble far less. But if every tree makes the same mistake, averaging keeps it.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the number of trees 10
tree number 's prediction for one input 0.9, 0.2, 0.7, …
the forest's prediction: the average of all trees 0.6
variance: how much a prediction would swing if the training data were redrawn
the variance of one tree on its own 1 (a unit, for comparison)
the correlation between two trees' errors: 0 if they err independently, 1 if they always err together (Greek letter rho) 0.3

In words: "the forest's wobble has two parts: a part shared by all trees, which no amount of averaging removes, and a private part, which shrinks as you add trees."

With the numbers: with and : ten trees give ; a thousand trees give . Going from 10 to 1,000 trees barely helps; lowering would. With fully independent trees (), ten trees would cut the variance to 0.1.

Level 3: in Python
sigma2, rho = 1.0, 0.3
# ρσ² + (1 - ρ)σ²/B
round(rho * sigma2 + (1 - rho) * sigma2 / 10, 4)  # → 0.37
round(rho * sigma2 + (1 - rho) * sigma2 / 1000, 4)  # → 0.3007
# independent trees: the variance falls as 1/B
round(0.0 * sigma2 + (1 - 0.0) * sigma2 / 10, 4)  # → 0.1

That is why a forest adds random feature subsets on top of bagging: bagging alone leaves every tree free to pick the same strong feature at the top, so the trees stay correlated. Forcing each node to choose from a random subset pushes down, which is the only way to push the floor down. Each tree gets a little worse; the average gets better.

Figure 7 · Chart

one deep tree: 82.0% on validation forest of 100: 88.5% on validation

Left, one fully grown tree carves a jagged patchwork with small islands around noisy points and scores 82% on validation; right, a forest of 100 trees draws a smoother boundary and scores 88.5%

Reading it: both panels show the same 300 training points and the predicted class at every spot. On the left, a single unlimited tree fences off little islands around individual noisy points. On the right, a hundred unlimited trees vote: an island appears only where most trees agree, so the one-off islands mostly vanish and the boundary follows the two moons more smoothly. Each tree in the forest is just as overfitted as the one on the left; their average is not.

Figure 8 · Chart

1 0 0 1 0 1 1 0 2 number of trees (log scale) 0.82 0.83 0.84 0.85 0.86 0.87 0.88 0.89 validation accuracy Adding trees helps, then levels off random forest one deep tree, all the data

Validation accuracy against the number of trees on a log axis: one tree scores 85.5%, two dip to 83%, then the forest climbs to 88 to 89% by 10 to 20 trees and stays level, all above a single deep tree's 82%

Reading it: the horizontal axis is how many trees are averaged, on a log scale; the vertical axis is accuracy on the 200 validation points. The dashed line is a single deep tree grown on all the data. The first points are jumpy (two fully grown trees that disagree give a 50/50 tie, which is why two trees dip below one), but the curve climbs over the first ten or twenty trees and then flattens: the private part of the variance is gone, and what remains is the shared part the formula predicts. Unlike depth, adding trees does not overfit; past the plateau it only costs time.

In code: averaged_variance is the formula; forest_curve grows one forest and scores its first k trees for each k; RandomForest.predict_proba averages the trees' class shares.

Why it matters a random forest is the most forgiving model in machine learning. Its defaults (a few hundred unpruned trees, square root of the features per node) work well on most tables with no tuning at all, which is why it is the standard first model on new tabular data.

Chapter 7

Gradient boosting: each tree fixes the last one's mistakes

Everyday picture Golf. Your first shot from the tee gets you near the green. Your second shot does not start over from the tee: it aims at what is left, the gap between the ball and the hole. Each shot is small and corrects what remains. Gradient boosting builds a model the same way: start with a crude guess, then add small trees one at a time, each trained only on what the model so far still gets wrong.

Where a forest grows big trees side by side and averages them, boosting grows small trees in sequence and adds them up. The forest fights variance; boosting fights bias (being systematically off), a little at a time.

Figure 9 · Diagram

Reading it: the loop in the middle is the whole algorithm. Each round measures what is still wrong (the residuals), trains a small tree to predict those, and adds a shrunken copy of that tree to the model. The model is never retrained from scratch; it only grows by addition. The final model is the starting guess plus every tree's contribution, which is why a boosted model is literally a sum of trees.

Tiny example Four houses of size 1, 2, 3 and 4 sold for 1, 2, 6 and 7 (hundreds of thousands, say). Boost with stumps (trees with a single question) and learning rate :

size price residual stump new residual
1 1 4 −3 −2.5 2.75 −1.75
2 2 4 −2 −2.5 2.75 −0.75
3 6 4 2 2.5 5.25 0.75
4 7 4 3 2.5 5.25 1.75

is the average price, the best single guess. The stump's best question on the residuals is "size ≤ 2.5?", and each leaf predicts its average residual: −2.5 for the small houses, +2.5 for the big ones. Adding half of that moves every guess toward its price. The mean squared error falls from 6.5 to 1.8125 in one round, and a second round brings it to 0.64.

Residuals are the negative gradient

Why is this called gradient boosting? Because "fit what is still wrong" is a special case of gradient descent (primer.ml.optimizers), taken in the space of predictions instead of weights.

Level 3: the formula and its symbols

Symbols

Symbol Meaning here For house 4
the true value 7
the model's current prediction
the loss: half the squared miss (the ½ only tidies the slope)
the derivative: how fast the loss changes as the prediction is nudged up
the direction that lowers the loss fastest, called the negative gradient 3
the residual: what each tree is trained to predict 3
"which gives"

In words: "for squared error, the direction that most quickly reduces the loss at each example is simply the true value minus the prediction: the residual."

With the numbers: house 4 has and . Nudge up to 4.001 and the loss falls from 4.5 to about 4.497, a slope of −3; the negative gradient is +3, which is exactly .

Level 3: in Python
y, F, eps = 7.0, 4.0, 1e-6
# ∂L/∂F, measured by nudging F both ways
slope = (0.5 * (y - (F + eps)) ** 2 - 0.5 * (y - (F - eps)) ** 2) / (2 * eps)
round(slope, 6)  # → -3.0
# r = -∂L/∂F is the plain residual y - F
round(-slope, 6), y - F  # → (3.0, 3.0)

The payoff of the gradient view: swap in any loss with a slope and the same loop still works. For yes/no labels with the log loss (primer.ml.losses), is a score in log-odds and the negative gradient is , the label minus the predicted probability ( is the sigmoid, which squashes a score into 0 to 1). Each tree then fits "how surprised were we by each example".

In code: GradientBoosting.negative_gradient returns for loss="squared" and for loss="log".

The update, and the learning rate

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
one input (a house's size) size 4
the round number, 1, 2, …, 1
the model's prediction before this round
this round's small tree, trained on the residuals
the learning rate (Greek letter eta), also called shrinkage: the fraction of each tree's correction actually applied 0.5
the prediction after this round 5.25

In words: "after each round, the new prediction is the old prediction plus a shrunken copy of the tree that was trained to fix it."

With the numbers: big houses: ; small houses: . Mean squared error before: ; after: $(1.75^2 + 0.75^2 + 0.75^2 + 1.75^2)/4 = 1.8125$.

Level 3: in Python
prices = [1, 2, 6, 7]
eta = 0.5
# F_0: the average price
F0 = sum(prices) / len(prices)
F0  # → 4.0
residuals = [y - F0 for y in prices]
residuals  # → [-3.0, -2.0, 2.0, 3.0]
# h_1: the stump predicts each side's average residual
small, big = (residuals[0] + residuals[1]) / 2, (residuals[2] + residuals[3]) / 2
h1 = [small, small, big, big]
h1  # → [-2.5, -2.5, 2.5, 2.5]
# F_1 = F_0 + η h_1
F1 = [F0 + eta * h for h in h1]
F1  # → [2.75, 2.75, 5.25, 5.25]
# mean squared error before and after the round
sum((y - F0) ** 2 for y in prices) / 4  # → 6.5
sum((y - f) ** 2 for y, f in zip(prices, F1)) / 4  # → 1.8125

Why not take the whole correction ()? Each tree is fitted to the training data, noise included. Taking the whole step lets the first few trees commit hard to whatever they saw; taking small steps means many trees must agree before the model moves far, which averages away some of the noise. The price is more rounds.

Figure 10 · Chart

0 2 4 6 x −1.5 −1.0 −0.5 0.0 0.5 1.0 1.5 y after 1 round true curve sin(x) boosted trees 0 2 4 6 x after 5 rounds 0 2 4 6 x after 50 rounds

Boosted depth-2 trees fitting noisy points around a sine curve: after 1 round two flat steps, after 5 rounds a coarse staircase, after 50 rounds a jagged staircase that tracks the sine and starts chasing individual points

Reading it: grey dots are 80 noisy samples of the dotted sine curve, and the blue line is the boosted model's prediction everywhere between 0 and 6. After 1 round the model is the average plus 0.3 of one small tree: a line with just two flat steps. After 5 rounds it has the rough shape. After 50 it follows the curve closely and has begun to jump for individual noisy points, the first sign of the overfitting the next figure measures. Every version is made of flat steps, because every tree predicts a constant in each leaf; the curve is built from many small staircases added together.

Figure 11 · Chart

0 25 50 75 100 125 150 175 200 boosting round 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 mean squared error Solid: validation. Dashed: training. noise floor 0.3² = 0.09 η = 1 (best validation at round 4) η = 0.3 (best validation at round 9) η = 0.1 (best validation at round 31)

Mean squared error per boosting round for learning rates 1, 0.3 and 0.1: dashed training curves fall toward zero; solid validation curves bottom out at round 4, 9 and 31 respectively, lowest for 0.1, and then climb

Reading it: dashed lines are training error and solid lines are error on 400 fresh points, per round, for three learning rates. Every training curve falls toward zero, since 200 rounds of trees can memorise 80 points. Each validation curve has a best round (the dot) and then rises, because later trees are fitting noise. With the best comes at round 4 (0.122) and is poor; with it comes at round 31 and is the lowest (0.110), close to the noise floor of 0.09 that no model can beat. Smaller steps generalize better but need more rounds, and every learning rate needs a stopping point chosen on validation data: early stopping, as in primer.ml.regularization.

In code: GradientBoosting runs the loop and records the training loss after each round; GradientBoosting.staged_loss scores held-out data after each round; boosting_worked_example is the four-house table; boosting_curves sweeps the learning rate on sine_data.

Why it matters gradient-boosted trees (through libraries such as XGBoost, LightGBM and CatBoost) are the most common winning model on tabular data in practice. They add refinements this lesson leaves out, such as a second-order (Newton) step for each leaf's value, penalties on tree size, fast histogram-based split search, and native handling of missing values, but the loop is the one above. The two knobs that matter most are the ones you have met: tree depth and the learning rate, with the number of rounds chosen by early stopping.

Chapter 8

When trees win, and when neural networks do

Everyday picture A spreadsheet and a photograph are both grids of numbers, but they carry meaning differently. In a spreadsheet each column means something on its own: "income is under 30,000" is a sentence a loan officer would write. In a photo, "pixel (212, 40) is brighter than 0.7" means nothing; the meaning is in how thousands of pixels are arranged. A tree asks questions of one column at a time, which is perfect for the spreadsheet and hopeless for the photo. A neural network builds its own features out of combinations of inputs, which is what the photo needs.

Tiny example A toy loan table: 800 training rows and 400 validation rows, five columns in their natural units: age in years, income in dollars (spanning a factor of 100), a count of late payments, a region code 0 to 5 (where region 4 is not "twice region 2"), and one column of pure random noise. The label follows rules of the kind a lender writes (four or more late payments; under 30,000 and under 30 years old; two late payments in certain regions or on a very low income), with 5% of labels flipped at random.

model what it was given validation accuracy
always predict "fine" nothing 77.75%
single tree, depth 5 the raw table 92.25%
random forest, 50 trees the raw table 95.75%
gradient boosting, 100 depth-3 trees the raw table 96.00%
small neural net (32 hidden units) the raw table 77.75%
same net every column rescaled to mean 0, spread 1 86.25%
same net rescaled, plus log income and one column per region 92.25%

Figure 12 · Chart

0.70 0.75 0.80 0.85 0.90 0.95 1.00 validation accuracy on the loan table single tree (depth 5) random forest gradient boosting neural net (raw inputs) neural net (scaled inputs) neural net (scaled + engineered) Trees read the table as it is; the net needs help always say 'fine': 77.75% 92.25% 95.75% 96.00% 77.75% 86.25% 92.25%

Validation accuracy on the loan table: random forest 95.75% and gradient boosting 96.0% lead; the neural net scores 77.75% on raw inputs (the same as always saying fine), 86.25% scaled, and 92.25% with engineered features

Reading it: blue bars are tree models, orange bars are the same small neural network given three versions of the same table, and the dashed line is the score for ignoring the inputs and always predicting "fine". The trees read the table exactly as it came. The net on raw inputs sits on the dashed line: incomes around 50,000 swamp every other column and saturate its neurons, so it learns nothing. Rescaling rescues it partly; hand-made features (the logarithm of income, a yes/no column per region) rescue it more. Even then it only ties a single depth-5 tree, and the ensembles stay ahead.

Figure 13 · Diagram

Reading it: the first question is about the shape of one input, not about fashion. Tables go down the left; perceptual and sequential data go right. On the left, the size of the data decides: at typical business scale, start with trees. The bottom-right branch is common in practice: a neural network turns text or images into embeddings (primer.ml.embeddings.word2vec), and those numbers become extra columns for a boosted-tree model.

Why trees fit tables so well, each point visible in this lesson's code:

  • Units don't matter. A tree only asks "is this bigger than that?", so any order-keeping change to a column (dollars to thousands, a logarithm, a square) leaves every prediction unchanged. A neural net multiplies and adds columns, so their scales collide.
  • Thresholds are native. Rules like "four or more late payments" are one question to a tree; a net must approximate the sharp edge with smooth curves.
  • Irrelevant columns are mostly ignored, because a column that never wins a split is never used.
  • Modest data is enough. A few hundred rows can grow a useful tree; a net with thousands of weights needs far more to pin them down.

Where neural networks win, and trees cannot follow:

  • Perceptual and sequential data: images, audio, text. The useful features are built from arrangements of raw values, which is what convolutions (primer.ml.cnn_rnn) and attention (primer.ml.transformer) learn.
  • Huge data and pretraining. A net's capacity keeps paying off as data grows, and a pretrained model brings knowledge from billions of examples. A tree starts from nothing every time.
  • Smooth trends and extrapolation. A tree's leaves hold averages of training values, so it can never predict outside the range it saw. The four-house model predicts the same price for a house of size 40 as for size 4.
  • End-to-end training with other neural parts, since a tree has no gradient to pass along.

In a careful benchmark on 45 tabular datasets of about 10,000 rows each (Grinsztajn, Oyallon and Varoquaux, 2022), tuned tree-based models remained ahead of tuned deep learning models. Their analysis names three reasons that match this lesson: trees shrug off uninformative columns, they respect each column's own axis, and they fit irregular, step-like functions easily. Research on neural networks for tables is active and the gap may narrow; the practical rule is to start with boosted trees on a table and make any other model beat them on your validation data.

In code: make_tabular builds the loan table; tabular_showdown trains every model in the table above (the net is primer.ml.neural_net.MLP); engineer_features is the hand-made preparation the net needed.

Feature importance, and its limits

Everyday picture After a football season you want to know which players mattered. One way: count how often each player touched the ball in training. Another: bench each player for a real match and see how much the score suffers. The first is quick but credits whoever is busy; the second measures what the team actually depends on.

Trees offer the first kind for free. Impurity importance adds up, for each feature, the impurity its splits removed (weighted by how many examples reached each split), measured on the training data. Permutation importance is the second kind: take held-out data, shuffle one column so it no longer lines up with the labels, and measure how much accuracy drops.

Tiny example The 30-tree forest scores 96.0% on the 400 validation rows. Shuffle the late-payments column and it scores 77.5%: a drop of 18.5 points, so the model leans on that column heavily. Shuffle the noise column and it scores 95.75%, a drop of 0.25 points (one row in 400).

Level 3: the formula and its symbols

Symbols

Symbol Meaning here For late payments
the feature (column) being tested late_payments
held-out rows the model never trained on 400 rows
the share of those rows the model gets right 0.96
shuffled the column's values randomly reordered: same values, links to the labels broken 0.775
the permutation importance of feature : accuracy lost without it 0.185

In words: "a feature's importance is how much accuracy the model loses on fresh data when that feature is scrambled."

With the numbers: late payments ; noise .

Level 3: in Python
# validation rows answered correctly, out of 400
correct_of_400 = {"intact": 384, "late_payments shuffled": 310, "noise shuffled": 383}
acc = {k: v / 400 for k, v in correct_of_400.items()}
# I_j = acc(X_val) - acc(X_val with column j shuffled)
round(acc["intact"] - acc["late_payments shuffled"], 4)  # → 0.185
round(acc["intact"] - acc["noise shuffled"], 4)  # → 0.0025

Figure 14 · Chart

0.00 0.05 0.10 0.15 0.20 0.25 share of impurity removed age income late_payments region noise Impurity importance (training data) 21% 25% 25% 19% 11% 0.00 0.05 0.10 0.15 accuracy lost when the column is shuffled Permutation importance (validation data) 7.75% 6.50% 18.50% 9.00% 0.25%

Two bar charts over age, income, late payments, region and noise: impurity importance gives the noise column 11% of the credit; permutation importance gives it 0.25 points of accuracy, far below late payments at 18.5 points

Reading it: the left panel is impurity importance from training, as shares that add to 100%; the right is permutation importance on validation data, in accuracy lost. Both agree the real columns matter. They disagree about noise: on the left it gets 11% of the credit, because deep trees can always find some threshold on a continuous random column that tidies up a few training rows. On the right, scrambling it costs almost nothing, because none of those splits help on new data.

The limits worth remembering:

  • Impurity importance favours columns with many distinct values (continuous numbers, IDs), since they offer more thresholds to overfit with. Measure on held-out data instead.
  • Correlated columns split the credit. If income appears twice (in dollars and in thousands), each copy looks half as important, and shuffling one does little because the other covers for it.
  • Importance is not cause. It says what this model relies on to predict, not what would change the outcome in the world.

A single shallow tree is the one truly readable model here: print DecisionTree.rules and you are reading the entire model. A forest of hundreds of deep trees is not readable, and importances are a summary of it, not an explanation.

In code: permutation_importance shuffles one column at a time on held-out data; DecisionTree.feature_importances and RandomForest.feature_importances are the impurity kind.

Test yourself

11 questions

Answer each one out loud or on paper before you open it. If you can explain it, you know it.

Question 1Why is Gini impurity 0.5 for a pile that is half spam and half not?Think it through, then reveal

It is the chance that two emails drawn at random (with replacement) carry different labels: . For two labels that is the most mixed a pile can be.

Question 2Why weight each child pile by its size when scoring a split?Think it through, then reveal

Otherwise a question that peels off one example into a tiny pure pile would look as good as one that sorts half the data cleanly. Weighting by size measures how much of the data the question actually tidied.

Question 3Why does a tree need no feature scaling, when a neural network does?Think it through, then reveal

A tree only compares one column against a threshold, so any change that keeps the order of values (rescaling, logarithms) produces the same splits and the same predictions. A network multiplies and adds columns together, so a column measured in tens of thousands drowns the others and saturates its neurons.

Question 4A tree scores 100% on training data and 80% on validation. What happened, and what are two fixes?Think it through, then reveal

It overfitted: it kept splitting until each leaf fenced off individual noisy examples. Limit its depth (or require a minimum number of examples per leaf, or prune), or replace it with a random forest that averages many such trees.

Question 5Why does a random forest use random feature subsets, not just bootstrap samples?Think it through, then reveal

The forest's variance is . More trees only shrink the second term; the floor is set by the correlation between trees. With bagging alone every tree tends to pick the same strong feature first and they stay correlated. Random subsets force different trees down different paths, lowering .

Question 6Does adding more trees overfit a random forest? Does adding more rounds overfit gradient boosting?Think it through, then reveal

More trees in a forest does not: accuracy rises and then levels off, only costing time. More rounds of boosting does: each round fits the training residuals more closely, so validation error reaches a minimum and then climbs. Boosting needs early stopping; forests don't.

Question 7Why is the residual the right target for each boosting tree?Think it through, then reveal

For squared-error loss , the negative derivative with respect to the prediction is , the residual. Fitting a tree to it and taking a step is gradient descent on the predictions. With another loss, the tree fits that loss's negative gradient instead, such as for log loss.

Question 8What does a smaller learning rate buy in gradient boosting, and what does it cost?Think it through, then reveal

Each tree contributes only a fraction of its correction, so no single noisy tree can move the model far and many trees must agree. That usually generalizes better (0.110 against 0.122 in this lesson's sweep). It costs more rounds, so more training and prediction time.

Question 9Why can't a boosted-tree model predict a house price above the highest price it trained on?Think it through, then reveal

Every leaf predicts an average of training values, and the model is a sum of such leaves anchored at the training mean. Beyond the edge of the training data every question gives the same answer as at the edge, so the prediction stays flat.

Question 10What is wrong with trusting impurity-based feature importance?Think it through, then reveal

It is measured on training data, where splits on noise still tidy up piles. It favours columns with many distinct values and splits credit between correlated columns. Permutation importance on held-out data is more honest, and neither measures cause and effect.

Question 11When would you choose a neural network over gradient-boosted trees?Think it through, then reveal

When one input is an image, audio clip, text or other sequence whose meaning lies in the arrangement of raw values; when the data is huge or a pretrained model can be reused; when predictions must extrapolate smoothly; or when the model must be trained end to end with other neural parts.

Primary sources

The papers behind this lesson

Breiman, Friedman, Olshen and Stone, Classification and Regression Trees (1984)

The CART book: binary trees split by Gini impurity, regression trees with averaged leaves, and cost-complexity pruning, the recipe this lesson's tree follows.

The paper ↗
Quinlan, Induction of Decision Trees, Machine Learning 1 (1986)

ID3, which grows trees by choosing the split with the largest information gain (drop in entropy).

The paper ↗
Breiman, Bagging Predictors, Machine Learning 24 (1996)

Showed that averaging models trained on bootstrap samples reduces the error of unstable learners such as trees.

The paper ↗
Breiman, Random Forests, Machine Learning 45 (2001)

Added random feature subsets at each split, out-of-bag error estimates, and permutation importance.

Read the annotated companion →The paper ↗
Friedman, Greedy Function Approximation: A Gradient Boosting Machine, Annals of Statistics 29 (2001)

Framed boosting as gradient descent in function space, fitting each tree to the negative gradient of any differentiable loss, with shrinkage.

Read the annotated companion →The paper ↗
Chen and Guestrin, XGBoost: A Scalable Tree Boosting System (2016)

A regularized, second-order boosting objective with fast, sparsity-aware split finding, which made boosted trees the default on tabular problems.

Read the annotated companion →The paper ↗
Grinsztajn, Oyallon and Varoquaux, Why do tree-based models still outperform deep learning on tabular data? (2022)

A benchmark on 45 medium-sized tabular datasets where tuned tree ensembles beat tuned neural networks, and an analysis of why.

The paper ↗

Researcher's shelf

Further reading

  • scikit-learn user guide, Decision Trees: https://scikit-learn.org/stable/modules/tree.html
  • scikit-learn user guide, Ensembles: gradient boosting, random forests, bagging: https://scikit-learn.org/stable/modules/ensemble.html
  • scikit-learn user guide, Permutation feature importance: https://scikit-learn.org/stable/modules/permutation_importance.html
  • XGBoost documentation, Introduction to Boosted Trees: https://xgboost.readthedocs.io/en/stable/tutorials/model.html
  • Hastie, Tibshirani and Friedman, The Elements of Statistical Learning (free PDF from the authors; chapters 9, 10 and 15 cover trees, boosting and random forests): https://hastie.su.domains/ElemStatLearn/
  • Strobl et al., Bias in random forest variable importance measures (2007): https://doi.org/10.1186/1471-2105-8-25
  • Grinsztajn et al., Why do tree-based models still outperform deep learning on tabular data? (2022): https://arxiv.org/abs/2207.08815

About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit c8d5c21, so the two always agree: the explanation, the code that builds it and the tests that prove it.