Μετάβαση στο περιεχόμενο
5/30Κεφάλαιο 5 από 30

Backpropagation από το μηδέν: πρώτα η μηχανή, μετά το δίκτυο

Γράψτε μια autodiff μηχανή 120 γραμμών σε καθαρή Python, ελέγξτε τη με PyTorch και δείτε τι κάνει το zero_grad.

Σε αυτή τη σελίδα

Τέσσερα κεφάλαια μετά, υπάρχει ένα κενό στο κέντρο του μαθήματος.

Το Κεφάλαιο 3 μας έδωσε το gradient descent: για να βελτιώσετε μια παράμετρο, βρείτε την κλίση της απώλειας ως προς αυτήν και κάντε ένα βήμα προς τα κάτω. Το Κεφάλαιο 4 μας έδωσε μια απώλεια που αξίζει να κατεβούμε. Αλλά και στα δύο, η παράγωγος υπολογίστηκε με το χέρι — ένα μοντέλο, μία παράμετρος, μία γραμμή λογισμού, και χωρούσε σε μια σελίδα.

Τώρα στοιβάξτε δύο επίπεδα. Η έξοδος του πρώτου τροφοδοτεί το δεύτερο, άρα κάθε βάρος στο πρώτο επηρεάζει την απώλεια μέσω κάθε νευρώνα στο δεύτερο. Ένα δίκτυο με δύο κρυφά επίπεδα των εκατό μονάδων το καθένα έχει περίπου είκοσι χιλιάδες παραμέτρους, και καθεμία χρειάζεται τη δική της μερική παράγωγο της ίδιας απώλειας. Το να το κάνετε με το χέρι δεν είναι κουραστικό· είναι αδύνατο, και παραμένει αδύνατο για κάθε αρχιτεκτονική στο υπόλοιπο αυτού του μαθήματος.

Η διέξοδος δεν είναι ένας καλύτερος συμβολισμός. Είναι η συνειδητοποίηση ότι η παράγωγος μιας σύνθεσης μπορεί να υπολογιστεί μηχανικά, από ένα πρόγραμμα, από τη δομή του ίδιου του υπολογισμού — και ότι αν το κάνετε προς τη σωστή κατεύθυνση, παίρνετε και τις είκοσι χιλιάδες παραγώγους με κόστος περίπου όσο ο υπολογισμός της απώλειας μία φορά.

Αυτός ο μηχανισμός είναι η αυτόματη διαφοροποίηση reverse-mode. Όταν εφαρμόζεται σε ένα νευρωνικό δίκτυο ονομάζεται backpropagation, και μέχρι το τέλος αυτού του κεφαλαίου θα έχετε γράψει ένα σε περίπου 120 γραμμές Python χωρίς βιβλιοθήκες, θα το έχετε ελέγξει απέναντι στο PyTorch και θα το έχετε χρησιμοποιήσει για να λύσετε το πρόβλημα XOR που σκότωσε το perceptron στο Κεφάλαιο 1.

Πριν χτίσουμε τη μηχανή, πρέπει να ξεκαθαρίσουμε ένα ερώτημα, γιατί αν η απάντηση ήταν διαφορετική δεν θα υπήρχε τίποτα να χτίσουμε.

Το perceptron απέτυχε στο XOR επειδή μία γραμμή δεν μπορεί να διαχωρίσει τα τέσσερα σημεία. Η προφανής διόρθωση είναι η στοίβαξη: περάστε την είσοδο μέσα από ένα γραμμικό επίπεδο και μετά από άλλο ένα. Βοηθάει αυτό;

Όχι, και η απόδειξη είναι δύο γραμμές. Ένα γραμμικό επίπεδο είναι h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1. Τροφοδοτήστε το σε ένα άλλο, y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2, και αντικαταστήστε:

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)

