← Foundation lessonsFoundations · lesson 1

PyTorch basics

You know how to write a Python function that turns an input into an answer.

But what if you only have examples of the answers you want, and need the computer to find the rule? PyTorch gives us the tools for that job.

We’ll start with how it holds numbers, then follow one small model as it learns from a mistake.

Model
in machine learning, a rule that turns inputs into answers. From here on we use that name.

Overview

Imagine we give a model the number 2 and want it to answer 5. At first it answers 2.

We score how wrong that is, work out which way to adjust the model’s numbers, and adjust them a little.

Its next answer is 3.5: closer, not yet right.

That is one learning update. Repeating it over many examples is training.

Make a guessModel takes input 2; returns 2.Score the missCompare guess 2 with answer 5.Adjust the modelFind a direction, then move its numbers.

We’ll take three stops: organize the numbers, build an adjustable model, then follow the update.

Who does what:

  • PyTorch does the arithmetic and works out the adjustments.
  • We choose the examples, the model, how to score a miss, and how big a step to take.

Tensors

Suppose you have three inputs: 1, 2, 3. PyTorch keeps them in a tensor: a block of numbers you can calculate with all at once. The difference from a Python list shows up as soon as you do arithmetic:

A Python list times 2 becomes six numbers, the list repeated. A tensor times 2 stays three numbers, each doubled. list [1, 2, 3] * 2 123123 repeated tensor [1, 2, 3] * 2 246 each number doubled
Same-looking brackets, different meaning.

A tensor’s arithmetic applies to every number inside it.

examples/pytorch_tensors.py · numbers and shapeOpen source ↗
import torch

inputs = torch.tensor([[1.0], [2.0], [3.0]])
print(inputs * 2)              # three rows containing 2, 4, 6
print(inputs.shape)            # torch.Size([3, 1])
print(inputs[0])               # tensor([1.]) — the first row
print(inputs[0, 0].item())     # 1.0 — one plain Python number
Full file · pytorch_tensors.py · 10 lines
"""Run with: python3 examples/pytorch_tensors.py"""
import torch

inputs = torch.tensor([[1.0], [2.0], [3.0]])
print([1.0, 2.0, 3.0] * 2)  # a Python list repeats itself
print(inputs * 2)           # a tensor doubles every number
print(inputs.shape)
print(inputs[0])
print(inputs[0, 0].item())
print(inputs.dtype, inputs.device)  # the defaults: float32, on the CPU
  • Each inner list becomes a row.
  • inputs * 2 makes a new tensor and leaves inputs alone.
  • inputs[0] picks a row.
  • .item() turns a one-number tensor into a plain Python number.

Shape counts the entries along each direction. In this lesson each row is one example, and each column is one number describing it:

Shape (3,) is one row of three numbers. Shape (3, 1) is three rows of one number, one row per example. Shape (1, 1) is a single number in one row: our single example. shape (3,) 123 one row of three numbers shape (3, 1) 123 example 1example 2example 3 three rows · one number each shape (1, 1) 2 our single example
(3,) and (3, 1) hold the same three numbers, arranged differently.

PyTorch layers expect the second kind: rows of examples.

Every tensor also has a dtype, its number format, and a device, where it lives (CPU or GPU). The defaults, float32 on the CPU, are what this lesson uses; float32 allows fractions such as 1.6.

Check yourself: three examples, two numbers each — what shape?

(3, 2): three rows with two numbers in each. For example, each row might hold a ball’s height and velocity.

Linear model

Our model multiplies the input by a weight, then adds a bias:

\[ \text{guess} = \text{weight} \times \text{input} + \text{bias} \]

Drawn as a line, the weight is how steep the line is, and the bias is where it crosses input 0. We start with weight 1 and bias 0:

At input 2 the line gives 2, but we wanted 5: the dashed gap is the miss.

Learning will tilt and lift the line until it passes through the wanted point.

In PyTorch this model is nn.Linear(1, 1). The two numbers are how many numbers go in and how many come out: one input, one output.

