models · Level 3

Tensors, gradients and a first training loop

Train tiny models on a CPU in seconds, by hand and then with automatic differentiation, and see what training is and is not.

By Mickarle Wagstaff-Irons - Micky Irons

  • Level 3Building
  • 95 min
  • 6 chapters
  • Online course and PDF workbook
The Structuremodels / 03

Read, practise and review

Your next step

Start this course online

Register your start, then work through the lessons and exercises at your own pace. You can download the PDF separately. Where a scored exam is available, you will find it after the lessons.

Register or resume your course

Mickai privately records this registration and sends its team a course-start notification. It records your intention to start, not proof of attendance or completion. Our course-start privacy notice explains the details and the 30-day resume cookie.

Follow the Mickai company page on LinkedIn

You can start learning without subscribing or following. Newsletter email confirmation and provider enrolment are recorded separately.

Start with the essentials

The short answer

Training repeats one loop: run the model on examples, score how wrong it is with a single number called the loss, compute the gradient (which way each parameter should move to lower that loss), and take a small step that way. Tiny models train on an ordinary CPU in seconds. Published frontier-scale language-model runs use the same idea with vastly more data, hardware, engineering and cost.

What you will learn

  • You will be able to read a tensor's shape and dtype and predict when broadcasting will stretch it, including the cases that fail silently.
  • You will be able to compute a mean squared error loss and its gradient by hand, and check it by two independent routes.
  • You will be able to spot a learning rate that is too small or too large from the loss alone.
  • You will be able to write the PyTorch training loop with batches and epochs, and train a tiny network on non-linear data.
  • You will be able to separate training from validation, recognise overfitting and find the classic training bugs.
  • You will be able to explain, with sources, what changes when language models are pretrained at frontier scale.

Who it is for

People comfortable writing small Python programs who want to understand what training a model actually means by doing it. You need a computer that can run Python 3.12 (the version tested here) and install two packages; on a Mac, PyTorch 2.14.0 needs Apple silicon and macOS 14 or later. No GPU, no account and no dataset download.

Before you start

  • Comfort with Python basics: variables, loops, functions and running a script from a terminal, as taught in Python: from first script to a useful automation. School algebra is enough; the one idea from calculus you need, a slope, is explained as you go.

Learn in your browser · Chapter 01 of 06

01

Tensors: shape, dtype and broadcasting

Everything a model reads, stores and learns is a grid of numbers. Learn to read one before trusting any result.

A tensor is a grid of numbers in any number of dimensions (NumPy says array). It has a shape, the size of each dimension, and a dtype, the kind of number stored, such as float64 or int64.

Set up a virtual environment in a new folder (PowerShell shown; on macOS and Linux use python3, then .venv/bin/python). Pinned versions keep results comparable.

bash · 2 lines
python -m venv .venv
.venv\Scripts\python -m pip install numpy==2.5.3 torch==2.14.0

This follows the CPU choice in PyTorch's install selector, pinned and without the unneeded torchvision: for Windows and macOS it gives pip3 install torch torchvision. Its Linux CPU line adds an --index-url option, so on Linux install numpy==2.5.3 alone, then torch==2.14.0 with that option. Only the Windows lines were run here. Save every script in this folder; per the venv documentation, calling the environment's Python needs no activation: .venv\Scripts\python shapes.py.

python · 12 lines
# shapes.py
import numpy as np

x = np.array([1.0, 2.0, 3.0])
m = np.array([[1, 2, 3], [4, 5, 6]])
print(x.shape, x.dtype, m.shape, m.dtype)
print(m + x)                  # x is stretched across both rows
col = x.reshape(3, 1)
print((col - x).shape)        # (3, 1) and (3,) broadcast to (3, 3)
counts = np.array([1, 2, 3])
counts[0] = 0.9               # an integer array silently drops the .9
print(counts)

text · 5 lines
(3,) float64 (2, 3) int64
[[2. 4. 6.]
 [5. 7. 9.]]
(3, 3)
[0 2 3]

Broadcasting combines different shapes. Compare shapes from the right: sizes fit if they are equal or one is 1, and a missing dimension counts as 1. So x (3,) is reused for each row of m (2, 3). The danger: a column (3, 1) minus a row (3,) silently becomes a (3, 3) table of every pairing.

