Machine Learning Algorithms

Ch.7: Gradient Descent - Intuition, Math, and Implementation

By Ayush Arora15 min read

Inspired by: YouTube

Ch.1 and Ch.2 derived the closed-form Ordinary Least Squares (OLS) solution for simple linear regression. Ch.3 and Ch.4 covered evaluation metrics and core diagnostic assumptions. Ch.5 and Ch.6 extended linear regression to kk features using matrix algebra, solving for parameters directly with the closed-form normal equation β^=(XTX)1XTy\hat{\beta} = (X^T X)^{-1} X^T y.

In this post, we introduce Gradient Descent, the foundational optimization algorithm that powers modern machine learning and deep learning. We examine why gradient descent is essential when closed-form solutions become computationally intractable, derive its partial derivatives step by step, animate its convergence live, build a custom estimator from scratch in Python, and analyze the impact of learning rate and epochs.

Why Gradient Descent Matters: Beyond Closed-Form Solutions

In the previous post on multiple linear regression math, we solved for parameter vector β\beta using the normal equation:

β^=(XTX)1XTy\hat{\beta} = (X^T X)^{-1} X^T y

While mathematically elegant, the normal equation suffers from a severe computational limitation: matrix inversion. For a dataset with nn rows and kk features, the matrix XTXX^T X has dimensions (k+1)×(k+1)(k+1) \times (k+1). Inverting a matrix of shape (k,k)(k, k) requires roughly O(k3)\mathcal{O}(k^3) operations (or O(k2.81)\mathcal{O}(k^{2.81}) using Strassen's algorithm).

When feature count kk is small (e.g., k=10k = 10 or k=100k = 100), matrix inversion completes in milliseconds. However, when working with high-dimensional datasets such as text embeddings, genomic data, or image pixels where kk reaches 100,000100{,}000 or 1,000,0001{,}000{,}000, computing (XTX)1(X^T X)^{-1} requires extreme computational time and exceeds available RAM.

Furthermore, closed-form solutions exist only for a small family of algorithms like linear regression. Non-linear models, logistic regression, Support Vector Machines, and Deep Neural Networks do not have closed-form solutions.

Gradient descent solves both problems. Instead of calculating optimal parameters in a single massive matrix calculation, gradient descent starts with arbitrary initial guesses and iteratively steps down the loss function surface until it converges to the optimal solution.

PropertyClosed-Form Normal EquationGradient Descent
Computation ModelAnalytical (single matrix inversion step)Iterative (repeated small update steps)
Time ComplexityO(k3)\mathcal{O}(k^3) w.r.t features kkO(kn)\mathcal{O}(k \cdot n) per epoch
Memory UsageHigh (stores (k,k)(k, k) matrix in memory)Low (processes data sequentially or in batches)
Scalability (k>100,000k > 100{,}000)Fails or slows dramaticallyScales efficiently to millions of features
GeneralitySpecific to Linear RegressionUniversal (Logistic Regression, Neural Networks, Deep Learning)

1D Intuition: Fixing Slope mm and Optimizing Intercept bb

To build intuition, consider simple linear regression y=mx+by = m x + b. Suppose slope mm is fixed to a known constant (e.g., m=27.83m = 27.83). The model must optimize a single parameter: intercept bb.

The objective is to minimize the Sum of Squared Errors (SSE) loss function L(b)L(b):

L(b)=i=1n(yi(mxi+b))2L(b) = \sum_{i=1}^n \left(y_i - (m x_i + b)\right)^2

Because L(b)L(b) is a quadratic function of bb, plotting L(b)L(b) against bb creates a 1D convex parabola with a single global minimum bb^*.

1D loss parabola L(b) showing negative slope on left requiring right step, positive slope on right requiring left step, and vertical translation of fitted regression line

Imagine standing on this loss parabola blindfolded. You cannot view the whole curve, but you can feel the local incline (the derivative or slope of the tangent line):

  1. If you start at b0=80b_0 = -80 (left side of minimum): The tangent slope dLdb\frac{\mathrm{d}L}{\mathrm{d}b} is negative. To move closer to the minimum bb^*, you must increase bb (step to the right).
  2. If you start at b0=70b_0 = 70 (right side of minimum): The tangent slope dLdb\frac{\mathrm{d}L}{\mathrm{d}b} is positive. To move closer to the minimum bb^*, you must decrease bb (step to the left).

This yields the fundamental update rule:

bnew=boldηdLdbb_{\text{new}} = b_{\text{old}} - \eta \cdot \frac{\mathrm{d}L}{\mathrm{d}b}

where η\eta (eta) is the learning rate, a small positive scalar (e.g., η=0.001\eta = 0.001). The minus sign automatically handles direction:


Batch Gradient Descent Derivation for mm and bb

Now consider the full simple linear regression problem where both slope mm and intercept bb are unknown.

The target model is y^i=mxi+b\hat{y}_i = m x_i + b. The total loss across all nn data points is:

L(m,b)=i=1n(yi(mxi+b))2L(m, b) = \sum_{i=1}^n \left(y_i - (m x_i + b)\right)^2

Because L(m,b)L(m, b) depends on two parameters, the loss function forms a 3D bowl-shaped surface (a paraboloid). To find the direction of steepest descent, we compute the gradient vector L(m,b)\nabla L(m, b), which contains partial derivatives with respect to each parameter.

That surface is the real cost function L(m,b)L(m, b) for this exact dataset, drag to rotate, scroll to zoom. The orange trail is the actual batch gradient descent path from the 60-epoch run below, stepping from the starting guess down the bowl to the global minimum.

Deriving the Partial Derivative w.r.t Intercept bb

Using the chain rule:

Lb=bi=1n(yimxib)2\frac{\partial L}{\partial b} = \frac{\partial}{\partial b} \sum_{i=1}^n \left(y_i - m x_i - b\right)^2

Bring the derivative inside the summation and apply the power rule:

Lb=i=1n2(yimxib)b(yimxib)\frac{\partial L}{\partial b} = \sum_{i=1}^n 2 \left(y_i - m x_i - b\right) \cdot \frac{\partial}{\partial b}\left(y_i - m x_i - b\right)

Since b(yimxib)=1\frac{\partial}{\partial b}(y_i - m x_i - b) = -1:

Lb=2i=1n(yi(mxi+b))=2i=1n(yiy^i)\frac{\partial L}{\partial b} = -2 \sum_{i=1}^n \left(y_i - (m x_i + b)\right) = -2 \sum_{i=1}^n (y_i - \hat{y}_i)

Deriving the Partial Derivative w.r.t Slope mm

Similarly, differentiate L(m,b)L(m, b) with respect to mm:

Lm=mi=1n(yimxib)2\frac{\partial L}{\partial m} = \frac{\partial}{\partial m} \sum_{i=1}^n \left(y_i - m x_i - b\right)^2

Apply the chain rule:

Lm=i=1n2(yimxib)m(yimxib)\frac{\partial L}{\partial m} = \sum_{i=1}^n 2 \left(y_i - m x_i - b\right) \cdot \frac{\partial}{\partial m}\left(y_i - m x_i - b\right)

Since m(yimxib)=xi\frac{\partial}{\partial m}(y_i - m x_i - b) = -x_i:

Lm=2i=1n(yi(mxi+b))xi=2i=1n(yiy^i)xi\frac{\partial L}{\partial m} = -2 \sum_{i=1}^n \left(y_i - (m x_i + b)\right) x_i = -2 \sum_{i=1}^n (y_i - \hat{y}_i) x_i

Simultaneous Parameter Update Rules

At each epoch kk, both parameters are updated simultaneously using their respective partial derivatives and learning rate η\eta:

b(k+1)=b(k)η(2i=1n(yiy^i))b^{(k+1)} = b^{(k)} - \eta \cdot \left( -2 \sum_{i=1}^n (y_i - \hat{y}_i) \right)

m(k+1)=m(k)η(2i=1n(yiy^i)xi)m^{(k+1)} = m^{(k)} - \eta \cdot \left( -2 \sum_{i=1}^n (y_i - \hat{y}_i) x_i \right)

Notice how step size is governed by gradient magnitude. Far from the minimum, residuals (yiy^i)(y_i - \hat{y}_i) and gradients are large, producing larger initial steps. As parameters approach optimal values (m,b)(m^*, b^*), gradients diminish toward zero, naturally slowing step sizes so the algorithm settles cleanly into the minimum.


Live Convergence Animation

The interactive component below demonstrates 60 epochs of batch gradient descent starting from initial parameters m0=100,b0=120m_0 = 100, b_0 = -120 with learning rate η=0.001\eta = 0.001. Watch the fitted line rotate and translate in real time as the cost curve drops smoothly toward zero.


Building GDRegressor from Scratch in Python

We validate our derivation by constructing a custom estimator class named GDRegressor, following the implementation structure in the source notebook gradient-descent-code-from-scratch.ipynb.

We generate a synthetic regression dataset using sklearn.datasets.make_regression with 100 samples, 1 feature, noise level 20, and random_state=13.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression
 
# Generate synthetic dataset matching source notebook
X, y = make_regression(
    n_samples=100, 
    n_features=1, 
    n_informative=1, 
    n_targets=1, 
    noise=20, 
    random_state=13
)
 
# Benchmark using scikit-learn's OLS LinearRegression
ols = LinearRegression()
ols.fit(X, y)
 
print(f"OLS Slope (m): {ols.coef_[0]:.2f}")
print(f"OLS Intercept (b): {ols.intercept_:.2f}")
# Output:
# OLS Slope (m): 27.83
# OLS Intercept (b): -2.29

Now we build the custom GDRegressor class implementing batch gradient descent:

class GDRegressor:
    def __init__(self, learning_rate=0.001, epochs=100):
        self.lr = learning_rate
        self.epochs = epochs
        # Initial guesses far from true values (m=27.83, b=-2.29)
        self.m = 100.0
        self.b = -120.0
 
    def fit(self, X, y):
        X_ravel = X.ravel()
        n_samples = len(y)
 
        for epoch in range(self.epochs):
            # Compute current predictions
            y_pred = self.m * X_ravel + self.b
            
            # Compute partial derivatives (Sum of Squared Errors)
            loss_slope_b = -2 * np.sum(y - y_pred)
            loss_slope_m = -2 * np.sum((y - y_pred) * X_ravel)
 
            # Update parameters simultaneously
            self.b = self.b - self.lr * loss_slope_b
            self.m = self.m - self.lr * loss_slope_m
 
    def predict(self, X):
        return self.m * X.ravel() + self.b

We instantiate GDRegressor, fit it on the exact same dataset, and print the converged parameters:

# Instantiate and fit custom estimator
gd = GDRegressor(learning_rate=0.001, epochs=100)
gd.fit(X, y)
 
print(f"GD Slope (m): {gd.m:.2f}")
print(f"GD Intercept (b): {gd.b:.2f}")
# Output:
# GD Slope (m): 27.83
# GD Intercept (b): -2.29

Our from-scratch gradient descent implementation converges to m=27.83m = 27.83 and b=2.29b = -2.29, matching scikit-learn's OLS values to two decimal places.


Conceptual Analysis: Learning Rate and Epochs

The performance and stability of gradient descent depend directly on two key hyperparameters: learning rate η\eta and number of epochs.

Impact of Learning Rate (η\eta)

The tool below fixes slope mm at its optimal value (the same 1D slice from earlier) and lets you drive gradient descent on bb by hand: drag the learning rate slider, hit Step to take one update, and watch where the orange marker lands on the loss curve.

Because this 1D slice is an exact parabola, the update has a closed form: b(k+1)b=(12ηn)(b(k)b)b^{(k+1)} - b^* = (1 - 2\eta n)(b^{(k)} - b^*), where n=100n = 100 is the sample count. That geometric recurrence converges only when 12ηn<1|1 - 2\eta n| < 1, i.e. η<1n=0.01\eta < \frac{1}{n} = 0.01 for this dataset:

  1. Learning Rate Too Small (η0.0005\eta \approx 0.0005): (12ηn)(1 - 2\eta n) sits close to 1, so each step shrinks the distance to bb^* by only a sliver. The algorithm needs hundreds of steps to make visible progress, crawling slowly down the loss curve. Safe, but wasteful.
  2. Optimal Learning Rate (η0.004\eta \approx 0.004): Steps are well-sized. Loss drops smoothly every step and settles at the minimum within a handful of clicks.
  3. Learning Rate Too Large (η0.01\eta \geq 0.01): 12ηn|1 - 2\eta n| crosses 1, so each step overshoots bb^* by a larger margin than the last. Instead of descending, bb bounces between opposite walls of the parabola with growing amplitude, and loss explodes toward infinity, exactly the "will never reach the minimum" case the tool warns about.

Impact of Epochs

An epoch represents one full iteration over the training dataset.


Why Feature Scaling Speeds Up Convergence

Picture the loss surface as a valley, and gradient descent as walking downhill toward its lowest point. If every feature is on a similar scale, that valley is a nice round bowl, and a single step size works equally well no matter which direction you're walking. But if one feature ranges from 0 to 1 and another ranges from 0 to 100,000, the bowl gets stretched into a long, narrow canyon: steep, cliff-like walls along the large-scale feature's axis, and an almost flat, gentle slope along the small-scale one.

Gradient descent only has one step size (the learning rate η\eta), and it has to use that same step size in every direction at once. To avoid overshooting and bouncing off the steep canyon walls, η\eta has to be kept small, but that same small η\eta makes it crawl along the gentle slope. So instead of walking straight down to the bottom, it zig-zags back and forth across the narrow direction while barely creeping forward along the wide one, taking far more steps than it should to actually get anywhere.

Formally: for MSE-style loss, the curvature of LL along a weight's axis scales with that feature's variance, since the Hessian is 2XTX2 X^T X, and the diagonal entry for feature jj is approximately 2nσj22n \cdot \sigma_j^2. If one feature's standard deviation is 5×5\times another's, its axis carries roughly 25×25\times more curvature, which is exactly the steep-canyon-vs-gentle-slope mismatch described above, just measured precisely instead of by eye.

Both panels above run real gradient descent for 40 steps from the same starting point, on quadratic bowls L(w1,w2)=aw12+bw22L(w_1, w_2) = a w_1^2 + b w_2^2 that model this curvature relationship directly. With a:b=25:1a{:}b = 25{:}1 (mimicking a 5:15{:}1 feature standard-deviation ratio), the largest stable learning rate is dictated by aa, and w2w_2 is still at 0.2740.274 after 40 steps while w1w_1 has already oscillated its way to convergence. Standardizing so both features have unit variance (a=b=1a = b = 1) makes the bowl circular: the same 40-step budget with a much larger learning rate lands within 101610^{-16} of the minimum, no zig-zag required.

This is why StandardScaler (subtracting the mean, dividing by the standard deviation) is standard practice before gradient-based training, closed-form OLS doesn't care about feature scale since it solves for the minimum directly, but gradient descent's convergence speed depends heavily on it.


Non-Convex Loss Surfaces and the Saddle Point Problem

Every loss surface in this post so far has been a convex bowl (a paraboloid) with exactly one minimum, which is guaranteed for linear regression's squared-error loss: no matter where gradient descent starts, it eventually reaches the same global optimum. Logistic regression, neural networks, and most other models people apply gradient descent to do not get this guarantee. Their loss surfaces can be non-convex: full of dips, ridges, and flat regions where the same algorithm behaves very differently depending on where it starts.

A minimal function that captures this is f(x,y)=x42x2+y2f(x, y) = x^4 - 2x^2 + y^2. Setting both partial derivatives to zero, fx=4x34x=4x(x21)=0\frac{\partial f}{\partial x} = 4x^3 - 4x = 4x(x^2 - 1) = 0 and fy=2y=0\frac{\partial f}{\partial y} = 2y = 0, gives three critical points: (1,0)(-1, 0), (0,0)(0, 0), and (1,0)(1, 0). Checking the second derivative along xx, 2fx2=12x24\frac{\partial^2 f}{\partial x^2} = 12x^2 - 4, shows (±1,0)(\pm 1, 0) are curved upward (+8+8, a local minimum along xx) while (0,0)(0, 0) is curved downward (4-4, a local maximum along xx). Combined with y2y^2 always curving upward along yy, the origin is concave in one direction and convex in the other: a saddle point, not a minimum, even though its gradient is exactly zero there.

The plot runs real batch gradient descent (learning rate 0.050.05, analytic gradients, no approximation) from three starting points on this exact surface. Paths starting clearly to one side of the saddle converge quickly, reaching within 10410^{-4} of a global minimum in around 15 steps. The path starting almost directly above the saddle at (0.01,1.0)(0.01, 1.0) behaves very differently: xx crawls from 0.010.01 to only 0.060.06 over the first 10 steps, because the gradient 4x(x21)4x(x^2-1) is nearly zero when xx is near zero. It takes roughly 30 steps of this near-flat plateau before xx escapes and accelerates into the same basin the direct paths reached in half the time.

This is the practical saddle point problem: near a saddle, the gradient magnitude shrinks toward zero just like it does near a true minimum, so a plain fixed-step gradient descent update slows to a crawl without actually being close to done. In low dimensions like this 2D example, the descent does eventually escape (a random or off-axis start almost never lands exactly on the unstable direction). In the very high-dimensional loss landscapes typical of neural networks, saddle points vastly outnumber true local minima, which is part of why optimizers used in practice (momentum, Adam, and similar) add mechanisms specifically to accelerate through these flat, low-gradient regions rather than relying on plain gradient descent alone.


Summary & Key Takeaways

  1. Why Gradient Descent: Matrix inversion in the OLS normal equation scales as O(k3)\mathcal{O}(k^3), making it computationally intractable for large feature counts. Gradient descent scales as O(kn)\mathcal{O}(k \cdot n) per epoch and generalizes to non-linear models and deep learning.
  2. Direction via Derivative: The negative gradient direction L-\nabla L always points toward steepest loss reduction. The minus sign in pnew=poldηLpp_{\text{new}} = p_{\text{old}} - \eta \cdot \frac{\partial L}{\partial p} ensures updates move opposite to the gradient.
  3. Partial Derivatives: For SSE loss L(m,b)=(yi(mxi+b))2L(m, b) = \sum (y_i - (m x_i + b))^2, partial derivatives are Lb=2(yiy^i)\frac{\partial L}{\partial b} = -2 \sum (y_i - \hat{y}_i) and Lm=2(yiy^i)xi\frac{\partial L}{\partial m} = -2 \sum (y_i - \hat{y}_i) x_i.
  4. Hyperparameter Tuning: Learning rate η\eta controls step size. Too small leads to slow convergence; too large causes oscillation and divergence.
  5. Exact OLS Match: Our custom GDRegressor built from scratch converges to m=27.83,b=2.29m = 27.83, b = -2.29 on synthetic data, matching scikit-learn's LinearRegression benchmark.
  6. Feature Scaling: Loss curvature scales with feature variance, so unscaled features stretch the loss surface into thin ellipses. A single global learning rate then zig-zags on the steep axis while crawling on the shallow one; standardizing features to unit variance makes the surface circular and convergence direct.
  7. Non-Convexity and Saddle Points: Linear regression's loss is convex with one guaranteed global minimum, but models like neural networks have non-convex loss surfaces with local minima and saddle points, flat regions with zero gradient that aren't minima, where plain gradient descent slows to a crawl before escaping.

In the next post, we explore variants of gradient descent (Batch, Stochastic, and Mini-Batch Gradient Descent) to optimize performance on massive datasets.