İçeriğe geç
5/3030 bölümden 5. bölüm

Backpropagation Sıfırdan: Önce Motor, Sonra Ağ

Saf Python ile 120 satırlık autodiff motoru yaz, PyTorch ile 16 ondalığa kadar doğrula ve zero_grad'in ne yaptığını onu silerek öğren.

Bu sayfada

Dört bölüm geride kaldı ve kursun ortasında bir boşluk var.

3. Bölüm bize gradient descent'i verdi: bir parametreyi iyileştirmek için kaybın ona göre eğimini bul ve yokuş aşağı bir adım at. 4. Bölüm bize inmeye değer bir loss verdi. Ama ikisinde de türev elle hesaplandı — tek model, tek parametre, tek satır calculus ve hepsi bir sayfaya sığdı.

Şimdi iki katmanı üst üste koy. Birincinin çıktısı ikincisini besler, bu yüzden birinci katmandaki her weight, ikinci katmandaki her neuron üzerinden loss'u etkiler. Yüzer birimlik iki hidden layer içeren bir ağda yaklaşık yirmi bin parametre vardır ve her birinin aynı loss'a ait kendi kısmi türevine ihtiyacı olur. Bunu elle yapmak sıkıcı değildir; imkânsızdır ve bu kursun geri kalanındaki her mimari için imkânsız kalır.

Çıkış yolu daha iyi bir notasyon değildir. Çıkış yolu şunu fark etmektir: bir bileşkenin türevi, hesaplamanın yapısından hareketle, bir program tarafından mekanik olarak hesaplanabilir — ve bunu doğru yönde yaparsan, loss'u bir kez hesaplamanın maliyetine yakın bir maliyetle yirmi bin türevin hepsini elde edersin.

Bu mekanizma reverse-mode automatic differentiation'dır. Bir neural network'e uygulandığında buna backpropagation denir ve bu bölümün sonunda hiçbir library kullanmadan yaklaşık 120 satır Python ile bir tane yazmış, PyTorch ile kontrol etmiş ve 1. Bölüm'de perceptron'u öldüren XOR problemini çözmek için kullanmış olacaksın.

Önce: neden mutlaka bir nonlinearity gerekir

Bölüme bağlantı: Önce: neden mutlaka bir nonlinearity gerekir

Makineyi kurmadan önce bir soruyu netleştirmek gerekiyor; çünkü cevap ters yönde olsaydı kurulacak hiçbir şey kalmazdı.

Perceptron XOR'da başarısız oldu çünkü tek bir doğru dört noktayı ayıramaz. Bariz düzeltme stack etmektir: girdiyi bir linear layer'dan geçir, sonra bir diğerinden. Bu işe yarar mı?

Hayır ve kanıtı iki satır. Bir linear layer h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1'dır. Onu bir diğerine ver, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, ve yerine koy:

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)\mathbf{y} = W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = (W_2W_1)\mathbf{x} + (W_2\mathbf{b}_1 + \mathbf{b}_2)

Bileşke W=W2W1W = W_2W_1 ve b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2 ile Wx+bW\mathbf{x} + \mathbf{b} olur. Linear layer'lardan oluşan bir stack, tek bir linear layer'dır. On tane de olsa, bin tane de: hâlâ tek bir doğru, hâlâ XOR yapamaz.

Buna inanmak yerine gerçekleştiğini izlemeye değer:

linear_is_linear.pyPYTHON
import numpy as np
rng = np.random.default_rng(0)
W1, b1 = rng.normal(size=(3, 2)), rng.normal(size=3)
W2, b2 = rng.normal(size=(1, 3)), rng.normal(size=1)
x = rng.normal(size=2)

two_layers = W2 @ (W1 @ x + b1) + b2
one_layer  = (W2 @ W1) @ x + (W2 @ b1 + b2)
print(two_layers[0], one_layer[0], abs(two_layers[0] - one_layer[0]))
TEXT
-4.612963371048  -4.612963371048  0.00e+00

Yaklaşık olarak eşit değil. Bit-for-bit aynı, çünkü aynı aritmetiğin yeniden düzenlenmiş hâli.

