Neural Networks / Deep Learning Foundations · Laboratory Handout

Topic 08 — Gradient Descent & Its Types

Batch vs. Mini-batch vs. Stochastic Gradient Descent (PyTorch)

Topic: Gradient Descent variants
Framework: PyTorch
Dataset: Social Network Ads — purchase prediction
Prerequisite: basic Python

1 Lab Introduction

Gradient Descent (GD) is the optimization algorithm neural networks use to minimize a loss function J(θ) by repeatedly moving the parameters (weights & biases) in the direction opposite to the gradient — i.e. “downhill” on the loss surface.

In this lab we train the same network three times on a small purchase-prediction dataset, changing only one thing — the batch_size — and watch how that single choice trades training time against how fast the loss falls. Every GD variant plugs into the same backpropagation skeleton; what changes is only how many points are used before an update is applied.

Learning Objectives

  1. Tell Batch, Mini-batch, and Stochastic GD apart by their number of weight updates per epoch.
  2. Write a small PyTorch training function (fit) whose update frequency is set by the batch_size you pass it.
  3. Compare regimes by reading the printed per-epoch loss and total time.
  4. Relate each line of the PyTorch loop to the update rule Wn = W0 − η ∂L/∂w.
  5. Distinguish the two meanings of “fast”: speed per epoch vs. speed to convergence.
How to use this document

Sections 2–4 develop the theory. Sections 5–8 connect that theory to the PyTorch implementation, line by line. The outputs shown are those recorded when the code was actually run — this page does not execute PyTorch; it explains, highlights and lets you reason about the code. To obtain new numbers, run the code yourself.

2 Conceptual Overview

2.1 What is Gradient Descent?

Training repeatedly updates each weight by stepping against the gradient of the loss:

Wn = W0 − η ∂L∂w
where W0 is the current weight, Wn the updated weight, η the learning rate (lr in the code), and ∂L/∂w the gradient of the loss with respect to that weight. In the loop, loss.backward() computes ∂L/∂w and optimizer.step() applies this formula.
Show intuition — why the minus sign?

The gradient ∂L/∂w points in the direction in which the loss increases fastest. We want the loss to go down, so we move the parameters in the opposite direction — hence the minus sign. The learning rate η decides how large that downhill step is: it scales the gradient before it is subtracted from the current weight.

Three-dimensional loss surface J(theta0, theta1) with a stepped descent path toward the minimum
Fig. 1 — The loss surface J(θ0, θ1). Gradient descent takes a path of successive steps down the bowl toward the minimum.

2.2 Where Updates Happen — The Backpropagation Template

Every GD variant plugs into the same backpropagation skeleton — what changes is only how many points are used before an update is applied.

Backpropagation template
for epoch in range(epochs):
    # Loop over points depends entirely on the chosen GD Variant
    for point in dataset:  # select 1 single row
        prediction = forward_propagate(point)
        loss = calculate_loss(point, prediction)
        gradients = backward_propagate(loss)
        update_weights_and_biases(gradients)
    average_loss = calculate_epoch_average()
Key Idea

1 Epoch = one full pass over the training data. How many weight updates happen per epoch is exactly what distinguishes Batch GD, SGD, and Mini-Batch GD.

2.3 The Variants at a Glance

The variants differ only in how many samples feed each update, which fixes the number of updates per epoch:

VariantBatch size BUpdates / epoch
Batch GDall N1
Mini-batch GDe.g. 32N/B
Stochastic GD1N

More updates per epoch → the loss falls faster per epoch, but more time is spent per epoch. That tension is the whole lab.

3 Types of Gradient Descent

Select a variant to see what data it uses, how the update is performed, and its key characteristics.

Batch Gradient Descent (Vanilla GD)

In Batch Gradient Descent, gradients are computed across the entire dataset simultaneously before a single weight or bias is changed. The update direction reflects the average signal from every data point in the training set.

Rule: 1 Epoch = 1 Weight Update
Metaphor

Batch GD is like a collective, democratic decision-making process — every single data point casts a “vote” and the model takes exactly one careful step downhill based on the combined opinion of the whole population.

Pseudocode

Batch GD — pseudocode
import numpy as np

