Skip to main content

Why Every AI Model is Built on Gradient Descent

Why Every AI Model is Built on Gradient Descent

Topic Overview

Welcome. Today, we are going to learn about the most important algorithm in modern artificial intelligence.

Gradient Descent is an iterative optimization algorithm used to find the minimum of a differentiable function.

Why does it exist? In machine learning, we define our success as a mathematical function—called a loss function. We want to find the specific configuration of our model’s parameters that makes this number as small as possible. If we cannot find this minimum, our models are useless.

What problem does it solve? Analytical solutions—setting the derivative to zero and solving for the parameters—fail catastrophically when you have millions or billions of parameters. The equations become mathematically intractable. Gradient descent solves this by taking a series of small, computationally cheap steps calculated from local information (the slope) to eventually arrive at the bottom.

Why should you care? If gradient descent did not exist, GPT-4, ResNet, and every autonomous driving system would literally not exist. Every single weight in every single neural network you will ever build in your career is updated using a direct descendant of this exact algorithm. You are not just learning a formula today; you are learning the engine of the entire field.


Prerequisites

Before we begin, ensure you have internalized these two concepts:

  1. Gradients
    • Why it is needed: Gradient descent requires a compass. The gradient is that compass. It is a vector pointing in the direction of the steepest ascent.
    • Where it appears: It is the core term \(\nabla_\theta \mathcal{L}(\theta_t)\) in our update equation. Without it, we wouldn’t know which way to step.
  2. Loss Functions
    • Why it is needed: Gradient descent needs a landscape to traverse. The loss function defines this landscape by mapping every possible set of model parameters to a scalar error value.
    • Where it appears: It is the \(\mathcal{L}(\theta_t)\) in our equation. It tells us how “high up” we currently are on the error mountain.

Historical Motivation

Imagine it is the 1940s and 1950s. Researchers like Augustin Cauchy (who conceptualized it much earlier) and later Haskell Curry were trying to solve non-linear systems of equations.

The standard approach at the time was Newton’s Method. Newton’s method looks at the curvature of the function (the second derivative, or Hessian matrix) to jump straight to the minimum. Here was the engineering failure: To compute the Hessian for a model with \(N\) parameters, you must store and invert an \(N \times N\) matrix. If you have a modest neural network with 1 million parameters, that matrix has 1 trillion entries. In 1950, computers had kilobytes of memory. Today, even with gigabytes of GPU memory, inverting a trillion-entry matrix every single training step is computationally impossible.

Researchers desperately needed a method that relied only on first-order information (the gradient, which costs \(O(N)\) to compute) and avoided second-order information entirely. Gradient descent was the answer. It traded the “perfect jump” of Newton’s method for a “guaranteed downhill step” that scales linearly with parameters.


Intuitive Explanation

I want you to imagine you are blindfolded at the top of a rugged mountain. Your goal is to reach the very bottom of the valley. You cannot see the valley. You can only feel the ground immediately beneath your feet.

How do you get down? You feel the slope of the ground under your shoes. You feel around in a 360-degree circle to find the exact direction where the ground slopes steepest downward. You take a step in that direction. Then, you stop, feel the ground again, find the new steepest downward direction, and take another step.

Let’s translate this physically: * The mountain is your Loss Function. * Your exact coordinates on the mountain are your Parameters (\(\theta\)). * The sense of feeling the slope under your feet is computing the Gradient (\(\nabla \mathcal{L}\)). * The length of your step is the Learning Rate (\(\eta\)).

Why do we step opposite the gradient? Because the mathematical gradient points in the direction of steepest ascent (up the mountain). To go down, we must subtract it.

What would happen if you didn’t have this algorithm? You would be blindfolded on the mountain, stepping in random directions. You might wander forever, or worse, walk to the top of a nearby peak.


Visual Understanding

