İçeriğe geç

Chain Rule ve Backpropagation: Mini-Autograd'ı Sıfırdan İnşa Et (Karpathy micrograd Türkçe)

Karpathy'nin micrograd'ını Türkçe sıfırdan inşa etmek — 200 satır PyTorch-benzeri otomatik türev motoru. Computational graph, topological sort, operator overloading, _backward closures, gradient accumulation. Sonunda bir MLP'yi eğit.

Şükrü Yusuf KAYA
45 dakikalık okuma
Orta
Chain Rule ve Backpropagation: Mini-Autograd'ı Sıfırdan İnşa Et (Karpathy micrograd Türkçe)
🔧 Bu ders kursun en eğitici lab'larından biri
Karpathy 'Neural Networks: Zero to Hero' serisinin 1. videosu 2.5 milyon izlenme aldı çünkü bu — derinlemesine anlama. Bu derste micrograd'ı Türkçe sıfırdan inşa edeceğiz: tek bir Value class, 4 operator, 1 backward() metodu. 45 dakika sonunda PyTorch autograd'i 'sihir' olarak görmekten kurtulacaksın. Hatta sevip eleştirebileceksin.

Ders Haritası#

  1. Neden autograd? Manuel backprop'un sınırları
  2. Computational graph — bir DAG olarak yapı
  3. Value class — node'umuzun anatomisi
  4. Operator overloading:
    +
    ,
    *
    ,
    **
    ,
    tanh
    _backward
    closure'ları
  5. backward()
    metodu
    : topological sort + reverse traversal
  6. Sayısal gradient ile doğrulama
  7. Mini-MLP: micrograd ile sıfırdan bir nöral ağ inşa et ve eğit
  8. PyTorch ile karşılaştırma: aynı sonuç, daha az kod
  9. Sınırlar: niye gerçek autograd çok daha karmaşık

1. Neden Autograd?#

Önceki derste (1.3) manuel backprop'u 4 katmanlı bir ağ için yazdık. Çalıştı. Ama:
  • 80 katmanlı Llama 70B için kim el ile yazacak?
  • Yeni bir mimari icat ettin (örn. Mamba): backprop'unu yeniden mi yazıyorsun?
  • Bir hata yaptın: yanlış işaret. Loss düşmüyor ama hangi türev yanlış?
Autograd = bu sorunları çözen tasarım deseni. Sen forward pass'i yazarsın; o gradient'i otomatik üretir.

Otomatik türevin iki yolu#

YöntemNasılHızBellek
Sembolik (SymPy gibi)Formülü sembolik manipüle etYavaş, expression bloatAz
Numerik (finite differences)
(f(x+ε) - f(x)) / ε
Yavaş, n evalAz
Forward-mode autodiffDual numbers ile sayısalHızlıAz
Reverse-mode autodiffComputational graph + backwardHızlı (1 pass)Tüm ara aktiviteler
LLM eğitiminin standart'ı: reverse-mode (backprop). Bizim yazacağımız bu.

2. Computational Graph — Bir DAG#