epochs = 10
weights = np.random.randn(input_dim, output_dim)
bias = np.zeros((1, output_dim))
learning_rate = 0.01

for i in range(epochs):
    # Process the ENTIRE dataset simultaneously (e.g. 50 points)
    y_hat = np.dot(X_train, weights) + bias

    # Cumulative loss across ALL samples combined
    loss = calculate_cumulative_loss(y_train, y_hat)

    # Gradient of the GLOBAL loss
    d_weights = compute_global_gradient(X_train, y_train, y_hat)

    # Parameter update happens precisely ONCE per epoch
    weights = weights - (learning_rate * d_weights)
    print(f"Epoch {i+1} complete. One update applied.")

Properties

  • Fully vectorized — one matrix dot product per epoch.
  • Very fast per epoch on suitable hardware (efficient use of NumPy / GPU matrix math).
  • Smooth, stable convergence path — low variance, low noise.
  • Needs many epochs to converge since it updates only once per pass.
  • Requires the whole dataset to fit in memory for the matrix multiplication.
How this variant appears in the PyTorch code

Batch GD is obtained by passing the full row count as the batch size, so the inner loop of fit() executes exactly once per epoch:

Batch GD run
model = build_model()
start = time.time()
fit(model, epochs=10, batch_size=n_train)     # whole training set in one batch
print('Total time:', round(time.time() - start, 2), 's')

3.1 Summary Cheat-Sheet

FeatureBatch GDSGDMini-Batch GD
Data used per updateEntire dataset1 rowSmall batch (e.g. 32–256)
Updates per epoch1N (rows)N / batch_size
Speed per epochFast (vectorized)Slow (looped)Fast (vectorized per batch)
Epochs to convergeManyFewFew–moderate
Loss curveSmoothNoisy / jaggedSlightly noisy, mostly smooth
Escapes local minima?No — can get stuckYes — noise helps escapeSometimes — some noise remains
Memory usageHigh (whole dataset in RAM)Very low (1 row)Low–moderate (1 batch)
Common batch size= dataset size= 132, 64, 128, 256 (powers of 2)

4 Loss-Curve Geometry & the Golden Nuggets

4.1 The Optimization Path

Zoomed in on a set of contour rings around the minimum, the two algorithms trace very different paths to the target.

Contour plots comparing a jagged stochastic path and a straight gradient/batch path toward the minimum
Fig. 2 — Stochastic path (left): a chaotic zig-zag caused by noise from individual samples. Gradient/Batch path (right): a clean, direct line pulled by the whole dataset’s average signal.
AspectBatch GD PathSGD Path
ShapeStraight, smooth line perpendicular to the contour ringsJagged zig-zag, like a lightning bolt
CauseDirection computed from the whole population — high certaintyDirection computed from one sample at a time — high individual noise
Overall trendDrives directly to the deepest pointBounces around walls, but generally trends toward the base

4.2 Escaping Local Minima

On a non-convex, rugged loss landscape with several shallow local valleys plus one deep global canyon, the two algorithms behave very differently when they hit a shallow trap.

PathBehaviour at a shallow local minimum
Batch GDGradient from the smooth aggregate trend goes to zero at the base of the shallow valley — the model freezes and gets permanently trapped in the suboptimal local minimum.
SGDAn outlying data sample can cause a sudden large jump in loss/gradient. This noise “jolts” the optimizer out of the shallow valley wall, so it keeps moving until it finds the deeper global minimum.
Takeaway

SGD’s noise is not purely a weakness — that same instability is what lets it escape shallow local minima that would trap a smoother algorithm like Batch GD.

4.3 The Double Definition of “Fast”

There are two very different meanings of “fast” in this context, and confusing them is a common mistake.

Meaning of “Fast”Batch GDSGD
Speed PER epochVery fast — one efficient vectorized matrix operationSlower — many small sequential updates
Speed TO convergence (total epochs needed)Slow — only 1 update per epoch, needs many epochsFast — hundreds of updates within a single pass, needs far fewer epochs

4.4 Vectorization vs. Memory: The 10-Crore-Row Wall

