Jak zbudować silnik propagacji wstecznej od zera - przewodnik
Źródło: Link
Źródło: Link
Andrej Karpathy, jeden z najbardziej rozpoznawalnych naukowców w świecie deep learningu, powiedział kiedyś: "Jeśli nie rozumiesz backpropagation, nie rozumiesz neural networks". Miał rację. Większość kursów AI pokazuje Ci gotowe biblioteki, nie to, co dzieje się pod maską.
Zbudujesz teraz własny silnik propagacji wstecznej. Od zera. Bez PyTorcha, bez TensorFlow - tylko Python i matematyka na poziomie liceum. Dopiero wtedy zrozumiesz, jak naprawdę działają sieci neuronowe w GPT-5, Claude czy Gemini.
Ten poradnik wymaga:
Nie musisz być programistą. Nie musisz znać zaawansowanej matematyki. Jeśli potrafisz obliczyć pochodną funkcji kwadratowej - wystarczy.

Propagacja wsteczna to algorytm, który pozwala sieci neuronowej "uczyć się" - czyli dostosowywać swoje wagi, żeby poprawiać wyniki. Działa w dwóch krokach:
Kluczowe pojęcie: gradient. To matematyczna miara "jak bardzo zmiana wagi X wpłynie na błąd końcowy". Jeśli gradient wagi wynosi -0.5, zwiększenie tej wagi o 0.1 zmniejszy błąd o ~0.05.
Problem? Sieć neuronowa to setki tysięcy (często miliardy) wag połączonych w skomplikowany graf operacji matematycznych. Liczenie gradientów ręcznie dla każdej wagi to koszmar. Dlatego potrzebujesz autograd - silnika, który robi to automatycznie.
Micrograd to biblioteka stworzona przez Andreja Karpathy'ego, która implementuje backpropagation w ~150 liniach kodu. Nie ma w niej tensorów, GPU ani optymalizacji - tylko czysty mechanizm propagacji wstecznej.
Dzięki temu widzisz dokładnie, co się dzieje. Każda operacja (dodawanie, mnożenie, potęgowanie) buduje graf obliczeniowy. Każdy węzeł grafu wie, jak obliczyć swój gradient. Na końcu wywołujesz .backward() i gradienty płyną wstecz - od wyniku do wejść.
Otwierasz nowy plik Python lub notebook. Zaczynasz od klasy Value - to opakowanie na zwykłą liczbę, które będzie pamiętać, skąd ta liczba pochodzi i jak obliczyć jej gradient.
class Value:
def __init__(self, data):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._prev = set()
Co tu się dzieje:
self.data - faktyczna wartość liczbowa (np. 2.5)self.grad - gradient tej wartości (na razie 0.0)self._backward - funkcja, która obliczy gradienty rodziców (na razie pusta)self._prev - zbiór wartości, z których powstała ta wartość (rodzice w grafie)Teraz dodajesz podstawowe operacje matematyczne. Zaczynasz od dodawania:
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data)
out._prev = {self, other}
def _backward():
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out
Teraz możesz pisać a = Value(2); b = Value(3); c = a + b i c.data będzie wynosić 5. Co ważniejsze - c pamięta, że powstało z a i b, i wie, jak obliczyć ich gradienty.

Matematyka: jeśli c = a + b, to pochodna c po a wynosi 1.0 (zmiana a o 1 zmienia c o 1). Dlatego w funkcji _backward po prostu przekazujesz gradient c do a i b bez zmian.
Operator += zamiast = jest kluczowy - ta sama wartość może być użyta w wielu miejscach grafu, więc jej gradient to suma wszystkich wpływów.
Dodajesz mnożenie analogicznie:
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data)
out._prev = {self, other}
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out
Tutaj matematyka jest ciekawsza: jeśli c = a * b, to pochodna c po a wynosi b, a po b wynosi a. Dlatego gradient a to b.data * out.grad.
Potęgowanie:
def __pow__(self, other):
assert isinstance(other, (int, float))
out = Value(self.data ** other)
out._prev = {self}
def _backward():
self.grad += other * (self.data ** (other - 1)) * out.grad
out._backward = _backward
return out
Tutaj używasz reguły potęgowania: pochodna x^n to n * x^(n-1).
Dodajesz najprostszą funkcję aktywacji - ReLU (Rectified Linear Unit). Zwraca wartość jeśli jest dodatnia, zero w przeciwnym razie:
def relu(self):
out = Value(0 if self.data < 0 else self.data)
out._prev = {self}
def _backward():
self.grad += (out.data > 0) * out.grad
out._backward = _backward
return out
Gradient ReLU to 1.0 jeśli wartość była dodatnia, 0.0 w przeciwnym razie. Proste, skuteczne - to właśnie ReLU napędza większość nowoczesnych sieci neuronowych.
Masz już operacje, które budują graf. Teraz potrzebujesz funkcji backward(), która przejdzie przez cały graf od końca do początku i obliczy wszystkie gradienty.
Problem: nie możesz wywołać _backward() w losowej kolejności. Musisz zacząć od wyniku końcowego i iść wstecz, upewniając się, że gradient każdego węzła jest obliczony zanim obliczysz gradienty jego rodziców.
Rozwiązanie: sortowanie topologiczne grafu.
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 node in reversed(topo):
node._backward()
Co tu się dzieje:
build_topo rekurencyjnie przechodzi przez graf i buduje listę węzłów w kolejności topologicznej_backward() każdego węzłaTeraz możesz zrobić:
a = Value(2.0)
b = Value(-3.0)
c = Value(10.0)
e = a * b
d = e + c
f = d.relu()
f.backward()
print(a.grad) # Gradient 'a' względem 'f'
print(b.grad) # Gradient 'b' względem 'f'
I dostaniesz dokładne wartości gradientów - automatycznie, bez ręcznego liczenia pochodnych.

