Sıradaki

Mini autograd

Zorluk: Zor

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:

  1. Kütüphane ifadeyi hesaplarken her işlemi kaydeder. Bu kayda hesaplama grafiği (computational graph) denir.
  2. 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şta 0.0’dır.

Sınıf üç işlemi desteklemeli:

  • a + b: iki Value’yu toplar.
  • a * b: iki Value’yu çarpar.
  • a.tanh(): tanh⁡(a)\tanh(a)‘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:

  1. Sonucun grad’i 11‘dir.
  2. Her işlem, kendi sonucunun grad’ini yerel türeviyle (local derivative) çarpar.
  3. İşlem, çarpımı girdilerine ekler, üstüne yazmaz.

c bir işlemin sonucu, a da girdisiyse:

a.grad+=∂c∂a⋅c.grad\text{a.grad} \mathrel{+}= \frac{\partial c}{\partial a} \cdot \text{c.grad}

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:

c=a+b:∂c∂a=1,∂c∂b=1c=a⋅b:∂c∂a=b,∂c∂b=ac=tanh⁡(a):∂c∂a=1−c2\begin{aligned} c = a + b:&\quad \frac{\partial c}{\partial a} = 1, \quad \frac{\partial c}{\partial b} = 1 \\ c = a \cdot b:&\quad \frac{\partial c}{\partial a} = b, \quad \frac{\partial c}{\partial b} = a \\ c = \tanh(a):&\quad \frac{\partial c}{\partial a} = 1 - c^2 \end{aligned}

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:

  1. İfadedeki her ismi verilen sayıyla bir Value’ya çevirir.
  2. İfadeyi kurar.
  3. Sonucun backward() metodunu çağırır.
  4. {'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 hangi Value’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.

  1. Sonuçtan başla.
  2. Her Value’nun girdilerini ziyaret et. Aynı Value’yu bir kez ziyaret et.
  3. 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: data ve başta 0.0 olan grad.
  • a + b, a * b ve a.tanh() yeni bir Value döndürür ve işlenenleri değiştirmez. İşlenenler Value’dur. Sayılarla işlem ve çıkarma gerekmez.
  • backward() sonucun grad’ini 1.0 yapar ve ifadedeki her Value’nun grad’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. Bir Value birden çok kez kullanılabilir: x * x, x + x ya 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.

out=(a+b)⋅c\text{out} = (a + b) \cdot c için a=1a = 1, b=2b = 2, c=4c = 4 alalım. İleri geçişte s=a+b=3s = a + b = 3 ve out=12\text{out} = 12 olur. Geri geçiş üç adımdır:

  1. Sonucun gradyanı 11‘dir.
  2. Çarpma bunu girdilerine yerel türevleriyle iletir: ss için c⋅1=4c \cdot 1 = 4, cc için s⋅1=3s \cdot 1 = 3.
  3. Toplama ss‘nin gradyanını olduğu gibi iletir: aa ve bb için 44.

Aynı değer birden çok yerde kullanılırsa her kullanım kendi gradyanını getirir ve hepsi toplanır. x⋅xx \cdot x‘te xx iki yoldan gradyan alır:

  • Her yolun gradyanı xx‘tir.
  • Toplamları 2x2x‘tir, yani x2x^2‘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.

  1. Her işlem, yeni Value’ya girdilerini (_prev) ve gradyanı onlara ileten _backward fonksiyonunu verir.
  2. backward() grafiği sonuçtan girdilere doğru dolaşır ve bir topological order çıkarır.
  3. 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 * x için x.grad, 2x2x yerine xx çı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?

solution.py
import mathclass Value:    """Hesaplama grafiğinin bir düğümü: bir sayı (data) ve sonucun ona göre türevi (grad)."""    def __init__(self, data: float):        self.data = float(data)        self.grad = 0.0        # Grafiği kurmak için gereken alanları buraya ekleyebilirsin.    def __repr__(self) -> str:        return f"Value(data={self.data}, grad={self.grad})"    def __add__(self, other: "Value") -> "Value":        # Kodunu buraya yaz.        return Value(0.0)    def __mul__(self, other: "Value") -> "Value":        # Kodunu buraya yaz.        return Value(0.0)    def tanh(self) -> "Value":        # Kodunu buraya yaz.        return Value(0.0)    def backward(self) -> None:        # Kodunu buraya yaz.        pass

İçindekiler

Geri bildirim

Tür