Kurs Python · Moduł 9: Machine Learning

Neural Networks - sieci neuronowe

6 min czytania
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 wagi

Powody 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.Module i implementując forward(). 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.Linear to warstwa gęsta, a nn.Sequential to tylko skrót dla prostych stosów.
  • nn.CrossEntropyLoss stosujesz w klasyfikacji wieloklasowej. Do regresji służy nn.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()]}")
83

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jak definiuje się własny model sieci neuronowej w PyTorch?

  2. 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:

Przydatne artykuły