In de vorige notebook leerden we tensors aanmaken en inspecteren. Nu kijken we naar:
Operaties op tensors — grotendeels identiek aan NumPy
Device management — tensors verplaatsen naar GPU
Autograd — automatisch differentiëren, de kern van deep learning
import numpy as np
import torch
print(torch.__version__)2.12.0+cu130
Element-gewijs operaties¶
Alle basis wiskundige operaties werken element-gewijs, net als in NumPy.
a = torch.tensor([1.0, 2.0, 3.0, 4.0])
b = torch.tensor([5.0, 6.0, 7.0, 8.0])
print(a + b) # torch.add(a, b)
print(a * b) # torch.mul(a, b)
print(a**2) # element-wise square
print(torch.sqrt(a)) # element-wise sqrttensor([ 6., 8., 10., 12.])
tensor([ 5., 12., 21., 32.])
tensor([ 1., 4., 9., 16.])
tensor([1.0000, 1.4142, 1.7321, 2.0000])
# Reduction operations (like NumPy)
t = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print("sum: ", t.sum().item()) # .item() converts 0-d tensor to Python scalar
print("mean: ", t.mean().item())
print("max: ", t.max().item())
print()
print("sum per column (dim=0):", t.sum(dim=0)) # like np.sum(axis=0)
print("sum per row (dim=1):", t.sum(dim=1))sum: 21.0
mean: 3.5
max: 6.0
sum per column (dim=0): tensor([5., 7., 9.])
sum per row (dim=1): tensor([ 6., 15.])
Matrixoperaties¶
Voor matrix-vermenigvuldiging gebruik je @ of torch.matmul — identiek aan NumPy.
A = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
B = torch.tensor([[5.0, 6.0], [7.0, 8.0]])
print(A @ B) # matrix multiply — like np.matmul(A, B)
print()
print(A * B) # element-wise multiply (NOT matrix multiply)tensor([[19., 22.],
[43., 50.]])
tensor([[ 5., 12.],
[21., 32.]])
# Dot product of two vectors
u = torch.tensor([1.0, 2.0, 3.0])
v = torch.tensor([4.0, 5.0, 6.0])
print(torch.dot(u, v)) # 1*4 + 2*5 + 3*6 = 32tensor(32.)
Broadcasting¶
PyTorch volgt dezelfde broadcasting-regels als NumPy: shapes worden van rechts uitgelijnd en dimensies van grootte 1 worden automatisch uitgebreid.
# Scalar broadcasting
t = torch.ones(3, 4)
print(t * 5) # every element multiplied by 5tensor([[5., 5., 5., 5.],
[5., 5., 5., 5.],
[5., 5., 5., 5.]])
# Row vector added to each row of a matrix
M = torch.zeros(4, 3)
row = torch.tensor([1.0, 2.0, 3.0]) # shape (3,)
print(M + row) # row broadcast across all 4 rows — like NumPytensor([[1., 2., 3.],
[1., 2., 3.],
[1., 2., 3.],
[1., 2., 3.]])
In-place operaties¶
PyTorch-functies met een underscore-suffix (_) voeren de operatie in-place uit — ze wijzigen de tensor direct, zonder een nieuwe te alloceren.
t = torch.tensor([1.0, 2.0, 3.0])
t.add_(10) # t = t + 10, in-place
print(t)
t.mul_(2) # t = t * 2, in-place
print(t)tensor([11., 12., 13.])
tensor([22., 24., 26.])
In-place operaties zijn geheugenefficiënt maar kunnen problemen geven met autograd (zie verder). Gebruik ze voorzichtig.
Device management¶
PyTorch tensors leven op een device: cpu (standaard) of cuda (GPU). Bereken je iets op GPU, dan moeten alle tensors op hetzelfde device staan.
# Check if a GPU is available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Selected device:", device)Selected device: cpu
# Create tensor directly on the chosen device
t = torch.rand(3, 4, device=device)
print(t.device)
# Move existing tensor to device
t_cpu = torch.rand(3, 4)
t_device = t_cpu.to(device)
print(t_device.device)cpu
cpu
# To bring back to CPU (e.g. to convert to NumPy)
t_back_to_cpu = t_device.to("cpu")
arr = t_back_to_cpu.numpy() # .numpy() only works on CPU tensors
print(type(arr))<class 'numpy.ndarray'>
Autograd — automatisch differentiëren¶
Het verschil dat PyTorch uniek maakt ten opzichte van NumPy: autograd. PyTorch houdt bij hoe tensors berekend zijn (de computational graph) en kan automatisch de afgeleiden (gradiënten) berekenen.
Dit is de basis van backpropagation: het algoritme waarmee neurale netwerken leren.
Wat is een afgeleide?¶
De afgeleide van een functie meet hoe snel de functie-output verandert als je de input een klein beetje aanpast. Geometrisch is het de helling van de raaklijn aan de curve op dat punt.
Voorbeeld: voor geldt .
Bij : de afgeleide is — een toename van met 1 geeft een toename van met ongeveer 6.
Bij : de afgeleide is 0 — de curve is plat op dat punt.
In ML gebruik je afgeleiden om te bepalen in welke richting je de parameters van een model moet aanpassen om de fout te verkleinen.
De volledige wiskundige behandeling — limieten, afgeleidregels, partiële afgeleiden en gradiënten — komt aan bod in de cursus Mathematical Foundations.
requires_grad=True¶
Je vertelt PyTorch om gradiënten bij te houden door requires_grad=True in te stellen.
# Simple example: y = x^2, dy/dx = 2x
x = torch.tensor(3.0, requires_grad=True)
y = x**2 # builds the computational graph
print("y =", y)y = tensor(9., grad_fn=<PowBackward0>)
# .backward() computes dy/dx and stores it in x.grad
y.backward()
print("dy/dx at x=3:", x.grad) # 2 * 3 = 6dy/dx at x=3: tensor(6.)
# More complex: z = 3x^2 + 2x + 1, dz/dx = 6x + 2
x = torch.tensor(2.0, requires_grad=True)
z = 3 * x**2 + 2 * x + 1
z.backward()
print(f"dz/dx at x=2: {x.grad}") # 6*2 + 2 = 14dz/dx at x=2: 14.0
torch.no_grad()¶
Soms wil je berekeningen doen zonder de computational graph op te bouwen — bv. bij inferentie (voorspellingen maken) of evaluatie. Gebruik dan de torch.no_grad()-contextmanager. Dit bespaart geheugen en is sneller.
x = torch.tensor(3.0, requires_grad=True)
# Inside no_grad: no graph is built, gradients are not tracked
with torch.no_grad():
y = x**2
print("y:", y)
print("y.requires_grad:", y.requires_grad) # Falsey: tensor(9.)
y.requires_grad: False
.detach()¶
.detach() geeft een nieuwe tensor terug die los staat van de computational graph. Handig als je een tussenresultaat wil omzetten naar NumPy.
x = torch.tensor(3.0, requires_grad=True)
y = x**2
# Cannot call .numpy() on a tensor with requires_grad=True
# y.numpy() # -> RuntimeError
arr = y.detach().numpy() # detach first, then convert
print(arr)9.0
Samenvatting autograd¶
| Concept | Wat het doet |
|---|---|
requires_grad=True | Vertelt PyTorch: houd de computational graph bij voor deze tensor |
.backward() | Berekent de gradiënten terug door de graph (backpropagation) |
.grad | Bevat de berekende gradiënt na .backward() |
torch.no_grad() | Context waar geen graph wordt bijgehouden (sneller, minder geheugen) |
.detach() | Geeft tensor terug losgemaakt van de graph |
In de volgende cursussen (matematische grondslagen en ML principes) zul je autograd in context zien — bij het trainen van neurale netwerken is dit het mechanisme achter het leerproces.
Oefeningen¶
Oefening 1 — Maak twee tensors a = torch.tensor([[1., 2.], [3., 4.]]) en b = torch.tensor([[5., 6.], [7., 8.]]). Bereken:
Element-gewijs product
Matrixproduct (
@)Som van alle elementen in het matrixproduct
import torch
# your solution hereOefening 2 — Demonstreer broadcasting: maak een matrix van vorm (5, 3) gevuld met nullen. Trek er een rij-vector bias = torch.tensor([0.1, 0.2, 0.3]) bij op. Druk het resultaat af en verifieer dat elke rij [0.1, 0.2, 0.3] is.
# your solution hereOefening 3 — Verplaats een tensor naar het beschikbare device (cuda of cpu) en doe een matrixvermenigvuldiging op dat device. Zorg dat beide tensors op hetzelfde device staan.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# your solution hereOefening 4 — Bereken de afgeleide van bij . Controleer analytisch: , dus . Klopt PyTorch’s uitkomst?
# your solution hereOefening 5 — Maak een tensor x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) en bereken y = (x * 2).sum(). Roep .backward() aan. Wat zijn de gradiënten in x.grad? Leg uit waarom. Probeer daarna x.numpy() rechtstreeks aan te roepen en vang de fout op — gebruik vervolgens .detach().numpy().
# your solution here