Skip to content

Gradient Descent

After this lesson, you will:

  • Understand how gradient descent (GD) finds loss function minima, and why moving opposite the gradient guarantees descent.
  • Express the parameter update rule θ←θ−η∇L(θ)\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta\nabla\mathcal{L}(\boldsymbol{\theta}) for MSE in vector form, and implement it from scratch using NumPy.
  • Explain why small learning rates η\eta cause slow convergence while excessive rates induce oscillations or divergence, characterized by stability threshold η<2/L\eta < 2/L; why feature scaling accelerates optimization, and how to define stopping criteria.
  • Distinguish batch GD, stochastic gradient descent (SGD), and mini-batch GD, define an epoch, and train models with scikit-learn’s SGDRegressor.
  • Linear Regression, in Machine Learning (Learned)
  • Calculus, in Math for AI(Not translated yet) (Learned)

Imagine standing on a mountain slope blanketed in dense fog, attempting to reach the valley floor. You cannot see the destination, but you can feel the slope of the ground beneath your boots. The intuitive strategy: take a short step downward in the direction of steepest descent, pause, assess the slope once more, and step again. When the terrain flattens completely beneath your feet, you have reached the valley bottom.

This describes the entire operational concept of gradient descent (GD). The “mountain” is the loss function surface, your “position” represents the model parameters, and local “slope” is the gradient.

While our earlier lesson solved parameters directly via the normal equation, many advanced models (such as logistic regression and deep neural networks) possess no closed-form analytical formulas. Furthermore, when feature dimension nn scales into thousands, solving linear systems with n+1n + 1 unknowns requires approximately O(n3)\mathcal{O}(n^3) operations. Gradient descent only requires computing local derivatives at the current parameter position. We apply it to our 10-apartment dataset to trace and verify every step numerically.

Rent by floor area for 10 apartments. The numbers are made up for the lesson, not market prices.
Floor area (m²)Rent (million VND/month)
256.7
306.5
368.8
428.8
489.4
5512.2
6012.0
6814.1
7513.9
8516.8

Begin with an uneducated initial baseline: w=0w = 0 and b=0b = 0, predicting zero rent for every apartment. The resulting MSE is 129.75. Unsure where the minimum lies, nudge each parameter slightly and inspect the change in MSE:

TrialMSE
Initial state: w=0w = 0, b=0b = 0129.75
Increase ww by 0.01117.42
Increase bb by 0.01129.53

Nudging either parameter downward reduces loss, confirming both should increase. However, incrementing ww slashes MSE by 12.33, whereas incrementing bb reduces loss by merely 0.22—a 50-fold difference. The physical intuition: increasing ww by 0.01 raises the predicted rent of an average 52 m² apartment by 0.52 million VND, whereas increasing bb by 0.01 lifts all predictions uniformly by only 0.01 million VND. Local derivatives indicate which direction to step, and with what relative urgency. This directional disparity causes severe optimization challenges addressed by feature scaling.

The objective of this lesson is: from an arbitrary starting parameter vector, iteratively measure gradients and take descent steps until reaching the global MSE minimum, tuning step sizes to converge rapidly without overshooting the valley.

The gradient: combining partial derivatives into a vector

Section titled “The gradient: combining partial derivatives into a vector”

As established earlier, concatenate parameters into column vector θ=[w,b]⊤\boldsymbol{\theta} = [w, b]^\top. The gradient of L\mathcal{L} at θ\boldsymbol{\theta} is the vector of all first-order partial derivatives, denoted ∇L(θ)\nabla\mathcal{L}(\boldsymbol{\theta}):

∇L(θ)=[∂L/∂w∂L/∂b]\nabla\mathcal{L}(\boldsymbol{\theta}) = \begin{bmatrix} \partial\mathcal{L}/\partial w \\ \partial\mathcal{L}/\partial b \end{bmatrix}

For nn features, the gradient contains n+1n + 1 elements. Our linear regression lesson derived the partial derivatives of MSE, where y^(i)=wx(i)+b\hat{y}^{(i)} = wx^{(i)} + b:

