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 sieci neuronowej definiujesz w PyTorch jako klasę dziedziczącą z
i implementującą metodę nn.Module
. Warto od razu odsunąć trzy inne pomysły.forward()
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.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 to funkcja zwracająca
: liczby ujemne zamienia na zero, dodatnie przepuszcza bez zmian. Najłatwiej zobaczyć to na krótkim tensorze.max(0, x)
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.
Świeżo utworzony model ma losowe wagi. Do nauki potrzebuje miary błędu (funkcji straty) i mechanizmu poprawiania wag (optymalizatora).
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ą.nn.CrossEntropyLoss
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
na końcu modelu i podasz jego wynik do nn.Softmax()
, nie zobaczysz żadnego błędu - trening będzie po prostu szedł gorzej, bo ta funkcja straty stosuje softmax u siebie w środku.nn.CrossEntropyLoss
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ż, @name, że optim.Adam dostał model.parameters() - kolekcję zebraną przez nn.Module.Trening trwa godzinami, więc wynik warto zachować. Zapisuje się nie cały obiekt modelu, lecz
- słownik nazw warstw i tensorów ich wag.state_dict()
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.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.Warstwa wejściowa (Input Layer) → Warstwa ukryta (Hidden Layer) → Funkcja aktywacji (ReLU) → Warstwa wyjściowa (Output Layer).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.Forward pass - outputs = model(X) → Oblicz loss → optimizer.zero_grad() → loss.backward() → optimizer.step().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.