Faza 03 · lecția 10

Construiți-vă propriul mini-framework

Scopul lecției: Ați construit neuroni, straturi, rețele, retropropagare, activări, funcții de pierdere, optimizatori, regularizare, inițializare și planificatoare ale ratei de învățare. Toate ca piese separate. Acum conectați-le într-un framework. Nu…

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ă
28 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Abstracția Module
  5. Containerul Sequential
  6. Modul de antrenare și modul de evaluare
  7. Optimizatorul
  8. DataLoader
  9. Arhitectura frameworkului
  10. Bucla de antrenare
  11. Ierarhia Module
  12. Construiți-l
  13. Pasul 1: clasa de bază Module
  14. Pasul 2: stratul Linear
  15. Pasul 3: module de activare
  16. Pasul 4: modulul Dropout
  17. Pasul 5: modulul BatchNorm
  18. Pasul 6: containerul Sequential
  19. Pasul 7: funcțiile de pierdere
  20. Pasul 8: optimizatorii SGD și Adam
  21. Pasul 9: DataLoader
  22. Pasul 10: antrenați o rețea cu patru straturi pentru clasificarea punctelor dintr-un cerc
  23. Folosiți-l
  24. Livrați-l
  25. Exerciții
  26. Termeni-cheie
  27. Lecturi suplimentare

Ați construit neuroni, straturi, rețele, retropropagare, activări, funcții de pierdere, optimizatori, regularizare, inițializare și planificatoare ale ratei de învățare. Toate ca piese separate. Acum conectați-le într-un framework. Nu PyTorch. Nu TensorFlow. Al dumneavoastră.

Tip: Construcție Limbaje: Python Cerințe preliminare: Toată faza 03 (lecțiile 01–09) Timp: ~120 de minute

Obiective de învățare

  • Construiți un framework complet de învățare profundă (~500 de linii), cu Module, Linear, ReLU, Sigmoid, Dropout, BatchNorm, Sequential, funcții de pierdere, optimizatori și DataLoader
  • Explicați abstracția Module (forward, backward, parameters) și de ce este necesară comutarea între modurile de antrenare și evaluare
  • Conectați toate componentele într-o buclă de antrenare funcțională, care antrenează o rețea cu patru straturi pentru clasificarea punctelor dintr-un cerc
  • Asociați fiecare componentă a frameworkului cu echivalentul său PyTorch (nn.Module, nn.Sequential, optim.Adam, DataLoader)

Problema

Aveți nouă lecții anterioare cu blocuri de construcție răspândite în fișiere separate. O clasă Value aici, o buclă de antrenare dincolo, inițializarea ponderilor într-un alt fișier și planificatoare ale ratei de învățare în încă unul. Pentru a antrena o rețea, copiați fragmente din cinci lecții diferite și le conectați manual.

Notă tehnică a traducerii: Originalul spune „zece lecții”, însă cerințele preliminare și structura fazei indică nouă lecții anterioare, 01–09. Lecția de față este a zecea și le integrează.

Aceasta este problema pe care o rezolvă frameworkurile. PyTorch vă oferă nn.Module, nn.Sequential, optim.Adam, DataLoader și un tipar de buclă de antrenare care le leagă. TensorFlow vă oferă keras.Layer, keras.Sequential, keras.optimizers.Adam. Acestea nu sunt magie. Sunt tipare organizaționale care permit definirea, antrenarea și evaluarea rețelelor fără a reinventa de fiecare dată infrastructura de legătură.

Veți construi același lucru în aproximativ 500 de linii de Python. Fără numpy. Fără dependențe externe. Un framework care poate defini orice rețea cu propagare înainte, o poate antrena cu SGD sau Adam, poate grupa datele în loturi, poate aplica dropout și normalizarea pe loturi, poate folosi orice activare și poate planifica rata de învățare.

Notă tehnică a traducerii: Implementarea efectivă este mult mai restrânsă decât afirmă originalul: compune numai lanțuri secvențiale care prelucrează câte un vector Python unidimensional. Nu oferă grafuri ramificate, straturi convoluționale sau recurente, un traseu general pe loturi, dispozitive ori tipuri de date configurabile. De asemenea, nici optimizatorii, nici restul codului nu implementează un scheduler sau o metodă set_lr(); planificarea ratei de învățare apare numai ca exercițiu.

