Cum îl faci să se antreneze și să generalizeze
O rețea cu șase straturi blocată la loss ln 2, reparată pas cu pas. Apoi double descent: 5.000 de parametri pe 40 de puncte.
Pe această pagină
Rețeaua din Capitolul 5 funcționează. Are nouă parametri, învață XOR, iar gradientele ei coincid cu PyTorch până la a șaisprezecea zecimală.
Fă-o adâncă de șase straturi și nu mai învață deloc. Nu încet — deloc. Iată o rețea cu șase straturi pe o problemă de clasificare cu două spirale, antrenată timp de 5000 de pași:
step 1: loss 0.693147
step 5000: loss 0.693147
accuracy: 50.0 %Numărul acela nu este arbitrar. este binary cross-entropy pentru un model care produce probabilitatea pentru orice, iar 50 % este o aruncare de monedă pe un dataset echilibrat. După cinci mii de pași, rețeaua nu a mișcat nici măcar o cifră. Nimic nu s-a blocat, nu a apărut niciun avertisment, iar gradientele sunt în continuare exact corecte.
Acest capitol este despre diferența dintre o rețea care rulează și o rețea care funcționează. Are două jumătăți care par subiecte diferite, dar sunt aceeași treabă: să faci loss-ul să meargă în jos și să îl faci să meargă în jos pe date pe care modelul nu le-a mai văzut.
De ce rețeaua cu șase straturi este moartă
Link către secțiunea: De ce rețeaua cu șase straturi este moartăÎncepe prin a te uita, nu prin a ghici. Treci un batch de intrări prin rețea și afișează deviația standard a activărilor la fiecare strat, apoi deviația standard a gradientelor ponderilor:
def profile(model, x):
h = x
for layer in model:
h = layer(h)
if isinstance(layer, (nn.Tanh, nn.ReLU)):
print(f"activation std: {h.std().item():.4f}")
model(x).sum().backward()
for p in model.parameters():
if p.dim() == 2:
print(f"gradient std: {p.grad.std().item():.2e}")Trei inițializări, aceeași arhitectură, șase straturi de :
| inițializare | std activare, straturile 1→6 |
|---|---|
| normală, std | 0.0145 · 0.0016 · 0.0002 · 0.0000 · 0.0000 · 0.0000 |
| normală, std | 0.6573 · 0.9296 · 0.9585 · 0.9634 · 0.9637 · 0.9625 |
| Xavier | 0.1579 · 0.1493 · 0.1353 · 0.1333 · 0.1325 · 0.1403 |
| inițializare | std gradient, primul strat → ultimul |
|---|---|
| normală, std | 3.20e-06 · 4.97e-07 · … · 6.40e-06 |
| normală, std | 1.94e+03 · 2.28e+02 · 1.22e+02 · 4.43e+01 · 1.85e+01 · 7.30e+00 |
| Xavier | 2.31e+00 · 4.50e-01 · 4.26e-01 · 3.89e-01 · 4.39e-01 · 4.73e-01 |
Primul rând este rețeaua de mai sus, iar ea nu învață încet — nu mai are niciun semnal. Până la stratul patru, deviația standard a activărilor a ajuns la zero în patru zecimale. Fiecare intrare produce aceeași ieșire, ieșirea este o constantă, iar gradientul unei constante nu este nimic. Ponderile au fost inițializate mici „ca să fie sigur”, iar mic a fost fatal.
Al doilea rând este eșecul opus și merită înțeles fiindcă este contraintuitiv. Activările par sănătoase — în jur de 0.96 — dar acela este saturat, prins aproape de limita lui, exact regimul pe care Capitolul 5 l-a măsurat ca pierzând un factor de aproape zece mii în gradient. Și totuși gradientele sunt enorme: 1940 la primul strat. Ambele lucruri sunt adevărate în același timp. Fiecare pas backward înmulțește cu , iar cu 128 de intrări la varianță unitară acel factor are un câștig de aproximativ , care copleșește contracția din saturat. Gradientele cresc geometric pe drumul înapoi. Acesta este exploding gradient și produce valori de loss de nan în câțiva pași în orice rulare reală de antrenare.
Al treilea rând este ce vrei: activări cu scară aproximativ constantă de-a lungul adâncimii, gradiente cu scară aproximativ constantă de-a lungul adâncimii. Nimic nu moare, nimic nu explodează.
Normalizare și care dintre ele a supraviețuit
Link către secțiunea: Normalizare și care dintre ele a supraviețuitO inițializare bună repară scara la pasul zero. Nu o păstrează fixă: ponderile se mișcă, iar până la pasul cinci mii argumentul atent despre varianță nu se mai aplică.
Straturile de normalizare impun scara continuu. Dat fiind un vector de activări, scazi o medie, împarți la o deviație standard, apoi aplici o scară învățată și o deplasare , astfel încât stratul să poată anula normalizarea dacă se dovedește că asta vrea:
Singura întrebare reală este peste ce faci media. Batch normalisation3 ia și pe dimensiunea batch-ului, o statistică pentru fiecare feature. Layer normalisation4 le ia peste feature-uri, o statistică pentru fiecare exemplu.
Alegerea aceasta pare minoră și decide aproape tot ce urmează:
BatchNorm face ca ieșirea fiecărui exemplu să depindă de celelalte exemple care s-au întâmplat să fie în batch-ul lui. La antrenare, acesta este un regularizator blând. La inference nu există batch, așa că trebuie să păstreze o medie rulantă a statisticilor colectate în timpul antrenării — ceea ce înseamnă că stratul se comportă diferit în modul de antrenare și în modul de evaluare, iar uitatul comutării modurilor este unul dintre cele mai comune buguri din domeniu. De asemenea, se degradează cu batch-uri mici și este incomod cu secvențe de lungime variabilă, fiindcă „media peste batch la poziția 40” este calculată din oricâte secvențe se întâmplă să fie atât de lungi.
LayerNorm normalizează fiecare exemplu separat. Fără dependență de batch, fără statistici rulante, comportament identic la antrenare și inference, indiferent la dimensiunea batch-ului, indiferent la lungimea secvenței. Fiecare dintre aceste proprietăți este o cerință, nu un detaliu plăcut, odată ce generezi câte un token pentru câte un utilizator, acolo unde ajunge Capitolul 13.
De aceea LayerNorm este cea pe care o vei întâlni din nou în Capitolul 9 neschimbată: blocul transformer o folosește și o folosește din motivele din coloana din dreapta, nu fiindcă ar funcționa mai bine în abstract.
Să repari câte un lucru pe rând, adică skill-ul real
Link către secțiunea: Să repari câte un lucru pe rând, adică skill-ul realPatru remedii candidate pentru rețeaua moartă: inițializare Xavier, LayerNorm, conexiuni reziduale și Adam în loc de SGD. Tentația este să le aplici pe toate patru și să mergi mai departe. Fă asta și nu vei ști niciodată care a contat, iar data viitoare când se întâmplă nu vei avea nicio metodă — doar un ritual.
Așa că aplică-le pe rând. Același seed, aceleași date, aceeași arhitectură, 800 de pași:
| ce s-a adăugat | loss final | acuratețe |
|---|---|---|
| nimic | 0.6931 | 50.0 % |
| inițializare Xavier | 0.5692 | 60.4 % |
| LayerNorm | 0.6230 | 61.5 % |
| conexiuni reziduale | 0.6651 | 56.6 % |
| Adam | 0.6787 | 58.7 % |
| toate patru | 0.0000 | 100.0 % |
Citește tabelul acela cum l-ai citi la 2 dimineața și concluzia este: nimic nu funcționează singur, totul funcționează împreună, deci deep learning este alchimie. Concluzia aceasta este greșită, iar a afla de ce este cel mai util lucru din acest capitol.
Dă fiecărei rulări de șase ori mai mult buget — 5000 de pași în loc de 800 — și se schimbă complet:
| ce s-a adăugat | loss final @ 5000 | acuratețe |
|---|---|---|
| nimic | 0.6931 | 50.0 % |
| inițializare Xavier | 0.0007 | 100.0 % |
| LayerNorm | 0.0002 | 100.0 % |
| conexiuni reziduale | 0.6653 | 56.7 % |
| Adam | 0.6908 | 53.4 % |
| Xavier + Adam | 0.0000 | 100.0 % |
| Xavier + LayerNorm | 0.0001 | 100.0 % |
Acum imaginea este clară și este un diagnostic, nu un ritual.
Inițializarea singură o repară. Normalizarea singură o repară. Fiecare atacă boala reală — semnalul forward care se prăbușește la zero — și oricare dintre ele este suficientă. La 800 de pași doar păreau punctaj parțial, fiindcă rezolvaseră problema și încă urcau din groapă.
Conexiunile reziduale și Adam nu o repară, indiferent de buget. Nu pentru că sunt rele, ci pentru că tratează o altă boală. O conexiune reziduală oferă gradientului o cale pe lângă un strat care blochează; asta valorează enorm când gradientul este problema și nu valorează nimic când semnalul forward este deja zero, fiindcă o scurtătură pe lângă un strat mort tot transportă o valoare moartă. Adam rescalează pasul fiecărui parametru după propriul istoric de gradient; asta ajută când gradientele au magnitudini foarte diferite și nu poate resuscita o rețea a cărei ieșire nu depinde de intrare.
Iar „nimic” este încă exact 0.6931 după cinci mii de pași. Nu 0.6929. Nu este lentă; este moartă, iar distincția aceea se vede într-un mod în care nu se vedea înainte, fiindcă ai rândul care spune că un remediu funcționează ca termen de comparație.
Să meriți PyTorch
Link către secțiunea: Să meriți PyTorchDe aici înainte, acest curs folosește PyTorch. Asta ar trebui meritat, nu doar anunțat, așa că iată exact ce face el din lucrurile pe care știi deja să le faci.
Un optimizer este o regulă pentru a transforma gradientele în actualizări de parametri. Gradient descent simplu folosește gradientul. Momentum folosește o medie rulantă a lui, ceea ce netezește zgomotul și acumulează viteză pe direcțiile care rămân consistente:
v = beta * v + p.grad
p -= lr * v Adam5 păstrează două medii rulante — a gradientului și a gradientului la pătrat — și o împarte pe una la rădăcina pătrată a celeilalte, astfel încât fiecare parametru primește un pas scalat la propria magnitudine recentă a gradientului:
m = b1 * m + (1 - b1) * g # mean of the gradient
v = b2 * v + (1 - b2) * g * g # mean of the squared gradient
m_hat = m / (1 - b1 ** t) # bias correction: both averages start at zero
v_hat = v / (1 - b2 ** t)
p -= lr * m_hat / (v_hat.sqrt() + eps) Zece linii. Rulează-le pe ambele împotriva torch.optim pe aceeași problemă timp de 50 de pași:
SGD+momentum by hand [2.7781870365142822, -1.0304985046386719]
torch [2.7781870365142822, -1.0304983854293823] max |diff| = 1.19e-07
Adam by hand [0.4893140196800232, -0.46317872405052185]
torch [0.48931416869163513, -0.46317875385284424] max |diff| = 1.49e-07Identic până la precizia float32. torch.optim.Adam înseamnă acele cinci linii, plus decenii de grijă pentru cazuri-limită și un kernel C++. Acesta este schimbul pe care îl faci de aici înainte: nu magie în loc de înțelegere, ci viteză pentru linii pe care le-ai scris deja.
De ce există Adam: curbura
Link către secțiunea: De ce există Adam: curburaExplicația obișnuită pentru Adam este „learning rates adaptive per parametru”, ceea ce este o descriere, nu un motiv. Motivul este geometria și poate fi măsurat.
Ia un loss a cărui curbură diferă între direcții: abruptă într-una, lină în alta. SGD are un singur learning rate global, deci trebuie să aleagă o valoare suficient de mică pentru a fi stabilă în direcția cea mai abruptă — iar acea valoare este apoi mult prea mică pentru direcția lină, unde progresul se târăște. Asta produce imaginea clasică în care gradient descent coboară în zigzag printr-o vale îngustă.
Două rapoarte de curbură, trei optimizeri, 300 de pași, iar fiecare optimizer primește cel mai bun learning rate dintr-o explorare, ca nimeni să nu fie dezavantajat:
| raport de curbură | SGD | SGD + momentum | Adam |
|---|---|---|---|
| 10 : 1 | eroare 0.000002 | eroare 0.000000 | eroare 0.000000 |
| 1000 : 1 | eroare 1.925485 | eroare 0.001432 | eroare 0.000000 |
| a divergat la (1000:1) | 4 din 8 rate | 4 din 8 rate | 0 din 6 rate |
La un raport de zece, totul funcționează și nu este nimic de discutat. La o mie, SGD simplu nu poate ajunge la răspuns la niciun learning rate încercat — cel mai bun rezultat al lui este tot o eroare de 1.93 — și diverge direct la jumătate dintre rate. Adam aterizează exact pe țintă și nu diverge la niciuna.
Acea ultimă coloană este motivul practic pentru care Adam este implicit. Nu pentru că Adam găsește soluții mai bune; pe probleme bine condiționate, SGD bine ajustat îl egalează sau îl depășește adesea. Ci pentru că Adam este mult mai puțin sensibil la learning rate-ul pe care l-ai ales, iar rețelele reale au rapoarte de curbură mult mai rele decât o mie de-a lungul milioanelor lor de parametri.
Încă două piese aparțin aici și ambele sunt o linie. Gradient clipping rescalează vectorul gradientului ori de câte ori norma lui depășește un prag, ceea ce transformă rândul „loss-ul sare brusc la o valoare uriașă” din tabelul de diagnostic într-un non-eveniment. Și learning rate schedules: un warmup scurt de la aproape zero în primele câteva sute de pași, fiindcă estimările de varianță ale lui Adam sunt gunoi până când au văzut câteva gradiente, iar un pas de dimensiune completă făcut pe gunoi poate distruge o inițializare; apoi cosine decay spre zero, fiindcă a încheia o rulare cu aceeași mărime de pas cu care ai început înseamnă să tremuri în jurul minimului în loc să te așezi în el.
A doua jumătate: modelul care se potrivește perfect și nu prezice nimic
Link către secțiunea: A doua jumătate: modelul care se potrivește perfect și nu prezice nimicTotul până aici a fost despre a face loss-ul să scadă. Acum urmează jumătatea mai grea, fiindcă scăderea loss-ului nu este scopul — este un proxy pentru scop, iar proxy-ul eșuează într-un mod specific și celebru.
Douăsprezece puncte dintr-o funcție netedă, cu puțin zgomot. Potrivește polinoame de grad crescător:
| grad | train RMSE | test RMSE |
|---|---|---|
| 1 | 0.764499 | 0.6985 |
| 3 | 0.252605 | 0.3031 |
| 5 | 0.164437 | 0.1568 |
| 9 | 0.088960 | 0.2347 |
| 11 | 0.000000 | 1.2094 |
Gradul 11 prin 12 puncte trece prin fiecare dintre ele exact — eroare de train zero până la șase zecimale — și este de opt ori mai prost decât gradul 5 pe date pe care nu le-a văzut. Cere gradului 3 și gradului 11 să prezică la , imediat în afara intervalului de antrenare:
degree 3: predicts -1.053 (truth -0.012)
degree 11: predicts +61.224 (truth -0.012)Șaizeci și unu, când răspunsul este aproximativ zero. Modelul nu a învățat funcția; a învățat cele douăsprezece puncte, iar între ele face orice cere aritmetica.
Acesta este overfitting, iar opusul lui — gradul 1, care nu poate reprezenta deloc curba și este prost peste tot — este underfitting. Explicația clasică împarte eroarea așteptată a unui model în trei părți: bias, eroarea dată de faptul că modelul este prea rigid pentru a reprezenta adevărul; variance, eroarea dată de faptul că modelul este atât de flexibil încât urmărește zgomotul din acest eșantion particular; și zgomot ireductibil, pe care nimic nu îl repară. Modelele simple au bias, modelele flexibile au variance mare, iar prescripția clasică este să găsești punctul optim de la mijloc — gradul 5 din tabelul de mai sus.
Instrumentele standard atacă toate termenul variance:
- Regularizarea L2 (weight decay) adaugă la loss, trăgând ponderile spre zero și făcând funcția mai netedă. În tabelul de mai sus, cel mai mare coeficient al gradului 11 face paguba; penalizarea mărimii îl dezamorsează.
- L1 adaugă în schimb . Diferența nu este cosmetică: gradientul L2 este proporțional cu ponderea și deci se micșorează pe măsură ce ponderea se micșorează, apropiindu-se de zero fără să ajungă, în timp ce gradientul L1 este o constantă care continuă să împingă până la capăt. Prin urmare, L1 produce ponderi care sunt exact zero — selectează feature-uri. L2 produce ponderi mici. Folosește L2 când vrei netezime, L1 când vrei raritate.
- Dropout7 pune la zero un subset aleator de activări la fiecare pas de antrenare, astfel încât nicio unitate să nu se poată baza pe prezența unei anumite alte unități.
- Early stopping urmărește validation loss și se oprește când acesta începe să urce.
- Data augmentation fabrică mai multe exemple de antrenare din cele pe care le ai, atacând problema la sursă: overfitting este o lipsă de date la fel de mult cât este un exces de parametri.
- Cross-validation împarte datele în feluri și antrenează de ori, ceea ce cumpără o estimare fiabilă a erorii de test când ai prea puține date ca să pui deoparte un set separat.
Double descent, sau de ce secțiunea anterioară nu este toată povestea
Link către secțiunea: Double descent, sau de ce secțiunea anterioară nu este toată povesteaAcum faptul care sparge imaginea.
Povestea bias-variance spune că, după punctul optim, mai mulți parametri înseamnă generalizare mai proastă. Modelele de limbaj moderne au mult mai mulți parametri decât permit regulile clasice pentru datele pe care le văd și generalizează superb. Ambele afirmații sunt adevărate, iar împăcarea lor este cel mai util lucru din acest capitol.
Patruzeci de puncte de antrenare, intrări cu douăzeci de dimensiuni, feature-uri ReLU aleatoare, iar numărul de feature-uri variat de la 2 la 5000 — cu soluția de normă minimă aleasă ori de câte ori există multe care se potrivesc:
| train RMSE | test RMSE | |||
|---|---|---|---|---|
| 10 | 0.25 | 0.8822 | 1.2520 | 1.89 |
| 20 | 0.50 | 0.5962 | 1.1634 | 2.59 |
| 30 | 0.75 | 0.3896 | 1.5323 | 4.15 |
| 38 | 0.95 | 0.1769 | 3.7163 | 10.25 |
| 40 | 1.00 | 0.0000 | 5.8140 | 14.83 |
| 42 | 1.05 | 0.0000 | 3.1623 | 9.35 |
| 60 | 1.50 | 0.0000 | 1.1058 | 2.78 |
| 200 | 5.00 | 0.0000 | 0.6638 | 0.98 |
| 1500 | 37.50 | 0.0000 | 0.5859 | 0.33 |
| 5000 | 125.00 | 0.0000 | 0.5664 | 0.18 |
Citește-l în trei părți. Până la , povestea clasică se ține exact: eroarea scade, apoi începe să crească. La — pragul de interpolare, unde modelul are exact destui parametri ca să treacă prin fiecare punct de antrenare — eroarea de test atinge un vârf, la 5.81, de cinci ori mai rău decât modelul mic. Acel vârf este avertismentul clasic și este real.
Apoi coboară din nou. Și continuă să coboare, dincolo de , dincolo de , până la , unde eroarea de test de 0.5664 este mai bună decât a reușit vreodată cel mai bun model sub-parametrizat. Un model cu 5000 de parametri potrivit pe 40 de puncte este cel mai bun model din tabel.
Acesta este double descent,89 iar mecanismul se vede în ultima coloană. Odată ce există infinit de multe setări de parametri care se potrivesc exact datelor de antrenare, iar cea pe care o obții depinde de cum alegi. Soluția de normă minimă o alege pe cea mai mică, iar arată ce înseamnă asta: atinge vârful la 14.83 exact la prag — unde există exact o soluție interpolatoare și ești blocat cu ea, oricât de extremă — și apoi scade monoton pe măsură ce crește, fiindcă mai mulți parametri înseamnă mai multe soluții interpolatoare dintre care să alegi, ceea ce înseamnă că cea mai mică disponibilă devine mai mică. La norma este 0.18, de optzeci de ori mai mică decât la prag.
Deci parametrii în plus nu adaugă complexitate. Adaugă alegere, iar regula de selecție cheltuie acea alegere pe simplitate. Regularizarea nu este în funcția de loss; este în algoritm. Gradient descent pornit dintr-o inițializare mică are un bias documentat către soluții cu normă mică, motiv pentru care acest comportament apare în rețele reale antrenate în mod obișnuit, nu doar în algebra liniară de mai sus.
Consecința practică, de care depinde Capitolul 10: „modelul are mai mulți parametri decât date, deci va face overfit” nu este un argument valid. Era o regulă bună când modelele trăiau la stânga pragului. Tot ce este interesant acum trăiește mult la dreapta lui, unde regula se inversează.
Unde mergem mai departe
Link către secțiunea: Unde mergem mai departeInstrumentele din acest capitol sunt suficiente ca să antrenezi o rețea care funcționează pe date pe care le poți pune într-un tabel: rânduri de numere, o coloană de etichete.
Limbajul nu este așa. Înainte ca un model să poată prezice următorul cuvânt, ceva trebuie să decidă ce este, de fapt, un „cuvânt” — iar răspunsul nu este nici litere, nici cuvinte, ci un vocabular pe care modelul îl învață din octeții brute ai datelor de antrenare. Acea decizie, luată o singură dată înainte de începerea antrenării, determină câte lucruri poate spune modelul, cât costă o cerere și de ce modele care pot trece un examen de drept nu pot număra fiabil literele din strawberry.
Capitolul 7 construiește un tokenizer.
Surse și metodă
Link către secțiunea: Surse și metodăPentru conexiunile reziduale folosite mai sus, He et al., Deep Residual Learning for Image Recognition (arXiv:1512.03385). Building makemore Part 3: Activations & Gradients, BatchNorm de Andrej Karpathy parcurge diagnosticul histogramelor de activare pe un model real și este cel mai bun tratament practic al primei jumătăți a acestui capitol. Lecțiile 8 și 11–13 din Learning From Data de Yaser Abu-Mostafa prezintă corect teoria clasică a generalizării, inclusiv părțile pe care acest capitol le-a comprimat într-un paragraf.
Referințe
Link către secțiunea: Referințe-
Glorot, X. și Bengio, Y. Understanding the difficulty of training deep feedforward neural networks. AISTATS (2010). Argumentul de conservare a varianței reprodus în caseta de mai sus. ↩
-
He, K., Zhang, X., Ren, S. și Sun, J. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. arXiv:1502.01852 (2015). ↩
-
Ioffe, S. și Szegedy, C. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. arXiv:1502.03167 (2015). Reține că explicația „internal covariate shift” din titlu a fost între timp contestată substanțial; stratul funcționează, explicația originală pentru motivul funcționării este disputată. ↩
-
Ba, J. L., Kiros, J. R. și Hinton, G. E. Layer Normalization. arXiv:1607.06450 (2016). ↩
-
Kingma, D. P. și Ba, J. Adam: A Method for Stochastic Optimization. arXiv:1412.6980 (2014). ↩
-
Loshchilov, I. și Hutter, F. Decoupled Weight Decay Regularization. arXiv:1711.05101 (2017). ↩
-
Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I. și Salakhutdinov, R. Dropout: A Simple Way to Prevent Neural Networks from Overfitting. JMLR 15, pp. 1929–1958 (2014). ↩
-
Belkin, M., Hsu, D., Ma, S. și Mandal, S. Reconciling modern machine-learning practice and the classical bias–variance trade-off. PNAS 116(32), pp. 15849–15854 (2019). Lucrarea care a numit fenomenul. ↩
-
Nakkiran, P., Kaplun, G., Bansal, Y., Yang, T., Barak, B. și Sutskever, I. Deep Double Descent: Where Bigger Models and More Data Hurt. arXiv:1912.02292 (2019). Arată efectul în rețele deep reale și de-a lungul axei timpului de antrenare, nu doar al axei dimensiunii modelului. ↩