The idea
PyTorch is the most popular library for deep learning: the neural networks behind image recognition, translation and chat assistants. Its core data structure is the tensor: an n-dimensional array of numbers, just like a NumPy array, with two superpowers:
- it can live on a GPU, where maths on millions of numbers runs dramatically faster;
- it can remember how it was computed, so PyTorch can calculate gradients automatically (autograd, module 4).
Everything you learned in the NumPy track carries over almost one to one.
How this track works: PyTorch is too large to run in your browser, so PyTorch code in these lessons is shown with its real output (checked against a real PyTorch installation) but has no Try it button. You practise the same ideas in NumPy, which does run here, and the API itself in quizzes. To run the PyTorch code yourself: uv add torch, then uv run python.
Creating tensors
If you know NumPy arrays, you already know most of what a tensor is: a grid of numbers of one type, with a shape. The simplest way to make one is from a Python list:
import torch
daily_orders = torch.tensor([42, 38, 51, 47, 60, 75, 68])
prices = torch.tensor([185.0, 320.0, 95.0])
print(daily_orders)
print(prices)
tensor([42, 38, 51, 47, 60, 75, 68])
tensor([185., 320., 95.])
The NumPy equivalent, which you can run:
import numpy as np
daily_orders = np.array([42, 38, 51, 47, 60, 75, 68])
prices = np.array([185.0, 320.0, 95.0])
print(daily_orders)
print(prices)
[42 38 51 47 60 75 68]
[185. 320. 95.]
The familiar creation functions all exist:
print(torch.zeros(2, 3))
print(torch.ones(3))
print(torch.arange(0, 10, 2))
print(torch.linspace(0, 1, 5))
print(torch.full((2, 2), 7.0))
tensor([[0., 0., 0.],
[0., 0., 0.]])
tensor([1., 1., 1.])
tensor([0, 2, 4, 6, 8])
tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
tensor([[7., 7.],
[7., 7.]])
One small difference from NumPy: PyTorch takes shapes as separate arguments (torch.zeros(2, 3)) or a tuple (torch.zeros((2, 3))); both work.
Random tensors and seeds
Neural networks start from random weights, so random tensors are everywhere. Set a seed for reproducibility:
torch.manual_seed(42)
print(torch.rand(2, 3)) # uniform in [0, 1)
print(torch.randn(3)) # standard normal: mean 0, std 1
tensor([[0.8823, 0.9150, 0.3829],
[0.9593, 0.3904, 0.6009]])
tensor([ 1.1561, 0.3965, -2.4661])
torch.manual_seed(42) plays the role of np.random.default_rng(42). The numbers differ between the two libraries, but the idea is identical: same seed, same numbers, every run.
Shape, dtype, size
When a PyTorch model complains, the error is almost always about shapes or types, so checking them is the first habit to build. These attributes tell you what you're holding:
units = torch.tensor([[120, 135, 150], [80, 85, 90]])
print(units.shape)
print(units.ndim, units.numel())
print(units.dtype)
print(torch.tensor([1.5, 2.0]).dtype)
torch.Size([2, 3])
2 6
torch.int64
torch.float32
.shapeis atorch.Size, which behaves like a tuple. (units.size()returns the same thing.).numel()is the number of elements, NumPy's.size.- Integers default to
int64. - Decimals default to
float32, notfloat64as in NumPy. Deep learning uses 32-bit floats almost everywhere: half the memory, much faster on GPUs, and precise enough for learning.
A tensor is a NumPy-style array with a shape and a dtype. Models expect float32 inputs. Mixing dtypes (an int64 tensor into a layer with float32 weights) is one of the most common beginner errors.
Convert with .float(), .long() or .to(torch.float32):
counts = torch.tensor([3, 1, 4])
print(counts.float(), counts.float().dtype)
tensor([3., 1., 4.]) torch.float32
Indexing and single values
Indexing and slicing work exactly like NumPy:
print(units[0]) # first row
print(units[:, 1]) # second column
print(units[1, 2]) # one element, still a tensor
print(units[1, 2].item()) # a plain Python number
tensor([120, 135, 150])
tensor([135, 85])
tensor(90)
90
.item() converts a one-element tensor to a Python number: you'll call it constantly to print or log a loss value.
Reshaping
Models are fussy about shapes. A layer might expect a batch of examples with shape (batch, features) when you have a single example with shape (features,). Reshaping rearranges the same numbers into the shape you need:
monthly = torch.arange(12)
print(monthly.reshape(4, 3))
print(monthly.reshape(-1, 6).shape)
print(monthly.unsqueeze(0).shape, monthly.unsqueeze(1).shape)
tensor([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
torch.Size([2, 6])
torch.Size([1, 12]) torch.Size([12, 1])
reshape and -1 work as in NumPy. unsqueeze(dim) inserts a dimension of size 1, the PyTorch name for np.newaxis / np.expand_dims; squeeze() removes size-1 dimensions. Adding a "batch" dimension with unsqueeze(0) before feeding a single example into a model is a daily routine.
Why this matters for clean code
Shapes and dtypes are the contract between your data and your model. The habits from the NumPy track (print .shape and .dtype when unsure, document what each dimension means, convert explicitly) matter even more here, because a shape mistake inside a network often produces a confusing error ten layers away from its cause.
Recap
- A tensor is PyTorch's n-dimensional array; it can run on GPUs and track gradients.
- Create with
torch.tensor,zeros,ones,arange,linspace,full,rand,randn; seed withtorch.manual_seed. .shape/.size(),.ndim,.numel(),.dtype; floats default to float32.- Index like NumPy;
.item()extracts a Python number. reshape,unsqueeze,squeezechange shapes; convert dtypes with.float(),.long(),.to(...).