Ch.7: Gradient Descent - Intuition, Math, and Implementation
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 features using matrix algebra, solving for parameters directly with the closed-form normal equation .
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 using the normal equation:
While mathematically elegant, the normal equation suffers from a severe computational limitation: matrix inversion. For a dataset with rows and features, the matrix has dimensions . Inverting a matrix of shape requires roughly operations (or using Strassen's algorithm).
When feature count is small (e.g., or ), matrix inversion completes in milliseconds. However, when working with high-dimensional datasets such as text embeddings, genomic data, or image pixels where reaches or , computing 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.
| Property | Closed-Form Normal Equation | Gradient Descent |
|---|---|---|
| Computation Model | Analytical (single matrix inversion step) | Iterative (repeated small update steps) |
| Time Complexity | w.r.t features | per epoch |
| Memory Usage | High (stores matrix in memory) | Low (processes data sequentially or in batches) |
| Scalability () | Fails or slows dramatically | Scales efficiently to millions of features |
| Generality | Specific to Linear Regression | Universal (Logistic Regression, Neural Networks, Deep Learning) |
1D Intuition: Fixing Slope and Optimizing Intercept
To build intuition, consider simple linear regression . Suppose slope is fixed to a known constant (e.g., ). The model must optimize a single parameter: intercept .
The objective is to minimize the Sum of Squared Errors (SSE) loss function :
Because is a quadratic function of , plotting against creates a 1D convex parabola with a single global minimum .
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):
- If you start at (left side of minimum): The tangent slope is negative. To move closer to the minimum , you must increase (step to the right).
- If you start at (right side of minimum): The tangent slope is positive. To move closer to the minimum , you must decrease (step to the left).
This yields the fundamental update rule:
where (eta) is the learning rate, a small positive scalar (e.g., ). The minus sign automatically handles direction:
- When slope is negative (), subtracting a negative quantity adds to , moving rightward.
- When slope is positive (), subtracting a positive quantity reduces , moving leftward.
Batch Gradient Descent Derivation for and
Now consider the full simple linear regression problem where both slope and intercept are unknown.
The target model is . The total loss across all data points is:
Because 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 , which contains partial derivatives with respect to each parameter.
That surface is the real cost function 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
Using the chain rule:
Bring the derivative inside the summation and apply the power rule:
Since :
Deriving the Partial Derivative w.r.t Slope
Similarly, differentiate with respect to :
Apply the chain rule:
Since :
Simultaneous Parameter Update Rules
At each epoch , both parameters are updated simultaneously using their respective partial derivatives and learning rate :
Notice how step size is governed by gradient magnitude. Far from the minimum, residuals and gradients are large, producing larger initial steps. As parameters approach optimal values , 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 with learning rate . 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.29Now 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.bWe 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.29Our from-scratch gradient descent implementation converges to and , 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 and number of epochs.
Impact of Learning Rate ()
The tool below fixes slope at its optimal value (the same 1D slice from earlier) and lets you drive gradient descent on 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: , where is the sample count. That geometric recurrence converges only when , i.e. for this dataset:
- Learning Rate Too Small (): sits close to 1, so each step shrinks the distance to by only a sliver. The algorithm needs hundreds of steps to make visible progress, crawling slowly down the loss curve. Safe, but wasteful.
- Optimal Learning Rate (): Steps are well-sized. Loss drops smoothly every step and settles at the minimum within a handful of clicks.
- Learning Rate Too Large (): crosses 1, so each step overshoots by a larger margin than the last. Instead of descending, 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.
- If epochs are too low (e.g., 5 epochs), training stops prematurely before parameters reach the minimum (underfitting).
- As epochs increase, parameter changes and shrink toward zero.
- Practical stopping criteria include setting a fixed max epoch budget or monitoring convergence: stopping early when (where ).
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 ), and it has to use that same step size in every direction at once. To avoid overshooting and bouncing off the steep canyon walls, has to be kept small, but that same small 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 along a weight's axis scales with that feature's variance, since the Hessian is , and the diagonal entry for feature is approximately . If one feature's standard deviation is another's, its axis carries roughly 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 that model this curvature relationship directly. With (mimicking a feature standard-deviation ratio), the largest stable learning rate is dictated by , and is still at after 40 steps while has already oscillated its way to convergence. Standardizing so both features have unit variance () makes the bowl circular: the same 40-step budget with a much larger learning rate lands within 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 . Setting both partial derivatives to zero, and , gives three critical points: , , and . Checking the second derivative along , , shows are curved upward (, a local minimum along ) while is curved downward (, a local maximum along ). Combined with always curving upward along , 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 , 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 of a global minimum in around 15 steps. The path starting almost directly above the saddle at behaves very differently: crawls from to only over the first 10 steps, because the gradient is nearly zero when is near zero. It takes roughly 30 steps of this near-flat plateau before 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
- Why Gradient Descent: Matrix inversion in the OLS normal equation scales as , making it computationally intractable for large feature counts. Gradient descent scales as per epoch and generalizes to non-linear models and deep learning.
- Direction via Derivative: The negative gradient direction always points toward steepest loss reduction. The minus sign in ensures updates move opposite to the gradient.
- Partial Derivatives: For SSE loss , partial derivatives are and .
- Hyperparameter Tuning: Learning rate controls step size. Too small leads to slow convergence; too large causes oscillation and divergence.
- Exact OLS Match: Our custom
GDRegressorbuilt from scratch converges to on synthetic data, matching scikit-learn'sLinearRegressionbenchmark. - 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.
- 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.
