Faza 01 · lecția 05
Regula lanțului și diferențierea automată
Scopul lecției: Regula lanțului este mecanismul din spatele oricărei rețele neuronale care învață.
Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Regula lanțului
- Grafuri de calcul
- Modul direct și modul invers
- Numere duale pentru modul direct
- Construirea unui motor autograd
- Cum funcționează PyTorch Autograd în interior
- Construirea implementării
- Pasul 1: clasa Value
- Pasul 2: operații aritmetice cu urmărirea gradienților
- Pasul 3: propagarea înapoi
- Pasul 4: operații suplimentare pentru un motor complet
- Pasul 5: un mini-MLP de la zero
- Pasul 6: verificarea gradienților
- Pasul 7: verificarea prin calcul manual
- Utilizarea implementării
- Compararea cu PyTorch
- O expresie mai complexă
- Livrarea rezultatului
- Exerciții
- Termeni-cheie
- Lecturi suplimentare
Regula lanțului este mecanismul din spatele oricărei rețele neuronale care învață.
Tip: Construire Limbaj: Python Cerințe preliminare: Faza 1, lecția 04 (Calcul diferențial și integral pentru învățarea automată) Durată: ~90 de minute
Obiective de învățare
- Construirea unui motor autograd minimal (clasa Value), care înregistrează operațiile și calculează gradienții prin diferențiere automată în mod invers
- Implementarea propagării înainte și înapoi printr-un graf de calcul, folosind sortarea topologică
- Construirea și antrenarea unui perceptron multistrat pentru XOR folosind numai motorul autograd realizat de la zero
- Verificarea corectitudinii diferențierii automate prin compararea gradienților cu valori numerice calculate prin diferențe finite
Problema
Puteți calcula derivatele funcțiilor simple. Însă o rețea neuronală nu este o funcție simplă. Este alcătuită din sute de funcții compuse: înmulțiți matrice, adăugați biasul, aplicați o funcție de activare, înmulțiți din nou matrice, apoi aplicați softmax și funcția de pierdere cu entropie încrucișată. Ieșirea este o funcție a unei funcții a altei funcții.
Pentru a antrena rețeaua, aveți nevoie de gradientul pierderii în raport cu fiecare pondere. Calculul manual este imposibil în cazul milioanelor de parametri. Calculul numeric prin diferențe finite este prea lent.
Regula lanțului vă oferă fundamentul matematic. Diferențierea automată vă oferă algoritmul. Împreună, acestea vă permit să calculați gradienți exacți, până la erorile de virgulă mobilă, prin compuneri de operații diferențiabile, într-un timp proporțional cu o singură propagare înainte. În punctele nediferențiabile, precum ReLU la zero, implementarea folosește o convenție sau un subgradient.
Notă tehnică a traducerii: Originalul extinde afirmația la compuneri arbitrare. Diferențierea automată produce derivata exactă a operațiilor diferențiabile; într-un punct nediferențiabil nu există o derivată obișnuită unică.
Astfel funcționează PyTorch, TensorFlow și JAX. Veți construi de la zero o versiune în miniatură.
Conceptul
Regula lanțului
Dacă y = f(g(x)), derivata lui y în raport cu x este:
dy/dx = dy/dg * dg/dx = f'(g(x)) * g'(x)
Înmulțiți derivatele de-a lungul lanțului. Fiecare verigă contribuie cu derivata sa locală.
Exemplu: y = sin(x^2)
g(x) = x^2 g'(x) = 2x
f(g) = sin(g) f'(g) = cos(g)
dy/dx = cos(x^2) * 2x
Pentru compuneri mai profunde, lanțul se prelungește:
y = f(g(h(x)))
dy/dx = f'(g(h(x))) * g'(h(x)) * h'(x)
Fiecare strat al unei rețele neuronale reprezintă o verigă din acest lanț.
Grafuri de calcul
Un graf de calcul oferă o reprezentare vizuală a regulii lanțului. Fiecare operație devine un nod. Datele circulă înainte prin graf, iar gradienții circulă înapoi.
Propagarea înainte (calcularea valorilor):
Propagarea înapoi (calcularea gradienților):
Propagarea înapoi aplică regula lanțului în fiecare nod, transmițând gradienții de la ieșire către intrări.
Modul direct și modul invers
Regula lanțului poate fi aplicată printr-un graf în două moduri.
Modul direct pornește de la intrări și propagă derivatele înainte. Calculează dx/dx = 1 și transmite rezultatul prin fiecare operație. Este potrivit când aveți puține intrări și multe ieșiri.
Mod direct: inițializați dx/dx = 1 și propagați înainte
x = 2 (dx/dx = 1)
a = x^2 (da/dx = 2x = 4)
y = sin(a) (dy/dx = cos(a) * da/dx = cos(4) * 4 = -2.615)
Modul invers pornește de la ieșire și propagă gradienții înapoi. Calculează dy/dy = 1 și parcurge fiecare operație în ordine inversă. Este potrivit când aveți multe intrări și puține ieșiri.
Mod invers: inițializați dy/dy = 1 și propagați înapoi
y = sin(a) (dy/dy = 1)
a = x^2 (dy/da = cos(a) = cos(4) = -0.654)
x = 2 (dy/dx = dy/da * da/dx = -0.654 * 4 = -2.615)
Rețelele neuronale au milioane de intrări (ponderi) și o singură ieșire (pierderea). Modul invers calculează toți gradienții printr-o singură propagare înapoi. Din acest motiv, retropropagarea folosește modul invers.
| Mod | Valoare inițială | Direcție | Potrivit când |
|---|---|---|---|
| Direct | dx_i/dx_i = 1 |
De la intrare la ieșire | Puține intrări, multe ieșiri |
| Invers | dy/dy = 1 |
De la ieșire la intrare | Multe intrări, puține ieșiri (rețele neuronale) |
Numere duale pentru modul direct
Modul direct poate fi implementat elegant cu numere duale. Un număr dual are forma a + b*epsilon, unde epsilon^2 = 0.
Număr dual: (valoare, derivată)
(2, 1) înseamnă: valoarea este 2, iar derivata în raport cu x este 1
Reguli aritmetice:
(a, a') + (b, b') = (a+b, a'+b')
(a, a') * (b, b') = (a*b, a'*b + a*b')
sin(a, a') = (sin(a), cos(a)*a')
Inițializați derivata variabilei de intrare cu 1. Derivata se propagă automat prin fiecare operație.
Construirea unui motor autograd
Un motor autograd are nevoie de trei componente:
- Încapsularea valorilor. Încapsulați fiecare număr într-un obiect care îi stochează valoarea și gradientul.
- Înregistrarea grafului. Fiecare operație își înregistrează intrările și funcția gradientului local.
- Propagarea înapoi. Sortați topologic graful, apoi parcurgeți-l în ordine inversă, aplicând regula lanțului în fiecare nod.
Exact astfel funcționează autograd din PyTorch. Clasa torch.Tensor încapsulează valorile, înregistrează operațiile când requires_grad=True și calculează gradienții când apelați .backward().
Cum funcționează PyTorch Autograd în interior
Când scrieți cod PyTorch:
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1
y.backward()
print(x.grad) # 7.0 = 2*x + 3 = 2*2 + 3
În interior, PyTorch:
- Creează un nod
Tensorpentrux, curequires_grad=True - Fiecare operație (
**,*,+) creează un nod nou și înregistrează funcția pentru propagarea înapoi y.backward()declanșează diferențierea automată în mod invers prin graful înregistrat- Proprietatea
grad_fna fiecărui nod calculează gradienții locali și îi transmite nodurilor părinte - Gradienții se acumulează în atributele
.gradprin adunare, nu prin înlocuire
Graful este dinamic (define-by-run, adică definire prin execuție). La fiecare propagare înainte se construiește un graf nou. De aceea, PyTorch acceptă structuri de control (if/else, bucle) în interiorul modelelor.
chain-rule
Construirea implementării
Pasul 1: clasa Value
class Value:
def __init__(self, data, children=(), op=''):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._prev = set(children)
self._op = op
def __repr__(self):
return f"Value(data={self.data:.4f}, grad={self.grad:.4f})"
Fiecare obiect Value își stochează datele numerice, gradientul (inițial zero), o funcție pentru propagarea înapoi și referințe către nodurile precedente care l-au produs.
Pasul 2: operații aritmetice cu urmărirea gradienților
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), '+')
def _backward():
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), '*')
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out
def relu(self):
out = Value(max(0, self.data), (self,), 'relu')
def _backward():
self.grad += (1.0 if out.data > 0 else 0.0) * out.grad
out._backward = _backward
return out
Fiecare operație creează o închidere (closure) care știe să calculeze gradienții locali și să îi înmulțească cu gradientul din amonte (out.grad). Operatorul += tratează cazul în care o valoare este folosită în mai multe operații.
Pasul 3: propagarea înapoi
def backward(self):
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1.0
for v in reversed(topo):
v._backward()
Sortarea topologică garantează că gradientul fiecărui nod este calculat complet înainte de a fi propagat către nodurile precedente. Gradientul inițial este 1.0 (dy/dy = 1).
Pasul 4: operații suplimentare pentru un motor complet
Clasa Value de bază acceptă adunarea, înmulțirea și relu. Un motor autograd real are nevoie de mai multe operații. Iată operațiile necesare pentru construirea rețelelor neuronale:
def __neg__(self):
return self * -1
def __sub__(self, other):
return self + (-other)
def __radd__(self, other):
return self + other
def __rmul__(self, other):
return self * other
def __rsub__(self, other):
return other + (-self)
def __pow__(self, n):
out = Value(self.data ** n, (self,), f'**{n}')
def _backward():
self.grad += n * (self.data ** (n - 1)) * out.grad
out._backward = _backward
return out
def __truediv__(self, other):
return self * (other ** -1) if isinstance(other, Value) else self * (Value(other) ** -1)
def exp(self):
import math
e = math.exp(self.data)
out = Value(e, (self,), 'exp')
def _backward():
self.grad += e * out.grad
out._backward = _backward
return out
def log(self):
import math
out = Value(math.log(self.data), (self,), 'log')
def _backward():
self.grad += (1.0 / self.data) * out.grad
out._backward = _backward
return out
def tanh(self):
import math
t = math.tanh(self.data)
out = Value(t, (self,), 'tanh')
def _backward():
self.grad += (1 - t ** 2) * out.grad
out._backward = _backward
return out
De ce contează fiecare operație:
| Operație | Regula pentru propagarea înapoi | Utilizare |
|---|---|---|
__sub__ |
Reutilizează adunarea și negarea | Calculul pierderii (predicție - țintă) |
__pow__ |
n * x^(n-1) | Activări polinomiale, MSE (eroare^2) |
__truediv__ |
Reutilizează înmulțirea și pow(-1) | Normalizare, scalarea ratei de învățare |
exp |
exp(x) * gradientul din amonte | Softmax, log-verosimilitate |
log |
(1/x) * gradientul din amonte | Pierderea cu entropie încrucișată, log-probabilități |
tanh |
(1 - tanh^2) * gradientul din amonte | Funcție de activare clasică |
Aspectul ingenios este că __sub__ și __truediv__ sunt definite în termenii operațiilor existente. Gradienții corecți rezultă automat, deoarece regula lanțului se aplică operațiilor subiacente de adunare, înmulțire și ridicare la putere.
Pasul 5: un mini-MLP de la zero
Cu o clasă Value completă, puteți construi o rețea neuronală. Fără PyTorch. Fără NumPy. Doar obiecte Value și regula lanțului.
import random
class Neuron:
def __init__(self, n_inputs):
self.w = [Value(random.uniform(-1, 1)) for _ in range(n_inputs)]
self.b = Value(0.0)
def __call__(self, x):
act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
return act.tanh()
def parameters(self):
return self.w + [self.b]
class Layer:
def __init__(self, n_inputs, n_outputs):
self.neurons = [Neuron(n_inputs) for _ in range(n_outputs)]
def __call__(self, x):
return [n(x) for n in self.neurons]
def parameters(self):
return [p for n in self.neurons for p in n.parameters()]
class MLP:
def __init__(self, sizes):
self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(sizes)-1)]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x[0] if len(x) == 1 else x
def parameters(self):
return [p for layer in self.layers for p in layer.parameters()]
Un obiect Neuron calculează tanh(w1*x1 + w2*x2 + ... + b). Un obiect Layer este o listă de neuroni. Un obiect MLP conectează straturile succesiv. Fiecare pondere este un obiect Value, astfel încât apelul loss.backward() propagă gradienții către fiecare parametru.
Antrenarea pe XOR:
random.seed(42)
model = MLP([2, 4, 1]) # 2 inputs, 4 hidden neurons, 1 output
xs = [[0, 0], [0, 1], [1, 0], [1, 1]]
ys = [-1, 1, 1, -1] # XOR pattern (using -1/1 for tanh)
for step in range(100):
preds = [model(x) for x in xs]
loss = sum((p - y) ** 2 for p, y in zip(preds, ys))
for p in model.parameters():
p.grad = 0.0
loss.backward()
lr = 0.05
for p in model.parameters():
p.data -= lr * p.grad
if step % 20 == 0:
print(f"step {step:3d} loss = {loss.data:.4f}")
print("\nPredictions after training:")
for x, y in zip(xs, ys):
print(f" input={x} target={y:2d} pred={model(x).data:6.3f}")
Aceasta este micrograd: o buclă completă de antrenare a unei rețele neuronale, scrisă în Python pur și bazată pe diferențiere automată. Orice cadru software comercial de învățare profundă aplică același principiu la scară foarte mare.
Pasul 6: verificarea gradienților
Cum puteți ști dacă diferențierea automată este corectă? Comparați-o cu derivatele numerice. Aceasta este verificarea gradienților.
def gradient_check(build_expr, x_val, h=1e-7):
x = Value(x_val)
y = build_expr(x)
y.backward()
autodiff_grad = x.grad
y_plus = build_expr(Value(x_val + h)).data
y_minus = build_expr(Value(x_val - h)).data
numerical_grad = (y_plus - y_minus) / (2 * h)
diff = abs(autodiff_grad - numerical_grad)
return autodiff_grad, numerical_grad, diff
Testați mecanismul pe o expresie complexă:
def expr(x):
return (x ** 3 + x * 2 + 1).tanh()
ad, num, diff = gradient_check(expr, 0.5)
print(f"Autodiff: {ad:.8f}")
print(f"Numerical: {num:.8f}")
print(f"Difference: {diff:.2e}")
# Difference should be < 1e-5
Verificarea gradienților este esențială atunci când implementați operații noi. Dacă propagarea înapoi conține o eroare, verificarea numerică o detectează. Orice implementare serioasă de învățare profundă rulează verificări ale gradienților în timpul dezvoltării.
Când să folosiți verificarea gradienților:
| Situație | Verificați gradienții? |
|---|---|
| Adăugarea unei operații noi în motorul autograd | Da, întotdeauna |
| Depanarea unei bucle de antrenare care nu converge | Da, verificați mai întâi gradienții |
| Antrenare în producție | Nu, este prea lentă (câte două propagări înainte pentru fiecare parametru) |
| Teste unitare pentru codul autograd | Da, automatizați verificarea |
Pasul 7: verificarea prin calcul manual
x1 = Value(2.0)
x2 = Value(3.0)
a = x1 * x2 # a = 6.0
b = a + Value(1.0) # b = 7.0
y = b.relu() # y = 7.0
y.backward()
print(f"y = {y.data}") # 7.0
print(f"dy/dx1 = {x1.grad}") # 3.0 (= x2)
print(f"dy/dx2 = {x2.grad}") # 2.0 (= x1)
Verificare manuală: y = relu(x1*x2 + 1). Deoarece x1*x2 + 1 = 7 > 0, relu este funcția identitate.
dy/dx1 = x2 = 3. dy/dx2 = x1 = 2. Motorul produce același rezultat.
Utilizarea implementării
Compararea cu PyTorch
import torch
x1 = torch.tensor(2.0, requires_grad=True)
x2 = torch.tensor(3.0, requires_grad=True)
a = x1 * x2
b = a + 1.0
y = torch.relu(b)
y.backward()
print(f"PyTorch dy/dx1 = {x1.grad.item()}") # 3.0
print(f"PyTorch dy/dx2 = {x2.grad.item()}") # 2.0
Aceiași gradienți. Motorul dvs. calculează același rezultat ca PyTorch, deoarece fundamentul matematic este același: diferențiere automată în mod invers prin regula lanțului.
O expresie mai complexă
a = Value(2.0)
b = Value(-3.0)
c = Value(10.0)
f = (a * b + c).relu() # relu(2*(-3) + 10) = relu(4) = 4
f.backward()
print(f"df/da = {a.grad}") # -3.0 (= b)
print(f"df/db = {b.grad}") # 2.0 (= a)
print(f"df/dc = {c.grad}") # 1.0
Livrarea rezultatului
În urma acestei lecții obțineți:
outputs/skill-autodiff.md– un modul de instrucțiuni („skill”) pentru construirea și depanarea sistemelor autogradcode/autodiff.py– un motor autograd minimal pe care îl puteți extinde
Clasa Value construită aici reprezintă fundamentul buclei de antrenare a rețelei neuronale din faza 3.
Exerciții
-
Adăugați
__pow__în clasa Value, astfel încât să puteți calculax ** n. Verificați dacăd/dx(x^3)pentrux=2este egal cu12.0. -
Adăugați
tanhca funcție de activare. Verificați dacătanh'(0) = 1șitanh'(2) = 0.0707(aproximativ). -
Construiți un graf de calcul pentru un singur neuron:
y = relu(w1*x1 + w2*x2 + b). Calculați toți cei cinci gradienți și verificați-i prin comparație cu PyTorch. -
Implementați diferențierea automată în mod direct folosind numere duale. Creați o clasă
Dualși verificați dacă produce aceleași derivate ca motorul în mod invers.
Termeni-cheie
| Termen | Cum îi spune lumea | Ce înseamnă de fapt |
|---|---|---|
| Regula lanțului | „Înmulțiți derivatele” | Derivata funcțiilor compuse este egală cu produsul derivatelor locale ale fiecărei funcții, evaluate în punctele corespunzătoare. |
| Graf de calcul | „Diagrama rețelei” | Un graf orientat aciclic în care nodurile sunt operații, iar muchiile transportă valori înainte sau gradienți înapoi. |
| Mod direct | „Propagați derivatele înainte” | Diferențiere automată care propagă derivatele de la intrări către ieșiri. Necesită câte o parcurgere pentru fiecare variabilă de intrare. |
| Mod invers | „Retropropagare” | Diferențiere automată care propagă gradienții de la ieșiri către intrări. Necesită câte o parcurgere pentru fiecare variabilă de ieșire. |
| Autograd | „Gradienți automați” | Un sistem care înregistrează operațiile asupra valorilor, construiește un graf și calculează prin regula lanțului gradienți exacți pentru operații diferențiabile; la punctele nediferențiabile folosește convenția implementării. |
| Numere duale | „Valoare plus derivată” | Numere de forma a + b*epsilon (epsilon^2 = 0), care transportă informația despre derivată prin operațiile aritmetice. |
| Sortare topologică | „Ordinea dependențelor” | Ordonarea nodurilor grafului astfel încât fiecare nod să apară după toate dependențele sale. Este necesară pentru propagarea corectă a gradienților. |
| Acumularea gradienților | „Adunați, nu înlocuiți” | Când o valoare alimentează mai multe operații, gradientul său este suma tuturor contribuțiilor de gradient primite. |
| Graf dinamic | „Definire prin execuție” | Un graf de calcul reconstruit la fiecare propagare înainte, care permite folosirea structurilor de control Python în modele, în stilul PyTorch. |
| Verificarea gradienților | „Verificare numerică” | Compararea gradienților obținuți prin diferențiere automată cu gradienții numerici calculați prin diferențe finite, pentru verificarea corectitudinii. Este esențială în depanare. |
| MLP | „Perceptron multistrat” | O rețea neuronală cu unul sau mai multe straturi ascunse de neuroni. Fiecare neuron calculează o sumă ponderată plus biasul, apoi aplică o funcție de activare. |
| Neuron | „Sumă ponderată + activare” | Unitatea de bază: ieșire = activare(w1x1 + w2x2 + … + b). Ponderile și biasul sunt parametri care se învață. |
Lecturi suplimentare
- 3Blue1Brown: Backpropagation calculus – explicație vizuală a regulii lanțului în rețelele neuronale
- PyTorch Autograd mechanics – modul de funcționare a sistemului real
- Baydin et al., Automatic Differentiation in Machine Learning: a Survey – referință cuprinzătoare
Sursă: Chain Rule & Automatic Differentiation — original
Navigare: înapoi: 01.04 — Calcul diferențial și integral pentru învățarea automată · Faza 1 — Fundamente matematice · Catalog complet · în continuare: 01.06 — Probabilități și distribuții.