Η σύνθεση είναι Wx+bW\mathbf{x} + \mathbf{b} με W=W2W1W = W_2W_1 και b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2. Μια στοίβα γραμμικών επιπέδων είναι ένα μόνο γραμμικό επίπεδο. Δέκα από αυτά, χίλια από αυτά: ακόμα μία γραμμή, ακόμα ανίκανη να κάνει XOR.

Αξίζει να το δείτε να συμβαίνει αντί να το πιστέψετε απλώς:

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

Όχι περίπου ίσα. Πανομοιότυπα bit προς bit, επειδή είναι η ίδια αριθμητική απλώς αναδιατεταγμένη.

Άρα το βάθος από μόνο του δεν αγοράζει τίποτα. Αυτό που αγοράζει κάτι είναι η τοποθέτηση μιας μη γραμμικής συνάρτησης ανάμεσα στα επίπεδα — και αυτός είναι ολόκληρος ο λόγος που υπάρχουν οι συναρτήσεις ενεργοποίησης. Δεν είναι βιολογικό στολίδι ούτε κόλπο κανονικοποίησης. Χωρίς μία, το δεύτερο επίπεδο είναι διακόσμηση.

Τώρα τα μαθηματικά, και είναι ένας κανόνας που ήδη γνωρίζετε, εφαρμοσμένος κάπου ελαφρώς λιγότερο οικεία.

Ο κανόνας της αλυσίδας για μία μεταβλητή λέει ότι αν το LL εξαρτάται από το cc και το cc εξαρτάται από το xx, τότε dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}. Οι παράγωγοι πολλαπλασιάζονται κατά μήκος μιας αλυσίδας.

Το μέρος που έχει σημασία εδώ είναι τι συμβαίνει όταν μια μεταβλητή τροφοδοτεί περισσότερα από ένα downstream μονοπάτια. Αν το xx επηρεάζει το LL μέσω του aa και επίσης μέσω του bb, οι συνεισφορές προστίθενται:

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}

Πολλαπλασιασμός κατά μήκος ενός μονοπατιού, άθροιση ανάμεσα σε μονοπάτια. Αυτό είναι ολόκληρο το backpropagation, και κάθε λεπτομέρεια υλοποίησης στο υπόλοιπο αυτού του κεφαλαίου — συμπεριλαμβανομένου του += στον κώδικα και της κλήσης zero_grad() που μπερδεύει όλους όσοι γράφουν το πρώτο τους training loop — είναι άμεση συνέπεια αυτής της δεύτερης λέξης.

Πάρτε ένα συγκεκριμένο κύκλωμα πέντε πράξεων, με x=0.5x = 0.5 και 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

Σημειώστε ότι το xx εμφανίζεται τρεις φορές: στο aa, στο bb, και απευθείας στο LL. Κάντε το backward pass στο χαρτί, από δεξιά προς αριστερά, ξεκινώντας από dLdL=1\frac{dL}{dL} = 1:

L=d+xL = d + x, άρα Ld=1\frac{\partial L}{\partial d} = 1 και το άμεσο μονοπάτι συνεισφέρει Lx=1\frac{\partial L}{\partial x} = 1. Η πρόσθεση διανέμει το εισερχόμενο gradient αμετάβλητο και στις δύο εισόδους.

d=tanh(c)d = \tanh(c) με c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33, άρα dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444.

c=abc = ab, άρα dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 και dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711. Ο πολλαπλασιασμός ανταλλάσσει: το gradient κάθε εισόδου κλιμακώνεται από την άλλη είσοδο.

Μέσω του aa: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501. Μέσω του bb: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711. Απευθείας: 11.

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

Κρατήστε αυτόν τον αριθμό. Σε λίγες σελίδες ένα πρόγραμμα θα τον παράγει χωρίς να του έχει ειπωθεί τίποτα από αυτά.