∂L∂w=2m∑i=1mx(i)(y^(i)−y(i))∂L∂b=2m∑i=1m(y^(i)−y(i))\begin{aligned} \frac{\partial\mathcal{L}}{\partial w} &= \frac{2}{m}\sum_{i=1}^{m}x^{(i)}\left(\hat{y}^{(i)} - y^{(i)}\right) \\ \frac{\partial\mathcal{L}}{\partial b} &= \frac{2}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right) \end{aligned}

Expressed via design matrix X\mathbf{X} (feature column followed by the bias column of ones): vector Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y} collects residual errors y^(i)−y(i)\hat{y}^{(i)} - y^{(i)}. Multiplying by X⊤\mathbf{X}^\top computes the respective weighted sums across both components. Multiplying by factor 2m\frac{2}{m} yields the exact gradient vector:

∇L(θ)=2mX⊤(Xθ−y)\nabla\mathcal{L}(\boldsymbol{\theta}) = \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right)

This matrix formulation holds for any arbitrary feature dimension nn. Setting the gradient to zero yields X⊤(Xθ−y)=0\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) = \mathbf{0}, which is the normal equation. The stationary termination point of gradient descent coincides with ordinary least squares.

At initial state (0,0)(0, 0), the gradient evaluates to [−1264.08,−21.84]⊤[-1264.08, -21.84]^\top. Scaling by step size 0.01 gives shifts of −12.64-12.64 and −0.22-0.22, matching our manual sensitivity checks.

Why moving opposite the gradient descends the loss

Section titled “Why moving opposite the gradient descends the loss”

Single-variable Taylor approximation. Inspected within an infinitesimal neighborhood of point ww, function L\mathcal{L} closely matches its linear tangent line with slope L′(w)\mathcal{L}'(w). For a small perturbation Δ\Delta:

L(w+Δ)≈L(w)+L′(w) Δ\mathcal{L}(w + \Delta) \approx \mathcal{L}(w) + \mathcal{L}'(w)\,\Delta

Choosing step displacement Δ=−η L′(w)\Delta = -\eta\,\mathcal{L}'(w) for small scalar η>0\eta > 0:

L(w+Δ)≈L(w)−η(L′(w))2\mathcal{L}(w + \Delta) \approx \mathcal{L}(w) - \eta\left(\mathcal{L}'(w)\right)^2

Because squared terms are non-negative, L\mathcal{L} strictly decreases (unless L′(w)=0\mathcal{L}'(w) = 0, indicating a flat stationary point). In words: if the function ascends to the right (L′>0\mathcal{L}' > 0), step left; if it descends to the right (L′<0\mathcal{L}' < 0), step right.

Multivariate generalization. Displace parameters by vector Δ=[Δw,Δb]⊤\boldsymbol{\Delta} = [\Delta_w, \Delta_b]^\top. The total first-order differential is the inner product of the gradient and displacement:

L(θ+Δ)≈L(θ)+∂L∂wΔw+∂L∂bΔb=L(θ)+∇L(θ)⊤Δ\begin{aligned} \mathcal{L}(\boldsymbol{\theta} + \boldsymbol{\Delta}) &\approx \mathcal{L}(\boldsymbol{\theta}) + \frac{\partial\mathcal{L}}{\partial w}\Delta_w + \frac{\partial\mathcal{L}}{\partial b}\Delta_b \\ &= \mathcal{L}(\boldsymbol{\theta}) + \nabla\mathcal{L}(\boldsymbol{\theta})^\top\boldsymbol{\Delta} \end{aligned}

Setting Δ=−η∇L(θ)\boldsymbol{\Delta} = -\eta\nabla\mathcal{L}(\boldsymbol{\theta}) transforms the inner product into −η∥∇L(θ)∥2-\eta\lVert\nabla\mathcal{L}(\boldsymbol{\theta})\rVert^2, which is strictly non-positive. Furthermore, among all candidate vectors of fixed Euclidean length, the inner product ∇L⊤Δ=∥∇L∥ ∥Δ∥cos⁡α\nabla\mathcal{L}^\top\boldsymbol{\Delta} = \lVert\nabla\mathcal{L}\rVert\,\lVert\boldsymbol{\Delta}\rVert\cos\alpha minimizes when angle α=180∘\alpha = 180^\circ. Thus, the gradient points along the direction of steepest ascent, and its negation points along steepest descent. On a topographic contour map connecting equal-loss coordinates, this descent vector is orthogonal to the local level curve.

The term “approximation” is vital: tangent approximations hold only locally, meaning excessively large steps can overshoot and cause loss to increase.

This foundational concept was formalized in 1847 by Augustin-Louis Cauchy, who solved astronomical orbit calculations by minimizing residual sums of squares through iterative coordinate updates opposite their partial derivatives.

Gradient descent iteratively applies a single vector update:

θt+1=θt−η∇L(θt)\boldsymbol{\theta}_{t+1} = \boldsymbol{\theta}_t - \eta\nabla\mathcal{L}(\boldsymbol{\theta}_t)

Here t=0,1,2,…t = 0, 1, 2, \dots indexes the iteration step, θ0\boldsymbol{\theta}_0 is the initial parameter vector (typically 0\mathbf{0}), and η>0\eta > 0 is the learning rate hyperparameter. Substituting the MSE gradient:

θt+1=θt−η⋅2mX⊤(Xθt−y)\boldsymbol{\theta}_{t+1} = \boldsymbol{\theta}_t - \eta \cdot \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta}_t - \mathbf{y}\right)

