Wpływ hiperparametrów na trening
W poprzednim artykule zbudowaliśmy sieć neuronową w NumPy, odtworzyliśmy ją w Kerasie i osiągnęliśmy około 97% trafności walidacyjnej na MNIST. Użyliśmy jednej warstwy ukrytej ze 128 neuronami, współczynnika uczenia 0.1 i rozmiaru batcha 32.
Te wybory to hiperparametry: ustawienia, które dobieramy sami, zamiast parametrów uczonych przez model. Będziemy zmieniać je pojedynczo, aby zobaczyć ich wpływ na trening. Wyniki posłużą jako punkt wyjścia dla tego modelu i zbioru danych; inne zadania mogą wymagać innych ustawień.
Każdy eksperyment poniżej ma interaktywny wykres. Możesz klikać etykiety, żeby pokazywać lub ukrywać poszczególne przebiegi, przełączać się między widokiem straty i trafności, włączyć „show baseline”, żeby dołączyć początkowy pomiar z losowymi wagami (epoka 0), oraz rozwinąć sekcję „Computation log”, żeby zobaczyć surowe wyjście treningu. Wykresy domyślnie pokazują metryki treningowe — nasza analiza opiera się na wartościach treningowych — ale możesz kliknąć „val”, żeby nałożyć metryki walidacyjne dla porównania. Bloki kodu z ikoną marimo (widoczną po najechaniu) prowadzą do interaktywnego notatnika, w którym możesz sam uruchomić eksperyment i zmodyfikować kod.
Użyjemy tego samego zbioru danych i tej samej konfiguracji modelu co w poprzednim artykule:
import keras
import numpy as np
# Zbiór treningowy: 60000 obrazów, testowy: 10000 obrazów
(train_images, train_labels), (test_images, test_labels) = keras.datasets.mnist.load_data()
# Spłaszczamy 28×28 → 784 i normalizujemy do [0, 1]
X_train = train_images.reshape(-1, 784).astype("float32") / 255.0
X_test = test_images.reshape(-1, 784).astype("float32") / 255.0
y_train = train_labels
y_test = test_labelsWspółczynnik uczenia
Gdy przyglądaliśmy się temu, jak uczą się sieci, zobaczyliśmy, że współczynnik uczenia steruje wielkością kroku spadku gradientu. Za mały i trening idzie bardzo wolno; za duży i krok przestrzeliwuje minimum, co utrudnia osiągnięcie niskiej straty. Pokazaliśmy to na modelu z dwoma parametrami i interaktywnym widżecie. Teraz zobaczmy dokładnie to samo zjawisko na prawdziwej sieci ze 100 000+ parametrów.
Porównamy pięć współczynników uczenia przez dziesięć epok:
for lr in [0.001, 0.01, 0.1, 1.0, 10.0]:
model = keras.Sequential([
keras.layers.Dense(128, activation="relu", input_shape=(784,)),
keras.layers.Dense(10, activation="softmax"),
])
model.compile(
optimizer=keras.optimizers.SGD(learning_rate=lr),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
model.fit(X_train, y_train, epochs=10, batch_size=32,
validation_split=0.2, verbose=2)Zobaczmy, jak strata zmienia się z epoki na epokę dla każdego współczynnika uczenia:
Computation log
Włącz „show baseline”, aby zobaczyć pomiary sprzed treningu. Początkowa strata wynosi około 2.3, z różnicami wynikającymi z losowej inicjalizacji. Przypisanie każdej z dziesięciu klas prawdopodobieństwa 0.1 daje , ale losowe wagi nie gwarantują dokładnie jednostajnego rozkładu.
Przy lr = 10.0 trening w tym przebiegu się nie udaje. Strata skacze do 12.2 w pierwszej epoce, a później pozostaje w okolicach 2.5. Trafność treningowa utrzymuje się blisko 10%. Te wyniki pokazują, że model nie uczy się rozróżniać cyfr, ale same nie dowodzą, że zwraca tę samą predykcję dla każdego obrazu.
Wyłączmy lr=10.0, klikając jego etykietę — bo jego skok do 12.2 ściska oś Y i utrudnia dostrzeżenie szczegółów pozostałych krzywych. Po ukryciu tej krzywej łatwiej porównać cztery pozostałe przebiegi:
- lr = 0.001 — strata po dziesięciu epokach wynosi 0.41, więcej niż 0.33 osiągnięte przez lr=0.1 po jednej epoce. Uczenie przy takim kroku jest wolne.
- lr = 0.01 — strata osiąga 0.18 i nadal maleje. Dłuższy trening może pomóc, ale nie musi doprowadzić do tego samego rozwiązania co lr=0.1.
- lr = 0.1 — najniższa końcowa strata treningowa w tych przebiegach, około 0.03. Największa początkowa poprawa następuje w pierwszych dwóch–trzech epokach.
- lr = 1.0 — strata treningowa dochodzi do około 0.15, a walidacyjna waha się w końcowych epokach w okolicach 0.20–0.24. Włącz krzywe walidacyjne, aby zobaczyć również spadek trafności pod koniec treningu.
Współczynnik uczenia zależy też od optymalizatora. Adam korzysta z bieżących oszacowań gradientów i ich kwadratów, aby skalować aktualizacje poszczególnych parametrów, ale nadal wymaga dobrania współczynnika uczenia. Optymalizatory, harmonogramy współczynnika uczenia i warmup omówimy w następnym artykule.
Rozmiar batcha
Porównamy rozmiary batcha 1, 32, 256 i 60 000. Po odłożeniu 20% danych na walidację do treningu pozostaje 48 000 obrazów, więc batch_size=60000 umieszcza wszystkie te obrazy w jednym batchu.
for bs in [1, 32, 256, 60000]:
model = keras.Sequential([
keras.layers.Dense(128, activation="relu", input_shape=(784,)),
keras.layers.Dense(10, activation="softmax"),
])
model.compile(
optimizer=keras.optimizers.SGD(learning_rate=0.1),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
model.fit(X_train, y_train, epochs=10, batch_size=bs,
validation_split=0.2, verbose=2)Zobaczmy, jak strata zmienia się z epoki na epokę dla każdego rozmiaru batcha:
Computation log
Środek stawki — bs=32 i bs=256 — sprawdza się w obu przypadkach, przy czym bs=32 zbiega szybciej:
- bs = 32 — strata spada gwałtownie z 2.4 do 0.03 do dziesiątej epoki. Przy 1500 batchach na epokę model dostaje częste aktualizacje z gradientami, które są zaszumione, ale z grubsza poprawne.
- bs = 256 — wolniej, ale stabilnie. Strata spada do 0.17 po 10 epokach, a krzywa wciąż opada. Każda epoka ma tylko 188 batchy (wobec 1500 przy bs=32), czyli mniej aktualizacji — ale każda opiera się na pewniejszej średniej gradientu.
Najmniejszy i największy batch zachowują się inaczej:
- bs = 1 — niestabilny trening przy lr=0.1. Każdy gradient pochodzi z jednego obrazu, a duże aktualizacje mogą cofać wcześniejszy postęp. Uśrednianie po większej liczbie obrazów ogranicza tę zmienność.
- bs = 60000 — strata spada z około 2.4 do 1.6 po dziesięciu epokach. Jedna aktualizacja na epokę daje tylko dziesięć kroków gradientu, wobec 15 000 przy bs=32. Przy tym współczynniku uczenia pełny gradient nie rekompensuje tak małej liczby aktualizacji.
Rozmiar batcha 32 sprawdził się w tym eksperymencie. Większe batche potrzebują więcej pamięci na aktywacje i mogą zwiększyć wydajność sprzętu, ale krótsza epoka nie musi oznaczać szybszego osiągnięcia tej samej trafności walidacyjnej. Porównuj zarówno czas treningu, jak i jakość na walidacji.
Dobieraj rozmiar batcha i współczynnik uczenia razem. W przebiegu z bs=1 obniżenie lr z 0.1 do 0.001 pozwala osiągnąć stratę 0.09. Większe batche mogą korzystać z większego współczynnika uczenia, ale nie jest to uniwersalna reguła skalowania.
Głębokość i szerokość sieci
Nasza bazowa sieć ma jedną warstwę ukrytą ze 128 neuronami. Co się stanie, gdy pójdziemy szerzej, głębiej albo i tak, i tak? Sprawdźmy — wytrenujemy pięć wariantów, trzymając wszystko inne bez zmian (lr=0.1, bs=32, 10 epok):
configs = {
"narrow (32)": [32],
"baseline (128)": [128],
"wide (512)": [512],
"deep (2×128)": [128, 128],
"deep (3×128)": [128, 128, 128],
}
for name, hidden_sizes in configs.items():
model = keras.Sequential()
model.add(keras.layers.Dense(hidden_sizes[0], activation="relu", input_shape=(784,)))
for size in hidden_sizes[1:]:
model.add(keras.layers.Dense(size, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))
model.compile(
optimizer=keras.optimizers.SGD(learning_rate=0.1),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
model.fit(X_train, y_train, epochs=10, batch_size=32,
validation_split=0.2, verbose=2)Zobaczmy, jak strata zmienia się dla każdej architektury:
Computation log
Kilka rzeczy rzuca się w oczy:
- Większa szerokość pomaga w tym eksperymencie: „szerokość” oznacza tu liczbę neuronów w pojedynczej warstwie ukrytej — przejście z 32 na 128, a potem na 512 neuronów systematycznie obniża stratę. Szersza warstwa ma więcej parametrów do wykrywania wzorców. Ale 512 ma 4× więcej parametrów niż 128 przy tylko niewielkiej poprawie straty — malejące zyski.
- Druga warstwa również pomaga: dodanie drugiej warstwy ukrytej (2×128) osiąga niższą stratę niż jednowarstwowa sieć bazowa, mimo że łączna liczba parametrów jest podobna. Głębsze sieci potrafią uczyć się cech hierarchicznych — pierwsza warstwa może wykrywać krawędzie, druga może łączyć krawędzie w kształty.
- Trzecia warstwa nie poprawiła wyniku tego przebiegu: końcowa strata jest zbliżona do bazowej, a strata walidacyjna bardziej się waha. Same wykresy nie wskazują przyczyny ani nie dowodzą zanikania gradientów.
Dla tego zadania z MNIST jedna lub dwie warstwy ukryte o szerokości 128–512 to użyteczny punkt wyjścia. Zanim zwiększysz model, porównaj wyniki walidacyjne. Znaczenie ma też rodzaj warstwy: warstwy konwolucyjne wykorzystują strukturę przestrzenną obrazów, którą nasz model w pełni połączony pomija.
Funkcje aktywacji: sigmoida kontra ReLU
Porównamy sigmoidę i ReLU, aby sprawdzić zachowanie gradientów omówione w artykule teoretycznym. Sigmoida wprowadza w każdej warstwie pochodną nie większą niż 0.25; ReLU daje 1 dla neuronów aktywnych i 0 dla nieaktywnych. Te czynniki wpływają na propagację gradientów w głębokiej sieci.
W Kerasie zmiana funkcji aktywacji to po prostu zmiana napisu — "relu" kontra "sigmoid". Wytrenujemy sieci z 1, 3 i 5 warstwami ukrytymi, żeby zobaczyć, jak głębokość współgra z wyborem aktywacji:
for n_layers in [1, 3, 5]:
for activation in ["relu", "sigmoid"]:
model = keras.Sequential()
model.add(keras.layers.Dense(128, activation=activation, input_shape=(784,)))
for _ in range(n_layers - 1):
model.add(keras.layers.Dense(128, activation=activation))
model.add(keras.layers.Dense(10, activation="softmax"))
model.compile(
optimizer=keras.optimizers.SGD(learning_rate=0.1),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
model.fit(X_train, y_train, epochs=10, batch_size=32,
validation_split=0.2, verbose=2)Zobaczmy, jak strata zmienia się dla każdej kombinacji głębokości i funkcji aktywacji:
Computation log
W tych przebiegach sigmoida utrudnia trening wraz ze wzrostem głębokości:
- 1 warstwa ukryta — obie aktywacje działają dobrze. Strata ReLU spada do 0.03 do dziesiątej epoki, sigmoidy do 0.15. Przy tylko jednej warstwie gradient przechodzi przez jedną aktywację, więc kurczenie gradientu przez sigmoidę nie ma większego znaczenia.
- 3 warstwy ukryte — sigmoida zaczyna zostawać w tyle. Strata ReLU sięga 0.016, sigmoidy tylko 0.14 — prawie 10× więcej. Sigmoida jest zauważalnie wolniejsza w pierwszych epokach — jej strata w epoce 3 jest wciąż wyższa niż ReLU po epoce 1.
- 5 warstw ukrytych — sigmoida całkowicie zawodzi. Strata ledwo rusza się z początkowej wartości ~2.3 przez wszystkie 10 epok — model praktycznie niczego się nie nauczył. Tymczasem ReLU z 5 warstwami dochodzi do straty 0.024 — praktycznie tyle samo co przy 1 i 3 warstwach.
Te wyniki są zgodne z problemem zanikającego gradientu. W pięciu warstwach sigmoidy iloczyn samych pochodnych aktywacji wynosi najwyżej . Na pełny gradient wpływają również macierze wag, więc nie jest to dokładne oszacowanie gradientu docierającego do pierwszej warstwy.
ReLU nie zmniejsza gradientu na etapie aktywacji dla aktywnych neuronów, co pomaga wyjaśnić uzyskane tu wyniki. Nie gwarantuje jednak stabilnych gradientów w całej sieci.
ReLU to dobry punkt wyjścia dla tego modelu. Leaky ReLU i ELU pozwalają na niezerowe gradienty przy ujemnych wejściach i mogą pomóc, gdy neurony pozostają nieaktywne. Inne architektury stosują inne aktywacje, np. GELU w części transformerów. W standardowym LSTM sigmoida steruje bramkami, a tanh służy do obliczania wartości kandydujących i przekształcania stanu komórki na wyjściu.
Liczba epok
We wszystkich powyższych eksperymentach trenowaliśmy przez 10 epok. Ale przez ile epok właściwie należy trenować? Sprawdźmy, trenując naszą najlepszą konfigurację (lr=0.1, rozmiar batcha 32) przez 50 epok i obserwując, co się stanie:
model = keras.Sequential([
keras.layers.Dense(128, activation="relu", input_shape=(784,)),
keras.layers.Dense(10, activation="softmax"),
])
model.compile(
optimizer=keras.optimizers.SGD(learning_rate=0.1),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
history = model.fit(X_train, y_train, epochs=50, batch_size=32,
validation_split=0.2, verbose=2)Strata treningowa spada z 0.33 w pierwszej epoce do 0.0015 w pięćdziesiątej. Raportowana trafność treningowa osiąga 100% do epoki 29. To bardzo dobre dopasowanie do zbioru treningowego, ale samo nie mówi, jak dobrze model generalizuje.
Strata walidacyjna maleje z 0.19 w epoce 1 do 0.075 około epoki 13, potem przestaje się poprawiać i zaczyna powoli rosnąć — 0.078 w epoce 20, 0.081 w epoce 30, 0.085 w epoce 50. Tymczasem trafność walidacyjna stabilizuje się w okolicach 98% od epoki 13 i ledwo drgnie przez pozostałe 37 epok.
Rosnąca strata walidacyjna jest oznaką przeuczenia. Strata treningowa nadal maleje, ale trafność walidacyjna prawie się nie zmienia, a strata walidacyjna rośnie. Model może przypisywać coraz większą pewność pozostałym błędnym predykcjom na nowych danych.
Zbyt mała liczba epok może prowadzić do niedouczenia, a dalszy trening może z czasem spowodować przeuczenie. Moment zatrzymania zależy od modelu, danych i innych hiperparametrów, dlatego monitorujemy wyniki walidacyjne.
Ogólnie, zamiast zgadywać właściwą liczbę epok, użyj wczesnego zatrzymania — callbacku Kerasa, który monitoruje stratę walidacyjną i automatycznie przerywa trening, gdy przestaje się ona poprawiać:
early_stop = keras.callbacks.EarlyStopping(
monitor="val_loss",
patience=5,
restore_best_weights=True,
)
model.fit(X_train, y_train, epochs=100, batch_size=32,
validation_split=0.2,
callbacks=[early_stop])Ustaw limit, np. 100 epok. Przy patience=5 trening kończy się po pięciu kolejnych epokach bez poprawy straty walidacyjnej. restore_best_weights=True przywraca wagi z epoki o najlepszym wyniku walidacyjnym. Nadal wybierasz monitorowaną metrykę, liczbę epok oczekiwania na poprawę i maksymalny budżet treningu.
Przeuczenie i wczesne zatrzymanie omówimy dokładniej w kolejnych artykułach.
Zautomatyzowane przeszukiwanie hiperparametrów
W tym artykule dobieraliśmy hiperparametry ręcznie — zmieniając po jednym i obserwując efekt. To buduje intuicję, ale nie skaluje się. Gdy masz dziesiątki hiperparametrów i tysiące możliwych kombinacji, potrzebujesz strategii przeszukiwania i narzędzia, które ją zautomatyzuje.
Strategie przeszukiwania różnią się sposobem wyboru kolejnej konfiguracji i wykorzystania dostępnego budżetu ocen:
- Przeszukiwanie siatki (grid search). Wybierz skończony zestaw wartości dla każdego hiperparametru i oceń wszystkie kombinacje. Trzy parametry po pięć wartości wymagają 125 przebiegów. Ta metoda sprawdza się przy małych przestrzeniach poszukiwań i niedrogich ocenach.
- Przeszukiwanie losowe (random search). Losuj kombinacje z wybranych zakresów lub rozkładów. Jeśli tylko kilka parametrów silnie wpływa na wynik, przy tym samym budżecie można sprawdzić więcej różnych wartości tych parametrów niż w siatce. Zobacz Bergstra i Bengio, 2012.
- Optymalizacja współrzędnościowa (coordinate descent). Zacznij od jednej konfiguracji, zmieniaj po jednym parametrze i zachowuj poprawy. Powtarzaj proces, aby ponownie sprawdzać wcześniejsze wybory po zmianie innych parametrów. Pozwala to uniknąć oceny całej siatki, ale metoda może utknąć w punkcie, w którym żadna pojedyncza zmiana nie pomaga, choć zmiana kilku parametrów jednocześnie poprawiłaby wynik.
- Optymalizacja bayesowska. Dopasuj model probabilistyczny do wcześniejszych konfiguracji i wyników, a następnie wybierz kolejną ocenę, równoważąc badanie nowych obszarów z wykorzystaniem obiecujących wyników. Może to zmniejszyć liczbę kosztownych ocen, ale skuteczność zależy od przestrzeni poszukiwań i modelu.
Strategię dobieraj do kosztu oceny, przestrzeni poszukiwań i dostępnego budżetu. Przeszukiwanie losowe daje użyteczny punkt odniesienia; przy kosztownych ocenach warto rozważyć przeszukiwanie lokalne lub metody oparte na modelu. Na przykład w pracy o AgentDiet dobierano cztery hiperparametry na zestawie 100 zadań dla agenta, zmieniając po jednym parametrze naraz. Po pierwszej rundzie autorzy zmienili dwa ustawienia, a w drugiej nie uzyskali dalszej poprawy.
Weights & Biases Sweeps i Optuna udostępniają narzędzia do przeszukiwania siatki, losowego i opartego na modelu, automatycznie śledząc każdy przebieg i porównując wyniki, a Keras Tuner oferuje to samo, zintegrowane bezpośrednio z Kerasem. Pomagają zarządzać eksperymentami, gdy ręczne śledzenie wyników staje się uciążliwe.