Η ιδέα που το κάνει προγραμματίσιμο: κάθε ένα από αυτά τα βήματα ήταν τοπικό. Για να σπρώξετε ένα gradient μέσα από τον κόμβο πολλαπλασιασμού, χρειαζόσασταν το εισερχόμενο gradient και τις δύο αποθηκευμένες τιμές εισόδου — τίποτα για το υπόλοιπο κύκλωμα. Κάθε πράξη ξέρει πώς να διαφοροποιεί τον εαυτό της.

Οπότε φτιάξτε έναν αριθμό που θυμάται τι τον παρήγαγε.

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

Τέσσερα πεδία. Το data είναι η τιμή. Το grad συσσωρεύει το Lself\frac{\partial L}{\partial \text{self}}. Το _prev είναι το σύνολο των Value από τα οποία υπολογίστηκε αυτό — οι ακμές του γράφου. Και το _backward είναι ένα closure που εγκαθιστά κάθε πράξη: ξέρει πώς να σπρώχνει το gradient αυτού του κόμβου ένα βήμα πίσω προς τις εισόδους του.

Κάθε τελεστής ακολουθεί το ίδιο σχήμα: υπολογίστε την έξοδο, καταγράψτε τους γονείς, εγκαταστήστε τον τοπικό κανόνα.

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

Διαβάστε τα τέσσερα σώματα _backward σαν πίνακα και τα μοτίβα ροής από την απόδειξη στο χαρτί βρίσκονται ακριβώς εκεί:

πράξητι κάνει στο gradient
+διανέμει — το ίδιο gradient σε κάθε είσοδο
*ανταλλάσσει — κάθε είσοδος κλιμακώνεται από την τιμή της άλλης
reluδρομολογεί — το περνάει μέσα ή το μπλοκάρει εντελώς
tanhεξασθενεί — κλιμακώνει κατά 1t21 - t^2, που είναι το πολύ 1 και συνήθως μικρότερο

Κάθε ένα χρησιμοποιεί += και ποτέ =. Αυτός είναι ο κανόνας «άθροιση ανάμεσα σε μονοπάτια», κωδικοποιημένος. Ένας κόμβος που τροφοδοτεί δύο καταναλωτές καλείται δύο φορές, και οι δύο συνεισφορές προστίθενται από μόνες τους.

Μετά ο οδηγός, που είναι το μόνο μέρος με οποιαδήποτε καθολική γνώση:

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 παράγει μια τοπολογική διάταξη του γράφου: κάθε κόμβος εμφανίζεται μετά από όλες τις εισόδους του. Η διάσχιση αυτής της λίστας αντίστροφα εγγυάται ότι όταν καλείτε το _backward ενός κόμβου, το δικό του gradient είναι ήδη πλήρες — κάθε downstream καταναλωτής του έχει ήδη συνεισφέρει. Αν κάνετε λάθος στη σειρά, σπρώχνετε προς τα πίσω ένα μισοτελειωμένο gradient, κάτι που παράγει λάθος απάντηση χωρίς μήνυμα σφάλματος.

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. Ο ίδιος αριθμός, από ένα πρόγραμμα στο οποίο ειπώθηκε ο κανόνας για το +, ο κανόνας για το *, ο κανόνας για το tanh, και τίποτα για αυτό το κύκλωμα.

Δύο ανεξάρτητοι έλεγχοι, επειδή το «ταιριάζει με αυτό που παρήγαγα» είναι αδύναμο τεστ όταν το ίδιο άτομο έκανε και τα δύο.

Αριθμητική διαφοροποίηση. Μετακινήστε ελάχιστα την είσοδο και μετρήστε. Η κεντρική διαφορά L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} εκτιμά την παράγωγο χωρίς καθόλου λογισμό:

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, που έχει μια βιομηχανική autodiff μηχανή γραμμένη από ανθρώπους που κάνουν αυτή τη δουλειά επαγγελματικά:

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}, που είναι το machine epsilon για float 64 bit: οι δύο μηχανές εκτελούν πανομοιότυπη αριθμητική. Κρατήστε τον αριθμητικό έλεγχο στην τσέπη σας — είναι το εργαλείο για το debugging του backward pass ενός νέου layer, και είναι ο λόγος που ένα λάθος gradient μπορεί να βρεθεί.