Because each step computes gradients across all mm training samples, this variant is called batch gradient descent. Two notable properties emerge:

  • Step lengths naturally shrink near the minimum. The step displacement is η∥∇L∥\eta\lVert\nabla\mathcal{L}\rVert. As parameters approach stationary points, gradients diminish, smoothly dampening steps without requiring manually scheduled decay.
  • MSE loss for linear regression is strictly convex. The loss surface forms an ideal paraboloid bowl containing a single global minimum, free from deceptive local minima. Neural networks lack this convexity guarantee.

Consider the canonical 1D quadratic function:

f(w)=L2(w−w∗)2+cf(w) = \frac{L}{2}\left(w - w^\ast\right)^2 + c

where w∗w^\ast is the minimizer, cc is minimum loss, and L>0L > 0 represents curvature (the second derivative f′′(w)f''(w) measuring valley steepness). Do not confuse curvature LL with loss L\mathcal{L}.

The first derivative is f′(w)=L(w−w∗)f'(w) = L\left(w - w^\ast\right). Substituting into update rule wt+1=wt−ηf′(wt)w_{t+1} = w_t - \eta f'(w_t) and subtracting w∗w^\ast from both sides:

wt+1−w∗=(wt−w∗)−ηL(wt−w∗)=(1−ηL)(wt−w∗)\begin{aligned} w_{t+1} - w^\ast &= \left(w_t - w^\ast\right) - \eta L\left(w_t - w^\ast\right) \\ &= \left(1 - \eta L\right)\left(w_t - w^\ast\right) \end{aligned}

At each step, parameter distance to the target is multiplied by constant factor q=1−ηLq = 1 - \eta L. After tt steps, error scales as qt(w0−w∗)q^t\left(w_0 - w^\ast\right). System behavior depends entirely on multiplier qq:

Learning rate η\etaContraction factor q=1−ηLq = 1 - \eta LOptimization behavior
0<η<1/L0 < \eta < 1/L0<q<10 < q < 1Monotonic convergence from one side. Smaller η\eta yields q→1q \to 1, leading to sluggish progress
η=1/L\eta = 1/Lq=0q = 0Exact convergence in a single step
1/L<η<2/L1/L < \eta < 2/L−1<q<0-1 < q < 0Alternating sign jumps across the minimum: oscillatory yet asymptotically convergent
η=2/L\eta = 2/Lq=−1q = -1Undamped oscillation between boundary states, failing to converge
η>2/L\eta > 2/Lq<−1q < -1Exponentially expanding step lengths: divergence

Evaluating against f(w)=w2f(w) = w^2 (where L=2L = 2, optimum w∗=0w^\ast = 0, stability bound 2/L=12/L = 1, starting from w0=1w_0 = 1):

η\etaqqSequence w0,w1,…,w5w_0, w_1, \dots, w_5
0.10.81, 0.8, 0.64, 0.512, 0.4096, 0.3277
0.501, 0, 0, 0, 0, 0
0.9−0.81, −0.8, 0.64, −0.512, 0.4096, −0.3277
1.1−1.21, −1.2, 1.44, −1.728, 2.0736, −2.4883