If you were to draw this on a whiteboard right now, draw a 2D topographical map (contour lines) representing a loss landscape.

  1. The Axes: The X and Y axes represent two parameters, \(\theta_1\) and \(\theta_2\).
  2. The Contours: Draw concentric, elongated ovals. The center of the innermost oval is labeled \(\mathcal{L}^*\) (the global minimum).
  3. The Path: Draw a zig-zagging line starting from the outside, moving inward.
    • Each straight segment of the line is one “step” of gradient descent.
    • Notice that the line does not point directly at the center. It points perpendicular to the contour line it is currently touching. This is a crucial geometric insight: the gradient is always orthogonal (90 degrees) to the level set of the function.
  4. Learning Rate Visualization:
    • Draw a path where the steps are tiny. The path looks smooth but takes forever to reach the center. (Learning rate too small).
    • Draw a path where the steps are massive. The line bounces back and forth between the outer walls, never getting closer to the center, or completely flying off the page. (Learning rate too large).

Mathematical Foundations

Let’s formalize our blindfolded walk.

Suppose you are at point \(\theta_t\). You want to move to a new point \(\theta_{t+1} = \theta_t + \Delta\theta\). You want to choose \(\Delta\theta\) such that your loss decreases: \(\mathcal{L}(\theta_{t+1}) < \mathcal{L}(\theta_t)\).

Let’s look at the first-order Taylor expansion of the loss function around \(\theta_t\): \[ \mathcal{L}(\theta_t + \Delta\theta) \approx \mathcal{L}(\theta_t) + \nabla_\theta \mathcal{L}(\theta_t)^T \Delta\theta \]

We want the change in loss, \(\Delta \mathcal{L} = \mathcal{L}(\theta_t + \Delta\theta) - \mathcal{L}(\theta_t)\), to be negative. So: \[ \nabla_\theta \mathcal{L}(\theta_t)^T \Delta\theta < 0 \]

This is a dot product between two vectors: the gradient \(\nabla_\theta \mathcal{L}\) and our step \(\Delta\theta\). When is the dot product of two vectors most negative? When they point in exact opposite directions.

Therefore, we set our step to be a scaled version of the negative gradient: \[ \Delta\theta = -\eta \nabla_\theta \mathcal{L}(\theta_t) \]

Where \(\eta > 0\) is a scalar controlling the step size. This gives us the Master Equation: \[ \theta_{t+1} = \theta_t - \eta \nabla_\theta \mathcal{L}(\theta_t) \]

Convergence Constraint: Why can’t we just set \(\eta = 1,000,000\) and jump to the bottom? Because the Taylor expansion is only accurate locally. If the function has high curvature (changes direction quickly), a massive step will overshoot. For a convex function with a Lipschitz continuous gradient (meaning the gradient cannot change faster than a rate \(L\)), it is mathematically proven that if \(\eta > 1/L\), the algorithm diverges. If \(\eta \leq 1/L\), it is guaranteed to converge, and the error decreases at a rate of \(O(1/t)\): \[ \mathcal{L}(\theta_t) - \mathcal{L}^* \leq \frac{\|\theta_0 - \theta^*\|^2}{2\eta t} \]


Worked Numerical Example

Let’s explicitly compute this. No skipping steps.

Setup: Let our loss function be a simple parabola in 1D: \(\mathcal{L}(\theta) = \theta^2 + 2\theta + 1\) (which is \((\theta+1)^2\)). The true minimum is at \(\theta^* = -1\), where \(\mathcal{L}^* = 0\). The gradient is the derivative: \(\nabla_\theta \mathcal{L}(\theta) = 2\theta + 2\). Let our starting point be \(\theta_0 = 3\). Let our learning rate be \(\eta = 0.1\).

Step 0 to Step 1: 1. Calculate gradient at \(\theta_0 = 3\): \(\nabla \mathcal{L}(3) = 2(3) + 2 = 6 + 2 = 8\) 2. Calculate the step: \(\Delta\theta = -\eta \nabla \mathcal{L}(3) = -0.1 \times 8 = -0.8\) 3. Update parameter: \(\theta_1 = \theta_0 + \Delta\theta = 3 - 0.8 = 2.2\) 4. Verify loss decreased: \(\mathcal{L}(3) = 3^2 + 2(3) + 1 = 16\) \(\mathcal{L}(2.2) = 2.2^2 + 2(2.2) + 1 = 4.84 + 4.4 + 1 = 10.24\) (Loss went from 16 to 10.24. It worked.)

