Background
Sections
IntroductionModule 01 β€” Tensors 🧊01 Β· Creating Tensors 🧊02 Β· Indexing & Reshaping πŸ”ͺ03 Β· Tensor Math βž—04 Β· Device Placement πŸ–₯️⚑Module 02 β€” Autograd βš™οΈ01 Β· The Computational Graph πŸ•ΈοΈ02 Β· Backward Pass & Gradients ⬅️03 Β· Turning Autograd Off πŸ›‘04 Β· Gradient Gotchas πŸͺ€Module 03 β€” Neural Networks01 Β· The `nn.Module` Basics 🧱02 Β· Common Layers 🧩03 Β· Building a Network πŸ—οΈ04 Β· Inspecting Models πŸ”Module 04 β€” Data Handling πŸ—‚οΈ01 Β· Dataset Basics πŸ“‡02 Β· The DataLoader 🚚03 Β· Transforms 🎨04 Β· Splits & Built-in Datasets βœ‚οΈModule 05 β€” The Training Loop πŸ”01 Β· Loss Functions 🎯02 Β· Optimizers βš™οΈ03 Β· The Training Loop πŸ”04 Β· Evaluation & Metrics πŸ“ŠModule 06 β€” Saving & Loading πŸ’Ύ01 Β· `state_dict` Basics πŸ’Ύ02 Β· Checkpoints & Resuming ⏸️03 Β· Loading for Inference πŸš€04 Β· Best Model & Early Stopping πŸ…Module 07 β€” Computer Vision πŸ‘οΈ01 Β· Convolutions πŸ”²02 Β· CNN Architecture πŸ›οΈ03 Β· Transfer Learning πŸ”04 Β· Image Classification Project πŸ§ͺModule 08 β€” NLP & Transformers πŸ’¬01 Β· Text Data & Tokenization πŸ”€02 Β· Embeddings 🧭03 Β· Recurrent Layers & LSTMs πŸ”„04 Β· Intro to Transformers ⚑Module 09 β€” Ecosystem: PyTorch Lightning ⚑01 Β· Why Lightning? πŸ€”02 Β· The LightningModule 🧩03 Β· The Trainer πŸŽ›οΈ04 Β· DataModules & Callbacks 🧰Module 10 β€” Deployment & Optimization πŸš€01 Β· Exporting Models πŸ“¦02 Β· `torch.compile` ⚑03 Β· Inference Optimization πŸͺΆ04 Β· Serving & Next Steps πŸŽ“

03 Β· The Training Loop πŸ”

5 min read

This is the payoff. Every concept in the course so far snaps together here into the loop that makes a model learn.


1. The 'Why'

You now have all the pieces: a model (Module 03), batched data (Module 04), a loss (01), and an optimizer (02). The training loop is the small piece of code that orchestrates them into learning. Its job is repetitive by design: for every batch of data, predict, measure the error, compute gradients, and take an optimization step β€” then do it again, batch after batch, epoch after epoch, until the model is good. This loop is the single most-written pattern in all of deep learning, and once it's in your fingers you'll write it almost without thinking.

What makes the loop worth studying carefully is that its correctness depends on getting a specific sequence right, and on managing the model's mode. The canonical order β€” zero gradients, forward, loss, backward, step β€” is not arbitrary: shuffle it and you'll silently accumulate stale gradients or update with the wrong ones (the Module 02 gotcha made real). And because layers like Dropout behave differently while learning versus while being evaluated, you must switch the model into train() mode for the training pass and eval() mode for validation. This sub-module assembles the complete, correct loop β€” including a validation pass β€” that you'll reuse for every model in Phase 3.


2. Core Concepts

Epoch vs. batch. One batch is a single group of samples; one epoch is one full pass over the whole training set. The loop is two nested loops: for epoch: for batch:.

The canonical five steps (per batch). optimizer.zero_grad() β†’ model(x) (forward) β†’ criterion(pred, y) (loss) β†’ loss.backward() β†’ optimizer.step(). Memorize this order.

model.train() vs. model.eval(). Call model.train() before the training pass so Dropout/BatchNorm behave for learning; call model.eval() before validation so they behave for inference. These set a mode flag β€” they don't move data or touch gradients.

torch.no_grad() for validation. During the validation pass you're not learning, so wrap it in torch.no_grad() to skip graph-building β€” faster and lighter. Real evaluation uses both eval() and no_grad() (as previewed in Modules 02–03).

Move data to the device. The DataLoader yields CPU tensors; send each batch (and the model) to the same device inside the loop.


3. Code in Action

The minimal training loop

import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Linear(4, 3)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)

for epoch in range(10):                       # outer loop: epochs
    model.train()                             # training mode ON
    for batch_X, batch_y in train_loader:     # inner loop: batches
        batch_X = batch_X.to(device)          # data to the model's device
        batch_y = batch_y.to(device)

        optimizer.zero_grad()                 # 1. clear old gradients
        logits = model(batch_X)               # 2. forward pass
        loss = criterion(logits, batch_y)     # 3. compute loss
        loss.backward()                       # 4. backward pass
        optimizer.step()                      # 5. update weights

Adding a validation pass

import torch

for epoch in range(10):
    # ---- Train ----
    model.train()
    train_loss = 0.0
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)
        optimizer.zero_grad()
        loss = criterion(model(batch_X), batch_y)
        loss.backward()
        optimizer.step()
        train_loss += loss.item()             # .item() -> Python float, detached

    # ---- Validate ----
    model.eval()                              # inference mode (Dropout off, etc.)
    val_loss = 0.0
    with torch.no_grad():                     # no graph, no gradients
        for batch_X, batch_y in val_loader:
            batch_X, batch_y = batch_X.to(device), batch_y.to(device)
            val_loss += criterion(model(batch_X), batch_y).item()

    print(f"epoch {epoch}: "
          f"train {train_loss/len(train_loader):.4f} | "
          f"val {val_loss/len(val_loader):.4f}")

A reusable structure

def train_one_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total = 0.0
    for X, y in loader:
        X, y = X.to(device), y.to(device)
        optimizer.zero_grad()
        loss = criterion(model(X), y)
        loss.backward()
        optimizer.step()
        total += loss.item()
    return total / len(loader)                # average loss for the epoch

4. Common Pitfalls

Pitfall 1 β€” Wrong step order. Calling optimizer.step() before loss.backward(), or forgetting zero_grad(), breaks learning silently. Burn the order in: zero β†’ forward β†’ loss β†’ backward β†’ step.

Pitfall 2 β€” Forgetting model.train() / model.eval(). If you validate without eval(), Dropout stays active and your validation numbers are noisy and pessimistic; if you train while stuck in eval(), regularization is off. Set the mode explicitly at the top of each phase.

Pitfall 3 β€” Accumulating the loss tensor instead of its value. Writing train_loss += loss (not loss.item()) keeps the whole computational graph alive across the epoch, leaking memory until you crash. Always accumulate loss.item() (or loss.detach()).


5. Further Reading & Watch List


⬅️ Prev: 02 Β· Optimizers Β· ➑️ Next: 04 Β· Evaluation & Metrics