Compare rows η=0.1\eta = 0.1 and η=0.9\eta = 0.9: distance magnitudes are identical at every step, differing only in alternating signs. Taking 9-fold larger steps provides zero speed advantage.

Multivariate dynamics: anisotropic curvature

Section titled “Multivariate dynamics: anisotropic curvature”

The MSE surface over (w,b)(w, b) is a 2D paraboloid bowl with asymmetric curvature along different axes. Second-order partial derivatives form the Hessian matrix:

H=[2m∑i(x(i))22m∑ix(i)2m∑ix(i)2]=2mX⊤X\mathbf{H} = \begin{bmatrix} \frac{2}{m}\sum_i \left(x^{(i)}\right)^2 & \frac{2}{m}\sum_i x^{(i)} \\ \frac{2}{m}\sum_i x^{(i)} & 2 \end{bmatrix} = \frac{2}{m}\mathbf{X}^\top\mathbf{X}

Because H\mathbf{H} is independent of parameter state θ\boldsymbol{\theta}, bowl curvature is uniform throughout.

Any quadratic bowl decomposes into orthogonal principal directions (the eigenvectors of H\mathbf{H}), along which gradient descent behaves identically to decoupled 1D systems, governed by their respective curvatures (the eigenvalues of H\mathbf{H}). Let LL denote the maximal eigenvalue (steepest curvature), and μ\mu denote the minimal eigenvalue (flattest curvature). Distance to the minimum contracts by factors 1−ηL1 - \eta L and 1−ημ1 - \eta\mu along these respective axes:

  • The steepest axis determines stability. Convergence strictly requires η<2/L\eta < 2/L. Exceeding this threshold triggers explosive divergence along the steep coordinate.
  • The slowest contraction factor dictates convergence rate. When η≪2/L\eta \ll 2/L, the flat axis bottlenecks progress: 1−ημ≈11 - \eta\mu \approx 1 when μ\mu is small. Near the upper bound, 1−ηL≈−11 - \eta L \approx -1, creating undamped high-frequency oscillations across the steep valley walls.

The ratio κ=L/μ\kappa = L/\mu defines the condition number. High condition numbers indicate highly ill-conditioned, elongated elliptical ravines where no single scalar η\eta satisfies both axes effectively.

Evaluating raw apartment areas in square meters:

H=[6197.6104.8104.82],L≈6199.4,μ≈0.228\mathbf{H} = \begin{bmatrix} 6197.6 & 104.8 \\ 104.8 & 2 \end{bmatrix}, \qquad L \approx 6199.4, \quad \mu \approx 0.228

This produces condition number κ≈27 200\kappa \approx 27\,200. The steep axis aligns with ww while the flat axis aligns with bb: the loss surface forms an extremely long, narrow canyon. Steep walls restrict learning rates to η<2/L≈3.2×10−4\eta < 2/L \approx 3.2 \times 10^{-4}. At η=3×10−4\eta = 3 \times 10^{-4}, progress along the canyon floor contracts by 1−3×10−4×0.228≈0.999931 - 3 \times 10^{-4} \times 0.228 \approx 0.99993 per step: after 100 iterations, bb advances from 0 to merely 0.017 (against true target 2.04).

The solution is feature scaling: normalizing features to comparable unit ranges. We apply min-max scaling, mapping floor areas from [25,85][25, 85] m² into dimensionless interval [0,1][0, 1]:

x~=x−2585−25=x−2560\tilde{x} = \frac{x - 25}{85 - 25} = \frac{x - 25}{60}

Normalizing strictly the input feature preserves target scale yy and MSE comparability. The transformed model is y^=w~x~+b~\hat{y} = \tilde{w}\tilde{x} + \tilde{b}, where parameters carry clear intuitive physical interpretations:

  • b~\tilde{b} represents predicted rent for the smallest apartment (25 m²), where x~=0\tilde{x} = 0.
  • w~\tilde{w} represents rent differential between the largest (85 m²) and smallest unit, as x~\tilde{x} traverses 0 to 1.

Re-evaluating the Hessian on normalized feature matrix X\mathbf{X}:

H=[0.6130.9130.9132],L≈2.4533,μ≈0.160\mathbf{H} = \begin{bmatrix} 0.613 & 0.913 \\ 0.913 & 2 \end{bmatrix}, \qquad L \approx 2.4533, \quad \mu \approx 0.160

The stability bound expands to 2/L≈0.8152/L \approx 0.815, and the condition number drops from 27 200 down to κ≈15\kappa \approx 15—a well-conditioned ellipse with aspect ratio κ≈3.9\sqrt{\kappa} \approx 3.9.

Starting from (0,0)(0, 0) with η=0.3\eta = 0.3, the first step shifts parameters to (3.59,6.55)(3.59, 6.55), collapsing MSE from 129.75 down to 12.05. Gradient descent rapidly converges to (w~,b~)≈(10.17,6.28)(\tilde{w}, \tilde{b}) \approx (10.17, 6.28). Unpacking parameters back to physical units:

w~ x−2560+b~=w~60 x+(b~−25 w~60)⇒w=w~60,b=b~−25w\tilde{w}\,\frac{x - 25}{60} + \tilde{b} = \frac{\tilde{w}}{60}\,x + \left(\tilde{b} - 25\,\frac{\tilde{w}}{60}\right) \quad\Rightarrow\quad w = \frac{\tilde{w}}{60}, \qquad b = \tilde{b} - 25w

This yields w=10.17/60≈0.1695w = 10.17/60 \approx 0.1695 and b≈6.28−25×0.1695≈2.04b \approx 6.28 - 25 \times 0.1695 \approx 2.04, recovering the analytical solution.

Standardization via z-scores (StandardScaler, detailed in Ridge, Lasso, Elastic Net) standardizes columns to zero mean and unit variance. For single features, it produces circular level curves (κ=1\kappa = 1). Both transformers must derive scaling parameters strictly from training data.

Gradient descent requires programmatic stopping criteria:

  1. Maximum iteration count. An essential safeguard preventing runaway execution during numerical divergence.
  2. Gradient norm threshold: ∥∇L∥<ε\lVert\nabla\mathcal{L}\rVert < \varepsilon (e.g. 10−610^{-6}), confirming local terrain is sufficiently flat.
  3. Loss improvement threshold. Tracking relative change in loss ΔL\Delta\mathcal{L} across iterations. Scikit-learn implements this via parameter tol, which can trigger premature convergence along flat ravines.

On datasets with m=10m = 10 million instances, evaluating full-batch gradients at every step is computationally prohibitive. Stochastic approximations evaluate gradients over data subsets:

  • Stochastic gradient descent (SGD): computes gradients over a single randomly sampled instance at each step.
  • Mini-batch gradient descent: computes gradients across small cohorts of size BB: 2BXB⊤(XBθ−yB)\frac{2}{B}\mathbf{X}_B^\top\left(\mathbf{X}_B\boldsymbol{\theta} - \mathbf{y}_B\right). This constitutes the universal standard for deep neural network optimization.
  • Epoch: a complete pass over the entire training corpus, encompassing m/Bm/B gradient steps. Training data should be shuffled before each epoch.

Within a single epoch, SGD executes mm updates, whereas batch GD performs only one. However, SGD introduces stochastic gradient variance, causing parameters to fluctuate near the optimum under constant learning rates. Decaying learning rates over time (learning rate schedules) dampens these oscillations. Modern optimizers such as Momentum and Adam are explored in Advanced Optimization Algorithms.

Gradient descent requires careful learning rate tuning and feature standardization, guarantees global convergence only on convex surfaces, and is computationally less efficient than direct analytical solutions on small tabular datasets. It becomes indispensable when feature counts nn or dataset sizes mm scale beyond memory limits, or when models lack closed-form estimators (as in Logistic Regression and Artificial Neural Networks).

