Kurs Python · Moduł 9: Machine Learning
Neural Networks - sieci neuronowe
W tej lekcji6
Masz już dwa filary PyTorcha: tensor liczący na GPU i autograd wyliczający pochodne. Brakuje modelu. Da się go złożyć gołymi rękami - trzymać wagi w liście i mnożyć macierze w pętli - ale przy dziesięciu warstwach tracisz rachubę jak przy liczeniu stada bawołów przez lornetkę. Moduł torch.nn daje gotowe klocki: warstwy, funkcje aktywacji i funkcje straty.
Własny model to klasa dziedzicząca po nn.Module
Własny model sieci neuronowej definiujesz w PyTorch jako klasę dziedziczącą z nn.Module i implementującą metodę forward(). Warto od razu odsunąć trzy inne pomysły.
Architektury nie opisuje się w pliku JSON - JSON przenosi dane, ale nie wykona ani jednego mnożenia macierzy. W PyTorch sieć jest kodem Pythona.
Nie budujesz jej też z samych list i słowników. Gdyby wagi leżały w zwykłej liście, model.parameters() nie miałoby czego zwrócić, optymalizator dostałby pustą kolekcję i trening szedłby bez jednego komunikatu o błędzie, tylko model nie zmieniłby się ani o krok. nn.Module rejestruje warstwy jako parametry do uczenia.
Nie ma wreszcie mowy o imporcie gotowej sieci z zewnętrznego API: takie API zwraca odpowiedź cudzego modelu, a Ty budujesz własny, uczony na Twoich danych.
1import torch
2import torch.nn as nn
3import torch.optim as optim
4
5class SafariClassifier(nn.Module):
6 def __init__(self, input_size, hidden_size, num_classes):
7 super(SafariClassifier, self).__init__()
8 self.hidden = nn.Linear(input_size, hidden_size)
9 self.relu = nn.ReLU()
10 self.output = nn.Linear(hidden_size, num_classes)
11
12 def forward(self, x):
13 x = self.hidden(x)
14 x = self.relu(x)
15 x = self.output(x)
16 return x
17
18model = SafariClassifier(input_size=10, hidden_size=64, num_classes=3)Klasa ma dwie części. W __init__ deklarujesz, z czego sieć się składa, i musi tam paść super().__init__() - bez tej linii warstwy nie zostaną zarejestrowane. W forward() opisujesz, którędy dane płyną. Samej metody nie wołasz wprost - piszesz model(x).
nn.Linear(10, 64) to warstwa gęsta - przyjmuje 10 liczb, oddaje 64 i sama trzyma macierz wag. Dla prostego stosu warstw istnieje skrót nn.Sequential, ale rozgałęzień w nim nie zapiszesz - własny model o niestandardowym przepływie zawsze wraca do nn.Module.
Kolejność przejścia przez sieć
Dane wędrują ustalonym porządkiem: Warstwa wejściowa (Input Layer) → Warstwa ukryta (Hidden Layer) → Funkcja aktywacji (ReLU) → Warstwa wyjściowa (Output Layer).
Warstwa wejściowa nie jest osobnym obiektem - to tensor wchodzący do modelu, o szerokości input_size, u nas 10 pomiarów jednego tropionego zwierzęcia. Warstwa ukryta miesza te liczby i produkuje 64 nowe. Funkcja aktywacji stoi za nią, nie przed, bo przetwarza jej wynik - przed nią są dopiero surowe dane. Warstwa wyjściowa idzie na końcu, bo sprowadza 64 liczby do trzech, po jednej na klasę.
Gdyby aktywacja wypadła z łańcucha, dwie warstwy liniowe złożyłyby się w jedną: więcej wag, tyle samo możliwości.
ReLU, czyli max(0, x)
ReLU to funkcja zwracająca max(0, x): liczby ujemne zamienia na zero, dodatnie przepuszcza bez zmian. Najłatwiej zobaczyć to na krótkim tensorze.
1relu = nn.ReLU()
2
3print(relu(torch.tensor([-2.0, -0.5, 0.0, 3.0])))
4# tensor([0., 0., 0., 3.])Wartości -2.0 i -0.5 wyszły jako zero, a 3.0 przeszło nietknięte. To złamanie wykresu w zerze to nieliniowość - jedyny powód, dla którego ReLU stoi w sieci.
Nie jest za to metodą kompresji danych: z czterech liczb wyszły cztery. Nie jest funkcją sortującą - kolejność została nietknięta, 3.0 nie przeskoczyło na początek. Nie jest algorytmem klasteryzacji - klasteryzacja, jak KMeans, grupuje całe obserwacje, a ReLU pracuje osobno na każdej pojedynczej liczbie.
Trening: strata i pięć kroków pętli
Świeżo utworzony model ma losowe wagi. Do nauki potrzebuje miary błędu (funkcji straty) i mechanizmu poprawiania wag (optymalizatora).
nn.CrossEntropyLoss to funkcja straty do zadań klasyfikacji wieloklasowej - gdzie każda obserwacja należy do jednej z rozłącznych klas: lew, słoń albo zebra. Model zwraca po jednej liczbie na klasę, a strata mierzy, jak bardzo rozminął się z prawdziwą etykietą.
Nie stosuje się jej w regresji, gdzie przewidujesz liczbę (masę zwierzęcia) i sięgasz po nn.MSELoss. Nie nadaje się do klasteryzacji, bo ta nie ma etykiet, więc nie ma z czym porównać przewidywania. Nie jest też narzędziem do generowania tekstu: potrafi jedynie ocenić pomyłkę przy wyborze jednej klasy z wielu, sama niczego nie tworzy.
Czai się tu pułapka: jeśli dołożysz nn.Softmax() na końcu modelu i podasz jego wynik do nn.CrossEntropyLoss, nie zobaczysz żadnego błędu - trening będzie po prostu szedł gorzej, bo ta funkcja straty stosuje softmax u siebie w środku.
Sama pętla ma pięć kroków, zawsze w tej kolejności: Forward pass - outputs = model(X) → Oblicz loss → optimizer.zero_grad() → loss.backward() → optimizer.step().
1criterion = nn.CrossEntropyLoss()
2optimizer = optim.Adam(model.parameters(), lr=0.001)
3
4for epoch in range(100):
5 for batch_X, batch_y in train_loader:
6 outputs = model(batch_X) # 1. forward pass
7 loss = criterion(outputs, batch_y) # 2. oblicz loss
8
9 optimizer.zero_grad() # 3. wyzeruj gradienty
10 loss.backward() # 4. policz nowe gradienty
11 optimizer.step() # 5. popraw wagiPowody tej kolejności są konkretne. Najpierw forward pass, bo bez przewidywania nie ma czego porównywać z prawdą. Potem loss - jedna liczba mówiąca, jak bardzo model się pomylił. Dopiero mając ją, wywołasz loss.backward(), które cofa rachunek przez sieć i wypełnia pole grad każdego parametru.
Najciekawsze jest miejsce optimizer.zero_grad(). PyTorch domyślnie sumuje gradienty, zamiast je nadpisywać, więc bez wyzerowania do bieżącego kroku dolicza się wszystko z poprzednich - a trening nie zgłosi żadnego wyjątku, model będzie się po prostu uczył coraz gorzej. Zerowanie musi więc wypaść przed loss.backward(). Gdyby trafiło za nim, skasowałoby dopiero co policzone gradienty i optimizer.step() poprawiłby wagi o zero. Znowu bez śladu w konsoli.
Krok optimizer.step() zamyka cykl: przesuwa każdą wagę w stronę mniejszego błędu, o tyle, na ile pozwala lr. Zauważ że optim.Adam dostał model.parameters() - kolekcję zebraną przez nn.Module.
Zapisywanie wag modelu
Trening trwa godzinami, więc wynik warto zachować. Zapisuje się nie cały obiekt modelu, lecz state_dict() - słownik nazw warstw i tensorów ich wag.
1torch.save(model.state_dict(), 'model.pth')
2
3model = SafariClassifier(10, 64, 3)
4model.load_state_dict(torch.load('model.pth'))
5model.eval()Zapis czytany element po elemencie to torch, ., save, (, model.state_dict(), , , 'model.pth', ). Najpierw idą wagi, potem nazwa pliku - odwrotna kolejność to najczęstsza literówka.
Odczyt jest dwuetapowy: najpierw tworzysz model o tej samej architekturze, dopiero potem wlewasz w niego wagi przez torch.load i load_state_dict - plik .pth niesie same liczby i nie wie, ile warstw ma sieć. Końcowe model.eval() przełącza sieć w tryb ewaluacji.
Podsumowanie
- Własny model definiujesz, dziedzicząc z
nn.Modulei implementującforward(). Nie przez pliki JSON, nie przez zwykłe listy i słowniki, nie przez import z zewnętrznego API. - Kolejność w sieci:
Warstwa wejściowa (Input Layer)→Warstwa ukryta (Hidden Layer)→Funkcja aktywacji (ReLU)→Warstwa wyjściowa (Output Layer). - ReLU zwraca
max(0, x)i wprowadza nieliniowość - nie kompresuje danych, nie sortuje ich i nie jest algorytmem klasteryzacji. nn.Linearto warstwa gęsta, ann.Sequentialto tylko skrót dla prostych stosów.nn.CrossEntropyLossstosujesz w klasyfikacji wieloklasowej. Do regresji służynn.MSELoss, klasteryzacja obywa się bez etykiet, a tekstu ta funkcja nie generuje.- Pętla treningowa:
Forward pass - outputs = model(X)→Oblicz loss→optimizer.zero_grad()→loss.backward()→optimizer.step(). - Gradienty sumują się domyślnie, dlatego brak zerowania psuje trening po cichu.
- Zapis wag:
torch.save(model.state_dict(), 'model.pth'), odczyt:load_state_dict.
W następnej lekcji poznasz MLflow - narzędzie, które zapamięta parametry i metryki każdego treningu, żebyś po dwudziestu próbach wiedział, która wypadła najlepiej. Zapamiętaj jedno: sieć to warstwy i aktywacje ułożone w forward(), a nauka to pięć kroków pętli powtarzanych tysiące razy.
Kod do tej lekcji: main.py
1# ===========================================
2# Safari Lab: Simple Neural Network
3# ===========================================
4# Build a basic neural network using PyTorch
5# to classify safari animal behavior patterns.
6
7import torch
8import torch.nn as nn
9import torch.optim as optim
10
11# --- Define the neural network ---
12class SafariNet(nn.Module):
13 """Neural network for classifying animal behavior."""
14
15 def __init__(self, input_size, hidden_size, num_classes):
16 super(SafariNet, self).__init__()
17 self.layer1 = nn.Linear(input_size, hidden_size)
18 self.relu = nn.ReLU()
19 self.layer2 = nn.Linear(hidden_size, num_classes)
20
21 def forward(self, x):
22 out = self.layer1(x)
23 out = self.relu(out)
24 out = self.layer2(out)
25 return out
26
27# --- Initialize the model ---
28input_size = 4 # features: speed, distance, group_size, hour
29hidden_size = 8 # hidden neurons
30num_classes = 3 # grazing, resting, hunting
31
32model = SafariNet(input_size, hidden_size, num_classes)
33print("=== SafariNet Architecture ===")
34print(model)
35
36# --- Loss function and optimizer ---
37criterion = nn.CrossEntropyLoss()
38optimizer = optim.Adam(model.parameters(), lr=0.01)
39
40# --- Sample training data ---
41X_train = torch.tensor([
42 [2.5, 0.3, 12, 6], [0.8, 2.1, 1, 14],
43 [3.1, 0.5, 8, 7], [0.3, 3.5, 1, 12],
44], dtype=torch.float32)
45y_train = torch.tensor([0, 1, 0, 1], dtype=torch.long)
46
47# --- Training loop (simplified) ---
48print("\n=== Training ===")
49for epoch in range(5):
50 outputs = model(X_train)
51 loss = criterion(outputs, y_train)
52
53 optimizer.zero_grad()
54 loss.backward()
55 optimizer.step()
56
57 print(f"Epoch [{epoch+1}/5], Loss: {loss.item():.4f}")
58
59# TODO: Add a third hidden layer to the network
60# class SafariNetV2(nn.Module):
61# def __init__(self, input_size, hidden1, hidden2, num_classes):
62# super(SafariNetV2, self).__init__()
63# self.layer1 = nn.Linear(input_size, hidden1)
64# self.relu1 = nn.ReLU()
65# self.layer2 = nn.Linear(hidden1, ...)
66# self.relu2 = nn.ReLU()
67# self.layer3 = nn.Linear(..., num_classes)
68#
69# def forward(self, x):
70# out = self.relu1(self.layer1(x))
71# out = self.relu2(self.layer2(out))
72# out = self.layer3(out)
73# return out
74
75# TODO: Run inference on a new sample
76# model.eval()
77# with torch.no_grad():
78# sample = torch.tensor([[2.0, 0.5, 10, 7]], dtype=torch.float32)
79# prediction = model(sample)
80# predicted_class = torch.argmax(prediction, dim=1)
81# labels = ["Grazing", "Resting", "Hunting"]
82# print(f"\nPrediction: {labels[predicted_class.item()]}")
83Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak definiuje się własny model sieci neuronowej w PyTorch?
2. Czym jest funkcja aktywacji ReLU?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki:
- Układanie w pionie
Ułóż kroki pętli treningowej w PyTorch:
- Edytor kodu
Zdefiniuj model z warstwami Linear, ReLU i wytrenuj go.
- Klikanie w kolejności
Kliknij w przyciski, aby zapisać wagi modelu: