Finite-Sum Objectives#
Nearly all supervised learning objectives have the form:
where is the loss on the -th training example. This is the ERMEmpirical Risk Minimization objective from Week 1.
The full gradient is , which requires scanning all examples — one full pass through the dataset — for a single parameter update. When is large (millions or billions), this is impossibly slow.
SGD replaces the full sum with an estimate computed from a small, randomly chosen mini-batch.
Mini-Batch SGD#
At iteration , sample a mini-batch of size uniformly at random (without replacement). The mini-batch gradient estimate is:
The SGD update is then .
Two key properties of :
-
Unbiased: . On average, the mini-batch gradient points in the right direction. The expectation is over the random choice of .
-
Noisy: is proportional to . A smaller batch has higher variance (noisier gradient estimate) but is cheaper to compute.
Computational cost per step:
- Full-batch GD: to compute , then to update
- Mini-batch SGD (size ): to compute , then to update
SGD makes roughly times as many updates in the time it takes GD to do one update.
Noise and Variance#
The variance of the mini-batch gradient estimate is:
The term measures how different individual example gradients are from the average. When examples are "similar" (homogeneous data), is small and SGD works well even with . When examples are very different (heterogeneous data), you need larger batches to reduce variance.
The batch-size tradeoff:
- Small : cheap per iteration, many noisy updates per epoch → better exploration
- Large : expensive per iteration, fewer but more accurate updates → reliable descent
- : recovers full-batch GD (zero variance, maximum cost per step)
In practice, is typically chosen as a power of 2 (32, 64, 128, 256) to fit well with GPU memory and vectorised computation.
Step Sizes Under Noise#
With a fixed step size , SGD does not converge to the exact optimum — it oscillates in a ball around whose radius is proportional to . To converge exactly, we must decay the step size:
The schedule is common in theory; in practice, step-decay (halve every epochs) and cosine annealing (Week 9) are more popular.
A useful heuristic: for SGD with batch size , a rule of thumb is . Doubling the batch size approximately allows doubling the learning rate (because the gradient estimate is twice as accurate).
Epoch vs Step#
- Step (or iteration): one mini-batch processed, one parameter update
- Epoch: one full pass through the entire dataset ( steps)
At the end of each epoch, the data is typically shuffled (random permutation) to avoid periodicity effects and to decorrelate consecutive mini-batches. Mini-batches drawn from a shuffled epoch are not independent — within an epoch they partition the data, so they are negatively correlated — but each is an unbiased estimate of the full gradient, which is what the convergence theory actually requires.
Tracking loss per epoch is more meaningful than loss per step, because epoch-level loss estimates the true objective while step-level loss is computed on a single mini-batch and is noisier.
Knowledge Check#
If the mini-batch size B is doubled, the variance of the gradient estimate is approximately:
The mini-batch gradient g_k is an unbiased estimate of the full gradient ∇f(w) because:
Browser lab: GD vs SGD noise tradeoff#
Compare GD, SGD(B=1), and SGD(B=16) on a finite-sum quadratic objective. Plot loss vs epoch and observe the noise–convergence tradeoff.
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
d, n = 3, 200
w_true = np.array([1.5, -0.8, 0.6])
X = rng.normal(size=(n, d))
y = X @ w_true + rng.normal(scale=0.3, size=n)
def loss(w, X, y):
return 0.5 * np.mean((X @ w - y)**2)
def run_method(X, y, w0, n_epochs, method, B=None, eta=None):
w = w0.copy()
n = len(X)
epoch_losses = []
for ep in range(n_epochs):
perm = rng.permutation(n)
for start in range(0, n, B or n):
idx = perm[start:start + (B or n)]
g = X[idx].T @ (X[idx] @ w - y[idx]) / len(idx)
w -= eta * g
epoch_losses.append(loss(w, X, y))
return epoch_losses
w0 = np.zeros(d)
n_epochs = 40
L = np.linalg.eigvalsh(X.T @ X / n).max()
eta = 1.0 / L
losses_gd = run_method(X, y, w0, n_epochs, "GD", B=n, eta=eta)
losses_sgd1 = run_method(X, y, w0, n_epochs, "SGD1", B=1, eta=eta*0.05)
losses_sgd16 = run_method(X, y, w0, n_epochs, "SGD16", B=16, eta=eta*0.5)
plt.figure(figsize=(8, 5))
plt.plot(losses_gd, label="GD (full batch)", linewidth=2)
plt.plot(losses_sgd1, alpha=0.8, label="SGD B=1 (noisy)", linewidth=1)
plt.plot(losses_sgd16, alpha=0.8, label="SGD B=16", linewidth=1.5)
plt.yscale("log")
plt.xlabel("Epoch"); plt.ylabel("Loss")
plt.title("GD vs SGD: Loss per Epoch")
plt.legend(); plt.grid(True, alpha=0.3)
plt.show()
print("Notice: full-batch GD is smooth but makes one update per epoch; SGD B=1 is noisy yet far cheaper per step.")
Further Reading#
- Léon Bottou, Frank E. Curtis & Jorge Nocedal, Optimization Methods for Large-Scale Machine Learning (SIAM Review, 2018). Section 5 is the standard analysis of SGD, mini-batching, and step-size schedules.
- Ian Goodfellow, Yoshua Bengio & Aaron Courville, Deep Learning (MIT Press, 2016). §8.3 covers mini-batch and stochastic gradient methods.
- Herbert Robbins & Sutton Monro, "A Stochastic Approximation Method" (Annals of Mathematical Statistics, 1951). The original stochastic-approximation result behind SGD.