We use cookies to enhance your experience on the site
CodeWorlds

PyTorch - the two pillars of deep learning

The classic models you built with scikit-learn have a ceiling. Once the data turns huge and tangled - millions of animal photographs collected across the whole reserve - you need neural networks, and for those you need heavier equipment: PyTorch. Before you stack a single layer, get to know the two pillars the entire library rests on - the tensor and autograd. Everything else is detail.

1pip install torch torchvision

One command brings in both packages:

torch
is the library itself, and
torchvision
carries the image datasets and transforms you will reach for the moment your safari photographs enter the story. Install once, and the whole toolkit is on the truck.

Pillar one: the tensor

A tensor is the basic container for data in PyTorch - a multidimensional array much like a NumPy array, but with GPU support and one more superpower you will meet shortly: it remembers how it came to be. It is not a sorting algorithm, not a kind of database and not a file format - it is simply the box your numbers ride in. For now, treat it as the familiar array of numbers.

1import torch
2
3tensor = torch.tensor([1, 2, 3, 4, 5])
4matrix = torch.tensor([[1, 2], [3, 4], [5, 6]])
5
6zeros = torch.zeros(3, 4)   # 3x4 matrix of zeros
7random = torch.rand(3, 3)   # random values 0-1
8
9print(tensor.shape)    # size
10print(tensor.device)   # cpu or cuda (GPU)

You build tensors almost the way you build NumPy arrays:

torch.tensor(...)
takes a plain Python list,
torch.zeros
and
torch.rand
fill a shape for you. The import line is worth a second look, because Python lets you rename a module on the way in - the keyword
import
, then the module name, then
as
, then the alias you choose, as in
import torch as pt
. Most projects keep the plain
import torch
, but recognise that four-part shape when you see it. And note the last attribute,
device
: it reveals the first superpower, because a tensor knows whether it sits in processor memory (
cpu
) or on the graphics card (
cuda
). We come back to that in a moment.

Operations on tensors feel like NumPy too - you add, you multiply, you compute statistics:

1a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
2b = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32)
3
4c = a + b        # element-wise addition
5c = a @ b        # matrix multiplication (the @ operator)
6
7print(a.mean())  # mean
8print(a.argmax())  # index of the largest value
9
10reshaped = torch.arange(12).reshape(3, 4)  # change the shape

One distinction matters more than the rest:

+
and
*
work element by element, while
@
is real matrix multiplication - and in neural networks it is the second one you lean on constantly.
mean
and
argmax
summarise a tensor the way you would summarise a column of measurements.
reshape
rearranges the shape without touching the underlying numbers, which is exactly what you need when pushing images through a network.

The tensor superpower: computing on the GPU

Here is why deep learning is feasible at all in reasonable time. A network performs billions of matrix multiplications, and a graphics card (GPU) chews through them hundreds of times faster than a processor. Moving a tensor there takes a single method call.

1device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
2
3tensor = torch.tensor([1, 2, 3]).to(device)   # move to the GPU if there is one
4tensor_gpu = torch.rand(3, 3, device='cuda')  # or create it there right away

The

device = 'cuda' if ... else 'cpu'
pattern is the standard: the same code runs on the GPU when one is available and on an ordinary processor when it is not, with nothing rewritten. To move a tensor onto the card you write
tensor.to('cuda')
, or the shorthand
tensor.cuda()
- both do the same job, and
tensor.cpu()
brings it back. There is no
tensor.gpu()
, no
tensor.accelerate()
and no global
gpu(tensor)
function; those names simply do not exist in PyTorch. Remember one rule as well: for two tensors to be added or multiplied, they must live on the same device. Mixing CPU and GPU is the classic beginner error.

Pillar two: autograd

Now the second superpower - the one that lets a model learn at all. Learning means nudging parameters towards a smaller error, and that requires derivatives, better known as gradients. Autograd is PyTorch's engine for automatic gradient computation, the machinery behind backpropagation: mark a tensor with

requires_grad=True
and PyTorch records every operation applied to it, then replays the arithmetic backwards. It does not generate code for you, it does not load your data - that is the DataLoader's job, coming up shortly - and it does not run tests. It computes derivatives, nothing else.

1x = torch.tensor([2.0], requires_grad=True)
2y = x ** 2 + 3 * x + 1
3
4y.backward()      # compute the derivative backwards
5print(x.grad)     # dy/dx = 2x + 3 = 7

This is the heart of PyTorch. You only wrote the formula going forward (

y = x**2 + 3x + 1
), and
backward()
worked out the derivative on its own - 7, exactly what you would get on paper. You differentiated nothing by hand. In a real model the very same mechanism figures out how each weight should change so the error shrinks:

1w = torch.tensor([1.0], requires_grad=True)
2b = torch.tensor([0.0], requires_grad=True)
3
4y_pred = w * torch.tensor([2.0]) + b   # the model prediction
5loss = (y_pred - torch.tensor([5.0])) ** 2  # how badly we are wrong
6
7loss.backward()
8print(w.grad, b.grad)   # which way to nudge w and b

Follow the logic: you compute a prediction, you compute the error (

loss
), you call
backward()
- and
w.grad
and
b.grad
tell you which way to push each parameter so that the error drops. Training a network is nothing more than repeating that step thousands of times. That is why autograd is a pillar and not a curiosity.

Feeding the model: Dataset and DataLoader

One practical question is left: how do you feed the model? Networks learn from small portions called batches, so PyTorch hands you two building blocks -

Dataset
wraps your data, and
DataLoader
slices it into batches and shuffles them.

1from torch.utils.data import Dataset, DataLoader
2
3class SafariDataset(Dataset):
4    def __init__(self, X, y):
5        self.X = torch.tensor(X, dtype=torch.float32)
6        self.y = torch.tensor(y, dtype=torch.long)
7
8    def __len__(self):
9        return len(self.X)
10
11    def __getitem__(self, idx):
12        return self.X[idx], self.y[idx]
13
14dataset = SafariDataset(X_train, y_train)
15dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
16
17for batch_X, batch_y in dataloader:
18    print(batch_X.shape, batch_y.shape)
19    break

Your

Dataset
has to do exactly two things: report how many examples it holds (
__len__
) and return one of them by index (
__getitem__
).
DataLoader
takes it from there and serves the data in packs of 32 (
batch_size
), reshuffling the order at every epoch (
shuffle=True
) - so the model learns the animals rather than the accidental order in which you photographed them.

Take two pillars away from this lesson: the tensor is data that can compute on the GPU, and autograd works out the derivatives that let the model learn. Once those two make sense, building the network itself is only a matter of stacking layers.

Go to CodeWorlds