Contents
(where is the learning rate)
Deep Breakdown
What it tells me (Situation Awareness): This is the core iteration of gradient descent: given current parameters , subtract the gradient of the loss (scaled by learning rate ) to obtain the next iterate . The negative gradient points in the direction of steepest descent, so this step moves downhill on the loss surface. The superscript converts the gradient from a row vector (Jacobian convention) to a column vector (parameter vector convention) — in practice, for , and the update subtracts it directly. This single equation is the foundation of all ML training: SGD, mini-batch GD, Adam, and RMSProp are all modifications of this update rule.
Flowchart
Real-world use
Where it is used
Training every gradient-based ML model: linear regression, logistic regression, SVM (subgradient), neural networks. Also: fine-tuning pre-trained language models, optimizing hyperparameters using gradient through validation loss, training embedding models (word2vec, matrix factorization).
Problem it solves
Given a differentiable loss , finding that minimizes it. The update rule provides the iterative procedure: compute gradient at current point, step in the negative gradient direction, repeat. Each step is guaranteed to decrease (for small enough ) because the gradient is the direction of steepest ascent.
How it solves the problem
Taylor expansion: (for small ), where . Since , each gradient step decreases by approximately . The exact decrease depends on curvature, which is why learning rate selection matters. For strongly convex with -Lipschitz gradient, the optimal fixed step is and convergence is linear: .
Example in a business
A DS trains logistic regression on 100k e-commerce transactions. . Gradient: where is sigmoid. Update: . With and 500 iterations, the model converges to AUC 0.87 on the test set.
The formula, piece by piece
- : the new parameter vector after one gradient step
- : the current parameter vector at iteration
- : the learning rate at iteration ; scalar that scales the step size;
- : the gradient of evaluated at ; a vector of partial derivatives; points uphill
- : the gradient step; negative because we go downhill
- : transpose notation; in ML, gradient is often written as a row vector (Jacobian), transposed to match column vector ; in practice, numpy computes as a column vector directly
- : iteration index; subscript denotes which iterate (not element-wise indexing)
- Variable learning rate: can change each iteration (learning rate schedules, line search)
The formula in words
"The new parameters equal the old parameters minus the learning rate times the gradient. Move a small step in the direction opposite to the gradient (which points uphill), so we go downhill on the loss surface." Tiny example: , , . . . . . . Converging to .
Critical Concepts
- Too large : overshooting; can diverge. For , instability occurs when (since for ). At : . At : — still converges but oscillates.
- Too small : slow convergence; needs many more iterations.
- Learning rate schedules: constant (), step decay ( every epochs), cosine annealing, warm restarts, linear warmup + cosine decay (common for transformers).
- Difference from Newton's method: gradient descent uses only (first order); Newton uses (second order). Newton converges faster (quadratically near optimum) but requires computing and inverting the Hessian (expensive for large).
- Mini-batch vs full batch: full batch uses the exact gradient (one pass over all samples); mini-batch uses the gradient of a random subset of size ; stochastic (SGD) uses . Noisy gradients add implicit regularization and help escape local minima in deep learning.
- Gradient clipping: in deep learning, gradients can become very large (exploding gradients in RNNs).
Clipping: prevents instability without changing direction.
Step-by-step calculation (tiny example)
Run 4 gradient descent steps on starting at with . .
| Step | |||||
|---|---|---|---|---|---|
| 0 | 3.000 | 2.000 | 6.000 | 16.000 | 25.000 |
| 1 | 1.800 | -1.200 | 3.600 | -9.600 | 9.000 |
| 2 | 1.080 | 0.720 | 2.160 | 5.760 | 3.240 |
| 3 | 0.648 | -0.432 | 1.296 | -3.456 | 1.166 |
| 4 | 0.389 | 0.259 | 0.778 | 2.074 | 0.420 |
Converging to . Note: large step for term () causes oscillation; the term converges faster.
Code examples
Beginner example (Python)
import numpy as np
def gradient_descent_step(
x: np.ndarray,
grad: np.ndarray,
learning_rate: float,
) -> np.ndarray:
"""Single gradient descent update: x_{i+1} = x_i - gamma * grad."""
return x - learning_rate * grad
# f(w) = w1^2 + 4*w2^2; gradient = (2w1, 8w2)
def f(w: np.ndarray) -> float:
return w[0]**2 + 4 * w[1]**2
def grad_f(w: np.ndarray) -> np.ndarray:
return np.array([2 * w[0], 8 * w[1]])
LEARNING_RATE = 0.2
N_STEPS = 10
w = np.array([3.0, 2.0])
print(f"Gradient descent: f(w) = w1^2 + 4*w2^2, gamma={LEARNING_RATE}")
print(f"{'Step':>4} {'w1':>8} {'w2':>8} {'f(w)':>10} {'||grad||':>10}")
for step in range(N_STEPS):
g = grad_f(w)
print(f"{step:>4} {w[0]:>8.4f} {w[1]:>8.4f} {f(w):>10.4f} {np.linalg.norm(g):>10.4f}")
w = gradient_descent_step(w, g, LEARNING_RATE)
print(f"\nFinal w* = {w}, f(w*) = {f(w):.2e}")Beginner example (R)
# Gradient descent update rule: x_{i+1} = x_i - gamma_i * grad_f(x_i)
gd_step <- function(x, grad, learning_rate) x - learning_rate * grad
f <- function(w) w[1]^2 + 4*w[2]^2
grad_f <- function(w) c(2*w[1], 8*w[2])
LEARNING_RATE <- 0.2
N_STEPS <- 10
w <- c(3, 2)
cat(sprintf("%-4s %-8s %-8s %-10s %-10s\n", "Step", "w1", "w2", "f(w)", "||grad||"))
for (step in 0:(N_STEPS-1)) {
g <- grad_f(w)
cat(sprintf("%-4d %-8.4f %-8.4f %-10.4f %-10.4f\n", step, w[1], w[2], f(w), sqrt(sum(g^2))))
w <- gd_step(w, g, LEARNING_RATE)
}
cat(sprintf("Final w* = (%.4e, %.4e), f(w*) = %.2e\n", w[1], w[2], f(w)))Professional example (Python)
import numpy as np
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score
np.random.seed(42)
# -- Full gradient descent with learning rate schedule for logistic regression --
N_SAMPLES = 800
X_raw, y_raw = make_classification(n_samples=N_SAMPLES, n_features=8, n_informative=5, random_state=42)
scaler = StandardScaler()
X = scaler.fit_transform(X_raw)
y = y_raw * 2 - 1 # {-1, +1} encoding
N = len(y)
D = X.shape[1]
LAMBDA_REG = 0.01
def log_loss_with_reg(w: np.ndarray) -> float:
margins = y * (X @ w)
return float(np.mean(np.log1p(np.exp(-margins)))) + LAMBDA_REG * np.sum(w**2)
def log_loss_gradient(w: np.ndarray) -> np.ndarray:
margins = y * (X @ w)
sigmoid_neg = 1 / (1 + np.exp(margins)) # sigmoid(-margin)
return -1/N * X.T @ (y * sigmoid_neg) + 2 * LAMBDA_REG * w
# Learning rate schedules to compare
def constant_lr(step: int, lr0: float = 0.5) -> float:
return lr0
def step_decay(step: int, lr0: float = 0.5, decay: float = 0.5, drop_every: int = 50) -> float:
return lr0 * (decay ** (step // drop_every))
def sqrt_decay(step: int, lr0: float = 0.5) -> float:
return lr0 / np.sqrt(step + 1)
schedules = {
"constant": constant_lr,
"step decay": step_decay,
"sqrt decay": sqrt_decay,
}
N_ITER = 300
results = {}
for name, lr_func in schedules.items():
w = np.zeros(D)
losses = []
for step in range(N_ITER):
gamma = lr_func(step)
g = log_loss_gradient(w)
# Core update rule: x_{i+1} = x_i - gamma_i * grad f(x_i)
w = w - gamma * g
losses.append(log_loss_with_reg(w))
y_score = X @ w
auc = roc_auc_score(y_raw, y_score)
results[name] = {"w": w, "final_loss": losses[-1], "auc": auc, "losses": losses}
print(f"{'Schedule':<15} {'Final Loss':>12} {'AUC':>8}")
for name, res in results.items():
print(f"{name:<15} {res['final_loss']:>12.6f} {res['auc']:>8.4f}")
print(f"\nUpdate rule: x_{{i+1}} = x_i - gamma_i * grad_f(x_i)")
print(f"gamma_i is the learning rate — the single most important hyperparameter.")Source: MML1 p. 234