Το ίδιο κύκλωμα, διαφορετικές είσοδοι. Θέστε x=2x = 2 και y=3y = -3, που κάνει 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

Το gradient που διέσχισε τον κόμβο tanh\tanh έπεσε κατά έναν παράγοντα 9.945. Όλα όσα βρίσκονται ανάντη του — σε ένα πραγματικό δίκτυο, κάθε επίπεδο πριν από αυτό — λαμβάνουν ουσιαστικά τίποτα. Τα δύο μονοπάτια μέσα από το κύκλωμα έχουν σιωπήσει· μόνο η άμεση σύνδεση που παρακάμπτει το tanh\tanh εξακολουθεί να μεταφέρει σήμα.

Αυτό είναι το πρόβλημα του vanishing gradient, σε έναν κόμβο. Στοιβάξτε σαράντα επίπεδα tanh\tanh και πολλαπλασιάστε σαράντα τέτοιους παράγοντες μαζί, και τα πρώτα επίπεδα σταματούν εντελώς να μαθαίνουν. Είναι επίσης, παρεμπιπτόντως, ένα επιχείρημα υπέρ των skip connections που μπορείτε να δείτε εδώ σε μικρογραφία: το μονοπάτι που παρέκαμψε τη μη γραμμικότητα είναι το μόνο που επέζησε.

Τι κάνει πραγματικά το zero_grad και γιατί το bug κρύβεται

Σύνδεσμος στην ενότητα: Τι κάνει πραγματικά το zero_grad και γιατί το bug κρύβεται

Κάθε _backward χρησιμοποιεί +=. Αυτό είναι σωστό — έτσι αθροίζονται τα μονοπάτια. Αλλά έχει μια συνέπεια που πιάνει τους πάντες: τα gradients συσσωρεύονται και ανάμεσα σε κλήσεις του backward(). Η μηχανή δεν έχει ιδέα ότι η δεύτερη κλήση σας είναι ένα νέο βήμα εκπαίδευσης και όχι άλλο ένα μονοπάτι στον ίδιο γράφο.

Οπότε ένα training loop πρέπει να τα καθαρίζει:

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

Αυτό είναι το optimizer.zero_grad() στο PyTorch, και η συνηθισμένη συμβουλή είναι ότι αν το ξεχάσετε, η εκπαίδευση χαλάει. Ας διαγράψουμε λοιπόν αυτές τις δύο γραμμές και ας δούμε πόσο χαλασμένο είναι. Ίδια seeds, ίδια όλα, 200 βήματα XOR:

learning rateseedμε resetχωρίς 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

Στα μικρά learning rates, η ελαττωματική έκδοση κερδίζει σε κάθε γραμμή. Συγκλίνει όταν η σωστή έκδοση κολλάει.

Αυτό δεν είναι σύμπτωση και αξίζει να το καταλάβετε, επειδή εξηγεί γιατί αυτό το bug είναι τόσο δύσκολο να εντοπιστεί. Αν δεν καθαρίζετε ποτέ το gradient, τότε στο βήμα kk η παράμετρος ενημερώνεται από το άθροισμα κάθε gradient που έχει υπολογιστεί μέχρι τότε. Σε μια απώλεια που συνεχίζει να δείχνει περίπου προς την ίδια κατεύθυνση, αυτό το άθροισμα αυξάνεται σταθερά, και το αποτέλεσμα είναι ένα learning rate που μεγαλώνει από μόνο του. Στο η=0.05\eta = 0.05, όπου ο σωστός αλγόριθμος σέρνεται, το ανεξέλεγκτο μέγεθος βήματος μοιάζει ακριβώς με διόρθωση.

Μετά κοιτάξτε τις τρεις τελευταίες γραμμές. Στο η=0.3\eta = 0.3 ο ίδιος μηχανισμός διαλύει το μοντέλο — loss 8.0 είναι αυτό που σκοράρει ένα μοντέλο που κατέρρευσε σε μια σταθερά ±1\pm 1 — το μισό από τα 16 που θα κόστιζαν τέσσερις μέγιστα λάθος απαντήσεις — — ενώ η σωστή έκδοση τώρα συγκλίνει καθαρά.

Άρα η ειλικρινής διατύπωση δεν είναι «να καλείτε πάντα το zero_grad αλλιώς το μοντέλο σας δεν θα εκπαιδευτεί». Είναι: χωρίς αυτό δεν εκτελείτε πλέον gradient descent. Εκτελείτε κάτι του οποίου το μέγεθος βήματος ολισθαίνει προς τα πάνω με ρυθμό που κανείς δεν επέλεξε, και θα φαίνεται να δουλεύει, μερικές φορές καλύτερα από το πραγματικό πράγμα, μέχρι τη στιγμή που δεν θα δουλεύει — οπότε θα κατηγορήσετε το learning rate, την αρχικοποίηση ή τα δεδομένα. Αυτή είναι η μορφή των χειρότερων bugs στη μηχανική μάθηση: δεν κρασάρουν, αλλάζουν τον αλγόριθμο σε διαφορετικό αλγόριθμο που περιστασιακά σκοράρει καλύτερα.

Με τη μηχανή έτοιμη, ένα νευρωνικό δίκτυο είναι ελάχιστος κώδικας. Ένας νευρώνας είναι ένα dot product, ένα bias και μια ενεργοποίηση· ένα επίπεδο είναι μια λίστα νευρώνων· ένα δίκτυο είναι μια λίστα επιπέδων.

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

Δεν υπάρχει backward pass σε τίποτα από αυτά. Ούτε μία γραμμή. Η κλάση Value ξέρει ήδη πώς να διαφοροποιεί οτιδήποτε τυχαίνει να χτίζουν αυτές οι κλάσεις, και αυτό είναι το νόημα του να την έχουμε γράψει πρώτη: μια autodiff μηχανή δεν ξέρει ότι χρησιμοποιείται για νευρωνικό δίκτυο.

Τώρα το πρόβλημα από το Κεφάλαιο 1. Δύο είσοδοι, δύο κρυφές μονάδες, μία έξοδος, εννέα παράμετροι:

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

Τέσσερα στα τέσσερα. Η συνάρτηση που κανένα perceptron δεν μπορεί να υπολογίσει — αποδεδειγμένο στο Κεφάλαιο 1 με τέσσερις ανισότητες που απαιτούσαν το bb να είναι και θετικό και αρνητικό — υπολογίζεται από εννέα αριθμούς που βρέθηκαν αυτόματα.

Το ικανοποιητικό μέρος δεν είναι ότι δουλεύει. Είναι ότι μπορείτε να δείτε πώς, επειδή με δύο κρυφές μονάδες η ενδιάμεση αναπαράσταση είναι ένα σημείο σε επίπεδο και μπορείτε απλώς να την εκτυπώσετε.

Εκπαιδευμένο σε loss 0.001241, εδώ είναι πού προσγειώνεται κάθε είσοδος μετά το κρυφό επίπεδο, και τι κάνει με αυτήν ο νευρώνας εξόδου:

είσοδοςέξοδος κρυφού επιπέδουscore εξόδουlabel
(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

Κοιτάξτε την πρώτη και την τέταρτη γραμμή. Οι είσοδοι (0,0)(0,0) και (1,1)(1,1) είναι διαγώνια αντίθετες γωνίες του τετραγώνου — όσο μακριά μπορούν να είναι δύο σημεία σε αυτό το πρόβλημα — και το κρυφό επίπεδο τις χαρτογραφεί στα (0.82,0.85)(0.82, -0.85) και (0.84,0.86)(0.84, -0.86). Σχεδόν το ίδιο σημείο. Το επίπεδο έχει διπλώσει το επίπεδο έτσι ώστε οι δύο απορριπτέες γωνίες να πέσουν η μία πάνω στην άλλη, και μόλις βρεθούν στο ίδιο μέρος, μία γραμμή τις διαχωρίζει από τις άλλες δύο.

Και ο νευρώνας εξόδου είναι ακριβώς αυτή η γραμμή. Οι μαθημένες παράμετροί του είναι w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893), b=+2.7697b = +2.7697, άρα το όριο απόφασής του είναι

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

