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ă.

Curs
AI Engineering from Scratch
Fază
Fundamentele învățării profunde
Lectură
32 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Mentalitatea de depanare
  5. Simptomul 1: pierderea nu scade
  6. Simptomul 2: pierderea scade, dar modelul este slab
  7. Simptomul 3: NaN sau Inf în pierdere
  8. Tehnica 1: verificarea gradienților
  9. Tehnica 2: statisticile activărilor
  10. Tehnica 3: vizualizarea fluxului gradienților
  11. Tehnica 4: testul de supraînvățare a unui singur lot
  12. Tehnica 5: găsirea ratei de învățare
  13. Erori PyTorch frecvente
  14. Tabelul principal de depanare
  15. Construiți-l
  16. Pasul 1: clasa NetworkDebugger
  17. Pasul 2: testul de supraînvățare a unui singur lot
  18. Pasul 3: găsirea ratei de învățare
  19. Pasul 4: verificatorul de gradienți
  20. Pasul 5: rețele defectate intenționat
  21. Utilizați-l
  22. Instrumente integrate în PyTorch
  23. Integrarea Weights & Biases
  24. TensorBoard
  25. Lista de verificare pentru depanare (înainte de antrenarea completă)
  26. Livrați-l
  27. Exerciții
  28. Termeni-cheie
  29. 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.

Диаграмма к уроку «Depanarea rețelelor neuronale»

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, CrossEntropyLoss primește logituri și combină intern operațiile stabile LogSoftmax și NLLLoss; nu limitați logiturile ca și cum ar fi probabilități. Țintele pot fi indici de clasă de tip Long sau distribuții soft în virgulă mobilă cu aceeași formă ca logiturile. Pentru clasificarea binară, BCEWithLogitsLoss este 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 oficial torch.autograd.gradcheck folosește toleranțe configurabile și recomandă implicit intrări în dublă precizie.

Диаграмма к уроку «Depanarea rețelelor neuronale»

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.

Диаграмма к уроку «Depanarea rețelelor neuronale»

Диаграмма к уроку «Depanarea rețelelor neuronale»

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.

Диаграмма к уроку «Depanarea rețelelor neuronale»

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 > 100 poate 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 tip Long. Conversia necondiționată y_double = y.double() din codul original provoacă o eroare de tip în acest caz. Păstrați y ca Long pentru entropia încrucișată și convertiți țintele la double numai 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ă)

  1. Rulați testul de supraînvățare a unui singur lot. Dacă eșuează, opriți-vă.
  2. Afișați rezumatul modelului — verificați dacă numărul parametrilor este rezonabil.
  3. Rulați o singură propagare înainte cu date aleatoare — verificați forma ieșirii.
  4. Antrenați timp de cinci epoci — verificați dacă pierderea scade.
  5. Verificați statisticile activărilor — fără straturi inactive, fără explozii.
  6. Verificați fluxul gradienților — fără dispariții, fără explozii.
  7. 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 neuronale
  • outputs/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

  1. Adăugați un detector de gradienți care explodează. Modificați NetworkDebugger pentru 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.

  2. 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.

  3. Implementați găsirea ratei de învățare cu reprezentare grafică. Extindeți find_learning_rate pentru 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.

  4. 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.

  5. 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 și torch.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