Try it yourself · Activity 01

5 min

Predict the shape, then check

Start .venv\Scripts\python, type import numpy as np, and predict each result before running it.

  1. (np.ones((4, 1)) + np.ones(4)).shape
  2. np.ones((2, 3)) + np.ones(2), then (np.ones((2, 3)) + np.ones(3)).shape

Which would you rather meet as an error than as a result?

Worked answer

(4, 4): a column and a row stretched into a table. The second raises ValueError: operands could not be broadcast together with shapes (2,3) (2,): the last sizes, 3 and 2, differ and neither is 1. With np.ones(3): (2, 3).

Next chapter

Learn in your browser · Chapter 02 of 06

02

The loss and the gradient, by hand

Training needs one number that says how wrong the model is, and a way to tell which direction makes it smaller.

toy.py invents 20 points near y = 3x + 2. Its seed fixes the random numbers, so every run makes the same data. The model is a line with two parameters, weight w and bias b: prediction = w * x + b.

python · 15 lines
# toy.py: data, loss and gradient
import numpy as np

rng = np.random.default_rng(seed=0)
x = rng.uniform(-1, 1, size=20)
y = 3 * x + 2 + rng.normal(0, 0.1, size=20)   # a line plus noise


def loss(w, b):
    return np.mean((w * x + b - y) ** 2)


def gradient(w, b):
    err = w * x + b - y
    return np.mean(2 * err * x), np.mean(2 * err)

The loss scores 'how wrong' as one number. Here it is the mean squared error: the average of (prediction minus target) squared, so big misses count most.

The gradient holds the loss's slope for each parameter. It points uphill, says the Deep Learning textbook, so its negative points downhill. Differentiating gives the slopes in gradient: the mean of 2 * err * x for w, and of 2 * err for b.

Always check a hand-derived gradient, for instance by nudging each parameter slightly both ways and measuring the change in loss: a finite difference.

python · 7 lines
# check.py
from toy import loss, gradient

w, b, h = 0.5, -1.0, 1e-6          # h is a tiny nudge
print("by hand %.6f %.6f" % gradient(w, b))
print("nudged  %.6f %.6f" % ((loss(w + h, b) - loss(w - h, b)) / (2 * h),
                             (loss(w, b + h) - loss(w, b - h)) / (2 * h)))

text · 2 lines
by hand -2.194225 -6.135977
nudged  -2.194225 -6.135977

They agree to six decimals. Both slopes are negative, so raising w and b lowers the loss: the true 3 and 2 are above 0.5 and -1.

Try it yourself · Activity 02

15 min

One step by hand

With pencil and paper, take the points x = 1, 2, 3 and y = 5, 8, 11 (exactly on y = 3x + 2), starting at w = 0, b = 0.

  1. Work out the errors (prediction minus target) and the loss.
  2. Work out the slopes for w and b.
  3. Take one step with learning rate 0.1 (subtract 0.1 times each slope), find the new loss, and check it in Python.

Why did one step help so much? Would it always?

Worked answer

Errors -5, -8, -11; loss (25 + 64 + 121) / 3 = 70. Slope for w: 2 x (-5 - 16 - 33) / 3 = -36. Slope for b: 2 x (-24) / 3 = -16. New w = 3.6, b = 1.6; new errors 0.2, 0.8, 1.4; loss (0.04 + 0.64 + 1.96) / 3 = 0.88. With x = np.array([1, 2, 3]) and y = np.array([5, 8, 11]), print(np.mean((3.6 * x + 1.6 - y) ** 2)) prints 0.8800000000000008, a floating-point rounding artefact.

Next chapter

Learn in your browser · Chapter 03 of 06

03

Gradient descent and the learning rate

Gradient descent repeats one move: a small step downhill, then look again. The step size decides everything.

Each update is: new value = old value minus learning rate (a small positive number you choose) times slope. descend.py takes 50 steps from w = 0, b = 0 (loss 7.9804) at three rates.

python · 8 lines
from toy import loss, gradient

for lr in [0.01, 0.3, 1.1]:
    w, b = 0.0, 0.0
    for step in range(50):
        gw, gb = gradient(w, b)
        w, b = w - lr * gw, b - lr * gb   # step downhill
    print(f"lr {lr}: w {w:.3f} b {b:.3f} loss {loss(w, b):.4g}")

