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🔧 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ı#
- Neden autograd? Manuel backprop'un sınırları
- Computational graph — bir DAG olarak yapı
- Value class — node'umuzun anatomisi
- Operator overloading: ,
+,*,**—tanhclosure'ları_backward - metodu: topological sort + reverse traversal
backward() - Sayısal gradient ile doğrulama
- Mini-MLP: micrograd ile sıfırdan bir nöral ağ inşa et ve eğit
- PyTorch ile karşılaştırma: aynı sonuç, daha az kod
- 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öntem | Nasıl | Hız | Bellek |
|---|---|---|---|
| Sembolik (SymPy gibi) | Formülü sembolik manipüle et | Yavaş, expression bloat | Az |
| Numerik (finite differences) | (f(x+ε) - f(x)) / ε | Yavaş, n eval | Az |
| Forward-mode autodiff | Dual numbers ile sayısal | Hızlı | Az |
| Reverse-mode autodiff | Computational graph + backward | Hı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#
y = (a + b) * ca b \ / (+) | d = a + b | * ─── c | y = d * c
a, b, cd, yForward 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∂y/∂y = 1Her node'da zincir kuralı:
- (y = d*c,
∂y/∂d = c'ye göre)d - (y = d*c,
∂y/∂c = d'ye göre)c - (d = a+b,
∂y/∂a = ∂y/∂d · ∂d/∂a = c · 1 = c'ya göre)a ∂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.
3. Value Class — Her Şeyin Atomu#
Micrograd'da tek bir veri yapısı var: . Tek bir skaler tutar + metadata:
Valueclass 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:
- : forward değeri
data - :
grad(henüz 0, backward'da dolacak)∂loss/∂self - : bu node'un gradient'ini parent'larından alıp child'larına nasıl yayacağını söyleyen closure
_backward - : bu node'u oluşturan child'lar (graph kenarları için)
_prev - : işlem tipi (görselleştirme için)
_op
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})" # Testa = 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#
_backwardŞimdi Python'un , , operatörlerini Value class için overload edeceğiz. Aynı zamanda her operasyonun local türev kuralını closure'ında tanımlayacağız.
+***_backwardToplama: out = a + b#
out = a + bForward:
Lokal türev: ,
Zincir kuralı (parent gradient 'i child'lara yay):
out.data = a.data + b.data∂out/∂a = 1∂out/∂b = 1out.grada.grad += out.grad * 1b.grad += out.grad * 1
Çarpma: out = a * b#
out = a * bForward:
Lokal türev: ,
Zincir:
out.data = a.data * b.data∂out/∂a = b∂out/∂b = aa.grad += out.grad * b.datab.grad += out.grad * a.data
Üst alma: out = a ** n (n sabit)#
out = a ** nForward:
Lokal türev:
Zincir:
out.data = a.data ** n∂out/∂a = n * a^(n-1)a.grad += out.grad * n * a.data ** (n-1)tanh: out = tanh(a)#
out = tanh(a)Forward:
Lokal türev:
Zincir:
out.data = (e^{2a}-1)/(e^{2a}+1)1 - tanh²(a) = 1 - out²a.grad += out.grad * (1 - out.data ** 2)Önemli: += (gradient accumulation)#
+=Aynı node birden fazla yere besleniyorsa (örn. ), her path'ten gelen gradient'i topluyoruz (). Bu yüzden her zaman yazar, yazmaz.
y = a + a+=_backwardgrad += ...grad = ...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 fonksiyonu, kendi yarattığı , , 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.
_backwardselfotherout5. backward() Metodu — Topological Sort + Reverse#
backward()Her node'un kendi 'ı var; ama bunları doğru sırada çağırmamız gerekiyor. Sıra şu olmalı:
_backwardOutput'tan başla, child'lara doğru reverse-topological order'da gez.
Yani: bir node'un 'ını çağırmadan önce, tüm parent'ları gradient'ini almış olmalı (çünkü her node parent'tan gradient alır).
_backwardTopological 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()#
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: ile :
y = (a + b) * ca=2, b=-3, c=10python
a = Value(2.0)b = Value(-3.0)c = Value(10.0)d = a + by = d * cprint(y) # Value(data=-10.0, grad=0.0) y.backward() print(a) # Value(data=2.0, grad=10.0) → ∂y/∂a = c = 10print(b) # Value(data=-3.0, grad=10.0) → ∂y/∂b = c = 10print(c) # Value(data=10.0, grad=-1.0) → ∂y/∂c = d = -1print(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:
(central difference — daha doğru)
h = 1e-51e-6python
def f(a, b, c): return (a + b) * c a_val, b_val, c_val = 2.0, -3.0, 10.0h = 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 subclass'ı), ile otomatik sayısal kontrol yapabilirsin. Bu, custom CUDA/Triton kernel'lerini debug ederken hayat kurtarır. Modül 37'de detayda.
torch.autograd.Functiontorch.autograd.gradcheck8. 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 classificationxs = [ [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] # Modelrandom.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: memory tasarrufu (ama bazen autograd ile çakışır)
a.add_(b)
Higher-order derivatives#
backward()grad_gradDistributed#
DistributedDataParallelStatic analysis#
torch.compileModül 5'te (PyTorch Engineering) detayda işliyoruz.
11. Mini Egzersizler#
-
operator: Value class'a
expmetodu ekle. Forward:exp. Backward:e^x(çünküout * d_out). Test et.d/dx e^x = e^x -
operator: ReLU'yu ekle. Backward kuralı:
relu.grad = out_grad * (1 if x > 0 else 0) -
Aynı node iki kere kullanılırsa?:. Manuel gradient'i hesapla, ardından micrograd çıkarın aynı sonucu vermesini doğrula. (İpucu:
y = a * a + a3 kez güncellenmeli.)a.grad -
Gradient zeroing: Bir eğitim adımındaçağırmayı unutursan ne olur? Loss'un 2. step'te neden iki katı olduğunu açıkla.
p.grad = 0.0 -
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: , , , ile lokal türev kurallarını birleştirme
✓ : 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
+***tanhbackward()Sıradaki Ders#
1.5 — Olasılık Temelleri: Joint, Marginal, Conditional, Bayes
LLM'ler özünde conditional probability machines: . Olasılığın temellerini, Bayes teoreminin gücünü ve LLM'lerin neden 'olasılıksal' olduğunu kavrayacağız.
P(x_t | x_{<t})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
Modül 0: Kurs Çerçevesi ve Atölye Kurulumu
LLM Engineer Kimdir? Junior'dan Staff'a Yapay Zekâ Mühendisliği Kariyer Haritası
Öğrenmeye BaşlaModül 0: Kurs Çerçevesi ve Atölye Kurulumu
Kurs Felsefesi: Neden Bu Yol, Neden Bu Sıra — 8 Aylık Müfredatın İskeleti
Öğrenmeye BaşlaModül 0: Kurs Çerçevesi ve Atölye Kurulumu