Backpropagation από το μηδέν: πρώτα η μηχανή, μετά το δίκτυο
Γράψτε μια autodiff μηχανή 120 γραμμών σε καθαρή Python, ελέγξτε τη με PyTorch και δείτε τι κάνει το zero_grad.
Σε αυτή τη σελίδα
Τέσσερα κεφάλαια μετά, υπάρχει ένα κενό στο κέντρο του μαθήματος.
Το Κεφάλαιο 3 μας έδωσε το gradient descent: για να βελτιώσετε μια παράμετρο, βρείτε την κλίση της απώλειας ως προς αυτήν και κάντε ένα βήμα προς τα κάτω. Το Κεφάλαιο 4 μας έδωσε μια απώλεια που αξίζει να κατεβούμε. Αλλά και στα δύο, η παράγωγος υπολογίστηκε με το χέρι — ένα μοντέλο, μία παράμετρος, μία γραμμή λογισμού, και χωρούσε σε μια σελίδα.
Τώρα στοιβάξτε δύο επίπεδα. Η έξοδος του πρώτου τροφοδοτεί το δεύτερο, άρα κάθε βάρος στο πρώτο επηρεάζει την απώλεια μέσω κάθε νευρώνα στο δεύτερο. Ένα δίκτυο με δύο κρυφά επίπεδα των εκατό μονάδων το καθένα έχει περίπου είκοσι χιλιάδες παραμέτρους, και καθεμία χρειάζεται τη δική της μερική παράγωγο της ίδιας απώλειας. Το να το κάνετε με το χέρι δεν είναι κουραστικό· είναι αδύνατο, και παραμένει αδύνατο για κάθε αρχιτεκτονική στο υπόλοιπο αυτού του μαθήματος.
Η διέξοδος δεν είναι ένας καλύτερος συμβολισμός. Είναι η συνειδητοποίηση ότι η παράγωγος μιας σύνθεσης μπορεί να υπολογιστεί μηχανικά, από ένα πρόγραμμα, από τη δομή του ίδιου του υπολογισμού — και ότι αν το κάνετε προς τη σωστή κατεύθυνση, παίρνετε και τις είκοσι χιλιάδες παραγώγους με κόστος περίπου όσο ο υπολογισμός της απώλειας μία φορά.
Αυτός ο μηχανισμός είναι η αυτόματη διαφοροποίηση reverse-mode. Όταν εφαρμόζεται σε ένα νευρωνικό δίκτυο ονομάζεται backpropagation, και μέχρι το τέλος αυτού του κεφαλαίου θα έχετε γράψει ένα σε περίπου 120 γραμμές Python χωρίς βιβλιοθήκες, θα το έχετε ελέγξει απέναντι στο PyTorch και θα το έχετε χρησιμοποιήσει για να λύσετε το πρόβλημα XOR που σκότωσε το perceptron στο Κεφάλαιο 1.
Πρώτα: γιατί πρέπει να υπάρχει μη γραμμικότητα
Σύνδεσμος στην ενότητα: Πρώτα: γιατί πρέπει να υπάρχει μη γραμμικότηταΠριν χτίσουμε τη μηχανή, πρέπει να ξεκαθαρίσουμε ένα ερώτημα, γιατί αν η απάντηση ήταν διαφορετική δεν θα υπήρχε τίποτα να χτίσουμε.
Το perceptron απέτυχε στο XOR επειδή μία γραμμή δεν μπορεί να διαχωρίσει τα τέσσερα σημεία. Η προφανής διόρθωση είναι η στοίβαξη: περάστε την είσοδο μέσα από ένα γραμμικό επίπεδο και μετά από άλλο ένα. Βοηθάει αυτό;
Όχι, και η απόδειξη είναι δύο γραμμές. Ένα γραμμικό επίπεδο είναι . Τροφοδοτήστε το σε ένα άλλο, , και αντικαταστήστε:
Η σύνθεση είναι με και . Μια στοίβα γραμμικών επιπέδων είναι ένα μόνο γραμμικό επίπεδο. Δέκα από αυτά, χίλια από αυτά: ακόμα μία γραμμή, ακόμα ανίκανη να κάνει XOR.
Αξίζει να το δείτε να συμβαίνει αντί να το πιστέψετε απλώς:
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]))-4.612963371048 -4.612963371048 0.00e+00Όχι περίπου ίσα. Πανομοιότυπα bit προς bit, επειδή είναι η ίδια αριθμητική απλώς αναδιατεταγμένη.
Άρα το βάθος από μόνο του δεν αγοράζει τίποτα. Αυτό που αγοράζει κάτι είναι η τοποθέτηση μιας μη γραμμικής συνάρτησης ανάμεσα στα επίπεδα — και αυτός είναι ολόκληρος ο λόγος που υπάρχουν οι συναρτήσεις ενεργοποίησης. Δεν είναι βιολογικό στολίδι ούτε κόλπο κανονικοποίησης. Χωρίς μία, το δεύτερο επίπεδο είναι διακόσμηση.
Ο κανόνας της αλυσίδας, στο χαρτί, με κοινό κόμβο
Σύνδεσμος στην ενότητα: Ο κανόνας της αλυσίδας, στο χαρτί, με κοινό κόμβοΤώρα τα μαθηματικά, και είναι ένας κανόνας που ήδη γνωρίζετε, εφαρμοσμένος κάπου ελαφρώς λιγότερο οικεία.
Ο κανόνας της αλυσίδας για μία μεταβλητή λέει ότι αν το εξαρτάται από το και το εξαρτάται από το , τότε . Οι παράγωγοι πολλαπλασιάζονται κατά μήκος μιας αλυσίδας.
Το μέρος που έχει σημασία εδώ είναι τι συμβαίνει όταν μια μεταβλητή τροφοδοτεί περισσότερα από ένα downstream μονοπάτια. Αν το επηρεάζει το μέσω του και επίσης μέσω του , οι συνεισφορές προστίθενται:
Πολλαπλασιασμός κατά μήκος ενός μονοπατιού, άθροιση ανάμεσα σε μονοπάτια. Αυτό είναι ολόκληρο το backpropagation, και κάθε λεπτομέρεια υλοποίησης στο υπόλοιπο αυτού του κεφαλαίου — συμπεριλαμβανομένου του += στον κώδικα και της κλήσης zero_grad() που μπερδεύει όλους όσοι γράφουν το πρώτο τους training loop — είναι άμεση συνέπεια αυτής της δεύτερης λέξης.
Πάρτε ένα συγκεκριμένο κύκλωμα πέντε πράξεων, με και :
Σημειώστε ότι το εμφανίζεται τρεις φορές: στο , στο , και απευθείας στο . Κάντε το backward pass στο χαρτί, από δεξιά προς αριστερά, ξεκινώντας από :
Μέσα από την πρόσθεση
Σύνδεσμος στην ενότητα: Μέσα από την πρόσθεση, άρα και το άμεσο μονοπάτι συνεισφέρει . Η πρόσθεση διανέμει το εισερχόμενο gradient αμετάβλητο και στις δύο εισόδους.
Μέσα από την tanh
Σύνδεσμος στην ενότητα: Μέσα από την tanhμε , άρα .
Μέσα από τον πολλαπλασιασμό
Σύνδεσμος στην ενότητα: Μέσα από τον πολλαπλασιασμό, άρα και . Ο πολλαπλασιασμός ανταλλάσσει: το gradient κάθε εισόδου κλιμακώνεται από την άλλη είσοδο.
Συγκεντρώστε τα τρία μονοπάτια στο x
Σύνδεσμος στην ενότητα: Συγκεντρώστε τα τρία μονοπάτια στο xΜέσω του : . Μέσω του : . Απευθείας: .
Κρατήστε αυτόν τον αριθμό. Σε λίγες σελίδες ένα πρόγραμμα θα τον παράγει χωρίς να του έχει ειπωθεί τίποτα από αυτά.
Χτίζοντας τη μηχανή
Σύνδεσμος στην ενότητα: Χτίζοντας τη μηχανήΗ ιδέα που το κάνει προγραμματίσιμο: κάθε ένα από αυτά τα βήματα ήταν τοπικό. Για να σπρώξετε ένα gradient μέσα από τον κόμβο πολλαπλασιασμού, χρειαζόσασταν το εισερχόμενο gradient και τις δύο αποθηκευμένες τιμές εισόδου — τίποτα για το υπόλοιπο κύκλωμα. Κάθε πράξη ξέρει πώς να διαφοροποιεί τον εαυτό της.
Οπότε φτιάξτε έναν αριθμό που θυμάται τι τον παρήγαγε.
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 συσσωρεύει το . Το _prev είναι το σύνολο των Value από τα οποία υπολογίστηκε αυτό — οι ακμές του γράφου. Και το _backward είναι ένα closure που εγκαθιστά κάθε πράξη: ξέρει πώς να σπρώχνει το gradient αυτού του κόμβου ένα βήμα πίσω προς τις εισόδους του.
Κάθε τελεστής ακολουθεί το ίδιο σχήμα: υπολογίστε την έξοδο, καταγράψτε τους γονείς, εγκαταστήστε τον τοπικό κανόνα.
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 | εξασθενεί — κλιμακώνει κατά , που είναι το πολύ 1 και συνήθως μικρότερο |
Κάθε ένα χρησιμοποιεί += και ποτέ =. Αυτός είναι ο κανόνας «άθροιση ανάμεσα σε μονοπάτια», κωδικοποιημένος. Ένας κόμβος που τροφοδοτεί δύο καταναλωτές καλείται δύο φορές, και οι δύο συνεισφορές προστίθενται από μόνες τους.
Μετά ο οδηγός, που είναι το μόνο μέρος με οποιαδήποτε καθολική γνώση:
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, κάτι που παράγει λάθος απάντηση χωρίς μήνυμα σφάλματος.
Συμφωνεί με το χαρτί;
Σύνδεσμος στην ενότητα: Συμφωνεί με το χαρτί;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)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.40331.8212. Ο ίδιος αριθμός, από ένα πρόγραμμα στο οποίο ειπώθηκε ο κανόνας για το +, ο κανόνας για το *, ο κανόνας για το tanh, και τίποτα για αυτό το κύκλωμα.
Δύο ανεξάρτητοι έλεγχοι, επειδή το «ταιριάζει με αυτό που παρήγαγα» είναι αδύναμο τεστ όταν το ίδιο άτομο έκανε και τα δύο.
Αριθμητική διαφοροποίηση. Μετακινήστε ελάχιστα την είσοδο και μετρήστε. Η κεντρική διαφορά εκτιμά την παράγωγο χωρίς καθόλου λογισμό:
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 μηχανή γραμμένη από ανθρώπους που κάνουν αυτή τη δουλειά επαγγελματικά:
torch dL/dx=1.821202805316 ours=1.821202805316 |diff|=2.22e-16
torch dL/dy=0.403269234753 ours=0.403269234753 |diff|=1.11e-16Συμφωνία στο , που είναι το machine epsilon για float 64 bit: οι δύο μηχανές εκτελούν πανομοιότυπη αριθμητική. Κρατήστε τον αριθμητικό έλεγχο στην τσέπη σας — είναι το εργαλείο για το debugging του backward pass ενός νέου layer, και είναι ο λόγος που ένα λάθος gradient μπορεί να βρεθεί.
Κορεσμός, μετρημένος
Σύνδεσμος στην ενότητα: Κορεσμός, μετρημένοςΤο ίδιο κύκλωμα, διαφορετικές είσοδοι. Θέστε και , που κάνει :
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 που διέσχισε τον κόμβο έπεσε κατά έναν παράγοντα 9.945. Όλα όσα βρίσκονται ανάντη του — σε ένα πραγματικό δίκτυο, κάθε επίπεδο πριν από αυτό — λαμβάνουν ουσιαστικά τίποτα. Τα δύο μονοπάτια μέσα από το κύκλωμα έχουν σιωπήσει· μόνο η άμεση σύνδεση που παρακάμπτει το εξακολουθεί να μεταφέρει σήμα.
Αυτό είναι το πρόβλημα του vanishing gradient, σε έναν κόμβο. Στοιβάξτε σαράντα επίπεδα και πολλαπλασιάστε σαράντα τέτοιους παράγοντες μαζί, και τα πρώτα επίπεδα σταματούν εντελώς να μαθαίνουν. Είναι επίσης, παρεμπιπτόντως, ένα επιχείρημα υπέρ των skip connections που μπορείτε να δείτε εδώ σε μικρογραφία: το μονοπάτι που παρέκαμψε τη μη γραμμικότητα είναι το μόνο που επέζησε.
Τι κάνει πραγματικά το zero_grad και γιατί το bug κρύβεται
Σύνδεσμος στην ενότητα: Τι κάνει πραγματικά το zero_grad και γιατί το bug κρύβεταιΚάθε _backward χρησιμοποιεί +=. Αυτό είναι σωστό — έτσι αθροίζονται τα μονοπάτια. Αλλά έχει μια συνέπεια που πιάνει τους πάντες: τα gradients συσσωρεύονται και ανάμεσα σε κλήσεις του backward(). Η μηχανή δεν έχει ιδέα ότι η δεύτερη κλήση σας είναι ένα νέο βήμα εκπαίδευσης και όχι άλλο ένα μονοπάτι στον ίδιο γράφο.
Οπότε ένα training loop πρέπει να τα καθαρίζει:
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 rate | seed | με reset | χωρίς reset |
|---|---|---|---|
| 0.05 | 1337 | loss 3.255088, 3/4 | loss 0.000000, 4/4 |
| 0.05 | 7 | loss 2.144820, 2/4 | loss 0.000000, 4/4 |
| 0.05 | 42 | loss 2.126074, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 1337 | loss 0.038597, 4/4 | loss 0.000000, 4/4 |
| 0.1 | 7 | loss 2.055048, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 42 | loss 2.049876, 2/4 | loss 0.000073, 4/4 |
| 0.3 | 1337 | loss 4.512310, 2/4 | loss 8.000000, 2/4 |
| 0.3 | 7 | loss 0.015247, 4/4 | loss 4.000000, 3/4 |
| 0.3 | 42 | loss 0.005478, 4/4 | loss 4.000000, 3/4 |
Στα μικρά learning rates, η ελαττωματική έκδοση κερδίζει σε κάθε γραμμή. Συγκλίνει όταν η σωστή έκδοση κολλάει.
Αυτό δεν είναι σύμπτωση και αξίζει να το καταλάβετε, επειδή εξηγεί γιατί αυτό το bug είναι τόσο δύσκολο να εντοπιστεί. Αν δεν καθαρίζετε ποτέ το gradient, τότε στο βήμα η παράμετρος ενημερώνεται από το άθροισμα κάθε gradient που έχει υπολογιστεί μέχρι τότε. Σε μια απώλεια που συνεχίζει να δείχνει περίπου προς την ίδια κατεύθυνση, αυτό το άθροισμα αυξάνεται σταθερά, και το αποτέλεσμα είναι ένα learning rate που μεγαλώνει από μόνο του. Στο , όπου ο σωστός αλγόριθμος σέρνεται, το ανεξέλεγκτο μέγεθος βήματος μοιάζει ακριβώς με διόρθωση.
Μετά κοιτάξτε τις τρεις τελευταίες γραμμές. Στο ο ίδιος μηχανισμός διαλύει το μοντέλο — loss 8.0 είναι αυτό που σκοράρει ένα μοντέλο που κατέρρευσε σε μια σταθερά — το μισό από τα 16 που θα κόστιζαν τέσσερις μέγιστα λάθος απαντήσεις — — ενώ η σωστή έκδοση τώρα συγκλίνει καθαρά.
Άρα η ειλικρινής διατύπωση δεν είναι «να καλείτε πάντα το zero_grad αλλιώς το μοντέλο σας δεν θα εκπαιδευτεί». Είναι: χωρίς αυτό δεν εκτελείτε πλέον gradient descent. Εκτελείτε κάτι του οποίου το μέγεθος βήματος ολισθαίνει προς τα πάνω με ρυθμό που κανείς δεν επέλεξε, και θα φαίνεται να δουλεύει, μερικές φορές καλύτερα από το πραγματικό πράγμα, μέχρι τη στιγμή που δεν θα δουλεύει — οπότε θα κατηγορήσετε το learning rate, την αρχικοποίηση ή τα δεδομένα. Αυτή είναι η μορφή των χειρότερων bugs στη μηχανική μάθηση: δεν κρασάρουν, αλλάζουν τον αλγόριθμο σε διαφορετικό αλγόριθμο που περιστασιακά σκοράρει καλύτερα.
Το δίκτυο, και επιτέλους το XOR
Σύνδεσμος στην ενότητα: Το δίκτυο, και επιτέλους το XORΜε τη μηχανή έτοιμη, ένα νευρωνικό δίκτυο είναι ελάχιστος κώδικας. Ένας νευρώνας είναι ένα dot product, ένα bias και μια ενεργοποίηση· ένα επίπεδο είναι μια λίστα νευρώνων· ένα δίκτυο είναι μια λίστα επιπέδων.
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. Δύο είσοδοι, δύο κρυφές μονάδες, μία έξοδος, εννέα παράμετροι:
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 με τέσσερις ανισότητες που απαιτούσαν το να είναι και θετικό και αρνητικό — υπολογίζεται από εννέα αριθμούς που βρέθηκαν αυτόματα.
Τι έκανε το κρυφό επίπεδο
Σύνδεσμος στην ενότητα: Τι έκανε το κρυφό επίπεδοΤο ικανοποιητικό μέρος δεν είναι ότι δουλεύει. Είναι ότι μπορείτε να δείτε πώς, επειδή με δύο κρυφές μονάδες η ενδιάμεση αναπαράσταση είναι ένα σημείο σε επίπεδο και μπορείτε απλώς να την εκτυπώσετε.
Εκπαιδευμένο σε loss 0.001241, εδώ είναι πού προσγειώνεται κάθε είσοδος μετά το κρυφό επίπεδο, και τι κάνει με αυτήν ο νευρώνας εξόδου:
| είσοδος | έξοδος κρυφού επιπέδου | score εξόδου | label |
|---|---|---|---|
Κοιτάξτε την πρώτη και την τέταρτη γραμμή. Οι είσοδοι και είναι διαγώνια αντίθετες γωνίες του τετραγώνου — όσο μακριά μπορούν να είναι δύο σημεία σε αυτό το πρόβλημα — και το κρυφό επίπεδο τις χαρτογραφεί στα και . Σχεδόν το ίδιο σημείο. Το επίπεδο έχει διπλώσει το επίπεδο έτσι ώστε οι δύο απορριπτέες γωνίες να πέσουν η μία πάνω στην άλλη, και μόλις βρεθούν στο ίδιο μέρος, μία γραμμή τις διαχωρίζει από τις άλλες δύο.
Και ο νευρώνας εξόδου είναι ακριβώς αυτή η γραμμή. Οι μαθημένες παράμετροί του είναι , , άρα το όριο απόφασής του είναι
που είναι μια ευθεία γραμμή — ένα perceptron, το ίδιο αντικείμενο από το Κεφάλαιο 1, αμετάβλητο. Δεν μπορούσε να λύσει το XOR τότε και δεν μπορεί ούτε τώρα. Αυτό που άλλαξε είναι ότι δεν κοιτάζει πλέον την είσοδο· κοιτάζει έναν χώρο που έχτισε γι’ αυτό το πρώτο επίπεδο, στον οποίο το πρόβλημα είναι γραμμικά διαχωρίσιμο.
Αυτό είναι μια μαθημένη αναπαράσταση, και αξίζει να είμαστε ακριβείς επειδή η φράση χρησιμοποιείται χαλαρά στο υπόλοιπο αυτού του μαθήματος, και στο υπόλοιπο πεδίο. Δεν είναι συμπίεση, σύνοψη ή embedding με κάποια μυστικιστική έννοια. Είναι μια αλλαγή συντεταγμένων, μαθημένη αντί για σχεδιασμένη, της οποίας η μόνη δουλειά είναι να κάνει εύκολη τη δουλειά του επόμενου επιπέδου.
Το θεώρημα καθολικής προσέγγισης, και τι δεν λέει
Σύνδεσμος στην ενότητα: Το θεώρημα καθολικής προσέγγισης, και τι δεν λέειΥπάρχει ένα θεώρημα εδώ, και συνήθως παρατίθεται άσχημα.
Ο Cybenko το 1989 και ο Hornik το 1991 απέδειξαν ότι ένα feedforward δίκτυο με ένα μόνο κρυφό επίπεδο και κατάλληλη συνάρτηση ενεργοποίησης μπορεί να προσεγγίσει οποιαδήποτε συνεχή συνάρτηση σε συμπαγές σύνολο, με όση ακρίβεια θέλετε, αν του δοθούν αρκετές κρυφές μονάδες.34 Είναι γνήσιο και σημαντικό αποτέλεσμα: λέει ότι η αρχιτεκτονική δεν είναι ο περιορισμός.
Τώρα διαβάστε τι παραλείπει. Δεν λέει πόσες μονάδες — το όριο μπορεί να είναι αστρονομικά μεγάλο. Δεν λέει ότι τα βάρη μπορούν να βρεθούν· ισχυρίζεται ύπαρξη, και το gradient descent από τυχαία αφετηρία δεν είναι μαντείο. Και δεν λέει τίποτα για τη συμπεριφορά σε δεδομένα που δεν έχετε δει, που είναι το δεύτερο μισό του Κεφαλαίου 6.
Το χάσμα ανάμεσα στο «υπάρχει» και στο «μπορεί να βρεθεί» δεν είναι ακαδημαϊκό. Εδώ είναι το ίδιο πρόβλημα XOR, 50 τυχαίες αρχικοποιήσεις η καθεμία, 1000 βήματα, με αλλαγή μόνο στο μέγεθος του κρυφού επιπέδου:
| κρυφές μονάδες | αρχικοποιήσεις που έφτασαν 4/4 |
|---|---|
| 2 | 38 / 50 (76 %) |
| 3 | 49 / 50 (98 %) |
| 4 | 50 / 50 (100 %) |
| 8 | 47 / 50 (94 %) |
Με την ελάχιστη βιώσιμη αρχιτεκτονική, μία εκτέλεση στις τέσσερις δεν φτάνει ποτέ εκεί — καταλήγει σε μια διαμόρφωση από την οποία δεν μπορεί να κατέβει, ακριβώς το τοπικό ελάχιστο που έδειξε το Κεφάλαιο 3 σε μονοδιάστατη επιφάνεια. Προσθέστε μία μονάδα και οι αποτυχίες σχεδόν εξαφανίζονται, όχι επειδή το δίκτυο έγινε πιο εκφραστικό (δύο μονάδες ήδη αρκούν — 38 εκτελέσεις το αποδεικνύουν), αλλά επειδή οι επιπλέον διαστάσεις δίνουν στην κάθοδο περισσότερες κατευθύνσεις διαφυγής.
Και μετά οι οκτώ μονάδες τα πάνε ελαφρώς χειρότερα από τις τέσσερις. Με σταθερό learning rate και προϋπολογισμό βημάτων, περισσότερη χωρητικότητα δεν είναι μονότονα καλύτερη. Όποιος σας λέει ότι η λύση για ένα κολλημένο δίκτυο είναι πάντα ένα μεγαλύτερο δίκτυο κάνει extrapolation από τη μέση εκείνου του πίνακα.
Αυτό είναι το ίδιο μάθημα με το θεώρημα σύγκλισης στο Κεφάλαιο 1, και θα είναι το ίδιο μάθημα στο Κεφάλαιο 10 για τους scaling laws, στη μορφή που του δίνει εκείνο το κεφάλαιο: μια πρόβλεψη της απώλειας δεν είναι πρόβλεψη της capability για την οποία πληρώνετε, και η απόσταση ανάμεσα στα δύο είναι εκεί όπου ζει η μηχανική.
Εμφάνιση λεπτομερειών
Προαιρετικό: η μορφή πινάκων, και γιατί ο παραπάνω κώδικας δεν τη χρησιμοποιεί.
Όλα εδώ έχουν γραφτεί ένα scalar τη φορά, που είναι ο πιο καθαρός τρόπος να δείτε τον μηχανισμό και ο πιο αργός τρόπος να τον εκτελέσετε. Στην πράξη ένα επίπεδο είναι ένας πολλαπλασιασμός πινάκων, και το backward pass του είναι
Οι ανάστροφοι πίνακες δεν είναι κόλπο για απομνημόνευση· είναι η μορφή που παίρνει ο κανόνας άθροισης πάνω στα μονοπάτια όταν τα μονοπάτια είναι δεικτοδοτημένα από στοιχεία πινάκων. Το γενικό αντικείμενο είναι η Ιακωβιανή, ο πίνακας όλων των μερικών παραγώγων όλων των εξόδων ως προς όλες τις εισόδους, και το 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 που συζητήθηκε παραπάνω.
Παραπομπές
Σύνδεσμος στην ενότητα: Παραπομπές-
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 συσσώρευση, δεκαέξι χρόνια πριν φτάσει σε αυτό το πεδίο και με εντελώς διαφορετικό κίνητρο. ↩
-
Rumelhart, D. E., Hinton, G. E. and Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). Το άρθρο που έκανε γνωστή τη μέθοδο, και η πηγή της ανάγνωσης των κρυφών μονάδων ως μαθημένων αναπαραστάσεων στην οποία αφιερώνει τις μετρήσεις της η ενότητα Τι έκανε το κρυφό επίπεδο αυτού του κεφαλαίου. ↩
-
Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, pp. 303–314 (1989). ↩
-
Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), pp. 251–257 (1991). Γενικεύει τον Cybenko: το αποτέλεσμα εξαρτάται από το να είναι η ενεργοποίηση μη πολυωνυμική, όχι από το να είναι sigmoidal. ↩