text · 3 lines
lr 0.01: w 1.035 b 1.313 loss 2.104
lr 0.3: w 2.971 b 2.007 loss 0.004496
lr 1.1: w -893.426 b -21299.910 loss 4.55e+08

At 0.01 the loss is still 2.104: right direction, too slow. At 0.3 it reaches 0.004496, the best any line can do here, as NumPy's least-squares line fit np.polyfit(x, y, 1) confirms. At 1.1 the first five losses are 6.599, 9.495, 13.722, 19.834 and 28.668: each step overshoots further.

A rising or swinging loss means too large a rate. PyTorch's tutorial warns that small values learn slowly and large ones may behave unpredictably. Try several, spread well apart, watching the loss.

Try it yourself · Activity 03

10 min

Find the edge

Explore between the three rates in descend.py.

  1. Change the list to [0.03, 0.1, 1.0] and run it.
  2. Set the list to [0.01] and range(50) to range(500), then run it.

What does 1.0 tell you about where 'too large' begins?

Worked answer

Losses: 0.2932 at 0.03 and 0.005115 at 0.1. At 1.0, w 2.871 b -0.354 loss 5.595: just past the edge, bouncing without settling while the loss slowly creeps up. With 500 steps, 0.01 reaches 0.005355: a small rate gets there, ten times more slowly. The edge sits near 1.0, for this data alone.

Next chapter

Learn in your browser · Chapter 04 of 06

04

Automatic differentiation and the training loop

Hand-derived gradients do not scale. Let PyTorch compute them, check it, then write the standard training loop.

Automatic differentiation (autograd) records the forward calculation as a graph, then applies the chain rule back through it; that backward sweep is backpropagation. Mark a tensor requires_grad=True, call loss.backward(), and its slope appears in .grad. This uses check.py's point, in float64 to match NumPy.

python · 12 lines
# autograd_check.py
import torch
from toy import x, y, gradient

xt, yt = torch.tensor(x), torch.tensor(y)
w = torch.tensor(0.5, dtype=torch.float64, requires_grad=True)
b = torch.tensor(-1.0, dtype=torch.float64, requires_grad=True)
print("by hand    %.6f %.6f" % gradient(0.5, -1.0))
for run in ["autograd  ", "not zeroed"]:
    loss = ((w * xt + b - yt) ** 2).mean()      # forward and loss
    loss.backward()                              # backward
    print(run, "%.6f %.6f" % (w.grad.item(), b.grad.item()))

text · 3 lines
by hand    -2.194225 -6.135977
autograd   -2.194225 -6.135977
not zeroed -4.388451 -12.271953

Three routes agree; in full, autograd and hand differ by under 1e-15, float64 rounding. The last line is a classic bug: the backward documentation says gradients accumulate, so a second pass without clearing doubled them.

Now the real loop, in float32, PyTorch's default. Linear(1, 1) is the same line, MSELoss the same loss, SGD the same update. A batch is the examples behind one update; an epoch is one pass through the data, here 4 batches of 5.

python · 23 lines
# train_line.py
import torch
from toy import x, y

torch.manual_seed(0)
X = torch.tensor(x, dtype=torch.float32).reshape(20, 1)
Y = torch.tensor(y, dtype=torch.float32).reshape(20, 1)
model = torch.nn.Linear(1, 1)
loss_fn = torch.nn.MSELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.1)

for epoch in range(1, 31):
    order = torch.randperm(20)                 # shuffle each epoch
    for start in range(0, 20, 5):              # batches of 5
        batch = order[start:start + 5]
        opt.zero_grad()                        # 1 clear
        loss = loss_fn(model(X[batch]), Y[batch])  # 2 forward, 3 loss
        loss.backward()                        # 4 backward
        opt.step()                             # 5 step
    if epoch % 10 == 0:
        with torch.no_grad():
            print(epoch, round(loss_fn(model(X), Y).item(), 5))
print(round(model.weight.item(), 3), round(model.bias.item(), 3))

text · 4 lines
10 0.00759
20 0.00452
30 0.0045
2.971 2.007