Explore optimization dynamics visually. The contour map plots MSE level curves over (w~,b~)(\tilde{w}, \tilde{b}), with the global minimum marked at × (10.17,6.28)(10.17, 6.28) and initial parameters at (0,0)(0, 0). Use Step for single updates, Run to simulate up to 100 iterations, and Reset to return to origin. The slider selects among five learning rate configurations. The secondary plot traces loss trajectories on logarithmic scales, with the dashed reference indicating optimal loss 0.3615. Stability bound evaluates to 2/L≈0.8152/L \approx 0.815. Try:

  1. At η=0.30\eta = 0.30: click Step. Loss progresses 129.75→12.05→3.5960129.75 \to 12.05 \to 3.5960. Initial steps descend steeply into the basin, after which trajectories pivot along the ravine floor, entering within 1% of optimal loss by step 69.
  2. At η=0.05\eta = 0.05: click Run. Step lengths contract gradually; after 100 iterations, loss remains stuck at 1.0023.
  3. At η=0.70\eta = 0.70: trajectories oscillate in a narrowing zigzag across the valley, entering the 1% error margin by step 29 (the fastest among all five presets).
  4. At η=0.80\eta = 0.80 and 0.850.85: near the boundary, updates bounce across the full ravine width, finishing 100 steps at 0.4240. Exceeding the threshold (η=0.85\eta = 0.85) triggers immediate divergence: loss expands 151.40→177.24→207.95→244.32151.40 \to 177.24 \to 207.95 \to 244.32, blowing off the viewport by step 4.
Each step goes against the gradient of the MSE. Pick a learning rate η, then press Step or Run. Floor area is scaled to [0, 1], so the axes are w̃ and b̃. For this data, 2/L ≈ 0.815.
  • MSE contour
  • Gradient descent path
  • Start (0, 0)
  • Current position
  • Bottom (normal equation)
Steps0 / 100
MSE129.75
(w̃, b̃)(0.00, 0.00)
In the original unitsŷ = 0.0000x + 0.00
  • Smallest MSE (0.3615)

Press Step for one step, or Run for up to 100 steps.

The contours are long, tilted ellipses around the bottom (10.17, 6.28), where the MSE is smallest, 0.3615. The path starts at (0, 0), where the MSE is 129.75.

The dashed line is the smallest MSE, 0.3615. Each mark on the MSE axis is 10 times the one below it.

The three cells below run independently; each constructs the apartment dataset locally.

The first cell implements batch gradient descent from scratch in NumPy: min-max normalizes features, starts from (0,0)(0, 0) with η=0.3\eta = 0.3, halts when ∥∇L∥<10−6\lVert\nabla\mathcal{L}\rVert < 10^{-6} or at 10 000 steps, and maps learned parameters back to physical units.

Batch gradient descent in NumPy

import numpy as np
# The 10-apartment table (made-up data for the lesson).
# area: floor area (m²), rent: monthly rent (million VND).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m = len(area)
# Min-max scaling: area onto [0, 1], rent left as it is.
lo, span = area.min(), area.max() - area.min() # 25 and 60
x = (area - lo) / span
X = np.column_stack([x, np.ones(m)]) # a column of ones for b
eta = 0.3 # learning rate
theta = np.zeros(2) # starting point: w~ = 0, b~ = 0
for step in range(10_000): # stopping criterion 1: a maximum number of steps
grad = 2 / m * X.T @ (X @ theta - rent) # gradient of the MSE
if np.linalg.norm(grad) < 1e-6: # stopping criterion 2: tiny gradient
break
theta = theta - eta * grad # the update rule
w_s, b_s = theta # w~, b~ on the scaled area
print(f"stopped after {step} steps")
print(f"scaled: w~ = {w_s:.4f}, b~ = {b_s:.4f}")
# Back to original units: w~ (x - 25) / 60 + b~ = w x + b.
w = w_s / span
b = b_s - lo * w
mse = np.mean((w * area + b - rent) ** 2)
print(f"GD: w = {w:.4f}, b = {b:.4f}, MSE = {mse:.4f}")
# Compare with the normal equation of the previous lesson, on the raw area.
A = np.column_stack([area, np.ones(m)])
w_ne, b_ne = np.linalg.solve(A.T @ A, A.T @ rent)
mse_ne = np.mean((w_ne * area + b_ne - rent) ** 2)
print(f"normal: w = {w_ne:.4f}, b = {b_ne:.4f}, MSE = {mse_ne:.4f}")

The first run downloads Python, about 10 MB.