With one of each, the layer holds exactly one weight and one bias. That match is a coincidence: a layer has one weight for every input-output pair, and one bias per output. Bigger layers, in Neural network: one input, have many more.

Module
an object that holds a calculation together with its adjustable numbers. nn.Linear is one.
Parameters
a module’s adjustable numbers. model.parameters() hands them to whatever will adjust them.

Calling model(input_value) returns the guess as a tensor.

Loss

The guess 2 is three short of 5. We turn the miss into one score by squaring it: \((2 - 5)^2 = 9\).

Loss
one number that scores how wrong the model is. Lower is better.

Why square the miss?

  • It makes a miss in either direction positive: a guess of 8 also scores 9.
  • It makes big misses count far more than small ones: a guess of 4 scores only 1.
  • A perfect 5 scores 0.

Plot the score for every possible guess and you get a bowl:

Every possible guess for the wanted answer 5, with its loss; one update takes us from loss 9 to 2.25.

Learning means moving the guess down into the bowl.

PyTorch’s nn.MSELoss computes this score. MSE stands for mean squared error: the average of the squared misses.

With a single example, the average is just the square: 9.

Gradient

From guess 2, moving right goes downhill. But we can’t move the guess directly: it comes from the weight and the bias. So we ask each of them, “if I nudge you up a little, what happens to the loss?”

Nudge weight 1 → 1.01guess 2.02 · loss 9 → 8.88 · \(\dfrac{-0.12}{0.01} \approx -12\)
Nudge bias 0 → 0.01guess 2.01 · loss 9 → 8.94 · \(\dfrac{-0.06}{0.01} \approx -6\)

The loss changes about −0.12 for a nudge of 0.01: an estimated rate of −12.

A smaller nudge gets closer to the slope right at the starting point.

Gradient
the slope of the loss for one parameter: how fast the loss changes as that parameter moves. The weight’s gradient here is −12.

It is negative because raising the weight lowers the loss.

The bias’s gradient is only −6. Nudging the bias by 0.01 moves the guess by 0.01; nudging the weight moves it by 0.02, because the weight is multiplied by the input 2.

  • loss.backward() computes these rates for every parameter at once, exactly, without trying any nudges.
  • It writes each one beside its parameter, in a slot called .grad.
  • It does not change the parameters.

The optimizer makes the move. Plain SGD subtracts \(\text{learning rate} \times \text{gradient}\) from each parameter. (What “stochastic” means is explained with batches in Gradients and backpropagation.) With a learning rate of 0.05:

Weight\(1 - 0.05 \times (-12) = 1.6\)
Bias\(0 - 0.05 \times (-6) = 0.3\)
New guess\(1.6 \times 2 + 0.3 = 3.5\) · loss \((3.5 - 5)^2 = 2.25\)

We choose the learning rate. The gradient only describes the slope where we are standing, so a step that is too big can jump right past the bottom of the bowl. Try it below.

The Gradients and backpropagation lesson slows this step down: why each parameter gets its own direction, the chain rule, and how a batch averages its examples.

Try an update

Predict first, then press. At the defaults, one step reproduces the worked example.

Then press Reset, pick learning rate 0.1, 0.2 or 0.3 under Change the example, and watch what the guess does in the bowl.

The loss bowl
The model as a line
Guess2.00
Squared loss9.00
Weight · bias1.00 · 0.00
Gradients (.grad)—

Predict: should the weight go up or down to bring the guess toward 5? Then press Find the gradients.

Change the example

Update steps

In code, the update is a short sequence of calls, and the order matters.

Step through it with input 2, answer 5 and learning rate 0.05. Each call changes one thing: watch which box changes.

Swipe the diagram sideways to follow the arrows.

Forward calculation and parameter update Input enters the model, producing a guess. Loss compares the guess with the answer. Backward writes gradients. Step uses gradients to change the parameters that the next model call reads. Zero grad clears the gradient slots. Input = 2Wanted answer = 5 model(input_value)weight × input + bias1 × 2 + 0 Stored guess2.00 loss_fnSquared miss— backward() Gradient slots · .gradweight: empty · bias: emptyzero_grad() clears these slots step() · lr = 0.05 Model's parametersweight 1.00bias 0.00 next model() reads these

Code

Here is the whole update as it appears in the example file:

examples/pytorch_first_update.py · the complete updateOpen source ↗
guess_before = model(input_value)
loss_before = loss_fn(guess_before, answer)
optimizer.zero_grad()
loss_before.backward()  # write gradients
optimizer.step()        # change weight and bias
Full file · pytorch_first_update.py · 38 lines
"""A complete PyTorch update with one input and one adjustable model.

Run: python3 examples/pytorch_first_update.py
The matching browser lesson starts from these exact numbers.
"""

import torch
from torch import nn


# The model is guess = weight × input + bias.
model = nn.Linear(1, 1)
with torch.no_grad():
    model.weight.fill_(1.0)
    model.bias.fill_(0.0)

input_value = torch.tensor([[2.0]])
answer = torch.tensor([[5.0]])
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

guess_before = model(input_value)
loss_before = loss_fn(guess_before, answer)
optimizer.zero_grad()  # Remove notes from an earlier example, if any.
loss_before.backward()  # Write a gradient beside each parameter; do not move it yet.

print(f"Guess before: {guess_before.item():.2f}; answer: {answer.item():.2f}")
print(f"Squared error: {loss_before.item():.2f}")
print(f"Gradient for weight: {model.weight.grad.item():.2f}")
print(f"Gradient for bias: {model.bias.grad.item():.2f}")

optimizer.step()  # This is the line that moves the parameters.
with torch.no_grad():
    guess_after = model(input_value)
    loss_after = loss_fn(guess_after, answer)

print(f"Weight: {model.weight.item():.2f}; bias: {model.bias.item():.2f}")
print(f"Guess after: {guess_after.item():.2f}; squared error: {loss_after.item():.2f}")

Read it top to bottom: guess, score, clear old gradients, compute new gradients, move.

zero_grad() is there because backward() adds to .grad instead of replacing it. Without clearing, the previous update’s gradients would pile up on top of the new ones.

To check the new guess, the file calls the model inside with torch.no_grad():. That tells PyTorch not to record anything for gradients, because here we only want the number.

Check yourself: after backward(), has the weight changed?

No. The gradient has been written in the weight’s .grad slot, but the weight stays at 1 until optimizer.step() moves it.

Training loop

One example is easy to fit, because many different lines pass through a single point. Real training uses many examples.

Give the model three inputs, 1, 2, 3, with answers 3, 5, 7, stacked as rows: shape (3, 1).

Batch
several examples stacked into one tensor, one example per row.

The same single weight and bias make all three guesses at once. The loss averages the three squared misses, and backward() finds one combined direction. Then we repeat the update, which is all a training loop is:

examples/pytorch_training_loop.py · the loopOpen source ↗
inputs = torch.tensor([[1.0], [2.0], [3.0]])
answers = torch.tensor([[3.0], [5.0], [7.0]])

for step in range(500):
    guesses = model(inputs)
    loss = loss_fn(guesses, answers)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
Full file · pytorch_training_loop.py · 34 lines
"""Repeat the update on three examples until the model fits them.

Run: python3 examples/pytorch_training_loop.py
The browser lesson's training animation uses the same numbers.
"""

import torch
from torch import nn


# Three examples, one per row. The answers follow 2 × input + 1.
inputs = torch.tensor([[1.0], [2.0], [3.0]])
answers = torch.tensor([[3.0], [5.0], [7.0]])

model = nn.Linear(1, 1)
with torch.no_grad():
    model.weight.fill_(1.0)
    model.bias.fill_(0.0)

loss_fn = nn.MSELoss()  # The average of the three squared misses.
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

for step in range(500):
    guesses = model(inputs)
    loss = loss_fn(guesses, answers)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if step in (0, 9, 49, 499):
        print(f"Step {step + 1:3d}: loss {loss.item():.4f}  "
              f"weight {model.weight.item():.3f}  bias {model.bias.item():.3f}")