που είναι μια ευθεία γραμμή — ένα perceptron, το ίδιο αντικείμενο από το Κεφάλαιο 1, αμετάβλητο. Δεν μπορούσε να λύσει το XOR τότε και δεν μπορεί ούτε τώρα. Αυτό που άλλαξε είναι ότι δεν κοιτάζει πλέον την είσοδο· κοιτάζει έναν χώρο που έχτισε γι’ αυτό το πρώτο επίπεδο, στον οποίο το πρόβλημα είναι γραμμικά διαχωρίσιμο.

Αυτό είναι μια μαθημένη αναπαράσταση, και αξίζει να είμαστε ακριβείς επειδή η φράση χρησιμοποιείται χαλαρά στο υπόλοιπο αυτού του μαθήματος, και στο υπόλοιπο πεδίο. Δεν είναι συμπίεση, σύνοψη ή embedding με κάποια μυστικιστική έννοια. Είναι μια αλλαγή συντεταγμένων, μαθημένη αντί για σχεδιασμένη, της οποίας η μόνη δουλειά είναι να κάνει εύκολη τη δουλειά του επόμενου επιπέδου.

Το θεώρημα καθολικής προσέγγισης, και τι δεν λέει

Σύνδεσμος στην ενότητα: Το θεώρημα καθολικής προσέγγισης, και τι δεν λέει

Υπάρχει ένα θεώρημα εδώ, και συνήθως παρατίθεται άσχημα.

Ο Cybenko το 1989 και ο Hornik το 1991 απέδειξαν ότι ένα feedforward δίκτυο με ένα μόνο κρυφό επίπεδο και κατάλληλη συνάρτηση ενεργοποίησης μπορεί να προσεγγίσει οποιαδήποτε συνεχή συνάρτηση σε συμπαγές σύνολο, με όση ακρίβεια θέλετε, αν του δοθούν αρκετές κρυφές μονάδες.34 Είναι γνήσιο και σημαντικό αποτέλεσμα: λέει ότι η αρχιτεκτονική δεν είναι ο περιορισμός.

Τώρα διαβάστε τι παραλείπει. Δεν λέει πόσες μονάδες — το όριο μπορεί να είναι αστρονομικά μεγάλο. Δεν λέει ότι τα βάρη μπορούν να βρεθούν· ισχυρίζεται ύπαρξη, και το gradient descent από τυχαία αφετηρία δεν είναι μαντείο. Και δεν λέει τίποτα για τη συμπεριφορά σε δεδομένα που δεν έχετε δει, που είναι το δεύτερο μισό του Κεφαλαίου 6.

Το χάσμα ανάμεσα στο «υπάρχει» και στο «μπορεί να βρεθεί» δεν είναι ακαδημαϊκό. Εδώ είναι το ίδιο πρόβλημα XOR, 50 τυχαίες αρχικοποιήσεις η καθεμία, 1000 βήματα, με αλλαγή μόνο στο μέγεθος του κρυφού επιπέδου:

κρυφές μονάδεςαρχικοποιήσεις που έφτασαν 4/4
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 50 (94 %)

