Ensemble Theory: Bagging, Random Forests & Boosting
Derive the mathematical proof of variance reduction in bagging, feature decorrelation in random forests, exponential loss minimization in AdaBoost, second-order Taylor expansions in XGBoost, and zero-dependency implementations in Python and Rust.
Ensemble learning combines multiple base models (weak learners) to produce a unified predictor whose predictive performance and robustness exceed that of any individual constituent. The two foundational paradigms in ensemble theory are Bagging (Variance Reduction) and Boosting (Bias Reduction).
Variance of an ensemble of B estimators converges to pairwise correlation rho * sigma^2 as B approaches infinity.
1. Mathematical Proof: Variance Reduction in Bagging#
Consider an ensemble of base estimators , each with variance and pairwise correlation coefficient for .
The ensemble prediction is the simple arithmetic mean:
Step 1: Expand Ensemble Variance
By the definition of the variance of a linear combination of random variables:
Step 2: Substitute Variance and Covariance
Since each estimator has variance and covariance :
Step 3: Simplify the Expression
Divide both terms by :
Rearranging the terms:
Asymptotic Implications & Random Forests#
Taking the limit as the number of estimators :
2. Derivation of Out-of-Bag (OOB) Error#
In bootstrap sampling, instances are drawn uniformly at random with replacement from a training set of size .
The probability that a specific training sample is not selected in a single draw is .
The probability that is completely excluded across all independent draws is:
Taking the calculus limit as dataset size :
Thus, approximately of training samples are withheld from each individual tree. These Out-Of-Bag (OOB) samples serve as built-in cross-validation test sets, evaluating generalization error without dedicated validation splits.
3. First-Principles Derivation of AdaBoost#
AdaBoost sequentially trains base learners by reweighting training samples to focus on instances misclassified by earlier rounds.
Deriving the Estimator Weight #
AdaBoost minimizes the exponential loss over additive ensemble :
Where sample weights .
Step 1: Split Loss into Correct and Incorrect Predictions
Because , the product is for correct predictions and for incorrect predictions:
Step 2: Express in Terms of Weighted Error $\epsilon_t$
Let denote total sample weight. The weighted training error is:
The weight of correctly classified samples is:
Substituting these into the loss expression:
Step 3: Differentiate with Respect to $\alpha_t$ and Solve for Zero
To minimize exponential loss with respect to :
Divide by and multiply by :
Taking the natural logarithm and dividing by 2:
When , . As error , estimator weight .
4. XGBoost: Second-Order Taylor Expansion#
Modern Gradient Boosted Machines like XGBoost optimize arbitrary loss functions using second-order Taylor approximations.
At boosting iteration , the regularized objective is:
Where tree regularization .
Expanding via a second-order Taylor series around :
Where first and second order gradients are defined as:
Grouping samples mapped to leaf node ():
Let and . Setting the derivative with respect to leaf weight to zero:
Substituting optimal weights back gives the optimal split quality metric:
5. From-Scratch Implementation: Zero Dependencies#
The following implementations construct an AdaBoost Classifier with Decision Stumps from scratch in Python and Rust using only built-in language primitives.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687import math from typing import List, Tuple class DecisionStump: def __init__(self): self.polarity: int = 1 self.feature_idx: int = 0 self.threshold: float = 0.0 self.alpha: float = 0.0 def predict(self, X: List[List[float]]) -> List[int]: num_samples = len(X) predictions = [1] * num_samples for i in range(num_samples): val = X[i][self.feature_idx] if (self.polarity == 1 and val < self.threshold) or (self.polarity == -1 and val > self.threshold): predictions[i] = -1 return predictions class AdaBoostFromScratch: def __init__(self, n_estimators: int = 10): self.n_estimators = n_estimators self.estimators: List[DecisionStump] = [] def fit(self, X: List[List[float]], y: List[int]) -> None: num_samples = len(X) num_features = len(X[0]) # Initialize uniform sample weights w = [1.0 / num_samples] * num_samples for _ in range(self.n_estimators): stump = DecisionStump() min_error = float("inf") # Search across all features and unique thresholds for feat in range(num_features): unique_vals = set(row[feat] for row in X) for thresh in unique_vals: for polarity in [1, -1]: # Generate predictions for this candidate stump error = 0.0 for i in range(num_samples): val = X[i][feat] pred = -1 if ((polarity == 1 and val < thresh) or (polarity == -1 and val > thresh)) else 1 if pred != y[i]: error += w[i] if error < min_error: min_error = error stump.polarity = polarity stump.threshold = thresh stump.feature_idx = feat # Clip error to avoid division by zero or negative logarithms eps = max(1e-10, min(min_error, 1.0 - 1e-10)) stump.alpha = 0.5 * math.log((1.0 - eps) / eps) # Update sample weights: w_i * exp(-alpha * y_i * h(x_i)) preds = stump.predict(X) for i in range(num_samples): w[i] *= math.exp(-stump.alpha * y[i] * preds[i]) # Normalize sample weights w_sum = sum(w) for i in range(num_samples): w[i] /= w_sum self.estimators.append(stump) def predict(self, X: List[List[float]]) -> List[int]: # Form additive sum: sum alpha_t * h_t(x) num_samples = len(X) final_scores = [0.0] * num_samples for stump in self.estimators: preds = stump.predict(X) for i in range(num_samples): final_scores[i] += stump.alpha * preds[i] return [1 if s >= 0.0 else -1 for s in final_scores] if __name__ == "__main__": X_train = [[1.0, 2.0], [1.5, 1.0], [2.0, 2.5], [6.0, 7.0], [7.0, 8.0], [8.0, 6.5]] y_train = [-1, -1, -1, 1, 1, 1] clf = AdaBoostFromScratch(n_estimators=5) clf.fit(X_train, y_train) preds = clf.predict([[1.2, 1.8], [7.5, 7.0]]) print(f"Predictions: {preds}")
6. Active Knowledge Checks#
In Random Forests, why does feature subspace sampling (evaluating only sqrt(d) random features per node) reduce ensemble test error compared to standard bagging?
What mathematical property enables XGBoost to optimize any custom objective loss function seamlessly?