The numbered steps are the training loop; PyTorch's documentation clears gradients first, though clearing after the step works too. torch.no_grad() stops tracking while you only measure, and .reshape(20, 1) gives targets the predictions' shape.

Try it yourself · Activity 04

10 min

Change the batch size

In train_line.py, change the two 5s on the batch lines to 1, run it, then try 20.

  1. First work out how many updates each version makes in 30 epochs.
  2. Compare the losses at epochs 10, 20 and 30.

Which would you choose if each update were expensive?

Worked answer

Batches of 1: 600 updates; losses 0.0046, 0.00557, 0.00468. Fast but jittery: each step sees one noisy point. Batches of 20: 30 updates; losses 0.65896, 0.11635, 0.02439, ending 2.751 2.014. Smooth, but unfinished.

Next chapter

Learn in your browser · Chapter 05 of 06

05

A tiny network, validation and overfitting

A straight line cannot fit every pattern. Add a hidden layer, then learn to tell learning from memorising.

No straight line separates XOR-shaped data: points near (0, 1) and (1, 0) are one class, points near (0, 0) and (1, 1) the other. The Deep Learning textbook shows a linear model fitted to the four XOR points outputting 0.5 everywhere. A multilayer network adds a hidden layer and a non-linear function (Tanh).

xor.py makes 200 noisy points, holds the last 50 back as a validation set, and trains a line, then a 33-parameter network. Targets are 0 or 1, so the loss is binary cross-entropy (BCEWithLogitsLoss takes raw scores).

python · 30 lines
import torch

torch.manual_seed(0)
corners = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
pick = torch.randint(0, 4, (200,))
X = corners[pick] + 0.3 * torch.randn(200, 2)
Y = torch.tensor([0.0, 1.0, 1.0, 0.0])[pick].reshape(200, 1)  # XOR labels
loss_fn = torch.nn.BCEWithLogitsLoss()


def train(model, n=150, report=(2000,)):
    opt = torch.optim.SGD(model.parameters(), lr=0.5)
    for epoch in range(1, 2001):
        opt.zero_grad()
        loss = loss_fn(model(X[:n]), Y[:n])    # learn from the first n
        loss.backward()
        opt.step()
        if epoch in report:                    # judge on the last 50
            with torch.no_grad():
                out = model(X[150:])
                acc = ((out > 0).float() == Y[150:]).float().mean().item()
            print(epoch, "train %.3f valid %.3f accuracy %.0f%%"
                  % (loss.item(), loss_fn(out, Y[150:]).item(), 100 * acc))


print("line")
train(torch.nn.Linear(2, 1))
print("network")
net = torch.nn.Sequential(torch.nn.Linear(2, 8), torch.nn.Tanh(), torch.nn.Linear(8, 1))
train(net, report=(200, 400, 1000, 2000))

text · 7 lines
line
2000 train 0.680 valid 0.746 accuracy 54%
network
200 train 0.327 valid 0.330 accuracy 90%
400 train 0.249 valid 0.269 accuracy 90%
1000 train 0.222 valid 0.306 accuracy 86%
2000 train 0.210 valid 0.364 accuracy 88%

The line's training loss, 0.680, is near 0.693 (ln 2), the score for always answering 50:50, and 54% is near a coin toss. The network reaches 90%.

Training loss keeps falling, but validation loss bottoms at 0.269 near epoch 400 (at 384, checked every epoch), then rises to 0.364: the network has started fitting noise. The textbook calls this overfitting, the gap between training and test error growing too large. Remedies: keep the weights from the best validation epoch (early stopping), add data, or shrink the model.

Seeds. torch.manual_seed(0) fixes the data and starting weights; every script here printed identical output in three fresh environments on one Windows 11 machine (macOS and Linux were not tested). The NumPy and PyTorch documentation expect identical numbers only with the same versions on the same platform, so elsewhere your numbers may differ. Compare the pattern first and, as a rough rule of thumb, the first two significant figures.

Try it yourself · Activity 05

15 min

Overfit on purpose, then change the seed

Make the network memorise, then see how much one seed decides.

  1. In xor.py, change both 8s to 64 and call train(net, n=12, report=(200, 400, 1000, 2000)).
  2. Undo that, set the seed to 1 and run again.

How many seeds would you run before believing a pattern?

Worked answer