Yani derinlik kendi başına hiçbir şey kazandırmaz. Kazandıran şey katmanların arasına nonlinear bir fonksiyon koymaktır — activation function'ların var olmasının bütün nedeni budur. Bunlar biyolojik bir süs ya da bir normalizasyon hilesi değildir. Biri yoksa ikinci katman dekorasyondur.

Chain rule, kâğıt üzerinde, paylaşılan bir node ile

Bölüme bağlantı: Chain rule, kâğıt üzerinde, paylaşılan bir node ile

Şimdi matematik; aslında zaten bildiğin tek bir kuralın biraz alışılmadık bir yere uygulanması.

Tek değişkenli chain rule şunu söyler: LL, cc'ye bağlıysa ve cc, xx'ye bağlıysa, o zaman dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. Türevler bir zincir boyunca çarpılır.

Burada önemli olan kısım, bir değişken birden fazla downstream yolu beslediğinde ne olduğudur. xx, LL'i hem aa üzerinden hem de bb üzerinden etkiliyorsa katkılar toplanır:

dLdx=Laax+Lbbx\frac{dL}{dx} = \frac{\partial L}{\partial a}\frac{\partial a}{\partial x} + \frac{\partial L}{\partial b}\frac{\partial b}{\partial x}

Bir yol boyunca çarp, yollar arasında topla. Backpropagation'ın tamamı budur ve bu bölümün geri kalanındaki her implementation detayı — koddaki += ve ilk training loop'unu yazan herkesin takıldığı zero_grad() çağrısı dahil — o ikinci kelimenin doğrudan sonucudur.

x=0.5x = 0.5 ve y=1.4y = 1.4 ile beş operation'lık somut bir circuit alalım:

a=xy,b=x+y,c=ab,d=tanh(c),L=d+xa = xy, \quad b = x + y, \quad c = ab, \quad d = \tanh(c), \quad L = d + x

xx'in üç kez göründüğüne dikkat et: aa içinde, bb içinde ve doğrudan LL içinde. Backward pass'i kâğıt üzerinde sağdan sola yap, dLdL=1\frac{dL}{dL} = 1'ten başlayarak:

L=d+xL = d + x, yani Ld=1\frac{\partial L}{\partial d} = 1 ve doğrudan yol Lx=1\frac{\partial L}{\partial x} = 1 katkısını yapar. Addition, gelen gradient'i değişmeden iki girdiye de dağıtır.

c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33 ile d=tanh(c)d = \tanh(c), dolayısıyla dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, yani dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 ve dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. Multiplication yer değiştirir: her girdinin gradient'i diğer girdinin değeriyle ölçeklenir.

aa üzerinden: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. bb üzerinden: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Doğrudan: 11.

dLdx=0.6501+0.1711+1.0000=1.8212\frac{dL}{dx} = 0.6501 + 0.1711 + 1.0000 = 1.8212

Bu sayıyı aklında tut. Birkaç sayfa sonra bir program, bunların hiçbirini ona söylemeden bunu üretecek.

Bunu programlanabilir kılan içgörü şu: o adımların her biri yereldi. Gradient'i multiplication node'undan geçirmek için gelen gradient'e ve saklanan iki input değerine ihtiyacın vardı — circuit'in geri kalanına dair hiçbir şeye değil. Her operation kendisini nasıl differentiate edeceğini bilir.

O hâlde, kendisini neyin ürettiğini hatırlayan bir sayı yap.

value.pyPYTHON
class Value:
    """A number that remembers where it came from."""

    def __init__(self, data, _children=(), _op=""):
        self.data = data
        self.grad = 0.0
        self._backward = lambda: None
        self._prev = set(_children)      
        self._op = _op

Dört alan. data değerdir. grad, Lself\frac{\partial L}{\partial \text{self}}'i biriktirir. _prev, bunun hesaplandığı Value kümesidir — graph'ın edge'leri. Ve _backward, her operation'ın kurduğu bir closure'dır: bu node'un gradient'ini girdilerine bir adım geri nasıl iteceğini bilir.

Her operator aynı şekli izler: çıktıyı hesapla, parent'ları kaydet, local rule'u kur.

value.py (continued)PYTHON
    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       
            other.grad += out.grad      

        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 += other.data * out.grad   
            other.grad += self.data * out.grad   

        out._backward = _backward
        return out

    def tanh(self):
        t = math.tanh(self.data)
        out = Value(t, (self,), "tanh")

        def _backward():
            self.grad += (1 - t * t) * out.grad

        out._backward = _backward
        return out

    def relu(self):
        out = Value(self.data if self.data > 0 else 0.0, (self,), "relu")

        def _backward():
            self.grad += (1.0 if out.data > 0 else 0.0) * out.grad

        out._backward = _backward
        return out

Dört _backward gövdesini bir tablo gibi oku; kâğıt üzerindeki türetmeden gelen akış pattern'leri tam orada duruyor:

operationgradient'e ne yapar
+dağıtır — her girdiye aynı gradient
*yer değiştirir — her girdi diğerinin değeriyle ölçeklenir
reluyönlendirir — geçirir ya da tamamen engeller
tanhzayıflatır — en fazla 1 olan ve genelde daha küçük olan 1t21 - t^2 ile ölçekler

Her biri += kullanır, asla = kullanmaz. Bu, «yollar arasında topla» kuralının kodlanmış hâlidir. İki consumer'ı besleyen bir node iki kez çağrılır ve iki katkı kendi kendine toplanır.

Sonra driver gelir; herhangi bir global bilgiye sahip tek parça:

value.py (continued)PYTHON
    def backward(self):
        order, seen = [], set()

        def build(v):
            if v in seen:
                return
            seen.add(v)
            for child in v._prev:
                build(child)
            order.append(v)

        build(self)
        self.grad = 1.0
        for v in reversed(order):       
            v._backward()               

build, graph'ın topological ordering'ini üretir: her node, bütün girdilerinden sonra görünür. Bu listeyi tersten yürümek, bir node'un _backward'ını çağırdığında kendi gradient'inin zaten tamamlanmış olmasını garanti eder — downstream'deki her consumer katkısını zaten yapmıştır. Sırayı yanlış yaparsan yarım kalmış bir gradient'i geriye itersin; bu da hiçbir error message vermeden yanlış cevap üretir.

check.pyPYTHON
x = Value(0.5)
y = Value(1.4)
a = x * y
b = x + y
c = a * b
d = c.tanh()
L = d + x
L.backward()
print(x.grad, y.grad)
TEXT
forward:  a=0.7000  b=1.9000  c=1.3300  d=0.8692  L=1.3692
backward: dL/dd=1.0000  dL/dc=0.2444  dL/da=0.4644  dL/db=0.1711
          dL/dx=1.8212   dL/dy=0.4033

1.8212. Aynı sayı; + için kural, * için kural, tanh için kural söylenmiş ama bu circuit hakkında hiçbir şey söylenmemiş bir programdan geldi.

İki bağımsız kontrol; çünkü «türettiğim şeyle eşleşiyor» zayıf bir testtir, hele ikisini de aynı kişi yaptıysa.

Numerical differentiation. Input'u azıcık oynat ve ölç. Centred difference L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h}, hiç calculus kullanmadan türevi tahmin eder:

TEXT
dL/dx:  analytic=1.821202805  numeric=1.821202805  |diff|=1.80e-10
dL/dy:  analytic=0.403269235  numeric=0.403269235  |diff|=7.64e-12

PyTorch'a karşı, yani bu işi meslek edinmiş insanların yazdığı endüstriyel bir autodiff engine'e karşı:

TEXT
torch dL/dx=1.821202805316   ours=1.821202805316   |diff|=2.22e-16
torch dL/dy=0.403269234753   ours=0.403269234753   |diff|=1.11e-16

2×10162 \times 10^{-16} seviyesinde uyum; bu, 64-bit float için machine epsilon'dır: iki engine aynı aritmetiği yapıyor. Numerical check'i cebinde tut — yeni bir layer'ın backward pass'ini debug etme aracıdır ve yanlış bir gradient'in bulunabilir olmasının nedenidir.

Aynı circuit, farklı inputs. x=2x = 2 ve y=3y = -3 ayarla; bu c=6c = 6 yapar:

TEXT
x=0.5, y=1.4:  dL/dc = 0.244400     three paths into x:  0.6501 + 0.1711 + 1.0000 = 1.8212
x=2.0, y=-3.0: dL/dc = 0.000025     three paths into x:  0.0001 + -0.0001 + 1.0000 = 0.9999

tanh\tanh node'undan geçen gradient 9.945 kat düştü. Onun upstream'indeki her şey — gerçek bir network'te ondan önceki her layer — neredeyse hiçbir şey almaz. Circuit'teki iki yol sessizleşti; yalnızca tanh\tanh'yı bypass eden direct connection hâlâ signal taşıyor.

Vanishing gradient problemi, tek bir node içinde budur. Kırk tanh\tanh layer'ını stack et ve bu tür kırk faktörü birbiriyle çarp; early layer'lar tamamen öğrenmeyi bırakır. Bu aynı zamanda, tesadüfen, skip connection'lar için burada minyatür hâlini görebileceğin bir argümandır: nonlinearity'yi bypass eden yol hayatta kalan tek yoldur.

zero_grad aslında ne yapar ve bug neden saklanır

Bölüme bağlantı: zero_grad aslında ne yapar ve bug neden saklanır

Her _backward, += kullanır. Bu doğrudur — yolların toplanma şekli budur. Ama herkesi yakalayan bir sonucu vardır: gradients, backward() çağrıları arasında da birikir. Engine, ikinci çağrının aynı graph'taki başka bir yol değil de yeni bir training step olduğunu bilmez.

Bu yüzden bir training loop onları temizlemek zorundadır:

train.pyPYTHON
for step in range(steps):
    ys = [model(x) for x, _ in DATA]
    loss = sum((yp - yt) ** 2 for yp, (_, yt) in zip(ys, DATA))

    for p in model.parameters():   
        p.grad = 0.0

    loss.backward()
    for p in model.parameters():
        p.data -= lr * p.grad

PyTorch'ta bu optimizer.zero_grad()'dir ve genel tavsiye şudur: unutursan training bozulur. O zaman o iki satırı silelim ve ne kadar bozulduğunu görelim. Aynı seed'ler, aynı her şey, XOR üzerinde 200 step:

learning rateseedreset ilereset olmadan
0.051337loss 3.255088, 3/4loss 0.000000, 4/4
0.057loss 2.144820, 2/4loss 0.000000, 4/4
0.0542loss 2.126074, 2/4loss 0.000000, 4/4
0.11337loss 0.038597, 4/4loss 0.000000, 4/4
0.17loss 2.055048, 2/4loss 0.000000, 4/4
0.142loss 2.049876, 2/4loss 0.000073, 4/4
0.31337loss 4.512310, 2/4loss 8.000000, 2/4
0.37loss 0.015247, 4/4loss 4.000000, 3/4
0.342loss 0.005478, 4/4loss 4.000000, 3/4

Küçük learning rate'lerde buggy version her satırı kazanıyor. Correct version takılırken converge ediyor.

Bu bir tesadüf değil ve anlamaya değer; çünkü bu bug'ın neden bu kadar zor yakalandığını açıklar. Gradient'i hiç temizlemezsen, kk adımında parametre şimdiye kadar hesaplanmış tüm gradient'lerin toplamı ile update edilir. Loss kabaca aynı yönü göstermeye devam ediyorsa bu toplam düzenli olarak büyür ve etkisi kendi kendine artan bir learning rate olur. Correct algorithm'in süründüğü η=0.05\eta = 0.05'da, kontrolden çıkan step size tam bir düzeltme gibi görünür.

Sonra alttaki üç satıra bak. η=0.3\eta = 0.3'da aynı mekanizma modeli dağıtır — loss 8.0, sabit ±1\pm 1'ya çökmüş bir modelin aldığı skordur; dört maksimum yanlış cevabın maliyeti olan 16'nın yarısı — — correct version ise artık temiz şekilde converge eder.

Yani dürüst ifade «her zaman zero_grad çağır, yoksa modelin train olmaz» değildir. Şudur: onsuz artık gradient descent çalıştırmıyorsun. Step size'ı kimsenin seçmediği bir hızda yukarı kayan başka bir şey çalıştırıyorsun; bu da bazen gerçek şeyden daha iyi bile çalışıyor gibi görünecek, ta ki çalışmayana kadar — o noktada learning rate'i, initialisation'ı ya da data'yı suçlayacaksın. Machine learning'deki en kötü bug'ların şekli budur: crash etmezler, algorithm'i arada sırada daha iyi skor alan farklı bir algorithm'e dönüştürürler.

Engine hazır olunca neural network neredeyse hiç kod değildir. Bir neuron bir dot product, bir bias ve bir activation'dır; bir layer neuron listesidir; bir network layer listesidir.

nn.pyPYTHON
class Neuron:
    def __init__(self, nin):
        self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]
        self.b = Value(0.0)

    def __call__(self, x):
        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, nin, nout):
        self.neurons = [Neuron(nin) for _ in range(nout)]

    def __call__(self, x):
        out = [n(x) for n in self.neurons]
        return out[0] if len(out) == 1 else out

    def parameters(self):
        return [p for n in self.neurons for p in n.parameters()]


class MLP:
    def __init__(self, nin, nouts):
        sizes = [nin] + nouts
        self.layers = [Layer(sizes[i], sizes[i + 1]) for i in range(len(nouts))]

    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()]

Bunların hiçbirinde backward pass yok. Tek satır bile. Value class'ı bu class'ların kurduğu her şeyi zaten nasıl differentiate edeceğini biliyor; önce onu yazmış olmanın amacı da bu: bir autodiff engine, neural network için kullanıldığını bilmez.

Şimdi 1. Bölüm'deki problem. İki input, iki hidden unit, bir output, dokuz parametre:

TEXT
step   1: loss 4.156690
step  10: loss 4.005572
step  50: loss 3.996708
step 100: loss 3.510700
step 200: loss 0.038597

[0, 0] -> -0.9081  (target -1)  ok
[0, 1] -> +0.8934  (target +1)  ok
[1, 0] -> +0.8906  (target +1)  ok
[1, 1] -> -0.9207  (target -1)  ok

Dörtte dört. Hiçbir perceptron'un hesaplayamadığı fonksiyon — 1. Bölüm'de bb'nin hem pozitif hem negatif olmasını gerektiren dört inequality ile kanıtlanmıştı — otomatik olarak bulunan dokuz sayı tarafından hesaplanıyor.

Tatmin edici olan çalışması değil. Nasıl çalıştığını görebilmek; çünkü iki hidden unit ile intermediate representation bir düzlemdeki noktadır ve onu doğrudan print edebilirsin.

0.001241 loss'a eğitildikten sonra her input'un hidden layer'dan sonra nereye düştüğü ve output neuron'un onunla ne yaptığı şöyle:

inputhidden layer outputoutput scorelabel
(0,0)(0, 0)(+0.8206, 0.8474)(+0.8206,\ -0.8474)2.4045-2.40451-1
(0,1)(0, 1)(+0.9985, +0.8564)(+0.9985,\ +0.8564)+2.3049+2.3049+1+1
(1,0)(1, 0)(0.8401, 0.9990)(-0.8401,\ -0.9990)+2.3006+2.3006+1+1
(1,1)(1, 1)(+0.8368, 0.8550)(+0.8368,\ -0.8550)2.4786-2.47861-1

Birinci ve dördüncü satırlara bak. (0,0)(0,0) ve (1,1)(1,1) inputs'u karenin çapraz karşıt köşeleri — bu problemde iki noktanın olabileceği kadar uzaklar — ve hidden layer onları (0.82,0.85)(0.82, -0.85) ile (0.84,0.86)(0.84, -0.86)'ya map ediyor. Neredeyse aynı nokta. Layer düzlemi katladı; böylece reddedilen iki köşe üst üste düştü ve aynı yerde olduklarında tek bir doğru onları diğer ikisinden ayırdı.