La final, veți înțelege exact ce se întâmplă când scrieți model = nn.Sequential(...) în PyTorch. Veți înțelege de ce există model.train() și model.eval(). Veți înțelege de ce optimizer.zero_grad() este un apel separat. Veți înțelege toate acestea deoarece le-ați construit chiar dumneavoastră.

Conceptul

Abstracția Module

Fiecare strat din PyTorch moștenește nn.Module. Un Module are trei responsabilități:

  1. forward() — calculează ieșirea pentru intrările primite
  2. parameters() — returnează toate ponderile antrenabile
  3. backward() — calculează gradienții (gestionat de autograd în PyTorch, explicit în implementarea noastră)

Notă tehnică a traducerii: Lista descrie interfața frameworkului didactic, nu contractul real al fiecărui nn.Module. În PyTorch, un strat definește în mod obișnuit calculul înainte, iar operațiile pe tensori construiesc graful autograd; apelul loss.backward() parcurge acel graf. Autorul unui modul nu trebuie să implementeze câte o metodă proprie backward() pentru fiecare strat, iar parametrii atribuiți ca submodule sau Parameter sunt înregistrați automat.

Un strat Linear este un Module. O activare ReLU este un Module. Un strat dropout este un Module. Un strat de normalizare pe loturi este un Module. Toate au aceeași interfață.

Containerul Sequential

nn.Sequential înlănțuie Module. Propagarea înainte: datele trec prin Module 1, apoi Module 2, apoi Module 3. Propagarea înapoi: lanțul este parcurs în ordine inversă. Containerul însuși este un Module — are forward(), parameters() și backward(). Acesta este tiparul Composite: o secvență de Module este ea însăși un Module.

Modul de antrenare și modul de evaluare

Dropout anulează aleator neuroni în timpul antrenării, dar lasă totul să treacă în timpul evaluării. Normalizarea pe loturi folosește statisticile lotului în timpul antrenării și mediile mobile în timpul evaluării. Metodele train() și eval() comută acest comportament. Fiecare Module are un indicator training.

Notă tehnică a traducerii: În PyTorch, model.eval() schimbă comportamentul modulelor afectate, precum Dropout și BatchNorm, dar nu dezactivează urmărirea gradienților. Pentru inferență fără construirea grafului se folosește separat un context precum torch.no_grad() sau torch.inference_mode().

Optimizatorul

Optimizatorul actualizează parametrii folosind gradienții lor. SGD: param -= lr * grad. Adam: păstrează estimări ale momentului și ale momentului brut de ordinul al doilea, apoi face actualizarea. Optimizatorul nu cunoaște arhitectura rețelei — vede doar o listă liniară de parametri și gradienții lor.

DataLoader

Gruparea în loturi contează din două motive. În primul rând, pentru probleme mari este posibil ca întregul set de date să nu încapă în memorie. În al doilea rând, coborârea gradientului pe mini-loturi introduce zgomot care poate ajuta optimizarea să iasă din unele minime locale. DataLoader împarte datele în loturi și, opțional, le amestecă între epoci.

Notă tehnică a traducerii: Acestea sunt avantaje generale ale încărcătoarelor de date, nu toate proprietăți ale clasei simplificate din lecție. Implementarea de la pasul 9 primește întregul set în self.data și mai construiește o listă completă de indici, deci nu permite în sine procesarea prin streaming a unui set care nu încape în memorie. Zgomotul estimării gradientului poate modifica dinamica optimizării, dar nu garantează ieșirea din minime locale; efectul depinde de geometria pierderii, dimensiunea lotului, rata de învățare și ceilalți hiperparametri.

Arhitectura frameworkului

Диаграмма к уроку «Construiți-vă propriul mini-framework»

Bucla de antrenare

Диаграмма к уроку «Construiți-vă propriul mini-framework»

Ierarhia Module

Диаграмма к уроку «Construiți-vă propriul mini-framework»

gradient-clipping

Notă tehnică a traducerii: Blocul gradient-clipping există în sursă și este păstrat literal, însă tema lui nu este introdusă sau explicată în această lecție și nu corespunde direct diagramelor despre arhitectura mini-frameworkului. Nu a fost inventată și nu a fost substituită nicio ilustrație.

Construiți-l

Pasul 1: clasa de bază Module

Interfața abstractă pe care o implementează fiecare strat.

class Module:
    def __init__(self):
        self.training = True

    def forward(self, x):
        raise NotImplementedError

    def backward(self, grad):
        raise NotImplementedError

    def parameters(self):
        return []

    def train(self):
        self.training = True

    def eval(self):
        self.training = False