Vectorization with NumPy dot products is very fast because it avoids slow Python-level loops — but it comes with a hardware cost.

  • A full-batch matrix dot product requires loading the entire dataset into RAM at once.
  • If the training set is massive (e.g. 100 million / 10 crore rows), this immediately causes an Out-Of-Memory (OOM) crash.
Fix

Mini-batching solves this hardware limit by only ever loading a small chunk of the dataset into RAM at a time.

5 Theory → Code Connection

Select a concept on the left. The matching lines of the fit() function are highlighted on the right.

Concept / Theory
How it appears in PyTorch
fit()
def fit(model, epochs, batch_size, lr=0.1):
    optimizer = torch.optim.SGD(model.parameters(), lr=lr)
    criterion = nn.BCELoss()

    for epoch in range(epochs):
        perm = torch.randperm(n_train)                 # reshuffle the rows each epoch
        epoch_loss, n_batches = 0.0, 0
        for i in range(0, n_train, batch_size):        # step through in batch_size chunks
            idx = perm[i:i + batch_size]               # indices of this batch
            xb, yb = X_train_t[idx], y_train_t[idx]    # slice out one batch by hand

            optimizer.zero_grad()             # 1. clear old gradients
            loss = criterion(model(xb), yb)   # 2+3. forward pass + loss L
            loss.backward()                   # 4. backprop -> computes dL/dw
            optimizer.step()                  # 5. update: W_n = W_0 - lr * dL/dw

            epoch_loss += loss.item()         # accumulate to report the epoch average
            n_batches += 1

        print(f'Epoch {epoch + 1:2d}/{epochs} - loss: {epoch_loss / n_batches:.4f}')
Pitfall — optimizer.zero_grad() first

PyTorch accumulates gradients; without clearing them, each batch corrupts the next.

6 Code Walkthrough

The implementation is broken here into logical sections. Each block shows the code, what it does, and how it connects to gradient descent.

6.1 Imports and Reproducibility

Setup
import numpy as np
import pandas as pd
import time
import torch
import torch.nn as nn
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

torch.manual_seed(42)
np.random.seed(42)

What happens: the tensor library (torch), the layer module (torch.nn), a timer (time) and two scikit-learn utilities are imported. Both random seeds are fixed.

Connection to gradient descent: the seeds make weight initialisation and the per-epoch shuffle reproducible, so that when we later change only batch_size the comparison between the regimes is fair. time is what lets us measure the “speed per epoch” half of the double definition of fast (Section 4.3).

6.2 Data — Purchase Prediction

Predict Purchased (0/1) from Age and EstimatedSalary.

Load the dataset
url = 'https://raw.githubusercontent.com/shivang98/Social-Network-ads-Boost/master/Social_Network_Ads.csv'
df = pd.read_csv(url)[['Age', 'EstimatedSalary', 'Purchased']]
df.head()
Recorded output
   Age  EstimatedSalary  Purchased
0   19            19000          0
1   35            20000          0
2   26            43000          0
3   27            57000          0
4   19            76000          0

What happens: a small binary-classification table with two numeric features is loaded from a URL.

Connection to gradient descent: this table is the “dataset” of the backpropagation template (Section 2.2) — the collection of points over which every epoch iterates.

6.3 Split, then Scale

Pitfall — scale after splitting

Fit the StandardScaler on the training rows only, then just transform the validation rows; fitting on everything leaks validation info into training.

Train/validation split and standardisation
X = df[['Age', 'EstimatedSalary']].values
y = df['Purchased'].values

X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=2)

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)   # fit on train
X_val   = scaler.transform(X_val)         # transform val only

What happens: 20 % of the rows are held out for validation; the scaler learns its mean and standard deviation from the training rows only and is then applied to both sets.

Connection to gradient descent: Age (tens) and EstimatedSalary (tens of thousands) live on wildly different scales. Standardising them puts both features on comparable footing before the same learning rate η is applied to every weight.

6.4 Tensors and the Row Count N

Pitfall — label shape for BCELoss

The loss compares outputs of shape [N, 1]. Leaving labels as a flat [N] vector makes PyTorch broadcast to [N, N] and compute nonsense — so reshape to a column with .reshape(-1, 1).