Output neuron da tam olarak o doğrudur. Öğrenilmiş parametreleri w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, dolayısıyla decision boundary'si

3.1153h1+3.0893h2+2.7697=0-3.1153\,h_1 + 3.0893\,h_2 + 2.7697 = 0

ki bu düz bir doğru — bir perceptron, 1. Bölüm'deki aynı nesne, değişmemiş. O zaman XOR'u çözemiyordu, şimdi de çözemez. Değişen şey artık input'a bakmaması; birinci layer'ın onun için kurduğu ve problemin linearly separable olduğu bir space'e bakmasıdır.

Learned representation budur; kesin olmakta fayda var çünkü ifade kursun geri kalanında ve alanın geri kalanında gevşekçe kullanılacak. Bu mistik bir anlamda compression, summary ya da embedding değildir. Tasarlanmak yerine öğrenilmiş bir coordinate change'dir; tek işi bir sonraki layer'ın işini kolaylaştırmaktır.

Universal approximation theorem ve ne söylemediği

Bölüme bağlantı: Universal approximation theorem ve ne söylemediği

Burada bir theorem var ve genelde kötü alıntılanır.

Cybenko 1989'da ve Hornik 1991'de, tek hidden layer'a ve uygun bir activation function'a sahip bir feedforward network'ün, yeterli hidden unit verildiğinde compact set üzerindeki herhangi bir continuous function'ı istediğin doğrulukta approximate edebileceğini kanıtladı.34 Bu gerçek ve önemli bir sonuçtur: mimarinin sınırlama olmadığını söyler.

Şimdi neyi dışarıda bıraktığını oku. Kaç unit gerektiğini söylemez — bound astronomik olabilir. Weights'in bulunabileceğini söylemez; existence iddia eder ve random start'tan gradient descent bir oracle değildir. Ayrıca görmediğin data üzerindeki davranış hakkında hiçbir şey söylemez; bu da 6. Bölüm'ün ikinci yarısıdır.

«Var» ile «bulunabilir» arasındaki boşluk akademik değildir. İşte aynı XOR problemi, her biri 50 random initialisation, 1000 step; yalnızca hidden layer size değiştirildi:

hidden units4/4'e ulaşan initialisation'lar
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

Minimum viable architecture ile her dört çalıştırmadan biri oraya hiç ulaşamaz — Chapter 3'ün one-dimensional surface üzerinde gösterdiği local minimum'un aynısı olan, içinden inemediği bir configuration'a yerleşir. Bir unit ekle ve failures neredeyse yok olur; network daha expressive olduğu için değil (iki unit zaten yeter — 38 run bunu kanıtlıyor), extra dimension'lar descent'e kaçabileceği daha fazla direction verdiği için.

Sonra sekiz unit, dörtten biraz daha kötü olur. Fixed learning rate ve step budget altında daha fazla capacity monoton biçimde daha iyi değildir. Sana takılmış bir network'ün çözümünün her zaman daha büyük bir network olduğunu söyleyen herkes, o tablonun ortasından extrapolate ediyordur.

Bu, 1. Bölüm'deki convergence theorem ile aynı derstir ve 10. Bölüm'de scaling laws hakkında, o bölümün verdiği biçimde yine aynı ders olacaktır: loss tahmini, parasını ödediğin capability'nin tahmini değildir; ikisi arasındaki mesafe engineering'in yaşadığı yerdir.

Ayrıntıları göster

Opsiyonel: matrix form ve yukarıdaki kodun neden onu kullanmadığı.

Buradaki her şey tek tek scalar olarak yazıldı; bu mekanizmayı görmenin en açık, çalıştırmanın en yavaş yoludur. Pratikte bir layer bir matrix multiply'dır ve y=Wx\mathbf{y} = W\mathbf{x}'nin backward pass'i şudur:

LW=Lyx,Lx=WLy\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{y}}\mathbf{x}^\top, \qquad \frac{\partial L}{\partial \mathbf{x}} = W^\top\frac{\partial L}{\partial \mathbf{y}}