Pasul 2: stratul Linear

Blocul de construcție fundamental. Stochează ponderile și biasurile, calculează Wx + b la propagarea înainte și gradienții ponderilor și ai intrării la propagarea înapoi.

import math
import random


class Linear(Module):
    def __init__(self, fan_in, fan_out):
        super().__init__()
        std = math.sqrt(2.0 / fan_in)
        self.weights = [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
        self.biases = [0.0] * fan_out
        self.weight_grads = [[0.0] * fan_in for _ in range(fan_out)]
        self.bias_grads = [0.0] * fan_out
        self.fan_in = fan_in
        self.fan_out = fan_out
        self.input = None

    def forward(self, x):
        self.input = x
        output = []
        for i in range(self.fan_out):
            val = self.biases[i]
            for j in range(self.fan_in):
                val += self.weights[i][j] * x[j]
            output.append(val)
        return output

    def backward(self, grad):
        input_grad = [0.0] * self.fan_in
        for i in range(self.fan_out):
            self.bias_grads[i] += grad[i]
            for j in range(self.fan_in):
                self.weight_grads[i][j] += grad[i] * self.input[j]
                input_grad[j] += grad[i] * self.weights[i][j]
        return input_grad

    def parameters(self):
        params = []
        for i in range(self.fan_out):
            for j in range(self.fan_in):
                params.append((self.weights, i, j, self.weight_grads))
            params.append((self.biases, i, None, self.bias_grads))
        return params

Notă tehnică a traducerii: Linear inițializează toate matricele cu abaterea standard sqrt(2/fan_in), specifică inițializării Kaiming/He pentru rectificatoare în anumite ipoteze. Aceasta este o alegere potrivită ca punct de pornire pentru straturile urmate de ReLU, dar nu este adaptată automat stratului final urmat de Sigmoid; o inițializare de tip Xavier/Glorot este alegerea uzuală pentru activări sigmoidale.

Pasul 3: module de activare

ReLU, Sigmoid și Tanh sub formă de Module. Fiecare păstrează în cache ceea ce îi trebuie pentru propagarea înapoi.

class ReLU(Module):
    def __init__(self):
        super().__init__()
        self.mask = None

    def forward(self, x):
        self.mask = [1.0 if v > 0 else 0.0 for v in x]
        return [max(0.0, v) for v in x]

    def backward(self, grad):
        return [g * m for g, m in zip(grad, self.mask)]


class Sigmoid(Module):
    def __init__(self):
        super().__init__()
        self.output = None

    def forward(self, x):
        self.output = []
        for v in x:
            v = max(-500, min(500, v))
            self.output.append(1.0 / (1.0 + math.exp(-v)))
        return self.output

    def backward(self, grad):
        return [g * o * (1 - o) for g, o in zip(grad, self.output)]


class Tanh(Module):
    def __init__(self):
        super().__init__()
        self.output = None

    def forward(self, x):
        self.output = [math.tanh(v) for v in x]
        return self.output

    def backward(self, grad):
        return [g * (1 - o * o) for g, o in zip(grad, self.output)]

Pasul 4: modulul Dropout

Anulează aleator elemente în timpul antrenării. Scalează elementele rămase cu 1/(1-p), astfel încât valorile așteptate să rămână neschimbate. Nu face nimic în timpul evaluării.

class Dropout(Module):
    def __init__(self, p=0.5):
        super().__init__()
        self.p = p
        self.mask = None

    def forward(self, x):
        if not self.training:
            return x
        self.mask = [0.0 if random.random() < self.p else 1.0 / (1 - self.p) for _ in x]
        return [v * m for v, m in zip(x, self.mask)]

    def backward(self, grad):
        if self.mask is None:
            return grad
        return [g * m for g, m in zip(grad, self.mask)]

Notă tehnică a traducerii: Constructorul nu validează domeniul lui p. La p=1, generatorul standard produce întotdeauna valori mai mici decât p, astfel încât ramura cu împărțirea nu este evaluată în mod normal, dar stratul devine degenerat, iar factorul teoretic 1/(1-p) nu este definit; valori în afara intervalului probabilistic sunt acceptate și ele fără eroare explicită. În plus, propagarea înainte în modul de evaluare returnează x fără să golească self.mask; dacă se apelează apoi backward(), poate fi reutilizată masca rămasă de la o propagare de antrenare anterioară. În utilizarea PyTorch obișnuită nu se execută backward după inferența fără gradienți, dar clasa didactică nu impune această condiție.

Pasul 5: modulul BatchNorm

Normalizează activările la medie zero și varianță unitară pentru fiecare caracteristică din întregul lot. Menține statistici mobile pentru modul de evaluare.

class BatchNorm(Module):
    def __init__(self, size, momentum=0.1, eps=1e-5):
        super().__init__()
        self.size = size
        self.gamma = [1.0] * size
        self.beta = [0.0] * size
        self.gamma_grads = [0.0] * size
        self.beta_grads = [0.0] * size
        self.running_mean = [0.0] * size
        self.running_var = [1.0] * size
        self.momentum = momentum
        self.eps = eps
        self.x_norm = None
        self.std_inv = None
        self.batch_input = None

    def forward_batch(self, batch):
        batch_size = len(batch)
        output_batch = []

        if self.training:
            mean = [0.0] * self.size
            for sample in batch:
                for j in range(self.size):
                    mean[j] += sample[j]
            mean = [m / batch_size for m in mean]

            var = [0.0] * self.size
            for sample in batch:
                for j in range(self.size):
                    var[j] += (sample[j] - mean[j]) ** 2
            var = [v / batch_size for v in var]

            self.std_inv = [1.0 / math.sqrt(v + self.eps) for v in var]

            self.x_norm = []
            self.batch_input = batch
            for sample in batch:
                normed = [(sample[j] - mean[j]) * self.std_inv[j] for j in range(self.size)]
                self.x_norm.append(normed)
                output = [self.gamma[j] * normed[j] + self.beta[j] for j in range(self.size)]
                output_batch.append(output)

            for j in range(self.size):
                self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
                self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
        else:
            std_inv = [1.0 / math.sqrt(v + self.eps) for v in self.running_var]
            for sample in batch:
                normed = [(sample[j] - self.running_mean[j]) * std_inv[j] for j in range(self.size)]
                output = [self.gamma[j] * normed[j] + self.beta[j] for j in range(self.size)]
                output_batch.append(output)

        return output_batch

    def forward(self, x):
        result = self.forward_batch([x])
        return result[0]

    def backward(self, grad):
        if self.x_norm is None:
            return grad
        for j in range(self.size):
            self.gamma_grads[j] += self.x_norm[0][j] * grad[j]
            self.beta_grads[j] += grad[j]
        return [grad[j] * self.gamma[j] * self.std_inv[j] for j in range(self.size)]

    def parameters(self):
        params = []
        for j in range(self.size):
            params.append((self.gamma, j, None, self.gamma_grads))
            params.append((self.beta, j, None, self.beta_grads))
        return params

Notă tehnică a traducerii: Modulul nu este integrat într-un traseu pe loturi al containerului Sequential: acesta apelează numai module.forward(x), iar BatchNorm.forward(x) împachetează un singur exemplu în [x]. În modul de antrenare, un asemenea „lot” are medie egală cu exemplul și activare normalizată zero. backward() tratează numai primul element și omite derivatele prin media și varianța lotului. Totodată, running_var este actualizată direct cu estimatorul biased calculat prin împărțirea la dimensiunea lotului, în timp ce PyTorch folosește varianța biased pentru normalizarea lotului, dar păstrează în media mobilă estimatorul unbiased. Fișierul original code/main.py nu repetă literal această implementare: are un alt forward, pe exemple individuale, care deplasează running_mean către fiecare exemplu, nu actualizează running_var pe acea cale și normalizează cu running_var inițială; backward() rămâne incomplet. Nici această variantă nu remediază problemele. Prin urmare, clasa nu oferă comportament BatchNorm corect pe loturi sau o retropropagare corectă. Codul a fost păstrat exact ca în sursă.

Pasul 6: containerul Sequential

Înlănțuie module. Propagarea înainte merge de la stânga la dreapta, iar propagarea înapoi de la dreapta la stânga.

class Sequential(Module):
    def __init__(self, *modules):
        super().__init__()
        self.modules = list(modules)

    def forward(self, x):
        for module in self.modules:
            x = module.forward(x)
        return x

    def backward(self, grad):
        for module in reversed(self.modules):
            grad = module.backward(grad)
        return grad

    def parameters(self):
        params = []
        for module in self.modules:
            params.extend(module.parameters())
        return params

    def train(self):
        self.training = True
        for module in self.modules:
            module.train()

    def eval(self):
        self.training = False
        for module in self.modules:
            module.eval()

Pasul 7: funcțiile de pierdere

MSE și entropia încrucișată binară. Fiecare returnează valoarea pierderii și oferă o metodă backward() care returnează gradientul.

class MSELoss:
    def __call__(self, predicted, target):
        self.predicted = predicted
        self.target = target
        n = len(predicted)
        self.loss = sum((p - t) ** 2 for p, t in zip(predicted, target)) / n
        return self.loss

    def backward(self):
        n = len(self.predicted)
        return [2 * (p - t) / n for p, t in zip(self.predicted, self.target)]


class BCELoss:
    def __call__(self, predicted, target):
        self.predicted = predicted
        self.target = target
        eps = 1e-7
        n = len(predicted)
        self.loss = 0
        for p, t in zip(predicted, target):
            p = max(eps, min(1 - eps, p))
            self.loss += -(t * math.log(p) + (1 - t) * math.log(1 - p))
        self.loss /= n
        return self.loss

    def backward(self):
        eps = 1e-7
        n = len(self.predicted)
        grads = []
        for p, t in zip(self.predicted, self.target):
            p = max(eps, min(1 - eps, p))
            grads.append((-t / p + (1 - t) / (1 - p)) / n)
        return grads

Notă tehnică a traducerii: Compoziția explicită Sigmoid + BCELoss, cu limitarea probabilităților, este mai puțin stabilă numeric decât calculul BCE direct din logiți printr-o formulare de tip BCEWithLogitsLoss, bazată pe trucul log-sum-exp. Mai mult, toate calculele folosesc zip(predicted, target) fără verificarea lungimilor: dacă vectorii au dimensiuni diferite, Python ignoră în tăcere elementele suplimentare, iar împărțirea rămâne făcută la len(predicted).

Pasul 8: optimizatorii SGD și Adam

Ambii primesc o listă de parametri și actualizează ponderile folosind gradienții.

class SGD:
    def __init__(self, parameters, lr=0.01):
        self.params = parameters
        self.lr = lr

    def step(self):
        for container, i, j, grad_container in self.params:
            if j is not None:
                container[i][j] -= self.lr * grad_container[i][j]
            else:
                container[i] -= self.lr * grad_container[i]

    def zero_grad(self):
        for container, i, j, grad_container in self.params:
            if j is not None:
                grad_container[i][j] = 0.0
            else:
                grad_container[i] = 0.0


class Adam:
    def __init__(self, parameters, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
        self.params = parameters
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.eps = eps
        self.t = 0
        self.m = [0.0] * len(parameters)
        self.v = [0.0] * len(parameters)

    def step(self):
        self.t += 1
        for idx, (container, i, j, grad_container) in enumerate(self.params):
            if j is not None:
                g = grad_container[i][j]
            else:
                g = grad_container[i]

            self.m[idx] = self.beta1 * self.m[idx] + (1 - self.beta1) * g
            self.v[idx] = self.beta2 * self.v[idx] + (1 - self.beta2) * g * g

            m_hat = self.m[idx] / (1 - self.beta1 ** self.t)
            v_hat = self.v[idx] / (1 - self.beta2 ** self.t)

            update = self.lr * m_hat / (math.sqrt(v_hat) + self.eps)

            if j is not None:
                container[i][j] -= update
            else:
                container[i] -= update

    def zero_grad(self):
        for container, i, j, grad_container in self.params:
            if j is not None:
                grad_container[i][j] = 0.0
            else:
                grad_container[i] = 0.0

Pasul 9: DataLoader

Împarte datele în loturi și, opțional, le amestecă la fiecare epocă.

class DataLoader:
    def __init__(self, data, batch_size=32, shuffle=True):
        self.data = data
        self.batch_size = batch_size
        self.shuffle = shuffle

    def __iter__(self):
        indices = list(range(len(self.data)))
        if self.shuffle:
            random.shuffle(indices)
        for start in range(0, len(indices), self.batch_size):
            batch_indices = indices[start:start + self.batch_size]
            batch = [self.data[i] for i in batch_indices]
            inputs = [item[0] for item in batch]
            targets = [item[1] for item in batch]
            yield inputs, targets

    def __len__(self):
        return (len(self.data) + self.batch_size - 1) // self.batch_size

Pasul 10: antrenați o rețea cu patru straturi pentru clasificarea punctelor dintr-un cerc

Conectați totul. Definiți un model, alegeți o funcție de pierdere și un optimizator, apoi executați bucla de antrenare.

def make_circle_data(n=500, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], [label]))
    return data


