Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Neural Networks - sieci neuronowe

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ż, @name, ż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.

Przejdź do CodeWorlds