Faza 03 · lecția 13
Depanarea rețelelor neuronale
Scopul lecției: Rețeaua s-a compilat. A rulat. A produs un număr. Numărul este greșit și nimic nu s-a blocat. Bun-venit la cel mai dificil tip de depanare — acela în care nu există niciun mesaj de eroare.
Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Mentalitatea de depanare
- Simptomul 1: pierderea nu scade
- Simptomul 2: pierderea scade, dar modelul este slab
- Simptomul 3: NaN sau Inf în pierdere
- Tehnica 1: verificarea gradienților
- Tehnica 2: statisticile activărilor
- Tehnica 3: vizualizarea fluxului gradienților
- Tehnica 4: testul de supraînvățare a unui singur lot
- Tehnica 5: găsirea ratei de învățare
- Erori PyTorch frecvente
- Tabelul principal de depanare
- Construiți-l
- Pasul 1: clasa NetworkDebugger
- Pasul 2: testul de supraînvățare a unui singur lot
- Pasul 3: găsirea ratei de învățare
- Pasul 4: verificatorul de gradienți
- Pasul 5: rețele defectate intenționat
- Utilizați-l
- Instrumente integrate în PyTorch
- Integrarea Weights & Biases
- TensorBoard
- Lista de verificare pentru depanare (înainte de antrenarea completă)
- Livrați-l
- Exerciții
- Termeni-cheie
- Lecturi suplimentare
Rețeaua s-a compilat. A rulat. A produs un număr. Numărul este greșit și nimic nu s-a blocat. Bun-venit la cel mai dificil tip de depanare — acela în care nu există niciun mesaj de eroare.
Tip: Construcție Limbaje: Python, PyTorch Cerințe preliminare: Faza 03, lecțiile 01–10 (în special retropropagarea, funcțiile de pierdere și optimizatorii) Timp: ~90 de minute
Obiective de învățare
- Diagnosticați defecțiunile frecvente ale rețelelor neuronale (pierdere NaN, curbă plată a pierderii, supraînvățare, oscilație) folosind strategii sistematice de depanare
- Aplicați tehnica „supraînvățați un singur lot” pentru a verifica dacă arhitectura modelului și bucla de antrenare sunt corecte
- Inspectați magnitudinile gradienților, distribuțiile activărilor și normele ponderilor pentru a identifica problemele gradienților care dispar sau explodează
- Construiți o listă de verificare pentru depanare care acoperă problemele pipeline-ului de date, ale arhitecturii modelului, ale funcției de pierdere, ale optimizatorului și ale ratei de învățare
Problema
Software-ul tradițional se blochează când este defect. Un pointer nul declanșează o excepție. O nepotrivire de tipuri eșuează la compilare. O eroare de tip „cu unu în plus sau în minus” produce un rezultat evident greșit.
Rețelele neuronale nu vă oferă acest lux.
O rețea neuronală defectă rulează până la capăt, afișează o valoare a pierderii și produce predicții. Pierderea poate să scadă. Predicțiile pot părea plauzibile. Modelul este însă greșit în tăcere — învață scurtături, memorează zgomot sau converge către un minim local inutil. Textul-sursă atribuie cercetătorilor Google estimarea că 60–70% din timpul de depanare ML este consumat de erori „silențioase”, care nu produc mesaje de eroare, dar degradează calitatea modelului.
Notă tehnică a traducerii: Pentru procentul de 60–70% și atribuirea lui cercetătorilor Google, originalul nu indică o sursă primară verificabilă; tratați cifra drept afirmație anecdotică, nu drept estimare generală stabilită.
Diferența dintre un model funcțional și unul defect este adesea o singură linie amplasată greșit: un zero_grad() omis, o dimensiune transpusă, o rată de învățare diferită de cea potrivită cu un factor de 10. Articolul consacrat „A Recipe for Training Neural Networks” (2019) începe de la ideea că cele mai frecvente greșeli ale rețelelor neuronale sunt erori care nu provoacă blocarea programului.
Notă tehnică a traducerii: Articolul lui Andrej Karpathy susține abordarea de la simplu la complex și testul de supraînvățare a unui singur lot, dar fraza pusă între ghilimele în original nu apare literal în articol, iar acesta nu începe cu ea. Formularea lecției trebuie înțeleasă drept parafrază, nu drept citat exact.
Această lecție vă învață să găsiți asemenea erori.
Conceptul
Mentalitatea de depanare
Renunțați la depanarea bazată pe afișări și speranță. Depanarea rețelelor neuronale cere o abordare sistematică, deoarece bucla de feedback este lentă (de la minute la ore pentru fiecare rulare de antrenare), iar simptomele sunt ambigue (o pierdere slabă poate avea 20 de cauze diferite).
Regula de aur: începeți simplu, adăugați complexitate câte o componentă și verificați fiecare componentă independent.
Simptomul 1: pierderea nu scade
Aceasta este cea mai frecventă nemulțumire. Bucla de antrenare rulează, epocile se succed, iar pierderea rămâne constantă sau oscilează puternic.
Rată de învățare greșită. Prea mare: pierderea oscilează sau sare la NaN. Prea mică: pierderea scade atât de lent încât pare constantă. Pentru Adam, începeți de la 1e-3. Pentru SGD, începeți de la 1e-1 sau 1e-2. Încercați întotdeauna trei rate de învățare, fiecare diferită de următoarea cu un factor de 10 (de exemplu, 1e-2, 1e-3, 1e-4), înainte de a conchide că altceva este greșit.
ReLU inactive. Dacă un neuron ReLU primește intrări negative pentru toate exemplele relevante, produce 0, iar gradientul său este 0; fără un semnal de gradient care să-i schimbe regimul, poate rămâne inactiv. Dacă mor suficienți neuroni, rețeaua nu mai poate învăța. Verificare: afișați fracția activărilor care sunt exact 0 după fiecare strat ReLU. Dacă peste 50% sunt inactive, treceți la LeakyReLU sau reduceți rata de învățare.
Notă tehnică a traducerii: O singură intrare negativă nu face un neuron ReLU „mort pentru totdeauna”, așa cum sugerează formularea originală. Diagnosticul se referă la un neuron care rămâne în regiunea nepozitivă pentru toate datele relevante și, prin urmare, nu primește gradient util. O fracție mare de activări zero într-un singur lot nu dovedește că anumiți neuroni sunt permanent inactivi; verificați fiecare unitate pe mai multe loturi.
Gradienți care dispar. În rețelele profunde cu activări sigmoidă sau tanh, gradienții se micșorează exponențial pe măsură ce se propagă înapoi. Până ajung la primul strat, sunt aproximativ 0. Primele straturi încetează să învețe. Remediu: folosiți ReLU/GELU, adăugați conexiuni reziduale sau utilizați normalizarea pe lot.
Gradienți care explodează. Problema opusă — gradienții cresc exponențial. Este frecventă în RNN-uri și în rețele foarte profunde. Pierderea sare la NaN. Remediu: aplicați limitarea gradienților (torch.nn.utils.clip_grad_norm_), reduceți rata de învățare sau adăugați normalizare.
Simptomul 2: pierderea scade, dar modelul este slab
Pierderea scade. Acuratețea la antrenare ajunge la 99%. Acuratețea la testare este însă 55%. Sau modelul produce rezultate fără sens pe date reale.
Supraînvățare. Modelul memorează datele de antrenare în loc să învețe tipare. Diferența dintre pierderea de antrenare și cea de validare crește în timp. Remediu: mai multe date, dropout, penalizarea ponderilor, oprire timpurie, augmentarea datelor.
Scurgere de date. Informații din datele de testare au pătruns în antrenare. Acuratețea este suspect de mare. Cauze frecvente: împărțirea care ignoră grupurile sau ordinea temporală, preprocesarea ajustată cu statistici din întregul set de date și exemplele duplicate între partiții. Remediu: împărțiți întâi, preprocesați apoi și verificați duplicatele.
Notă tehnică a traducerii: Originalul enumeră „amestecarea înainte de împărțire” drept cauză a scurgerii. Amestecarea urmată de o împărțire disjunctă nu provoacă singură scurgere; riscul apare când procedura de împărțire încalcă independența necesară sau când informații din afara partiției de antrenare sunt folosite la ajustarea preprocesării ori a modelului.
Erori de etichetare. Originalul afirmă că 5–10% dintre etichetele din majoritatea seturilor reale de date sunt greșite și îl citează pe Northcutt și colaboratorii (2021), „Pervasive Label Errors in Test Sets”. Modelul poate învăța zgomotul. Remediu: folosiți confident learning pentru a găsi și corecta exemplele etichetate greșit sau trunchierea pierderii pentru a ignora exemplele cu pierdere mare.
Notă tehnică a traducerii: Lucrarea citată estimează în medie cel puțin 3,3% erori în cele zece seturi de testare studiate și cel puțin 6% în setul de validare ImageNet. Ea nu demonstrează o rată universală de 5–10% pentru majoritatea seturilor reale de date.
Simptomul 3: NaN sau Inf în pierdere
Valoarea pierderii devine nan sau inf. Antrenarea nu mai poate continua.
Rată de învățare prea mare. Actualizările gradientului depășesc atât de mult ținta încât ponderile explodează. Remediu: reduceți rata de zece ori.
log(0) sau log dintr-o valoare negativă. O implementare directă a entropiei încrucișate calculează log(p). Dacă primește o probabilitate exact egală cu 0 sau o valoare negativă, rezultatul devine nedefinit. Pentru o implementare proprie bazată pe probabilități, puteți limita valorile la [eps, 1-eps], unde eps=1e-7.
Notă tehnică a traducerii: În PyTorch,
CrossEntropyLossprimește logituri și combină intern operațiile stabileLogSoftmaxșiNLLLoss; nu limitați logiturile ca și cum ar fi probabilități. Țintele pot fi indici de clasă de tipLongsau distribuții soft în virgulă mobilă cu aceeași formă ca logiturile. Pentru clasificarea binară,BCEWithLogitsLosseste alternativa stabilă numeric la aplicarea separată a sigmoidei și a BCE.
Împărțire la zero. Normalizarea pe lot împarte la abaterea standard. Un lot cu valori constante are abaterea standard 0. Remediu: adăugați epsilon la numitor (PyTorch face aceasta implicit, dar implementările proprii s-ar putea să nu o facă).
Overflow numeric. Activările mari furnizate funcției exp() produc Inf. Softmax este deosebit de vulnerabilă. Remediu: scădeți maximul înainte de exponențiere (trucul log-sum-exp).
Tehnica 1: verificarea gradienților
Comparați gradienții analitici (din retropropagare) cu gradienții numerici (din diferențe finite). Dacă nu coincid, propagarea înapoi conține o eroare.
Gradientul numeric pentru parametrul w:
grad_numerical = (loss(w + eps) - loss(w - eps)) / (2 * eps)
Metrica de concordanță (diferența relativă):
rel_diff = |grad_analytical - grad_numerical| / max(|grad_analytical|, |grad_numerical|, 1e-8)
Dacă rel_diff < 1e-5: corect. Dacă rel_diff > 1e-3: aproape sigur există o eroare.
Notă tehnică a traducerii: Aceste praguri sunt euristice, nu universale. Rezultatul verificării depinde de precizia numerică, pasul
eps, scara valorilor, nedeterminism și punctele nediferențiabile; instrumentul oficialtorch.autograd.gradcheckfolosește toleranțe configurabile și recomandă implicit intrări în dublă precizie.
Tehnica 2: statisticile activărilor
Monitorizați media și abaterea standard ale activărilor după fiecare strat în timpul antrenării. Rețelele sănătoase păstrează activările cu media aproape de 0 și abaterea standard aproape de 1 (după normalizare) sau cel puțin în limite controlate.
| Indicator de sănătate | Medie | Abatere standard | Diagnostic |
|---|---|---|---|
| Sănătoasă | ~0 | ~1 | Rețeaua învață normal |
| Saturată | >>0 sau <<0 | ~0 | Activările sunt blocate la valori extreme |
| Inactivă | 0 | 0 | Neuronii sunt inactivi (toate valorile sunt zero) |
| Explozivă | >>10 | >>10 | Activările cresc fără limită |
Tehnica 3: vizualizarea fluxului gradienților
Reprezentați grafic magnitudinea medie a gradientului pentru fiecare strat. Într-o rețea sănătoasă, magnitudinile gradienților ar trebui să fie aproximativ similare între straturi. Dacă primele straturi au gradienți de 1.000 de ori mai mici decât ultimele, aveți gradienți care dispar.
Tehnica 4: testul de supraînvățare a unui singur lot
Aceasta este cea mai importantă tehnică de depanare din învățarea profundă.
Luați un singur lot mic (8–32 de exemple). Antrenați modelul pe el timp de peste 100 de iterații. Pierderea ar trebui să ajungă aproape de zero, iar acuratețea la antrenare ar trebui să atingă 100%. Dacă nu se întâmplă astfel, modelul sau bucla de antrenare are o eroare fundamentală — nu treceți la antrenarea completă.
Acest test detectează:
- Funcții de pierdere defecte
- Propagări înapoi defecte
- O arhitectură prea mică pentru a reprezenta datele
- Un optimizator neconectat la parametrii modelului
- Date și etichete nealiniate
Testul rulează în 30 de secunde și economisește ore de depanare a antrenărilor complete.
Tehnica 5: găsirea ratei de învățare
Leslie Smith (2017) a propus parcurgerea ratelor de învățare de la o valoare foarte mică (1e-7) la una foarte mare (10) în cursul unei epoci, înregistrând pierderea. Reprezentați grafic pierderea în funcție de rata de învățare. Rata optimă este aproximativ de zece ori mai mică decât rata la care pierderea începe să scadă cel mai rapid.
Cea mai bună rată de învățare din acest exemplu: aproximativ 1e-3 (cu un ordin de mărime înaintea punctului cu scăderea cea mai abruptă).
Notă tehnică a traducerii: Lucrarea lui Smith descrie creșterea liniară a ratei de învățare timp de câteva epoci pentru a estima limite rezonabile. Alegerea unei rate de zece ori mai mici decât punctul scăderii celei mai abrupte, într-o singură epocă, este o euristică practică, nu o regulă universală demonstrată de lucrare.
Erori PyTorch frecvente
Acestea sunt erorile care consumă colectiv cele mai multe ore în comunitatea PyTorch:
| Eroare | Simptom | Remediu |
|---|---|---|
Omiterea optimizer.zero_grad() |
Gradienții se acumulează între loturi, pierderea oscilează | Adăugați optimizer.zero_grad() înainte de loss.backward() |
Omiterea model.eval() la testare |
Dropout și normalizarea pe lot se comportă diferit, acuratețea la testare variază între rulări | Adăugați model.eval() și torch.no_grad() |
| Forme greșite ale tensorilor | Broadcasting-ul silențios produce rezultate greșite, fără eroare | Afișați formele după fiecare operație în timpul depanării |
| Nepotrivire CPU/GPU | RuntimeError: expected CUDA tensor |
Folosiți .to(device) atât pentru model, cât ȘI pentru date |
| Tensori nedetașați | Graful de calcul crește continuu, OOM | Folosiți .detach() sau with torch.no_grad() |
| Operații in-place care perturbă autograd | RuntimeError: modified by in-place operation |
Înlocuiți x += 1 cu x = x + 1 |
| Date nenormalizate | Pierderea rămâne la nivelul alegerii aleatoare | Normalizați intrările la media 0 și abaterea standard 1 |
| Etichete cu tip de date greșit | Entropia încrucișată așteaptă Long, dar primește Float |
Convertiți etichetele: labels.long() |
Tabelul principal de depanare
| Simptom | Cauză probabilă | Primul lucru de încercat |
|---|---|---|
Pierderea rămâne la -log(1/num_classes) |
Modelul prezice o distribuție uniformă | Verificați pipeline-ul de date și dacă etichetele corespund intrărilor |
| Pierderea devine NaN după câțiva pași | Rata de învățare este prea mare | Reduceți rata de învățare de zece ori |
| Pierderea devine NaN imediat | log(0) sau împărțire la zero |
Adăugați epsilon la operațiile de logaritmare/împărțire |
| Pierderea oscilează puternic | Rata de învățare este prea mare sau lotul este prea mic | Reduceți rata de învățare, măriți lotul |
| Pierderea scade, apoi ajunge pe un platou | Rata de învățare este prea mare pentru etapa de reglare fină | Adăugați un program pentru rata de învățare (amortizare cosinusoidală sau în trepte) |
| Acuratețe mare la antrenare, mică la testare | Supraînvățare | Adăugați dropout, penalizarea ponderilor, mai multe date |
| Acuratețe la antrenare = acuratețe la testare = nivel aleator | Modelul nu învață nimic | Rulați testul de supraînvățare a unui singur lot |
| Acuratețe la antrenare = acuratețe la testare, dar ambele sunt mici | Subînvățare | Model mai mare, mai multe straturi, mai multe caracteristici |
| Toți gradienții sunt zero | ReLU inactive sau graf de calcul detașat | Treceți la LeakyReLU, verificați .requires_grad |
| Memorie insuficientă în timpul antrenării | Lot prea mare sau graf neeliberat | Reduceți lotul, folosiți torch.no_grad() la evaluare |
learning-curves
Construiți-l
Construiți un set de instrumente de diagnosticare care monitorizează activările, gradienții și curbele pierderii. Veți defecta intenționat o rețea și veți folosi instrumentele pentru a diagnostica fiecare problemă.
Pasul 1: clasa NetworkDebugger
Atașați hook-uri unui model PyTorch pentru a înregistra statisticile activărilor și gradienților pentru fiecare strat.
import torch
import torch.nn as nn
import math
class NetworkDebugger:
def __init__(self, model):
self.model = model
self.activation_stats = {}
self.gradient_stats = {}
self.loss_history = []
self.lr_losses = []
self.hooks = []
self._register_hooks()
def _register_hooks(self):
for name, module in self.model.named_modules():
if isinstance(module, (nn.Linear, nn.Conv2d, nn.ReLU, nn.LeakyReLU)):
hook = module.register_forward_hook(self._make_activation_hook(name))
self.hooks.append(hook)
hook = module.register_full_backward_hook(self._make_gradient_hook(name))
self.hooks.append(hook)
def _make_activation_hook(self, name):
def hook(module, input, output):
with torch.no_grad():
out = output.detach().float()
self.activation_stats[name] = {
"mean": out.mean().item(),
"std": out.std().item(),
"fraction_zero": (out == 0).float().mean().item(),
"min": out.min().item(),
"max": out.max().item(),
}
return hook
def _make_gradient_hook(self, name):
def hook(module, grad_input, grad_output):
if grad_output[0] is not None:
with torch.no_grad():
grad = grad_output[0].detach().float()
self.gradient_stats[name] = {
"mean": grad.mean().item(),
"std": grad.std().item(),
"abs_mean": grad.abs().mean().item(),
"max": grad.abs().max().item(),
}
return hook
def record_loss(self, loss_value):
self.loss_history.append(loss_value)
def check_loss_health(self):
if len(self.loss_history) < 2:
return "NOT_ENOUGH_DATA"
recent = self.loss_history[-10:]
if any(math.isnan(v) or math.isinf(v) for v in recent):
return "NAN_OR_INF"
if len(self.loss_history) >= 20:
first_half = sum(self.loss_history[:10]) / 10
second_half = sum(self.loss_history[-10:]) / 10
if second_half >= first_half * 0.99:
return "NOT_DECREASING"
if len(recent) >= 5:
diffs = [recent[i+1] - recent[i] for i in range(len(recent)-1)]
if max(diffs) - min(diffs) > 2 * abs(sum(diffs) / len(diffs)):
return "OSCILLATING"
return "HEALTHY"
def check_activations(self):
issues = []
for name, stats in self.activation_stats.items():
if stats["fraction_zero"] > 0.5:
issues.append(f"DEAD_NEURONS: {name} has {stats['fraction_zero']:.0%} zero activations")
if abs(stats["mean"]) > 10:
issues.append(f"EXPLODING_ACTIVATIONS: {name} mean={stats['mean']:.2f}")
if stats["std"] < 1e-6:
issues.append(f"COLLAPSED_ACTIVATIONS: {name} std={stats['std']:.2e}")
return issues if issues else ["HEALTHY"]
def check_gradients(self):
issues = []
grad_magnitudes = []
for name, stats in self.gradient_stats.items():
grad_magnitudes.append((name, stats["abs_mean"]))
if stats["abs_mean"] < 1e-7:
issues.append(f"VANISHING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if stats["abs_mean"] > 100:
issues.append(f"EXPLODING_GRADIENT: {name} abs_mean={stats['abs_mean']:.2e}")
if len(grad_magnitudes) >= 2:
first_mag = grad_magnitudes[0][1]
last_mag = grad_magnitudes[-1][1]
if last_mag > 0 and first_mag / last_mag > 100:
issues.append(f"GRADIENT_RATIO: first/last = {first_mag/last_mag:.0f}x (vanishing)")
return issues if issues else ["HEALTHY"]
def print_report(self):
print("\n=== NETWORK DEBUGGER REPORT ===")
print(f"\nLoss health: {self.check_loss_health()}")
if self.loss_history:
print(f" Last 5 losses: {[f'{v:.4f}' for v in self.loss_history[-5:]]}")
print("\nActivation diagnostics:")
for item in self.check_activations():
print(f" {item}")
print("\nGradient diagnostics:")
for item in self.check_gradients():
print(f" {item}")
print("\nPer-layer activation stats:")
for name, stats in self.activation_stats.items():
print(f" {name}: mean={stats['mean']:.4f} std={stats['std']:.4f} zero={stats['fraction_zero']:.1%}")
print("\nPer-layer gradient stats:")
for name, stats in self.gradient_stats.items():
print(f" {name}: abs_mean={stats['abs_mean']:.2e} max={stats['max']:.2e}")
def remove_hooks(self):
for hook in self.hooks:
hook.remove()
self.hooks.clear()
Notă tehnică a traducerii: Codul este păstrat identic cu originalul. În demonstrația secvențială, hook-urile backward tind să populeze dicționarul dinspre ieșire spre intrare, astfel încât raportul
first_mag / last_mag > 100poate avea sensul intenționat. Totuși, diagnosticul se bazează pe ordinea de executare și inserare a hook-urilor, nu pe o asociere explicită cu primul și ultimul strat al arhitecturii, deci nu este robust pentru grafuri ramificate sau pentru alte ordini de execuție.
Pasul 2: testul de supraînvățare a unui singur lot
def overfit_one_batch(model, x_batch, y_batch, criterion, lr=0.01, steps=200):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
model.train()
print("\n=== OVERFIT ONE BATCH TEST ===")
print(f"Batch size: {x_batch.shape[0]}, Steps: {steps}")
for step in range(steps):
optimizer.zero_grad()
output = model(x_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
if step % 50 == 0 or step == steps - 1:
with torch.no_grad():
preds = (output > 0).float() if output.shape[-1] == 1 else output.argmax(dim=1)
targets = y_batch if y_batch.dim() == 1 else y_batch.squeeze()
acc = (preds.squeeze() == targets).float().mean().item()
print(f" Step {step:3d} | Loss: {loss.item():.6f} | Accuracy: {acc:.1%}")
final_loss = loss.item()
if final_loss > 0.1:
print(f"\n FAIL: Loss did not converge ({final_loss:.4f}). Model or training loop is broken.")
return False
print(f"\n PASS: Loss converged to {final_loss:.6f}")
return True
Pasul 3: găsirea ratei de învățare
def find_learning_rate(model, x_data, y_data, criterion, start_lr=1e-7, end_lr=10, steps=100):
import copy
original_state = copy.deepcopy(model.state_dict())
optimizer = torch.optim.SGD(model.parameters(), lr=start_lr)
lr_mult = (end_lr / start_lr) ** (1 / steps)
model.train()
results = []
best_loss = float("inf")
current_lr = start_lr
print("\n=== LEARNING RATE FINDER ===")
for step in range(steps):
optimizer.zero_grad()
output = model(x_data)
loss = criterion(output, y_data)
if math.isnan(loss.item()) or loss.item() > best_loss * 10:
break
best_loss = min(best_loss, loss.item())
results.append((current_lr, loss.item()))
loss.backward()
optimizer.step()
current_lr *= lr_mult
for param_group in optimizer.param_groups:
param_group["lr"] = current_lr
model.load_state_dict(original_state)
if len(results) < 10:
print(" Could not complete LR sweep -- loss diverged too quickly")
return results
min_loss_idx = min(range(len(results)), key=lambda i: results[i][1])
suggested_lr = results[max(0, min_loss_idx - 10)][0]
print(f" Swept {len(results)} steps from {start_lr:.0e} to {results[-1][0]:.0e}")
print(f" Minimum loss {results[min_loss_idx][1]:.4f} at lr={results[min_loss_idx][0]:.2e}")
print(f" Suggested learning rate: {suggested_lr:.2e}")
return results
Pasul 4: verificatorul de gradienți
def _flat_to_multi_index(flat_idx, shape):
multi_idx = []
remaining = flat_idx
for dim in reversed(shape):
multi_idx.insert(0, remaining % dim)
remaining //= dim
return tuple(multi_idx)
def gradient_check(model, x, y, criterion, eps=1e-4):
model.train()
x_double = x.double()
y_double = y.double()
model_double = model.double()
print("\n=== GRADIENT CHECK ===")
overall_max_diff = 0
checked = 0
for name, param in model_double.named_parameters():
if not param.requires_grad:
continue
layer_max_diff = 0
model_double.zero_grad()
output = model_double(x_double)
loss = criterion(output, y_double)
loss.backward()
analytical_grad = param.grad.clone()
num_checks = min(5, param.numel())
for i in range(num_checks):
idx = _flat_to_multi_index(i, param.shape)
original = param.data[idx].item()
param.data[idx] = original + eps
with torch.no_grad():
loss_plus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original - eps
with torch.no_grad():
loss_minus = criterion(model_double(x_double), y_double).item()
param.data[idx] = original
numerical = (loss_plus - loss_minus) / (2 * eps)
analytical = analytical_grad[idx].item()
denom = max(abs(numerical), abs(analytical), 1e-8)
rel_diff = abs(numerical - analytical) / denom
layer_max_diff = max(layer_max_diff, rel_diff)
checked += 1
overall_max_diff = max(overall_max_diff, layer_max_diff)
status = "OK" if layer_max_diff < 1e-5 else "MISMATCH"
print(f" {name}: max_rel_diff={layer_max_diff:.2e} [{status}]")
model.float()
print(f"\n Checked {checked} parameters")
if overall_max_diff < 1e-5:
print(" PASS: Gradients match (rel_diff < 1e-5)")
elif overall_max_diff < 1e-3:
print(" WARN: Small differences (1e-5 < rel_diff < 1e-3)")
else:
print(" FAIL: Gradient mismatch detected (rel_diff > 1e-3)")
return overall_max_diff
Notă tehnică a traducerii: Apelul demonstrativ de mai jos folosește
CrossEntropyLoss, care cere ținte cu indici de clasă de tipLong. Conversia necondiționatăy_double = y.double()din codul original provoacă o eroare de tip în acest caz. PăstrațiycaLongpentru entropia încrucișată și convertiți țintele ladoublenumai pentru criterii care necesită ținte în virgulă mobilă.
Pasul 5: rețele defectate intenționat
Aplicați acum setul de instrumente unor rețele defecte și diagnosticați fiecare problemă.
def demo_broken_networks():
torch.manual_seed(42)
x = torch.randn(64, 10)
y = (x[:, 0] > 0).long()
print("\n" + "=" * 60)
print("BUG 1: Learning rate too high (lr=10)")
print("=" * 60)
model1 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger1 = NetworkDebugger(model1)
optimizer1 = torch.optim.SGD(model1.parameters(), lr=10.0)
criterion = nn.CrossEntropyLoss()
for step in range(20):
optimizer1.zero_grad()
out = model1(x)
loss = criterion(out, y)
debugger1.record_loss(loss.item())
loss.backward()
optimizer1.step()
debugger1.print_report()
debugger1.remove_hooks()
print("\n" + "=" * 60)
print("BUG 2: Dead ReLUs from bad initialization")
print("=" * 60)
model2 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2))
with torch.no_grad():
for m in model2.modules():
if isinstance(m, nn.Linear):
m.weight.fill_(-1.0)
m.bias.fill_(-5.0)
debugger2 = NetworkDebugger(model2)
optimizer2 = torch.optim.Adam(model2.parameters(), lr=1e-3)
for step in range(50):
optimizer2.zero_grad()
out = model2(x)
loss = criterion(out, y)
debugger2.record_loss(loss.item())
loss.backward()
optimizer2.step()
debugger2.print_report()
debugger2.remove_hooks()
print("\n" + "=" * 60)
print("BUG 3: Missing zero_grad (gradients accumulate)")
print("=" * 60)
model3 = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger3 = NetworkDebugger(model3)
optimizer3 = torch.optim.SGD(model3.parameters(), lr=0.01)
for step in range(50):
out = model3(x)
loss = criterion(out, y)
debugger3.record_loss(loss.item())
loss.backward()
optimizer3.step()
debugger3.print_report()
debugger3.remove_hooks()
print("\n" + "=" * 60)
print("HEALTHY NETWORK: Correct setup for comparison")
print("=" * 60)
model_good = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
debugger_good = NetworkDebugger(model_good)
optimizer_good = torch.optim.Adam(model_good.parameters(), lr=1e-3)
for step in range(50):
optimizer_good.zero_grad()
out = model_good(x)
loss = criterion(out, y)
debugger_good.record_loss(loss.item())
loss.backward()
optimizer_good.step()
debugger_good.print_report()
debugger_good.remove_hooks()
print("\n" + "=" * 60)
print("OVERFIT-ONE-BATCH TEST (healthy model)")
print("=" * 60)
model_test = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
overfit_one_batch(model_test, x[:8], y[:8], criterion)
print("\n" + "=" * 60)
print("LEARNING RATE FINDER")
print("=" * 60)
model_lr = nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 2))
find_learning_rate(model_lr, x, y, criterion)
print("\n" + "=" * 60)
print("GRADIENT CHECK")
print("=" * 60)
model_grad = nn.Sequential(nn.Linear(10, 8), nn.ReLU(), nn.Linear(8, 2))
gradient_check(model_grad, x[:4], y[:4], criterion)
Utilizați-l
Instrumente integrate în PyTorch
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(768, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
with torch.autograd.detect_anomaly():
output = model(input_tensor)
loss = criterion(output, target)
loss.backward()
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: grad_mean={param.grad.abs().mean():.2e}")
Notă tehnică a traducerii:
torch.autograd.detect_anomaly(check_nan=True)păstrează informații din propagarea înainte pentru a raporta operația care a provocat o defecțiune în propagarea înapoi și semnalează valorile NaN generate în calculul backward. Nu este un detector general pentru toate valorile Inf și trebuie activat numai la depanare, deoarece încetinește execuția.
Integrarea Weights & Biases
import wandb
wandb.init(project="debug-training")
for epoch in range(100):
loss = train_one_epoch()
wandb.log({
"loss": loss,
"lr": optimizer.param_groups[0]["lr"],
"grad_norm": torch.nn.utils.clip_grad_norm_(model.parameters(), float("inf")),
})
for name, param in model.named_parameters():
if param.grad is not None:
wandb.log({f"grad/{name}": wandb.Histogram(param.grad.cpu().numpy())})
TensorBoard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/debug_experiment")
for epoch in range(100):
loss = train_one_epoch()
writer.add_scalar("Loss/train", loss, epoch)
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, epoch)
if param.grad is not None:
writer.add_histogram(f"gradients/{name}", param.grad, epoch)
Lista de verificare pentru depanare (înainte de antrenarea completă)
- Rulați testul de supraînvățare a unui singur lot. Dacă eșuează, opriți-vă.
- Afișați rezumatul modelului — verificați dacă numărul parametrilor este rezonabil.
- Rulați o singură propagare înainte cu date aleatoare — verificați forma ieșirii.
- Antrenați timp de cinci epoci — verificați dacă pierderea scade.
- Verificați statisticile activărilor — fără straturi inactive, fără explozii.
- Verificați fluxul gradienților — fără dispariții, fără explozii.
- Verificați pipeline-ul de date — afișați cinci exemple aleatoare împreună cu etichetele lor.
Livrați-l
Această lecție produce:
outputs/prompt-nn-debugger.md— un prompt pentru diagnosticarea defecțiunilor din antrenarea rețelelor neuronaleoutputs/skill-debug-checklist.md— o listă de verificare sub formă de arbore decizional pentru depanarea problemelor de antrenare
Tipare esențiale de implementare pentru depanare:
- Adăugați hook-uri de monitorizare în scripturile de antrenare pentru producție
- Înregistrați statisticile activărilor și gradienților în W&B sau TensorBoard la fiecare N pași
- Implementați alerte automate pentru pierdere NaN, neuroni inactivi (peste 80% valori zero) sau explozia gradienților
- Rulați întotdeauna testul de supraînvățare a unui singur lot când schimbați arhitecturile sau pipeline-urile de date
Exerciții
-
Adăugați un detector de gradienți care explodează. Modificați
NetworkDebuggerpentru a detecta când gradienții depășesc un prag și a sugera automat o valoare pentru limitarea lor. Testați-l pe o rețea cu 20 de straturi, fără normalizare. -
Construiți un mecanism de reactivare a neuronilor inactivi. Scrieți o funcție care identifică neuronii ReLU inactivi (produc întotdeauna 0) și le reinițializează ponderile de intrare cu inițializarea Kaiming. Arătați că astfel se recuperează o rețea în care peste 70% dintre neuroni sunt inactivi.
-
Implementați găsirea ratei de învățare cu reprezentare grafică. Extindeți
find_learning_ratepentru a salva rezultatele într-un fișier CSV și scrieți un script separat care citește fișierul CSV și afișează curba ratei de învățare în funcție de pierdere folosind matplotlib. Identificați rata optimă pentru ResNet-18 pe CIFAR-10. -
Creați un validator pentru pipeline-ul de date. Scrieți o funcție care verifică: exemple duplicate între partițiile de antrenare și testare, dezechilibrul distribuției etichetelor (raport peste 10:1), normalizarea intrărilor (media aproape de 0, abaterea standard aproape de 1) și valori NaN/Inf în date. Rulați-o pe un set de date corupt intenționat.
-
Depanați o defecțiune reală. Luați mini-framework-ul din Lecția 10, introduceți o eroare subtilă (de exemplu, transpuneți matricea ponderilor în propagarea înapoi) și folosiți verificarea gradienților pentru a localiza exact parametrul cu gradienți incorecți. Documentați procesul de depanare.
Termeni-cheie
| Termen | Ce spun oamenii | Ce înseamnă de fapt |
|---|---|---|
| Eroare silențioasă | „Rulează, dar dă rezultate slabe” | O eroare care nu produce un mesaj, dar degradează calitatea modelului — un mod de defecțiune dominant în ML |
| ReLU inactivă | „Neuronii au murit” | Un neuron ReLU a cărui intrare este mereu negativă, astfel încât produce 0 și primește permanent gradient 0 |
| Gradienți care dispar | „Primele straturi încetează să învețe” | Gradienții se micșorează exponențial prin straturi, făcând ponderile primelor straturi practic înghețate |
| Gradienți care explodează | „Pierderea a devenit NaN” | Gradienții cresc exponențial prin straturi, provocând actualizări ale ponderilor atât de mari încât produc overflow |
| Verificarea gradienților | „Verificați dacă retropropagarea este corectă” | Compararea gradienților analitici din retropropagare cu gradienții numerici din diferențe finite |
| Supraînvățarea unui singur lot | „Cel mai important test de depanare” | Antrenarea pe un singur lot mic pentru a verifica dacă modelul POATE învăța — dacă nu poate, există o problemă fundamentală |
| Instrument de găsire a ratei de învățare | „Parcurgeți valori pentru a găsi rata potrivită” | Creșterea exponențială a ratei de învățare în cursul unei epoci și alegerea ratei dinaintea divergenței pierderii |
| Scurgere de date | „Datele de testare au pătruns în antrenare” | Situația în care informații din setul de testare contaminează antrenarea și produc o acuratețe artificial de mare |
| Statisticile activărilor | „Monitorizați sănătatea straturilor” | Urmărirea mediei, abaterii standard și fracției de valori zero din ieșirea fiecărui strat pentru a detecta neuroni inactivi, saturați sau explozivi |
| Limitarea gradienților | „Plafonați magnitudinea gradientului” | Scalarea gradienților în jos când norma lor depășește un prag, prevenind actualizările provocate de gradienți care explodează |
Lecturi suplimentare
- Smith, „Cyclical Learning Rates for Training Neural Networks” (2017) — lucrarea care introduce testul intervalului ratei de învățare (instrumentul de găsire a ratei de învățare)
- Northcutt și colaboratorii, „Pervasive Label Errors in Test Sets Destabilize Machine Learning Benchmarks” (2021) — arată că erorile de etichetare sunt răspândite în zece seturi de referință și estimează o medie de cel puțin 3,3% în seturile de testare studiate
- Zhang și colaboratorii, „Understanding Deep Learning Requires Rethinking Generalization” (2017) — lucrarea care arată că rețelele neuronale pot memora etichete aleatoare, motiv pentru care funcționează testul de supraînvățare a unui singur lot
- Documentația PyTorch pentru
torch.autograd.detect_anomalyșitorch.autograd.set_detect_anomaly, instrumente integrate care urmăresc operația din propagarea înainte asociată unei defecțiuni în propagarea înapoi și, implicit, semnalează valorile NaN generate în calculul backward
Sursă: Originalul în limba engleză
Navigare: ← Lecția 03.12 — Introducere în JAX · Faza 3 — Fundamentele învățării profunde · Catalog complet