def train():
    random.seed(42)

    model = Sequential(
        Linear(2, 16),
        ReLU(),
        Linear(16, 16),
        ReLU(),
        Linear(16, 8),
        ReLU(),
        Linear(8, 1),
        Sigmoid(),
    )

    criterion = BCELoss()
    optimizer = Adam(model.parameters(), lr=0.01)

    data = make_circle_data(500)
    split = int(len(data) * 0.8)
    train_data = data[:split]
    test_data = data[split:]

    loader = DataLoader(train_data, batch_size=16, shuffle=True)

    model.train()

    for epoch in range(100):
        total_loss = 0
        total_correct = 0
        total_samples = 0

        for batch_inputs, batch_targets in loader:
            batch_loss = 0
            for x, t in zip(batch_inputs, batch_targets):
                pred = model.forward(x)
                loss = criterion(pred, t)
                batch_loss += loss

                optimizer.zero_grad()
                grad = criterion.backward()
                model.backward(grad)
                optimizer.step()

                predicted_class = 1.0 if pred[0] >= 0.5 else 0.0
                if predicted_class == t[0]:
                    total_correct += 1
                total_samples += 1

            total_loss += batch_loss

        avg_loss = total_loss / total_samples
        accuracy = total_correct / total_samples * 100

        if epoch % 10 == 0 or epoch == 99:
            print(f"Epoch {epoch:3d} | Loss: {avg_loss:.6f} | Train Accuracy: {accuracy:.1f}%")

    model.eval()
    correct = 0
    for x, t in test_data:
        pred = model.forward(x)
        predicted_class = 1.0 if pred[0] >= 0.5 else 0.0
        if predicted_class == t[0]:
            correct += 1
    test_accuracy = correct / len(test_data) * 100
    print(f"\nTest Accuracy: {test_accuracy:.1f}% ({correct}/{len(test_data)})")

    return model, test_accuracy

