Faza 00 · lecția 12

Depanare și profilare

Scopul lecției: Cele mai grave erori din sistemele de IA nu provoacă oprirea programului. Antrenarea continuă în tăcere pe date eronate și raportează o curbă impecabilă a pierderii.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Configurare și instrumente
Lectură
12 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Implementare
  5. Partea 1: Depanarea cu print (da, funcționează)
  6. Partea 2: Depanatorul Python (pdb și breakpoint)
  7. Partea 3: Jurnalizarea în Python
  8. Partea 4: Cronometrarea secțiunilor de cod
  9. Partea 5: cProfile și line_profiler
  10. Partea 6: Profilarea memoriei
  11. Partea 7: Erori frecvente în sistemele de IA și metode de detectare
  12. Partea 8: Noțiuni de bază despre TensorBoard
  13. Partea 9: Depanatorul VS Code
  14. Utilizare
  15. Livrarea rezultatului
  16. Exerciții

Cele mai grave erori din sistemele de IA nu provoacă oprirea programului. Antrenarea continuă în tăcere pe date eronate și raportează o curbă impecabilă a pierderii.

Tip: Construire Limbaj: Python Cerințe preliminare: Lecția 1 (Mediul de dezvoltare), cunoștințe de bază despre PyTorch Durată: aproximativ 60 de minute

Obiective de învățare

  • Folosiți puncte de întrerupere condiționale (breakpoints) prin breakpoint() și funcția debug_print pentru a examina formele tensorilor, tipurile de date și valorile NaN în timpul antrenării
  • Profilați buclele de antrenare cu cProfile, line_profiler și tracemalloc pentru a găsi sursele încetinirilor
  • Detectați erorile frecvente din sistemele de IA: nepotriviri între forme, pierdere cu valoarea NaN, scurgere de date (data leakage) și tensori aflați pe dispozitivul greșit
  • Configurați TensorBoard pentru a vizualiza curbele pierderii, histogramele ponderilor și distribuțiile gradienților

Problema

Codul pentru IA eșuează altfel decât codul obișnuit. O aplicație web se oprește și afișează trasarea stivei (stack trace). O buclă de antrenare configurată greșit rulează timp de 8 ore, consumă resurse GPU în valoare de $200 și produce un model care prezice aceeași valoare medie pentru fiecare intrare. Codul nu a raportat nicio eroare. Problema a fost un tensor aflat pe dispozitivul greșit, un apel .detach() omis sau etichete care au ajuns în caracteristici.

Aveți nevoie de instrumente de depanare care să detecteze aceste erori tăcute înainte să vă irosească timpul și resursele de calcul.

Conceptul

Depanarea codului pentru IA se desfășoară pe trei niveluri:

Диаграмма к уроку «Depanare și profilare»

Majoritatea persoanelor trec direct la nivelul 3 și privesc graficele din TensorBoard. Însă 80% dintre erorile din sistemele de IA se află la nivelurile 1 și 2.

Implementare

Partea 1: Depanarea cu print (da, funcționează)

Depanarea cu instrucțiuni print este adesea desconsiderată, deși nu ar trebui. În codul care lucrează cu tensori, o instrucțiune print bine aleasă este mai eficientă decât parcurgerea pas cu pas într-un depanator, deoarece trebuie să vedeți simultan formele, tipurile de date și intervalele valorilor.

def debug_print(name, tensor):
    print(f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, "
          f"device={tensor.device}, "
          f"min={tensor.min().item():.4f}, max={tensor.max().item():.4f}, "
          f"mean={tensor.mean().item():.4f}, "
          f"has_nan={tensor.isnan().any().item()}")

Apelați această funcție după fiecare operație suspectă. După ce găsiți eroarea, eliminați instrucțiunile print. Simplu.

Partea 2: Depanatorul Python (pdb și breakpoint)

Depanatorul încorporat este subapreciat în lucrul cu IA. Introduceți breakpoint() în bucla de antrenare și examinați interactiv tensorii.

def training_step(model, batch, criterion, optimizer):
    inputs, labels = batch
    outputs = model(inputs)
    loss = criterion(outputs, labels)

    if loss.item() > 100 or torch.isnan(loss):
        breakpoint()

    loss.backward()
    optimizer.step()

Când depanatorul întrerupe execuția, vă sunt utile următoarele comenzi:

  • p outputs.shape pentru a verifica formele
  • p loss.item() pentru a vedea valoarea pierderii
  • p torch.isnan(outputs).sum() pentru a număra valorile NaN
  • p model.fc1.weight.grad pentru a verifica gradienții
  • c pentru a continua, q pentru a ieși

Aceasta este depanare condițională: execuția se oprește numai când ceva pare în neregulă. Pentru o antrenare cu 10.000 de pași, acest lucru contează.

Partea 3: Jurnalizarea în Python