Με την ελάχιστη βιώσιμη αρχιτεκτονική, μία εκτέλεση στις τέσσερις δεν φτάνει ποτέ εκεί — καταλήγει σε μια διαμόρφωση από την οποία δεν μπορεί να κατέβει, ακριβώς το τοπικό ελάχιστο που έδειξε το Κεφάλαιο 3 σε μονοδιάστατη επιφάνεια. Προσθέστε μία μονάδα και οι αποτυχίες σχεδόν εξαφανίζονται, όχι επειδή το δίκτυο έγινε πιο εκφραστικό (δύο μονάδες ήδη αρκούν — 38 εκτελέσεις το αποδεικνύουν), αλλά επειδή οι επιπλέον διαστάσεις δίνουν στην κάθοδο περισσότερες κατευθύνσεις διαφυγής.

Και μετά οι οκτώ μονάδες τα πάνε ελαφρώς χειρότερα από τις τέσσερις. Με σταθερό learning rate και προϋπολογισμό βημάτων, περισσότερη χωρητικότητα δεν είναι μονότονα καλύτερη. Όποιος σας λέει ότι η λύση για ένα κολλημένο δίκτυο είναι πάντα ένα μεγαλύτερο δίκτυο κάνει extrapolation από τη μέση εκείνου του πίνακα.

Αυτό είναι το ίδιο μάθημα με το θεώρημα σύγκλισης στο Κεφάλαιο 1, και θα είναι το ίδιο μάθημα στο Κεφάλαιο 10 για τους scaling laws, στη μορφή που του δίνει εκείνο το κεφάλαιο: μια πρόβλεψη της απώλειας δεν είναι πρόβλεψη της capability για την οποία πληρώνετε, και η απόσταση ανάμεσα στα δύο είναι εκεί όπου ζει η μηχανική.

Εμφάνιση λεπτομερειών

Προαιρετικό: η μορφή πινάκων, και γιατί ο παραπάνω κώδικας δεν τη χρησιμοποιεί.

Όλα εδώ έχουν γραφτεί ένα scalar τη φορά, που είναι ο πιο καθαρός τρόπος να δείτε τον μηχανισμό και ο πιο αργός τρόπος να τον εκτελέσετε. Στην πράξη ένα επίπεδο είναι ένας πολλαπλασιασμός πινάκων, και το backward pass του y=Wx\mathbf{y} = W\mathbf{x} είναι

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}}

Οι ανάστροφοι πίνακες δεν είναι κόλπο για απομνημόνευση· είναι η μορφή που παίρνει ο κανόνας άθροισης πάνω στα μονοπάτια όταν τα μονοπάτια είναι δεικτοδοτημένα από στοιχεία πινάκων. Το γενικό αντικείμενο είναι η Ιακωβιανή, ο πίνακας όλων των μερικών παραγώγων όλων των εξόδων ως προς όλες τις εισόδους, και το reverse mode είναι ακριβώς ο υπολογισμός ενός vector-Jacobian product χωρίς ποτέ να σχηματίζεται η Ιακωβιανή — κάτι που έχει σημασία, επειδή για ένα επίπεδο με 4096 εισόδους και 4096 εξόδους αυτός ο πίνακας έχει δεκαέξι εκατομμύρια στοιχεία και δεν αξίζει ποτέ να κατασκευαστεί.

Δεν χρειάζεστε τίποτα από αυτά για να ακολουθήσετε τα επόμενα κεφάλαια· η scalar έκδοση κάνει όλα όσα κάνει η μορφή πινάκων, πιο αργά. Γίνεται απαραίτητη στο Κεφάλαιο 9, όπου τα σχήματα παύουν να είναι προφανή.

Τώρα έχετε ένα δίκτυο που εκπαιδεύεται. Αυτό είναι μικρότερο επίτευγμα απ’ όσο μοιάζει, επειδή το δίκτυο που έχετε εκπαιδεύεται σε τέσσερα παραδείγματα και μετριέται στα ίδια τέσσερα.