Step 1 to Step 2: 1. Calculate gradient at \(\theta_1 = 2.2\): \(\nabla \mathcal{L}(2.2) = 2(2.2) + 2 = 4.4 + 2 = 6.4\) 2. Calculate the step: \(\Delta\theta = -0.1 \times 6.4 = -0.64\) 3. Update parameter: \(\theta_2 = 2.2 - 0.64 = 1.56\) 4. Verify loss decreased: \(\mathcal{L}(1.56) = 1.56^2 + 2(1.56) + 1 = 2.4336 + 3.12 + 1 = 6.5536\)

Notice how the gradient (8, then 6.4) is shrinking as we get closer to the minimum (-1), causing the steps to become smaller automatically. This is the fundamental behavior of gradient descent on convex functions.


Computational Interpretation

When you type loss.backward() in PyTorch, what is the computer actually doing?

Forget math for a second. We are moving memory. * Data Structure: Your parameters \(\theta\) are stored as a contiguous 1D array of floating-point numbers (e.g., FP32) in GPU VRAM. Let’s say you have a 10-million parameter model. That is 10 million floats. * The Gradient: The computer allocates another 10 million floats to store \(\nabla \mathcal{L}\). * The Computation: The update step \(\theta_{t+1} = \theta_t - \eta \nabla \mathcal{L}\) is a vectorized element-wise operation. It is a Fused Multiply-Add (FMA) instruction executed in parallel across thousands of GPU CUDA cores. * Complexity: The computational complexity of the update step is exactly \(O(N)\), where \(N\) is the number of parameters. It takes microseconds. * The Bottleneck: The update step is never the bottleneck. The bottleneck is computing \(\nabla \mathcal{L}\) in the first place (the backward() pass), which requires traversing the entire computational graph of the neural network, typically at \(O(N)\) to \(O(N^2)\) depending on the architecture (like attention).

Memory Cost: Standard gradient descent requires storing two arrays of size \(N\): the parameters and their gradients. If you use 32-bit floats, a 1-billion parameter model requires 8 GB of VRAM just to hold the weights and gradients for the update step.


Implementation Perspective

Let’s look at how this manifests in code.

1. NumPy (From Scratch)

This shows you exactly what the GPU is doing under the hood.

import numpy as np

# Assume theta is our parameter vector (e.g., shape: [1000])
theta = np.random.randn(1000) 
lr = 0.01 # This is eta

# 1. Compute the gradient (using some hypothetical function)
# grad has the exact same shape as theta: [1000]
grad = compute_gradient(theta) 

# 2. The Gradient Descent Update
# theta -= lr * grad is executing theta = theta - (lr * grad)
# NumPy broadcasts the scalar lr across the entire grad vector,
# then does element-wise subtraction.
theta -= lr * grad 

2. PyTorch (Production Grade)

In PyTorch, gradient descent is bundled into an Optimizer object.

import torch
import torch.optim as optim

# A simple model. model.parameters() is a generator yielding every 
# weight tensor (e.g., Linear layer bias of shape [64], weight of shape [64, 128]).
model = MyNeuralNetwork()
criterion = torch.nn.MSELoss()

# We wrap the parameters in the SGD optimizer.
# The optimizer literally just holds pointers to the tensor memory locations.
optimizer = optim.SGD(model.parameters(), lr=0.01)

# --- THE TRAINING LOOP ---
x, y = get_batch() # x shape: [B, features], y shape: [B, targets]

# Step 1: Zero out the gradients from the PREVIOUS step.
# Why? PyTorch ACCUMULATES gradients by default. If you don't zero them, 
# your new gradient is added to the old one.
optimizer.zero_grad()

# Step 2: Forward pass + Loss calculation
predictions = model(x) # Shape: [B, targets]
loss = criterion(predictions, y) # Scalar value

# Step 3: Backward pass (Compute the gradient)
# This traverses the graph backwards and populates the .grad attribute 
# of EVERY parameter tensor in model.parameters().
loss.backward()