With 257 parameters and 12 points, training loss falls to 0.002 while validation loss climbs from 1.653 to 2.730, accuracy 48% to 56%: memorised, not learned. With seed 1 the line scores 38% and the network's validation loss keeps falling, to 0.186. Fifty validation points is a small sample: try several seeds.

Next chapter

Learn in your browser · Chapter 06 of 06

06

Classic bugs, and what changes at frontier scale

Most failed training runs fail quietly. Learn the usual suspects, then see what this toy leaves out.

Scroll sideways to see every column.

Classic bugs, and what changes at frontier scale · Table 1
BugSymptomFix
Gradients never clearedOversized, erratic stepsopt.zero_grad() each update
Silent broadcastingLoss stalls high; perhaps a size warningMatch prediction and target shapes
Judging on training dataPoor results on new dataHold out validation data
Learning rate wrongLoss rises, swings or crawlsTry values spread well apart
Integer dtypeTruncated values, or a RuntimeError when asking for gradientsUse floats: 1.0, not 1

Save this broken loop as buggy.py and run it. The target is w = 3, b = 2.

python · 15 lines
import torch
from toy import x, y

torch.manual_seed(0)
X = torch.tensor(x, dtype=torch.float32).reshape(20, 1)
Y = torch.tensor(y, dtype=torch.float32)
model = torch.nn.Linear(1, 1)
loss_fn = torch.nn.MSELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.1)

for step in range(100):
    loss = loss_fn(model(X), Y)
    loss.backward()
    opt.step()
print(round(loss.item(), 4), round(model.weight.item(), 3), round(model.bias.item(), 3))

text · 1 line
4.5777 -0.057 1.767

PyTorch also prints a UserWarning that the target size differs from the input size. Never scroll past warnings.

Try it yourself · Activity 06

15 min

Fix the loop

buggy.py contains two bugs from the table.

  1. Fix one and run, then fix the other and run.
  2. Explain each wrong result.

Which bug would you have caught without the warning?

Worked answer

Y has shape (20,), so against (20, 1) predictions it broadcasts to a 20 x 20 comparison: add .reshape(20, 1). And opt.zero_grad() is missing. Fixing only the clearing prints 3.6526 0.0 2.08: matched against every target, the best line ignores x and predicts their mean, about 2.08. Fixing only the shape prints 3.9772 5.192 1.587: piled-up gradients overshoot. Fixing both prints 0.0045 2.97 2.007.

Toy training is not frontier pretraining. The loop is the same idea, but published reports of large language-model runs show how much surrounds it (GPU memory maths covers the sums). These figures describe particular research runs, not how any current product is trained.

Scroll sideways to see every column.

Classic bugs, and what changes at frontier scale · Table 2
AspectThis workbookPublished reports
Data220 invented pointsScaling-laws paper: 2.29 x 10^10 tokens from 20.3 million web documents. OPT: about 180 billion tokens, near-duplicates removed
HardwareOne CPU, secondsOPT: 992 GPUs. Megatron paper: training steps for a 1-trillion-parameter model on 3,072 GPUs, combining data, tensor and pipeline parallelism
Precisionfloat32, float64Mixed-precision paper: 16-bit storage and arithmetic (accumulating in 32-bit), a 32-bit master copy of the weights, loss scaling
OptimiserPlain SGD, fixed rateAdam-style optimisers; scaling-laws paper: 3,000 warm-up steps, then cosine decay to zero
Batch1 to 150 examplesScaling-laws paper: an ideal batch of roughly 1 to 2 million tokens for its largest models
FailuresRerun in secondsOPT: hardware failures contributed to at least 35 manual restarts from checkpoints in 2 months; loss divergences met by lowering the rate
Evaluation and safety50 held-out pointsOPT: 16 standard language tasks, plus hate-speech, stereotype and toxicity benchmarks
CostNegligibleOPT: such models often take hundreds of thousands of compute days; an estimated 75 tons of CO2 equivalent for its largest

The scaling-laws paper found loss falls as a power law as model size, data and compute grow, some trends spanning more than seven orders of magnitude; the compute-optimal paper trained over 400 models, from 70 million to over 16 billion parameters, to work out how best to spend a compute budget. The toy teaches the idea, not the practice.

Assessment options

Keep learning

The complete workbook