Her hesap, aslında bir Directed Acyclic Graph (yönlü asiklik graf). Düğümler: değerler (tensor'lar). Kenarlar: işlemler.

Örnek:
y = (a + b) * c
#

a b \ / (+) | d = a + b | * ─── c | y = d * c
a, b, c
leaf nodes (girdi).
d, y
intermediate nodes (hesaplanmış).

Forward pass#

Düğümleri topological order'da hesapla: önce input'lar, sonra output'lar. Üstteki örnekte sıra: a, b, c → d → y.

Backward pass#

y
'nin loss olduğunu varsayalım.
∂y/∂y = 1
ile başla. Reverse topological order'da geri git: y → d → a, b, c.
Her node'da zincir kuralı:
  • ∂y/∂d = c
    (y = d*c,
    d
    'ye göre)
  • ∂y/∂c = d
    (y = d*c,
    c
    'ye göre)
  • ∂y/∂a = ∂y/∂d · ∂d/∂a = c · 1 = c
    (d = a+b,
    a
    'ya göre)
  • ∂y/∂b = ∂y/∂d · ∂d/∂b = c · 1 = c
İşte autograd. Her node, kendi türevini ve child'larına geri yayılma kuralını biliyor.
Computational graph — forward (ileri) ve backward (geri) pass.
Computational graph örneği: forward'da değer akar, backward'da gradient akar.

3. Value Class — Her Şeyin Atomu#

Micrograd'da tek bir veri yapısı var:
Value
. Tek bir skaler tutar + metadata:
class Value: def __init__(self, data, _children=(), _op=''): self.data = data # gerçek sayı self.grad = 0.0 # bu node'a göre türev (backward sonrası dolacak) self._backward = lambda: None # bu node'un gradient'i child'larına nasıl yayar self._prev = set(_children) # bu node'u oluşturan child'lar self._op = _op # debug için (örn. '+', '*')
Önemli alanlar:
  • data
    : forward değeri
  • grad
    :
    ∂loss/∂self
    (henüz 0, backward'da dolacak)
  • _backward
    : bu node'un gradient'ini parent'larından alıp child'larına nasıl yayacağını söyleyen closure
  • _prev
    : bu node'u oluşturan child'lar (graph kenarları için)
  • _op
    : işlem tipi (görselleştirme için)
python
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}, grad={self.grad})"
 
# Test
a = Value(2.0)
b = Value(-3.0)
print(a) # Value(data=2.0, grad=0.0)
print(b) # Value(data=-3.0, grad=0.0)
Value class'ın temel iskeleti.

4. Operatörler — Operator Overloading +
_backward
Closure#

Şimdi Python'un
+
,
*
,
**
operatörlerini Value class için overload edeceğiz. Aynı zamanda her operasyonun local türev kuralını
_backward
closure'ında tanımlayacağız.

Toplama:
out = a + b
#

Forward:
out.data = a.data + b.data
Lokal türev:
∂out/∂a = 1
,
∂out/∂b = 1
Zincir kuralı (parent gradient
out.grad
'i child'lara yay):
  • a.grad += out.grad * 1
  • b.grad += out.grad * 1

Çarpma:
out = a * b
#

Forward:
out.data = a.data * b.data
Lokal türev:
∂out/∂a = b
,
∂out/∂b = a
Zincir:
  • a.grad += out.grad * b.data
  • b.grad += out.grad * a.data

Üst alma:
out = a ** n
(n sabit)#

Forward:
out.data = a.data ** n
Lokal türev:
∂out/∂a = n * a^(n-1)
Zincir:
a.grad += out.grad * n * a.data ** (n-1)

tanh:
out = tanh(a)
#

Forward:
out.data = (e^{2a}-1)/(e^{2a}+1)
Lokal türev:
1 - tanh²(a) = 1 - out²
Zincir:
a.grad += out.grad * (1 - out.data ** 2)

Önemli:
+=
(gradient accumulation)#

Aynı node birden fazla yere besleniyorsa (örn.
y = a + a
), her path'ten gelen gradient'i topluyoruz (
+=
). Bu yüzden
_backward
her zaman
grad += ...
yazar,
grad = ...
yazmaz.
python
import math
 
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}, grad={self.grad})"
 
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 * 1.0
other.grad += out.grad * 1.0
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 += out.grad * other.data
other.grad += out.grad * self.data
out._backward = _backward
return out
 
def __pow__(self, other):
assert isinstance(other, (int, float)), "only int/float pow"
out = Value(self.data ** other, (self,), f'**{other}')
 
def _backward():
self.grad += out.grad * (other * self.data ** (other - 1))
out._backward = _backward
return out
 
def tanh(self):
x = self.data
t = (math.exp(2*x) - 1) / (math.exp(2*x) + 1)
out = Value(t, (self,), 'tanh')
 
def _backward():
self.grad += out.grad * (1 - t ** 2)
out._backward = _backward
return out
 
def __radd__(self, other): return self + other
def __rmul__(self, other): return self * other
def __neg__(self): return self * -1
def __sub__(self, other): return self + (-other)
def __truediv__(self, other): return self * (other ** -1)
Value class'ın operatörleri — 50 satır.
🔑 Closure'lar her şeyin anahtarı
Python'un closure özelliği sayesinde her
_backward
fonksiyonu, kendi yarattığı
self
,
other
,
out
değerlerini hatırlar. Bu, autograd'in çekirdeğidir: graph'i tekrar gezmek yerine, her node kendi 'nasıl geri yayarım' bilgisini birlikte taşır.

5.
backward()
Metodu — Topological Sort + Reverse#

Her node'un kendi
_backward
'ı var; ama bunları doğru sırada çağırmamız gerekiyor. Sıra şu olmalı:
Output'tan başla, child'lara doğru reverse-topological order'da gez.
Yani: bir node'un
_backward
'ını çağırmadan önce, tüm parent'ları gradient'ini almış olmalı (çünkü her node parent'tan gradient alır).

