Od nachylenia do propagacji wstecznej: zbuduj mały silnik autogradowy
W naszym artykule na temat uczenia się sieci neuronowych prześledziliśmy pętlę treningową modelu: dokonaliśmy prognozy, zmierzyliśmy błąd za pomocą funkcji straty, obliczyliśmy gradienty i zaktualizowaliśmy wagi. Aby dostosować wagi, użyliśmy gradientów — pochodnych, które mówią nam, jak zmienia się strata w odniesieniu do każdej wagi. Zejście gradientowe wykorzystuje te pochodne do wyboru kierunku aktualizacji.
W tym artykule chcemy się skupić na sposobie obliczania tych gradientów. Jeśli strata modelu wynosi , jak możemy obliczyć, że zwiększenie jednej wagi spowoduje jej zmniejszenie? Którą funkcję różniczkujemy i skąd wynikają reguły jej pochodnej?
Aby odpowiedzieć na to pytanie, musimy zastosować się do obliczeń, które spowodowały stratę. Przewidywanie i strata razem tworzą funkcję parametrów modelu. Będziemy budować na podstawie , obliczać pochodne poprzez poszczególne operacje i implementować mały silnik automatycznego różnicowania w Pythonie. Następnie użyjemy go do uczenia modelu.
Nasz silnik wykorzystuje automatyczne różnicowanie w trybie odwrotnym, czyli autodiff: rejestruje operacje w ocenie w przód przy bieżących wartościach parametrów, a następnie stosuje reguły pochodne wstecz. Będziemy również używać różnic skończonych do dwóch odrębnych celów: wizualizacji, co oznacza nachylenie i niezależnego sprawdzania odpowiedzi silnika. Te szacunki dotyczące pobliskich punktów należą do wyjaśnienia i weryfikacji; pętla treningowa pobiera gradienty z autodiff.
Potrzebujesz tylko podstawowego języka Python i algebry. Wprowadzimy rachunek różniczkowy tak, jak go używamy. Implementacja jest zgodna z podejściem obliczeniowym skalarnym nauczanym w wykładzie mikrogradowym Andreja Karpathy’ego, przy użyciu jawnej tabeli lokalnych pochodnych na krawędziach wykresu. Nie wymaga bibliotek uczenia maszynowego.
Zacznij od funkcji i jej pochodnej
Rozważ:
W wyjście to . Reguła potęgi podaje jej pochodną zapisaną jako lub :
Pochodna jest nachyleniem stycznej w tym punkcie. Mówi nam, jak czułe jest wyjście na niewielką zmianę sygnału wejściowego:
Pochodna to pożądana ilość. Istnieje kilka metod obliczenia lub przybliżenia tej ilości.
Trzy sposoby uzyskania tej samej pochodnej
Zachowaj stałą funkcję i punkt oceny :
| Metoda | Czego używa | Obliczenia w | Wynik |
|---|---|---|---|
| Symboliczne zróżnicowanie | Reguły stosowane do wyrażenia | Wyprowadź , następnie zamień | |
| Skończone różnice | Wyjścia na pobliskich wejściach | Około | |
| Automatyczne różnicowanie | Wykonywane operacje i ich lokalne reguły dotyczące instrumentów pochodnych | Dla x * x dodaj wkłady |
Zróżnicowanie symboliczne daje wyrażenie pochodne, które możemy ocenić później. Funkcja Autodiff ocenia reguły pochodne na podstawie wartości zarejestrowanych podczas obliczeń. W przypadku tej gładkiej funkcji oba obliczają pochodną w wybranym punkcie, z zastrzeżeniem arytmetyki zmiennoprzecinkowej w kodzie. Różnice skończone szacują to poprzez porównanie wyników w różnych punktach.
Wsteczna propagacja sieci neuronowej wykorzystuje trzecie podejście. Dostarczamy reguły pochodne dla podstawowych operacji, a silnik łączy je za pomocą reguły łańcuchowej. Może obliczyć gradient przy bieżących wartościach parametrów bez generowania jednego dużego wyrażenia symbolicznego dla całej sieci.
Automatyczne różnicowanie poprzez jedno mnożenie
Napisz tę samą funkcję co program:
def f(x):
return x * xZnamy już regułę pochodnej mnożenia. Jeśli , jego lokalnymi pochodnymi są:
W przypadku x * x oba operandy odnoszą się do tego samego wejścia. W każdy operand to , więc każda lokalna pochodna to także . Zmiana w wpływa na wynik poprzez obie ścieżki argumentów. Ich wkład dodaje:
Wybierz , a następnie . Postępuj zgodnie z każdym operandem po kolei, aby zobaczyć skumulowany gradient sięgający .
The trace uses the multiplication edges from the article’s scalar engine. Changing x starts a new trace at that value.
Widżet rozpoczyna przejście wstecz z pochodną wyjścia względem siebie: . Każda ścieżka mnoży nadchodzący gradient przez swoją lokalną pochodną. W zwykłym Pythonie arytmetyka wygląda następująco:
x = 5.0
left, right = x, x
q = left * right # forward: 25.0
output_grad = 1.0 # dq/dq
x_grad = 0.0
x_grad += output_grad * right # left operand contributes 5.0
x_grad += output_grad * left # right operand contributes 5.0
print(x_grad) # 10.0Każda wartość potrzebna dla tej pochodnej pochodzi z obliczeń w . Użyliśmy reguły mnożenia i zapisanych operandów. Obliczanie należy do metody różnic skończonych w tabeli; powyższy ślad autodiff jest kompletny i obejmuje pojedynczy punkt oceny.
Automatyzacja polega na zapisaniu tych ścieżek argumentów i zastosowaniu dla nas ich reguł pochodnych. Później nasza klasa Value wykona te same obliczenia, gdy napiszemy q = x * x i wywołamy q.backward().
Znajomość operacji i zrozumienie kształtu
Załóżmy, że mamy tylko stół:
| Zaobserwowany wynik | |
|---|---|
| 1 | 1 |
| 2 | 4 |
| 3 | 9 |
| 10 | 100 |
Te skończone obserwacje nie określają jednoznacznie funkcji między wymienionymi punktami ani jej dokładnej pochodnej w wymienionym punkcie. Wiele funkcji może zgadzać się z każdym wierszem i mieć różne nachylenia. Jedną z opcji jest obliczenie szacunkowej różnicy skończonej na podstawie dostępnych pobliskich próbek.
Dzięki programowi return x * x mamy więcej informacji: znamy operacje generujące wynik, więc możemy zastosować ich reguły pochodne. Sieć neuronowa również zna obliczenia dotyczące swoich przewidywań i strat. Przykłady szkoleniowe mogą mieć formę tabeli, ale model i strata są wykonywalnymi funkcjami matematycznymi.
Znajomość tych operacji również nie daje nam automatycznie zrozumienia całej krzywej. Rozważ:
Za pomocą tych wzorów możemy wyznaczyć wartość i pochodną w dowolnym wybranym punkcie. Czy potrafisz sobie wyobrazić każdy wzrost, spadek i punkt zwrotny po prostu na podstawie ich przeczytania? Przesuń poniżej, a następnie wybierz . Wartość lokalna i nachylenie są dostępne, zanim cały wykres będzie widoczny.
The value and derivative are computed from known rules at the selected point. Revealing the curve additionally samples the function across the displayed interval.
Wzór całkowicie określa funkcję. Zrozumienie jego globalnej geometrii wymaga dodatkowej oceny lub analizy. Wykres daje nam więcej informacji w wybranym przedziale, podczas gdy pochodna opisuje lokalną wrażliwość w jednym punkcie.
W przypadku sieci neuronowej strata może zależeć od milionów wag i odchyleń. Znamy program obliczający przy wybranym ustawieniu parametrów. Jego gradient nadaje tam lokalną wrażliwość. Obliczenia te nie pozwalają zlokalizować każdego minimum w innym miejscu tej wielowymiarowej przestrzeni. Trening wykorzystuje informacje lokalne do wyboru aktualizacji, a następnie oblicza nowy gradient przy zaktualizowanych parametrach.
Użyj pobliskich punktów, aby zwizualizować nachylenie
Wróćmy teraz do , aby zapoznać się z pierwszym z dwóch zastosowań skończonych różnic: budowanie intuicji nachylenia. Obliczyliśmy już jego pochodną poprzez funkcję autodiff. Ten eksperyment ilustruje, co ta pochodna oznacza geometrycznie.
Porównaj i . Ich średnie nachylenie wynosi:
W przypadku ogólnej niezerowej zmiany wejścia :
Gdy zbliża się do zera, stosunek ten zbliża się do pochodnej . W oszacowanie różnicy skończonej zbliża się do .
Zmniejsz do i obserwuj, jak pomarańczowa sieczna zbliża się do fioletowej stycznej. Następnie przesuń na wartość ujemną: nachylenie ujemne oznacza, że niewielki wzrost sygnału wejściowego powoduje zmniejszenie sygnału wyjściowego.
A small increase in x increases f(x). The finite-difference error is h = 0.8.
Purpose: visualize the meaning of a derivative. The training experiments compute gradients with automatic differentiation.
To obliczenie pobliskiego punktu jest skończoną różnicą. Później wykorzystamy kolejną skończoną różnicę do niezależnej weryfikacji silnika. W eksperymentach szkoleniowych gradienty pochodzą z zarejestrowanych operacji i ich lokalnych reguł pochodnych.
W przypadku dokładna zmiana wyjścia wynosi . Pochodna obejmuje część pierwszego rzędu; nie przewiduje dokładnie dowolnych skończonych zmian.
Prognoza daje nam coś do oceny
Załóżmy teraz, że model otrzymuje dane wejściowe i powinien przewidzieć docelowy . Nadaj mu jedną wagę i jedno odchylenie :
Kapelusz w oznacza przepowiednię. Przy i prognoza to , czyli o poniżej wartości docelowej.
Co powinien minimalizować trening? Minimalizowanie samej prognozy obniżyłoby ją, mimo że potrzebujemy jej wzrostu. Potrzebujemy funkcji, która mierzy rozbieżność między przewidywaniem a celem.
Wybierz błąd kwadratowy:
Obecna strata wynosi . Zero oznaczałoby, że przewidywanie odpowiada celowi w tym przykładzie.
Zastąp obliczenia modelu stratą:
To jest połączenie, którego potrzebujemy. Strata jest teraz funkcją parametrów, które możemy zmienić. Dla ustalonego przykładu szkoleniowego :
Sama liczba nie mówi nam, w którą stronę mamy się poruszać. W przypadku zarówno , jak i powodują stratę , ale muszą poruszać się w przeciwnych kierunkach. Rozróżniamy funkcję, która spowodowała stratę, ocenianą przy bieżących parametrach.
To samo obliczenie, z wagą jako zmienną
Przytrzymaj wejście treningowe , cel i stałe odchylenie . Strata staje się zwykłą funkcją jednej zmiennej:
Możemy ręcznie rozróżnić to małe wyrażenie i zastąpić aktualną wagę, dokładnie tak, jak zrobiliśmy to w przypadku :
| Krok | Znana funkcja | Strata modelu przy stałych danych treningowych |
|---|---|---|
| Napisz funkcję | ||
| Różnicuj | ||
| Wstaw aktualną wartość | ||
| Uzyskaj pochodną |
Litera nazywająca zmienną została zmieniona z na . Dane wejściowe i cel szkolenia to stałe liczby w ; pytamy, jak strata reaguje na wagę. To samo rozróżnienie ma zastosowanie, gdy używamy całej partii: dane wejściowe i cele partii pozostają stałe, podczas gdy różnicujemy pod względem parametrów.
Model ten jest na tyle mały, że można go rozwinąć do wielomianu. W przypadku większej sieci funkcja autodiff rejestruje operacje do przodu przy bieżącym ustawieniu parametrów i ocenia ich lokalne reguły pochodne wstecz. Oblicza wartości liczbowe pochodnych parametrów bez uprzedniego konstruowania jednego rozszerzonego wzoru symbolicznego dla gradientu sieci.
Którą pochodną weźmiemy?
Na wagę chcemy:
Przeczytaj to w następujący sposób: jak szybko zmieniłaby się strata, gdybyśmy zmienili wagę, utrzymując inne niezależne wielkości na stałym poziomie? Używa się symbolu , ponieważ zależy od kilku zmiennych. W przypadku tych obliczeń dane wejściowe, cel i odchylenie pozostają stałe.
Model i strata tworzą kompozycję. Najpierw oblicz przewidywanie, następnie odejmij wartość docelową i podnieś błąd do kwadratu:
Każda operacja ma lokalną pochodną:
| Operacja | Jakie zmiany? | Lokalna pochodna |
|---|---|---|
| Zmień , przytrzymaj nieruchomo | ||
| Zmień , przytrzymaj nieruchomo | ||
| Zmień |
Reguła łańcucha mnoży te wrażliwości wzdłuż ścieżki:
Przy naszych wartościach wyjściowych i :
Zwiększenie masy powinno zatem zmienić stratę o około . Dokładna nowa strata to , zmiana . Zgodność poprawia się w miarę zmniejszania się kroku.
W przypadku błędu zmienia się ostatni czynnik: . A więc:
Pochodne w odniesieniu do wszystkich możliwych do wytrenowania parametrów tworzą razem gradient. Tutaj tym wektorem jest , w kolejności .
Wybór straty zmienia sygnał uczenia
Model mówi nam, jak jego przewidywania reagują na wagę. Strata mówi nam, jak bardzo zmiana przewidywania ma znaczenie dla celu.
Aby uzyskać tę samą prognozę, porównaj dwie straty:
| Strata | Pochodna ze względu na przewidywanie | Pochodna po wadze |
|---|---|---|
| Błąd kwadratowy: | ||
| Błąd bezwzględny: | , od zera | , od zera |
W naszym punkcie wyjścia gradient predykcji wynosi dla błędu kwadratowego i dla błędu bezwzględnego. Mnożenie przez tę samą pochodną modelu, , daje gradienty wagowe i .
Przełącz pomiędzy i poniżej. Poruszaj , obserwując zarówno gradient predykcji, jak i gradient ciężaru. Dane wejściowe pozostają stałe, więc zmienia się tylko pierwszy czynnik.
x = 2, y = 5, and b = 0 stay fixed. This experiment updates only w.
Błąd kwadratowy daje większy gradient przewidywania, gdy wielkość błędu jest większa. Błąd bezwzględny ma stałą wielkość gradientu w odniesieniu do przewidywania, gdy błąd jest różny od zera. Jego pochodna jest nieokreślona przy zera; widget wybiera tam podgradient zera.
Żadna strata nie dostarcza reguł różnicowania. Zasady te pochodzą z rachunku różniczkowego. Wybór strat określa funkcję, na którą działają te reguły, a co za tym idzie, sygnał początkowy propagowany w modelu.
Instrument pochodny staje się aktualizacją
Zejście gradientowe odejmuje gradient skalowany:
Szybkość uczenia określa wielkość kroku. Z błędem kwadratowym, i :
Utrzymując odchylenie na poziomie zerowym, nowa prognoza to , a nowa strata to . Użyj i w eksperymencie ze stratami, aby odtworzyć to obliczenie.
Tutaj zaktualizowaliśmy tylko wagę. Jeśli zaktualizujemy również odchylenie z tego samego gradientu, stanie się ono , dając prognozę i stratę zerową dla tego pojedynczego przykładu. Są to różne aktualizacje, więc dają różne wyniki.
Ujemna pochodna sugeruje zwiększenie parametru; dodatnia pochodna sugeruje jej zmniejszenie. To jest lokalne oświadczenie. Duży krok może przekroczyć minimum i zwiększyć stratę. Propagacja wsteczna oblicza gradient; Optymalizator i jego szybkość uczenia się określają, co z nim zrobić.
Propagacja wsteczna to reguła łańcuchowa przechodząca przez wykres
Obliczenia możemy zapisać w postaci wykresu. W każdym pudełku znajduje się liczba, a każda operacja łączy swój wynik z wartościami użytymi do jego wygenerowania.
Zacznij od skrajnego prawego pola. Pochodna straty względem samej siebie jest równa:
Następnie pracuj do tyłu. Pudełko otrzymuje gradient informujący nas, jak ostateczna strata reaguje na wartość tego pola. Aby przekazać wkład do jednego z wejść, pomnóż przez lokalną pochodną operacji:
Wybierz , następnie , aż dojdziesz do i . Każdy wyświetlany gradient jest pochodną końcowej straty, nawet jeśli ramka przedstawia wynik pośredni.
Scroll the graph sideways to follow all the operations.
| Value | Forward value | Gradient |
|---|---|---|
| w | 1 | — |
| x | 2 | — |
| p = w × x | 2 | — |
| b | 0 | — |
| ŷ = p + b | 2 | — |
| y (target) | 5 | — |
| e = ŷ − y | -3 | — |
| L = e² | 9 | — |
Wykres przedstawia również pochodne w odniesieniu do wartości wejściowej i docelowej. To nie sprawia, że nadają się do szkolenia. Wybieramy, które wartości aktualizuje optymalizator: tutaj tylko i . W głębszej sieci „wejście” warstwy może być wyjściem poprzedniej warstwy, więc jej gradient staje się drogą powrotną do wcześniejszych parametrów.
Ważne rozróżnienie pojawia się, gdy wartość ma wiele ścieżek prowadzących do straty. Wkłady z tych ścieżek dodaj. Na przykład:
W przypadku tą pochodną jest . Przejście wstecz musi zachować wszystkie trzy wkłady, w tym oba użycia w mnożeniu. Dlatego nasz silnik będzie używać += podczas propagacji gradientów.
Zbuduj liczbę, która pamięta obliczenia
Zwykły float Pythona przechowuje wartość. Nasz Value będzie również przechowywać:
grad: pochodna wybranego wyjścia końcowego w odniesieniu do tej wartości.edges: sekwencja par(parent, local_derivative)rejestrująca sposób obliczenia tej wartości.
Dodawanie i mnożenie stają się operacjami tworzenia grafów. W przypadku obie pochodne krawędziowe to . W przypadku są to i .
Oto pierwsza część silnika, zawierająca potęgi całkowite i aktywację, z której skorzystamy później:
import math
class Value:
def __init__(self, data, edges=()):
self.data = float(data)
self.grad = 0.0
self.edges = tuple(edges) # (parent, local derivative) pairs
@staticmethod
def wrap(value):
return value if isinstance(value, Value) else Value(value)
def __add__(self, other):
other = Value.wrap(other)
return Value(self.data + other.data, ((self, 1.0), (other, 1.0)))
def __mul__(self, other):
other = Value.wrap(other)
return Value(self.data * other.data,
((self, other.data), (other, self.data)))
def __pow__(self, exponent):
if not isinstance(exponent, int) or exponent < 1:
raise ValueError("This engine supports positive integer powers only")
return Value(self.data ** exponent,
((self, exponent * self.data ** (exponent - 1)),))
def tanh(self):
output = math.tanh(self.data)
return Value(output, ((self, 1.0 - output * output),))Kiedy mnożymy dwa obiekty Value, __mul__ natychmiast oblicza wynik i zapisuje dwie krawędzie z ich lokalnymi pochodnymi. Te wartości pochodnych są oceniane przy użyciu bieżących danych operandów. Nic się jeszcze nie nauczyło; rejestrujemy obliczenia w przód.
Realizujemy potęgi bezpośrednio, więc (prediction - target) ** 2 rejestruje operację kwadratową. Silnik celowo obsługuje tylko dodatnie potęgi całkowite. Obejmuje to nasze przykłady bez wprowadzania dziedzin takich jak potęgi ułamkowe liczb ujemnych.
Tangens hiperboliczny, tanh, jest gładką funkcją aktywacji z pochodną . Pozwala sieci wyrażać relacje nieliniowe.
Wygodne operatory Pythona sprawiają, że korzystanie z silnika jest przyjemne. Operatory odwrotne umożliwiają wyrażenia takie jak 2 * w i sum(...):
def __neg__(self):
return self * -1.0
def __sub__(self, other):
return self + -Value.wrap(other)
def __rsub__(self, other):
return Value.wrap(other) + -self
__radd__ = __add__
__rmul__ = __mul__Wszystkie te metody należą do Value. complete Python file zawiera silnik, modele i przykłady wykonalności. Możesz także download tinygrad.py.
Odwiedź wykres w odpowiedniej kolejności
Aby obliczyć gradienty, musimy przetworzyć każdy wynik przed jego elementami nadrzędnymi, po otrzymaniu wszystkich dalszych składek tego wyniku.
Po pierwsze, przechodzenie w głąb odwiedza rodziców węzła przed dołączeniem samego węzła. Tworzy to porządek topologiczny. Zestaw seen gwarantuje, że węzeł współdzielony zostanie dodany tylko raz. Odwrócenie kolejności powoduje umieszczenie końcowego wyniku na pierwszym miejscu.
Dodaj tę metodę do Value:
def backward(self):
order, seen = [], set()
def visit(node):
if node in seen:
return
seen.add(node)
for parent, _ in node.edges:
visit(parent)
order.append(node)
visit(self)
for node in order:
node.grad = 0.0
self.grad = 1.0
for node in reversed(order):
for parent, local_derivative in node.edges:
parent.grad += node.grad * local_derivativeIstnieją trzy oddzielne kroki: usunięcie starych gradientów z osiągalnego wykresu, zaszczepienie gradientu końcowego wynikiem jednym i przejście wstecz w celu zgromadzenia składowych.
Zestaw seen deduplikuje węzły, podczas gdy edges zachowuje każde wystąpienie argumentu. W a * a dwie krawędzie wskazują na tego samego rodzica. Obydwa wykonują aktualizację +=. Zastąpienie edges zestawem elementów nadrzędnych bez zachowania odpowiednich wkładów spowodowałoby utratę tego rozróżnienia.
Nasz backward() celowo usuwa osiągalne gradienty podczas każdego połączenia. Dzięki temu powtarzalne wywołania tego samego niezmienionego wykresu są powtarzalne. Różni się od interfejsów API, które gromadzą gradienty liści podczas wywołań wstecznych i wymagają osobnego kroku zerowania. W ramach jednego przejścia wstecz wkłady ze wszystkich ścieżek wykresu są nadal kumulowane.
Ta implementacja zapisuje numeryczne lokalne pochodne podczas przebiegu w przód. Zmień dane parametrów po wstecz, a następnie utwórz nowy wykres dla następnej iteracji. Ponowne uruchomienie starego wykresu po zmianie danych spowodowałoby użycie nieaktualnych lokalnych instrumentów pochodnych.
Rekursywne przechodzenie po grafie sprawia, że ten silnik uczący jest krótki. Długie łańcuchy obliczeniowe, takie jak straty zsumowane w tysiącach przykładów, mogą przekroczyć limit rekurencji Pythona; iteracyjne przechodzenie pozwala uniknąć tego ograniczenia.
Sprawdź silnik pod kątem arytmetycznym
Zapisz cały plik jako tinygrad.py. W innym pliku Pythona w tym samym katalogu:
from tinygrad import Value
w, b = Value(1), Value(0)
prediction = w * 2 + b
loss = (prediction - 5) ** 2
loss.backward()
print(loss.data) # 9.0
print(w.grad) # -12.0
print(b.grad) # -6.0
a = Value(3)
q = a * a + a
q.backward()
print(a.grad) # 7.0: all three paths contributeSilnik nigdy nie otrzymał specjalnego wzoru na gradient masy tego modelu. Rejestrował mnożenie, dodawanie, odejmowanie i kwadrat, a następnie łączył ich lokalne pochodne.
Użyj różnic skończonych, aby sprawdzić wynik
To drugi cel ogłoszony na wstępie: niezależna weryfikacja. Silnik uzyskał już w.grad z zarejestrowanych operacji w . Aby sprawdzić jego realizację, oszacuj stratę osobno po obu stronach tego ciężaru:
def numeric_loss(weight):
return (weight * 2 - 5) ** 2
h = 1e-5
finite_difference = (numeric_loss(1 + h) - numeric_loss(1 - h)) / (2 * h)
assert abs(w.grad - finite_difference) < 1e-6Ta centralna różnica jest kontrolą numeryczną, a nie algorytmem różnicowania silnika. Sprawdzenie każdego parametru w ten sposób wymaga dwóch ocen każdego parametru. Zamiast tego automatyczne różnicowanie w trybie odwrotnym ponownie wykorzystuje pojedynczy zarejestrowany wykres do obliczenia gradientów wszystkich parametrów dla wyjścia skalarnego, przy pracy proporcjonalnej do rozmiaru wykresu.
Niezwykle małe wartości ostatecznie wprowadzają anulowanie zmiennoprzecinkowe, więc mniejsze nie zawsze są lepsze w przypadku tej kontroli numerycznej. Poniższe pętle szkoleniowe uzyskują swoje gradienty poprzez wywołanie backward() na bieżącym wykresie.
Ucz się na kilku przykładach
Jeden przykład szkoleniowy nie może określić unikalnej linii: wiele par spełnia . Dodaj więcej przykładów:
| Wprowadź | Cel |
|---|---|
| 0 | 1 |
| 1 | 3 |
| 2 | 5 |
| 3 | 7 |
Punkty te leżą na , więc możemy sprawdzić, czy trening znajdzie i .
Zdefiniuj stratę jako średnią czterech kwadratów błędów:
Wspólna waga pojawia się we wszystkich czterech prognozach. Podczas odtwarzania wstecz każda ścieżka przyczynia się do tego samego w.grad. Mnożenie przez skaluje wszystkie składki tak, aby odpowiadały średniej stracie. Sumowanie zamiast uśredniania pomnożyłoby oba gradienty przez cztery.
Użyj , aby sprawdzić jedną aktualizację, a następnie . Wypróbuj współczynnik uczenia ze stanu resetu, aby zobaczyć wzrost straty, mimo że gradienty są obliczone poprawnie.
Each click builds a fresh graph, averages four squared errors, runs backward(), and updates both w and b. The line plot rescales if the parameters grow; the loss plot uses log₁₀, with a floor of 10⁻¹².
Widget korzysta z JavaScriptowego odpowiednika powyższego silnika skalarnego. Jego gradienty są obliczane na podstawie nowego wykresu na każdym kroku. Oto ta sama pętla treningowa w Pythonie:
def train_line(steps=1000, learning_rate=0.05):
data = [(0, 1), (1, 3), (2, 5), (3, 7)]
w, b = Value(-1), Value(0)
for _ in range(steps):
loss = sum((w * x + b - y) ** 2 for x, y in data) * (1 / len(data))
loss.backward() # also clears this graph's old gradients
for parameter in (w, b):
parameter.data -= learning_rate * parameter.grad
final_loss = sum((w.data * x + b.data - y) ** 2 for x, y in data) / len(data)
return w.data, b.data, final_lossObydwa gradienty pochodzą z tego samego przejścia do przodu przed zmianą któregokolwiek z parametrów. Następna iteracja ponownie oblicza prognozy, straty i instrumenty pochodne przy zaktualizowanych parametrach. Zwrócona strata jest szacowana po ostatniej aktualizacji, więc opisuje zwrócone wagi.
Użyj tego samego silnika dla sieci neuronowej
Do tej pory model był pokrewny. Mała sieć neuronowa nie potrzebuje nowych maszyn różnicujących: jej przebieg w przód stanowi większą kompozycję operacji, które już obsługujemy.
Neuron mnoży dane wejściowe przez wagi, dodaje obciążenie i opcjonalnie stosuje tanh. MLP łączy warstwy neuronów. W tym przykładzie regresji ostatnią warstwę utrzymujemy liniowo:
import random
class Neuron:
def __init__(self, n_inputs, rng, nonlinear=True):
self.weights = [Value(rng.uniform(-1, 1)) for _ in range(n_inputs)]
self.bias = Value(0)
self.nonlinear = nonlinear
def __call__(self, inputs):
total = sum((w * x for w, x in zip(self.weights, inputs)), self.bias)
return total.tanh() if self.nonlinear else total
def parameters(self):
return self.weights + [self.bias]
class MLP:
def __init__(self, sizes, seed=42):
rng = random.Random(seed)
self.layers = [
[Neuron(n_in, rng, nonlinear=i < len(sizes) - 2)
for _ in range(n_out)]
for i, (n_in, n_out) in enumerate(zip(sizes, sizes[1:]))
]
def __call__(self, inputs):
for layer in self.layers:
inputs = [neuron(inputs) for neuron in layer]
return inputs
def parameters(self):
return [p for layer in self.layers for neuron in layer
for p in neuron.parameters()]MLP([2, 4, 1]) tworzy dwa wejścia, cztery ukryte neurony za pomocą tanh i jedno wyjście liniowe. Jego parametry to 8 ukrytych wag, 4 ukryte obciążenia, 4 wagi wyjściowe i 1 obciążenie wyjściowe: 17 możliwych do wyszkolenia skalarów.
Trenuj go na XOR, kodując równe wejścia jako i różne wejścia jako . Żadna funkcja afiniczna dwóch wejść nie może dokładnie pasować do wszystkich czterech celów; ukryte aktywacje nieliniowe pozwalają modelowi je dopasować.
def train_xor(steps=1000, learning_rate=0.1):
# Same inputs twice equal -1; different inputs equal +1.
examples = [([0, 0], -1), ([0, 1], 1), ([1, 0], 1), ([1, 1], -1)]
model = MLP([2, 4, 1], seed=42)
parameters = model.parameters()
for _ in range(steps):
loss = sum((model(x)[0] - y) ** 2 for x, y in examples) * 0.25
loss.backward()
for parameter in parameters:
parameter.data -= learning_rate * parameter.grad
predictions = [model(x)[0].data for x, _ in examples]
final_loss = sum((p - y) ** 2 for p, (_, y) in zip(predictions, examples)) / 4
return predictions, final_lossStruktura uczenia pozostaje niezmieniona: zbuduj prognozy i stratę skalarną, wywołaj backward() i zaktualizuj parametry. Silnik widzi operacje i krawędzie. Nie musi wiedzieć, które operacje należą do neuronu lub warstwy.
Uruchom cały plik:
python3 tinygrad.pyPrzy dostarczonym ziarnie, 1000 krokach i szybkości uczenia się , przewidywania XOR zaokrąglają się do [-1.0, 1.0, 1.0, -1.0] z czterema miejscami po przecinku. Jest to mała demonstracja ze stałymi danymi i inicjalizacją, a nie gwarancja dla dowolnych sieci lub szybkości uczenia się.
Do czego przyczyniła się strata
Zaczęliśmy od pochodnej , która nie wymagała pojęcia modelu ani szkolenia. Następnie wybraliśmy funkcję predykcyjną i dołączyliśmy stratę. Podstawienie uczyniło stratę funkcją parametrów, dając nam precyzyjne pytanie: jak zmieniłby się ten cel, gdyby zmienił się jeden parametr?
Strata określa cel i jego wrażliwość na prognozę. Model dostarcza ścieżki od parametrów do tej prognozy. Propagacja wsteczna łączy lokalne pochodne wzdłuż tych ścieżek, dodając elementy w miejscach, gdzie ścieżki się spotykają. Zejście gradientowe wykorzystuje powstałe gradienty parametrów do wybrania aktualizacji.
W przypadku modelu zbudowanego z operacji z użytecznymi regułami pochodnymi, strata skalarna daje naszemu silnikowi różniczkowemu wynik, od którego można zacząć, i cel, który optymalizator ma zredukować.
Aby zapoznać się z szerszym opisem sieci neuronowych, zobacz Jak uczą się sieci neuronowe: propagacja wsteczna i opadanie gradientu. Aby zapoznać się z uzupełniającą implementacją, która dołącza funkcje wsteczne do operacji, zobacz Silnik mikrogradowy Karpathy’ego i jego Sieci neuronowe: materiały od zera do bohatera.