Lewati ke konten
5/30Bab 5 dari 30

Backpropagation dari Nol: Mesin Dulu, Lalu Jaringan

Tulis mesin autodiff 120 baris dengan Python murni, cocokkan dengan PyTorch hingga 16 desimal, dan pahami zero_grad dengan menghapusnya.

Di halaman ini

Empat bab masuk, ada lubang di tengah kursus ini.

Chapter 3 memberi kita gradient descent: untuk memperbaiki sebuah parameter, cari kemiringan loss terhadapnya dan melangkahlah menurun. Chapter 4 memberi kita loss yang layak dituruni. Tetapi di keduanya, turunan dihitung dengan tangan — satu model, satu parameter, satu baris kalkulus, dan semuanya muat di satu halaman.

Sekarang tumpuk dua layer. Output dari yang pertama menjadi input untuk yang kedua, jadi setiap weight di layer pertama memengaruhi loss melalui setiap neuron di layer kedua. Sebuah jaringan dengan dua hidden layer masing-masing seratus unit punya sekitar dua puluh ribu parameter, dan masing-masing membutuhkan turunan parsialnya sendiri dari loss yang sama. Mengerjakannya dengan tangan bukan membosankan; itu mustahil, dan tetap mustahil untuk setiap arsitektur di sisa kursus ini.

Jalan keluarnya bukan notasi yang lebih baik. Jalan keluarnya adalah menyadari bahwa turunan dari sebuah komposisi dapat dihitung secara mekanis, oleh program, dari struktur komputasi itu sendiri — dan bahwa jika kamu melakukannya dalam arah yang benar, kamu mendapat semua dua puluh ribu turunan dengan biaya kira-kira setara menghitung loss satu kali.

Mekanisme itu adalah reverse-mode automatic differentiation. Ketika diterapkan pada jaringan neural, ia disebut backpropagation, dan di akhir bab ini kamu akan menulisnya dalam sekitar 120 baris Python tanpa library, memeriksanya terhadap PyTorch, dan menggunakannya untuk menyelesaikan masalah XOR yang membunuh perceptron di Chapter 1.

Pertama: mengapa nonlinearity harus ada

Tautan ke bagian: Pertama: mengapa nonlinearity harus ada

Sebelum membangun mesinnya, satu pertanyaan harus dibereskan, karena jika jawabannya sebaliknya, tidak akan ada yang perlu dibangun.

Perceptron gagal pada XOR karena satu garis tidak dapat memisahkan empat titik. Perbaikan yang terlihat jelas adalah menumpuk: jalankan input melalui satu linear layer, lalu satu lagi. Apakah itu membantu?

Tidak, dan buktinya dua baris. Sebuah linear layer adalah h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1. Masukkan ke yang lain, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, lalu substitusikan:

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)

Komposisinya adalah Wx+bW\mathbf{x} + \mathbf{b} dengan W=W2W1W = W_2W_1 dan b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2. Tumpukan linear layer adalah satu linear layer. Sepuluh buah, seribu buah: tetap satu garis, tetap tidak mampu melakukan XOR.

Lebih baik melihatnya terjadi daripada sekadar percaya:

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

Bukan kira-kira sama. Identik bit demi bit, karena itu aritmetika yang sama, hanya disusun ulang.

Jadi depth tidak memberi apa-apa dengan sendirinya. Yang memberi sesuatu adalah menaruh fungsi nonlinear di antara layer — dan itulah seluruh alasan activation function ada. Mereka bukan hiasan biologis atau trik normalisasi. Tanpa itu, layer kedua hanyalah dekorasi.

Aturan rantai, di atas kertas, dengan node bersama

Tautan ke bagian: Aturan rantai, di atas kertas, dengan node bersama

Sekarang matematikanya, dan ini adalah satu aturan yang sudah kamu kenal, diterapkan di tempat yang sedikit kurang familier.

Aturan rantai satu variabel mengatakan bahwa jika LL bergantung pada cc dan cc bergantung pada xx, maka dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. Turunan dikalikan sepanjang sebuah rantai.

Bagian yang penting di sini adalah apa yang terjadi ketika sebuah variabel memberi makan lebih dari satu jalur downstream. Jika xx memengaruhi LL melalui aa dan juga melalui bb, kontribusinya ditambahkan:

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}

