Wpływ hiperparametrów na trening

W poprzednim artykule wytrenowaliśmy sieć neuronową na zbiorze MNIST — biorąc każde pojęcie z artykułu teoretycznego (softmax, entropia krzyżowa, propagacja wsteczna, spadek gradientu) i stosując je do klasyfikacji odręcznych cyfr. Zbudowaliśmy sieć od zera w NumPy, odtworzyliśmy ją w Kerasie i osiągnęliśmy 97% dokładności na zbiorze testowym przy prostej dwuwarstwowej architekturze i domyślnych ustawieniach: jedna warstwa ukryta ze 128 neuronami, współczynnik uczenia 0.1, rozmiar batcha 32.

Każdy z tych wyborów — współczynnik uczenia, rozmiar batcha, liczba warstw, funkcja aktywacji — nazywa się hiperparametrem: to ustawienia wybierane przez Ciebie, a nie uczone przez model. Celem tego artykułu jest pokazanie, jak bardzo te wybory mają znaczenie — zły współczynnik uczenia może być różnicą między 97% dokładności a całkowitą porażką — oraz podanie ogólnych praktycznych zaleceń dla każdego z nich. Ale te zalecenia to tylko punkty wyjścia, nie odpowiedzi. Właściwe hiperparametry odkrywa się przez eksperymentowanie — zmień jedną rzecz, zaobserwuj efekt, skoryguj — i zależą one od Twojego konkretnego modelu, zbioru danych i zadania.

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 dokładnoś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_labels

Współ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, przez co trudniej zbiec do 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.

Wytrenujemy ten sam model cztery razy, zmieniając wyłącznie współczynnik uczenia:

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=5, batch_size=32,
              validation_split=0.2, verbose=2)

Zobaczmy, jak strata zmienia się z epoki na epokę dla każdego współczynnika uczenia:

Loss by Learning Rate
Computation log

Pierwsze, co rzuca się w oczy przy włączonym „show baseline” — wszystkie pięć przebiegów startuje z tego samego miejsca: losowa sieć ze stratą ~2.3. To ma sens: przy 10 klasach i losowych wagach model przypisuje mniej więcej równe prawdopodobieństwo każdej klasie — około 0.1 na klasę. Entropia krzyżowa dla poprawnej klasy z prawdopodobieństwem 0.1 wynosi log(0.1)2.3-\log(0.1) \approx 2.3. To jest linia bazowa — strata modelu, który niczego się nie nauczył.

Następnie spójrz na lr = 10.0 — całkowita porażka. Strata skacze do 12.2 w pierwszej epoce, gdy aktualizacje wag przestrzeliwują tak daleko, że wagi eksplodują, a potem utyka w okolicach 2.5 na pozostałe epoki. Model zwraca to samo przewidywanie dla każdego wejścia — dokładność treningowa to ~10% (zgadywanie na chybił trafił) i nigdy się nie poprawia. Oto co się dzieje, gdy współczynnik uczenia jest tak duży, że spadek gradientu w ogóle nie potrafi zrobić postępu.

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. Bez niego cztery pozostałe przebiegi opowiadają jaśniejszą historię:

  • lr = 0.001 — strata spada, ale boleśnie powoli. Po 10 epokach wciąż wynosi 0.41 — więcej niż lr=0.1 już po jednej epoce (0.33). Gradienty wskazują właściwy kierunek, ale każdy krok jest tak maleńki, że model ledwo się rusza. Potrzebowałby o wiele więcej epok, żeby nadrobić.
  • lr = 0.01 — równomierny postęp. Strata spada gładko z 2.3 do 0.18 do dziesiątej epoki, a krzywa wciąż opada — model wyraźnie nadal się poprawia. Przy większej liczbie epok osiągnąłby tę samą stratę co lr=0.1. Mniejsze kroki nie ograniczają tego, dokąd dojdziesz, tylko wydłużają drogę.
  • lr = 0.1 — złoty środek dla tej sieci. Strata spada gwałtownie do 0.03 do dziesiątej epoki, a krzywa wypłaszcza się w miarę zbliżania do minimum. Większość uczenia dzieje się w pierwszych 2–3 epokach.
  • lr = 1.0 — strata spada początkowo szybko, ale nie potrafi się ustatkować. Skacze — 0.20, potem 0.25, potem 0.21, potem 0.24 — zamiast opadać gładko. Przełącz się na widok dokładności, a zobaczysz, że dokładność walidacyjna w ostatnich epokach wręcz spada. Przestrzeliwanie nie uniemożliwia uczenia, ale uniemożliwia modelowi dostrojenie się do dobrego minimum.