Înlocuiți instrucțiunile print cu jurnalizarea atunci când depanarea depășește o verificare rapidă.

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("training.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

logger.info("Starting training: lr=%.4f, batch_size=%d", lr, batch_size)
logger.warning("Loss spike detected: %.4f at step %d", loss.item(), step)
logger.error("NaN loss at step %d, stopping", step)

Jurnalizarea vă oferă marcaje temporale, niveluri de severitate și scriere în fișier. Când o antrenare eșuează la ora 3 dimineața, aveți nevoie de un fișier jurnal, nu de ieșirea terminalului care a dispărut în urma derulării.

Partea 4: Cronometrarea secțiunilor de cod

Primul pas către optimizare este să aflați unde se consumă timpul.

import time

class Timer:
    def __init__(self, name=""):
        self.name = name

    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, *args):
        elapsed = time.perf_counter() - self.start
        print(f"[{self.name}] {elapsed:.4f}s")

with Timer("data loading"):
    batch = next(dataloader_iter)

with Timer("forward pass"):
    outputs = model(batch)

with Timer("backward pass"):
    loss.backward()

O constatare frecventă este că încărcarea datelor ocupă 60% din timpul de antrenare. Soluția este num_workers > 0 în DataLoader, nu un GPU mai rapid.

Partea 5: cProfile și line_profiler

Când aveți nevoie de mai mult decât cronometre manuale:

python -m cProfile -s cumtime train.py

Comanda afișează fiecare apel de funcție, sortat după timpul cumulat. Pentru profilarea linie cu linie:

pip install line_profiler
@profile
def train_step(model, data, target):
    output = model(data)
    loss = F.cross_entropy(output, target)
    loss.backward()
    return loss

# Run with: kernprof -l -v train.py

Partea 6: Profilarea memoriei

Memoria CPU cu tracemalloc

import tracemalloc

tracemalloc.start()

# your code here
model = build_model()
data = load_dataset()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
    print(stat)

Memoria CPU cu memory_profiler

pip install memory_profiler
from memory_profiler import profile

@profile
def load_data():
    raw = read_csv("data.csv")       # watch memory jump here
    processed = preprocess(raw)       # and here
    return processed

Rulați comanda python -m memory_profiler your_script.py pentru a vedea utilizarea memoriei linie cu linie.

Memoria GPU cu PyTorch

import torch

if torch.cuda.is_available():
    print(torch.cuda.memory_summary())

    print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
    print(f"Cached: {torch.cuda.memory_reserved() / 1e9:.2f} GB")

Când întâmpinați o eroare OOM (Out of Memory — memorie insuficientă):

  1. Reduceți dimensiunea lotului (batch); aceasta este întotdeauna prima soluție de încercat
  2. Folosiți torch.cuda.empty_cache() pentru a elibera memoria din cache
  3. Folosiți del tensor, urmat de torch.cuda.empty_cache(), pentru tensorii intermediari mari
  4. Folosiți precizia mixtă (torch.cuda.amp) pentru a reduce la jumătate memoria utilizată
  5. Folosiți salvarea selectivă a activărilor (gradient checkpointing) în cazul modelelor foarte profunde

Partea 7: Erori frecvente în sistemele de IA și metode de detectare

Nepotrivirea formelor

Aceasta este eroarea cea mai frecventă. Un tensor are forma [batch, features], în timp ce modelul se așteaptă la [batch, channels, height, width].

def check_shapes(model, sample_input):
    print(f"Input: {sample_input.shape}")
    hooks = []

    def make_hook(name):
        def hook(module, inp, out):
            in_shape = inp[0].shape if isinstance(inp, tuple) else inp.shape
            out_shape = out.shape if hasattr(out, "shape") else type(out)
            print(f"  {name}: {in_shape} -> {out_shape}")
        return hook

    for name, module in model.named_modules():
        hooks.append(module.register_forward_hook(make_hook(name)))

    with torch.no_grad():
        model(sample_input)

    for h in hooks:
        h.remove()

Rulați această funcție o dată, cu un lot de exemple. Funcția cartografiază fiecare transformare a formei din model.

Pierdere cu valoarea NaN

O pierdere cu valoarea NaN indică o instabilitate gravă. Cauze frecvente:

  • Rată de învățare prea mare
  • Împărțire la zero în funcția de pierdere personalizată
  • Logaritmul valorii zero sau al unui număr negativ
  • Explozia gradienților în rețelele neuronale recurente (RNN)
def detect_nan(model, loss, step):
    if torch.isnan(loss):
        print(f"NaN loss at step {step}")
        for name, param in model.named_parameters():
            if param.grad is not None:
                if torch.isnan(param.grad).any():
                    print(f"  NaN gradient in {name}")
                if torch.isinf(param.grad).any():
                    print(f"  Inf gradient in {name}")
        return True
    return False

Scurgerea de date (data leakage)

Modelul obține o acuratețe de 99% pe setul de testare. Pare excelent, dar este o eroare.