Topological sort#

DFS post-order:
def topological_sort(root): topo = [] visited = set() def build(v): if v in visited: return visited.add(v) for child in v._prev: build(child) topo.append(v) # post-order: child'lardan sonra build(root) return topo

backward()
#

def backward(self): topo = topological_sort(self) self.grad = 1.0 # output'un kendine göre türevi 1 for node in reversed(topo): # reverse: output'tan başla node._backward()
Bu kadar. 30 satır toplam autograd motoru.
python
# Yukarıdaki Value class'ına bu metodu ekle:
 
def backward(self):
topo = []
visited = set()
 
def build(v):
if v in visited:
return
visited.add(v)
for child in v._prev:
build(child)
topo.append(v)
 
build(self)
 
# gradient'i sıfırla (defensive)
for v in topo:
v.grad = 0.0
self.grad = 1.0 # output'un kendine göre türevi 1
 
for node in reversed(topo):
node._backward()
30 satırlık `backward()` metodu.

6. Test Et — Bir Hesap Yap, Gradient'leri Doğrula#

Önceki örneğimiz:
y = (a + b) * c
ile
a=2, b=-3, c=10
:
python
a = Value(2.0)
b = Value(-3.0)
c = Value(10.0)
d = a + b
y = d * c
print(y) # Value(data=-10.0, grad=0.0)
 
y.backward()
 
print(a) # Value(data=2.0, grad=10.0) → ∂y/∂a = c = 10
print(b) # Value(data=-3.0, grad=10.0) → ∂y/∂b = c = 10
print(c) # Value(data=10.0, grad=-1.0) → ∂y/∂c = d = -1
print(d) # Value(data=-1.0, grad=10.0) → ∂y/∂d = c = 10
İlk autograd testi — 4 satır.

7. Sayısal Gradient ile Doğrula#

Analitik (autograd) gradient'i, sayısal (numerical / finite-difference) gradient ile karşılaştırarak doğrulayabiliriz:
f(x)f(x+h)f(xh)2hf'(x) \approx \frac{f(x+h) - f(x-h)}{2h}
(central difference — daha doğru)
h = 1e-5
ya da
1e-6
tipik. Çok küçük → floating-point noise; çok büyük → quadratic error.
python
def f(a, b, c):
return (a + b) * c
 
a_val, b_val, c_val = 2.0, -3.0, 10.0
h = 1e-5
 
grad_a_num = (f(a_val + h, b_val, c_val) - f(a_val - h, b_val, c_val)) / (2*h)
grad_b_num = (f(a_val, b_val + h, c_val) - f(a_val, b_val - h, c_val)) / (2*h)
grad_c_num = (f(a_val, b_val, c_val + h) - f(a_val, b_val, c_val - h)) / (2*h)
 