W praktyce dobieranie współczynnika uczenia to proces bardziej zawiły niż samo wybranie liczby — jest powiązany z wyborem optymalizatora, a zwykły SGD z ręcznie dobranym współczynnikiem jest dziś rzadko używany. Adaptacyjne optymalizatory jak Adam dostrajają wielkość kroku dla każdego parametru z osobna i utrzymują pęd, przez co są znacznie mniej wrażliwe na początkowy wybór współczynnika uczenia. To, jak szum SGD pomaga wyrwać się z minimów lokalnych, jak działa adaptacyjne skalowanie w Adamie i dlaczego harmonogramy współczynnika uczenia oraz rozgrzewka są standardem we współczesnym treningu — wraz z interaktywnymi demonstracjami — omawiamy w następnym artykule.

Rozmiar batcha

Gdy przyglądaliśmy się temu, jak uczą się sieci neuronowe, porównaliśmy minibatche po 2 z pełnobatchowym spadkiem gradientu na 5 punktach danych. Wersja z minibatchami była bardziej zaszumiona (strata zygzakowała), ale zbiegała mniejszym kosztem obliczeniowym. Teraz zobaczmy, jak rozmiar batcha wpływa na trening na prawdziwym zbiorze danych — wytrenujemy ten sam model z rozmiarami batcha 1, 32, 256 i 60 000, czyli całym zbiorem treningowym naraz:

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=5, batch_size=bs,
              validation_split=0.2, verbose=2)

Zobaczmy, jak strata zmienia się z epoki na epokę dla każdego rozmiaru batcha:

Loss by Batch Size
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.

Dwie skrajności opowiadają ciekawszą historię:

  • bs = 1 — zaskakująco źle przy lr=0.1. Strata ledwo maleje i dziko oscyluje między epokami, bo każda aktualizacja opiera się na pojedynczym obrazie, więc gradient odzwierciedla specyfikę tego jednego przykładu, a nie całego zbioru. Przy bs=32 te indywidualne różnice uśredniają się w rozsądne oszacowanie. Przy bs=1 każda aktualizacja ciągnie w losowym kierunku, a lr=0.1 sprawia, że każdy taki losowy krok jest dość duży, by zniweczyć wcześniejszy postęp.
  • bs = 60000 — pełny batch. Strata spada boleśnie powoli — z 2.4 do 1.6 po 10 epokach, znacznie gorzej niż 0.03 przy bs=32 w tym samym czasie. Może się to wydawać nieintuicyjne — czy idealny gradient policzony po wszystkich obrazach nie powinien być lepszy niż zaszumione oszacowanie z 32? Co do kierunku — owszem. Ale liczy się też, jaki łączny dystans model pokonuje po krajobrazie straty: bs=32 robi 15 000 kroków w 10 epokach, podczas gdy bs=60000 robi zaledwie 1000 kroków nawet po 1000 epok. Przy jednej aktualizacji na epokę 10 epok oznacza tylko 10 kroków gradientu. Idealny gradient nie zrekompensuje tak nielicznych okazji do aktualizacji.

W naszych eksperymentach najlepszy wynik dał rozmiar batcha 32 — i to generalnie mocna wartość domyślna. Równoważy jakość gradientu z częstotliwością aktualizacji. W praktyce rozmiar batcha jest też ograniczony pamięcią GPU, bo większe batche potrzebują więcej pamięci na przechowanie aktywacji i gradientów wszystkich próbek jednocześnie. Jeśli GPU ma zapas, spróbuj 128 albo 256 — trening będzie szybszy w przeliczeniu na epokę (mniej aktualizacji, ale każda przetwarza równolegle więcej danych), choć może być konieczne zwiększenie współczynnika uczenia dla kompensacji. Rozmiar batcha 1 jest rzadko używany, bo jest zbyt zaszumiony i nie wykorzystuje równoległości GPU. Bardzo duże batche (tysiące i więcej) wymagają starannego dobrania współczynnika uczenia i stosuje się je głównie w treningu rozproszonym na wielu GPU.

Warto zauważyć, że rozmiar batcha i współczynnik uczenia należy dobierać razem. Nasze lr=0.1 jest dobrane pod bs=32 — jest za duże dla bs=1 (stąd oscylacje) i chyba za małe dla bs=60000 (któremu przydałby się większy krok). Przestaw bs=1 na lr=0.001, a dojdzie do straty 0.09 — niemal dorównując bs=32. Szum nie przeszkadza, gdy każdy krok jest na tyle mały, że żaden pojedynczy zły gradient nie narobi szkód. Większe batche potrzebują większych współczynników uczenia, żeby zrekompensować mniejszą liczbę aktualizacji na epokę. Nie da się ocenić rozmiaru batcha bez uwzględnienia współczynnika uczenia.

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:

Loss by Architecture
Computation log

Kilka rzeczy rzuca się w oczy:

  • Szerokość pomaga: „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.
  • Głębokość też 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.
  • Więcej głębokości, więcej ryzyka: przy 3 warstwach końcowa strata dorównuje bazowej, a strata walidacyjna jest zauważalnie bardziej zaszumiona. Głębsze sieci trudniej trenować — gradienty muszą przepłynąć przez więcej warstw i zaczyna mieć znaczenie problem zanikającego gradientu.

Ogólnie dla sieci w pełni połączonych zaczynaj prosto — jedna lub dwie warstwy ukryte często wystarczą. Dokładaj głębokości tylko wtedy, gdy strata prostszego modelu wychodzi na plateau. Jeśli chodzi o szerokość, 128–512 neuronów na warstwę to rozsądny zakres dla sieci w pełni połączonych. Prawdziwe zyski architektoniczne biorą się z użycia właściwego typu warstwy do danych: warstw konwolucyjnych do obrazów, warstw rekurencyjnych albo transformerów do sekwencji. Te wyspecjalizowane architektury są znacznie oszczędniejsze parametrowo niż używane tu warstwy w pełni połączone — sieć konwolucyjna potrafi dorównać stracie naszego modelu z 512 neuronami przy ułamku parametrów.

Funkcje aktywacji: sigmoida kontra ReLU

Funkcji aktywacji do wyboru jest wiele — ReLU, sigmoida, tanh, Leaky ReLU, GELU i inne. Skupimy się tu na dwóch, bo artykuł teoretyczny postawił co do nich konkretną prognozę: sigmoida powinna mieć kłopoty w głębokich sieciach, bo jej pochodna jest zawsze mniejsza od 1 (ściskając gradienty płynące wstecz przez warstwy), podczas gdy ReLU nie powinno (jego pochodna to 0 albo 1, więc gradienty przechodzą niezmienione). Sprawdźmy tę prognozę.

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=5, batch_size=32,
                  validation_split=0.2, verbose=2)

Zobaczmy, jak strata zmienia się dla każdej kombinacji głębokości i funkcji aktywacji:

Loss by Activation Function
Computation log

Wyniki potwierdzają prognozę z artykułu teoretycznego — a efekt jest nawet bardziej dramatyczny, niż się spodziewano:

  • 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.

To problem zanikającego gradientu w akcji. Pochodna sigmoidy jest zawsze mniejsza od 1 (maksymalnie 0.25 przy z=0z = 0), więc po 5 warstwach mnożenia gradient docierający do pierwszej warstwy stanowi 0.2550.0010.25^5 \approx 0.001 pierwotnego sygnału. Pierwsze warstwy nie mogą się uczyć, bo gradient jest zbyt mały, by poruszyć wagi.

ReLU nie ma tego problemu — jego pochodna wynosi 0 albo 1, więc gradienty przechodzą niezmienione (dla aktywnych neuronów). Dlatego wszystkie trzy sieci na ReLU zachowują się niemal identycznie niezależnie od głębokości.

Dziś ReLU jest domyślną aktywacją dla większości sieci jednokierunkowych i konwolucyjnych. Jeśli napotkasz problem „umierającego ReLU” (neurony zwracające zero dla wszystkich wejść i przestające się uczyć), spróbuj Leaky ReLU albo ELU — wariantów dopuszczających niewielki gradient przy ujemnym wejściu. W architekturach transformerowych standardem stało się GELU. Sigmoida i tanh są nadal używane w konkretnych miejscach — sigmoida do wyjść binarnych (np. ostatnia warstwa klasyfikatora binarnego), tanh do mechanizmów bramkowania w LSTM — ale nie jako uniwersalne aktywacje warstw ukrytych.

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 przez wszystkie 50 epok — z 0.33 w epoce 1 do 0.0015 w epoce 50. Dokładność treningowa osiąga 100% do epoki 29. Model zapamiętał doskonale każdy pojedynczy obraz treningowy.

Ale strata walidacyjna opowiada inną historię: poprawia się 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 dokładność walidacyjna wychodzi na plateau w okolicach 98% od epoki 13 i ledwo drgnie przez pozostałe 37 epok.

To przeuczenie — model zaczął zapamiętywać dane treningowe zamiast uczyć się wzorców, które się uogólniają. Po pewnym momencie dalszy trening pogarsza przewidywania modelu na nowych danych, nawet gdy staje się on lepszy na danych, które już widział.