Notă tehnică a traducerii: Deși DataLoader produce loturi de câte 16 exemple, bucla apelează zero_grad(), backward() și step() pentru fiecare exemplu. Așadar, actualizările sunt stocastice, câte una pe exemplu, nu actualizări pe mini-lot. Exercițiul 5 cere explicit implementarea acumulării reale pe lot. De asemenea, fragmentul doar definește train(); pentru a porni antrenarea trebuie să apelați funcția.

Folosiți-l

Iată echivalentul PyTorch al construcției de mai sus:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

model = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 16),
    nn.ReLU(),
    nn.Linear(16, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid(),
)

criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

for epoch in range(100):
    model.train()
    for inputs, targets in dataloader:
        optimizer.zero_grad()
        predictions = model(inputs)
        loss = criterion(predictions, targets)
        loss.backward()
        optimizer.step()

    model.eval()
    with torch.no_grad():
        test_predictions = model(test_inputs)

Structura este identică. Sequential, Linear, ReLU, Sigmoid, BCELoss, Adam, zero_grad, backward, step, train, eval. Fiecare concept are un corespondent direct. Diferența este că PyTorch gestionează automat autograd — nu trebuie să implementați backward() în fiecare modul —, rulează pe GPU și a fost optimizat timp de ani. Însă structura de bază este aceeași.

