Geri yayılım
Geri yayılım (backpropagation)geri yayılım (backpropagation): Zincir kuralıyla, kaybın her parametreye göre gradyanını çıkıştan girişe doğru hesaplama yöntemi., kaybın (loss) her parametreye göre gradyanını (gradient)gradyan (gradient): Bir fonksiyonun en hızlı arttığı yönü ve bu artışın dikliğini gösteren türev vektörü. tek bir geçişte verir. Hesabı bir hesaplama grafiğine (computational graph)hesaplama grafiği (computational graph): Bir hesabı adımlarına bölüp düğümler ve kenarlarla gösteren yapı: her düğüm bir değerdir, her kenar (ok) bir değerin hangi işlemde kullanıldığını söyler. döker ve zincir kuralını (chain rule)zincir kuralı (chain rule): İç içe fonksiyonların türevini veren kural: dış fonksiyonun iç fonksiyonun değerindeki türevi, iç fonksiyonun türeviyle çarpılır; geri yayılımın dayanağıdır. loss’tan başlayarak adım adım uygular.
Gradyan inişiyle (gradient descent)gradyan inişi (gradient descent): Kaybı azaltmak için parametreleri gradyanın tersine küçük adımlarla güncelleyen yöntem. bir sinir ağını eğitmek için bu gradyanların hepsi gerekir. Parametre sayısı binlerden milyarlara çıkabilir. Geri yayılım olmadan gradyanı iki yolla bulursun:
- Elle çıkarmak: her ağ için pratik değildir.
- Sayısal gradyan (numerical gradient): her parametre için loss’u iki kez hesaplamayı gerektirir.
Bu derste şunları yapacaksın:
- Küçük bir computational graph’ta gradyanların geriye nasıl aktığını göreceksin.
- Bir değer birden çok yerde kullanıldığında ne olduğunu öğreneceksin.
- Zincir kuralını kendi başına uygulayan, tek bir sayı tutan bir nesnenin mantığını kuracaksın.
Bir sonraki ders, Yoğun katmanın geri geçişi ve gradyan kontrolü, aynı kuralı matrislere taşır.
Hesabı bir graph’a dök
Hesabı tek işlemlik adımlara bölersen, bir computational graph elde edersin.
Düğümler ve kenarlar
Tek bir örnekte (sample) doğrusal bir modelin loss’uyla başlayalım.
Girdi , ağırlık (weight)ağırlık (weight): Bir girdiyle çarpılarak onun çıktıya ne kadar katkıda bulunacağını belirleyen parametre. Doğrusal modelde her özniteliğin kendi ağırlığı vardır. , sapma (bias)sapma (bias): Girdilerle çarpılmadan çıktıya eklenen parametre. Doğrusal modelde bütün öznitelikler 0 iken modelin verdiği tahmindir. ve hedef değer (target) olsun. Tahmin (prediction) ‘dir. Loss, tahminle target’ın farkının karesidir:
Bu, ortalama kare hatanın (mean squared error, MSE)ortalama kare hata (mean squared error, MSE): Tahminlerle hedef değerler arasındaki farkların karelerinin ortalaması. Büyük hataları küçüklerden daha ağır cezalandırır. tek örnek için, yani iken aldığı biçimdir. Değerleri , , ve alalım. Hesap, her biri tek bir işlem olan dört adımdır:
- Girdiyi ağırlıkla çarp: .
- Sapmayı ekle: .
- Hedef değeri çıkar: .
- Hatanın karesini al: .
Aynı dört adım NumPy’siz, düz Python’da şöyle yazılır:
x, w, b, y = 3.0, 2.0, 1.0, 10.0 # girdi, weight, bias, targetp = w * x # çarpımz = p + b # tahmine = z - y # hataL = e ** 2 # lossprint(p, z, e, L) # → 6.0 7.0 -3.0 9.0
Her adım yeni bir sayı üretir: çarpımdır, modelin tahminidir, hatadır. Bu sayıların her biri bir düğümdür (node).
Bir düğümden ötekine çizilen ok, ikincisinin hesabında birincinin kullanıldığını gösterir. Bu ok kenar (edge) adını taşır.
İleri geçiş (forward pass)
, , ve hiçbir şeyden hesaplanmaz; bunlar graph’ın başlangıç düğümleridir. ise son düğümdür. Düğümlerin değerlerini başlangıç düğümlerinden ‘ye doğru hesaplamaya ileri geçiş (forward pass) denir.
Bir fonksiyona dökersen, başlangıç değerlerini değiştirip bütün düğümleri yeniden hesaplayabilirsin:
def forward(x, w, b, y): p = w * x z = p + b e = z - y return p, z, e, e ** 2print(forward(3.0, 2.0, 1.0, 10.0)) # → (6.0, 7.0, -3.0, 9.0)print(forward(4.0, 2.0, 1.0, 10.0)) # → (8.0, 9.0, -1.0, 1.0)
Aşağıdaki grafik bu hesabı aynı değerlerle çiziyor. Şunları dene:
Hesaplama grafiği · computational graph · L = (w·x + b − y)²
ileri geçiş · p = 6 · z = 7 · e = -3 · L = 9
- İleri’ye bas: düğümler, girdileri hazır olur olmaz sırayla dolar. Değerler yukarıdaki dört adımdakilerle aynıdır.
- ‘i ya da ‘yi değiştir ve değerlerin yeniden hesaplandığına bak.
Yerel türevler
Computational graph’ın işe yarayan yanı şudur: her düğüm, yalnızca kendi girdilerine bakan tek bir basit işlemdir. Bu yüzden her birinin türevi (derivative) bir bakışta yazılır.
Bir işlemin sonucunun her girdisine göre kısmi türevine (partial derivative)kısmi türev (partial derivative): Birden çok değişkenli bir fonksiyonun, öbür değişkenler sabit tutulup yalnızca bir değişkene göre alınan türevi. yerel türev (local derivative) diyelim. Yerel türev yalnızca o işleme bağlıdır; graph’ın geri kalanından habersizdir. Dört işlemimizin yerel türevleri, forward pass’te bulunan değerlerle şöyledir:
Her yerel türev, bir girdi biraz artınca sonucun ne kadar değiştiğini söyler:
- Toplama değişikliği olduğu gibi geçirir.
- Çıkarma, ikinci girdide işareti çevirir.
- Çarpma, değişikliği diğer girdinin değeriyle büyütür.
- Kare, hatanın iki katını verir.
Çarpma ve kare, forward pass’te hesaplanan , ve değerlerini kullanır. Geriye doğru hesap bu değerlere ihtiyaç duyar. Bu yüzden forward pass’in değerlerini saklarsın.
Koddaki karşılığı şudur:
x, w, b, y = 3.0, 2.0, 1.0, 10.0e = (w * x + b) - y # forward pass'ten saklanan değer: -3.0dp_dw, dp_dx = x, w # p = w * xdz_dp, dz_db = 1.0, 1.0 # z = p + bde_dz, de_dy = 1.0, -1.0 # e = z - ydL_de = 2 * e # L = e ** 2print(dp_dw, dp_dx) # → 3.0 2.0print(dL_de) # → -6.0
Gradyanı geriye taşı
Bir düğümün gradyanı, loss’un o düğümün değerine göre türevidir. Değer biraz artarsa ‘nin ne kadar değişeceğini söyler. Yukarıdaki grafikte önce İleri’ye, sonra Geri’ye bas.
Kenarlar ‘den başlayarak girdilere doğru sırayla yanar ve her düğümün gradyanı yazılır. Bu sayıları birlikte çıkaralım.
Chain rule
Önce tek bir yola bakalım. ‘yi yalnızca ‘nin fonksiyonu olarak yazarsak iç içe iki fonksiyon elde ederiz:
- iç fonksiyon: ;
- dış fonksiyon: .
İç içe fonksiyonların türevini chain rule verir:
Dış fonksiyonun türevini, iç fonksiyonun değerinde hesapla ve iç fonksiyonun türeviyle çarp. için üç adım yeter:
- İç fonksiyonun değerini bul: .
- Dış fonksiyonun türevini bu değerde hesapla: .
- İç fonksiyonun türevini hesapla: .
Son iki sayının çarpımı gradyanı verir:
x, w, b, y = 3.0, 2.0, 1.0, 10.0g = x * w + b - y # iç fonksiyonun değeridf = 2 * g # dış türev, g'de hesaplanırdg = x # iç türevprint(g, df, dg) # → -3.0 -6.0 3.0print(df * dg) # → -18.0wrong = 2 * w * dg # yanlış: dış türev g'de değil w'de hesaplandıprint(wrong) # → 12.0
Dikkat
En sık yapılan hata, dış fonksiyonun türevini iç fonksiyonun değerinde değil, ‘nin kendisinde hesaplamaktır. için gerekir; ‘yi yerine koyarsan yanlış bir sayı çıkar.
Yol boyunca çarpma
Yol uzarsa çarpan sayısı artar. ‘den ‘ye giden yol ‘dir ve gradyan, yol boyunca yerel türevlerin çarpımıdır:
Değerleri koyunca yukarıdaki çıkar. ‘e giden yol da aynı ile başlar. Ortak parçayı her yol için yeniden hesaplamak gereksizdir.
import numpy as np# L <- e <- z <- p <- w yolundaki yerel türevlerpath_w = [-6.0, 1.0, 1.0, 3.0] # dL/de, de/dz, dz/dp, dp/dwpath_x = [-6.0, 1.0, 1.0, 2.0] # son çarpan dp/dx = wprint(np.prod(path_w), np.prod(path_x)) # → -18.0 -12.0
Geri geçiş (backward pass)
Yolları ayrı ayrı çarpmak yerine ‘den başlar ve her düğümde bir önceki sonucu taşırız. Bu işleme geri geçiş (backward pass) denir.
Kural şudur: bir düğümün gradyanı, onu kullanan düğümün gradyanı ile aradaki yerel türevin çarpımıdır. Kuralın ilk çarpanına, yani onu kullanan düğümün getirdiği gradyana, gelen gradyan (upstream gradient) denir.
Her düğümde gelen gradyanı yerel türevle çarp. ‘nin kendisine göre türevi 1’dir ve hesap buradan başlar:
- ‘nin gradyanını 1 al.
- ‘nin gradyanını bul: işleminin yerel türevi olduğundan .
- ‘nin gradyanını bul: işleminde yerel türev olduğundan .
- ‘nin gradyanını bul: işleminde yerel türev olduğundan .
- ‘nin gradyanını bul: işleminde yerel türev olduğundan .
- ‘nin gradyanını bul: işleminde yerel türev olduğundan .
- ‘nin gradyanını bul: işleminde yerel türev olduğundan .
- ‘in gradyanını bul: işleminde yerel türev olduğundan .
x, w, b, y = 3.0, 2.0, 1.0, 10.0e = (w * x + b) - y # forward pass: -3.0dL = 1.0 # L'nin gradyanıde = dL * (2 * e) # kare: yerel türev 2edz = de * 1.0 # çıkarma, z tarafıdy = de * -1.0 # çıkarma, y tarafıdp = dz * 1.0 # toplama, p tarafıdb = dz * 1.0 # toplama, b tarafıdw = dp * x # çarpma: öbür girdinin değeridx = dp * wprint(de, dz, dy) # → -6.0 -6.0 6.0print(dp, db) # → -6.0 -6.0print(dw, dx) # → -18.0 -12.0
Toplama ve çarpmada gradyan
Toplama ve çarpma gradyanı şöyle dağıtır:
- Toplama, gelen gradyanı iki girdisine olduğu gibi geçirir.
- Çarpma, her girdiye öbür girdinin değeriyle çarpılmış hâlini verir: ‘nin gradyanı ‘i, ‘inki ‘yi taşır.
Gradyanı okumak
ve parametredir; gradyanları, gradient descent’in ihtiyaç duyduğu sayılardır. ile veridir ve değişmez.
‘nin gradyanı ‘dir ve negatiftir. ‘yi artırırsan loss azalır. Gradient descent ‘yi tam bu yönde iter.
Grafiğe dön ve ‘yi değiştir:
- yap: tahmin olur ve target’a eşitlenir. dışındaki her gradyan çarpanını taşır. olunca hepsi sıfırlanır ve ayarlanacak bir şey kalmaz.
- yap: olur ve ‘nin gradyanı çıkar.
def dL_dw(w, x=3.0, b=1.0, y=10.0): return 2 * (w * x + b - y) * xprint(dL_dw(3.0), dL_dw(1.0)) # → 0.0 -36.0
Tek bir backward pass bütün gradyanları forward pass’e yakın bir maliyetle verir. Sayısal gradyan her parametre için loss’u iki kez hesaplardı.
Uzun yollarda gradyan küçülür
Yolun uzunluğunun da bir bedeli var: yol uzadıkça çarpan sayısı artar ve çarpanlar 1’den küçükse çarpım hızla küçülür. Önceki derste gördüğün kaybolan gradyanın (vanishing gradient)kaybolan gradyan (vanishing gradient): Gradyanın, türevi küçük aktivasyonlardan geçerek girişe yakın katmanlara ulaşana kadar neredeyse sıfıra inmesi; o katmanlar bu yüzden çok yavaş öğrenir ya da hiç öğrenemez. nedeni budur.
factors = [0.5] * 10 # 10 yerel türev, her biri 0.5print(np.prod(factors)) # → 0.0009765625
Bir değer birden çok yerde kullanılınca
Buraya kadar her düğümü tek bir düğüm kullanıyordu. Bir değer birden çok yerde kullanılınca graph dallanır.
Dallanan bir graph
Küçük bir örnekte , ve olsun. Forward pass’te ve çıkar.
iki yerde kullanılır, çarpımda ve toplamda. Bu yüzden ‘den ‘ya iki yol gider:
- : yerel türevlerin çarpımı .
- : yerel türev .
İki katkının toplamı gradyanı verir:
Sonucu başka bir yoldan doğrula. olarak yazarsan türevi çıkar. biraz arttığında hem çarpım hem toplam değişir ve ikisinin etkisini birlikte taşır.
u, v = 2.0, 3.0q = u * v # çarpım yoluf = q + u # toplam yoluprint(q, f) # → 6.0 8.0via_q = 1.0 * v # f <- q <- u: 1 * vdirect = 1.0 # f <- uprint(via_q + direct) # → 4.0def func(u): # sayısal kontrol: aynı f return u * v + uh = 1e-5print(round((func(u + h) - func(u - h)) / (2 * h), 4)) # → 4.0
Yollar arasında topla
Yol boyunca çarp, yollar arasında topla. Bir değerin gradyanı, ona giden bütün yolların katkılarının toplamıdır. Gerçek ağlarda dallanma her yerdedir:
- Yığındaki (batch) her örnek, aynı weight’i yeniden kullanır.
- Bir katmandaki (layer) her nöron, aynı girdiyi alır.
İkisiyle de bir sonraki derste, yoğun katmanın geri geçişinde karşılaşacaksın.
Birincisine sayılarla bakalım. İki örnek aynı ‘yi kullanır ve her örnek ‘ye ayrı bir yol açar:
import numpy as npw = 2.0xs = np.array([3.0, 1.0]) # iki örnek, aynı wys = np.array([10.0, 4.0])# her örneğin loss'u (w * x - y) ** 2; dL/dw = 2 * (w * x - y) * xper_sample = 2 * (w * xs - ys) * xsprint(per_sample) # → [-24. -4.]print(per_sample.sum()) # → -28.0
Skaler bir autograd
Binlerce düğümlü bir ağda backward pass’i bilgisayara bırakırsın. Hesap sırasında graph’ı kaydeden ve gradyanları kendiliğinden hesaplayan sisteme otomatik türev alma (automatic differentiation), kısaca autograd denir. PyTorch’ta loss.backward() çağrısı bunu yapar.
Burada onun en küçük hâlini düşüneceksin: yalnızca bir sayı tutan skaler bir nesne. Mini autograd problemindeki Value sınıfı budur.
Value nesnesi
Bir Value şunları taşır:
- hesabın değerini;
- gradyanını, başlangıçta 0;
- girdilerini, yani kendisini hesaplamakta kullanılan düğümleri;
- gradyanını girdilerine nasıl geçireceğini bilen bir kuralı.
u * v yazdığında yeni bir Value doğar:
- değeri çarpımdır;
- girdileri
uilev’dir; - kuralı çarpmanın yerel türevleridir.
Aşağıdaki bloklar sırayla aynı Value sınıfını kurar. Her blok öncekinin üstüne metot ekler: class Value(Value): önceki sınıfın bütün metotlarını alır ve yenilerini ekler.
Problemde hepsini tek bir sınıfa yazarsın. Orada __init__ yalnızca data alır; girdileri saklamak için ona bir alan eklersin.
İlk blok nesnenin dört parçasını tanımlar:
class Value: def __init__(self, data, prev=()): self.data = float(data) # hesabın değeri self.grad = 0.0 # gradyan, başta 0 self._prev = prev # girdiler self._backward = lambda: None # gradyanı girdilere geçiren kuralu = Value(2.0)print(u.data, u.grad) # → 2.0 0.0
Üç işlem ve yerel türevleri
Üç işlem yetiyor:
| İşlem | ||
|---|---|---|
Her kural yalnızca düğümün kendi girdilerini ve sonucunu kullanır. ‘ın kuralı sonuç ‘yi kullanır, bu yüzden forward pass’te bulunan değer yeter. Bu türevi önceki dersten biliyorsun.
Her işlem yeni bir Value döndürür ve kuralını _backward içine koyar. Gradyanı girdilere += ile eklediğine dikkat et:
import mathclass Value(Value): # önceki Value'ya metot ekler def __add__(self, other): out = Value(self.data + other.data, (self, other)) def _backward(): self.grad += out.grad # yerel türev 1 other.grad += out.grad out._backward = _backward return out def __mul__(self, other): out = Value(self.data * other.data, (self, other)) def _backward(): self.grad += other.data * out.grad # yerel türev: öbür girdi other.grad += self.data * out.grad out._backward = _backward return out def tanh(self): out = Value(math.tanh(self.data), (self,)) def _backward(): self.grad += (1 - out.data ** 2) * out.grad # 1 - c ** 2 out._backward = _backward return out
Öbür işlemler bu üçünden kurulur:
- , ‘yi ile çarpıp ‘ya eklemektir.
- Kare almak, bir değeri kendisiyle çarpmaktır ().
y = Value(10.0)e = Value(7.0) + y * Value(-1.0) # çıkarma: (-1) ile çarp, ekleL = e * e # kare: kendisiyle çarpprint(e.data, L.data) # → -3.0 9.0
Topolojik sıra (topological order)
Backward pass’te sıra önemlidir. Bir düğümün gradyanı, onu kullanan bütün düğümler gradyan katkılarını ilettikten sonra tamamlanır. Daha erken davranırsan eksik bir toplamı girdilerine geçirirsin.
Doğru sıra, her düğümün girdilerinden sonra geldiği bir listedir. Buna topolojik sıra denir. Bu dersin graph’ı için bir tanesi sırasıdır.
Topological order tek değildir ( ile yer değiştirebilir), ama hiçbir zaman ‘ten ya da ‘dan önce gelemez. Forward pass listeyi soldan sağa yürür. Backward pass sağdan sola yürür.
Listeyi ‘den başlayıp girdilere inerek kurarsın:
- Bir düğümü ancak girdilerinin hepsini listeye ekledikten sonra ekle.
- Zaten eklediğin düğümü atla.
def topological_order(root): order, visited = [], set() def visit(v): if v not in visited: visited.add(v) for child in v._prev: visit(child) order.append(v) # girdilerinden sonra gelir visit(root) return orderu, v = Value(2.0), Value(3.0)f = u * v + uprint([n.data for n in topological_order(f)]) # → [2.0, 3.0, 6.0, 8.0]
Backward pass’in üç adımı
Backward pass üç adımdır:
- Çıkıştan başlayarak topological order’ı kur.
- Çıkışın gradyanını 1 yap; öbür düğümlerin gradyanı 0 olarak başlar.
- Listeyi sondan başa dolaş: her düğüm, kendi gradyanını yerel türevlerle çarpıp girdilerinin gradyanına ekler.
Üç adım Value’ya tek bir metot olarak girer. Aynı f = u·v + u ifadesini deneyelim:
class Value(Value): # önceki Value'ya metot ekler def backward(self): self.grad = 1.0 # çıkışın gradyanı 1 for v in reversed(topological_order(self)): v._backward() # gradyanı girdilere ekleu, v = Value(2.0), Value(3.0)f = u * v + uf.backward()print(f.data, u.grad, v.grad) # → 8.0 4.0 2.0
Dikkat
Gradyanı girdiye eklemekle onun üstüne yazmak aynı şey değildir. ve olsun.
Çarpmanın iki girdisi de aynı Value’dur ve her biri getirir. Doğru gradyan ‘dur. Gelen her sayıyı öncekinin üstüne yazarsan yalnızca kalır.
u = Value(3.0)c = u * uc.backward()# += yerine = kullansaydık 3.0 kalırdıprint(u.grad) # → 6.0
Bir nöronda uçtan uca
Bir nöronla sayılara bakalım: , , , ve . Önce forward pass’i yürüt:
- Girdiyi ağırlıkla çarpıp sapmayı ekle: .
- uygula: .
- Hedef değeri çıkar: .
- Hatanın karesini al: .
Sonra backward pass’i yürüt:
- Kare kuralıyla ‘nın gradyanını bul: .
- ‘nin gradyanını bul: ‘ın yerel türevi olduğundan sonuç olur.
- Toplama, ‘nin gradyanını ‘ye ve çarpıma olduğu gibi geçirir: ikisi de alır.
- Çarpmada gradyanı ‘ye ile, ‘e ile çarparak ver.
Üç gradyan şöyle çıkar:
Sayılar dört basamağa yuvarlandı. Yuvarlanmış değerleri elle çarparsan son basamakta bir fark çıkabilir. Kendi Value’nu bu sayılarla sınayabilirsin:
x, w = Value(2.0), Value(0.5)b, y = Value(0.0), Value(1.0)z = w * x + ba = z.tanh()e = a + y * Value(-1.0) # a - yL = e * eL.backward()print(round(L.data, 4)) # → 0.0568print(round(w.grad, 4), round(x.grad, 4), round(b.grad, 4))# → -0.4005 -0.1001 -0.2002
Özet
- Computational graph, hesabı tek işlemlik düğümlere böler. Forward pass değerleri girdilerden loss’a doğru hesaplar. Backward pass gradyanları loss’tan girdilere doğru hesaplar.
- Bir yolda yerel türevleri çarparsın. Bir değer birden çok yerde kullanılıyorsa yollardan gelen gradyanları toplarsın.
- Skaler bir autograd, her düğümde değeri, gradyanı, girdileri ve yerel türev kuralını tutar.
- Autograd gradyanları topological order’ın tersinde, üstüne yazmadan ekleyerek hesaplar.
Terimler
| Türkçe | English |
|---|---|
| gradyan inişi | gradient descent |
| kayıp | loss |
| gradyan | gradient |
| sayısal gradyan | numerical gradient |
| geri yayılım | backpropagation |
| hesaplama grafiği | computational graph |
| zincir kuralı | chain rule |
| örnek | sample |
| ağırlık | weight |
| sapma | bias |
| hedef değer | target |
| tahmin | prediction |
| ortalama kare hata | mean squared error, MSE |
| düğüm | node |
| kenar | edge |
| ileri geçiş | forward pass |
| türev | derivative |
| kısmi türev | partial derivative |
| yerel türev | local derivative |
| geri geçiş | backward pass |
| gelen gradyan | upstream gradient |
| kaybolan gradyan | vanishing gradient |
| yığın | batch |
| katman | layer |
| otomatik türev alma | automatic differentiation |
| autograd | autograd |
| topolojik sıra | topological order |
Şimdi sıra sende
Ders tamamlandı. Sıradaki: ilk problem.
İlk problem: Zincir kuralı →Notların
Metinden bir bölüm seçip Not al'a basarak da ekleyebilirsin.
Bu derse dair söylemek istediğin bir şey var mı?