Transpose'lar hatırlanacak bir hile değildir; yollar-üzerinden-topla kuralının, yollar matrix entry'leriyle index'lendiğinde aldığı biçimdir. Genel nesne Jacobian'dır: tüm output'ların tüm input'lara göre tüm partial derivative'lerinden oluşan matrix. Reverse mode ise Jacobian'ı asla oluşturmadan bir vector-Jacobian product hesaplamaktır — bu önemlidir, çünkü 4096 input ve 4096 output'a sahip bir layer için o matrix'in on altı milyon entry'si vardır ve onu inşa etmeye asla değmez.

Sonraki bölümleri takip etmek için bunların hiçbirine ihtiyacın yok; scalar version, matrix version'ın yaptığı her şeyi daha yavaş yapar. Shape'lerin artık bariz olmadığı 9. Bölüm'de gerekli hâle gelir.

Artık train eden bir network'ün var. Bu, hissettirdiğinden daha küçük bir başarı; çünkü elindeki network dört example üzerinde train ediyor ve aynı dört example üzerinde ölçülüyor.

Aynı kodu gerçek bir dataset üzerinde çalıştırınca yeni bir problem kümesi ortaya çıkar ve bunların hiçbiri gradient'lerle ilgili değildir. Loss bir süre düşer, sonra durur. Ya da training data'da düşer ve diğer her şeyde yükselir. Ya da ilk step'ten itibaren hiç kıpırdamaz ve nedenin initial random weight'lerin range'i olduğu ortaya çıkar. Ya da bir unit'in input'u üçüncü epoch'taki her example'da negatife kaymıştır ve o zamandan beri sessizce ölüdür, model capacity'sinin bir chunk'ını da yanında götürür.

Bunlar egzotik failures değildir; yeni yazılmış bir network'ün normal durumudur ve hiçbiri kendini duyurmaz. Gradient doğrudur — PyTorch'a karşı on altı ondalık basamağa kadar kontrol ettin — ve model hâlâ öğrenmez.

  1. Bölüm bununla ilgilidir: initialisation, normalisation, overfitting ve regularisation; ayrıca bir şeyi değiştirmeden önce bunlardan hangisinin yaşandığını sorma diagnostic alışkanlığı. Çalışan bir network ile işe yarayan bir network arasındaki fark budur.

Bu bölümdeki Value class'ı doğrudan Andrej Karpathy'nin micrograd'inden gelir; onun The spelled-out intro to neural networks and backpropagation: building micrograd videosu, bu materyali başka birinden ikinci bir şekilde dinlemek istiyorsan harcayabileceğin en iyi üç saattir. 2016 tarihli Yes you should understand backprop yazısı, bunu kendin yazman gerektiğini savunur ve Stanford'un CS224n dersinde assigned reading'dir. Backpropagation üzerine CS231n notları (cs231n.github.io/optimization-2) yukarıda tablolaştırılan flow pattern'lerin canonical anlatımıdır. Matematiği neural-network folkloru olarak değil de graph üzerinde calculus olarak görmek için Deisenroth, Faisal ve Ong'un Mathematics for Machine Learning kitabının 5.6. bölümü olağanüstü nettir; Baydin, Pearlmutter, Radul ve Siskind'in Automatic Differentiation in Machine Learning: a Survey çalışması (arXiv:1502.05767) ise yukarıda tartışılan forward/reverse trade-off dahil olmak üzere alanın bütünü için referanstır.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Master's thesis, University of Helsinki (1970). Reverse-mode accumulation; bu alana ulaşmasından on altı yıl önce ve tamamen farklı bir motivasyonla.

  2. Rumelhart, D. E., Hinton, G. E. and Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). Yöntemi bilinir kılan makale ve hidden unit'leri learned representation olarak okumanın kaynağı; bu bölümdeki Hidden layer ne yaptı kısmı ölçümlerini buna ayırır.

  3. Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, pp. 303–314 (1989).

  4. Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), pp. 251–257 (1991). Cybenko'yu genelleştirir: sonuç activation'ın sigmoidal olmasına değil, non-polynomial olmasına bağlıdır.

Seçimi LIA'ya bırakmaya hazır mısın?

Tüm yapay zeka modelleriyle tek yerde üret — bugün ücretsiz başla.