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ă.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Abstracția Module
- Containerul Sequential
- Modul de antrenare și modul de evaluare
- Optimizatorul
- DataLoader
- Arhitectura frameworkului
- Bucla de antrenare
- Ierarhia Module
- Construiți-l
- Pasul 1: clasa de bază Module
- Pasul 2: stratul Linear
- Pasul 3: module de activare
- Pasul 4: modulul Dropout
- Pasul 5: modulul BatchNorm
- Pasul 6: containerul Sequential
- Pasul 7: funcțiile de pierdere
- Pasul 8: optimizatorii SGD și Adam
- Pasul 9: DataLoader
- Pasul 10: antrenați o rețea cu patru straturi pentru clasificarea punctelor dintr-un cerc
- Folosiți-l
- Livrați-l
- Exerciții
- Termeni-cheie
- 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:
- forward() — calculează ieșirea pentru intrările primite
- parameters() — returnează toate ponderile antrenabile
- 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; apelulloss.backward()parcurge acel graf. Autorul unui modul nu trebuie să implementeze câte o metodă propriebackward()pentru fiecare strat, iar parametrii atribuiți ca submodule sauParametersunt î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 precumtorch.no_grad()sautorch.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
Bucla de antrenare
Ierarhia Module
gradient-clipping
Notă tehnică a traducerii: Blocul
gradient-clippingexistă î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:
Linearinițializează toate matricele cu abaterea standardsqrt(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. Lap=1, generatorul standard produce întotdeauna valori mai mici decâtp, astfel încât ramura cu împărțirea nu este evaluată în mod normal, dar stratul devine degenerat, iar factorul teoretic1/(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ăxfără să goleascăself.mask; dacă se apelează apoibackward(), 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ă numaimodule.forward(x), iarBatchNorm.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_vareste 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 originalcode/main.pynu repetă literal această implementare: are un altforward, pe exemple individuale, care deplaseazărunning_meancătre fiecare exemplu, nu actualizeazărunning_varpe acea cale și normalizează curunning_variniț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 tipBCEWithLogitsLoss, bazată pe trucul log-sum-exp. Mai mult, toate calculele folosesczip(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ă lalen(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
DataLoaderproduce loturi de câte 16 exemple, bucla apeleazăzero_grad(),backward()șistep()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ștetrain(); 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șitest_inputsnu sunt definite, iar claseleDataLoaderșiTensorDatasetsunt 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
-
Adăugați o clasă
SoftmaxCrossEntropyLosspentru 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. -
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ă. -
Adăugați o metodă
save()și unaload()î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. -
Implementați decay-ul ponderilor (regularizarea L2) în optimizatorul Adam. Adăugați un parametru
weight_decaycare micșorează ponderile spre zero la fiecare pas. Comparați antrenarea cudecay=0și cudecay=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.
- Î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