Kalikan sepanjang jalur, jumlahkan lintas jalur. Itulah keseluruhan backpropagation, dan setiap detail implementasi di sisa bab ini — termasuk += di kode dan panggilan zero_grad() yang menjegal semua orang saat menulis training loop pertama mereka — adalah konsekuensi langsung dari kata kedua itu.

Ambil rangkaian konkret berisi lima operasi, dengan x=0.5x = 0.5 dan y=1.4y = 1.4:

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

Perhatikan bahwa xx muncul tiga kali: di aa, di bb, dan langsung di LL. Kerjakan backward pass di atas kertas, dari kanan ke kiri, mulai dari dLdL=1\frac{dL}{dL} = 1:

L=d+xL = d + x, jadi Ld=1\frac{\partial L}{\partial d} = 1 dan jalur langsung berkontribusi Lx=1\frac{\partial L}{\partial x} = 1. Penjumlahan mendistribusikan gradien masuk tanpa perubahan ke kedua input.

d=tanh(c)d = \tanh(c) dengan c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33, jadi dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, jadi dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 dan dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. Perkalian menukar: gradien setiap input diskalakan oleh input yang lain.

Melalui aa: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. Melalui bb: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Langsung: 11.

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

Ingat angka itu. Dalam beberapa halaman, sebuah program akan menghasilkannya tanpa diberi tahu satu pun dari semua ini.

Insight yang membuatnya bisa diprogram: setiap langkah tadi bersifat lokal. Untuk mendorong gradien melalui node perkalian, kamu hanya membutuhkan gradien masuk dan dua nilai input yang disimpan — tidak perlu tahu apa pun tentang sisa rangkaian. Setiap operasi tahu cara mendiferensiasikan dirinya sendiri.

Jadi buat sebuah angka yang mengingat apa yang menghasilkannya.

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

Empat field. data adalah nilainya. grad mengakumulasi Lself\frac{\partial L}{\partial \text{self}}. _prev adalah himpunan Value yang dipakai untuk menghitungnya — edge dari graph. Dan _backward adalah closure yang dipasang setiap operasi: ia tahu cara mendorong gradien node ini satu langkah mundur ke inputnya.

Setiap operator mengikuti bentuk yang sama: hitung output, catat parent, pasang aturan lokal.

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

Baca empat body _backward sebagai tabel dan pola aliran dari derivasi di atas kertas langsung terlihat di sana:

operationapa yang dilakukannya pada gradien
+mendistribusikan — gradien yang sama ke setiap input
*menukar — setiap input diskalakan oleh nilai input lainnya
relumerutekan — meneruskannya atau memblokirnya sepenuhnya
tanhmelemahkan — menskalakan dengan 1t21 - t^2, yang paling besar 1 dan biasanya lebih kecil

Setiap satunya menggunakan += dan tidak pernah =. Itulah aturan "jumlahkan lintas jalur", dikodekan. Node yang memberi makan dua consumer dipanggil dua kali, dan dua kontribusinya bertambah dengan sendirinya.

Lalu drivernya, satu-satunya bagian yang punya pengetahuan global:

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 menghasilkan urutan topologis graph: setiap node muncul setelah semua inputnya. Menelusuri daftar itu secara terbalik menjamin bahwa saat kamu memanggil _backward milik sebuah node, gradiennya sendiri sudah lengkap — setiap consumer downstream sudah berkontribusi. Salah urutan, dan kamu mendorong gradien yang baru setengah jadi ke belakang, menghasilkan jawaban yang salah tanpa pesan error.

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. Angka yang sama, dari program yang hanya diberi tahu aturan untuk +, aturan untuk *, aturan untuk tanh, dan tidak diberi tahu apa pun tentang rangkaian ini.

Dua pemeriksaan independen, karena "cocok dengan yang saya turunkan" adalah tes yang lemah ketika orang yang sama melakukan keduanya.

Diferensiasi numerik. Geser sedikit input dan ukur. Centred difference L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} mengestimasi turunan tanpa kalkulus sama sekali:

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

Terhadap PyTorch, yang punya mesin autodiff industrial yang ditulis oleh orang-orang yang memang bekerja di bidang ini:

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

Kesesuaian di 2×10162 \times 10^{-16}, yaitu machine epsilon untuk float 64-bit: kedua mesin melakukan aritmetika yang identik. Simpan pemeriksaan numerik ini — ia adalah alat untuk debugging backward pass layer baru, dan alasan mengapa gradien yang salah bisa ditemukan sama sekali.

Rangkaian yang sama, input berbeda. Set x=2x = 2 dan y=3y = -3, yang membuat c=6c = 6:

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