The loop terminates after 282 iterations at w~=10.1686\tilde{w} = 10.1686 and b~=6.2763\tilde{b} = 6.2763, mapping to w=0.1695w = 0.1695, b=2.0394b = 2.0394, and MSE =0.3615= 0.3615, replicating the analytical normal equation. Loss enters within 1% of optimality by step 69; remaining iterations refine gradient norms below 10−610^{-6}.

The second cell runs 100 iterations across multiple learning rates, comparing unstandardized features (left) against normalized inputs (right), plotting log-scale loss curves.

Learning rates with and without feature scaling

import matplotlib.pyplot as plt
import numpy as np
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m = len(area)
x = (area - area.min()) / (area.max() - area.min()) # min-max
# Run GD from (0, 0). Return the MSE at steps 0, 1, ..., steps.
def mse_curve(feature, eta, steps):
X = np.column_stack([feature, np.ones(m)])
theta = np.zeros(2)
curve = [np.mean((X @ theta - rent) ** 2)]
for _ in range(steps):
theta = theta - eta * 2 / m * X.T @ (X @ theta - rent)
curve.append(np.mean((X @ theta - rent) ** 2))
return curve
runs = [("raw", area, [3e-4, 3.3e-4]), # left: raw area (m²)
("scaled", x, [0.05, 0.3, 0.7, 0.8, 0.85])] # right: min-max
fig, axes = plt.subplots(1, 2, figsize=(9, 3.6), sharey=True)
for ax, (name, feature, etas) in zip(axes, runs):
for eta in etas:
curve = mse_curve(feature, eta, 100)
ax.plot(curve, label=f"eta = {eta:g}")
v = curve[-1] # MSE after 100 steps; huge values print as 1.234e+05
text = f"{v:.4f}" if v < 1e4 else f"{v:.3e}"
print(f"{name}, eta = {eta:g}: {text}")
ax.axhline(0.3615, color="k", ls=":") # the smallest MSE
ax.set(title=name, xlabel="Step", yscale="log", ylim=(0.1, 1e3))
ax.legend()
axes[0].set_ylabel("MSE (log scale)")
fig.tight_layout()
long_run = mse_curve(area, 3e-4, 10_000) # raw area, 10,000 steps
print(f"raw, eta = 0.0003, 10000 steps: {long_run[-1]:.4f}")
plt.show()

The first run downloads Python, about 10 MB.

  • Normalized features: matches interactive visualization metrics. Notice η=0.8\eta = 0.8: loss curves appear smooth despite parameter coordinate oscillations. At η=0.85\eta = 0.85, loss explodes to 1.635×1091.635 \times 10^{9}.
  • Raw features: stability bound is 2/L≈3.226×10−42/L \approx 3.226 \times 10^{-4}. Setting η=3×10−4\eta = 3 \times 10^{-4} descends into the canyon and stalls at 0.8273, taking 35 650 iterations to enter the 1% target window. Setting η=3.3×10−4\eta = 3.3 \times 10^{-4} exceeds the stability bound and diverges to 9.987×1059.987 \times 10^{5}.

The third cell compares batch sizes using custom optimization loops (η=0.1\eta = 0.1, 100 epochs), and benchmarks against scikit-learn’s SGDRegressor.

SGD, mini-batch, and SGDRegressor