Notă tehnică a traducerii: Corespondența este conceptuală, nu structurală unu-la-unu. Mini-frameworkul nu modelează tensori și loturi generale, graful autograd, înregistrarea recursivă completă a stării și bufferelor, state_dict, dispozitivele, tipurile de date, mutarea parametrilor ori multe dintre mecanismele de extensie ale PyTorch. Chiar și propagarea înapoi diferă fundamental: aici este un lanț manual, în timp ce PyTorch diferențiază graful operațiilor pe tensori.

Notă tehnică a traducerii: Fragmentul PyTorch este ilustrativ, nu un program autonom: dataloader și test_inputs nu sunt definite, iar clasele DataLoader și TensorDataset sunt importate, dar nu sunt instanțiate în bloc. Codul rămâne neschimbat față de sursă.

Acum, când vedeți cod PyTorch, știți exact ce se întâmplă la fiecare linie. Tocmai această înțelegere este scopul întregului exercițiu.

Livrați-l

Această lecție produce:

  • outputs/prompt-framework-architect.md — un prompt pentru proiectarea arhitecturilor de rețele neuronale folosind abstracțiile unui framework

Exerciții

  1. Adăugați o clasă SoftmaxCrossEntropyLoss pentru clasificarea multiclasa. Aplicați softmax predicțiilor, calculați pierderea de entropie încrucișată și gestionați propagarea înapoi combinată. Testați-o pe un set de date spirală cu trei clase.

  2. Implementați planificarea ratei de învățare în optimizator: adăugați o metodă set_lr() și conectați planificatorul cosinusoidal din lecția 09. Antrenați clasificatorul pentru cerc cu încălzire (warmup) și planificare cosinusoidală, apoi comparați-l cu o rată de învățare constantă.

  3. Adăugați o metodă save() și una load() în Sequential, care serializează toate ponderile într-un fișier JSON și le încarcă înapoi. Verificați că un model încărcat produce aceleași predicții ca originalul.

  4. Implementați decay-ul ponderilor (regularizarea L2) în optimizatorul Adam. Adăugați un parametru weight_decay care micșorează ponderile spre zero la fiecare pas. Comparați antrenarea cu decay=0 și cu decay=0.01.