# Step 4: The Gradient Descent Step
# Under the hood in C++, PyTorch iterates over model.parameters() 
# and executes: param.data -= lr * param.grad
optimizer.step()

Where It Appears In AI

Gradient descent is not a component; it is the fabric holding all components together. Here is exactly how it touches every part of an AI system:

AI Component Exact Usage of Gradient Descent
Linear Layer Updates the \(W\) matrix (\(d_{out} \times d_{in}\)) and \(b\) vector (\(d_{out}\)) by subtracting \(\eta\) times the gradient of the loss w.r.t \(W\) and \(b\).
CNN Updates the 4D kernel tensors (\(C_{out} \times C_{in} \times K \times K\)). It shifts the numerical values of these kernels so they activate strongly for useful features (edges, textures).
Transformer Updates the \(W_Q, W_K, W_V, W_O\) matrices. It is the sole mechanism by which the model learns what to attend to. Without GD, attention weights are random.
Embedding Layer Updates the lookup table matrix (\(V \times D\)). GD moves high-dimensional word vectors so that synonyms cluster together.
LayerNorm / BatchNorm Updates the scale (\(\gamma\)) and shift (\(\beta\)) parameters. GD learns how much to normalize and how much to scale back up to prevent vanishing gradients.
Diffusion Models Updates the U-Net parameters. The loss is the difference between predicted noise and actual noise. GD nudges the U-Net to become a better noise-remover.
Reinforcement Learning Updates the policy network. The “loss” is the negative expected reward. GD moves the policy parameters to make high-reward actions more probable.

Deep Dive Into Real Models

Let’s trace the exact tensor shape of a gradient descent step in a modern Transformer block.

Suppose we have an input batch \(X\) of shape [32, 128, 768] (Batch size 32, Sequence length 128, Hidden dimension 768). We have a Query weight matrix \(W_Q\) of shape [768, 768].

  1. We compute attention: \(Q = X W_Q\). Shape of \(Q\) is [32, 128, 768].
  2. We compute the loss (e.g., Cross Entropy). It is a single scalar.
  3. We call loss.backward().
  4. PyTorch uses the chain rule: \(\frac{\partial \mathcal{L}}{\partial W_Q} = X^T \frac{\partial \mathcal{L}}{\partial Q}\).
  5. The resulting gradient tensor \(\nabla W_Q\) has the exact same shape as \(W_Q\): [768, 768]. It contains 589,824 floating point numbers.
  6. optimizer.step() executes: \(W_Q := W_Q - 0.0001 \times \nabla W_Q\).
  7. This updates all 589,824 values simultaneously. This happens for dozens of such matrices in a single layer, across dozens of layers, thousands of times per second on an A100 GPU.

Failure Modes

When gradient descent fails, it fails in specific, diagnosable ways.

Issue Cause Symptoms Solution
Divergence (NaNs) \(\eta\) is too large; step overshoots the minimum and lands on a steeper slope, causing the next step to be even larger. Loss suddenly goes from 2.5 to nan. Weights become inf. Reduce learning rate (e.g., divide by 10). Use gradient clipping.
Vanishing Gradients Gradients multiply through many layers (or sigmoids), shrinking to zero. Weights in early layers do not change (param.grad == 0). Loss plateaus immediately. Use ReLU activations, proper weight initialization (Xavier/He), or different architectures.
Slow Convergence \(\eta\) is too small. Loss decreases, but at a glacial pace (e.g., drops by 0.001 per epoch). Training takes weeks. Increase learning rate. Use learning rate warmup.
Oscillation in Ravines The loss landscape is a long, narrow canyon (common in ML). The gradient points at the wall, not the bottom. Loss drops, then spikes, drops, then spikes. The path zig-zags violently. Use Momentum (which we will cover next), or Adam optimizer.
Saddle Points In high dimensions, minima are rare, but saddle points (where one direction curves up, another down) are abundant. The gradient is zero, so GD stops. Loss is high, but param.grad approaches exactly zero. No progress. Introduce stochasticity (use Mini-batch SGD instead of Batch GD) to “kick” the algorithm out of the saddle point.