This workbook teaches training by doing it at toy scale. You will compute a gradient by hand in NumPy, confirm it with PyTorch's automatic differentiation, then fit a straight line and a tiny network while watching the learning rate, validation loss and common bugs. It ends with a sourced account of what changes when language models are pretrained at frontier scale.

  1. 01
    Tensors: shape, dtype and broadcasting

    Everything a model reads, stores and learns is a grid of numbers. Learn to read one before trusting any result.

    Read here · 1 exercise
  2. 02
    The loss and the gradient, by hand

    Training needs one number that says how wrong the model is, and a way to tell which direction makes it smaller.

    Read here · 1 exercise
  3. 03
    Gradient descent and the learning rate

    Gradient descent repeats one move: a small step downhill, then look again. The step size decides everything.

    Read here · 1 exercise
  4. 04
    Automatic differentiation and the training loop

    Hand-derived gradients do not scale. Let PyTorch compute them, check it, then write the standard training loop.

    Read here · 1 exercise
  5. 05
    A tiny network, validation and overfitting

    A straight line cannot fit every pattern. Add a hidden layer, then learn to tell learning from memorising.

    Read here · 1 exercise
  6. 06
    Classic bugs, and what changes at frontier scale

    Most failed training runs fail quietly. Learn the usual suspects, then see what this toy leaves out.

    Read here · 1 exercise

Also inside: a 8-point checklist, a glossary of 12 terms and 10 questions and answers to test yourself. 6 hands-on exercises, each with a worked answer at the back where the workbook gives one.

The PDF is free. We ask for your name and email before downloading. Read the privacy notice at the download form. Free to download and use for personal learning, study groups and inside your own team. Please do not resell the workbooks or republish them as your own. Link people to trust-agent.ai instead.

Get your free PDF workbook

Enter your name and email, then download the complete workbook. This form also works without JavaScript.

We privately record your name, email and request IP for this download. Read our privacy notice and retention policy.

Visit Mickai on LinkedIn

Newsletter confirmation does not affect your download. Following Mickai on LinkedIn is optional; this site does not verify followers.

Test yourself

Questions and answers

Do I need a GPU for this workbook?

No. Every script runs on an ordinary CPU in seconds, using PyTorch's CPU-only build. The data is invented by seeded scripts, so there is nothing to download and no account to create.

What is the difference between a NumPy array and a PyTorch tensor?

Both are grids of numbers with a shape and a dtype, and both broadcast the same way. PyTorch adds automatic differentiation and support for accelerators. This workbook uses NumPy for the hand-worked gradient and PyTorch for autograd and the network, and torch.tensor(array) copies one into the other.

What exactly is a gradient?

For each parameter, it is the slope of the loss: how fast the loss rises if you nudge that parameter up. Together the slopes point uphill, so gradient descent moves each parameter a little in the opposite direction, then recomputes.

How do I choose a learning rate?

By experiment. Try a few values spread well apart and watch the loss. If it rises or swings wildly, the rate is too large; if it barely moves, too small. The best value depends on the data and the model, so it moves when either changes.

Why do I have to zero the gradients?

PyTorch adds each new gradient to whatever is already stored, so without clearing, every update uses the sum of old and new gradients. Call optimizer.zero_grad() once per update. The PyTorch documentation clears at the start of each step; clearing straight after the step also works.

What is the difference between a batch and an epoch?

A batch is the group of examples behind one parameter update. An epoch is one full pass through the training data. With 20 examples in batches of 5, one epoch makes 4 updates. Smaller batches give more, noisier updates per epoch.

Why does my validation loss rise while my training loss falls?

The model is starting to fit noise in the training data rather than the pattern: overfitting. Keep the weights from the epoch with the best validation loss, add data, simplify the model, and check several seeds before drawing conclusions from a small validation set.

Why do my numbers differ slightly from the workbook's?

The printed numbers came from Windows 11, and the NumPy and PyTorch documentation expect identical numbers only with the same versions on the same platform. Use the pinned versions and the same seeds, then compare the overall pattern and, as a rule of thumb, the first two significant figures, rather than every digit.

My model predicts almost the same value for every input. Why?

