ML Systems Lab Open interactive version →
Foundational 40 min read sgdmini-batchnoisebatch-sizeregularization

SGD and Mini-Batch Training

Why noisy gradients help, batch size effects, and the implicit regularization of SGD.

You are training a 500,000-parameter network on 10 million examples. To take *one* honest gradient step, plain gradient descent must first run all 10 million examples through the network. On a GPU chewing through a thousand examples a second, that is nearly three hours — per step. One step, wait three hours, one step, wait three hours. Training would take decades. Before mini-batching, this was the actual wall people hit.

The fix is almost cheeky in hindsight: do not measure the slope from all 10 million examples. Grab a small *random handful* — say 32 — and estimate the slope from those. It is like working out which way is downhill by feeling the ground under a few random footsteps instead of surveying the whole mountain. Any single estimate is a little *noisy* — a different handful gives a slightly different direction — but on average it points the right way, and you can compute it in a blink. Trading one perfect step every three hours for a slightly-wobbly step every fraction of a second is a spectacular deal: with batches of 32, you go from *one* update per pass through the data to over 300,000. That is mini-batch stochastic gradient descent (SGD), and it is how essentially every neural network is trained.


The surprise: the noise actually helps

Here is the twist nobody saw coming. That gradient noise, which sounds like a necessary evil, turns out to make the final model *better*.

Picture the loss landscape as having two kinds of valleys: narrow, steep-walled sharp pits, and wide, gently-sloping flat basins. A perfectly noiseless optimizer slides straight into whichever valley is nearest and stops — sharp pit or not. But a *noisy* optimizer keeps getting jostled. In a narrow sharp pit, a jostle easily knocks it back out; in a wide flat basin, the same jostle is not enough to escape. So the noise acts like a filter: the optimizer keeps getting bounced out of sharp pits until it settles into a flat basin — and flat basins are exactly the ones that generalize better to new data. The messiness is doing quiet regularization work, for free.


Batch size is a dial, not just a memory setting

Most people pick batch size by "whatever fits in GPU memory." But because the batch size controls how much noise there is, it is really a *regularization dial*. Bigger batches mean *less* noise (each estimate averages more examples), so the optimizer behaves more like the noiseless one — faster and smoother, but drawn toward those sharp, worse-generalizing pits. Push the batch very large and you can match the training loss of a small-batch run while landing a couple of points *worse* on test accuracy, purely from the lost noise. So sensible defaults are modest batches (32 to 256); go bigger only when you truly need the speed, and expect to add explicit regularization (like weight decay) to make up for the noise you removed.


One rule you cannot skip: shuffle

Finally, a practical trap that quietly ruins training: shuffle your data before every pass. The whole "noisy but right on average" guarantee assumes each mini-batch is a random sample. If your data happens to be sorted by class — all the cats, then all the dogs — an unshuffled batch contains *only cats*, and its gradient screams "get better at cats" while forgetting dogs entirely. Consecutive batches then yank the model in wildly different directions. Shuffle before each epoch and every batch becomes a fair little snapshot of the whole dataset, which is what makes the estimate honest.


Epoch, step, iteration — pin down the vocabulary

These get muddled constantly, so be exact. One epoch is one full pass through the entire dataset. One step (also called one iteration) is a single mini-batch update. The relationship: updates per epoch = ceil(N / batch_size). So 1,000,000 examples at batch size 256 is ⌈1,000,000 / 256⌉ = 3,907 steps per epoch. When a paper says "trained for 100k steps" versus "100 epochs," these mean different amounts of compute unless you also know N and the batch size — always convert to one consistent unit before comparing runs.


"Small batch generalises better" is a tendency, not a law

The flat-minima story is real but *not universal*. Whether small batches actually generalise better depends on the learning rate, the dataset size, the architecture, whether you use normalisation, the schedule, and the training budget. With careful warmup, LR scaling, and enough steps, large-batch training can close much of the gap (this is how models train on thousands of GPUs). So don't state "small batch = better generalisation" as a rule — state it as a default tendency that a well-tuned large-batch setup can partly overcome.