Masz silnik autograd. Teraz budujesz na nim neuron - podstawową jednostkę sieci neuronowej.
Neuron to funkcja, która:
n wejśćimport random
class Neuron:
def __init__(self, nin):
self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]
self.b = Value(random.uniform(-1, 1))
def __call__(self, x):
act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
out = act.relu()
return out
def parameters(self):
return self.w + [self.b]
Teraz możesz stworzyć neuron z 3 wejściami i przepuścić przez niego dane:
n = Neuron(3)
x = [Value(2.0), Value(0.0), Value(-1.5)]
output = n(x)
print(output.data) # Wynik neuronu
Warstwa to zbiór neuronów działających równolegle:
class Layer:
def __init__(self, nin, nout):
self.neurons = [Neuron(nin) for _ in range(nout)]
def __call__(self, x):
outs = [n(x) for n in self.neurons]
return outs[0] if len(outs) == 1 else outs
def parameters(self):
return [p for neuron in self.neurons for p in neuron.parameters()]
Sieć to stos warstw:
class MLP:
def __init__(self, nin, nouts):
sz = [nin] + nouts
self.layers = [Layer(sz[i], sz[i+1]) for i in range(len(nouts))]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
def parameters(self):
return [p for layer in self.layers for p in layer.parameters()]
Teraz możesz zbudować sieć 3-4-4-1 (3 wejścia, dwie warstwy ukryte po 4 neurony, 1 wyjście):
model = MLP(3, [4, 4, 1])
print(len(model.parameters())) # Liczba wag w sieci
Masz sieć. Masz autograd. Teraz uczysz sieć na prostym zbiorze danych.
Tworzysz zbiór treningowy - 4 przykłady wejść i oczekiwanych wyjść:
xs = [
[2.0, 3.0, -1.0],
[3.0, -1.0, 0.5],
[0.5, 1.0, 1.0],
[1.0, 1.0, -1.0]
]
ys = [1.0, -1.0, -1.0, 1.0] # Oczekiwane wyniki
Pętla uczenia:
for k in range(100): # 100 epok
# Forward pass
ypred = [model(x) for x in xs]
loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred))
# Backward pass
for p in model.parameters():
p.grad = 0.0 # Zeruj gradienty
loss.backward()
# Update wag
for p in model.parameters():
p.data += -0.05 * p.grad # Learning rate = 0.05
if k % 10 == 0:
print(f"Epoka {k}, Loss: {loss.data}")
Co tu się dzieje:
loss.backward() i wszystkie gradienty są obliczane automatyczniePo 100 epokach loss powinien spaść z ~4-5 do ~0.01. Sieć nauczyła się mapowania wejść na wyjścia.