Check shapes first. If targets have shape (N,) and predictions (N, 1), broadcasting compares every prediction with every target, and under a squared-error loss the best a model can do is predict their mean. Also check for a model too simple for the data, such as a line on XOR, or a learning rate far too small.

Is this how large language models are trained?

The core loop is the same idea, but published reports show far more around it: huge curated datasets, hundreds or thousands of accelerators working in parallel, mixed precision, tuned optimisers and schedules, checkpoints and restarts, extensive evaluation and substantial cost. This workbook teaches the idea, not frontier pretraining.

Write as you go

My notes

Use this scratchpad for your own notes, working or answers as you read. Notes are saved in this browser on this device. Downloading makes a local copy; it does not send your notes to Mickai.

Consolidate your learning

Review your work

Work through the exercises and questions, then compare your reasoning with the worked answers. A scored exam and verifiable certificate are not yet available for this course.

Review the questions

Learn the language

Key terms

Tensor
A grid of numbers with any number of dimensions. NumPy calls it an array.
Shape
The size of each dimension of a tensor, such as (20, 1) for 20 rows and 1 column.
Dtype
The kind of number a tensor stores, such as float32, float64 or int64. Gradients need floating-point dtypes.
Broadcasting
Automatically stretching tensors of different shapes so they can be combined. Shapes are compared from the right; sizes must be equal or 1.
Loss
A single number scoring how wrong a model's predictions are. Training tries to make it smaller.
Gradient
The slope of the loss with respect to each parameter. It points uphill, so training steps the opposite way.

6 of the workbook's 12 terms. The complete glossary is in the workbook.

Follow the evidence

Sources and checks

Facts last checked: .

Examples in this workbook were run on: Python 3.12.10 on Windows 11, in fresh virtual environments with numpy 2.5.3 and torch 2.14.0 (CPU build, reported as 2.14.0+cpu), installed by pip 25.0.1 (2026-09-26).

These workbooks use AI assistance. See how the workbooks are made.

  1. Start Locally (installation selector)PyTorch
  2. torch 2.14.0 release filesPython Package Index (PyPI)
  3. venv: creation of virtual environmentsPython Software Foundation
  4. Broadcasting (NumPy 2.5 user guide)NumPy
  5. Data types (NumPy 2.5 user guide)NumPy
  6. Random Generator (NumPy 2.5 reference)NumPy
  7. Random compatibility policy (NumPy 2.5 reference)NumPy
  8. numpy.polyfit (NumPy 2.5 reference)NumPy
  9. Broadcasting semantics (PyTorch 2.14)PyTorch
  10. Tensor attributes: dtype (PyTorch 2.14)PyTorch
  11. torch.tensor (PyTorch 2.14)PyTorch
  12. torch.set_default_dtype (PyTorch 2.14)PyTorch
  13. Autograd mechanics (PyTorch 2.14)PyTorch
  14. torch.Tensor.backward (PyTorch 2.14)PyTorch
  15. torch.optim (PyTorch 2.14)PyTorch
  16. torch.optim.Optimizer.zero_grad (PyTorch 2.14)PyTorch
  17. torch.no_grad (PyTorch 2.14)PyTorch
  18. MSELoss (PyTorch 2.14)PyTorch
  19. BCEWithLogitsLoss (PyTorch 2.14)PyTorch
  20. Optimizing model parameters (tutorial)PyTorch
  21. Reproducibility (PyTorch 2.14)PyTorch
  22. Deep Learning, chapter 4: Numerical computation (Goodfellow, Bengio and Courville)MIT Press, free online edition
  23. Deep Learning, chapter 5: Machine learning basics (Goodfellow, Bengio and Courville)MIT Press, free online edition
  24. Deep Learning, chapter 6: Deep feedforward networks (Goodfellow, Bengio and Courville)MIT Press, free online edition
  25. Scaling Laws for Neural Language ModelsarXiv, 2001.08361
  26. Training Compute-Optimal Large Language ModelsarXiv, 2203.15556
  27. OPT: Open Pre-trained Transformer Language ModelsarXiv, 2205.01068
  28. Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LMarXiv, 2104.04473
  29. Mixed Precision TrainingarXiv, 1710.03740

Created by Mickarle Wagstaff-Irons - Micky Irons with the Mickai team. Published by Mickai LTD. Last updated 26 September 2026.

NextKeep going

Where to go next