Od pochodnych do propagacji wstecznej: teoria stojąca za Micrograd
Proces trenowania sieci neuronowej polega na wyszukiwaniu wag i odchyleń, dzięki którym jej przewidywania będą dokładniejsze. Porównujemy te przewidywania z oczekiwanymi odpowiedziami, korzystając z funkcji straty — formuły, która zamienia niedopasowanie na liczbę, przy czym niższa wartość wskazuje lepsze dopasowanie. Aby wybrać aktualizację, musimy wiedzieć, jak zmiana każdego parametru wpłynie na tę stratę. Moglibyśmy losowo zmienić parametr, ponownie obliczyć stratę i porównać wyniki. Jednak sprawdzenie w ten sposób milionów parametrów pojedynczo wymagałoby milionów dodatkowych ocen.
Aby poprowadzić to wyszukiwanie, używamy gradientu, który zbiera pochodne cząstkowe straty w odniesieniu do parametrów. Te pochodne pozwalają oszacować, jak małe zmiany parametrów wpłyną na stratę, bez jej ponownego przeliczania. Dla każdego parametru pochodna opisuje, jak wrażliwa jest strata na ten parametr, podczas gdy pozostałe pozostają stałe.
Matematycznie jest to problem optymalizacji, a konkretnie minimalizacji: szukamy wartości parametrów modelu, które sprawią, że strata będzie jak najmniejsza. Chociaż techniki zerowego rzędu, takie jak MeZO, mogą kierować aktualizacjami poprzez losowe zakłócanie wielu parametrów jednocześnie i ocenę strat, standardowe podejście polega na wykorzystaniu informacji pochodnych do kierowania wyszukiwaniem. Dzisiejsze sieci neuronowe są głównie trenowane przy użyciu gradientów obliczonych przez algorytm znany jako propagacja wsteczna — podejście, które tutaj rozwiniemy.
Aby zrozumieć propagację wsteczną, oprzemy się na kilku koncepcjach: pochodnych lokalnych, regule łańcuchowej i grafach obliczeniowych. Po drodze przyjrzymy się trzem metodom obliczania pochodnych: różniczkowaniu numerycznemu, różniczkowaniu symbolicznemu i różniczkowaniu automatycznemu. Zaczniemy od funkcji na tyle małych, że można je różniczkować ręcznie i porównać ich dokładne pochodne z szacunkami na podstawie różnic skończonych, a następnie przejdziemy do funkcji z kilkoma danymi wejściowymi. Użyjemy pochodnych cząstkowych, aby wyizolować efekt każdego sygnału wejściowego, utrzymując pozostałe na stałym poziomie i zbierając te współczynniki w postaci gradientu.
Następnie prześledzimy zmiany przez połączone operacje: reguła łańcuchowa mnoży pochodne wzdłuż ścieżki i sumuje wkłady, gdy jedno wejście wpływa na wynik kilkoma ścieżkami. Kluczowa idea brzmi: znając pochodne niewielkiego zestawu operacji podstawowych, możemy różniczkować zbudowane z nich funkcje. Nawet pochodną kwadratu można otrzymać z samego mnożenia. Powiążemy tę ideę z różniczkowaniem symbolicznym, a następnie prześledzimy przebieg w przód i wstecz, aby zobaczyć, jak automatyczne różniczkowanie w trybie odwrotnym wykorzystuje wyniki pośrednie do obliczania gradientów dla wielu wejść. Na tym opiera się propagacja wsteczna.
W tym artykule przedstawiono teoretyczne podstawy zrozumienia Micrograd Andreja Karpathy’ego, małego silnika autogradowego z interfejsem API podobnym do PyTorch. W swoim filmie instruktażowym buduje silnik i używa go do uczenia sieci neuronowej. Te same zasady leżą u podstaw torch.autograd.backward firmy PyTorch, który oblicza gradienty przy użyciu reguły łańcuchowej.
Od tempa zmian do pochodnych
Zacznijmy od prostej zależności liniowej i znajdź jej pochodną, badając nachylenie jej wykresu. Pochodna mówi nam, jak szybko zmienia się wynik funkcji, gdy zmieniamy jej dane wejściowe, przy określonej wartości wejściowej. W tym przypadku każdy wzrost powoduje trzykrotny wzrost . Używając do reprezentowania zmiany, możemy napisać:
Kiedy wykreślamy poziomo i pionowo, to stałe tempo zmian daje linię prostą. W przypadku linii prostej pochodna jest równa jej nachyleniu, więc możemy ją znaleźć, obliczając stosunek zmiany sygnału wyjściowego do zmiany sygnału wejściowego, często nazywany „wzrostem po przebiegu”:
Na poniższym wykresie możemy zobaczyć, jak niebieska linia wznosi się o dla przebiegu , dając nachylenie .
Szara przerywana linia i przerywana fioletowa linia rosną tylko o i dla tego samego przebiegu . Przy tych samych skalach osi większe nachylenie bezwzględne oznacza bardziej stromą linię. Wszystkie trzy nachylenia są dodatnie, więc linie rosną, gdy poruszamy się w prawo; nachylenie ujemne spowodowałoby spadek linii.
Odkryliśmy, że niebieska linia ma nachylenie , więc pochodna to . Zapisujemy to jako i czytamy jako „pochodną względem ”. Wyrażenie „w odniesieniu do ” identyfikuje dane wejściowe, które zmieniamy: pochodna mierzy, jak szybko zmienia się sygnał wyjściowy wraz ze zmianami . Nasza funkcja ma tylko jedno wejście, , więc nie ma innych wejść, które można by ustawić na stałe. Dla naszej niebieskiej linii otrzymujemy:
Symbole oznaczają pochodną, natomiast opisuje średnią szybkość zmian w skończonym przedziale czasu. Dla tempo zmian jest stałe i niezależne od , więc pochodną jest , gdziekolwiek ją obliczymy.
Wykorzystaliśmy nachylenie linii do znalezienia pochodnej. Możemy to również obliczyć, korzystając z reguł różniczkowania. Od możemy zastąpić jego wyrażeniem i napisać:
W tym przypadku operator różniczkowy oznacza „różniczkowanie następującego wyrażenia w odniesieniu do ”. W wyrażenie to to , traktowane jako funkcja . Notacja ta jest szczególnie przydatna, gdy poszczególne terminy w większym wyrażeniu traktujemy jako funkcje i różniczkujemy je osobno.
Stosując regułę stałego mnożnika, zachowujemy mnożnik i różniczkujemy . Pochodną względem jest , ponieważ zwiększenie wartości wejściowej o dowolną wielkość powoduje zwiększenie wartości wyjściowej o dokładnie tę samą wielkość:
Reguły różniczkowania umożliwiają nam systematyczne obliczanie współczynników, które trudniej dostrzec w bardziej skomplikowanych wyrażeniach. Więcej tych reguł wprowadzimy, gdy połączymy kilka operacji.
Pochodna zależna od wartości wejścia
Przyjrzyjmy się teraz funkcji, której szybkość zmian zależy od jej wartości wejściowych. Dzięki temu lokalny charakter pochodnej jest jaśniejszy: opisuje, jak zmienia się wynik w pobliżu określonej wartości wejściowej, gdzie ją oceniamy. Aby to zademonstrować, użyjemy funkcji z jednym wejściem o nazwie :
Możemy zapisać tę pochodną jako , używając tej samej notacji, którą wprowadziliśmy powyżej, zwanej notacją Leibniza. Innym sposobem zapisania tego jest , odczytywane jako „L liczba pierwsza x”, co nazywa się Lagrange notation lub notacją pierwszą. Obydwa oznaczają pochodną względem .
Aby znaleźć pochodną , zróżniczkujemy jej dwa wyrazy oddzielnie, a następnie dodamy wyniki. Wykorzystuje trzy reguły:
| Zasada | Jak go używamy |
|---|---|
| Reguła potęgowa | Zróżniczkuj |
| Stała zasada | Zróżniczkuj człon stały |
| Reguła sumy | Dodaj pochodne obu terminów |
Zastosujmy najpierw regułę potęgową do : pomnóż przez wykładnik, a następnie zmniejsz wykładnik o jeden. Ogólnie rzecz biorąc,
W przypadku daje to . Następnie stosujemy regułę stałej do . Jej pochodna względem wynosi zero, ponieważ nie zmienia się wraz ze zmianami . Na koniec używamy reguły sumy, aby dodać te pochodne. Zatem dla naszej funkcji pochodna wynosi:
Aby zobaczyć, co nam mówi ten wzór, obliczmy wynik funkcji i jej pochodną przy trzech wartościach :
| Wejście | Wyjście | Pochodna |
|---|---|---|
W przeciwieństwie do stałego tempa zmian w , to tempo zależy od bieżącej wartości . Wynik funkcji jest taki sam w i , ale pochodne mają przeciwne znaki. W przypadku dodatnia pochodna oznacza, że wartość wyjściowa wzrasta wraz z nieznacznym wzrostem . W przypadku ujemna pochodna oznacza, że wartość wyjściowa spada, gdy nieznacznie wzrasta.
Poniższy wykres skupia się na kierunku: dwa punkty mają tę samą wysokość, ale zielone strzałki pokazują różne sposoby poruszania się wzdłuż krzywej, aby zmniejszyć wartość wyjściową.
Ten wykres pokazuje nam, że jeśli naszym celem jest zmniejszenie wyniku funkcji, znak pochodnej mówi nam, w którą stronę mamy się poruszać z każdego punktu początkowego. Zielone strzałki pokazują te kierunki, które możemy określić bez prób zwiększania i zmniejszania i ponownego obliczania wyniku funkcji. Obie strzałki prowadzą w stronę , gdzie funkcja ta osiąga minimum, a pochodna wynosi zero, chociaż pochodna zerowa nie zawsze oznacza minimum.
Powszechnym sposobem wizualizacji pochodnej jest nachylenie stycznej do wykresu w wybranym punkcie. W pochodna to , więc styczna ma nachylenie . Zmień poniżej, aby zobaczyć, jak nachylenie fioletowej stycznej zmienia się wzdłuż niebieskiej krzywej.
Tangens pomaga nam również oszacować, jak bardzo zmienia się wynik funkcji w pobliżu. W , gdzie wynik to , a pochodna to , a mała zmiana w powoduje zmianę wyniku w przybliżeniu razy większą:
W powyższym wyrażeniu używamy , ponieważ szacujemy zmianę wartości wyjściowej w małym kroku. W pochodna wynosi dokładnie , ale nachylenie zmienia się w miarę przesuwania się wzdłuż krzywej. Ustawienie na w widżecie daje nachylenie ; mniejszy ruch do dałby . Im mniejszy krok, tym mniej zmienia się nachylenie, więc traktowanie go jako stałego w daje bliższe oszacowanie zmiany wartości wyjściowej.
Szacowanie pochodnej za pomocą różnic skończonych
Szacowanie pochodnych z wartości funkcji na pobliskich wejściach nazywa się różniczkowaniem numerycznym. Jednym ze sposobów osiągnięcia tego są różnice skończone. Pozostaje nam jedynie ocenić funkcję, nie znając jej wewnętrznych operacji ani nie wyprowadzając wzoru na jej pochodną.
Oszacowanie różnicy w przód dzieli zmianę sygnału wyjściowego przez małą, niezerową zmianę sygnału wejściowego :
Obliczmy oszacowanie różnicy skończonej za pomocą . Zaczynając od , krok prowadzi nas do :
Jest to średnia szybkość zmian pomiędzy dwoma wejściami. Geometrycznie jest to nachylenie linii prostej łączącej ich punkty na krzywej, zwane secant. Pochodna podaje nachylenie stycznej w punkcie początkowym.
W poniższym widgecie ustaw na , gdzie fioletowa styczna ma nachylenie . Przy w , pomarańczowa sieczna ma nachylenie . Jest to oszacowanie metodą skończonej różnicy. Gdy zmniejszamy , pomarańczowe nachylenie zbliża się do , dokładnej pochodnej.
Nazywamy to średnią, ponieważ dzielimy całkowitą zmianę sygnału wyjściowego przez całkowitą zmianę sygnału wejściowego w danym przedziale. Przybliżenie różnic skończonych wykorzystuje tylko dwie wartości punktów końcowych, więc nie mówi nam, jak funkcja zachowuje się między nimi. Jego wartość wyjściowa może zmieniać się wolniej w jednej części interwału i szybciej w innej.
Ponieważ znamy wzór, możemy wykreślić krzywą pomiędzy punktami końcowymi i zobaczyć to rozróżnienie. Poniższy wykres wykorzystuje szerszy przedział, od do , aby różnica była widoczna. Linia przerywana łącząca punkty końcowe ma średnie nachylenie; zielona krzywa pokazuje, jak faktycznie zmienia się wartość wyjściowa. Zacieniony trójkąt pokazuje całkowitą zmianę sygnału wejściowego i zmianę sygnału wyjściowego , zatem średnie nachylenie wynosi .
Możemy porównać średnią stopę w całym przedziale ze stopami w jego dwóch połówkach. Podział w , gdzie wyjście to , daje:
| Interwał | Zmiana wejścia | Zmiana wyjścia | Średnia stawka |
|---|---|---|---|
| Cały interwał | |||
| Pierwsza połowa | |||
| Druga połowa |
Definicja granicy precyzuje ideę tempa zmian w jednym punkcie: jest to wartość, do której zbliża się średnia szybkość zmian, gdy przedział zmniejsza się do zera:
Pozwoliliśmy zbliżyć się do zera, zamiast zastępować , co dałoby . Biorąc tę granicę, możemy wyprowadzić regułę . Dla naszej funkcji najpierw uprośćmy stosunek, zachowując niezerowy:
To sprawia, że rozróżnienie jest wyraźne: obliczenie różnicy skończonej w wybranym kroku daje . Przyjęcie granicy, gdy zbliża się do zera, usuwa dodatkowe , dając nam regułę pochodną:
To wyjaśnia regułę, którą zastosowaliśmy wcześniej symbolicznie. Kiedy już mamy formułę , możemy ją obliczyć bezpośrednio na dowolnym wejściu bez oceniania funkcji na pobliskim wejściu.
Różnica w przód porównuje z . Próbkowanie za daje oszacowanie różnicy wstecznej, podczas gdy próbkowanie jednakowe po obu stronach daje oszacowanie różnicy centralnej. Są to inne typy aproksymacja różnic skończonych. Dla tej samej funkcji użyjemy teraz większego kroku :
| Metoda | Formuła | Oszacowanie na , |
|---|---|---|
| Naprzód | ||
| Do tyłu | ||
| Centralny |
W arytmetyce dokładnej, gdy zmniejszamy krok , oszacowania różnicy w przód, w tył i centralne zbiegają się do dokładnej pochodnej — dla naszej funkcji w wartość ta wynosi . Różnica środkowa jest dokładna dla tej funkcji kwadratowej w dokładnej arytmetyce, ale generalnie pozostaje przybliżeniem.
Oprogramowanie może oszacować pochodną przy użyciu różnic skończonych: ocenić funkcję na dwóch pobliskich wejściach, odjąć wyniki i podzielić przez różnicę wejściową. Wybór tej różnicy wymaga ostrożności. Skończony krok zazwyczaj wprowadza błąd obcięcia, ponieważ używamy przedziału do oszacowania szybkości w punkcie. Zmniejszenie kroku zmniejsza ten błąd, ale niezwykle mały krok stwarza kolejny problem: komputery przechowują liczby z ograniczoną precyzją. Odejmowanie prawie równych wartości funkcji może pozostawić różnicę zdominowaną przez błąd zaokrąglenia, który potęguje dzielenie przez maleńki .
Metody te są przydatne do sprawdzania pochodnych, ale ich dodatkowe oceny funkcji czynią je kosztownymi, gdy są stosowane oddzielnie do milionów parametrów.
Wiele wejść i pochodne cząstkowe
Do tej pory używaliśmy funkcji i z tylko jednym parametrem . Przyjrzyjmy się teraz i , z dwoma parametrami i , które możemy zmieniać oddzielnie. Aby zrozumieć wpływ każdego parametru na , zmieniamy jeden, utrzymując drugi na stałym poziomie. Zmiana obu na raz spowodowałaby zmieszanie ich efektów.
Wcześniej napisaliśmy , aby opisać, jak zmienia się wyjście przy pojedynczym wejściu . W przypadku naszych funkcji z dwoma wejściami musimy określić, które dane wejściowe będziemy zmieniać. Aby zmierzyć, jak zmienia się , gdy zmieniamy , trzymając na stałe, używamy pochodnej cząstkowej, zapisanej jako . Czytamy to jako 'pochodną cząstkową względem '. Kolejność oznacza to samo co poprzednio; symbol wskazuje, że zmieniamy jedno wejście, utrzymując pozostałe na stałym poziomie.
Geometrycznie oba wejścia wyznaczają punkt na powierzchni . Pochodna cząstkowa mierzy nachylenie w tym punkcie w kierunku jednego sygnału wejściowego, podczas gdy drugie wejście pozostaje stałe. Dlatego potrzebujemy obu wartości wejściowych, aby określić, gdzie mierzymy nachylenie, nawet jeśli badamy zmiany tylko w jednym kierunku.
Przy obliczaniu pochodnej cząstkowej stosujemy zasady różniczkowania, a pozostałe dane wejściowe traktujemy jako stałe.Zacznijmy od dodatku . Trzymanie naprawiono, zwiększanie o zwiększa o , więc . Odwrócenie ról daje .
W przypadku mnożenia współczynnik zależy od wartości wejściowych, które utrzymujemy na stałym poziomie. Poniższa tabela pokazuje jak zmienia się w miarę zwiększania przy jednoczesnym utrzymaniu na stałej wartości :
Widzimy, że za każdym razem, gdy wzrasta o , wzrasta o . W przypadku utrzymywanego na poziomie , tempo zmian w odniesieniu do wynosi zatem :
Jak widać, tempo zmian jest równe stałej wartości . Wykorzystuje tę samą regułę stałej-wielokrotności, którą zastosowaliśmy w przypadku . Mnożnik w wynosił . W mnożnikiem jest , który utrzymujemy na stałym poziomie podczas różniczkowania . Zatem jeśli utrzymujemy jedną zmienną na stałym poziomie, nadal uczestniczy ona w obliczeniach i wpływa na to, jak dane wyjściowe reagują na zmiany drugiej zmiennej.
Różniczkowanie względem działa w ten sam sposób: utrzymujemy jako stałą i stosujemy regułę stałej-wielokrotności, dając . Obydwa wejścia pełnią role symetryczne, ale ich działanie zależy od bieżących wartości i . W przypadku i wynikiem jest , a pochodne cząstkowe to odpowiednio i .
Możemy teraz podsumować lokalne zasady dotyczące pochodnych dla obu operacji:
| Operacja | ||
|---|---|---|
Ponadto zmiana dowolnego wejścia powoduje zmianę sygnału wyjściowego o tę samą wielkość. W przypadku mnożenia każda pochodna cząstkowa jest równa drugiej wartości wejściowej. Reguły te nazywane są lokalnymi, ponieważ opisują, w jaki sposób dane wyjściowe pojedynczej operacji odpowiadają na dane wejściowe. Później połączymy je, aby śledzić zmiany poprzez kilka operacji.
Pochodne cząstkowe i opisują czułość, a nie części wyniku . W przypadku równie małych zmian zmiana samego daje razy większy efekt zmiany samego . Aby oszacować efekt zmiany obu danych wejściowych, potrzebujemy zarówno pochodnych, jak i proponowanych zmian wejściowych.
Aby to zrobić, zbieramy pochodne cząstkowe w wektor zwany gradientem. Oznaczamy go symbolem , więc dla funkcji przy i piszemy . Wektor ten ma również znaczenie geometryczne w płaszczyźnie wejść: spośród przesunięć o tej samej małej długości euklidesowej wskazuje kierunek największego wzrostu wyniku w przybliżeniu pierwszego rzędu. Wektor przeciwny wskazuje kierunek najszybszego lokalnego spadku.
Poniższy wykres przedstawia te wskazówki dla . Możesz zobaczyć, jak fioletowa strzałka prowadzi w górę od , gdzie wyjście to , podczas gdy zielona strzałka prowadzi w dół. Śledzą ścieżki powierzchni, gdy przesuwamy dane wejściowe w kierunku gradientu lub odwrotnie. Wykres skaluje wysokość oddzielnie; gradient określa kierunek w płaszczyźnie wejściowej.
Znając pochodne cząstkowe, możemy skorzystać z a przybliżenie Taylora pierwszego rzędu, zwany także A przybliżenie liniowe. Pozwala nam to oszacować łączny wpływ małych zmian wejściowych na wynik: mnożymy każdą pochodną cząstkową przez odpowiednią zmianę wejściową i dodajemy wyniki.
Jest to iloczyn skalarny gradientu i wektora zmiany sygnału wejściowego. Dla naszej funkcji i daje to:
Na przykład zwiększenie obu wejść o daje:
Zwiększanie o przyczynia się do do przewidywanej zmiany, podczas gdy zwiększanie o przyczynia się do . Każdy udział jest pochodną cząstkową pomnożoną przez odpowiednią zmianę wejściową. Pochodne opisują wrażliwość; wybrane zmiany określają, jaka część tej wrażliwości wchodzi do prognozy.
Ocena funkcji na nowych wejściach daje rzeczywistą zmianę . Pozostała część to składnik interakcji , który jest pomijany w przybliżeniu liniowym. Terminy i opisują zatem wkład w przewidywaną zmianę, a nie udział pierwotnej wartości wyjściowej . To samo rozróżnienie dotyczy propagacji wstecznej: oblicza ona wrażliwość straty na każdą wagę. Mnożąc te pochodne przez proponowane zmiany wagi, szacuje się ich udział w zmianie straty.
Łączenie reguł różniczkowania dla bardziej złożonej funkcji
W poprzedniej sekcji obliczyliśmy pochodne cząstkowe dodawania i mnożenia. Teraz połączymy reguły różniczkowania w wyrażeniu zawierającym kilka terminów i potęg:
Porównamy dwa równoważne podejścia do obliczania pochodnych cząstkowych w wybranym punkcie: najpierw różniczkuj, a następnie podstawiaj wartości lub najpierw podstawiaj wartość drugiego wejścia, różniczkuj wynikową funkcję jednowyjściową, a następnie wyznaczaj jej pochodną. Obydwa podejścia dają wzory pochodne. Wykreślenie funkcji jednowejściowych w postaci przekrojów powierzchni pokaże, dlaczego każda pochodna cząstkowa jest również zwykłym nachyleniem stycznym.
Najpierw różniczkujemy, potem podstawiamy wartości
Zachowamy i jako symbole, znajdując pochodną cząstkową dla każdego wejścia, traktując drugie jako stałe. Następnie wycenimy obie pochodne w .
Najpierw różniczkujemy w odniesieniu do , utrzymując na stałe i różnicując każdy człon. Współczynnik pełni rolę stałego mnożnika :
Przytrzymanie na stałe nie usuwa go z obliczeń: jego wartość wyznacza mnożnik . Różnicujemy stosując regułę potęgową.
Następnie różniczkujemy względem , odwracając role i utrzymując na stałe. Teraz jest stałym mnożnikiem , a cały wyraz jest stały:
Termin wnosi zero, ponieważ zmiana podczas trzymania na stałe nie może zmienić jego wartości.
Daje nam to następujące pochodne cząstkowe na wybranych przez nas danych wejściowych:
| Dane wejściowe zmieniamy | Wejście wstrzymane | Częściowa pochodna w | Przewidywana zmiana wartości wyjściowej |
|---|---|---|---|
Najpierw ustalamy jedno wejście, potem różniczkujemy przekrój
Przyjrzyjmy się teraz drugiemu podejściu: najpierw ustalamy jedno wejście, a następnie różniczkujemy funkcję opisującą przekrój. Podstawiamy wartość jednego wejścia do , otrzymując zwykłą funkcję pozostałego wejścia.
Najpierw ustawiamy i pozostawiamy dowolność:
To jest przekrój oryginalnej funkcji, a nie pochodna: podstawiliśmy wartość tylko dla jednego wejścia.
W przypadku przekroju z nie mamy już jako zmiennej w wyrażeniu, ponieważ podstawienie zamieniło w stały mnożnik . Jednakże efekt jest nadal przenoszony przez mnożnik . Teraz, gdy zmienną pozostaje tylko , bierzemy pochodną zwykłą względem :
Następnie wracając do pierwotnej funkcji, ustawiamy i pozostawiamy dowolność:
A dla przekroju z różniczkujemy w odniesieniu do :
Obydwa przekroje przechodzą przez ten sam wybrany punkt, gdzie . Zróżniczkowanie każdego przekroju daje te same pochodne cząstkowe, które obliczyliśmy powyżej:
Oba podejścia utrzymują drugie wejście na stałym poziomie, więc możemy podstawić jego wartość przed różniczkowaniem albo zachować je jako symbol i podstawić wartość później. Pierwsze podejście daje ogólne wzory na pochodne, i , które można obliczyć dla dowolnej pary wejść. Drugie daje wzory dla konkretnych przekrojów: , gdy , oraz , gdy . Na przykład podstawienie do daje , tak samo jak zróżniczkowanie funkcji przekroju . Każdy przekrój otrzymujemy przez osobne podstawienie do pierwotnej funkcji.
Te pochodne mierzą raczej wrażliwość niż udziały w bieżącej wartości wyjściowej. Tak jak poprzednio, zbieramy pochodne cząstkowe do wektora gradientu. W przypadku daje to . Proponowane zmiany wejściowe tworzą wektor . Ich iloczyn skalarny szacuje łączną zmianę wartości wyjściowej:
W obie współrzędne określają punkt, w którym mierzymy nachylenia. Poruszanie się w kierunku przy trzymaniu daje styczne nachylenie ; poruszanie się w kierunku przy trzymaniu daje nachylenie . Są to dwa kierunki przechodzące przez ten sam punkt na powierzchni. Każda stopa jest pochodną cząstkową pierwotnej funkcji dwóch danych wejściowych i pochodną zwyczajną odpowiadającego jej przekroju o jednym wejściu. Gradient łączy te szybkości w kierunku największego lokalnego wzrostu w płaszczyźnie wejściowej.
Poniższy widżet pokazuje to, co obliczyliśmy powyżej: jak zmienia się wynik w każdym kierunku wejściowym, mierzony za pomocą pochodnych cząstkowych, oraz jak te pochodne łączą się w kierunku gradientu. Fioletowe przekroje pokazują, że każde wejście zmienia się osobno, ich zielone styczne pokazują pochodne cząstkowe, a pomarańczowa strzałka podąża ścieżką powierzchniową w kierunku gradientu.
∂f/∂x = 31
∂f/∂y = 12
Począwszy od , przesunięcie suwaka wpływa w różny sposób na oba wykresy. Po lewej stronie to zmienne wejście, więc wybrany punkt porusza się wzdłuż tej samej krzywej , podczas gdy pozostaje nieruchomy. Po prawej stronie to stałe wejście definiujące przekrój, więc zmiana go z na powoduje zmianę formuły z na . Innymi słowy, przesuwanie przesuwa punkt wzdłuż lewego przekroju i zmienia, który przekrój jest wyświetlany po prawej stronie. Przenoszenie powoduje odwrotną sytuację.
Śledzenie pochodnych przez połączone operacje
Wcześniej zróżniczkowaliśmy dwa terminy oddzielnie i dodaliśmy ich pochodne, ponieważ oryginalne wyrażenie dodało te terminy. Mówiąc bardziej ogólnie, możemy postrzegać każde obliczenia pośrednie jako funkcję pierwotnych danych wejściowych. Sposób, w jaki łączą się ich wyniki, określa sposób łączenia ich pochodnych.
Poniższa tabela przedstawia zasady dodawania, mnożenia i zagnieżdżania. Będziemy używać i jako ogólnych nazw dwóch różniczkowalnych funkcji :
| Zasada | Jak funkcje łączą się | Pochodna w odniesieniu do |
|---|---|---|
| Reguła sumy | ||
| Zasada iloczynu | ||
| Reguła łańcucha |
Reguła iloczynu uogólnia używaną wcześniej regułę stałej-wielokrotności na przypadki, w których zmieniają się oba czynniki. Mnożenie dwóch funkcji nie oznacza mnożenia ich pochodnych. Aby różniczkować poszczególne funkcje, możemy skorzystać z reguły takiej jak reguła potęgowa lub rozbić funkcje na prostsze operacje, których pochodne już znamy. Reguła łańcuchowa łączy ich lokalne pochodne, aby dowiedzieć się, jak oryginalne dane wejściowe wpływają na końcowe wyjście, co czyni je centralnymi dla propagacji wstecznej.
W poprzedniej sekcji zróżniczkowaliśmy terminy, traktując inne dane wejściowe jako stałe. Przyjrzymy się teraz bliżej tym terminom: każdą operację arytmetyczną można postrzegać jako funkcję jej danych wejściowych.
Najpierw podzielmy naszą funkcję na poszczególne operacje z nazwanymi wynikami pośrednimi:
Na przykład podnoszenie do kwadratu odwzorowuje do i odwzorowuje mnożenie i do . Wynik kwadratu staje się wejściem do mnożenia. Kiedy dane wyjściowe jednej funkcji stają się danymi wejściowymi innej, funkcje te tworzą relację zwaną kompozycją funkcji, pokazaną jako w powyższej tabeli. Operacja ma swoje własne bezpośrednie dane wejściowe; łączenie operacji powoduje, że ich wynik zależy również od wcześniejszych danych wejściowych.
Podział następuje zgodnie z zależnościami wyrażenia: musi być dostępne przed , a zarówno , jak i muszą być dostępne przed . Ponieważ nie zależy od ani , możemy obliczyć to przed lub po tej gałęzi. Podział nie jest wyjątkowy: możemy wyrazić podnoszenie do kwadratu i sześcian jako mnożenie. Tutaj traktujemy je jako pojedyncze operacje i stosujemy regułę potęgową dla ich lokalnych pochodnych.
Wykorzystamy teraz regułę łańcuchową do obliczenia pochodnych poprzez pojedyncze operacje, mnożąc lokalne pochodne wzdłuż każdej ścieżki i dodając udziały z różnych ścieżek. Najpierw będziemy podążać jedną ścieżką, a następnie sprawdzimy dane wejściowe, które docierają do wyjścia dwiema ścieżkami.
Reguła łańcuchowa: jedna ścieżka
Zaczniemy od różniczkowania w odniesieniu do , utrzymując na stałe. Podział pokazuje, dlaczego jest przydatnym punktem wyjścia: zmiana ma wpływ na , następnie na , podczas gdy pozostaje stała. Następnie sprawdzimy , który wpływa zarówno na , jak i . Na razie istnieje tylko jedna ścieżka od do końcowego wyniku:
Aby zobaczyć różnicę między pochodną lokalną a regułą łańcuchową, skup się tylko na dwóch węzłach operacji: kwadracie i mnożeniu , połączonych przez .
Patrząc osobno, mnożenie wymaga i jako danych wejściowych. Jego lokalne pochodne cząstkowe to:
Aby wyprowadzić każdą pochodną lokalną, potrzebujemy jedynie wyrażenia operacji i danych wejściowych, ze względu na które różniczkujemy. W przypadku traktowanie jako stałej daje niezależnie od tego, jak obliczono i gdzie zastosowano . W tym kroku nie potrzebujemy reszty wykresu. Kwadrat dostarczany jest osobno .
Zwróć uwagę, że reguła łańcuchowa uwzględnia pochodną węzła , oddzielnie, mimo że węzeł wykorzystuje już wartość . Lokalna pochodna mówi nam jedynie, jak reaguje na zmianę w . Nie obejmuje zmian w stosunku do . Stawka ta wynosi , więc potrzebujemy obu czynników:
Po ustaleniu mnożenie jest funkcją zewnętrzną, a podnoszenie do kwadratu funkcją wewnętrzną. Sama pochodna zewnętrzna nie może nam powiedzieć, jak zmienia się funkcja wewnętrzna: mnożenie przyjmuje wartość , nie wiedząc, czy pochodzi ona z , , czy z innego obliczenia. W zarówno , jak i wytwarzają , ale ich pochodnymi są odpowiednio i . Ta sama wartość nie oznacza zatem takiej samej reakcji na zmianę w . To rozróżnienie ma zastosowanie zawsze, gdy różniczkujemy kompozycję.
Jeśli podstawimy do , pełne wyrażenie będzie zawierało tę wewnętrzną zależność. Zróżniczkowanie go bezpośrednio daje , uwzględniając oba współczynniki. Wykres wyraźnie przedstawia te dwa czynniki.
Poniższa tabela przedstawia jedynie skutki zmiany , przy niezmienionym . Koncentruje się na dwóch operacjach i , zatrzymując się na w celu obliczenia :
| Krok | Forma symboliczna |
|---|---|
| Zdefiniuj operacje | , następnie |
| Wyprowadź wzory na pochodne lokalne | , |
| Połącz formuły, korzystając z reguły łańcuchowej |
Połączenia mają znaczenie, gdy łączymy pochodne lokalne: ścieżka każe nam pomnożyć przez , aby znaleźć . Operacja dostarcza reguły lokalnej; wykres mówi nam, jak połączyć to z innymi regułami.
Teraz przejdź do ostatniego dodania. Oryginalna funkcja wykorzystuje i , ale każda operacja ma własne dane wejściowe. W przypadku ostatniego dodatku te wejścia to i . Jego lokalna pochodna pyta, jak zmienia się wyjście dodatku, gdy zmienia się , podczas gdy pozostaje stałe. Jest to równe , ponieważ dodanie małej ilości do powoduje dodanie tej samej ilości do . Następnie reguła łańcuchowa łączy te lokalne odpowiedzi z pierwotnym wejściem .
W poniższej tabeli wymieniono lokalne pochodne na pełnej ścieżce:
| Operacja | Lokalna pochodna |
|---|---|
| Kwadrat daje | |
| Pomnóż przez stałą wartość , aby otrzymać | |
| Dodaj stałą wartość , aby otrzymać |
Aby otrzymać pochodną wyniku końcowego względem , , możemy teraz zastosować regułę łańcuchową, mnożąc pochodne lokalne przez :
Odpowiada to wzorowi, który otrzymaliśmy poprzez bezpośrednie różniczkowanie . Tutaj równa się , ponieważ ostatnia operacja mnoży stawkę przez .
Poniższy widget renderuje wykres połączonych obliczeń. Zaczyna się od i , wejść, od których zależy funkcja. Wyrażenia wewnątrz węzłów, takie jak i , definiują operacje. Strzałki ciągłe wskazują wynik operacji na operację, która go wykorzystuje. Zatem oznacza, że wykorzystuje wynik , dzięki czemu ich kompozycja jest widoczna. Mnożenie wykorzystuje , a nie jego pochodną ; etykiety pochodnych i przerywane strzałki prowadzące do pola reguł łańcuchowych pokazują, w jaki sposób różniczkujemy obliczenia. Podążaj pomarańczową ścieżką i pomnóż lokalne pochodne, aby otrzymać .
Scroll the graph sideways to see the chain-rule box.
Kiedy teraz weźmiemy wzór na pochodną i podstawimy i , otrzymamy , pasujący do obliczonej wcześniej pochodnej cząstkowej. Możemy ocenić ten wzór na dowolnej parze wejściowej bez uprzedniego obliczania wartości funkcji pośrednich.
W przypadku i niewielka zmiana w powoduje w przybliżeniu zmianę w . Skale mnożenia zmieniają się o , dając w przybliżeniu zmianę w . Dodanie niezmienionego pozostawia tę zmianę nietkniętą.
Wiele ścieżek: sumowanie wkładów do pochodnej
Prześledziliśmy, jak wpływa na sygnał wyjściowy jedną ścieżką. Znajdźmy teraz pochodną cząstkową względem , przy stałym . Używamy tych samych wyników pośrednich:
Wartość pozostaje stała, ale zarówno , jak i zmieniają się wraz z . Daje nam to dwie ścieżki od do końcowego wyniku:
Obliczyliśmy już różniczkując względem , utrzymując stałą . Reguła łańcuchowa daje tę samą odpowiedź, używając lokalnych pochodnych wzdłuż obu ścieżek:
| Operacja | Lokalna pochodna |
|---|---|
| Pomnóż przez stałą wartość , aby otrzymać | |
| Kostka do uzyskania | |
| Dodaj i , aby otrzymać , biorąc pod uwagę wpływ | |
| Dodaj i , aby otrzymać , biorąc pod uwagę wpływ |
Każda lokalna pochodna dodania utrzymuje swoje inne dane wejściowe na stałym poziomie. Oba wejścia zmieniają się, gdy zmienia się , więc potrzebujemy obu wkładów.
Stosujemy teraz regułę łańcuchową wielu zmiennych: mnożymy lokalne pochodne wzdłuż każdej ścieżki, a następnie dodajemy oba składowe, aby otrzymać pochodną końcowego wyniku względem :
Poniższy diagram przedstawia dwie ścieżki od do końcowego wyjścia: i . Obydwa śledzą wpływ zmiany , przy czym pozostaje stały. Węzły i są szare, ponieważ ta gałąź pozostaje niezmieniona; nadal dostarcza wartość używaną przy mnożeniu. Pomarańczowa ścieżka wnosi , a fioletowa ścieżka wnosi . Aktywne pola operacji pokazują ich lokalne pochodne. W powyższym ramce mnoży się pochodne wzdłuż każdej ścieżki, a następnie dodaje udziały, aby otrzymać .
Scroll the graph sideways to follow the paths.
Kiedy teraz weźmiemy wzór na pochodną i podstawimy i , otrzymamy . Odpowiada to pochodnej cząstkowej, którą obliczyliśmy wcześniej, bez obliczania najpierw wartości funkcji pośrednich.
Obliczanie pochodnych: różniczkowanie symboliczne i automatyczne
Powyższe przykłady łączą lokalne reguły różniczkowania, aby obliczyć pochodną większej funkcji. Możemy pójść dalej: podnoszenie do kwadratu i do sześcianu też da się zbudować z mnożenia, więc ich pochodne mogą wynikać z reguły mnożenia.
Pochodna kwadratu z samego mnożenia
Załóżmy, że nasz silnik obsługuje mnożenie, ale nie ma operacji potęgowania. Nadal możemy obliczyć jako . Dla mnożenia lokalne pochodne wynoszą:
Obie pozycje wejściowe odnoszą się tutaj do tej samej zmiennej: i . Zmiana wpływa na wynik przez obie pozycje, więc sumujemy ich wkłady:
Pochodna kwadratu wynika z mnożenia i reguły łańcuchowej; nie potrzebowaliśmy osobnej reguły dla kwadratu. W przebiegu wstecznym zaczynamy od gradientu równego . Każda pozycja wejściowa dodaje wartość drugiego argumentu do gradientu . Przy otrzymujemy dwa wkłady: . Choć istnieje tylko jeden węzeł , trzeba uwzględnić oba jego użycia.
Ta sama idea działa dla sześcianu. Zapiszmy , a następnie . Łącząc pochodne, otrzymujemy:
Silnik może więc różniczkować potęgi o dodatnich wykładnikach całkowitych, zapisane jako kolejne mnożenia, bez implementowania reguły potęgowej. Inne funkcje wymagają odpowiednich dodatkowych operacji podstawowych: samo mnożenie nie pozwala na przykład wyrazić ani dowolnych potęg ułamkowych. Ogólna zasada polega na tym, aby dostarczyć reguły różniczkowania operacji podstawowych, a następnie łączyć je w celu różniczkowania zbudowanych z nich funkcji.
Te same reguły mogą tworzyć wyrażenia lub obliczać liczby
W przykładzie kwadratu zachowaliśmy jako symbol, zbudowaliśmy wyrażenie i uprościliśmy je do . Takie tworzenie wyrażeń na pochodne to różniczkowanie symboliczne. Program może automatycznie budować te wyrażenia, korzystając z tych samych lokalnych reguł i zależności.
Micrograd stosuje te reguły do wartości liczbowych. Przy pierwotne mnożenie daje , a jego reguła wsteczna sumuje w gradiencie . To różniczkowanie automatyczne: wynik jest matematycznie równoważny obliczeniu przy , choć nie konstruujemy tego symbolicznego wzoru. Propagacja wsteczna wykorzystuje automatyczne różniczkowanie w trybie odwrotnym do obliczania gradientów straty potrzebnych do uczenia.
Obie metody łączą te same reguły rachunku różniczkowego. W tym porównaniu zmienia się to, czy budujemy wyrażenia na pochodne, czy wykonujemy obliczenia pochodnych na liczbach:
| Metoda | Na czym działa | Co daje |
|---|---|---|
| Różniczkowanie symboliczne | Wyrażenia symboliczne, w tym nazwane wyrażenia pośrednie | Wzory na pochodne, które można obliczyć dla wybranych wartości wejściowych |
| Różniczkowanie automatyczne | Operacje programu i ich wartości dla wybranych wejść | Wartości liczbowe pochodnych uzyskane przez połączenie pochodnych lokalnych |
Od wzorów na pochodne do reguł wykonywanych przez program
Na wcześniejszym jednościeżkowym wykresie symbolicznym napisaliśmy obok operacji . Wzór ten mówi nam również, jak obliczyć pochodną za pomocą arytmetyki: weź wartość wejściową i pomnóż ją przez . W przypadku wynikiem jest . To jest numeryczna pochodna lokalna: lokalna pochodna wyznaczana przy określonych wartościach wejściowych.
Autodiff nie wyprowadza reguł różniczkowania operacji podstawowych z samych wartości ani nie odkrywa ich symbolicznie podczas wykonywania programu. Twórca silnika dostarcza regułę dla każdej operacji podstawowej, zapisując znany wzór na pochodną jako kod. Jeśli wybierzemy podnoszenie do kwadratu jako operację podstawową, staje się 2 * input_value. Wzór opisuje regułę matematycznie, a kod wykonuje ją na liczbach. Dla wejścia 2 otrzymujemy 4. Jeśli zamiast tego zapiszemy kwadrat jako y * y, dwa wkłady z mnożenia dadzą ten sam wynik: 2 + 2 = 4.
Silnik korzysta z grafu i reguły łańcuchowej, aby różniczkować kombinacje tych operacji. Każdą regułę lokalną implementujemy raz i wykorzystujemy ponownie przy każdym wystąpieniu danej operacji:
| Działanie programu | Znana reguła lokalnej pochodnej | Obliczenia numeryczne |
|---|---|---|
u = y ** 2 | 2 * y.data | |
v = x * u | , | u.data, x.data |
w = x ** 3 | 3 * x.data ** 2 | |
f = v + w | , | 1, 1 |
Tutaj .data przechowuje wartość liczbową węzła. Pochodną mnożenia względem oblicza się poprzez odczyt wartości drugiego argumentu, . Aby zastosować tę regułę lokalną, silnik nie musi wiedzieć, w jaki sposób obliczono ten operand.
Silnik rejestruje również, które dane wejściowe dały dany wynik, zachowując zależności pokazane na naszym wykresie. Zwykła arytmetyka oblicza wartości; autodiff dodaje śledzenie zależności i reguły pochodne. Może także śledzić operacje wykonywane w pętlach i gałęziach warunkowych, bez konieczności przepisywania całego obliczenia w postaci pojedynczej formuły.
W grafie używanym w naszym przykładzie wybieramy osobną operację podstawową potęgowania, dzięki czemu każdy kwadrat i sześcian pozostaje jedną operacją. Możemy je wtedy zapisać jako y.pow(2) i x.pow(3). To wybór implementacyjny; dla tych potęg sprawdziłoby się również powtarzane mnożenie. Metoda pow(exponent) stosuje ogólną regułę potęgową:
W przypadku użytych tutaj dodatnich wykładników całkowitych obliczenie wygląda następująco:
pow(exponent) {
return new Value(
this.data ** exponent, // function value
[[this, exponent * this.data ** (exponent - 1)]] // input and local derivative
);
}Tutaj this jest węzłem wejściowym. Operacja oblicza wartość wyjściową i rejestruje połączenie z tym wejściem wraz z numeryczną pochodną lokalną. Przejście wsteczne wykorzysta zapisaną pochodną do propagacji wkładu gradientu na wejście.
Wywołanie y.pow(2) przekazuje wykładnik , więc lokalna pochodna wynosi . Wywołanie x.pow(3) daje . Silnik pobiera wykładnik z argumentu operacji, a programista dostarcza ogólną regułę różniczkowania. Silnik nie odkrywa reguły potęgowej podczas działania. Wyspecjalizowana metoda square() może używać tego samego obliczenia z wykładnikiem ustalonym na .
Wykres przedstawia połączenia; zaimplementowane reguły dostarczają lokalnych wartości pochodnych. Reguła łańcucha mówi następnie silnikowi, jak je połączyć.
Możemy teraz ponownie odwiedzić tabelę z symbolicznego przykładu, ponownie kierując się tylko z trzymanym na stałe w celu obliczenia . Autodiff wykonuje te same operacje na wybranych wejściach, więc oblicza także ich numeryczne wartości wyjściowe. Nadal potrzebuje lokalnych pochodnych, ale ocenia reguły już wdrożone dla każdej operacji, zamiast wyprowadzać nowe formuły podczas wykonywania:
| Krok | W , , przy trzymanym nieruchomo |
|---|---|
| Użyj tych samych operacji | , następnie |
| Oblicz wartości operacji | , następnie |
| Oceń lokalne zasady dotyczące pochodnych | , |
| Połącz pochodne liczbowe, korzystając z reguły łańcuchowej |
Kontynuacja ostatniego dodawania daje . W celu wyjaśnienia tabela ta wyodrębnia efekt jednego wejścia. Nasze symboliczne obliczenia połączyły we wzorze . Funkcja Autodiff ocenia poszczególne reguły na bieżących wejściach i zamiast tego łączy . Wykorzystuje znane reguły pochodne jako instrukcje numeryczne, bez konstruowania symbolicznego wzoru na pochodną dla całej funkcji.
Poniższy widget wykorzystuje ten sam wykres, co przykład z jedną ścieżką. Przełącz pomiędzy i w celu porównania łączenia wyrażeń pochodnych z łączeniem ich wartości liczbowych w i .
Scroll the graph sideways to see the chain-rule box.
W trybie symbolicznym ciągłe strzałki pokazują, w jaki sposób funkcje się łączą: każdy wynik pośredni staje się wejściem do innej operacji. W trybie automatycznego różniczkowania te same strzałki opisują również przepływ wartości numerycznych do przodu: podniesienie do kwadratu daje , a mnożenie z daje . Zależności pozostają takie same; teraz oceniamy operacje na określonych wejściach. Ten widżet porównania nie animuje tego przepływu. Poniższy przewodnik krok po kroku pokaże te wartości w przód i propagację wsteczną wkładów pochodnych.
Obliczanie wartości operacyjnych różni się od obliczania lokalnych pochodnych. Każda reguła pochodnej używa dowolnych wartości, których potrzebuje: na tej ścieżce reguła kwadratu potrzebuje , a pochodna mnożenia względem potrzebuje . Wiersze w tabeli obliczeń numerycznych opisują różne zadania, a nie oddzielne przebiegi: implementacja może ocenić lokalne reguły pochodne podczas przebiegu w przód lub w tył. W tym przewodniku oceniamy je i zapisujemy podczas podania do przodu.
Widget porównawczy wyświetla obliczone pochodne; w późniejszym instruktażu zostaną również wyświetlone wartości przekazywane dalej. Tak się składa, że wynik kwadratu i jego lokalna pochodna są równe w , ale są to różne wielkości: w będą to i .
Wyznaczanie wzoru na pochodną złożoną daje taki sam wynik, jak obliczanie najpierw pochodnych lokalnych, a następnie łączenie ich za pomocą reguły łańcuchowej. W przypadku obliczenie daje , podobnie jak mnożenie . Operacje i ich zależności pozostają takie same; zmienia się to, czy łączymy wyrażenia pochodne, czy wartości liczbowe.
Unikanie rozszerzonych formuł ma znaczenie, ponieważ wielokrotne podstawienie wyrażeń pośrednich może spowodować, że formuła pochodna będzie znacznie większa niż oryginalne wyrażenie — jest to problem zwany Zwiększenie wyrażenia. Rozwinięta formuła może zawierać wiele kopii tego samego podwyrażenia, zajmując więcej miejsca i powtarzając pracę, jeśli każda kopia jest oceniana osobno. Zachowanie nazwanych wyników pośrednich pozwala nam ponownie wykorzystać te obliczenia. Symboliczne zróżniczkowanie może również zachować i udostępnić te wyrażenia, więc wzrost ekspresji nie jest nieuniknionym ograniczeniem metody.
Tryb prosty i odwrotny
Widżet pokazuje, które pochodne lokalne łączymy, ale nie pokazuje kolejności, w jakiej je propagujemy. Połączenia przerywane łączą czynniki w iloczyn. Tryb do przodu i tryb do tyłu autodiff mogą używać tego samego wykresu obliczeń i oba oceniają funkcję od wejść do wyjść. Różnią się sposobem propagacji pochodnych: tryb do przodu przenosi je z wejść do wyjść, podczas gdy tryb odwrócony przenosi wkłady pochodne z wyjścia z powrotem do wejść.
Zaczynając od jednego wybranego wejścia, tryb forward może obliczyć, jak wszystkie wyjścia reagują na to wejście w jednym przebiegu różniczkującym. Zaczynając od jednego wybranego wyjścia, tryb odwrotny może obliczyć, jak to wyjście reaguje na wszystkie wejścia w jednym przejściu wstecz. Skoncentrujemy się na trybie odwrotnym, podejściu stosowanym w przypadku propagacji wstecznej. Pełne przejście wstecz obliczy zarówno , jak i , gromadząc wkład z obu ścieżek do .
Przejście pochodnej w trybie forward propaguje pochodne wzdłuż wartości funkcji. Zamiast tego przejście do przodu naszego obliczenia w trybie odwrotnym ocenia funkcję i rejestruje, czego potrzebuje późniejszy przebieg wstecz; pochodne wyniku końcowego są kumulowane podczas tego przejścia wstecz. Prześledzimy te obliczenia w dwóch przebiegach:
- Przepust do przodu ocenia funkcję. W tym przewodniku każda operacja oblicza swoją wartość i zapisuje połączenia wejściowe oraz numeryczne pochodne lokalne.
- Przejście wsteczne rozpoczyna się od pochodnej wyniku względem samego siebie, , i wykorzystuje zapisane lokalne pochodne do propagowania wkładów liczbowych wstecz przy użyciu reguły łańcuchowej. Wkłady do tych samych danych wejściowych kumulują się.
Poniższa tabela pokazuje, co oblicza i przechowuje nasze przejście do przodu w i . Typ operacji określa, która reguła pochodna zostanie uruchomiona; zapisane krawędzie łączą swoje wejścia z obliczonymi pochodnymi lokalnymi:
| Obliczenia | Wartość wyjściowa przy | Przechowywane połączenia wejściowe i lokalne pochodne |
|---|---|---|
| Wejście , z | ||
| Wejście , z ; wejście , z | ||
| Wejście , z | ||
| Wejście , z ; wejście , z |
Wartości wyjściowe i pochodne lokalne pełnią różne role. Na przykład mnożenie wykorzystuje do obliczenia wyniku i ta sama wartość argumentu dostarcza lokalną pochodną względem . Wstecz łączy zapisane lokalne pochodne ze skumulowaną wrażliwością na wynik końcowy.
Model jest programem, więc funkcja autodiff ma dostęp do operacji, które wygenerowały jego wynik; sama skończona tabela obserwacji wejścia-wyjścia nie może dostarczyć takich informacji ani określić dokładnych pochodnych. W przypadku ustalonej partii szkoleniowej obliczenia przewidywania i strat w modelu mają wiele parametrów i jedną wartość straty. Tryb odwrotny ponownie wykorzystuje wyniki pośrednie do obliczenia wszystkich pochodnych parametrów w jednym przejściu wstecz, zamiast powtarzać całe obliczenia dla każdego parametru.
Ustaliliśmy, co rejestruje silnik i w jaki sposób wykorzystuje lokalne zasady dotyczące pochodnych. Następnie prześledzimy te same obliczenia w interaktywnym przebiegu do przodu i do tyłu, obserwując, jak rejestrowane są wartości i kumulują się wkłady pochodne.
Automatyczne różniczkowanie w trybie odwrotnym krok po kroku
Przeprowadźmy teraz pełne obliczenia automatycznej różnicy w trybie odwrotnym przy użyciu tej samej funkcji i . Ocenimy cztery operacje z powyższej tabeli, a następnie prześledzimy ich pochodne wstecz, aby otrzymać gradient :
x = Value(3)
y = Value(2)
u = y ** 2
v = x * u
w = x ** 3
f = v + w
f.backward()Przejście do przodu daje , , , a wyjście końcowe . Każdy wynik zachowuje swoje powiązania z danymi wejściowymi, które go wytworzyły, wraz z informacjami potrzebnymi do jego lokalnej reguły pochodnej. Przejście wstecz oblicza, jak końcowe wyjście reaguje na każdy węzeł; nie zmienia to wartości przekazywanych dalej.
Wartości forward nazywane są także liczbami podstawowymi. Pochodne zgromadzone podczas przejścia wstecz nazywane są sprzężonymi: każda mierzy, jak zmienia się końcowy wynik w odniesieniu do określonej wartości wejściowej lub wartości pośredniej. W naszym widgecie są one oznaczone odpowiednio jako value i grad. Sprzężenie to pochodna wyniku końcowego, różniąca się od lokalnej pochodnej operacji.
Przed przejściem wstecz każdy .grad zaczyna się od zera. Wysiewamy f.grad = 1, ponieważ .
.grad każdego węzła skalarnego przechowuje pochodną końcowego wyniku w odniesieniu do tego węzła. Razem x.grad i y.grad tworzą wektor gradientu . Przewodnik przechowuje lokalne pochodne numeryczne na połączeniach i wykorzystuje je podczas odtwarzania wstecz: parent.grad += node.grad * local_derivative.
Wcześniejszy widget porównawczy korzystał wyłącznie ze ścieżki z do obliczenia . Poniższy widget przetwarza cały wykres w jednym przebiegu wstecznym, obliczając pochodne zarówno w odniesieniu do , jak i . Kroki te następują sekwencyjnie w ramach tego samego przebiegu. Każde pole pokazuje oddzielnie wartość wyjściową, numeryczne pochodne lokalne i skumulowany gradient.
Wejścia są ustawione na i , co odpowiada utworzeniu x = Value(3) i y = Value(2). Wybierz opcję , aby zobaczyć, jak cztery operacje wygenerują wynik. Następnie wybierz i wykonaj kroki wstecz, dodając, dzieląc, mnożąc i podnosząc do kwadratu. Każdy krok wstecz podkreśla operację i jej dane wejściowe, a następnie łączy symboliczne wyrażenie reguły łańcuchowej z jego podstawieniem numerycznym i odpowiednią aktualizacją .grad poniżej wykresu. Zobacz, jak te dwa wkłady kumulują się w .
W poniższej tabeli podsumowano kroki wstecz, które można wykonać w widgecie. Wartości forward pozostają niezmienione przez cały czas; zmieniają się tylko skumulowane gradienty.
| Krok wstecz | Aktualizacja gradientu |
|---|---|
| Nasiona | |
| Dodatek | , |
| Kostka | |
| Mnożenie, wkład do | Dodaj , dając |
| Mnożenie, wkład do | |
| Kwadrat |
Dwie ścieżki z przyczyniają się oddzielnie, podczas gdy otrzymuje swój udział poprzez i :
To zamówienie przetwarza gałąź kostki przed gałęzią mnożenia. Inne ważne zlecenia wstecz dają ten sam wynik, pod warunkiem, że węzeł otrzyma wszystkie dalsze wkłady, zanim przekaże zakumulowaną pochodną na swoje dane wejściowe.
Widżet pokazuje pełne podanie do tyłu. Połączmy teraz z kodem jego kroki mnożenia i kwadraty . Krok wstecz mnożenia powoduje mnożenie dwóch różnych pochodnych:
Pierwszy czynnik mówi nam, jak końcowe wyjście reaguje na . Drugi mówi nam, jak reaguje na , utrzymując stałe . Mnożenie ich to reguła łańcuchowa: daje udział tej ścieżki w pochodnej względem .
W instrukcji zapisywane są numeryczne pochodne lokalne podczas przejścia do przodu. Inna implementacja może zapisać wartości argumentów i ocenić te same lokalne reguły podczas odtwarzania wstecz. W tej formie metoda mnożenia __mul__(self, other) tworzy węzeł wynikowy o nazwie out, a jej regułę wsteczną można zapisać jako:
self.grad += out.grad * other.data # contribution to the left operand
other.grad += out.grad * self.data # contribution to the right operandDla v = x * u, out to , self to , a other to . Po aktualizacjach mnożenia, zapisanych tymi nazwami, następuje aktualizacja kwadratu:
# Multiplication: v = x * u
x.grad += v.grad * u.data # add 1 × 4 to the existing 27 → 31
u.grad += v.grad * x.data # add 1 × 3 → 3
# Square: u = y ** 2
y.grad += u.grad * (2 * y.data) # add 3 × 4 → 12Przepustka do przodu dostarczana z , i . Mnożenie wykorzystuje wartość drugiego operandu dla każdej lokalnej pochodnej. Kwadrat wykorzystuje i mnoży go przez skumulowany u.grad = 3.
| Kod | Znaczenie |
|---|---|
out.grad | Pochodna wyniku końcowego po wyniku tej operacji |
other.data | Wartość innego operandu, która jest równa lokalnej pochodnej mnożenia |
+= | Dodaj udział tej ścieżki do skumulowanej pochodnej danych wejściowych |
other.data przechowuje wartość, a nie pochodną. Reguła mnożenia mówi, że ta wartość jest równa potrzebnej pochodnej lokalnej. Dla dodawania lokalna pochodna wynosi , więc odpowiednia aktualizacja to self.grad += out.grad * 1. Dla potęgi obliczamy regułę potęgową przy danym wejściu: lokalna pochodna sześcianu to , a kwadratu — .
W symbolicznym widżecie połączyliśmy lokalne wyrażenia pochodne wzdłuż ścieżki z :
Kod opiera się na tych samych połączeniach, ale łączy liczby. Dodatek daje v.grad = 1; mnożenie daje u.grad = 1 × 3 = 3; kwadrat daje y.grad = 3 × 4 = 12. Zatem u.grad zawiera już dwa pierwsze czynniki wyrażenia symbolicznego. Autodiff otrzymuje tę samą pochodną bez konstruowania połączonego wzoru .
Te kroki obliczają pochodne w ; nie aktualizują wejść, aby zminimalizować funkcję. Klasa Value automatyzuje kroki wstecz w jednym wywołaniu f.backward().
Od obliczeń do silnika
Pochodna mierzy lokalną wrażliwość. Pochodne cząstkowe izolują efekt każdego sygnału wejściowego, a gradient gromadzi te pomiary w wektorze. Propagacja wsteczna oblicza je, wykonując zarejestrowane operacje wstecz: mnożysz pochodne wzdłuż każdej ścieżki i dodajesz udziały w miejscach, gdzie ścieżki się spotykają.
Aby zobaczyć te pomysły zaimplementowane w Pythonie, zapoznaj się z przewodnikiem Andreja Karpathy’ego dotyczącym Microgradu. Lokalne reguły pochodne, akumulacja gradientów i przechodzenie wstecz, które tu zbadaliśmy, zapewniają podstawy do zrozumienia jego implementacji.
Aby uzyskać głębszy pogląd matematyczny, zapoznaj się z tym wykładem na temat automatycznego różniczkowania i przekształceń liniowych. Łączy rachunek różniczkowy z algebrą liniową, pokazując, jak pochodne zachowują się jak przekształcenia liniowe, jak reprezentują je macierze Jakobianu i jak łączy je reguła łańcuchowa — matematyczne podstawy automatycznego różniczkowania.