print(f"Numerical: a={grad_a_num:.4f}, b={grad_b_num:.4f}, c={grad_c_num:.4f}")
print(f"Autograd: a={a.grad:.4f}, b={b.grad:.4f}, c={c.grad:.4f}")
# Numerical: a=10.0000, b=10.0000, c=-1.0000
# Autograd: a=10.0000, b=10.0000, c=-1.0000
# ✓ Eşleştiler!
Sayısal gradient ile analitik gradient karşılaştırması — gradient check.
🔍 Gradient check — production praktiği
Custom backward fonksiyonu yazarken (örn. PyTorch'ta
torch.autograd.Function
subclass'ı),
torch.autograd.gradcheck
ile otomatik sayısal kontrol yapabilirsin. Bu, custom CUDA/Triton kernel'lerini debug ederken hayat kurtarır. Modül 37'de detayda.

8. Mini-MLP: Micrograd ile Sıfırdan Nöral Ağ#

Sadece skalerlerle çalışan bir autograd motorumuz var. Hadi bunu kullanarak gerçek bir MLP eğitelim. Sıradan tabular regresyon problemi.
python
import random
 
class Neuron:
def __init__(self, n_in):
self.w = [Value(random.uniform(-1, 1)) for _ in range(n_in)]
self.b = Value(0.0)
 
def __call__(self, x):
# x: list of Value
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_in, n_out):
self.neurons = [Neuron(n_in) for _ in range(n_out)]
 
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 n in self.neurons for p in n.parameters()]
 
 
class MLP:
def __init__(self, n_in, layer_sizes):
sizes = [n_in] + layer_sizes
self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(layer_sizes))]
 
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()]
 
 
# Veri: 4 örnek, 3 özellik, binary classification
xs = [
[Value(2.0), Value(3.0), Value(-1.0)],
[Value(3.0), Value(-1.0), Value(0.5)],
[Value(0.5), Value(1.0), Value(1.0)],
[Value(1.0), Value(1.0), Value(-1.0)],
]
ys = [1.0, -1.0, -1.0, 1.0]
 
# Model
random.seed(42)
mlp = MLP(3, [4, 4, 1])
print(f"Toplam parametre: {len(mlp.parameters())}") # 41
 
# Eğitim döngüsü
for step in range(50):
# Forward + loss (MSE)
preds = [mlp(x) for x in xs]
loss = sum((p - y) ** 2 for p, y in zip(preds, ys))
 
# Backward
for p in mlp.parameters():
p.grad = 0.0
loss.backward()
 
# SGD update
lr = 0.05
for p in mlp.parameters():
p.data -= lr * p.grad
 
if step % 10 == 0 or step == 49:
print(f"Step {step:2d}: loss = {loss.data:.6f}")
 
# Beklenen çıktı:
# Step 0: loss = 4.5
# Step 10: loss = 0.05 (hızla düştü)
# Step 49: loss = 0.0001
# Predictions:
for x, y in zip(xs, ys):
print(f" pred={mlp(x).data:+.3f}, target={y:+.1f}")
Micrograd ile sıfırdan MLP eğitimi — Karpathy klasiği.
🤯 Ne yaptın? Bir bak
Yukarıdaki 80 satırda autograd motorun + MLP altyapın + eğitim döngüsü var. Karpathy'nin 'micrograd' repo'su 200 satır, eğitim örneği dahil. Pythorch'un autograd'ı milyon satırlık ama mantığı bu. Production'da tensor batched + GPU + CUDA + memory optimizations eklenir; matematik özü bu kadar.

9. PyTorch ile Aynı Eğitim#

