Debug a PyTorch Training Run: NaN Loss, Broadcasting Bugs, Learning Rate and Data Leaks
Hands-on lab · IDE in your browser

Debug a PyTorch Training Run: NaN Loss, Broadcasting Bugs, Learning Rate and Data Leaks

Fix a broken PyTorch training job one symptom at a time: a loss that turns into NaN, a model stuck at the mean because of a silent broadcasting bug, a learning rate past the edge of stability, and a validation score that is too good to be true.

Time
50 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
3 / 5 steps passingToo good to be true · step 4 of 5
diagnose.py▶ Run✓ Check
# ---------- Step 4: too good to be true ----------def single_feature_r2(df, features):    """How much of the target each column explains on its own: the squared correlation between the column and    T.TARGET, over the rows of df. Returns {feature: r2}."""   def suspicious(scores, threshold=0.95):    """The features that explain more than `threshold` of the target on their own, best first.""" 
TerminalOutput

The job

Kellmere Energy forecasts every building's yearly energy use so it can plan supply. A colleague's training script does not work: the loss is NaN, and they have left. You debug it the way professionals do, one measurable symptom at a time, and leave behind a preflight so it never breaks silently again.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Exploding loss

    Kellmere Energy forecasts each building's yearly energy use from what is known before the year starts: floor area, age, insulation, glazing, occupancy and how cold the winter is expected to be.

    You writegrad_norm()
  2. 2

    A model that learns nothing

    The loss is finite now, but it sits at about 1.0.

    You writebaseline_loss()
  3. 3

    The learning rate

    The pipeline can learn now, but the full run still bounces around 1.05.

    You writelr_range_test()
  4. 4

    Too good to be true

    The run trains, and validation says the typical error is 4.8%.

    You writesingle_feature_r2()
  5. 5

    A preflight for every run

    You found four bugs by hand.

    You writepreflight()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Exploding loss

Kellmere Energy forecasts each building's yearly energy use from what is known before the year starts: floor area, age, insulation, glazing, occupancy and how cold the winter is expected to be. A colleague wrote train.py before leaving. The loss turns into NaN within two batches, and they never found out why.

The first question in any blow-up is how big the gradients are. A gradient norm in the billions means every optimiser step flings the weights far away.

Do this

1. Write grad_norm() in diagnose.py, then Run. Read the loss and the gradient norm of the first batches, and the scale of the numbers the network is fed.

2. Fix Prep in train.py:

  • fit() learns the scaling from the training rows.
  • X() standardises every input column.
  • y() standardises log(kWh).
  • to_kwh() turns a network output back into kWh.

3. Run again. The loss should now be finite. It is still stuck near 1, which is the next step's problem.

diagnose.py, the file you edit69 lines
"""Your debugging tools. Each one answers a question about the training run in train.py."""
import math

import numpy as np
import torch

import train as T


# ---------- Step 1: exploding loss ----------
def grad_norm(model):
    """The size of the whole gradient: the square root of the sum of every parameter gradient squared (the L2
    norm over all parameters together). Parameters without a gradient are skipped."""
    # TODO (Step 1): add up p.grad squared over every parameter that has a gradient; return the square root.
    raise NotImplementedError("Step 1: write grad_norm()")


# ---------- Step 2: a model that learns nothing ----------
def baseline_loss(y):
    """The loss of the laziest model: predict the mean of y for every row. MSE of that = the variance of y."""
    raise NotImplementedError("baseline_loss() arrives in Step 2")


def overfit_one_batch(xb, yb, steps=300, lr=1e-2, seed=0):
    """Train a fresh T.make_model() on this one batch only, with Adam(lr) and T.compute_loss, for `steps` steps.
    Returns the final loss. A working pipeline can memorise one batch almost perfectly."""
    raise NotImplementedError("overfit_one_batch() arrives in Step 2")


# ---------- Step 3: the learning rate ----------
def lr_range_test(X, y, lrs, steps=200, seed=0):
    """For every learning rate: torch.manual_seed(seed), a fresh T.make_model(), and T.train() it until `steps`
    batches have run (T.train's on_batch sees each loss). Score = the mean of the last 50 batch losses, or
    float("inf") if any of them is not finite. Returns {lr: score}."""
    raise NotImplementedError("lr_range_test() arrives in Step 3")


def pick_lr(results):
    """The learning rate one step below the one with the lowest score (in the sorted list of tried rates): the
    best rate in a short test sits close to the edge where training becomes unstable."""
    raise NotImplementedError("pick_lr() arrives in Step 3")


# ---------- Step 4: too good to be true ----------
def single_feature_r2(df, features):
    """How much of the target each column explains on its own: the squared correlation between the column and
    T.TARGET, over the rows of df. Returns {feature: r2}."""
    raise NotImplementedError("single_feature_r2() arrives in Step 4")


def suspicious(scores, threshold=0.95):
    """The features that explain more than `threshold` of the target on their own, best first."""
    raise NotImplementedError("suspicious() arrives in Step 4")


# ---------- Step 5: a preflight that catches all four ----------
def preflight(df):
    """Run before every training job, on the meter data df, using train.py as it is now (T.FEATURES, T.CONFIG,
    T.Prep, T.compute_loss, T.make_model). Returns a list of (check, message) for every problem found; empty
    when the job is safe to run. The checks:
      "scaling":       after T.Prep, every input column and the target have |mean| < 0.1 and 0.5 < std < 2.
      "overfit":       overfit_one_batch() on the first 32 training rows ends below 0.05.
      "learning_rate": T.CONFIG["lr"] is higher than the best rate of lr_range_test() over LR_GRID.
      "leak":          suspicious(single_feature_r2()) on the training rows finds any feature.
    """
    raise NotImplementedError("preflight() arrives in Step 5")


LR_GRID = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 0.1, 0.3, 1.0]
Provided for you:buildings.csvnew_buildings.csvtrain.pytry_it.py

Frequently asked questions

Why does my PyTorch loss become NaN?

Usually the gradients are huge because the inputs or the target are on a large scale, so one optimiser step overshoots and the numbers overflow. Measure the gradient norm, standardise the inputs and put a wide-ranging target such as kWh or price on a log scale.

Why is my model stuck predicting the mean?

A common cause is a shape mismatch in the loss: predictions of shape (n, 1) against targets of shape (n,) broadcast to an (n, n) grid, and the best that loss allows is the mean. Try to overfit a single batch; a working pipeline memorises it.

How do I choose a learning rate?

Run a learning-rate range test: train briefly at each rate on a log grid and compare the losses. Pick a rate just below the one with the lowest loss, away from the edge where training diverges.

How do I spot a data leak?

Be suspicious of validation scores that are far better than the problem allows. Check how much each feature explains on its own, and ask whether that column exists at the moment you need to predict.

How to debug a neural network that will not train

Most training failures come from a short list of causes: unscaled data that makes the loss explode, shape mismatches that PyTorch broadcasts without complaint, a learning rate past the edge of stability, and features that leak the answer. In this lab you meet each of them in a real PyTorch job and find it with the standard tool: gradient norms, the overfit-one-batch test, a learning-rate range test and a single-feature leak scan. Then you automate all four into a preflight check.