{"id":1000,"date":"2026-06-15T12:58:03","date_gmt":"2026-06-15T12:58:03","guid":{"rendered":"https:\/\/tensorzen.online\/?p=1000"},"modified":"2026-06-15T12:58:03","modified_gmt":"2026-06-15T12:58:03","slug":"gradient-boosted-decision-trees-gbdt","status":"publish","type":"post","link":"https:\/\/tensorzen.online\/?p=1000","title":{"rendered":"Gradient Boosted Decision Trees (GBDT)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This notebook builds a complete understanding of GBDT from the ground up:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Decision trees as weak learners<\/li>\n\n\n\n<li>Ensemble methods: bagging vs. boosting<\/li>\n\n\n\n<li>The gradient boosting algorithm \u2014 mathematical derivation<\/li>\n\n\n\n<li>GBDT from scratch (regression)<\/li>\n\n\n\n<li>Loss functions and their gradients<\/li>\n\n\n\n<li>Scikit-learn implementation (regression + classification)<\/li>\n\n\n\n<li>Hyperparameter effects<\/li>\n\n\n\n<li>Feature importance<\/li>\n\n\n\n<li>Overfitting and regularisation<\/li>\n\n\n\n<li>Random Forest vs. GBDT comparison<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">import numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nfrom sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier, plot_tree\nfrom sklearn.ensemble import GradientBoostingRegressor, GradientBoostingClassifier, RandomForestClassifier\nfrom sklearn.datasets import make_regression, make_classification, make_moons\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import mean_squared_error, accuracy_score\nfrom sklearn.preprocessing import StandardScaler\nimport warnings\nwarnings.filterwarnings(&#039;ignore&#039;)\n\nrng = np.random.default_rng(42)\nplt.rcParams&#091;&#039;figure.dpi&#039;] = 100<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">1. Decision Trees: The Weak Learner<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GBDT builds an ensemble of <strong>decision trees<\/strong>. Each tree partitions the feature space into rectangular regions and predicts a constant value in each region.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A single deep tree overfits; a single shallow tree (a <em>stump<\/em>) underfits. GBDT uses many shallow trees \u2014 deliberately weak \u2014 and combines them sequentially.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why shallow trees?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Low variance: they don&#8217;t chase noise<\/li>\n\n\n\n<li>Fast to train<\/li>\n\n\n\n<li>Their errors are <em>systematic<\/em> (high bias), which subsequent trees can correct<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\"># Generate a simple 1D regression problem\nnp.random.seed(42)\nX_1d = np.sort(np.random.uniform(0, 10, 120))\ny_1d = np.sin(X_1d) + 0.3 * np.random.randn(120)\nX_1d = X_1d.reshape(-1, 1)\n\nx_plot = np.linspace(0, 10, 500).reshape(-1, 1)\n\nfig, axes = plt.subplots(1, 3, figsize=(15, 4))\ndepths = &#091;1, 3, 10]\ntitles = &#091;&#039;Stump (depth=1)\\nHigh bias, low variance&#039;,\n          &#039;Shallow tree (depth=3)\\nBetter balance&#039;,\n          &#039;Deep tree (depth=10)\\nLow bias, high variance&#039;]\n\nfor ax, d, title in zip(axes, depths, titles):\n    tree = DecisionTreeRegressor(max_depth=d, random_state=42)\n    tree.fit(X_1d, y_1d)\n    ax.scatter(X_1d, y_1d, s=15, alpha=0.5, color=&#039;gray&#039;, label=&#039;Data&#039;)\n    ax.plot(x_plot, tree.predict(x_plot), color=&#039;crimson&#039;, lw=2, label=f&#039;Tree (depth={d})&#039;)\n    ax.plot(x_plot, np.sin(x_plot), &#039;k--&#039;, lw=1.5, alpha=0.5, label=&#039;True signal&#039;)\n    train_mse = mean_squared_error(y_1d, tree.predict(X_1d))\n    ax.set_title(f&#039;{title}\\nTrain MSE={train_mse:.3f}&#039;)\n    ax.legend(fontsize=8)\n    ax.set_xlabel(&#039;x&#039;); ax.set_ylabel(&#039;y&#039;)\n\nplt.suptitle(&#039;Single decision trees: bias-variance trade-off&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"267\" src=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5-1024x267.png\" alt=\"\" class=\"wp-image-1001\" srcset=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5-1024x267.png 1024w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5-300x78.png 300w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5-768x200.png 768w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5-1536x400.png 1536w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-5.png 1866w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">2. Ensemble Methods: Bagging vs. Boosting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both methods combine many weak learners, but in fundamentally different ways:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>Bagging<\/th><th>Boosting<\/th><\/tr><\/thead><tbody><tr><td>Training order<\/td><td>Parallel (independent)<\/td><td>Sequential (each depends on previous)<\/td><\/tr><tr><td>How errors are addressed<\/td><td>Averaging reduces variance<\/td><td>Each model corrects prior errors<\/td><\/tr><tr><td>Data sampling<\/td><td>Bootstrap resamples<\/td><td>Reweights or fits residuals<\/td><\/tr><tr><td>Typical depth<\/td><td>Deep trees<\/td><td>Shallow trees (stumps)<\/td><\/tr><tr><td>Risk<\/td><td>Low variance, can still have bias<\/td><td>Low bias, can overfit<\/td><\/tr><tr><td>Example<\/td><td>Random Forest<\/td><td>GBDT, AdaBoost<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The key boosting idea:<\/strong> each new model is trained not on the original targets $y$, but on the <strong>errors<\/strong> of the current ensemble. The ensemble improves iteratively.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">fig, axes = plt.subplots(1, 2, figsize=(13, 5))\n\n# \u2500\u2500 Bagging diagram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;0]\nax.set_xlim(0, 10); ax.set_ylim(0, 8); ax.axis(&#039;off&#039;)\nax.set_title(&#039;Bagging (e.g. Random Forest)&#039;, fontsize=12, pad=10)\n\nax.text(5, 7.3, &#039;Original Data&#039;, ha=&#039;center&#039;, fontsize=10,\n        bbox=dict(boxstyle=&#039;round&#039;, facecolor=&#039;lightblue&#039;))\nfor i, x in enumerate(&#091;1.5, 3.5, 5.5, 7.5, 9.0]):\n    if i &lt; 4:\n        ax.annotate(&#039;&#039;, xy=(x, 5.8), xytext=(5, 6.9),\n                    arrowprops=dict(arrowstyle=&#039;-&gt;&#039;, color=&#039;gray&#039;))\n        ax.text(x, 5.4, f&#039;Bootstrap\\nSample {i+1}&#039;, ha=&#039;center&#039;, fontsize=8,\n                bbox=dict(boxstyle=&#039;round&#039;, facecolor=&#039;lightyellow&#039;))\n        ax.annotate(&#039;&#039;, xy=(x, 3.8), xytext=(x, 5.0),\n                    arrowprops=dict(arrowstyle=&#039;-&gt;&#039;, color=&#039;gray&#039;))\n        ax.text(x, 3.3, f&#039;Tree {i+1}&#039;, ha=&#039;center&#039;, fontsize=9,\n                bbox=dict(boxstyle=&#039;round&#039;, facecolor=&#039;lightgreen&#039;))\n        ax.annotate(&#039;&#039;, xy=(5, 1.8), xytext=(x, 2.9),\n                    arrowprops=dict(arrowstyle=&#039;-&gt;&#039;, color=&#039;gray&#039;))\nax.text(5, 1.3, &#039;Average\\nPredictions&#039;, ha=&#039;center&#039;, fontsize=10,\n        bbox=dict(boxstyle=&#039;round&#039;, facecolor=&#039;salmon&#039;))\n\n# \u2500\u2500 Boosting diagram \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;1]\nax.set_xlim(0, 10); ax.set_ylim(0, 8); ax.axis(&#039;off&#039;)\nax.set_title(&#039;Boosting (e.g. GBDT)&#039;, fontsize=12, pad=10)\n\nsteps = &#091;&#039;Train Tree 1\\non targets $y$&#039;,\n         &#039;Train Tree 2\\non residuals $r_1$&#039;,\n         &#039;Train Tree 3\\non residuals $r_2$&#039;,\n         &#039;Final:\\n$F = T_1 + T_2 + T_3$&#039;]\ncolors = &#091;&#039;lightblue&#039;, &#039;lightyellow&#039;, &#039;lightgreen&#039;, &#039;salmon&#039;]\nys = &#091;6.5, 4.8, 3.1, 1.3]\nfor i, (step, c, y) in enumerate(zip(steps, colors, ys)):\n    ax.text(5, y, step, ha=&#039;center&#039;, fontsize=9,\n            bbox=dict(boxstyle=&#039;round&#039;, facecolor=c))\n    if i &lt; len(steps) - 1:\n        ax.annotate(&#039;&#039;, xy=(5, ys&#091;i+1]+0.45), xytext=(5, y-0.1),\n                    arrowprops=dict(arrowstyle=&#039;-&gt;&#039;, color=&#039;gray&#039;, lw=1.5))\n\nplt.suptitle(&#039;Bagging vs. Boosting: structural difference&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()\n<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"428\" src=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-6-1024x428.png\" alt=\"\" class=\"wp-image-1002\" srcset=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-6-1024x428.png 1024w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-6-300x125.png 300w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-6-768x321.png 768w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-6.png 1329w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">3. The Gradient Boosting Algorithm<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Gradient boosting frames boosting as <strong>gradient descent in function space<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setup<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We want to find a function $F(x)$ that minimises the expected loss:<br>$$\\mathcal{L} = \\sum_{i=1}^{n} L(y_i, F(x_i))$$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of optimising over parameters, we optimise over functions by taking gradient steps. At each step $m$, we ask:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><em>In which direction should we nudge $F$ at each training point to most reduce the loss?<\/em><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The answer is the <strong>negative gradient<\/strong> of the loss with respect to $F(x_i)$:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$r_{im} = -\\left[\\frac{\\partial L(y_i,\\, F(x_i))}{\\partial F(x_i)}\\right]<em>{F = F<\/em>{m-1}}$$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These $r_{im}$ are called <strong>pseudo-residuals<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Algorithm<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Initialise:<\/strong> $F_0(x) = \\arg\\min_\\gamma \\sum_{i} L(y_i, \\gamma)$ (a constant \u2014 e.g. the mean for MSE)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For<\/strong> $m = 1, 2, \\ldots, M$:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Compute pseudo-residuals: $r_{im} = -\\frac{\\partial L(y_i, F_{m-1}(x_i))}{\\partial F_{m-1}(x_i)}$<\/li>\n\n\n\n<li>Fit a tree $h_m(x)$ to ${(x_i, r_{im})}$<\/li>\n\n\n\n<li>Find the optimal step size: $\\gamma_m = \\arg\\min_\\gamma \\sum_i L(y_i,\\, F_{m-1}(x_i) + \\gamma\\, h_m(x_i))$<\/li>\n\n\n\n<li>Update: $F_m(x) = F_{m-1}(x) + \\nu \\cdot \\gamma_m \\cdot h_m(x)$<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">where $\\nu \\in (0,1]$ is the <strong>learning rate<\/strong> (shrinkage).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">For MSE loss<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">$$L(y, F) = \\tfrac{1}{2}(y &#8211; F)^2 \\implies r_{im} = y_i &#8211; F_{m-1}(x_i)$$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The pseudo-residuals are exactly the ordinary residuals. Each tree fits the errors left by the current ensemble.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. GBDT from Scratch (Regression, MSE Loss)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We implement the algorithm step by step to see every moving part.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">class GBDTRegressor:\n    &quot;&quot;&quot;\n    Gradient Boosted Decision Trees for regression with MSE loss.\n    Pseudo-residuals = ordinary residuals (y - F).\n    &quot;&quot;&quot;\n    def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):\n        self.n_estimators  = n_estimators\n        self.learning_rate = learning_rate\n        self.max_depth     = max_depth\n        self.trees_         = &#091;]\n        self.F0_            = None     # initial constant prediction\n\n    def fit(self, X, y):\n        # Step 0: initialise with the mean\n        self.F0_ = np.mean(y)\n        F = np.full(len(y), self.F0_)\n\n        for m in range(self.n_estimators):\n            # Step 1: compute pseudo-residuals (negative gradient of MSE)\n            residuals = y - F\n\n            # Step 2: fit a tree to the pseudo-residuals\n            tree = DecisionTreeRegressor(max_depth=self.max_depth, random_state=m)\n            tree.fit(X, residuals)\n            self.trees_.append(tree)\n\n            # Step 3 &amp; 4: update prediction\n            F += self.learning_rate * tree.predict(X)\n\n        return self\n\n    def predict(self, X):\n        F = np.full(len(X), self.F0_)\n        for tree in self.trees_:\n            F += self.learning_rate * tree.predict(X)\n        return F\n\n    def staged_predict(self, X):\n        &quot;&quot;&quot;Yield predictions after each boosting round.&quot;&quot;&quot;\n        F = np.full(len(X), self.F0_)\n        yield F.copy()\n        for tree in self.trees_:\n            F += self.learning_rate * tree.predict(X)\n            yield F.copy()<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\"># Train and visualise the sequential build-up\ngbdt_scratch = GBDTRegressor(n_estimators=50, learning_rate=0.3, max_depth=2)\ngbdt_scratch.fit(X_1d, y_1d)\n\nx_plot = np.linspace(0, 10, 500).reshape(-1, 1)\nstages  = &#091;1, 3, 8, 20, 50]\npredictions = list(gbdt_scratch.staged_predict(x_plot))\n\nfig, axes = plt.subplots(1, len(stages), figsize=(18, 4), sharey=True)\nfor ax, m in zip(axes, stages):\n    ax.scatter(X_1d, y_1d, s=12, alpha=0.4, color=&#039;gray&#039;)\n    ax.plot(x_plot, np.sin(x_plot), &#039;k--&#039;, lw=1.5, alpha=0.5, label=&#039;True&#039;)\n    ax.plot(x_plot, predictions&#091;m], color=&#039;crimson&#039;, lw=2, label=f&#039;m={m}&#039;)\n    train_mse = mean_squared_error(y_1d, list(gbdt_scratch.staged_predict(X_1d))&#091;m])\n    ax.set_title(f&#039;{m} tree{&quot;s&quot; if m&gt;1 else &quot;&quot;}\\nMSE={train_mse:.4f}&#039;)\n    ax.legend(fontsize=8)\n    ax.set_xlabel(&#039;x&#039;)\naxes&#091;0].set_ylabel(&#039;y&#039;)\n\nplt.suptitle(&#039;GBDT from scratch: ensemble fit after $m$ trees (lr=0.3, depth=2)&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"226\" src=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-1024x226.png\" alt=\"\" class=\"wp-image-1003\" srcset=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-1024x226.png 1024w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-300x66.png 300w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-768x170.png 768w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-1536x339.png 1536w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-7-2048x452.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\"># Show the residuals being fitted at each step\nfig, axes = plt.subplots(1, 4, figsize=(16, 4))\nF = np.full(len(X_1d), gbdt_scratch.F0_)\n\nfor ax, m in zip(axes, &#091;0, 1, 2, 4]):\n    for i in range(m):\n        F += gbdt_scratch.learning_rate * gbdt_scratch.trees_&#091;i].predict(X_1d)\n    residuals = y_1d - F\n    ax.scatter(X_1d, residuals, s=12, alpha=0.5, color=&#039;steelblue&#039;)\n    ax.axhline(0, color=&#039;crimson&#039;, lw=1.5, linestyle=&#039;--&#039;)\n    ax.set_title(f&#039;Residuals after {m} tree{&quot;s&quot; if m!=1 else &quot;&quot;}&#039;)\n    ax.set_xlabel(&#039;x&#039;)\n    rss = np.sum(residuals**2)\n    ax.text(0.05, 0.95, f&#039;RSS={rss:.2f}&#039;, transform=ax.transAxes, va=&#039;top&#039;, fontsize=9,\n            bbox=dict(boxstyle=&#039;round&#039;, facecolor=&#039;wheat&#039;, alpha=0.7))\n    F = np.full(len(X_1d), gbdt_scratch.F0_)  # reset for next panel\n\naxes&#091;0].set_ylabel(&#039;Residual&#039;)\nplt.suptitle(&#039;Pseudo-residuals shrink as more trees are added&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"253\" src=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8-1024x253.png\" alt=\"\" class=\"wp-image-1004\" srcset=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8-1024x253.png 1024w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8-300x74.png 300w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8-768x190.png 768w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8-1536x380.png 1536w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-8.png 1993w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">5. Loss Functions and Their Gradients<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The choice of loss function determines what the pseudo-residuals represent.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Task<\/th><th>Loss $L(y, F)$<\/th><th>Pseudo-residual $r = -\\partial L \/ \\partial F$<\/th><\/tr><\/thead><tbody><tr><td>Regression (MSE)<\/td><td>$\\frac{1}{2}(y-F)^2$<\/td><td>$y &#8211; F$<\/td><\/tr><tr><td>Regression (MAE)<\/td><td>$<\/td><td>y &#8211; F<\/td><\/tr><tr><td>Regression (Huber)<\/td><td>Quadratic near 0, linear in tails<\/td><td>Clipped residuals<\/td><\/tr><tr><td>Classification<\/td><td>$\\log(1 + e^{-yF})$ (log-loss)<\/td><td>$y &#8211; \\sigma(F)$ where $\\sigma$ is sigmoid<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The gradient of MAE is just the sign \u2014 the tree fits the <em>direction<\/em> of the error, not the magnitude. This makes MAE-based GBDT robust to outliers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">F_vals = np.linspace(-3, 3, 300)\ny_true = 1.0\n\nmse_loss    = 0.5 * (y_true - F_vals)**2\nmse_grad    = -(y_true - F_vals)          # negative gradient = residual\nmae_loss    = np.abs(y_true - F_vals)\nmae_grad    = -np.sign(y_true - F_vals)\n\n# Huber\ndelta = 1.0\ndiff = y_true - F_vals\nhuber_loss = np.where(np.abs(diff) &lt;= delta,\n                      0.5 * diff**2,\n                      delta * (np.abs(diff) - 0.5 * delta))\nhuber_grad = np.where(np.abs(diff) &lt;= delta, -diff, -delta * np.sign(diff))\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\nax = axes&#091;0]\nax.plot(F_vals, mse_loss,   label=&#039;MSE&#039;,   color=&#039;steelblue&#039;, lw=2)\nax.plot(F_vals, mae_loss,   label=&#039;MAE&#039;,   color=&#039;crimson&#039;,   lw=2)\nax.plot(F_vals, huber_loss, label=&#039;Huber&#039;, color=&#039;green&#039;,     lw=2)\nax.axvline(y_true, color=&#039;gray&#039;, lw=1, linestyle=&#039;--&#039;, label=f&#039;y={y_true}&#039;)\nax.set_xlabel(&#039;F(x)&#039;); ax.set_ylabel(&#039;Loss&#039;); ax.set_title(&#039;Loss functions&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\nax = axes&#091;1]\nax.plot(F_vals, mse_grad,   label=&#039;MSE pseudo-residual&#039;,   color=&#039;steelblue&#039;, lw=2)\nax.plot(F_vals, mae_grad,   label=&#039;MAE pseudo-residual&#039;,   color=&#039;crimson&#039;,   lw=2)\nax.plot(F_vals, huber_grad, label=&#039;Huber pseudo-residual&#039;, color=&#039;green&#039;,     lw=2)\nax.axvline(y_true, color=&#039;gray&#039;, lw=1, linestyle=&#039;--&#039;)\nax.axhline(0, color=&#039;gray&#039;, lw=0.7)\nax.set_xlabel(&#039;F(x)&#039;); ax.set_ylabel(&#039;Pseudo-residual&#039;)\nax.set_title(&#039;Pseudo-residuals = negative gradient&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\nplt.suptitle(&#039;Loss functions and their pseudo-residuals (y=1.0)&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"310\" src=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9-1024x310.png\" alt=\"\" class=\"wp-image-1005\" srcset=\"https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9-1024x310.png 1024w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9-300x91.png 300w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9-768x232.png 768w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9-1536x465.png 1536w, https:\/\/tensorzen.online\/wp-content\/uploads\/2026\/06\/image-9.png 1620w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">6. Scikit-learn Implementation<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">6a. Regression<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">X_reg, y_reg = make_regression(n_samples=500, n_features=10, noise=20, random_state=42)\nX_tr, X_te, y_tr, y_te = train_test_split(X_reg, y_reg, test_size=0.2, random_state=42)\n\ngbr = GradientBoostingRegressor(\n    n_estimators=200,\n    learning_rate=0.1,\n    max_depth=3,\n    subsample=0.8,       # stochastic gradient boosting\n    random_state=42\n)\ngbr.fit(X_tr, y_tr)\n\n# Staged test error\ntrain_errors = &#091;mean_squared_error(y_tr, pred) for pred in gbr.staged_predict(X_tr)]\ntest_errors  = &#091;mean_squared_error(y_te, pred) for pred in gbr.staged_predict(X_te)]\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\nax = axes&#091;0]\nax.plot(train_errors, label=&#039;Train MSE&#039;, color=&#039;steelblue&#039;, lw=2)\nax.plot(test_errors,  label=&#039;Test MSE&#039;,  color=&#039;crimson&#039;,   lw=2)\nax.set_xlabel(&#039;Number of trees&#039;); ax.set_ylabel(&#039;MSE&#039;)\nax.set_title(&#039;Learning curve: MSE vs. number of trees&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\nax = axes&#091;1]\ny_pred = gbr.predict(X_te)\nax.scatter(y_te, y_pred, alpha=0.4, s=20, color=&#039;steelblue&#039;)\nmn, mx = min(y_te.min(), y_pred.min()), max(y_te.max(), y_pred.max())\nax.plot(&#091;mn, mx], &#091;mn, mx], &#039;k--&#039;, lw=1.5, label=&#039;Perfect prediction&#039;)\nax.set_xlabel(&#039;True&#039;); ax.set_ylabel(&#039;Predicted&#039;)\nax.set_title(f&#039;Predicted vs. True (Test MSE={test_errors&#091;-1]:.1f})&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\nplt.suptitle(&#039;GradientBoostingRegressor \u2014 scikit-learn&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()\n\nprint(f&quot;Final Train MSE : {train_errors&#091;-1]:.2f}&quot;)\nprint(f&quot;Final Test MSE  : {test_errors&#091;-1]:.2f}&quot;)<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6b. Classification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For binary classification GBDT uses <strong>log-loss<\/strong> (cross-entropy). The model outputs a log-odds score $F(x)$, converted to a probability via the sigmoid:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$P(y=1 \\mid x) = \\sigma(F(x)) = \\frac{1}{1 + e^{-F(x)}}$$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The pseudo-residuals become: $r_i = y_i &#8211; \\sigma(F(x_i))$ \u2014 the difference between the true label and the predicted probability.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">X_moons, y_moons = make_moons(n_samples=600, noise=0.25, random_state=42)\nX_tr_c, X_te_c, y_tr_c, y_te_c = train_test_split(X_moons, y_moons, test_size=0.25, random_state=42)\n\ngbc = GradientBoostingClassifier(n_estimators=200, learning_rate=0.1,\n                                  max_depth=3, random_state=42)\ngbc.fit(X_tr_c, y_tr_c)\n\n# Decision boundary\ndef plot_decision_boundary(ax, model, X, y, title):\n    h = 0.03\n    x_min, x_max = X&#091;:,0].min()-0.5, X&#091;:,0].max()+0.5\n    y_min, y_max = X&#091;:,1].min()-0.5, X&#091;:,1].max()+0.5\n    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),\n                         np.arange(y_min, y_max, h))\n    Z = model.predict_proba(np.c_&#091;xx.ravel(), yy.ravel()])&#091;:,1]\n    Z = Z.reshape(xx.shape)\n    ax.contourf(xx, yy, Z, alpha=0.3, cmap=&#039;RdBu_r&#039;, levels=20)\n    ax.contour(xx, yy, Z, levels=&#091;0.5], colors=&#039;black&#039;, linewidths=1.5)\n    ax.scatter(X&#091;:,0], X&#091;:,1], c=y, cmap=&#039;RdBu_r&#039;, s=20, edgecolors=&#039;k&#039;, lw=0.3)\n    acc = accuracy_score(y, model.predict(X))\n    ax.set_title(f&#039;{title}\\nAccuracy={acc:.3f}&#039;)\n\nfig, axes = plt.subplots(1, 3, figsize=(15, 4))\n\n# Show progression\nfor ax, n in zip(axes, &#091;5, 30, 200]):\n    m = GradientBoostingClassifier(n_estimators=n, learning_rate=0.1,\n                                    max_depth=3, random_state=42)\n    m.fit(X_tr_c, y_tr_c)\n    plot_decision_boundary(ax, m, X_te_c, y_te_c, f&#039;{n} trees&#039;)\n\nplt.suptitle(&#039;GBDT classification on moons dataset: decision boundary evolution&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">7. Hyperparameter Effects<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GBDT has several important hyperparameters:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Parameter<\/th><th>Effect<\/th><\/tr><\/thead><tbody><tr><td><code class=\"\" data-line=\"\">n_estimators<\/code><\/td><td>More trees \u2192 lower bias, risk of overfitting<\/td><\/tr><tr><td><code class=\"\" data-line=\"\">learning_rate<\/code><\/td><td>Smaller \u2192 need more trees, but generalises better<\/td><\/tr><tr><td><code class=\"\" data-line=\"\">max_depth<\/code><\/td><td>Controls complexity of each tree<\/td><\/tr><tr><td><code class=\"\" data-line=\"\">subsample<\/code><\/td><td>Fraction of data per tree \u2014 reduces variance (stochastic GB)<\/td><\/tr><tr><td><code class=\"\" data-line=\"\">min_samples_leaf<\/code><\/td><td>Smooths leaf predictions, reduces overfitting<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The learning rate\u2013n_estimators trade-off:<\/strong> a small learning rate requires many trees to achieve the same training loss, but typically yields better generalisation. These two are always tuned together.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">fig, axes = plt.subplots(2, 2, figsize=(13, 10))\n\n# \u2500\u2500 1. Learning rate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;0]&#091;0]\nfor lr in &#091;0.01, 0.05, 0.1, 0.5]:\n    m = GradientBoostingRegressor(n_estimators=200, learning_rate=lr,\n                                   max_depth=3, random_state=42)\n    m.fit(X_tr, y_tr)\n    te = &#091;mean_squared_error(y_te, p) for p in m.staged_predict(X_te)]\n    ax.plot(te, label=f&#039;lr={lr}&#039;, lw=1.8)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;Test MSE&#039;)\nax.set_title(&#039;Effect of learning rate&#039;); ax.legend(); ax.grid(True, alpha=0.3)\n\n# \u2500\u2500 2. Tree depth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;0]&#091;1]\nfor d in &#091;1, 2, 3, 5, 8]:\n    m = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1,\n                                   max_depth=d, random_state=42)\n    m.fit(X_tr, y_tr)\n    te = &#091;mean_squared_error(y_te, p) for p in m.staged_predict(X_te)]\n    ax.plot(te, label=f&#039;depth={d}&#039;, lw=1.8)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;Test MSE&#039;)\nax.set_title(&#039;Effect of tree depth&#039;); ax.legend(); ax.grid(True, alpha=0.3)\n\n# \u2500\u2500 3. Subsample \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;1]&#091;0]\nfor ss in &#091;0.3, 0.5, 0.8, 1.0]:\n    m = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1,\n                                   max_depth=3, subsample=ss, random_state=42)\n    m.fit(X_tr, y_tr)\n    te = &#091;mean_squared_error(y_te, p) for p in m.staged_predict(X_te)]\n    ax.plot(te, label=f&#039;subsample={ss}&#039;, lw=1.8)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;Test MSE&#039;)\nax.set_title(&#039;Effect of subsample (stochastic GB)&#039;); ax.legend(); ax.grid(True, alpha=0.3)\n\n# \u2500\u2500 4. lr \u00d7 n_estimators trade-off \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;1]&#091;1]\nconfigs = &#091;(0.5, 40), (0.1, 200), (0.05, 400), (0.01, 2000)]\nresults = &#091;]\nfor lr, n in configs:\n    m = GradientBoostingRegressor(n_estimators=n, learning_rate=lr,\n                                   max_depth=3, random_state=42)\n    m.fit(X_tr, y_tr)\n    results.append(mean_squared_error(y_te, m.predict(X_te)))\nlabels = &#091;f&#039;lr={lr}, n={n}&#039; for lr, n in configs]\nbars = ax.bar(labels, results, color=&#091;&#039;steelblue&#039;,&#039;darkorange&#039;,&#039;green&#039;,&#039;crimson&#039;], alpha=0.8)\nax.set_ylabel(&#039;Test MSE&#039;)\nax.set_title(&#039;lr \u00d7 n_estimators trade-off\\n(same approximate total &quot;work&quot;)&#039;)\nax.tick_params(axis=&#039;x&#039;, rotation=15)\nfor bar, val in zip(bars, results):\n    ax.text(bar.get_x() + bar.get_width()\/2, bar.get_height() + 5,\n            f&#039;{val:.0f}&#039;, ha=&#039;center&#039;, fontsize=9)\nax.grid(True, alpha=0.3, axis=&#039;y&#039;)\n\nplt.suptitle(&#039;Hyperparameter effects on test MSE&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">8. Feature Importance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GBDT computes feature importance as the <strong>total reduction in loss<\/strong> (impurity) due to splits on each feature, summed across all trees:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">$$\\text{importance}(f) = \\sum_{m=1}^{M} \\sum_{\\text{nodes } t \\in h_m \\text{ that split on } f} \\Delta L_t$$<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">where $\\Delta L_t$ is the reduction in the split criterion at node $t$.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Limitations:<\/strong> this measure favours high-cardinality features and can be misleading when features are correlated. Permutation importance is a more reliable alternative.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">from sklearn.inspection import permutation_importance\n\nfeature_names = &#091;f&#039;Feature {i+1}&#039; for i in range(X_reg.shape&#091;1])]\n\n# Built-in impurity importance\nimp_builtin = gbr.feature_importances_\n\n# Permutation importance (on test set)\nperm = permutation_importance(gbr, X_te, y_te, n_repeats=20, random_state=42)\nimp_perm = perm.importances_mean\n\nidx_b = np.argsort(imp_builtin)&#091;::-1]\nidx_p = np.argsort(imp_perm)&#091;::-1]\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\nax = axes&#091;0]\nax.bar(range(len(idx_b)), imp_builtin&#091;idx_b], color=&#039;steelblue&#039;, alpha=0.8)\nax.set_xticks(range(len(idx_b)))\nax.set_xticklabels(&#091;feature_names&#091;i] for i in idx_b], rotation=45, ha=&#039;right&#039;)\nax.set_title(&#039;Built-in impurity-based importance&#039;); ax.set_ylabel(&#039;Importance&#039;)\nax.grid(True, alpha=0.3, axis=&#039;y&#039;)\n\nax = axes&#091;1]\nax.bar(range(len(idx_p)), imp_perm&#091;idx_p], color=&#039;crimson&#039;, alpha=0.8,\n       yerr=perm.importances_std&#091;idx_p], capsize=4)\nax.set_xticks(range(len(idx_p)))\nax.set_xticklabels(&#091;feature_names&#091;i] for i in idx_p], rotation=45, ha=&#039;right&#039;)\nax.set_title(&#039;Permutation importance (test set)&#039;); ax.set_ylabel(&#039;Mean MSE increase&#039;)\nax.grid(True, alpha=0.3, axis=&#039;y&#039;)\n\nplt.suptitle(&#039;Feature importance: impurity-based vs. permutation&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">9. Overfitting and Regularisation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GBDT can overfit, especially with:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Too many trees (<code class=\"\" data-line=\"\">n_estimators<\/code> too large without shrinkage)<\/li>\n\n\n\n<li>Too large a learning rate<\/li>\n\n\n\n<li>Too deep trees (<code class=\"\" data-line=\"\">max_depth<\/code>)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Regularisation strategies:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Strategy<\/th><th>Parameter<\/th><th>Mechanism<\/th><\/tr><\/thead><tbody><tr><td>Shrinkage<\/td><td><code class=\"\" data-line=\"\">learning_rate<\/code><\/td><td>Scales each tree&#8217;s contribution down<\/td><\/tr><tr><td>Stochastic GB<\/td><td><code class=\"\" data-line=\"\">subsample &lt; 1<\/code><\/td><td>Trains each tree on a random subsample<\/td><\/tr><tr><td>Tree constraints<\/td><td><code class=\"\" data-line=\"\">max_depth<\/code>, <code class=\"\" data-line=\"\">min_samples_leaf<\/code><\/td><td>Limits individual tree complexity<\/td><\/tr><tr><td>Early stopping<\/td><td><code class=\"\" data-line=\"\">n_iter_no_change<\/code><\/td><td>Halts when validation loss stops improving<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\"># Demonstrate overfitting then early stopping\nfig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\n# \u2500\u2500 Overfitting: high lr, deep trees \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;0]\nm_overfit = GradientBoostingRegressor(n_estimators=500, learning_rate=0.5,\n                                       max_depth=6, random_state=42)\nm_overfit.fit(X_tr, y_tr)\ntr_e = &#091;mean_squared_error(y_tr, p) for p in m_overfit.staged_predict(X_tr)]\nte_e = &#091;mean_squared_error(y_te, p) for p in m_overfit.staged_predict(X_te)]\nbest_n = np.argmin(te_e)\nax.plot(tr_e, label=&#039;Train&#039;, color=&#039;steelblue&#039;, lw=2)\nax.plot(te_e, label=&#039;Test&#039;,  color=&#039;crimson&#039;,   lw=2)\nax.axvline(best_n, color=&#039;green&#039;, lw=2, linestyle=&#039;--&#039;,\n           label=f&#039;Best test @ n={best_n}&#039;)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;MSE&#039;)\nax.set_title(&#039;Overfitting: high lr (0.5), deep trees (depth=6)&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\n# \u2500\u2500 Early stopping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nax = axes&#091;1]\nm_es = GradientBoostingRegressor(\n    n_estimators=500,\n    learning_rate=0.5,\n    max_depth=6,\n    n_iter_no_change=15,   # stop if no improvement for 15 rounds\n    validation_fraction=0.15,\n    random_state=42\n)\nm_es.fit(X_tr, y_tr)\nte_e2 = &#091;mean_squared_error(y_te, p) for p in m_es.staged_predict(X_te)]\nax.plot(te_e,  label=&#039;Without early stopping&#039;, color=&#039;crimson&#039;,   lw=2, alpha=0.7)\nax.plot(te_e2, label=&#039;With early stopping&#039;,    color=&#039;steelblue&#039;, lw=2)\nax.axvline(m_es.n_estimators_ - 1, color=&#039;green&#039;, lw=2, linestyle=&#039;--&#039;,\n           label=f&#039;Stopped @ n={m_es.n_estimators_}&#039;)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;Test MSE&#039;)\nax.set_title(&#039;Early stopping prevents overfitting&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\nplt.suptitle(&#039;Overfitting and early stopping in GBDT&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()\n\nprint(f&quot;Without early stopping \u2014 final test MSE : {te_e&#091;-1]:.2f}  ({len(te_e)} trees)&quot;)\nprint(f&quot;With early stopping    \u2014 final test MSE : {te_e2&#091;-1]:.2f}  ({m_es.n_estimators_} trees)&quot;)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">10. Random Forest vs. GBDT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both are tree ensembles but operate on different principles. Neither is universally better \u2014 the choice depends on the problem.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">from sklearn.ensemble import RandomForestClassifier\n\nX_cl, y_cl = make_classification(n_samples=1000, n_features=20, n_informative=10,\n                                   n_redundant=5, random_state=42)\nX_tr2, X_te2, y_tr2, y_te2 = train_test_split(X_cl, y_cl, test_size=0.25, random_state=42)\n\nn_range = &#091;1, 5, 10, 20, 50, 100, 200]\nrf_scores, gbdt_scores = &#091;], &#091;]\n\nfor n in n_range:\n    rf = RandomForestClassifier(n_estimators=n, random_state=42)\n    rf.fit(X_tr2, y_tr2)\n    rf_scores.append(accuracy_score(y_te2, rf.predict(X_te2)))\n\n    gb = GradientBoostingClassifier(n_estimators=n, learning_rate=0.1,\n                                     max_depth=3, random_state=42)\n    gb.fit(X_tr2, y_tr2)\n    gbdt_scores.append(accuracy_score(y_te2, gb.predict(X_te2)))\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\nax = axes&#091;0]\nax.plot(n_range, rf_scores,   &#039;o-&#039;, label=&#039;Random Forest&#039;, color=&#039;steelblue&#039;, lw=2)\nax.plot(n_range, gbdt_scores, &#039;s-&#039;, label=&#039;GBDT&#039;,          color=&#039;crimson&#039;,   lw=2)\nax.set_xlabel(&#039;n_estimators&#039;); ax.set_ylabel(&#039;Test Accuracy&#039;)\nax.set_title(&#039;Accuracy vs. number of trees&#039;)\nax.legend(); ax.grid(True, alpha=0.3)\n\n# Side-by-side comparison on moons\nax = axes&#091;1]\nmodels = &#091;\n    (&#039;Random Forest&#039;,  RandomForestClassifier(n_estimators=200, random_state=42)),\n    (&#039;GBDT&#039;,           GradientBoostingClassifier(n_estimators=200, learning_rate=0.1,\n                                                   max_depth=3, random_state=42)),\n]\nfor name, mdl in models:\n    mdl.fit(X_tr_c, y_tr_c)\naccs = &#091;accuracy_score(y_te_c, m.predict(X_te_c)) for _, m in models]\nnames = &#091;n for n, _ in models]\nbars = ax.bar(names, accs, color=&#091;&#039;steelblue&#039;, &#039;crimson&#039;], alpha=0.8)\nfor bar, acc in zip(bars, accs):\n    ax.text(bar.get_x() + bar.get_width()\/2, bar.get_height() - 0.02,\n            f&#039;{acc:.3f}&#039;, ha=&#039;center&#039;, va=&#039;top&#039;, color=&#039;white&#039;, fontweight=&#039;bold&#039;)\nax.set_ylim(0.8, 1.0); ax.set_ylabel(&#039;Test Accuracy&#039;)\nax.set_title(&#039;Accuracy on moons dataset (n=200 trees)&#039;)\nax.grid(True, alpha=0.3, axis=&#039;y&#039;)\n\nplt.suptitle(&#039;Random Forest vs. GBDT&#039;, fontsize=13)\nplt.tight_layout()\nplt.show()\n\nprint(&quot;\\nRandom Forest vs. GBDT \u2014 summary:&quot;)\nprint(f&quot;{&#039;Property&#039;:&lt;30} {&#039;Random Forest&#039;:&lt;22} {&#039;GBDT&#039;}&quot;)\nprint(&quot;-&quot; * 70)\nrows = &#091;\n    (&#039;Training strategy&#039;,    &#039;Parallel (independent)&#039;, &#039;Sequential (corrective)&#039;),\n    (&#039;Bias-variance&#039;,        &#039;Reduces variance&#039;,       &#039;Reduces bias&#039;),\n    (&#039;Tree depth&#039;,           &#039;Deep trees&#039;,             &#039;Shallow trees&#039;),\n    (&#039;Overfitting risk&#039;,     &#039;Lower&#039;,                  &#039;Higher (needs tuning)&#039;),\n    (&#039;Training speed&#039;,       &#039;Faster (parallelisable)&#039;,&#039;Slower (sequential)&#039;),\n    (&#039;Hyperparameter sens.&#039;, &#039;Lower&#039;,                  &#039;Higher&#039;),\n    (&#039;Performance (typical)&#039;,&#039;Strong baseline&#039;,        &#039;Often best in class&#039;),\n]\nfor prop, rf, gbdt in rows:\n    print(f&quot;{prop:&lt;30} {rf:&lt;22} {gbdt}&quot;)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>Key point<\/th><\/tr><\/thead><tbody><tr><td>Weak learner<\/td><td>Shallow decision tree \u2014 high bias, low variance<\/td><\/tr><tr><td>Boosting idea<\/td><td>Each new tree corrects the errors of the current ensemble<\/td><\/tr><tr><td>Gradient descent analogy<\/td><td>Pseudo-residuals are the negative gradient of the loss w.r.t. $F$<\/td><\/tr><tr><td>MSE pseudo-residuals<\/td><td>Ordinary residuals $y &#8211; F$<\/td><\/tr><tr><td>Log-loss pseudo-residuals<\/td><td>$y &#8211; \\sigma(F)$ \u2014 label minus predicted probability<\/td><\/tr><tr><td>Learning rate $\\nu$<\/td><td>Shrinks each tree&#8217;s contribution \u2014 trades speed for generalisation<\/td><\/tr><tr><td>Subsample<\/td><td>Stochastic gradient boosting \u2014 reduces variance, speeds training<\/td><\/tr><tr><td>Feature importance<\/td><td>Total impurity reduction across all splits on that feature<\/td><\/tr><tr><td>Overfitting<\/td><td>Controlled via learning rate, depth, subsample, and early stopping<\/td><\/tr><tr><td>vs. Random Forest<\/td><td>GBDT reduces bias (sequential); RF reduces variance (parallel)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This notebook builds a complete understanding of GBDT from the ground up: 1. Decision Trees: The Weak Learner GBDT builds an ensemble of decision trees. Each tree partitions [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[13,25,4],"tags":[],"class_list":["post-1000","post","type-post","status-publish","format-standard","hentry","category-classical-algorithm","category-in-english","category-machine-learning"],"_links":{"self":[{"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/posts\/1000","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tensorzen.online\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1000"}],"version-history":[{"count":1,"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/posts\/1000\/revisions"}],"predecessor-version":[{"id":1006,"href":"https:\/\/tensorzen.online\/index.php?rest_route=\/wp\/v2\/posts\/1000\/revisions\/1006"}],"wp:attachment":[{"href":"https:\/\/tensorzen.online\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1000"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tensorzen.online\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1000"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tensorzen.online\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1000"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}