====== Diferențiere Automată ======
Amintiți-vă din că calcularea derivatelor este pasul crucial în toți algoritmii de optimizare pe care îi vom folosi pentru a antrena rețele profunde. Deși calculele sunt simple, efectuarea lor manuală poate fi plictisitoare și predispusă la erori, iar aceste probleme cresc doar pe măsură ce modelele noastre devin mai complexe.
Din fericire, toate cadrele moderne de deep learning ne iau această muncă de pe umeri oferind //diferențiere automată// (adesea prescurtată ca //autograd//). Pe măsură ce trecem datele prin fiecare funcție succesivă, cadrul construiește un //graf computational// care urmărește modul în care fiecare valoare depinde de celelalte. Pentru a calcula derivatele, diferențierea automată funcționează invers prin acest grafic aplicând regula lanțului. Algoritmul computațional pentru aplicarea regulii lanțului în acest mod se numește //backpropagation// (retropropagare).
Deși bibliotecile autograd au devenit o preocupare fierbinte în ultimul deceniu, ele au o istorie lungă. De fapt, cele mai vechi referințe la autograd datează de peste jumătate de secol ((Wengert.1964)). Ideile de bază din spatele retropropagării moderne datează dintr-o teză de doctorat din 1980 ((Speelpenning.1980)) și au fost dezvoltate în continuare la sfârșitul anilor 1980 ((Griewank.1989)). Deși retropropagarea a devenit metoda implicită pentru calcularea gradienților, nu este singura opțiune. De exemplu, limbajul de programare Julia folosește propagarea înainte ((Revels.Lubin.Papamarkou.2016)). Înainte de a explora metodele, să stăpânim mai întâi pachetul autograd.
import torch
===== O Funcție Simplă =====
Să presupunem că suntem interesați de (**diferențierea funcției $y = 2\mathbf{x}^{\top}\mathbf{x}$ în raport cu vectorul coloană $\mathbf{x}$.**) Pentru a începe, atribuim lui ''%%x%%'' o valoare inițială.
x = torch.arange(4.0)
x
[**Înainte de a calcula gradientul lui $y$ în raport cu $\mathbf{x}$, avem nevoie de un loc unde să-l stocăm.**] În general, evităm alocarea de memorie nouă de fiecare dată când luăm o derivată, deoarece deep learning-ul necesită calcularea succesivă a derivatelor în raport cu aceiași parametri de foarte multe ori și am putea risca să rămânem fără memorie. Rețineți că gradientul unei funcții cu valoare scalară în raport cu un vector $\mathbf{x}$ are valoare vectorială cu aceeași formă ca $\mathbf{x}$.
# Can also create x = torch.arange(4.0, requires_grad=True)
x.requires_grad_(True)
x.grad # The gradient is None by default
(**Acum calculăm funcția noastră de ''%%x%%'' și atribuim rezultatul lui ''%%y%%''.**)
y = 2 * torch.dot(x, x)
y
[**Acum putem lua gradientul lui ''%%y%%'' în raport cu ''%%x%%''**] apelând metoda sa ''%%backward%%''. Apoi, putem accesa gradientul prin atributul ''%%grad%%'' al lui ''%%x%%''.
y.backward()
x.grad
(**Știm deja că gradientul funcției $y = 2\mathbf{x}^{\top}\mathbf{x}$ în raport cu $\mathbf{x}$ ar trebui să fie $4\mathbf{x}$.**) Putem verifica acum că calculul automat al gradientului și rezultatul așteptat sunt identice.
x.grad == 4 * x
[**Acum să calculăm o altă funcție de ''%%x%%'' și să luăm gradientul acesteia.**] Rețineți că PyTorch nu resetează automat bufferul de gradient atunci când înregistrăm un nou gradient. În schimb, noul gradient este adăugat la gradientul deja stocat. Acest comportament este util atunci când dorim să optimizăm suma mai multor funcții obiectiv. Pentru a reseta bufferul de gradient, putem apela ''%%x.grad.zero_()%%'' după cum urmează:
x.grad.zero_() # Reset the gradient
y = x.sum()
y.backward()
x.grad
===== Backward pentru Variabile Non-Scalare =====
Când ''%%y%%'' este un vector, cea mai naturală reprezentare a derivatei lui ''%%y%%'' în raport cu un vector ''%%x%%'' este o matrice numită //Jacobian// care conține derivatele parțiale ale fiecărei componente a lui ''%%y%%'' în raport cu fiecare componentă a lui ''%%x%%''. La fel, pentru ''%%y%%'' și ''%%x%%'' de ordin superior, rezultatul diferențierii ar putea fi un tensor de ordin și mai mare.
În timp ce Jacobienii apar în unele tehnici avansate de învățare automată, mai frecvent dorim să însumăm gradienții fiecărei componente a lui ''%%y%%'' în raport cu vectorul complet ''%%x%%'', rezultând un vector de aceeași formă cu ''%%x%%''. De exemplu, avem adesea un vector care reprezintă valoarea funcției noastre de pierdere calculată separat pentru fiecare exemplu dintr-un //lot// (batch) de exemple de antrenament. Aici, vrem doar să (**însumăm gradienții calculați individual pentru fiecare exemplu**).
Deoarece cadrele de deep learning variază în modul în care interpretează gradienții tensorilor non-scalari, PyTorch ia măsuri pentru a evita confuzia. Invocarea ''%%backward%%'' pe un non-scalar provoacă o eroare, cu excepția cazului în care îi spunem lui PyTorch cum să reducă obiectul la un scalar. Mai formal, trebuie să furnizăm un vector $\mathbf{v}$ astfel încât ''%%backward%%'' să calculeze $\mathbf{v}^\top \partial_{\mathbf{x}} \mathbf{y}$ mai degrabă decât $\partial_{\mathbf{x}} \mathbf{y}$. Această parte următoare poate fi confuză, dar din motive care vor deveni clare mai târziu, acest argument (reprezentând $\mathbf{v}$) este numit ''%%gradient%%''. Pentru o descriere mai detaliată, consultați [[https://zhang-yang.medium.com/the-gradient-argument-in-pytorchs-backward-function-explained-by-examples-68f266950c29|postarea de pe Medium]] a lui Yang Zhang.
x.grad.zero_()
y = x * x
y.backward(gradient=torch.ones(len(y))) # Faster: y.sum().backward()
x.grad
===== Detașarea Calculului =====
Uneori, dorim să [**mutăm unele calcule în afara grafului computațional înregistrat.**] De exemplu, să spunem că folosim intrarea pentru a crea niște termeni intermediari auxiliari pentru care nu dorim să calculăm un gradient. În acest caz, trebuie să //detașăm// graful computațional respectiv de rezultatul final. Următorul exemplu simplu face acest lucru mai clar: să presupunem că avem ''%%z = x * y%%'' și ''%%y = x * x%%'' dar vrem să ne concentrăm pe influența //directă// a lui ''%%x%%'' asupra lui ''%%z%%'' mai degrabă decât influența transmisă prin ''%%y%%''. În acest caz, putem crea o nouă variabilă ''%%u%%'' care ia aceeași valoare ca ''%%y%%'' dar a cărei //proveniență// (cum a fost creată) a fost ștearsă. Astfel, ''%%u%%'' nu are strămoși în graf și gradienții nu curg prin ''%%u%%'' către ''%%x%%''. De exemplu, luarea gradientului lui ''%%z = x * u%%'' va produce rezultatul ''%%u%%'', (nu ''%%3 * x * x%%'' așa cum v-ați fi așteptat, deoarece ''%%z = x * x * x%%'').
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
Rețineți că, deși această procedură detașează strămoșii lui ''%%y%%'' de graful care duce la ''%%z%%'', graful computațional care duce la ''%%y%%'' persistă și astfel putem calcula gradientul lui ''%%y%%'' în raport cu ''%%x%%''.
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
===== Gradienți și Fluxul de Control Python =====
Până acum am trecut în revistă cazurile în care calea de la intrare la ieșire a fost bine definită printr-o funcție precum ''%%z = x * x * x%%''. Programarea ne oferă mult mai multă libertate în modul în care calculăm rezultatele. De exemplu, le putem face să depindă de variabile auxiliare sau de alegeri condiționate de rezultate intermediare. Un beneficiu al utilizării diferențierii automate este că [**chiar dacă**] construirea grafului computațional al (**unei funcții a necesitat trecerea printr-un labirint de flux de control Python**) (de exemplu, condiționale, bucle și apeluri de funcții arbitrare), (**putem calcula în continuare gradientul variabilei rezultate.**) Pentru a ilustra acest lucru, luați în considerare următorul fragment de cod în care numărul de iterații al buclei ''%%while%%'' și evaluarea instrucțiunii ''%%if%%'' depind ambele de valoarea intrării ''%%a%%''.
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
Mai jos, apelăm această funcție, trecând o valoare aleatorie, ca intrare. Deoarece intrarea este o variabilă aleatorie, nu știm ce formă va lua graful computațional. Cu toate acestea, ori de câte ori executăm ''%%f(a)%%'' pe o intrare specifică, realizăm un graf computațional specific și putem rula ulterior ''%%backward%%''.
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
Chiar dacă funcția noastră ''%%f%%'' este, în scop demonstrativ, un pic artificială, dependența sa de intrare este destul de simplă: este o funcție //liniară// de ''%%a%%'' cu scară definită pe porțiuni. Ca atare, ''%%f(a) / a%%'' este un vector de intrări constante și, mai mult, ''%%f(a) / a%%'' trebuie să se potrivească cu gradientul lui ''%%f(a)%%'' în raport cu ''%%a%%''.
a.grad == d / a
Fluxul de control dinamic este foarte comun în deep learning. De exemplu, la procesarea textului, graful computațional depinde de lungimea intrării. În aceste cazuri, diferențierea automată devine vitală pentru modelarea statistică, deoarece este imposibil să se calculeze gradientul //a priori//.
===== Discuție =====
Ați gustat acum din puterea diferențierii automate. Dezvoltarea bibliotecilor pentru calcularea derivatelor atât automat, cât și eficient a fost un stimulent masiv al productivității pentru practicienii de deep learning, eliberându-i astfel încât să se poată concentra mai puțin pe treburi mărunte. Mai mult, autograd ne permite să proiectăm modele masive pentru care calculele gradientului cu creionul și hârtia ar fi prohibitiv de mari consumatoare de timp. Interesant, în timp ce folosim autograd pentru a //optimiza// modele (într-un sens statistic), //optimizarea// bibliotecilor autograd în sine (într-un sens computațional) este un subiect bogat de interes vital pentru designerii de cadre. Aici, instrumentele din compilatoare și manipularea grafelor sunt valorificate pentru a calcula rezultatele în cel mai rapid și eficient mod din punct de vedere al memoriei.
Deocamdată, încercați să vă amintiți aceste baze: (i) atașați gradienți acelor variabile în raport cu care dorim derivate; (ii) înregistrați calculul valorii țintă; (iii) executați funcția de retropropagare; și (iv) accesați gradientul rezultat.
===== Exerciții =====
- De ce este mult mai costisitoare calcularea celei de-a doua derivate decât a primei derivate?
- După rularea funcției pentru retropropagare, rulați-o imediat din nou și vedeți ce se întâmplă. Investigați.
- În exemplul cu fluxul de control în care calculăm derivata lui ''%%d%%'' în raport cu ''%%a%%'', ce s-ar întâmpla dacă am schimba variabila ''%%a%%'' într-un vector aleatoriu sau o matrice? În acest moment, rezultatul calculului ''%%f(a)%%'' nu mai este un scalar. Ce se întâmplă cu rezultatul? Cum analizăm acest lucru?
- Fie $f(x) = \sin(x)$. Desenați graficul lui $f$ și al derivatei sale $f'$. Nu exploatați faptul că $f'(x) = \cos(x)$, ci folosiți diferențierea automată pentru a obține rezultatul.
- Fie $f(x) = ((\log x^2) \cdot \sin x) + x^{-1}$. Scrieți un graf de dependență care urmărește rezultatele de la $x$ la $f(x)$.
- Folosiți regula lanțului pentru a calcula derivata $\frac{df}{dx}$ a funcției menționate mai sus, plasând fiecare termen pe graful de dependență pe care l-ați construit anterior.
- Dat fiind graful și rezultatele intermediare ale derivatelor, aveți o serie de opțiuni atunci când calculați gradientul. Evaluați rezultatul o dată începând de la $x$ la $f$ și o dată de la $f$ urmărind înapoi la $x$. Calea de la $x$ la $f$ este cunoscută în mod obișnuit sub numele de //diferențiere înainte//, în timp ce calea de la $f$ la $x$ este cunoscută sub numele de diferențiere înapoi (backward differentiation).
- Când ați putea dori să utilizați diferențierea înainte și când diferențierea înapoi? Indiciu: luați în considerare cantitatea de date intermediare necesară, capacitatea de a paraleliza pașii și dimensiunea matricelor și vectorilor implicați.
[[https://discuss.d2l.ai/t/35|Discuții]]