NumPy → torch tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).reshape(-1, 1)
X_val_t   = torch.tensor(X_val,   dtype=torch.float32)
y_val_t   = torch.tensor(y_val,   dtype=torch.float32).reshape(-1, 1)

n_train = X_train_t.shape[0]   # number of training rows
print('training rows:', n_train)
Recorded output
training rows: 320

What happens: the arrays become float32 tensors, labels are reshaped into a column, and n_train records the number of training rows: 320.

Connection to gradient descent: n_train is the N of the theory. It is the number of updates SGD performs per epoch, the batch size that turns fit() into Batch GD, and the numerator of N/B for mini-batch.

6.5 The Model

A small network: 2 → 10 → 10 → 1 with a sigmoid output. build_model() returns fresh weights so each of the three runs starts from the same point (a fair comparison).

Model definition
def build_model():
    return nn.Sequential(
        nn.Linear(2, 10), nn.ReLU(),
        nn.Linear(10, 10), nn.ReLU(),
        nn.Linear(10, 1), nn.Sigmoid()
    )

What happens: three nn.Linear layers stacked with ReLU activations and a final sigmoid squashing the output into (0, 1) for binary classification.

Connection to gradient descent: the weights and biases inside those nn.Linear layers are the parameters θ that gradient descent updates. Rebuilding the model before each run is what keeps the Batch-vs-SGD comparison honest: both regimes start from the same point on the loss surface.

6.6 The Training Function

Keras’ model.fit() hides the training loop. Here we write that loop ourselves in a small fit() — so you can see exactly what happens on every batch. The number of times the inner loop runs per epoch (= updates per epoch) is set purely by batch_size.

fit()
def fit(model, epochs, batch_size, lr=0.1):
    optimizer = torch.optim.SGD(model.parameters(), lr=lr)
    criterion = nn.BCELoss()

    for epoch in range(epochs):
        perm = torch.randperm(n_train)                 # reshuffle the rows each epoch
        epoch_loss, n_batches = 0.0, 0
        for i in range(0, n_train, batch_size):        # step through in batch_size chunks
            idx = perm[i:i + batch_size]               # indices of this batch
            xb, yb = X_train_t[idx], y_train_t[idx]    # slice out one batch by hand

            optimizer.zero_grad()             # 1. clear old gradients
            loss = criterion(model(xb), yb)   # 2+3. forward pass + loss L
            loss.backward()                   # 4. backprop -> computes dL/dw
            optimizer.step()                  # 5. update: W_n = W_0 - lr * dL/dw

            epoch_loss += loss.item()         # accumulate to report the epoch average
            n_batches += 1

        print(f'Epoch {epoch + 1:2d}/{epochs} - loss: {epoch_loss / n_batches:.4f}')

What happens: the outer loop is one epoch; the rows are reshuffled with torch.randperm; the inner loop walks the shuffled indices in batch_size chunks, and each chunk performs the five numbered steps: clear gradients, forward pass, loss, backward pass, update. The batch losses are averaged and printed once per epoch.

Connection to gradient descent: this single function is all three variants at once. Because the inner loop is range(0, n_train, batch_size), it executes n_train / batch_size times — exactly the “updates per epoch” column of the theory table. Passing n_train gives 1 update (Batch GD), passing 1 gives N updates (SGD), passing 32 gives N/32 (Mini-batch).

6.7 Warm-up Run

A one-time warm-up run so PyTorch’s start-up cost doesn’t unfairly land on the first timed run below — ignore its printed loss.

Warm-up
fit(build_model(), epochs=1, batch_size=32)   # warm-up only; ignore this output
Recorded output
Epoch  1/1 - loss: 0.6831

Connection to gradient descent: this is methodological hygiene for the timing measurement, not a result. It is, incidentally, the only mini-batch call in Part 1.

6.8 Run 1 — Batch GD

batch_size = all rows → 1 update per epoch.