with torch.no_grad():
    print("A new input, 10, gives", round(model(torch.tensor([[10.0]])).item(), 2))
The model and three examples
Loss after each step (learning curve)
Loss (MSE)9.67
Weight1.000
Bias0.000
Guess for input 1010.00

This training run uses a learning rate of 0.01, so the gradual change is easier to follow. Press play.

  • Loss falls from 9.67 to about 3.00 after five updates,
  • and to 0.12 after twenty.

The line moves toward the three points, then improves more slowly as the weight and bias settle.

After 500 updates the model is \(\text{guess} \approx 2.062 \times \text{input} + 0.859\), with loss about 0.00285.

It is approaching the rule behind the examples, \(2 \times \text{input} + 1\), but has not reached it yet.

For a new input, 10, it predicts about 21.48; the wanted answer is 21. In real projects you keep some examples aside, never used for updates, to test exactly that.

Recap

Here is the training workflow in code order: set up the examples, model and optimizer once, then repeat the five update operations 500 times.

  • inputs holds the numbers we give the model: 1, 2, 3, stored as three rows in a tensor.
  • answers holds the wanted outputs: 3, 5, 7. Each row matches the same row in inputs. Multiple examples help pin down the model; one pair such as 2 → 5 fits many possible weights and biases.
  • model = nn.Linear(1, 1) creates the calculation \(\text{weight} \times \text{input} + \text{bias}\). We start its weight at 1 and bias at 0; training adjusts those same parameters throughout the loop.
  • loss_fn = nn.MSELoss() creates the scoring function. It squares each prediction’s gap from its answer, then averages those squared gaps into one loss.
  • optimizer = torch.optim.SGD(model.parameters(), lr=0.01) creates the updater. It keeps access to the model’s parameters and uses learning rate 0.01 to scale each adjustment.
  • for step in range(500) repeats the following five operations. The model and optimizer were created before the loop, so each iteration carries forward the updated weight and bias.
  • guesses = model(inputs) calculates fresh predictions for all three examples using the current weight and bias.
  • loss = loss_fn(guesses, answers) compares those predictions with the wanted answers and calculates the current loss.
  • optimizer.zero_grad() clears gradients left from the previous update, so this update starts with fresh gradient slots.
  • loss.backward() calculates how sensitive this loss is to each parameter and writes those gradients into their .grad slots. The parameters have not moved yet.
  • optimizer.step() changes the weight and bias using those gradients and the learning rate. The next iteration calls the model again to see what that adjustment achieved.

After training, model(new_inputs) uses the resulting weight and bias to predict answers. Calling the model makes predictions; the update sequence is what teaches it.

Key concepts

Tensor
A block of numbers with a shape. Arithmetic on it applies to every number.
Shape
The size along each direction; here rows count examples and columns count the numbers per example.
Model
The calculation that turns inputs into guesses (in plain words, the rule), such as \(\text{weight} \times \text{input} + \text{bias}\).
Module
A PyTorch object that packages a calculation and keeps its parameters, such as nn.Linear.
Parameter
An adjustable number inside the model, such as a weight or bias.
Loss
One number scoring how far the guesses are from the wanted answers. MSE averages the squared misses.
Gradient
How fast the loss changes when one parameter is nudged up, at its current value.
Backward pass
loss.backward(): fills every parameter’s .grad slot. It does not change parameters.
Learning rate
How big a step the optimizer takes against the gradient. Too big and the guess jumps past the bottom.
Optimizer step
optimizer.step(): uses the gradients and the learning rate to change the parameters.
Batch
Several examples stacked as rows and used together in one update.
Training loop
Guess, score, zero_grad, backward, step — repeated many times.

The local runnable examples produce every number on this page: tensors, one update, training loop. For further reference: PyTorch’s tensor tutorial, autograd tutorial and optimization tutorial. The page works offline; those reference links need a connection.