{"id":"l-ml-2-2","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Write gradient descent by hand in NumPy to fit a line, see how the learning rate makes it crawl, converge or explode, and explain why features must be scaled.","html":"<p><code dir=\"ltr\">LinearRegression().fit</code> returned the best line instantly, with no visible search. For a straight line there’s an exact formula, so that’s fair. But most models (logistic regression, neural networks, the gradient boosting you’ll meet in section 3) have no formula. They find their parameters by search, and nearly all of them use some form of the same search: gradient descent.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Learning as walking downhill</h2>\n<p>Take the BMI-only model from last lesson: <code dir=\"ltr\">prediction = w * x + b</code>. For any pair of <code dir=\"ltr\">w</code> and <code dir=\"ltr\">b</code> you can compute the mean squared error on the training data. Picture every possible <code dir=\"ltr\">w</code> laid out left to right and the loss for each one as a height. For a straight line with squared error, that picture is a smooth bowl, and the best <code dir=\"ltr\">w</code> sits at the bottom.</p>\n<p>You can’t see the bowl, but at any point you can compute its slope: the <strong>gradient</strong>. If the slope is positive, increasing <code dir=\"ltr\">w</code> makes things worse, so decrease it. If negative, increase it. Take a small step against the slope, recompute, repeat. That’s <strong>gradient descent</strong>.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 270\" role=\"img\" aria-labelledby=\"l-ml-2-2-t1\"><defs><marker id=\"arrow-l-ml-2-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-2-2-t1\">A U-shaped loss curve over the parameter w. Dots mark successive steps from the upper left down toward the minimum, each step shorter than the last as the slope flattens.</title>\n  <line class=\"d-line\" x1=\"40\" y1=\"240\" x2=\"640\" y2=\"240\"/>\n  <text class=\"d-label-muted\" x=\"620\" y=\"262\">w</text>\n  <text class=\"d-label-muted\" x=\"46\" y=\"30\">loss</text>\n  <path class=\"d-line\" d=\"M80 30 Q340 450 600 30\" fill=\"none\"/>\n  <circle class=\"d-dot\" cx=\"110\" cy=\"76\" r=\"7\"/>\n  <circle class=\"d-dot\" cx=\"190\" cy=\"170\" r=\"7\"/>\n  <circle class=\"d-dot\" cx=\"250\" cy=\"215\" r=\"7\"/>\n  <circle class=\"d-dot\" cx=\"295\" cy=\"234\" r=\"7\"/>\n  <circle class=\"d-dot\" cx=\"325\" cy=\"239\" r=\"7\"/>\n  <path class=\"d-arrow\" d=\"M118 84 L180 160\" marker-end=\"url(#arrow-l-ml-2-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M200 177 L240 208\" marker-end=\"url(#arrow-l-ml-2-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M260 220 L285 230\" marker-end=\"url(#arrow-l-ml-2-2-1)\"/>\n  <text class=\"d-label\" x=\"140\" y=\"60\">big slope, big step</text>\n  <text class=\"d-label\" x=\"360\" y=\"200\">flat at the minimum</text>\n</svg><figcaption>Gradient descent takes steps down the loss curve</figcaption></figure>\n<p>The steps shrink on their own as you approach the bottom, because the slope gets flatter. The size of each step is the gradient multiplied by the <strong>learning rate</strong>, a hyperparameter you choose.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Gradient descent in eight lines</h2>\n<p>For squared error, the gradients have short formulas: the slope for <code dir=\"ltr\">w</code> is <code dir=\"ltr\">2 * mean(error * x)</code> and for <code dir=\"ltr\">b</code> it’s <code dir=\"ltr\">2 * mean(error)</code>, where <code dir=\"ltr\">error = prediction - actual</code>. Here it is in NumPy, with BMI standardised to mean 0 and standard deviation 1 first (you’ll see why shortly).</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> numpy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.linear_model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LinearRegression</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_frame</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(X, y, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">x </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ((X_train[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bmi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> X_train[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bmi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].mean()) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> X_train[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bmi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].std(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ddof</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).to_numpy()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">target </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> y_train.to_numpy()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">w, b, learning_rate </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.1</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> step </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> range</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">101</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    error </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (w </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> x </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> target</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> step </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 25</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"step </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">step</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:3}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  loss </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">np.mean(error </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">**</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:9.1f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  w </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">w</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:6.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  b </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">b</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:7.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    w </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> learning_rate </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.mean(error </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> x)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    b </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> learning_rate </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.mean(error)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">exact </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LinearRegression().fit(x.reshape(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), target)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"LinearRegression:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, exact.coef_.round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(exact.intercept_, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>The loss falls from about 29,900 to 3,931 within 25 steps and then stops moving. The <code dir=\"ltr\">w</code> and <code dir=\"ltr\">b</code> it settles on match <code dir=\"ltr\">LinearRegression</code> to two decimals. You’ve trained a model by hand.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">The learning rate decides everything</h2>\n<p>Change <code dir=\"ltr\">learning_rate</code> and rerun. With <code dir=\"ltr\">0.01</code>, a hundred steps reach only <code dir=\"ltr\">w ≈ 40</code> and <code dir=\"ltr\">b ≈ 134</code>: the search crawls. With <code dir=\"ltr\">0.5</code>, it lands in a single step. With <code dir=\"ltr\">1.05</code>, each step overshoots the bottom by more than the last, and after a hundred steps the loss is in the trillions. That’s <strong>divergence</strong>, and when you see a loss that grows instead of shrinking, a too-large learning rate is the first suspect.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Blaming the model when the loss explodes</span></p>\n<p>A loss that rises every step, or turns into <code dir=\"ltr\">nan</code>, rarely means the model is wrong for the data. It usually means the learning rate is too large for the scale of the features. Lower the rate by a factor of ten, or scale the features, before changing anything else.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Why scale features</h2>\n<p>Real tables mix scales. In the churn data, <code dir=\"ltr\">total_spent</code> ranges into the thousands while <code dir=\"ltr\">used_coupon</code> is 0 or 1. With two features like that, the loss surface is no longer a round bowl but a long, narrow valley. A learning rate small enough to stay stable along the steep direction is painfully slow along the flat one, and the path zig-zags across the valley.</p>\n<p>Scale also hits models that never use gradients. k-nearest neighbours measures distance between customers, and a $300 difference in spend swamps a difference of four support tickets. Standardising every column to mean 0 and standard deviation 1 puts them on equal footing.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.neighbors </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> KNeighborsClassifier</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StandardScaler</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">features </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_order_value\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">            \"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"used_coupon\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"support_tickets\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    churn[features], churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    stratify</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">scaler </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StandardScaler().fit(X_train)          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># learn means and stds from TRAIN only</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train_s, X_test_s </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> scaler.transform(X_train), scaler.transform(X_test)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"learned means:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, scaler.mean_[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">raw </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> KNeighborsClassifier().fit(X_train, y_train).score(X_test, y_test)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">scaled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> KNeighborsClassifier().fit(X_train_s, y_train).score(X_test_s, y_test)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"k-NN raw </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">raw</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  scaled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">scaled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>Scaling alone takes k-NN from 0.72 to 0.75, past the 80-day rule from section 1. Same model, same data; the only change is that every feature now gets a fair vote.</p>\n<p>A scaler is an estimator too. It has <code dir=\"ltr\">fit</code> (learn each column’s mean and standard deviation) and <code dir=\"ltr\">transform</code> (apply them), and the same rule applies as for models: fit on training data only. Juggling <code dir=\"ltr\">X_train_s</code> and <code dir=\"ltr\">X_test_s</code> by hand gets error-prone, so section 4 bundles scaler and model into a single pipeline object.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Not every model cares</span></p>\n<p>Decision trees and the ensembles built from them split on one feature at a time, using only the order of values, so scaling doesn’t change their predictions. Plain least squares adjusts its coefficients to compensate. Scaling matters for gradient-trained models, distance-based models, and any model with a penalty on coefficient size, which is exactly what this section’s last lesson adds.</p>\n</aside>\n<p>Gradient descent will find the best line, but “best on the training data” can be a trap when the model is flexible. Next lesson makes the line bend, and watches it bend too far.</p>\n","toc":[{"id":"sec-1","text":"Learning as walking downhill","level":2},{"id":"sec-2","text":"Gradient descent in eight lines","level":2},{"id":"sec-3","text":"The learning rate decides everything","level":2},{"id":"sec-4","text":"Why scale features","level":2}],"takeaways":["Training a model means searching for the parameter values that make the loss as small as possible.","Gradient descent repeats one step: compute the slope of the loss for each parameter, then move each parameter a little in the downhill direction.","The learning rate sets the step size: too small crawls, too large overshoots and diverges.","Features on very different scales stretch the loss surface, which slows gradient descent and distorts distance-based models such as k-NN.","<code dir=\"ltr\">StandardScaler</code> learns each column’s mean and standard deviation from the training data and rescales to mean 0, standard deviation 1."],"further":[{"title":"StandardScaler","url":"https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html"},{"title":"Stochastic Gradient Descent","url":"https://scikit-learn.org/stable/modules/sgd.html"},{"title":"Importance of feature scaling","url":"https://scikit-learn.org/stable/auto_examples/preprocessing/plot_scaling_importance.html"}],"quiz":[{"q":"<p>During gradient descent, the gradient of the loss with respect to <code dir=\"ltr\">w</code> is positive. What does the update do to <code dir=\"ltr\">w</code>?</p>","options":[{"text":"Increases <code dir=\"ltr\">w</code>, because a positive gradient means <code dir=\"ltr\">w</code> should grow.","why":"A positive gradient means the loss rises as <code dir=\"ltr\">w</code> grows. Moving with it climbs uphill."},{"text":"Decreases <code dir=\"ltr\">w</code>, because the update subtracts learning rate times gradient.","why":"Correct. Stepping against the gradient is what makes the loss go down."},{"text":"Leaves <code dir=\"ltr\">w</code> unchanged until the gradient becomes zero.","why":"A non-zero gradient is exactly the signal to move. Only at the minimum is it zero."}],"answer":1},{"q":"<p>You double the learning rate and the printed loss goes 3,900 → 12,000 → 95,000 → 2,000,000. What happened?</p>","options":[{"text":"The model found a better minimum further away.","why":"A better minimum would show a lower loss, not an exploding one."},{"text":"The data has outliers that the larger rate exposed.","why":"Outliers don’t make the loss grow without bound step after step. Overshooting does."},{"text":"Each step overshoots the minimum by more than the last, so the search diverges.","why":"Correct. Lower the learning rate (or scale the features) until the loss falls steadily."}],"answer":2},{"q":"<p>Where should you fit a <code dir=\"ltr\">StandardScaler</code>?</p>","options":[{"text":"On the training data only, then use it to transform both training and test data.","why":"Correct. The test set must be treated like future data, which you can’t see when you fit the scaler."},{"text":"On the full dataset before splitting, so both parts use the same mean and standard deviation.","why":"That lets test-set statistics shape your preprocessing, a small leak that makes scores optimistic. Pipelines in section 4 prevent it."},{"text":"Separately on the training and test data, so each part has mean 0.","why":"Then the same raw value maps to different scaled values in each part, and the model sees inconsistent inputs."}],"answer":0},{"q":"<p>Which model’s predictions change when you rescale <code dir=\"ltr\">total_spent</code> from dollars to cents?</p>","options":[{"text":"A decision tree, because its thresholds are in dollars.","why":"A tree simply learns thresholds in cents instead. The order of values, which is all a split uses, doesn’t change."},{"text":"k-nearest neighbours, because distances become dominated by <code dir=\"ltr\">total_spent</code>.","why":"Correct. Distance-based models treat a 100-fold larger range as 100 times more important."},{"text":"Ordinary linear regression, because its coefficient changes.","why":"The coefficient shrinks 100-fold to compensate, and predictions stay identical. Scale matters for regularised models, not plain least squares."}],"answer":1}],"exercise":{"kind":"python","starter":"import numpy as np\nfrom sklearn.datasets import load_diabetes\nfrom sklearn.model_selection import train_test_split\n\nX, y = load_diabetes(return_X_y=True, as_frame=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)\nbmi = X_train[\"bmi\"]\nx = ((bmi - bmi.mean()) / bmi.std(ddof=0)).to_numpy()   # standardised feature\ntarget = y_train.to_numpy()\n\nw, b = 0.0, 0.0\nlearning_rate = 0.05\nlosses = []\n\nfor step in range(200):\n    error = (w * x + b) - target\n    losses.append(np.mean(error ** 2))\n    # Update w and b here: step against the gradient of the mean squared error\n    pass\n","solution":"import numpy as np\nfrom sklearn.datasets import load_diabetes\nfrom sklearn.model_selection import train_test_split\n\nX, y = load_diabetes(return_X_y=True, as_frame=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)\nbmi = X_train[\"bmi\"]\nx = ((bmi - bmi.mean()) / bmi.std(ddof=0)).to_numpy()   # standardised feature\ntarget = y_train.to_numpy()\n\nw, b = 0.0, 0.0\nlearning_rate = 0.05\nlosses = []\n\nfor step in range(200):\n    error = (w * x + b) - target\n    losses.append(np.mean(error ** 2))\n    w -= learning_rate * 2 * np.mean(error * x)\n    b -= learning_rate * 2 * np.mean(error)\n\nprint(round(w, 2), round(b, 2), round(losses[-1], 1))\n","checks":[{"id":"losses","test":"len(losses) == 200 and all(float(losses[i + 1]) <= float(losses[i]) + 1e-9 for i in range(199))"},{"id":"loss-drops","test":"float(losses[-1]) < 0.2 * float(losses[0])"},{"id":"w","test":"abs(float(w) - 45.97) < 0.5"},{"id":"b","test":"abs(float(b) - 154.34) < 0.5"}],"title":"Train a line with gradient descent","prompt":"<p>Finish the gradient descent loop so it fits <code dir=\"ltr\">prediction = w * x + b</code> to the diabetes target, where <code dir=\"ltr\">x</code> is standardised BMI.</p>\n<p>Inside the loop, update <code dir=\"ltr\">w</code> and <code dir=\"ltr\">b</code> by subtracting <code dir=\"ltr\">learning_rate</code> times their gradients. For mean squared error the gradients are <code dir=\"ltr\">2 * mean(error * x)</code> for <code dir=\"ltr\">w</code> and <code dir=\"ltr\">2 * mean(error)</code> for <code dir=\"ltr\">b</code>. Keep the <code dir=\"ltr\">losses</code> list: the checks look at it.</p>\n","hints":["Compute both gradients from the same <code dir=\"ltr\">error</code> before changing either parameter.","<code dir=\"ltr\">w -= learning_rate * 2 * np.mean(error * x)</code>","Do the same for <code dir=\"ltr\">b</code> with <code dir=\"ltr\">np.mean(error)</code>. The final values should land near <code dir=\"ltr\">w ≈ 46</code> and <code dir=\"ltr\">b ≈ 154</code>."],"explanation":"<p>Each pass computes the error of the current line, turns it into a slope for each parameter, and steps against that slope. With a learning rate of 0.05 the loss falls every step and the parameters settle where <code dir=\"ltr\">LinearRegression</code> puts them (<code dir=\"ltr\">w ≈ 45.97</code>, <code dir=\"ltr\">b ≈ 154.34</code>). If you flip the sign (<code dir=\"ltr\">+=</code>), the loss grows every step: you’re walking uphill.</p>\n","checkLabels":{"losses":"The loss never increases from one step to the next","loss-drops":"The final loss is under 20% of the starting loss","w":"w converges to about 45.97","b":"b converges to about 154.34"}},"words":734,"readMinutes":3}