Aynı problem PyTorch'la:
import torch import torch.nn as nn xs_t = torch.tensor([ [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_t = torch.tensor([1.0, -1.0, -1.0, 1.0]) torch.manual_seed(42) mlp_torch = nn.Sequential( nn.Linear(3, 4), nn.Tanh(), nn.Linear(4, 4), nn.Tanh(), nn.Linear(4, 1), nn.Tanh(), ) opt = torch.optim.SGD(mlp_torch.parameters(), lr=0.05) for step in range(50): preds = mlp_torch(xs_t).squeeze() loss = ((preds - ys_t) ** 2).sum() opt.zero_grad() loss.backward() opt.step() if step % 10 == 0: print(f"Step {step}: loss = {loss.item():.6f}")
Sonuç aynı. PyTorch sadece çok daha hızlı (tensor batched, GPU) ve çok daha çok özellik var (mixed precision, distributed, ...). Ama autograd çekirdeği özünde bizimkiyle aynı algoritma.

10. Gerçek Autograd Ne Ekler?#

Bizim 200 satır vs PyTorch'un milyon satır — fark nereden?

Tensor batching#

Bizimki scalar bazlı. PyTorch (B, T, d) tensor'larla aynı operasyonu batched yapar. CPU/GPU SIMD'ten faydalanır.

GPU acceleration#

CUDA kernel'leri her operatör için. Memory transfer optimizations.

Mixed precision#

FP16/BF16/FP8 desteği, autocast, GradScaler.

Memory optimization#

  • Activation checkpointing: bazı ara aktiviteleri saklamayıp yeniden hesapla (bellek vs compute tradeoff)
  • Inplace ops:
    a.add_(b)
    memory tasarrufu (ama bazen autograd ile çakışır)

Higher-order derivatives#

backward()
'in çıktısı da differentiable.
grad_grad
hesaplayabilirsin.

Distributed#

DistributedDataParallel
— gradient'leri sync, all_reduce ile.

Static analysis#

torch.compile
ile graph'i optimize et, fuse et, JIT et.
Modül 5'te (PyTorch Engineering) detayda işliyoruz.

11. Mini Egzersizler#

  1. exp
    operator
    : Value class'a
    exp
    metodu ekle. Forward:
    e^x
    . Backward:
    out * d_out
    (çünkü
    d/dx e^x = e^x
    ). Test et.
  2. relu
    operator
    : ReLU'yu ekle. Backward kuralı:
    grad = out_grad * (1 if x > 0 else 0)
    .
  3. Aynı node iki kere kullanılırsa?:
    y = a * a + a
    . Manuel gradient'i hesapla, ardından micrograd çıkarın aynı sonucu vermesini doğrula. (İpucu:
    a.grad
    3 kez güncellenmeli.)
  4. Gradient zeroing: Bir eğitim adımında
    p.grad = 0.0
    çağırmayı unutursan ne olur? Loss'un 2. step'te neden iki katı olduğunu açıkla.
  5. Topological sort doğrulaması: 5 node'lu bir graph örnek çiz. DFS post-order'da hangi sırayla ekleneceğini el ile bul. Kod ile karşılaştır.

Bu Derste Neler Öğrendik?#

Autograd'in 3 motivasyonu: ölçek, yeni mimari, hata ✓ Computational graph — DAG yapısı, forward/backward ✓ Value class — data, grad, _backward closure, _prev ✓ Operator overloading:
+
,
*
,
**
,
tanh
ile lokal türev kurallarını birleştirme ✓
backward()
: topological sort + reverse traversal — 30 satır ✓ Sayısal gradient check — analitik gradient'i doğrulama ✓ Mini-MLP: micrograd ile sıfırdan eğitilen 41-parametreli ağ ✓ PyTorch ile karşılaştırma: aynı algoritma, farklı ölçek ✓ Gerçek autograd'ın ek özellikleri: tensor batching, GPU, mixed precision, distributed

Sıradaki Ders#

1.5 — Olasılık Temelleri: Joint, Marginal, Conditional, Bayes LLM'ler özünde conditional probability machines:
P(x_t | x_{<t})
. Olasılığın temellerini, Bayes teoreminin gücünü ve LLM'lerin neden 'olasılıksal' olduğunu kavrayacağız.

Sık Sorulan Sorular

Eğitim verimi için skaler doğru seçim — operatör başına yalnızca 5-10 satır, tüm matematik göz önünde. Tensor versiyonu (örn. tinygrad) çok daha karmaşık çünkü broadcasting, shape inference, GPU dispatch, memory layout hepsini idare etmek gerekiyor. Karpathy'nin yaklaşımı: önce skaler ile prensipleri öğren, sonra tensor versiyonuna geç. Bu kursta Modül 2'de tinygrad benzeri tensor autograd yazacağız (mini-tinygrad lab'ı).

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

İlgili İçerikler