import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import MinMaxScaler
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
m, lo, span = len(area), area.min(), area.max() - area.min()
X = np.column_stack([(area - lo) / span, np.ones(m)]) # min-max, ones
def minibatch_gd(batch_size, eta=0.1, epochs=100, seed=0):
rng = np.random.default_rng(seed)
theta = np.zeros(2)
for _ in range(epochs): # each epoch: shuffle, cut into m // batch_size
for idx in np.array_split(rng.permutation(m), m // batch_size):
Xb, yb = X[idx], rent[idx]
theta = theta - eta * 2 / len(idx) * Xb.T @ (Xb @ theta - yb)
w = theta[0] / span # back to original units
return np.mean((X @ theta - rent) ** 2), w, theta[1] - lo * w
for size in [10, 2, 1]: # 10 = m: batch GD; 1: SGD
mse, w, b = minibatch_gd(size)
print(f"batch {size:2d}: MSE = {mse:.4f}, w = {w:.4f}, b = {b:.2f}")
A = area.reshape(-1, 1) # scikit-learn needs a 2-D table
sgd = SGDRegressor(random_state=0).fit(A, rent) # defaults, raw area
mse = np.mean((sgd.predict(A) - rent) ** 2)
print(f"default, raw area: MSE = {mse:.1e}, n_iter_ = {sgd.n_iter_}")
sgd = SGDRegressor(penalty=None, learning_rate="constant", eta0=0.05,
max_iter=1000, tol=None, random_state=0)
scaler = MinMaxScaler()
model = make_pipeline(scaler, sgd).fit(A, rent)
w = sgd.coef_[0] * scaler.scale_[0] # back to original units
b = sgd.intercept_[0] + sgd.coef_[0] * scaler.min_[0]
mse = np.mean((model.predict(A) - rent) ** 2)
print(f"min-max + constant: MSE = {mse:.4f}, w = {w:.4f}, b = {b:.2f}")

The first run downloads Python, about 10 MB.

Target parameters are w=0.1695w = 0.1695, b=2.04b = 2.04, and MSE 0.3615.

  • Batch 10 (Full batch GD) performs 100 updates, reaching MSE 0.4885 (incomplete convergence).
  • Batch 2 (Mini-batch) executes 500 updates, reaching closest to target: MSE 0.3632.
  • Batch 1 (Stochastic GD) executes 1 000 updates but finishes farther from the optimum (MSE 0.3766, b=2.15b = 2.15) due to stochastic oscillations around the minimum.

Default SGDRegressor() on raw features diverges to MSE 3.6×10243.6 \times 10^{24} within 6 epochs: default eta0=0.01 is far too large for unscaled square-meter coordinates. Our final configuration chains MinMaxScaler, disables default L2 penalties (penalty=None), sets constant learning rate 0.05, and sets tol=None to ensure 1000 full epochs. It recovers optimal MSE 0.3615.

Question 1 of 4 A 1D loss function is L(w)=2(w−3)2\mathcal{L}(w) = 2(w - 3)^2. You run gradient descent with η=0.4\eta = 0.4, starting from w=0w = 0. What happens?
Question 2 of 4 At parameter state θ=[w,b]⊤=[1,2]⊤\boldsymbol{\theta} = [w, b]^\top = [1, 2]^\top, the loss gradient is ∇L=[−4,2]⊤\nabla\mathcal{L} = [-4, 2]^\top. After one gradient descent update with learning rate η=0.1\eta = 0.1, what is the new value of θ\boldsymbol{\theta}?
Question 3 of 4 You run gradient descent on raw apartment area (m², unstandardized) with an η\eta that converges well. A colleague converts the area column to dm² (multiplying by 100) and keeps η\eta unchanged. What is most likely to happen?
Question 4 of 4 A training dataset contains m=1000m = 1000 instances. You train for 3 epochs using mini-batches of size 50. How many parameter updates take place?

Key takeaways:

  • Gradient descent iteratively updates θ←θ−η∇L(θ)\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta\nabla\mathcal{L}(\boldsymbol{\theta}), where ∇L=2mX⊤(Xθ−y)\nabla\mathcal{L} = \frac{2}{m}\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) for MSE, converging to the exact solution of the normal equation.
  • Convergence strictly requires 0<η<2/L0 < \eta < 2/L, where LL is the maximal eigenvalue of Hessian matrix 2mX⊤X\frac{2}{m}\mathbf{X}^\top\mathbf{X}. Learning rates below 1/L1/L converge monotonically; rates between 1/L1/L and 2/L2/L oscillate; rates above 2/L2/L diverge.
  • Min-max scaling x~=(x−25)/60\tilde{x} = (x - 25)/60 reduces condition number κ\kappa from 27 200 to 15, reducing steps required to enter the 1% target window from 35 650 down to 69 iterations. Learned parameters map back to original coordinates via w=w~/60w = \tilde{w}/60 and b=b~−25wb = \tilde{b} - 25w.
  • Full-batch GD computes gradients across all mm samples, mini-batch GD processes batches of size BB, and stochastic GD updates on single samples. A plateauing loss curve does not guarantee convergence along ill-conditioned ravines.

Further reading:

Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About