Gradien yang melintasi node tanh\tanh turun dengan faktor 9.945. Semua yang berada upstream darinya — dalam jaringan nyata, setiap layer sebelumnya — pada dasarnya tidak menerima apa-apa. Dua jalur melalui rangkaian itu membisu; hanya koneksi langsung yang melewati tanh\tanh yang masih membawa sinyal.

Itulah masalah vanishing gradient, dalam satu node. Tumpuk empat puluh layer tanh\tanh dan kalikan empat puluh faktor seperti itu bersama-sama, maka layer awal berhenti belajar sepenuhnya. Ini juga, kebetulan, argumen untuk skip connection yang bisa kamu lihat dalam bentuk mini di sini: jalur yang melewati nonlinearity adalah satu-satunya yang bertahan.

Apa yang sebenarnya dilakukan zero_grad, dan mengapa bug-nya bersembunyi

Tautan ke bagian: Apa yang sebenarnya dilakukan zero_grad, dan mengapa bug-nya bersembunyi

Setiap _backward menggunakan +=. Itu benar — begitulah cara jalur dijumlahkan. Tetapi ada konsekuensi yang menjebak semua orang: gradien juga terakumulasi lintas panggilan ke backward(). Mesin tidak tahu bahwa panggilan kedua kamu adalah training step baru, bukan jalur lain dalam graph yang sama.

Jadi training loop harus membersihkannya:

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

Ini adalah optimizer.zero_grad() di PyTorch, dan saran biasanya adalah bahwa lupa memanggilnya akan merusak training. Jadi mari hapus dua baris itu dan lihat seberapa rusaknya. Seed yang sama, semuanya sama, 200 step XOR:

learning rateseeddengan resettanpa reset
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

Pada learning rate kecil, versi buggy menang di setiap baris. Ia konvergen ketika versi yang benar macet.

Itu bukan kebetulan dan layak dipahami, karena ini menjelaskan mengapa bug ini begitu sulit ditangkap. Jika kamu tidak pernah membersihkan gradien, maka pada step kk parameter diperbarui oleh jumlah setiap gradien yang sudah dihitung sejauh ini. Pada loss yang terus menunjuk ke arah yang kurang lebih sama, jumlah itu tumbuh stabil, dan efeknya adalah learning rate yang meningkat sendiri. Pada η=0.05\eta = 0.05, ketika algoritme yang benar merangkak, ukuran step yang lepas kendali tampak persis seperti perbaikan.

Lalu lihat tiga baris terbawah. Pada η=0.3\eta = 0.3 mekanisme yang sama menghancurkan model — loss 8.0 adalah skor model yang kolaps menjadi konstanta ±1\pm 1 — setengah dari 16 yang akan dihasilkan empat jawaban yang salah maksimal — — sementara versi yang benar sekarang konvergen dengan bersih.

Jadi pernyataan yang jujur bukan "selalu panggil zero_grad atau modelmu tidak akan train". Melainkan: tanpa itu kamu tidak lagi menjalankan gradient descent. Kamu menjalankan sesuatu yang ukuran step-nya melayang naik pada laju yang tidak dipilih siapa pun, dan ia akan terlihat bekerja, kadang lebih baik daripada yang asli, sampai tiba-tiba tidak — pada titik itu kamu akan menyalahkan learning rate, initialization, atau data. Inilah bentuk bug terburuk dalam machine learning: ia tidak crash, ia mengubah algoritme menjadi algoritme lain yang sesekali mencetak skor lebih baik.

Dengan mesin selesai, jaringan neural hampir tidak butuh kode. Neuron adalah dot product, bias, dan activation; layer adalah daftar neuron; jaringan adalah daftar layer.

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

Tidak ada backward pass di semua itu. Tidak satu baris pun. Class Value sudah tahu cara mendiferensiasikan apa pun yang kebetulan dibangun class-class ini, dan itulah tujuan menulisnya terlebih dahulu: mesin autodiff tidak tahu bahwa ia sedang digunakan untuk jaringan neural.

Sekarang masalah dari Chapter 1. Dua input, dua hidden unit, satu output, sembilan parameter:

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

Empat dari empat. Fungsi yang tidak dapat dihitung oleh perceptron mana pun — dibuktikan di Chapter 1 dengan empat pertidaksamaan yang menuntut bb sekaligus positif dan negatif — dihitung oleh sembilan angka yang ditemukan secara otomatis.