Batch GD run
model = build_model()
start = time.time()
fit(model, epochs=10, batch_size=n_train)     # whole training set in one batch
print('Total time:', round(time.time() - start, 2), 's')
Recorded output
Epoch  1/10 - loss: 0.6661
Epoch  2/10 - loss: 0.6615
Epoch  3/10 - loss: 0.6571
Epoch  4/10 - loss: 0.6527
Epoch  5/10 - loss: 0.6483
Epoch  6/10 - loss: 0.6440
Epoch  7/10 - loss: 0.6397
Epoch  8/10 - loss: 0.6355
Epoch  9/10 - loss: 0.6312
Epoch 10/10 - loss: 0.6269
Total time: 0.0 s

What happens: ten epochs, ten weight updates in total. The loss drifts from 0.6661 to 0.6269 — smooth, monotonic, and small. Wall-clock time rounds to 0.0 s.

Connection to gradient descent: this is the theory’s “1 Epoch = 1 Weight Update” rule made visible. The path is smooth and stable (Section 4.1) but the model has taken only ten steps down the loss surface, so it has barely moved.

6.9 Run 2 — Stochastic GD

batch_size = 1 → one update per sample.

Stochastic GD run
model = build_model()
start = time.time()
fit(model, epochs=10, batch_size=1)
print('Total time:', round(time.time() - start, 2), 's')
Recorded output
Epoch  1/10 - loss: 0.3740
Epoch  2/10 - loss: 0.2825
Epoch  3/10 - loss: 0.2760
Epoch  4/10 - loss: 0.2648
Epoch  5/10 - loss: 0.2551
Epoch  6/10 - loss: 0.2589
Epoch  7/10 - loss: 0.2590
Epoch  8/10 - loss: 0.2543
Epoch  9/10 - loss: 0.2454
Epoch 10/10 - loss: 0.2473
Total time: 0.59 s

What happens: ten epochs, but 320 updates in each — 3 200 updates in total. The loss is already at 0.3740 after the first epoch and settles around 0.25. Wall-clock time is 0.59 s.

Connection to gradient descent: two theoretical claims are visible in one output. First, “needs far fewer epochs” — one epoch of SGD achieves what Batch GD could not in ten. Second, the noise: the loss rises at epochs 6, 7 and 10 (0.2551 → 0.2589, 0.2543 → 0.2454 → 0.2473). That is the jagged zig-zag of Fig. 2 showing up in the printed numbers.

6.10 Runs 3 & 4 — the Same Comparison over 100 Epochs

Why did the Batch GD timer read 0.0 s?

Not because training was free. The timer prints round(time.time() - start, 2), which keeps only two decimals — so anything under 0.005 s displays as 0.0. Ten Batch GD updates really take about 4.5 milliseconds, which is below the printed precision. Raising the epoch count to 100 lifts the measurement above that floor and lets us read a real number.

Both regimes are re-run with a freshly built model, changing only epochs=10 to epochs=100. Everything else — network, learning rate, data — is identical.

Run 3 — Batch GD, 100 epochs

Batch GD — 100 epochs
model = build_model()
start = time.time()
fit(model, epochs=100, batch_size=n_train)    # still 1 update per epoch
print('Total time:', round(time.time() - start, 2), 's')
Recorded output — abridged, middle epochs elided
Epoch  1/100 - loss: 0.7077
Epoch  2/100 - loss: 0.6983
Epoch  3/100 - loss: 0.6897
   ...
Epoch 50/100 - loss: 0.4412
   ...
Epoch 98/100 - loss: 0.3210
Epoch 99/100 - loss: 0.3199
Epoch 100/100 - loss: 0.3188
Total time: 0.02 s

Run 4 — Stochastic GD, 100 epochs

Stochastic GD — 100 epochs
model = build_model()
start = time.time()
fit(model, epochs=100, batch_size=1)          # still one update per sample
print('Total time:', round(time.time() - start, 2), 's')
Recorded output — abridged, middle epochs elided
Epoch  1/100 - loss: 0.4020
Epoch  2/100 - loss: 0.2719
Epoch  3/100 - loss: 0.2682
   ...
Epoch 50/100 - loss: 0.2301
   ...
Epoch 98/100 - loss: 0.2263
Epoch 99/100 - loss: 0.2260
Epoch 100/100 - loss: 0.2325
Total time: 5.46 s