Scaling the learning rate to the batch has limits

The linear scaling rule (multiply LR by k when you multiply batch size by k) works — but only up to a regime. Past a few thousand examples per batch it breaks down: the implied LR gets so large that early training destabilises, which is exactly why warmup (ramping the LR up over the first epochs) becomes essential at large batch. Some setups find square-root scaling (LR ∝ √k) safer than linear at scale. The takeaway: LR-vs-batch scaling is an approximation with a ceiling, not a guaranteed equivalence.


BatchNorm couples training to the batch size

If your network uses BatchNorm, batch size stops being just a noise/speed knob because BatchNorm computes its normalisation *statistics from the current batch*. Very small batches give noisy mean/variance estimates that destabilise training (a batch of 2 has almost meaningless statistics). Very large batches give near-exact statistics, which changes BatchNorm's own regularisation behaviour. This is a big reason transformers favour LayerNorm (which normalises per-example and doesn't depend on batch size). When you change batch size on a BatchNorm model, you're changing more than the gradient noise.


SGD versus Adam, fairly

"SGD generalises better than Adam" is too blunt. Adam/AdamW often *wins* — it's the standard for transformers and shines on sparse gradients and NLP. SGD-with-momentum can generalise better in some classic vision/CNN settings when well-tuned. Why the gap leans that way ties back to the noise story above: Adam's per-parameter adaptive steps damp gradient noise along some directions, so it explores less of the landscape and can settle into sharper minima — often training faster but finishing a touch worse than a well-tuned SGD run, whose undamped noise keeps bouncing it toward the flatter basins that generalize better. The honest summary: AdamW is the default for modern deep learning; SGD+momentum remains competitive or better in specific well-studied regimes. Pick based on the domain and tune both before declaring a winner.


Big batches live across many GPUs: distributed training

At scale the batch is split across devices, which adds vocabulary. Global batch size is the total across all GPUs; per-device batch size is what each one processes. Gradient accumulation simulates a large batch on limited memory by summing gradients over several forward/backward passes before one update. Data parallelism replicates the model on each GPU and all-reduces the gradients — which costs communication bandwidth, often the real bottleneck at scale. So "batch size 8,192" usually means a global batch spread over many devices, not one machine's memory.


Rare classes can get squeezed out of batches

Random mini-batching assumes every batch is a fair sample — but under heavy imbalance a batch of 32 from a 0.1%-positive dataset frequently contains *zero* positives, so many steps carry no signal about the rare class. Fixes: stratified batches (force a minimum number of rare-class examples per batch), weighted sampling (oversample the rare class into batches), and hard-example mining (bias batches toward the examples the model currently gets wrong). Under imbalance, how you *build* the batch matters as much as its size.

Key points

Takeaway

Mini-batch SGD was invented for computational feasibility; its gradient noise turned out to be the mechanism that finds flat, generalizing minima — making batch size a generalization hyperparameter, not just a throughput setting.

Recap

Check your understanding

Q1. Team A uses B=32; Team B uses B=2048 with 64× the learning rate (linear scaling rule). Both reach the same training loss, yet Team B generalizes worse. Why?

Q2. Why is shuffling the data before each epoch necessary for correct SGD, not just a speed tweak?

Q3. Which two of the following are true about SGD's "implicit regularization" and what tends to happen if you swap SGD for Adam without changing anything else?

Q4. On N=1,000,000 examples, compare full-batch GD with B=256 mini-batch SGD on gradient accuracy per step, updates per epoch, and convergence.

Q5. Your model uses BatchNorm and trained well at batch size 128. You drop to batch size 4 (memory limit) and training becomes unstable, with worse accuracy even holding everything else fixed. Why, and what's a fix?

Q6. You're training a fraud classifier where 0.1% of examples are positive, using random mini-batches of 32. Training is unstable and the model barely learns fraud. What's a likely cause tied to batching, and how do you fix it?

Try it interactively

ML Systems Lab is a free interview-prep platform for ML engineers — work through the full interactive module, quizzes, and drills.

Open ML Systems Lab →