One Tree Is a Liar

A minimodule on bagging, stacking, and boosting: how a crowd of unreliable models becomes one you can trust.

The Problem With One Good Model

Take the decision tree from the last class and grow it on the Titanic passengers with no depth limit. It splits until almost every passenger sits in their own leaf.

# a Titanic tree with no depth limit
from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(random_state=0).fit(X_train, y_train)

tree.score(X_train, y_train)   # 0.989
tree.score(X_test,  y_test)    # 0.734   the gap is the lie
tree.get_depth()               # 17

714 passengers, an 80/20 split. Almost perfect on the ones it trained on, barely past a guess on the rest.

One unstable model is a bad bet. A crowd of them, each wrong somewhere different, can be a good one. You let them disagree, then combine. The instability is the raw material: stable models like k-nearest-neighbours barely move when the data shifts, so copies of them agree on everything and the crowd adds nothing.

Why do ensembles prefer unstable base models like deep trees?


Bagging: The Crowd Votes

Resample the data with replacement, train a tree on each copy, then average the votes. Every tree saw a different slice and made different mistakes, so the disagreements cancel. Variance drops, bias stays put, and the trees train in parallel. Bagging on trees, plus one trick from the next module, is the random forest.

Bagging: different data subsets, same algorithm, results averaged

Different subsets of the data, the same algorithm on each, the answers averaged. Illustration after vas3k, Machine Learning for Everyone.

# the same depth-hungry trees, but 300 of them on bootstrap samples
from sklearn.ensemble import RandomForestClassifier

forest = RandomForestClassifier(n_estimators=300, random_state=0).fit(X_train, y_train)

forest.score(X_train, y_train)   # 0.989
forest.score(X_test,  y_test)    # 0.776   steadier on new passengers

Bagging mainly reduces which of these?


Boosting: The Line Forms

Train the trees in sequence, not side by side. Each new shallow tree pays attention to what the ones before it got wrong. The errors no single weak tree could reach get chipped away, so bias falls. Run it too long and it starts memorising. No parallel here, and the names you will meet are XGBoost and LightGBM.

Boosting: each new tree focuses on the cases the previous one got wrong

Each new tree gets the cases the previous one fumbled, and the chain repeats. Illustration after vas3k, Machine Learning for Everyone.

# shallow trees, each fixing the leftovers of the last
from sklearn.ensemble import GradientBoostingClassifier

boost = GradientBoostingClassifier(random_state=0).fit(X_train, y_train)

boost.score(X_train, y_train)   # 0.926
boost.score(X_test,  y_test)    # 0.790   smaller gap, less memorizing

What is the core difference between bagging and boosting?


Stacking: Ask Around First

Train several different models in parallel, a nearest-neighbours, a tree, a support vector machine, then let one more model learn how to weigh their answers. The base models have to disagree to be worth blending. It shows up less than the other two, because they usually win for less effort.

Stacking: different algorithms on the same data, a final model decides

Different algorithms on the same data, a final model deciding from their answers. Illustration after vas3k, Machine Learning for Everyone.

# three different models, a fourth learns to weigh them
from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC

stack = StackingClassifier(
    estimators=[("knn", KNeighborsClassifier()),
                ("tree", DecisionTreeClassifier(max_depth=4)),
                ("svm", SVC())],
    final_estimator=LogisticRegression(max_iter=1000),
).fit(X_train, y_train)

stack.score(X_train, y_train)   # 0.872
stack.score(X_test,  y_test)    # 0.797   different models, best test yet

Four models, one Titanic. Test accuracy walked 0.73, 0.78, 0.79, 0.80, and the gap between training and test closed at every step.

For the curious: where does the bootstrap leave 63.2% behind?

A bootstrap sample draws \(n\) rows from a dataset of \(n\) rows, with replacement. The chance that one particular row is missed on a single draw is \(1 - \tfrac{1}{n}\). Across all \(n\) draws, the chance it is missed every time is:

$$\left(1 - \frac{1}{n}\right)^{n} \;\xrightarrow{\; n \to \infty \;}\; \frac{1}{e} \approx 0.368$$

So about 36.8% of the rows never make it into a given sample, and about 63.2% do. The rows left out are called the out-of-bag set. Each model can be tested on exactly the rows it never trained on, which hands you a validation estimate for free, without setting any data aside.


Now Try It Yourself

Here is something wavy and noisy, the kind of shape a straight line gives up on. Pick a strategy, set how many trees and how deep, and watch what the crowd draws. The faint curves are the individual models. The bold one is what they agree on.

Data Individual trees Ensemble
8
4
Training error (MSE): 0.000
Test error (MSE): 0.000

Each tree trains on its own bootstrap sample, so they disagree, most of all out at the edges. Average them and the disagreement cancels. More trees, steadier result. Deeper trees give the average more wobble to absorb.

The other half of a random forest, picking a random subset of features at each split, needs more than one feature to mean anything. This curve has only one, so only the bootstrap sampling shows here. The feature trick gets its own treatment in the next module.


Before You Move On

You have a deep tree that scores perfectly on the training data and falls apart on new customers. Which of the three methods would you reach for first, and which kind of error does it fix?

A fraud system has to score each transaction in a few milliseconds, spread across many machines at once. Bagging or boosting? Say why in one sentence.

Overfitting has shown up in regression, in single trees, and now here. Which of these three methods has its own way of overfitting if you let it run too long?