What happens: Batch GD now performs 100 updates and reports a readable 0.02 s; Stochastic GD performs 100 × 320 = 32,000 updates and takes 5.46 s.

RegimeUpdatesTotal timeLoss, epoch 1 → 100
Batch GD1000.02 s0.7077 → 0.3188
Stochastic GD32,0005.46 s0.4020 → 0.2325

Connection to gradient descent: the longer run sharpens both halves of the tradeoff.

  • Convergence. After a full 100 epochs Batch GD reaches 0.3188 — a loss Stochastic GD had already beaten after its second epoch (0.2719). “Needs many epochs to converge” is no longer a prediction; it is measured.
  • Cost. Both regimes push the same amount of data through the network (100 × 320 = 32,000 row-passes each), yet SGD takes roughly 240× longer. The extra time is not extra arithmetic — it is the price of doing that arithmetic in 32,000 tiny Python-level steps instead of 100 vectorized matrix operations. This is the “Speed PER epoch” row of the cheat-sheet, in seconds.
  • Noise does not disappear. Stochastic GD flattens out around 0.22–0.23 and keeps bouncing to the very end (0.2260 → 0.2325). Batch GD’s curve stays perfectly smooth throughout.
A note on timings

Loss values are fully reproducible — the fixed seeds make every number above identical on any machine. Wall-clock times are not: they depend on your CPU, so expect the same ratio rather than the same seconds.

7 Training-Loop Visualisation

The five numbered steps inside fit(), in order. Click a step to see the exact line of code and what it does.

Reading the diagram

Steps 3–7 are the body of the inner batch loop. How many times that body runs before an epoch ends is decided entirely by batch_size — 1 time for Batch GD, n_train times for SGD. Everything else is identical.

8 Experiment / Playground

fit() exposes exactly three experimental knobs: batch_size, epochs and lr. Move them below to see how the loop structure changes. The counts are computed from the loop itself — for i in range(0, n_train, batch_size) with n_train = 320.

What this panel does and does not do

PyTorch cannot run in a browser, so this panel does not train anything. It computes the update arithmetic exactly as the training loop does, and it replays the recorded output whenever your settings match a run that was actually performed.

fit(model, epochs=…, batch_size=…, lr=…)
1 = Stochastic · 320 = n_train = Batch · anything between = Mini-batch
Held fixed for both timed runs so the comparison is fair — 10 in Sections 6.8–6.9, and 100 in Section 6.10.
Only the values used in this lab are selectable: 0.01 (bonus task), 0.1 (default), 1.0.
Regime
Batch GD
Rows per update
320
Updates / epoch
1
Total updates
10

Bonus tasks — early finishers

  • Re-run all three with lr=0.01 — does Batch GD fall even further behind?
  • Add a batch_size=8 run — where do its time and loss land relative to the others?
  • Print loss.item() for the very first and last epoch of each run and tabulate the drop by hand.

9 Key Observations

Each pair of runs used the same epoch count — 10 in Sections 6.8–6.9 and 100 in Section 6.10 — which lets us answer two separate questions. Use the toggle below the legend to switch between the two experiments.

9.1 The Recorded Loss Curves

Batch GD — batch_size=n_train Stochastic GD — batch_size=1

9.2 The Tradeoff

  • Question A — Which is faster? (wall-clock time): Batch GD does 1 update per epoch → finishes quickest (0.0 s). Stochastic does one update per sample (hundreds per epoch) → clearly the slowest (0.59 s).
  • Question B — For the same number of epochs, which converges faster?: the opposite order. Stochastic drops the loss sharply within the first epoch or two (0.6831 → 0.3740 → 0.2825); Batch GD, with a single update per epoch, barely moves in 10 epochs (0.6661 → 0.6269).
RegimeTotal timeLoss drop per epochVerdict
Batch GDfastest (1 update)slowstable but slow to converge
Stochastic GDslowest (N updates)fastfast to learn, expensive & noisy

9.3 Observations Tied Back to the Theory

