Machine Learning Algorithms

Ch.65: Stacking and Blending, Learning How to Combine Models

By Ayush Arora••8 min read

Inspired by: YouTube

1. From a Fixed Combination Rule to a Learned One

Voting ensembles train several different algorithms on the same data and combine their outputs with a fixed rule: average the predictions for regression, take the majority vote for classification. A linear regressor, a random forest, and a gradient boosting model might predict 3.5, 4.1, and 2.7 LPA for the same student, and voting just averages those three numbers to 3.43.

Stacking keeps the first half of that idea, train several different base models, and changes the second half. Instead of a fixed averaging rule, it trains a separate machine learning model, the meta-model, to learn how to combine the base models' outputs. The base models' predictions become the meta-model's input features, and the original target stays the target.

2. Building the Stacking Dataset, By Hand

Take the same CGPA/IQ/package dataset used throughout this series, and the same three base models as above: a linear regressor, a decision tree, and a KNN regressor. Stacking's training recipe:

  1. Train the base models. Fit all three on the full dataset, same as any other model.
  2. Collect their predictions as a new dataset. For every student, run all three trained models and record their outputs as three new columns. A student with CGPA 7, IQ 75, and an actual package of 3.5 might get predictions of 3.7, 4.1, and 2.5 from the three base models, so this student's row in the new dataset becomes [3.7, 4.1, 2.5] as input, with the target still 3.5.
  3. Train the meta-model on that new dataset. A random forest regressor, say, now trains on these three prediction columns instead of CGPA and IQ.

At inference time, a new query point goes through all three base models first, their three outputs become the meta-model's input, and the meta-model's output is the final prediction. Two differences from bagging and boosting are worth naming directly: the base models don't have to be the same type of algorithm, where bagging and boosting both reuse one algorithm throughout, and the combination step is a trained model rather than an average, a majority vote, or a weighted sum.

3. The Overfitting Trap in the Naive Version

The recipe above has a serious flaw. Step 2 generates predictions by running the base models on the same data they were just trained on. Any model that's prone to overfitting, a sufficiently deep decision tree, for instance, will produce predictions on its own training data that are better than it would ever achieve on new data; the model has partly memorized the answers. The meta-model then trains on these overly optimistic predictions, inherits that overfitting, and the whole stack generalizes worse than the base models would on their own.

The fix is to never let a base model produce a prediction on a row it was trained on. There are two ways to enforce that, and they go by different names depending on which one is used.

4. Blending: Fixing It With a Single Holdout Split

Split the training data into two pieces, DtrainD_{train} (used to actually build things) and DtestD_{test} (held back for final evaluation, same as always). Then split DtrainD_{train} itself a second time, into a train subset and a holdout subset, commonly 80/20:

D_train80%20%Train setHoldout setM1: Random ForestM2: KNNM3: Gradient Boostingtrainpredict on holdoutactual yMeta-featuresRF pred, KNN pred, GBDT pred+ actual y (holdout rows only)Meta-model(e.g. Logistic Regression)Finalprediction

Blending: a single holdout split produces the meta-model's training rows.

The base models only ever see the train subset during fitting. Once trained, they predict on the holdout subset, rows they've never touched, and those predictions, together with the holdout rows' actual target values, become the meta-model's training data. Since the base models never trained on the rows they're predicting here, there's no memorization to inherit. This holdout-based version of the recipe is specifically called blending.

5. Stacking Proper: Fixing It With K-Fold Cross-Validation

Blending works, but it only uses one slice of the data (the holdout split) to build the meta-model's training set, throwing away the rest. K-fold cross-validation fixes the same overfitting problem without wasting as much data, and this version is what "stacking" refers to specifically once the blending/stacking distinction is being made carefully (plenty of people use "stacking" loosely for both, but the distinction can come up in interviews).

Stage 1: build the meta-model's training datashown for one base model (Random Forest); the same 4 rounds repeat for KNN and Gradient BoostingD_trainK = 4 foldsF1F2F3F4round 1: train on the other 3, predict F1F1F2F3F4round 2: train on the other 3, predict F2F1F2F3F4round 3: train on the other 3, predict F3F1F2F3F4round 4: train on the other 3, predict F4Out-of-fold meta-featuresRF pred, KNN pred, GBDT pred+ actual y(every row predicted exactlyonce, by a model that nevertrained on that row)Meta-model (trained once)Logistic RegressionStage 2: rebuild the base models before inferenceD_train(full, no folds)Random Forest (final)KNN (final)Gradient Boosting (final)Final stacking model(base models + meta-model)already-trained meta-model plugs straight inNew / test rowFinalprediction

