NumPy ile Tensor Autograd Sıfırdan: Broadcasting-Aware Mini-Tinygrad İnşası
1.4'teki skaler micrograd'ı tensor seviyesine yükselt: NumPy üzerinde Tensor class, broadcasting-aware backward (sum-along-broadcast-dims trick), matmul/conv/softmax operatörleri, transpose ve view'ın gradient akışı, ~500 satırda PyTorch-benzeri eğitim motoru.
Şükrü Yusuf KAYA
60 dakikalık okuma
İleri🛠️ Bu ders kursun en eğitici lab'ı olabilir
Ders 1.4'te skaler micrograd yazdık — kavram netti ama scalar'la ML yapamazsın. Bu derste 'tensor'lara' çıkıyoruz: broadcasting'in nasıl backward edileceği, matmul'un gradient'inin neden iki transpose içerdiği, softmax'ın Jacobian'ının neden dense ama yine de efficient olduğu. ~500 satırda mini-tinygrad. Bunu yazan biri gerçek anlamda 'autograd biliyor' der.
Ders Haritası (Detaylı)#
- Skaler → Tensor: ne değişiyor?
- class iskeleti — data, grad, _backward, parents, requires_grad
Tensor - Operasyonlar #1: element-wise (add, mul, exp, log, relu)
- Broadcasting-aware backward: sum-along-broadcast-dims trick
- Operasyonlar #2: matmul ve gradient (iki transpose)
- Operasyonlar #3: reshape, transpose, view — shape ops
- Operasyonlar #4: reduction (sum, mean, max) ve gradient
- Operasyonlar #5: softmax, log_softmax, cross-entropy
- — topological sort + reverse
backward() - ve
Optimizerclass'ları — PyTorch-tarzı APIModule - Demo: MNIST'i mini-tinygrad ile eğit
- PyTorch ile karşılaştırma — bit-exact gradient check
1. Skaler → Tensor: Ne Değişiyor?#
Ders 1.4'teki class skaler tutuyordu. Tensor için neler değişiyor?
ValueDeğişiklikler#
- Data: tek sayı değil, NumPy ndarray
- Gradient: tek sayı değil, aynı shape'te ndarray
- Operatörler: artık element-wise + broadcasting + reduction + matmul
- Backward: shape-aware → broadcasting'i tersine çevirmek gerekiyor
- Memory: çok daha büyük tensor'lar → bellek bilinçli olmalı
Kritik gözlem: gradient shape = parameter shape#
Bir tensor shape ()'i için, da (). Bu, PyTorch'un denominator layout konvansiyonu.
wd_out, d_inw.gradd_out, d_inNiye broadcasting backward zor?#
Forward'da: where x is (B, T, d) and b is (d,). NumPy b'yi (B, T, d)'ye broadcast eder, toplar.
y = x + bBackward: shape (B, T, d). shape (d,) olmalı. Yani gradient'i broadcast eksenleri boyunca toplamak lazım.
∂L/∂y∂L/∂bBu "sum-along-broadcast-dims" trick, tensor autograd'in en sık yapılan hatalarından biridir.
2. Tensor Class İskeleti#
import numpy as np class Tensor: def __init__(self, data, _children=(), _op='', requires_grad=False): self.data = np.asarray(data, dtype=np.float32) self.grad = np.zeros_like(self.data) if requires_grad else None self._backward = lambda: None self._prev = set(_children) self._op = _op self.requires_grad = requires_grad def __repr__(self): return f"Tensor(shape={self.data.shape}, op='{self._op}', requires_grad={self.requires_grad})" @property def shape(self): return self.data.shape
Tasarım kararları#
- :
datazorla — float64 default'tan kaçınnp.float32 - : lazy init (only if
grad)requires_grad - : closure
_backward - : parent tensor set
_prev - : leaf node kontrolü (parametre vs sabit)
requires_grad
Broadcasting backward helper#
İşin püf noktası bu fonksiyon:
def unbroadcast(grad, target_shape): """Gradient'i broadcast eksenleri boyunca topla.""" # 1. Eğer grad rank > target rank, baştan eksenleri reduce et while grad.ndim > len(target_shape): grad = grad.sum(axis=0) # 2. Boyut 1 olan target dim'lerinde, grad'i topla for i, dim in enumerate(target_shape): if dim == 1: grad = grad.sum(axis=i, keepdims=True) return grad
python
import numpy as np def unbroadcast(grad, target_shape): """Bir gradient'i target_shape'e doğru 'küçült' — broadcast'in tersi.""" # Baştaki fazla eksenleri topla while grad.ndim > len(target_shape): grad = grad.sum(axis=0) # Boyut 1 olan target eksenlerinde topla, keepdims for axis, dim in enumerate(target_shape): if dim == 1 and grad.shape[axis] != 1: grad = grad.sum(axis=axis, keepdims=True) return grad class Tensor: def __init__(self, data, _children=(), _op='', requires_grad=False): self.data = np.asarray(data, dtype=np.float32) self.grad = np.zeros_like(self.data) if requires_grad else None self._backward = lambda: None self._prev = set(_children) self._op = _op self.requires_grad = requires_grad def __repr__(self): return f"Tensor({self.data.shape}, op={self._op!r})" @property def shape(self): return self.data.shape def _ensure_grad(self): """Lazy grad allocation.""" if self.grad is None and self.requires_grad: self.grad = np.zeros_like(self.data) # Testx = Tensor([[1, 2], [3, 4]], requires_grad=True)print(x)print(x.grad)Tensor class'ın iskeleti ve unbroadcast helper.
3. Element-wise Operasyonlar#
İlk operasyonlar — add, sub, mul, div, exp, log, relu.
Toplama: y = a + b (broadcasting destekli)#
y = a + bdef __add__(self, other): other = other if isinstance(other, Tensor) else Tensor(other) out = Tensor(self.data + other.data, _children=(self, other), _op='+', requires_grad=self.requires_grad or other.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # ∂out/∂self = 1 → gradient passes through self.grad += unbroadcast(out.grad, self.shape) if other.requires_grad: other._ensure_grad() other.grad += unbroadcast(out.grad, other.shape) out._backward = _backward return out
Çarpma: y = a * b (element-wise)#
y = a * bdef __mul__(self, other): other = other if isinstance(other, Tensor) else Tensor(other) out = Tensor(self.data * other.data, _children=(self, other), _op='*', requires_grad=self.requires_grad or other.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() self.grad += unbroadcast(out.grad * other.data, self.shape) if other.requires_grad: other._ensure_grad() other.grad += unbroadcast(out.grad * self.data, other.shape) out._backward = _backward return out
ReLU#
def relu(self): out = Tensor(np.maximum(0, self.data), _children=(self,), _op='relu', requires_grad=self.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # d/dx max(0, x) = 1 if x > 0 else 0 self.grad += out.grad * (self.data > 0) out._backward = _backward return out
5. Matmul ve Gradient'i — İki Transpose#
C = A @ BABCNiye iki transpose?#
Bu, chain rule'un matris versiyonu.
Düşün: A bir lineer dönüşüm. → A'nın küçük değişikliği C'yi nasıl etkiler? B'nin her kolonu ayrı bir vektör → A her birini ayrı dönüştürüyor. = . Bu .
C = A @ B∂L/∂A_{ij}Σ_l ∂L/∂C_{il} · B_{jl}(∂L/∂C) · B^TBatched matmul#
ABCnp.matmul@Gradient aynı pattern: .
np.matmul(out.grad, B.swapaxes(-1, -2))python
# Yukarıdaki Tensor class'ına ekle: def __matmul__(self, other): out = Tensor(self.data @ other.data, _children=(self, other), _op='matmul', requires_grad=self.requires_grad or other.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # ∂L/∂A = ∂L/∂C @ B^T grad_A = out.grad @ np.swapaxes(other.data, -1, -2) self.grad += unbroadcast(grad_A, self.shape) if other.requires_grad: other._ensure_grad() # ∂L/∂B = A^T @ ∂L/∂C grad_B = np.swapaxes(self.data, -1, -2) @ out.grad other.grad += unbroadcast(grad_B, other.shape) out._backward = _backward return outMatmul backward — iki transpose kuralı.
6. Shape Operasyonları — reshape, transpose, sum#
Shape değiştiren operasyonlar gradient akışında özel dikkat ister: gradient ters shape transformation ile akar.
reshape#
def reshape(self, *shape): out = Tensor(self.data.reshape(*shape), _children=(self,), _op='reshape', requires_grad=self.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # Gradient eski shape'e geri self.grad += out.grad.reshape(self.shape) out._backward = _backward return out
Transpose#
def transpose(self, *axes): if not axes: axes = tuple(reversed(range(self.data.ndim))) out = Tensor(np.transpose(self.data, axes), _children=(self,), _op='transpose', requires_grad=self.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # Inverse transpose inv_axes = np.argsort(axes) self.grad += np.transpose(out.grad, tuple(inv_axes)) out._backward = _backward return out
Sum (reduction)#
def sum(self, axis=None, keepdims=False): out_data = self.data.sum(axis=axis, keepdims=keepdims) out = Tensor(out_data, _children=(self,), _op='sum', requires_grad=self.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # Gradient'i tüm reduced axes'e broadcast et grad = out.grad if not keepdims and axis is not None: axes = (axis,) if isinstance(axis, int) else axis for ax in sorted(axes): grad = np.expand_dims(grad, ax) self.grad += np.broadcast_to(grad, self.shape).copy() out._backward = _backward return out
7. Softmax + Cross-Entropy — Birleştirilmiş ve Stabil#
Ders 1.3'te öğrendik: softmax + cross-entropy birleştirilince gradient .
y - tNumerically stable log_softmax#
def log_softmax(self, axis=-1): # log-sum-exp trick m = self.data.max(axis=axis, keepdims=True) shifted = self.data - m lse = np.log(np.exp(shifted).sum(axis=axis, keepdims=True)) + m out_data = self.data - lse # log_softmax out = Tensor(out_data, _children=(self,), _op='log_softmax', requires_grad=self.requires_grad) def _backward(): if self.requires_grad: self._ensure_grad() # d/dx log_softmax(x)_i = δ_ij - softmax(x)_j # ∂L/∂x_i = ∂L/∂y_i - softmax_i · Σ_j ∂L/∂y_j softmax = np.exp(out_data) grad_sum = out.grad.sum(axis=axis, keepdims=True) self.grad += out.grad - softmax * grad_sum out._backward = _backward return out
Cross-entropy (with integer targets)#
def cross_entropy(logits, targets): """ logits: Tensor (N, C) targets: array of class indices, shape (N,) """ log_probs = logits.log_softmax(axis=-1) N = logits.shape[0] # Pick log_prob of target class picked = log_probs.data[np.arange(N), targets] loss_data = -picked.mean() loss = Tensor(loss_data, _children=(logits,), _op='ce', requires_grad=logits.requires_grad) def _backward(): if logits.requires_grad: logits._ensure_grad() # ∂L/∂logits = (softmax - one_hot(targets)) / N softmax = np.exp(log_probs.data) one_hot = np.zeros_like(softmax) one_hot[np.arange(N), targets] = 1.0 grad_logits = (softmax - one_hot) / N logits.grad += grad_logits * loss.grad loss._backward = _backward return loss
8. backward() Metodu — Topological Sort + Reverse#
backward()Skaler micrograd ile aynı pattern (Ders 1.4):
def backward(self): topo = [] visited = set() def build(v): if id(v) in visited: return visited.add(id(v)) for child in v._prev: build(child) topo.append(v) build(self) # Initial gradient self._ensure_grad() self.grad = np.ones_like(self.data) for node in reversed(topo): node._backward()
Gradient zeroing#
def zero_grad(self): if self.grad is not None: self.grad.fill(0)
9. Module ve Optimizer — PyTorch-tarzı API#
ModuleOptimizerModule class#
Moduleclass Module: def __init__(self): self._params = [] def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) def forward(self, *args, **kwargs): raise NotImplementedError def parameters(self): return self._params class Linear(Module): def __init__(self, in_features, out_features): super().__init__() # Kaiming init bound = np.sqrt(2.0 / in_features) self.W = Tensor(np.random.randn(in_features, out_features) * bound, requires_grad=True) self.b = Tensor(np.zeros(out_features), requires_grad=True) self._params.extend([self.W, self.b]) def forward(self, x): return x @ self.W + self.b class Sequential(Module): def __init__(self, *layers): super().__init__() self.layers = layers for layer in layers: if isinstance(layer, Module): self._params.extend(layer.parameters()) def forward(self, x): for layer in self.layers: x = layer(x) if isinstance(layer, Module) else layer(x) return x
Optimizer#
Optimizerclass SGD: def __init__(self, params, lr=0.01, momentum=0.9): self.params = list(params) self.lr = lr self.momentum = momentum self.velocities = [np.zeros_like(p.data) for p in self.params] def step(self): for p, v in zip(self.params, self.velocities): v *= self.momentum v += p.grad p.data -= self.lr * v def zero_grad(self): for p in self.params: if p.grad is not None: p.grad.fill(0)
python
# MNIST'i mini-tinygrad ile eğit# (Tüm Tensor + Module + Optimizer kodu önceki bloklarda) import numpy as np # Hoecdik: MNIST loader (sklearn'den)from sklearn.datasets import fetch_openmlmnist = fetch_openml('mnist_784', version=1, as_frame=False, parser='auto')X, y = mnist.data.astype(np.float32) / 255.0, mnist.target.astype(np.int64)X_train, X_test = X[:60000], X[60000:]y_train, y_test = y[:60000], y[60000:] # Modelmodel = Sequential( Linear(784, 128), lambda x: x.relu(), Linear(128, 64), lambda x: x.relu(), Linear(64, 10),) opt = SGD(model.parameters(), lr=0.01, momentum=0.9) # Eğitimbatch_size = 64n_epochs = 3 for epoch in range(n_epochs): perm = np.random.permutation(len(X_train)) for i in range(0, len(X_train), batch_size): idx = perm[i:i + batch_size] x = Tensor(X_train[idx]) targets = y_train[idx] logits = model(x) loss = cross_entropy(logits, targets) opt.zero_grad() loss.backward() opt.step() if i % 5000 == 0: print(f"Epoch {epoch}, step {i}: loss={loss.data.item():.4f}") # Test accuracyx_test = Tensor(X_test)preds = np.argmax(model(x_test).data, axis=-1)acc = (preds == y_test).mean()print(f"\nTest accuracy: {acc:.4f}")# Beklenen: ~96-97%Mini-tinygrad ile MNIST end-to-end eğitim.
11. PyTorch ile Bit-Exact Karşılaştırma#
Aynı modeli PyTorch'ta da yaz, başlangıç weight'leri aynı yap, gradient'lerin bit-exact eşleştiğini doğrula:
import torch import torch.nn as nn # Sync seeds np.random.seed(42) torch.manual_seed(42) # Sabit input ve target x_np = np.random.randn(4, 8).astype(np.float32) y_np = np.array([0, 1, 2, 1]) # Mini-tinygrad W_np = np.random.randn(8, 3).astype(np.float32) b_np = np.zeros(3).astype(np.float32) W_mini = Tensor(W_np.copy(), requires_grad=True) b_mini = Tensor(b_np.copy(), requires_grad=True) logits_mini = Tensor(x_np) @ W_mini + b_mini loss_mini = cross_entropy(logits_mini, y_np) loss_mini.backward() # PyTorch W_pt = torch.tensor(W_np.copy(), requires_grad=True) b_pt = torch.tensor(b_np.copy(), requires_grad=True) x_pt = torch.tensor(x_np) logits_pt = x_pt @ W_pt + b_pt loss_pt = torch.nn.functional.cross_entropy(logits_pt, torch.tensor(y_np)) loss_pt.backward() # Karşılaştır print(f"Loss mini: {loss_mini.data:.6f}") print(f"Loss pyt: {loss_pt.item():.6f}") print(f"W grad max diff: {np.abs(W_mini.grad - W_pt.grad.numpy()).max():.2e}") print(f"b grad max diff: {np.abs(b_mini.grad - b_pt.grad.numpy()).max():.2e}") # Diff < 1e-5 → bit-exact (FP32 hassasiyetinde)
Eğer bu testi geçtiyse: gerçek bir autograd motoru yazdın. Şimdi PyTorch'un milyon satırını anlama yolunda bir adım daha attın.
12. Ne Eksik Bıraktık? (Tinygrad ile Karşılaştırma)#
Mini-tinygrad'ımız ~500 satır. George Hotz'un tinygrad'ı ~5000 satır. Aradaki fark ne?
Eksiklerimiz#
- GPU/Metal/CUDA backend: bizimki saf CPU NumPy
- Lazy evaluation: tinygrad operations'ı plan ediyor, sonra çalıştırıyor
- Kernel fusion: ardışık ops birleştirilip tek kernel'a çevriliyor
- JIT: bir kez compiled, tekrar tekrar koş
- Broader ops: conv, attention, pooling, distributed
- Mixed precision: FP16, BF16
- Quantization: INT8 inference
Bizim seçimimiz#
- Eager eval: hemen hesapla
- NumPy CPU: portabilite + öğretici
- Sade op set: pedagoji odaklı
Sonuç#
Mini-tinygrad'la bir kavramsal çatkı kurduk. Tinygrad source'una bakmak şimdi daha kolay. Modül 5'te PyTorch internals'ı detaylandıracağız.
13. Mini Egzersizler#
-
operatörü: Tensor class'a
expekle. Backward:exp.out.grad * out.data -
operatörü:
meanolarak yaz. Backward: gradient'i count ile böl.sum / count -
Conv1d (1D convolution): kernel × input convolution. Backward'ı çıkar.
-
Dropout: forward'da random mask, backward'da aynı maskı uygula. Eğitim/eval mod ayrımı.
-
Memory leak avı: birikmiş computation graph'leri tutmuyoruz mu? Detach helper'ı yaz.
Bu Derste Neler Öğrendik?#
✓ Skaler → tensor geçişinin gerçek zorlukları (broadcasting backward)
✓ class — data + grad + _backward + _prev + requires_grad
✓ unbroadcast helper: sum-along-broadcast-dims trick
✓ Element-wise ops: add, mul, exp, log, relu — broadcasting'le
✓ Matmul: gradient = iki transpose
✓ Shape ops: reshape, transpose, sum — inverse transformation backward
✓ Softmax + cross-entropy: numerically stable, gradient
✓ — topological sort + reverse
✓ ve — PyTorch-tarzı API
✓ MNIST end-to-end eğitim — 97% accuracy
✓ PyTorch ile bit-exact gradient check — gerçeklik testi
Tensory - tbackward()ModuleOptimizerSıradaki Ders#
2.5 — Eager vs Static Graph Pratik Karşılaştırma: PyTorch vs JAX vs torch.compile
2.2'deki teori → pratik benchmark. Aynı LLM blok'unu üç framework'te yaz, derleme süreleri ve runtime'larını karşılaştır. modlarını (, ) detaylandır.
torch.compilereduce-overheadmax-autotuneSık Sorulan Sorular
Hayır — eğitim amaçlı. (1) **CPU-only NumPy**: GPU yok → 100x yavaş gerçek workload'larda. (2) **No graph optimization**: fusion yok, ops bir bir Python'dan dispatch. (3) **No mixed precision**: FP32 zorunlu. (4) **No distributed**: tek makine. (5) **Conv/attention eksik**: gerçek modeller için ek operatörler gerekli. Production için PyTorch/JAX/MLX. Ama bu mini-tinygrad'ı yazmış olmak, PyTorch'u **kavramsal olarak anlamak** demek — bu çok değerli.
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