Observation in the recorded outputTheory it confirms
Batch GD: 10 epochs → 10 updates; loss 0.6661 → 0.6269. “1 Epoch = 1 Weight Update”; needs many epochs to converge since it updates only once per pass (Section 2).
Batch GD wall-clock rounds to 0.0 s over 10 epochs, and reads 0.02 s over 100. Very fast per epoch — one vectorized matrix operation (Sections 2, 4.3). The 10-epoch figure is a display artefact: two-decimal rounding hides the true ≈4.5 ms (Section 6.10).
SGD: loss reaches 0.3740 after a single epoch. “1 Epoch = N Weight Updates”; needs far fewer epochs because it updates so frequently (Section 3).
SGD loss rises at epochs 6, 7 and 10 instead of falling monotonically. High variance / noisy path — the loss can bounce up and down between updates; the jagged zig-zag of Fig. 2 (Sections 3, 4.1).
SGD takes 0.59 s versus 0.0 s for the same 10 epochs. Not vectorized across the dataset — a Python-level loop over rows, so each epoch is slower (Sections 3, 4.3).
Over 100 epochs Batch GD reaches only 0.3188 — worse than the 0.2719 Stochastic GD had after two epochs. Needs many epochs to converge since it updates only once per pass (Section 2); SGD needs far fewer epochs (Section 3).
At 100 epochs both regimes process 32,000 row-passes, yet SGD takes 5.46 s against 0.02 s — about 240×. Vectorization, not arithmetic volume, sets the cost: fully vectorized vs. a Python-level loop over rows (Sections 2, 3, 4.4).
Changing only batch_size moved time and convergence in opposite directions. The double definition of “fast”: speed per epoch vs. speed to convergence (Section 4.3).
Takeaway

Changing only batch_size moved both time and convergence — in opposite directions. “Faster” means different things for seconds vs epochs. Mini-batch GD sits between these two extremes, capturing most of stochastic’s fast convergence at a fraction of the cost — which is why it’s the practical default. In Part 2 you’ll see how PyTorch’s DataLoader automates the batching we did by hand here.

10 Student Checkpoints

Short questions on the material of this lab only. Answers are checked immediately.

Q7Match each theory term to the PyTorch statement that implements it.
Match theory with code
Checkpoint score   0 / 8

10.1 Written Questions

  1. Stochastic GD was slowest in seconds yet dropped the loss fastest, and Batch GD the reverse. Explain both using updates per epoch.
  2. Mini-batch GD (e.g. batch_size=32) was not run above. From its updates per epoch, where would its total time and loss-drop speed fall relative to the two extremes — and why is it the usual choice in practice?
  3. A classmate says “SGD is always best because its loss fell fastest.” Give one reason that’s misleading.

11 Lab Takeaways

Theory learned

  • GD minimises J(θ) by stepping against the gradient: Wn = W0 − η ∂L/∂w.
  • All variants share one backpropagation template; only the updates per epoch differ: 1, N/B, and N.
  • Batch GD is smooth but slow to converge and RAM-hungry; SGD is noisy, memory-light, converges in few epochs and can escape shallow local minima.
  • Mini-batch is the middle ground; powers of two align with binary memory registers, and a remainder batch handles leftover rows.
  • “Fast” has two meanings: per epoch, and to convergence.

What you implemented

  • Loaded and standardised the purchase-prediction data — scaling after the split.
  • Reshaped labels to [N, 1] for nn.BCELoss.
  • Built a 2 → 10 → 10 → 1 sigmoid network with build_model().
  • Wrote an explicit fit() loop whose update frequency is set purely by batch_size.
  • Ran the two extremes for the same 10 epochs and compared loss and wall-clock time.

Theory → PyTorch map

  • θ → model.parameters()
  • J(θ) → nn.BCELoss()
  • forward_propagate → model(xb)
  • backward_propagate → loss.backward()
  • update_weights_and_biases → optimizer.step()
  • η → lr
  • shuffle_data → torch.randperm(n_train)
  • updates per epoch → range(0, n_train, batch_size)
Next

In Part 2 you will see how PyTorch’s DataLoader automates the batching that was done by hand in this lab.

Topic 08 — Gradient Descent & Its Types · Part 1 · Neural Networks / Deep Learning Foundations. All numeric outputs shown are those recorded from an actual run of the code in this handout.