Stacking: K-fold CV builds out-of-fold meta-features for every row, then the base models are rebuilt on all of D_train before inference.

Split DtrainD_{train} into KK folds (commonly 5 or 10; the diagram above uses 4 for a readable example). For each base model, run KK rounds of training: in round kk, train on every fold except fold kk, then predict on fold kk alone, a fold the model hasn't seen this round. After all KK rounds, every single row in DtrainD_{train} has exactly one out-of-fold prediction from that base model, and no row was ever predicted by a model that trained on it. Repeating this for every base model produces a complete table of out-of-fold predictions covering all of DtrainD_{train}, which becomes the meta-model's training data.

There's a subtlety easy to miss here: this K-fold process produces a trained meta-model, but it leaves behind KK separately-trained copies of each base model (one per fold), none of which saw the full training set. None of those get used for inference. Instead, each base model gets retrained from scratch on the entirety of DtrainD_{train}, with the K-fold splits forgotten entirely. The final system pairs these fully-retrained base models with the meta-model trained on the out-of-fold predictions, so the meta-model gets trained first and the base models used at inference time get finalized second, a reversed order from what the setup might suggest.

6. Multi-Layer Architectures

Nothing restricts stacking to one layer of base models feeding one meta-model directly. A second layer of models can sit between the base models and the final meta-model, each layer's outputs becoming the next layer's inputs.

Building this with blending's holdout approach means DtrainD_{train} needs splitting into as many pieces as there are layers: a first chunk trains layer 1's models, a second chunk (which layer 1 hasn't seen) gets predicted on by layer 1 to produce layer 2's training data, a third chunk (which neither layer has seen) gets predicted on by layer 2 to produce the final meta-model's training data, and so on. Three layers means three-way splitting DtrainD_{train} into progressively smaller holdout chunks, as the diagram below shows for a three-layer stack:

D_train chunkLayer 1Layer 2Meta-modelD_trainChunk 1Chunk 2Chunk 3trainLayer 1RF, KNN, GBDTrow stops here: chunk 1 only ever trains Layer 1, nothing predicts on itpredictLayer 1RF, KNN, GBDTLayer 1 preds+ actual ytrainLayer 2SVM, Decision TreepredictLayer 1RF, KNN, GBDTLayer 1 predsLayer 2SVM, Decision TreeLayer 2 preds+ actual ytrainMeta-modelLogistic Regressiontrained on this row's chunkalready trained, only predictssame trained models, reused

Three-layer blending: each chunk of D_train trains exactly one layer, after passing through every layer already trained above it.

Notice that every chunk trains exactly one layer, and before it does, it gets pushed through every layer that was already trained above it, so no layer ever trains on predictions made by models that had already seen those rows. Multi-layer architectures like this show up frequently in Kaggle-winning solutions, sometimes with dozens of models in the first layer narrowing down through several layers to one final predictor, though working out the K-fold equivalent of this multi-layer holdout scheme (rather than progressively splitting the data, running nested K-fold CV at each layer) is a worthwhile exercise in its own right.

7. In Code: scikit-learn's StackingClassifier

scikit-learn's StackingClassifier (and its regression counterpart, StackingRegressor) implements the K-fold version from Section 5 directly: pass a list of base estimators, a final_estimator, and a cv value for KK. On the UCI heart disease dataset (binary target: heart disease present or not):

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, StackingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
 
df = pd.read_csv("heart.csv")
X = df.drop(columns=["target"])
y = df["target"]
 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=8)
 
estimators = [
    ("rf", RandomForestClassifier(n_estimators=10, random_state=42)),
    ("knn", KNeighborsClassifier(n_neighbors=10)),
    ("gbdt", GradientBoostingClassifier()),
]
 
clf = StackingClassifier(estimators=estimators, final_estimator=LogisticRegression(), cv=10)
clf.fit(X_train, y_train)
 
y_pred = clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
0.8688524590163934

A few parameters worth knowing about beyond what's used here: stack_method controls what a classifier's base estimators hand to the meta-model, raw class predictions, predicted probabilities, or decision function scores, with "auto" picking whichever each estimator supports; and passthrough=True appends the original input features alongside the base models' predictions when training the meta-model, rather than training it on the base models' outputs alone. This example leaves both at their defaults. StackingClassifier and StackingRegressor need scikit-learn 0.22 or newer; neither exists in earlier releases.

8. What to Take Away