Notă tehnică a traducerii: Pentru Adam, adăugarea unui termen L2 în gradient și micșorarea decuplată a ponderilor nu sunt echivalente, deoarece precondiționarea adaptivă transformă termenul inclus în gradient. AdamW aplică decay-ul decuplat de actualizarea adaptivă; exercițiul trebuie să precizeze ce variantă implementează și să nu le trateze drept sinonime.

  1. Înlocuiți bucla de antrenare pe exemple individuale cu acumularea corectă a gradienților pe mini-loturi: acumulați gradienții pentru toate exemplele dintr-un lot, apoi împărțiți-i la dimensiunea lotului și efectuați un singur pas al optimizatorului. Măsurați dacă aceasta schimbă viteza de convergență.

Termeni-cheie

Termen Ce spun oamenii Ce înseamnă de fapt
Module „Un strat” Abstracția de bază a unui framework — orice componentă care are forward(), backward() și parameters()
Sequential „Stivuiți straturile în ordine” Un container care înlănțuie module, aplicându-le în ordine la propagarea înainte și în ordine inversă la propagarea înapoi
Propagare înainte „Rulați rețeaua” Calcularea ieșirii prin trecerea intrării prin fiecare modul în ordine
Propagare înapoi „Calculați gradienții” Propagarea gradientului pierderii prin fiecare modul în ordine inversă, pentru a calcula gradienții parametrilor
Parametri „Ponderile antrenabile” Toate valorile din rețea pe care optimizatorul le poate actualiza — ponderi și biasuri
Optimizator „Componenta care actualizează ponderile” Un algoritm care folosește gradienții pentru a actualiza parametrii, implementând SGD, Adam sau alte reguli
DataLoader „Componenta care furnizează datele” Un iterator care împarte un set de date în loturi și, opțional, le amestecă între epoci
Mod de antrenare „model.train()” Un indicator care activează comportamente stocastice precum dropout și normalizarea pe loturi cu statisticile lotului
Mod de evaluare „model.eval()” Un indicator care dezactivează dropout și folosește statisticile mobile pentru normalizarea pe loturi
Zero grad „Ștergeți gradienții” Resetarea tuturor gradienților parametrilor la zero înainte de calcularea gradienților următorului lot

Lecturi suplimentare

  • Paszke și colaboratorii, „PyTorch: An Imperative Style, High-Performance Deep Learning Library” (2019) — lucrarea care descrie deciziile de proiectare ale PyTorch
  • Chollet, „Deep Learning with Python, Second Edition” (2021) — capitolul 3 prezintă mecanismele interne Keras cu aceeași abstracție de modul/strat
  • Johnson, „Tiny-DNN” (https://github.com/tiny-dnn/tiny-dnn) — un framework de învățare profundă C++ alcătuit numai din antete, util pentru înțelegerea mecanismelor interne ale frameworkurilor

Sursă: Originalul în limba engleză

Navigare: ← Lecția 03.09 — Scheme ale ratei de învățare și warmup · Faza 3 — Fundamentele învățării profunde · Catalog complet