def check_data_leakage(train_set, test_set, id_column="id"):
    train_ids = set(train_set[id_column].tolist())
    test_ids = set(test_set[id_column].tolist())
    overlap = train_ids & test_ids
    if overlap:
        print(f"DATA LEAKAGE: {len(overlap)} samples in both train and test")
        return True
    return False

Verificați și scurgerea temporală: folosirea datelor viitoare pentru a prezice trecutul. Sortați datele după marcajul temporal înainte de împărțire.

Tensor pe dispozitivul greșit

Tensorii aflați pe dispozitive diferite (CPU și GPU) provoacă erori în timpul execuției. Uneori însă, un tensor rămâne neobservat pe CPU, în timp ce toate celelalte se află pe GPU, iar antrenarea doar rulează lent.

def check_devices(model, *tensors):
    model_device = next(model.parameters()).device
    print(f"Model device: {model_device}")
    for i, t in enumerate(tensors):
        if t.device != model_device:
            print(f"  WARNING: tensor {i} on {t.device}, model on {model_device}")

Partea 8: Noțiuni de bază despre TensorBoard

TensorBoard vă arată în timp cum evoluează procesul de antrenare.

pip install tensorboard
from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter("runs/experiment_1")

for step in range(num_steps):
    loss = train_step(model, batch)

    writer.add_scalar("loss/train", loss.item(), step)
    writer.add_scalar("lr", optimizer.param_groups[0]["lr"], step)

    if step % 100 == 0:
        for name, param in model.named_parameters():
            writer.add_histogram(f"weights/{name}", param, step)
            if param.grad is not None:
                writer.add_histogram(f"grads/{name}", param.grad, step)

writer.close()

Lansați TensorBoard:

tensorboard --logdir=runs

Elemente de urmărit:

  • Pierderea nu scade: rata de învățare este prea mică sau există o problemă în arhitectura modelului
  • Pierderea oscilează puternic: rata de învățare este prea mare
  • Pierderea ajunge la NaN: instabilitate numerică (consultați secțiunea despre NaN de mai sus)
  • Pierderea de antrenare scade, iar cea de validare crește: supraadaptare (overfitting)
  • Histogramele ponderilor se restrâng spre zero: dispariția gradienților
  • Histogramele gradienților cresc necontrolat: este necesară limitarea gradienților (gradient clipping)

Partea 9: Depanatorul VS Code

Pentru depanarea interactivă, configurați VS Code cu un fișier launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Training",
            "type": "debugpy",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": false
        }
    ]
}

Setați puncte de întrerupere făcând clic pe marginea editorului. Folosiți panoul Variables pentru a examina proprietățile tensorilor. Debug Console vă permite să rulați expresii Python arbitrare în timpul execuției.

Această metodă este utilă pentru parcurgerea pas cu pas a fluxurilor de preprocesare a datelor, atunci când doriți să vedeți fiecare transformare.

Utilizare

Următorul flux de depanare detectează majoritatea erorilor din sistemele de IA:

  1. Înainte de antrenare: rulați check_shapes cu un lot de exemple. Verificați dacă dimensiunile de intrare și ieșire corespund așteptărilor.
  2. Primii 10 pași: folosiți debug_print pentru pierdere, rezultate și gradienți. Confirmați că nicio valoare nu este NaN și că valorile se află în intervale rezonabile.
  3. În timpul antrenării: înregistrați în jurnal pierderea, rata de învățare și normele gradienților. Folosiți TensorBoard pentru vizualizare.
  4. Când apare o problemă: introduceți breakpoint() în punctul în care apare eroarea. Examinați interactiv tensorii.
  5. Pentru performanță: cronometrați încărcarea datelor, propagarea înainte și propagarea înapoi. Profilați memoria dacă vă apropiați de limita OOM.

Livrarea rezultatului

Rulați scriptul cu instrumente de depanare:

python phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py

Consultați outputs/prompt-debug-ai-code.md pentru un prompt care ajută la diagnosticarea erorilor specifice sistemelor de IA.

Exerciții

  1. Rulați debug_tools.py și examinați rezultatul fiecărei secțiuni. Modificați modelul demonstrativ pentru a introduce o valoare NaN (indiciu: efectuați o împărțire la zero în propagarea înainte) și observați cum o detectează mecanismul.
  2. Profilați o buclă de antrenare cu cProfile și identificați cea mai lentă funcție.
  3. Folosiți tracemalloc pentru a afla care linie din fluxul de încărcare a datelor alocă cea mai multă memorie.
  4. Configurați TensorBoard pentru o antrenare simplă și stabiliți dacă modelul este supraadaptat.
  5. Folosiți breakpoint() într-o buclă de antrenare. Exersați examinarea formelor tensorilor, a dispozitivelor și a valorilor gradienților din linia de comandă a depanatorului.

Sursă: Debugging and Profiling — originalul

Navigare: înapoi: 00.11 — Linux pentru IA · Faza 0 — Configurare și instrumente · Catalog complet.