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

Curs
AI Engineering from Scratch
Fază
Fundamente matematice
Lectură
18 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Regula lanțului
  5. Grafuri de calcul
  6. Modul direct și modul invers
  7. Numere duale pentru modul direct
  8. Construirea unui motor autograd
  9. Cum funcționează PyTorch Autograd în interior
  10. Construirea implementării
  11. Pasul 1: clasa Value
  12. Pasul 2: operații aritmetice cu urmărirea gradienților
  13. Pasul 3: propagarea înapoi
  14. Pasul 4: operații suplimentare pentru un motor complet
  15. Pasul 5: un mini-MLP de la zero
  16. Pasul 6: verificarea gradienților
  17. Pasul 7: verificarea prin calcul manual
  18. Utilizarea implementării
  19. Compararea cu PyTorch
  20. O expresie mai complexă
  21. Livrarea rezultatului
  22. Exerciții
  23. Termeni-cheie
  24. 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):

Диаграмма к уроку «Regula lanțului și diferențierea automată»

Propagarea înapoi (calcularea gradienților):

Диаграмма к уроку «Regula lanțului și diferențierea automată»

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:

  1. Încapsularea valorilor. Încapsulați fiecare număr într-un obiect care îi stochează valoarea și gradientul.
  2. Înregistrarea grafului. Fiecare operație își înregistrează intrările și funcția gradientului local.
  3. 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:

  1. Creează un nod Tensor pentru x, cu requires_grad=True
  2. Fiecare operație (**, *, +) creează un nod nou și înregistrează funcția pentru propagarea înapoi
  3. y.backward() declanșează diferențierea automată în mod invers prin graful înregistrat
  4. Proprietatea grad_fn a fiecărui nod calculează gradienții locali și îi transmite nodurilor părinte
  5. Gradienții se acumulează în atributele .grad prin 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 autograd
  • code/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

  1. Adăugați __pow__ în clasa Value, astfel încât să puteți calcula x ** n. Verificați dacă d/dx(x^3) pentru x=2 este egal cu 12.0.

  2. Adăugați tanh ca funcție de activare. Verificați dacă tanh'(0) = 1 și tanh'(2) = 0.0707 (aproximativ).

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

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


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.