Model uczenia maszynowego ma znajdować w danych regularności, a mimo to na prawie każdym etapie jego budowy pojawiają się liczby losowe. Losowo dzieli się dane na zbiór treningowy i testowy, losowo ustawia wagi sieci przed treningiem, losowo wybiera porcje danych do kolejnych kroków uczenia i losowo wyłącza część neuronów.
W tym artykule opisujemy, po co w każdym z tych miejsc jest losowość i co by się stało bez niej. Pokazujemy też, jak ustawić ziarno, żeby wynik dało się powtórzyć, bo to w praktyce najczęstsze pytanie osób zaczynających pracę z modelami.
Gdzie pojawia się losowość
- Podział danych
- Losowy wybór przykładów do zbioru treningowego, walidacyjnego i testowego.
- Optymalizacja
- Losowa kolejność przykładów i losowe mini-batche w metodzie SGD.
- Inicjalizacja
- Losowe wartości początkowe wag, które różnicują neurony.
- Regularyzacja
- Dropout i dodawanie szumu, które utrudniają modelowi zapamiętanie danych.
- Zespoły modeli
- Losowe próbki danych i cech w lasach losowych i baggingu.
- Augmentacja
- Losowe obroty, przycięcia i zmiany kolorów obrazów treningowych.
Do tego dochodzi uczenie ze wzmocnieniem, w którym agent czasem celowo wybiera losowe działanie zamiast najlepszego znanego, żeby sprawdzić, czy nie istnieje lepsza strategia. Wszystkie te liczby pochodzą ze zwykłych generatorów pseudolosowych. Jak działają, opisujemy w artykule o algorytmach losowania w komputerach.
Podział i mieszanie danych
Model ocenia się na danych, których nie widział podczas treningu. Dlatego przed treningiem dzieli się zbiór na część treningową i testową. Podział musi być losowy: jeśli dane są posortowane, na przykład według daty albo klasy, odcięcie ostatnich 20 procent dałoby zbiór testowy zupełnie niepodobny do treningowego.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # 20% danych do testu
stratify=y, # zachowaj proporcje klas w obu częściach
random_state=42, # ziarno: ten sam podział przy każdym uruchomieniu
) Parametr random_state to ziarno. Z tą samą wartością podział wychodzi identyczny przy każdym uruchomieniu, co pozwala uczciwie porównywać modele. To samo dotyczy walidacji krzyżowej, w której dane dzieli się losowo na kilka części i każda z nich po kolei służy do testu.
SGD i mini-batche
Sieć uczy się, poprawiając wagi w kierunku, w którym maleje błąd. Klasyczny spadek gradientu liczy ten kierunek na całym zbiorze danych, co przy milionach przykładów jest bardzo wolne. Stochastyczny spadek gradientu (SGD) liczy go na małej, losowo wybranej porcji danych, czyli mini-batchu.
- Przemieszaj zbiór treningowy w losowej kolejności.
- Weź kolejną porcję, na przykład 32 przykłady.
- Oblicz błąd i jego gradient tylko dla tej porcji.
- Popraw wagi o mały krok w kierunku przeciwnym do gradientu.
- Gdy skończą się porcje, zacznij nową epokę od ponownego przemieszania danych.
Gradient z małej porcji jest tylko przybliżeniem, więc kolejne kroki są trochę chaotyczne. Ten szum ma jednak zalety: ułatwia wyjście z płytkich minimów i, jak sugerują badania, takie jak praca Smitha i Le z 2018 roku, sprzyja rozwiązaniom, które lepiej działają na nowych danych. Popularne optymalizatory, takie jak SGD z momentem, RMSprop i Adam, różnią się sposobem wyliczania kroku, ale wszystkie korzystają z tych samych losowych mini-batchy.
Losowość pomaga też przy wyborze hiperparametrów, na przykład tempa uczenia. James Bergstra i Yoshua Bengio pokazali w 2012 roku, że losowe próbkowanie ustawień często znajduje dobre wartości szybciej niż sprawdzanie regularnej siatki, bo zwykle tylko kilka hiperparametrów naprawdę ma znaczenie.
Inicjalizacja wag
Przed treningiem wagi sieci trzeba czymś wypełnić. Zera nie działają: wszystkie neurony w warstwie liczyłyby wtedy to samo i dostawały te same poprawki, więc do końca treningu pozostałyby identyczne. Losowe wartości początkowe łamią tę symetrię.
Ważna jest też skala. Zbyt duże wagi sprawiają, że sygnał rośnie z warstwy na warstwę, a zbyt małe, że zanika. Xavier Glorot i Yoshua Bengio pokazali w 2010 roku, jak dobrać rozrzut wag do liczby wejść i wyjść warstwy. Kaiming He i współpracownicy dopasowali tę metodę w 2015 roku do funkcji aktywacji ReLU.
| Metoda | Odchylenie standardowe wag | Zwykle z aktywacją |
|---|---|---|
| Xavier (Glorot) | √(2 / (nwe + nwy)) | tanh, sigmoid |
| He (Kaiming) | √(2 / nwe) | ReLU i pochodne |
| Ortogonalna | macierz wag jest losową macierzą ortogonalną | sieci rekurencyjne |
nwe i nwy to liczba wejść i wyjść warstwy. Wagi losuje się z rozkładu normalnego o średniej zero i podanym odchyleniu. Istnieją też wersje z rozkładem jednostajnym.
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 10)
# inicjalizacja He dla warstwy z ReLU
nn.init.kaiming_normal_(self.fc1.weight, nonlinearity='relu')
# biasy mogą zostać zerami, symetrię łamią już wagi
nn.init.zeros_(self.fc1.bias)
def forward(self, x):
return self.fc2(nn.functional.relu(self.fc1(x))) Dropout
Duża sieć potrafi zapamiętać dane treningowe zamiast nauczyć się ogólnych reguł. Dropout utrudnia to w prosty sposób: w każdym kroku treningu losowo wyłącza część neuronów, zwykle od 20 do 50 procent. Sieć nie może więc polegać na żadnym pojedynczym neuronie i musi rozłożyć wiedzę na wiele z nich. Metodę dokładnie opisali Nitish Srivastava, Geoffrey Hinton i współpracownicy w 2014 roku.
Wypróbuj: dropout w małej sieci
Każdy krok trenuje więc trochę inną, mniejszą sieć. Podczas przewidywania wyłączanie się kończy i działają wszystkie neurony. Żeby skala wyjść się zgadzała, wartości przeskalowuje się: w PyTorchu i Kerasie dzieje się to już podczas treningu, przez podzielenie wyjść pozostałych neuronów przez prawdopodobieństwo ich pozostawienia.
import torch.nn as nn
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(p=0.5), # w treningu wyłącza losowo 50% neuronów
nn.Linear(256, 10),
)
model.train() # dropout włączony
model.eval() # dropout wyłączony, np. przy testach i przewidywaniu Na tej samej zasadzie działają pokrewne metody. DropConnect wyłącza losowo pojedyncze połączenia zamiast neuronów. Stochastic depth pomija w czasie treningu całe bloki sieci rezydualnej. Prostszym sposobem jest dodawanie losowego szumu do wejść albo wag.
Lasy losowe i bagging
Pojedyncze drzewo decyzyjne łatwo dopasowuje się do przypadkowych szczegółów danych. Las losowy, opisany przez Leo Breimana w 2001 roku, buduje wiele drzew i łączy ich głosy. Żeby drzewa się od siebie różniły, korzysta z dwóch rodzajów losowości.
- Bagging
- Każde drzewo uczy się na losowej próbce danych, wylosowanej ze zwracaniem z całego zbioru. Ten sam przykład może trafić do próbki kilka razy, a inny wcale.
- Losowe cechy
- Przy każdym podziale drzewo wybiera najlepszą cechę tylko spośród losowo wybranego podzbioru cech.
Błędy pojedynczych drzew są wtedy w dużej mierze niezależne i przy głosowaniu się znoszą. Metoda extremely randomized trees idzie dalej i losuje także progi podziału.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_estimators=100, # liczba drzew
max_features='sqrt', # liczba losowo wybieranych cech przy podziale
bootstrap=True, # każde drzewo dostaje losową próbkę ze zwracaniem
random_state=42,
)
rf.fit(X_train, y_train)
print(rf.score(X_test, y_test)) Augmentacja danych
Zebranie nowych danych jest drogie, ale z istniejących można zrobić wiele wersji. Kot obrócony o kilka stopni albo trochę przyciemniony nadal jest kotem. Augmentacja losowo przekształca przykłady treningowe przy każdym ich użyciu, więc model rzadko widzi dwa razy dokładnie to samo zdjęcie.
Dobór przekształceń też bywa losowy. AutoAugment szuka najlepszego zestawu przy pomocy uczenia ze wzmocnieniem. RandAugment upraszcza to do losowego wyboru kilku przekształceń o zadanej sile, a AugMix losowo łączy kilka łańcuchów przekształceń w jeden obraz.
Ziarno i powtarzalność wyników
Skoro losowość pojawia się w tylu miejscach, dwa treningi tego samego modelu na tych samych danych dają trochę inne wyniki. Xavier Bouthillier i współpracownicy pokazali w 2021 roku, że sama zmiana ziarna potrafi przesunąć wynik modelu na tyle, że różnice między porównywanymi metodami w publikacjach bywają mniejsze niż ten rozrzut.
Żeby wynik dało się powtórzyć, ziarno ustawia się we wszystkich bibliotekach, z których korzysta program, bo każda ma własny generator:
import random
import numpy as np
import torch
SEED = 42
random.seed(SEED) # moduł random z biblioteki standardowej
np.random.seed(SEED) # NumPy
torch.manual_seed(SEED) # PyTorch, CPU i GPU
torch.use_deterministic_algorithms(True) # deterministyczne operacje na GPU
# TensorFlow i Keras: jedno wywołanie
# ustawia ziarno dla random, NumPy i TensorFlow
# keras.utils.set_random_seed(SEED) Nawet z ustawionym ziarnem część operacji na kartach graficznych może dawać minimalnie różne wyniki, bo kolejność sumowania liczb zmiennoprzecinkowych bywa niedeterministyczna. Tryb deterministyczny to wyłącza, kosztem szybkości. Przy ważnych porównaniach lepiej zrobić coś więcej: wytrenować model z kilkoma różnymi ziarnami i podać średnią oraz rozrzut wyników.
Prace źródłowe
Jeśli chcesz poczytać więcej, te publikacje opisują metody z artykułu u źródła:
-
2001
Leo Breiman
„Random Forests”. Opis lasów losowych.
-
2010
Glorot, Bengio
„Understanding the difficulty of training deep feedforward neural networks”. Inicjalizacja Xaviera.
-
2012
Bergstra, Bengio
„Random Search for Hyper-Parameter Optimization”. Losowe przeszukiwanie hiperparametrów.
-
2014
Srivastava i in.
„Dropout: A Simple Way to Prevent Neural Networks from Overfitting”.
-
2015
He i in.
„Delving Deep into Rectifiers”. Inicjalizacja He dla sieci z ReLU.
-
2018
Smith, Le
„A Bayesian Perspective on Generalization and Stochastic Gradient Descent”.
-
2018
Zhang i in.
„mixup: Beyond Empirical Risk Minimization”.
-
2021
Bouthillier i in.
„Accounting for Variance in Machine Learning Benchmarks”. Wpływ ziarna na wyniki porównań.
O tym, jak przez lata zmieniały się same generatory, piszemy w artykule o historii generatorów liczb losowych. Losowe liczby do własnych eksperymentów wylosujesz w generatorze liczb, a gotowe zestawy danych w generatorze danych testowych.
Często zadawane pytania
Dlaczego wag sieci neuronowej nie ustawia się na zero?
Jeśli wszystkie wagi w warstwie mają tę samą wartość, wszystkie neurony liczą to samo i dostają te same poprawki podczas uczenia. Pozostają identyczne przez cały trening, więc warstwa zachowuje się jak jeden neuron.
Losowe wartości początkowe sprawiają, że neurony od początku się różnią i mogą nauczyć się różnych cech.
Co oznacza random_state w scikit-learn?
To ziarno generatora liczb losowych używanego przez funkcję albo model. Ta sama wartość random_state daje ten sam podział danych i ten sam model przy każdym uruchomieniu, co ułatwia porównywanie wyników.
Bez niego każde uruchomienie może dać trochę inny wynik.
Czy dropout działa podczas przewidywania?
Nie. Neurony są wyłączane losowo tylko podczas treningu. Przy przewidywaniu sieć używa wszystkich neuronów, a skala wyjść jest wyrównana tak, żeby odpowiadała tej z treningu.
W PyTorchu przełącza się to metodami model.train() i model.eval().
Dlaczego ten sam model daje różne wyniki przy kolejnych uruchomieniach?
Bo losowe są inicjalizacja wag, kolejność danych w mini-batchach, dropout i augmentacja. Każde uruchomienie z innym ziarnem prowadzi trening trochę inną drogą.
Żeby wyniki dało się powtórzyć, ustawia się ziarno we wszystkich używanych bibliotekach, a przy obliczeniach na GPU włącza się dodatkowo tryb deterministyczny.
Czy do uczenia maszynowego potrzebny jest generator kryptograficzny?
Nie. Uczenie maszynowe potrzebuje dużo liczb szybko i często w powtarzalny sposób, więc korzysta ze zwykłych generatorów pseudolosowych z ustawianym ziarnem.
Generator kryptograficzny jest potrzebny tam, gdzie liczy się bezpieczeństwo, na przykład przy kluczach i hasłach.