Liczba epok to hiperparametr jak każdy inny, ale ma pewną szczególną własność: za mało i model za mało się nauczył (niedouczenie), za dużo i zapamiętuje dane treningowe (przeuczenie). W przeciwieństwie do współczynnika uczenia czy rozmiaru batcha, gdzie istnieje złoty środek, który można znaleźć i przy nim zostać, właściwa liczba epok zależy od modelu, danych i wszystkich pozostałych hiperparametrów.

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 hojną górną granicę epok (np. 100) i pozwól wczesnemu zatrzymaniu znaleźć właściwy moment. Parametr patience=5 oznacza, że trening zatrzyma się, jeśli strata walidacyjna nie poprawi się przez 5 kolejnych epok, a restore_best_weights=True cofa model do epoki z najlepszą stratą walidacyjną. Dzięki temu w ogóle nie musisz dobierać liczby epok — dane same powiedzą Ci, kiedy przestać.

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.

Warto znać tę garstkę strategii, które takie narzędzia implementują, bo wszystkie idą na kompromis wzdłuż tej samej osi: ile konfiguracji stać Cię ocenić. Każda z nich to inna odpowiedź na pytanie „skoro każdy przebieg kosztuje czas i moc obliczeniową, gdzie wydać następny?”

  • Przeszukiwanie siatki (grid search). Wybierz po kilka wartości na hiperparametr i wypróbuj każdą kombinację. Proste i wyczerpujące, ale koszt to iloczyn liczby opcji — trzy hiperparametry po pięć wartości każdy to już 125 przebiegów, a dalej rośnie lawinowo (ta sama kombinatoryczna eksplozja, która sprawia, że przestrzenie wielowymiarowe są trudne). W porządku, gdy przebieg jest tani i masz dwie–trzy gałki.
  • Przeszukiwanie losowe (random search). Losuj kombinacje zamiast układać kratę. Wbrew intuicji, przy tym samym budżecie zwykle bije to przeszukiwanie siatki: zazwyczaj naprawdę liczy się tylko kilka hiperparametrów, a losowanie próbuje więcej różnych wartości wzdłuż tych istotnych wymiarów, zamiast marnować przebiegi na sztywną kratę (standardowe odniesienie to Bergstra & Bengio, 2012).
  • Spadek po współrzędnych, czyli wspinaczka górska. To zautomatyzowana forma dokładnie tego, co robiliśmy ręcznie przez cały artykuł: zacznij od domyślnych, zmieniaj jeden hiperparametr naraz, zachowaj zmianę, jeśli wynik się poprawi, i powtarzaj, aż nic już nie pomaga. Jest tanie — nigdy nie oceniasz pełnej siatki — i w przeciwieństwie do jednorazowego strojenia każdej gałki w izolacji zawraca, więc może wychwycić, że najlepszy rozmiar batcha zależy od współczynnika uczenia, na którym ostatecznie stanęło. Jego słabość jest klasyczna dla każdego zachłannego przeszukiwania lokalnego: może utknąć w optimum lokalnym, czyli ustawieniu, w którym żadna pojedyncza zmiana nie pomaga, choć jakaś zmiana łączna by pomogła.
  • Optymalizacja bayesowska. Zbuduj probabilistyczny model „konfiguracja → wynik” na podstawie dotychczasowych przebiegów, a potem użyj go, by wybrać najbardziej obiecującą konfigurację do wypróbowania — równoważąc eksplorację nieznanych obszarów z eksploatacją tych dobrych. To najoszczędniejsza próbkowo opcja i dlatego jest domyślna, gdy każdy przebieg jest naprawdę drogi.

Wybór sprowadza się do kosztu pojedynczej oceny. Jeśli przebieg trwa sekundy, przeszukiwanie siatki albo losowe w zupełności wystarczy — po prostu spróbuj wielu wariantów. Jeśli każdy przebieg to pełne zadanie treningowe (albo gorzej), przechodzisz na metody, które myślą, zanim wydadzą: spadek po współrzędnych dla kilku gałek, optymalizacja bayesowska dla wielu. Ładny przykład spoza treningu: w pracy o AgentDiet dobierano cztery hiperparametry, gdzie każda ocena oznaczała przepuszczenie agenta LLM przez 100 zadań — o wiele za drogo na siatkę — więc użyto zwykłego spadku po współrzędnych („zmień jedną gałkę, zachowaj poprawy, powtórz”) i zbieżność nastąpiła po dwóch przejściach.

Weights & Biases Sweeps i Optuna implementują to wszystko — przeszukiwanie siatki, losowe i bayesowskie — automatycznie śledząc każdy przebieg i porównując wyniki, a Keras Tuner oferuje to samo, zintegrowane bezpośrednio z Kerasem. Stają się cenne dokładnie wtedy, gdy ręczne strojenie po jednym parametrze przestaje wystarczać.