Mini autograd
Zorluk: Zor Çözüldüsinir ağlarıkalkülüs
Sinir ağı kütüphaneleri gradyanları (gradient) senin yerine hesaplar. Buna otomatik türev alma (automatic differentiation), kısaca autograd denir.
Autograd iki adımda hesaplar:
- Kütüphane ifadeyi hesaplarken her işlemi kaydeder. Bu kayda hesaplama grafiği (computational graph) denir.
- Sonra kaydı geriye doğru izler ve her değerin sonuca etkisini bulur.
Hesaplama grafiğinde:
- Düğümler değerlerdir.
- Kenarlar, bir değerin hangi işlemde kullanıldığını gösterir.
Value adında skaler bir autograd sınıfı yaz. Her Value iki şey taşır:
data: tek bir sayı.grad: sonucun o sayıya göre türevi. Başta0.0’dır.
Sınıf üç işlemi desteklemeli:
a + b: ikiValue’yu toplar.a * b: ikiValue’yu çarpar.a.tanh(): ‘yı hesaplar.
Üçü de yeni bir Value döndürür. İskelette __init__ ve __repr__ hazır. Kalan metotları sen yaz.
Bir ifadeyi kur ve sonucun backward() metodunu çağır. İfadedeki her Value’nun grad’i, sonucun o Value’ya göre kısmi türevi (partial derivative) olur:
a, b = Value(2.0), Value(3.0)out = a * b + aout.backward()print(a.grad, b.grad) # 4.0 2.0
Geri geçiş (backward pass) sonuçtan başlar:
- Sonucun
grad’i ‘dir. - Her işlem, kendi sonucunun
grad’ini yerel türeviyle (local derivative) çarpar. - İşlem, çarpımı girdilerine ekler, üstüne yazmaz.
c bir işlemin sonucu, a da girdisiyse:
Bu, zincir kuralıdır (chain rule). a’nın c üzerindeki yerel etkisi, c’nin sonuç üzerindeki etkisiyle çarpılır. Yerel türevler şunlardır:
Test durumlarında ifade bir metindir: 'a * b + a' ya da bir ara değere ad veren 's = x * y; s * s'. Testler her ifade için şunları yapar:
- İfadedeki her ismi verilen sayıyla bir
Value’ya çevirir. - İfadeyi kurar.
- Sonucun
backward()metodunu çağırır. {'data': sonucun değeri, 'grads': {isim: grad}}döndürür.
Örnekler
Girdi: expr = 'a * b + a', a = 3.0, b = -2.0
Çıktı: {'data': -3.0, 'grads': {'a': -1.0, 'b': 3.0}}
Girdi: expr = 's = x * y; (s + c) * (s * d)', x = 1.0, y = 2.0, c = 0.5, d = 2.0
Çıktı: {'data': 10.0, 'grads': {'x': 18.0, 'y': 9.0, 'c': 4.0, 'd': 5.0}}
Girdi: expr = '(a * b).tanh()', a = 0.5, b = -1.0
Çıktı: {'data': -0.46211715726000974, 'grads': {'a': -0.7864477329659274, 'b': 0.3932238664829637}}
İpucu 1
Yeni Value’yu üretirken iki şeyi saklaman gerekir:
- Yeni
Value’nun hangiValue’lardan doğduğu. grad’ini bu girdilere nasıl ileteceği.
backward() bunları sonradan çağırır. Bu yüzden geri adımı bir iç fonksiyon olarak yazıp yeni Value’ya iliştirmek işe yarar.
İpucu 2
grad’e = ile yazma, += kullan. Aynı Value iki işlemin girdisi olabilir. x * x içinde self ve other aynı nesnedir.
grad’e = ile yazarsan son yazılan öncekini siler.
İpucu 3
Grafiği sonuçtan girdilere doğru dolaşıp bir sıra çıkar.
- Sonuçtan başla.
- Her
Value’nun girdilerini ziyaret et. AynıValue’yu bir kez ziyaret et. - Girdileri ziyaret edildikten sonra
Value’yu bir listeye ekle.
Bu liste hesaplamanın ileri sırasıdır. backward() bu listeyi tersten işler ve her düğümün geri adımını çağırır.
Gereksinimler
Value(data)iki özellik taşır:datave başta0.0olangrad.a + b,a * bvea.tanh()yeni birValuedöndürür ve işlenenleri değiştirmez. İşlenenlerValue’dur. Sayılarla işlem ve çıkarma gerekmez.backward()sonucungrad’ini1.0yapar ve ifadedeki herValue’nungrad’ini hesaplar. Her ifade için bir kez çağrılır. Birbirinden bağımsız ifadelerin gradyanları karışmaz.- Gradyanları
+=ile topla,=ile atama. BirValuebirden çok kez kullanılabilir:x * x,x + xya da paylaşılan bir ara değer. backward()düğümleri topolojik sırada (topological order) işler. Bir düğüm gradyanını girdilerine, onu kullanan her düğümden gradyanını aldıktan sonra iletir.
Her işlem, sonucun hangi Value’lardan ve hangi yerel türevle çıktığını saklar. Geri geçiş bu kaydı tersten okur.
için , , alalım. İleri geçişte ve olur. Geri geçiş üç adımdır:
- Sonucun gradyanı ‘dir.
- Çarpma bunu girdilerine yerel türevleriyle iletir: için , için .
- Toplama ‘nin gradyanını olduğu gibi iletir: ve için .
Aynı değer birden çok yerde kullanılırsa her kullanım kendi gradyanını getirir ve hepsi toplanır. ‘te iki yoldan gradyan alır:
- Her yolun gradyanı ‘tir.
- Toplamları ‘tir, yani ‘nin türevidir.
- Toplamak yerine atarsan yalnızca bir yol sayılır.
Sıra da önemlidir. Bir ara değer hem doğrudan hem de başka işlemler üzerinden kullanılabilir.
Ara değerin gradyanı ancak onu kullanan her düğüm işlendikten sonra tamamlanır. Ara değer daha erken işlenirse girdilerine eksik bir gradyan yayılır. Bu yüzden backward() düğümleri topological order’a göre işler.
PyTorch ve JAX gibi kütüphanelerin autograd’i aynı fikrin tensörlere uygulanmış hâlidir. Skalerlerle çalışınca her adımı tek tek görebilirsin.
Daha fazlası için Geri yayılım dersine bak.
Her işlem geri adımını saklar, backward() bunları topological order’da çağırır.
- Her işlem, yeni
Value’ya girdilerini (_prev) ve gradyanı onlara ileten_backwardfonksiyonunu verir. backward()grafiği sonuçtan girdilere doğru dolaşır ve bir topological order çıkarır.backward()bu sırayı tersten işler.
import mathclass Value: def __init__(self, data: float): self.data = float(data) self.grad = 0.0 self._prev = () self._backward = lambda: None def __repr__(self) -> str: return f"Value(data={self.data}, grad={self.grad})" def __add__(self, other: "Value") -> "Value": 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 def __mul__(self, other: "Value") -> "Value": 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 def tanh(self) -> "Value": out = Value(math.tanh(self.data)) out._prev = (self,) def _backward(): self.grad += (1 - out.data**2) * out.grad out._backward = _backward return out def backward(self) -> None: order = [] visited = set() def visit(v: "Value") -> None: if v not in visited: visited.add(v) for child in v._prev: visit(child) order.append(v) visit(self) self.grad = 1.0 for v in reversed(order): v._backward()
Sık yapılan iki hata var:
- Gradyanı
+=yerine=ile yazmak:x * xiçinx.grad, yerine çıkar. - Sırayı atlamak: sonuçtan başlayıp her düğümü ziyaret edildiği anda işlersen, paylaşılan bir ara değer gradyanı tamamlanmadan girdilerine yayar. Sonuç yanlış çıkar.
Bu problemde bir sorun mu var?