Bagian yang memuaskan bukan bahwa ia bekerja. Melainkan bisa melihat bagaimana, karena dengan dua hidden unit, representasi intermediate adalah titik di sebuah bidang dan kamu bisa langsung mencetaknya.

Setelah dilatih hingga loss 0.001241, berikut posisi setiap input setelah hidden layer, dan apa yang dilakukan output neuron terhadapnya:

inputoutput hidden layerskor outputlabel
(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

Lihat baris pertama dan keempat. Input (0,0)(0,0) dan (1,1)(1,1) adalah sudut-sudut diagonal berlawanan dari persegi — sejauh mungkin dua titik dalam masalah ini bisa terpisah — dan hidden layer memetakannya ke (0.82,0.85)(0.82, -0.85) dan (0.84,0.86)(0.84, -0.86). Hampir titik yang sama. Layer itu telah melipat bidang sehingga dua sudut yang ditolak jatuh di atas satu sama lain, dan begitu mereka berada di tempat yang sama, satu garis memisahkan mereka dari dua lainnya.

Dan output neuron persis garis itu. Parameter hasil belajarnya adalah w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, jadi decision boundary-nya adalah

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

yang merupakan garis lurus — sebuah perceptron, objek yang sama dari Chapter 1, tidak berubah. Dulu ia tidak bisa menyelesaikan XOR dan sekarang pun tidak bisa. Yang berubah adalah ia tidak lagi melihat input; ia melihat ruang yang dibangun layer pertama untuknya, tempat masalah itu linearly separable.

Itulah learned representation, dan kita perlu presisi karena frasa ini dipakai longgar di sisa kursus ini, dan di seluruh bidang ini. Ia bukan kompresi, ringkasan, atau embedding dalam arti mistis apa pun. Ia adalah perubahan koordinat, dipelajari alih-alih didesain, yang satu-satunya tugasnya adalah membuat pekerjaan layer berikutnya mudah.

Universal approximation theorem, dan apa yang tidak dikatakannya

Tautan ke bagian: Universal approximation theorem, dan apa yang tidak dikatakannya

Ada theorem di sini, dan biasanya ia dikutip dengan buruk.

Cybenko pada 1989 dan Hornik pada 1991 membuktikan bahwa feedforward network dengan satu hidden layer dan activation function yang sesuai dapat mengaproksimasi fungsi kontinu apa pun pada compact set, dengan akurasi berapa pun yang kamu mau, jika diberi hidden unit yang cukup.34 Ini hasil yang nyata dan penting: ia mengatakan bahwa arsitektur bukanlah batasannya.

Sekarang baca apa yang tidak ia sertakan. Ia tidak mengatakan berapa banyak unit — batasnya bisa sangat astronomis. Ia tidak mengatakan weight-nya dapat ditemukan; ia menyatakan eksistensi, dan gradient descent dari awal acak bukanlah oracle. Dan ia tidak mengatakan apa pun tentang perilaku pada data yang belum kamu lihat, yang menjadi paruh kedua Chapter 6.

Kesenjangan antara "ada" dan "bisa ditemukan" bukan akademis. Ini masalah XOR yang sama, masing-masing 50 initialization acak, 1000 step, hanya ukuran hidden layer yang diubah:

hidden unitinitialization yang mencapai 4/4
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

Dengan arsitektur minimum yang masih layak, satu dari empat run tidak pernah sampai — ia menetap dalam konfigurasi yang tidak bisa ia turuni keluar darinya, persis local minimum yang ditunjukkan Chapter 3 pada permukaan satu dimensi. Tambah satu unit dan kegagalan nyaris hilang, bukan karena jaringan menjadi lebih ekspresif (dua unit sudah cukup — 38 run membuktikannya), tetapi karena dimensi ekstra memberi descent lebih banyak arah untuk kabur.

Lalu delapan unit justru sedikit lebih buruk daripada empat. Pada learning rate dan step budget yang tetap, kapasitas lebih besar tidak monoton lebih baik. Siapa pun yang bilang solusi untuk jaringan yang macet selalu jaringan yang lebih besar sedang mengekstrapolasi dari bagian tengah tabel itu.

Ini pelajaran yang sama dengan convergence theorem di Chapter 1, dan akan menjadi pelajaran yang sama di Chapter 10 tentang scaling laws, dalam bentuk yang diberikan bab itu: prediksi atas loss bukan prediksi atas capability yang kamu bayar, dan jarak di antara keduanya adalah tempat engineering terjadi.

Tampilkan detail

Opsional: bentuk matriks, dan mengapa kode di atas tidak menggunakannya.

Semua di sini ditulis satu scalar setiap kali, cara paling jelas untuk melihat mekanismenya dan cara paling lambat untuk menjalankannya. Dalam praktiknya, sebuah layer adalah perkalian matriks, dan backward pass dari y=Wx\mathbf{y} = W\mathbf{x} adalah

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 bukan trik untuk diingat; itulah rupa aturan sum-over-paths ketika jalurnya diindeks oleh entry matriks. Objek umumnya adalah Jacobian, matriks semua turunan parsial dari semua output terhadap semua input, dan reverse mode persis merupakan komputasi vector-Jacobian product tanpa pernah membentuk Jacobian — yang penting, karena untuk layer dengan 4096 input dan 4096 output, matriks itu punya enam belas juta entry dan tidak pernah layak dibangun.

Kamu tidak membutuhkan semua ini untuk mengikuti bab berikutnya; versi scalar melakukan semua yang dilakukan versi matriks, lebih lambat. Ini menjadi perlu di Chapter 9, ketika shape berhenti terlihat jelas.

Sekarang kamu punya jaringan yang bisa train. Itu pencapaian yang lebih kecil daripada rasanya, karena jaringan yang kamu punya train pada empat contoh dan diukur pada empat contoh yang sama.

Jalankan kode yang sama pada dataset nyata dan sekumpulan masalah baru muncul, tidak satu pun tentang gradien. Loss turun sebentar lalu berhenti. Atau ia turun pada data training dan naik pada semua yang lain. Atau ia tidak bergerak sama sekali dari step pertama, dan penyebabnya ternyata rentang weight acak awal. Atau input satu unit bergeser negatif pada setiap contoh di epoch tiga dan sejak itu mati, diam-diam, membawa sebagian kapasitas model bersamanya.

Ini bukan kegagalan eksotis; ini kondisi normal jaringan yang baru saja ditulis, dan tidak satu pun mengumumkan dirinya. Gradiennya benar — kamu memeriksanya terhadap PyTorch hingga enam belas angka desimal — dan modelnya tetap tidak belajar.

Chapter 6 membahas itu: initialization, normalization, overfitting dan regularization, serta kebiasaan diagnostik untuk bertanya yang mana dari semua itu yang sedang terjadi sebelum mengubah apa pun. Itulah bedanya jaringan yang berjalan dan jaringan yang bekerja.


Class Value dalam bab ini turun langsung dari micrograd milik Andrej Karpathy, dan videonya The spelled-out intro to neural networks and backpropagation: building micrograd adalah tiga jam terbaik yang bisa kamu habiskan untuk materi ini jika kamu ingin penjelasan kedua dari orang lain. Post 2016-nya Yes you should understand backprop menyampaikan argumen mengapa kamu sebaiknya menulisnya sendiri dan menjadi bacaan wajib di Stanford CS224n. Catatan CS231n tentang backpropagation (cs231n.github.io/optimization-2) adalah pembahasan kanonis tentang pola aliran yang ditabulasikan di atas. Untuk matematika sebagai kalkulus pada graph, bukan sebagai folklore jaringan neural, bab 5.6 dari Mathematics for Machine Learning oleh Deisenroth, Faisal dan Ong luar biasa jelas; dan survei Baydin, Pearlmutter, Radul dan Siskind Automatic Differentiation in Machine Learning: a Survey (arXiv:1502.05767) adalah referensi untuk bidang ini secara keseluruhan, termasuk trade-off forward/reverse yang dibahas di atas.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Tesis master, University of Helsinki (1970). Akumulasi reverse-mode, enam belas tahun sebelum mencapai bidang ini dan dengan motivasi yang sepenuhnya berbeda.

  2. Rumelhart, D. E., Hinton, G. E. dan Williams, R. J. Learning representations by back-propagating errors. Nature 323, hlm. 533–536 (1986). Paper yang membuat metode ini dikenal, dan sumber pembacaan hidden unit sebagai learned representation yang diukur bagian Apa yang dilakukan hidden layer dalam bab ini.

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

  4. Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), hlm. 251–257 (1991). Menggeneralisasi Cybenko: hasilnya bergantung pada activation yang non-polynomial, bukan pada activation yang sigmoidal.

Siap membiarkan LIA yang memilih?

Berkarya dengan semua model AI dalam satu tempat — mulai gratis hari ini.