Τρέξτε τον ίδιο κώδικα σε ένα πραγματικό dataset και εμφανίζεται ένα νέο σύνολο προβλημάτων, κανένα από τα οποία δεν αφορά τα gradients. Η απώλεια πέφτει για λίγο και μετά σταματά. Ή πέφτει στα δεδομένα εκπαίδευσης και ανεβαίνει σε όλα τα υπόλοιπα. Ή δεν κινείται καθόλου από το πρώτο βήμα, και η αιτία αποδεικνύεται ότι είναι το εύρος των αρχικών τυχαίων βαρών. Ή η είσοδος μιας μονάδας μετακινήθηκε αρνητικά σε κάθε παράδειγμα στην τρίτη εποχή και είναι νεκρή από τότε, σιωπηλά, παίρνοντας μαζί της ένα κομμάτι της χωρητικότητας του μοντέλου.

Αυτές δεν είναι εξωτικές αποτυχίες· είναι η κανονική κατάσταση ενός δικτύου που μόλις γράφτηκε, και καμία δεν αναγγέλλει τον εαυτό της. Το gradient είναι σωστό — το ελέγξατε απέναντι στο PyTorch σε δεκαέξι δεκαδικά ψηφία — και το μοντέλο εξακολουθεί να μη μαθαίνει.

Το Κεφάλαιο 6 αφορά αυτό: αρχικοποίηση, κανονικοποίηση, overfitting και regularisation, και τη διαγνωστική συνήθεια να ρωτάτε ποιο από αυτά συμβαίνει πριν αλλάξετε οτιδήποτε. Είναι η διαφορά ανάμεσα σε ένα δίκτυο που τρέχει και ένα δίκτυο που δουλεύει.


Η κλάση Value σε αυτό το κεφάλαιο κατάγεται απευθείας από το micrograd του Andrej Karpathy, και το βίντεό του The spelled-out intro to neural networks and backpropagation: building micrograd είναι οι καλύτερες τρεις ώρες που μπορείτε να αφιερώσετε σε αυτό το υλικό αν θέλετε να σας το εξηγήσει με δεύτερο τρόπο κάποιος άλλος. Το post του 2016 Yes you should understand backprop επιχειρηματολογεί υπέρ του να γράψετε ένα μόνοι σας και είναι προτεινόμενη ανάγνωση στο CS224n του Stanford. Οι σημειώσεις CS231n για το backpropagation (cs231n.github.io/optimization-2) είναι η κανονική παρουσίαση των μοτίβων ροής που καταγράφηκαν στον παραπάνω πίνακα. Για τα μαθηματικά ως λογισμό πάνω σε γράφο αντί ως folklore των νευρωνικών δικτύων, το κεφάλαιο 5.6 του Mathematics for Machine Learning των Deisenroth, Faisal και Ong είναι ασυνήθιστα καθαρό· και η επισκόπηση των Baydin, Pearlmutter, Radul και Siskind Automatic Differentiation in Machine Learning: a Survey (arXiv:1502.05767) είναι η αναφορά για το πεδίο συνολικά, συμπεριλαμβανομένης της ανταλλαγής forward/reverse που συζητήθηκε παραπάνω.

  1. Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Μεταπτυχιακή διατριβή, University of Helsinki (1970). Reverse-mode συσσώρευση, δεκαέξι χρόνια πριν φτάσει σε αυτό το πεδίο και με εντελώς διαφορετικό κίνητρο.

  2. Rumelhart, D. E., Hinton, G. E. and Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). Το άρθρο που έκανε γνωστή τη μέθοδο, και η πηγή της ανάγνωσης των κρυφών μονάδων ως μαθημένων αναπαραστάσεων στην οποία αφιερώνει τις μετρήσεις της η ενότητα Τι έκανε το κρυφό επίπεδο αυτού του κεφαλαίου.

  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: το αποτέλεσμα εξαρτάται από το να είναι η ενεργοποίηση μη πολυωνυμική, όχι από το να είναι sigmoidal.

Έτοιμοι να αφήσετε τη LIA να επιλέγει;

Δημιουργήστε με κάθε μοντέλο AI σε ένα σημείο — ξεκινήστε δωρεάν σήμερα.