Teraz sprawdzasz, czy sieć naprawdę nauczyła się wzorca, czy tylko "zakuła" przykłady treningowe:
test_x = [2.5, 2.0, -0.5]
test_output = model(test_x)
print(f"Przewidywanie: {test_output.data}")
Jeśli sieć jest dobrze wytrenowana, wynik powinien być bliski 1.0 (testowe wejście jest podobne do pierwszego przykładu treningowego).
To, co właśnie zbudowałeś, to dokładnie ten sam mechanizm, który napędza:
torch.autograd)tf.GradientTape)jax.grad)Różnica? Skala. Produkcyjne biblioteki operują na tensorach (wielowymiarowych tablicach), używają GPU do równoległych obliczeń i mają setki optymalizacji. Rdzeń - graf obliczeniowy i propagacja wsteczna - jest identyczny.
Jeśli chcesz zgłębić temat, naturalnym krokiem jest nauka PyTorcha. Teraz rozumiesz, co dzieje się pod maską loss.backward() - to daje Ci przewagę nad 90% osób, które tylko kopiują kod z tutoriali.
Jeśli interesuje Cię, jak to wygląda w praktyce z prawdziwymi danymi, sprawdź przewodnik po pracy z danymi w Pythonie - tam pokazuję, jak przygotować dane do treningu modeli AI.
Podczas budowania własnego silnika autograd łatwo o błędy. Oto trzy najczęstsze:
Gradienty się akumulują. Jeśli nie wyzerujesz ich przed każdym backward(), będą rosnąć w nieskończoność i sieć nigdy się nie nauczy. Zawsze rób:
for p in model.parameters():
p.grad = 0.0
loss.backward()
Jeśli ta sama wartość jest użyta w wielu miejscach grafu, jej gradient to suma wszystkich wpływów. Dlatego piszesz self.grad += out.grad, nie self.grad = out.grad.
Jeśli loss eksploduje zamiast spadać, prawdopodobnie learning rate jest za duży. Zacznij od 0.01-0.05 i eksperymentuj. Za mały learning rate = wolne uczenie, za duży = chaos.
Więcej o tym, jak dobierać hiperparametry i debugować modele, znajdziesz w przewodniku po debugowaniu modeli AI.
Zbudowanie własnego silnika autograd to jedno. Żeby naprawdę wykorzystać AI w pracy - potrzebujesz systematycznej wiedzy. Na darmowym webinarze na żywo pokazuję krok po kroku, jak oszczędzać 10 godzin tygodniowo dzięki AI - bez wiedzy technicznej.
Zapisz się na darmowy webinar →Wolisz uczyć się we własnym tempie? Sprawdź kurs AI Evolution
Nie. Wystarczy matematyka na poziomie liceum - pochodne podstawowych funkcji (potęga, suma, iloczyn). Jeśli pamiętasz, że pochodna x² to 2x, dasz radę. Reszta to logika programowania - budowanie grafu i przechodzenie przez niego w odpowiedniej kolejności.
Ta sama wartość może być użyta w wielu miejscach grafu obliczeniowego. Jeśli a wpływa na wynik przez dwie różne ścieżki, jej gradient to suma wpływów z obu ścieżek. Dlatego używasz += zamiast = w funkcjach _backward.
Tak, rdzeń jest identyczny - graf obliczeniowy i propagacja wsteczna. Różnica to skala i optymalizacje: produkcyjne biblioteki operują na tensorach (wielowymiarowych tablicach), używają GPU i mają setki optymalizacji wydajnościowych. Fundamentalny mechanizm - ten, który właśnie zbudowałeś - jest ten sam.
Naturalnym krokiem jest nauka PyTorcha lub TensorFlow. Teraz rozumiesz, co dzieje się pod maską, więc będziesz uczyć się 10x szybciej. Możesz też rozbudować micrograd o obsługę tensorów, więcej funkcji aktywacji (sigmoid, tanh) lub optymalizatory (Adam, RMSprop). Jeśli interesuje Cię praktyczne zastosowanie, sprawdź przewodnik po budowaniu agentów AI.
Przejście przez ten poradnik i zbudowanie działającego silnika to 2-3 godziny. Pełne zrozumienie mechanizmu - kilka dni eksperymentowania z różnymi architekturami i zbiorami danych. Jeśli poświęcisz ten czas, będziesz rozumieć sieci neuronowe lepiej niż 90% osób, które tylko kopiują kod z tutoriali.
Masz teraz działający silnik propagacji wstecznej. Od zera. Rozumiesz, jak graf obliczeniowy buduje się podczas forward pass, jak gradienty płyną wstecz podczas backward pass i jak sieć neuronowa uczy się minimalizując loss.
To nie jest wiedza teoretyczna. To fundament, na którym stoją GPT-5, Claude, Gemini i każdy inny model deep learningowy. Różnica między Tobą a kimś, kto tylko używa gotowych bibliotek? Ty wiesz, co dzieje się pod maską. To daje Ci przewagę.
Jeden krok na start: Otwórz notebook, skopiuj kod klasy Value z kroku 1 i zbuduj prosty graf obliczeniowy - dwa wejścia, jedno dodawanie, jedno mnożenie. Wywołaj backward() i wypisz gradienty. Zobaczysz, że to działa. To będzie Twój pierwszy krok do zrozumienia, jak naprawdę działają sieci neuronowe.
Na podstawie: Andrej Karpathy - Neural Networks: Zero to Hero (materiał edukacyjny)
10 gotowych promptów do codziennej pracy + 5 narzędzi + plan na pierwszy tydzień. PDF, 4 strony konkretu.