Engineering Insights

As an AI engineer, you do not use pure “Batch Gradient Descent.” You use its variants. Here is why and how you tune them:

  1. Batch GD vs. SGD vs. Mini-batch SGD:
    • Batch GD: Calculates gradient over the entire dataset. Exact, but requires loading all data into memory. A single step takes hours on massive datasets. Never used in deep learning.
    • Stochastic GD (SGD): Calculates gradient on one sample. Extremely noisy. The noise acts as implicit regularization, helping escape saddle points. But you cannot parallelize one sample on a GPU.
    • Mini-batch SGD: The industry standard. You calculate the gradient on \(B\) samples (e.g., \(B=256\)).
    • The Linear Scaling Rule: If you increase your batch size \(B\) by a factor of \(k\) (e.g., from 256 to 1024), you can typically increase your learning rate \(\eta\) by a factor of \(k\). Why? Because the variance of your gradient estimate decreases by \(k\), so you can take a proportionally larger confident step.
  2. GPU Utilization:
    • If \(B=1\), your GPU compute units sit idle because matrix multiplications are too small to saturate the hardware.
    • If you increase \(B\), GPU utilization goes up. However, larger \(B\) means less noise, which can cause the model to converge to “sharp” minima that generalize poorly to unseen data.
    • Finding the perfect \(B\) is a tradeoff between GPU hardware efficiency and statistical generalization.

Interview Questions

Beginner: Q: What happens if you set the learning rate to zero? A: The update becomes \(\theta_{t+1} = \theta_t - 0 \cdot \nabla \mathcal{L} = \theta_t\). The parameters never change. Training freezes.

Intermediate: Q: Why do we shuffle our data at every epoch when using Mini-batch Gradient Descent? A: If we don’t shuffle, consecutive mini-batches will contain highly correlated examples (e.g., all cats, then all dogs). The gradient will point aggressively in one direction, then aggressively in another, causing massive oscillations. Shuffling ensures the gradient estimate is an unbiased representation of the whole dataset, smoothing the descent path.

Advanced: Q: Explain why Gradient Descent can get stuck at a saddle point, but is unlikely to get stuck at a strict local minimum in high-dimensional space. A: For a point to be a strict local minimum, the Hessian (second derivative matrix) must be positive definite in all dimensions. In a billion-dimensional space, the probability of all billion eigenvalues being positive is vanishingly small. A saddle point, however, only requires some eigenvalues to be positive and some negative. GD gets stuck because the gradient is exactly zero, and without momentum or stochastic noise, there is no force to push it down the negative curvature directions.

Research-Level: Q: Why does Stochastic Gradient Descent (with noise) often generalize better than Full-Batch Gradient Descent, even if they arrive at the same training loss? A: Mini-batch noise prevents GD from settling into the absolute deepest, narrowest point of the loss basin (a “sharp” minimum). Instead, it bounces around the bottom, effectively averaging out to a wider, flatter region of the loss landscape. Flat minima have lower Hessian eigenvalues, meaning the model’s output is less sensitive to small perturbations in the input weights, which corresponds directly to better out-of-distribution generalization.


Research Perspective

Gradient descent is solved for convex functions. In deep learning, loss functions are highly non-convex. Current research frontiers include:

  1. The Learning Rate Landscape: Research by entities like Meta (Lion optimizer) and Google shows that the sign of the gradient often matters more than its magnitude, leading to sign-based descent variants that use less memory.
  2. Sharpness-Aware Minimization (SAM): Standard GD minimizes loss. SAM modifies the loss function to explicitly penalize sharp minima: \(\mathcal{L}_{SAM}(\theta) = \max_{\|\epsilon\| \leq \rho} \mathcal{L}(\theta + \epsilon)\). It literally looks around the parameter space to ensure it’s at a flat bottom before stepping.
  3. Distributed Descent: In LLM training, a single model is split across thousands of GPUs. The core engineering challenge is synchronizing the gradient calculation across these GPUs without the network communication becoming a bottleneck (e.g., ZeRO optimization, Ring-AllReduce).

Connections

Prerequisites: * Gradients (Provides the direction vector) * Loss Functions (Provides the scalar field to minimize)

Enables: * All Optimizers (SGD with Momentum, Adam, AdaGrad are just modifications to this base step) * Convergence Theory (Mathematical proofs of \(O(1/t)\) rates)

Used by: * Backpropagation (Computes the \(\nabla \mathcal{L}\) that GD consumes) * Every neural network training loop ever written

Extended by: * Second-Order Methods (Adding Hessian information back in, like L-BFGS) * Adaptive Learning Rates (Scaling \(\eta\) per parameter based on historical gradients)

Related Concepts: * Loss Landscapes (The terrain GD walks on) * Learning Rate Schedulers (Dynamically changing \(\eta\) over time)


Final Mental Model

Lock this mental image into your brain:

Imagine a massive, multi-dimensional bowl filled with highly viscous honey. A heavy steel ball bearing (your parameters) is dropped near the rim.

Gradient Descent is not a person pushing the ball. Gradient Descent is the physical law of gravity within that specific bowl. The slope of the honey at the ball’s current location dictates the exact direction and strength of the gravitational pull (the gradient). The learning rate is the viscosity of the honey—if it’s too thick (LR too small), the ball barely moves; if it’s too thin (LR too large), the ball overshoots the center and flies out of the bowl.

Every time you train a neural network, you are simply dropping a billion-dimensional ball into a billion-dimensional bowl of honey and watching it slide to the bottom.

Comments

Popular posts from this blog

Why Every AI Model is Actually Bayesian

Why Every AI Model is Actually Bayesian Topic Overview Standard probability asks: “What is the chance of this event happening in the entire universe?” Conditional probability asks: “Given that I already know this to be true, what is the chance of that happening?” Bayes’ Theorem is the engineering tool that flips this condition around. It answers the most critical question in machine learning: “Given the data I just observed, what is the probability that my hypothesis is true?” Why does it exist? Because in the real world, we usually know \(P(\text{Data}|\text{Hypothesis})\) —how likely a certain observation is under a specific model—but what we actually want is \(P(\text{Hypothesis}|\text{Data})\) —how likely our model is to be correct given the observation. Bayes’ Theorem is the mathematical bridge between the data we generate and the truths we want to infer. What problem does it solve? It provides a rigorous, mathematically sound mechani...

Why Everything in AI is a Vector ?

Why Everything in AI is a Vector ? Topic Overview In the previous lesson, you learned about the scalar: a single number representing magnitude. But reality is rarely one-dimensional. A single pixel has three color channels (red, green, blue). A word’s meaning has dozens of grammatical and semantic features. A model’s parameters number in the millions. To represent entities that possess multiple attributes simultaneously, we need a mathematical object that holds multiple scalars in a specific order. That object is a vector. A vector \(\mathbf{v} \in \mathbb{R}^n\) is an ordered list of \(n\) scalars. It represents a point, or an arrow from the origin, in \(n\) -dimensional space. Why does it exist? Because we need a mathematical language for things that have more than one degree of freedom. A scalar tells you “how hot.” A vector tells you “where you are” and “which way you’re going.” What problem does it solve? It provides the fundamental d...

Why Your PyTorch Code Keeps Breaking (It’s the Scalars)

Why Your PyTorch Code Keeps Breaking (It’s the Scalars) Topic Overview You are about to learn about the most fundamental object in all of mathematics and machine learning: the scalar. A scalar is a single number. That is the entire definition. But do not confuse simplicity with insignificance. Every neural network, no matter how many billions of parameters it contains, is ultimately optimized by reading and writing scalars. The loss value is a scalar. The learning rate is a scalar. The temperature parameter in softmax is a scalar. The scale factor in attention is a scalar. Every gradient clipping decision is based on a scalar. The final prediction of a classification model is a scalar probability. Why does the scalar exist as a named concept? Because not all numbers are created equal. Some numbers exist as components of larger structures—a coordinate in a vector, an entry in a matrix, a pixel in an image. A scalar is a number that stands alone...