Das Training von Anfang bis Ende
Kurz gesagt: Ein neuronales Netz zu trainieren ist eine Schleife über Epochen: mischen, Batches, ein Vorwärtsdurchlauf, Backpropagation und ein Schritt, bis der Validierungsfehler nicht mehr fällt.
Problem
Das Netz sagt −0,23 Tage
Die Kasse soll „kommt in 2 Tagen an“ anzeigen. Die Lieferzeit hängt von der Entfernung ab, davon, ob die Bestellung nach 16 Uhr aufgegeben wird (dann geht sie erst am nächsten Tag raus), und ein wenig vom Gewicht. Eine Gerade aus der linearen Regression folgt dieser Kurve nicht: In der Stadt ist es etwa ein Tag, bis 200 km wächst es schnell, dann immer langsamer.
Das Netz aus der Backprop-Lektion kann einer Kurve folgen. Hier ist es mit 4 verborgenen Neuronen und zufälligen Gewichten, vor dem Training:
Rng rng = new Rng(42);Net net = new Net(4, 3);net.randomize(rng, 1.0);System.out.printf("Fehler (MSE): %.4f%n", net.loss(trainX, trainY));for (int k : new int[] {0, 12, 23}) System.out.printf("%.0f km: das Netz sagt %.2f, tatsächlich %.1f Tage%n", TRAIN[k][0], net.predict(trainX[k]), trainY[k]);Fehler (MSE): 7.18555 km: das Netz sagt -0.23, tatsächlich 1.0 Tage150 km: das Netz sagt -0.17, tatsächlich 2.3 Tage520 km: das Netz sagt 0.18, tatsächlich 3.0 TageEine Bestellung, die 5 km entfernt ist, kommt in −0,23 Tagen an. In der Backprop-Lektion hast du einen Schritt für ein Beispiel gesehen. Wie viele Schritte brauchst du, mit welchen Beispielen, in welcher Reihenfolge, und wann hörst du auf?
Die Idee
Die Trainingsschleife
Training ist eine Schleife, die sich dreht, bis das Netz sich nicht mehr verbessert:
- Startgewichte: kleine Zufallszahlen aus einem Generator mit Seed.
- Epoche: ein Durchgang durch alle Trainingsbeispiele. Vor jeder Epoche werden die Beispiele gemischt, damit das Netz ihre Reihenfolge nicht lernt.
- Batch: ein paar Beispiele auf einmal. Jedes bekommt einen Vorwärtsdurchlauf und einen Fehler, dann einen Rückwärtsdurchlauf. Die Steigungen werden gemittelt, und das Netz macht einen Schritt.
- Kontrolle: Alle 100 Epochen gibt das Programm den Fehler auf Training und Validierung aus.
Das ist die Trainingsschleife. Die Zahlen, die sie ausgibt, als Diagramm gezeichnet, sind die Fehlerkurve (Loss Curve). An ihr siehst du, ob das Netz lernt, ob die Rate gut ist und wann du aufhören solltest.
Die Daten sind 32 Bestellungen aus dem Shop: 24 zum Training und 8 zur Validierung. Mit den Validierungsbestellungen macht das Netz nie einen Schritt. Sie sind das Maß für neue Bestellungen.
Sieh es dir an
Die Daten und das Netz
Die Bestellungen, im Code aufgeschrieben:
// Bestellung: km, nach 16 Uhr (1 oder 0), kg, Tage bis zur Lieferungstatic final double[][] TRAIN = { {5, 0, 1.2, 1.0}, {12, 1, 3.5, 1.6}, {18, 0, 0.8, 1.0}, {25, 0, 6.0, 1.2}, {28, 1, 2.1, 1.8}, {35, 0, 4.4, 1.3}, {48, 1, 0.5, 2.2}, {60, 0, 9.0, 2.5}, {75, 0, 1.7, 1.6}, {90, 1, 2.9, 2.6}, {110, 0, 5.5, 1.9}, {130, 1, 0.9, 2.7}, {150, 0, 3.1, 2.3}, {170, 0, 8.2, 3.3}, {185, 1, 1.4, 3.1}, {195, 0, 2.6, 2.3}, {210, 0, 4.8, 2.6}, {240, 1, 0.6, 3.3}, {280, 0, 8.6, 3.6}, {320, 0, 1.9, 2.7}, {360, 1, 3.3, 3.6}, {410, 0, 5.0, 2.8}, {460, 1, 2.2, 3.6}, {520, 0, 0.7, 3.0}};// Validierung: Bestellungen, die das Netz beim Training nicht siehtstatic final double[][] VALID = { {8, 1, 2.4, 1.6}, {22, 0, 8.8, 1.0}, {40, 0, 1.1, 1.4}, {95, 1, 7.9, 2.5}, {160, 0, 2.0, 2.3}, {205, 1, 3.9, 3.1}, {300, 0, 9.4, 2.6}, {480, 1, 4.6, 3.6}};Die Merkmale werden auf ähnliche Größenordnungen gebracht: Kilometer in Hunderten, Gewicht in Zehnern von Kilogramm. Sonst würden 520 km alles andere übertönen:
// Merkmale in ähnlichen Größenordnungen: Hunderte km, 0 oder 1, Zehner kgstatic double[][] inputs(double[][] orders) { double[][] xs = new double[orders.length][]; for (int k = 0; k < orders.length; k++) xs[k] = new double[] {orders[k][0] / 100, orders[k][1], orders[k][2] / 10}; return xs;}
static double[] targets(double[][] orders) { double[] ys = new double[orders.length]; for (int k = 0; k < orders.length; k++) ys[k] = orders[k][3]; return ys;}Der Zufall kommt aus unserem eigenen Generator mit Seed. Ein Seed liefert auf jedem Computer und in allen vier Sprachen dieselben Zahlen, deshalb gibt die Lektion überall dieselbe Ausgabe aus. Die Zahl 48271 und die Division durch 2³¹ − 1 sind ein alter, gut getesteter Generator (Park–Miller). Das Produkt bleibt unter 2⁵³, ist also auch in JavaScript exakt:
class Rng { private long state;
Rng(long seed) { state = seed; }
// die nächste Zahl zwischen 0 und 1; ein Seed liefert immer dieselben Zahlen double next() { state = state * 48271 % 2147483647; return (double) state / 2147483647; }}new Random(42) in Java hat auch einen Seed. Hier ist der Generator unser eigener, also ist er in den anderen drei Sprachen derselbe.Das Netz ist das aus der Backprop-Lektion, in eine Klasse gepackt: 3 Eingaben, 4 verborgene Neuronen mit einer Sigmoid-Funktion und eine Ausgabe ohne Aktivierung, weil Tage keine Wahrscheinlichkeit sind. step macht für jede Bestellung des Batches einen Vorwärts- und einen Rückwärtsdurchlauf, addiert die Steigungen und bewegt jedes Gewicht einmal, um die mittlere Steigung:
class Net { final double[][] w1; // Eingabe → verborgene Schicht final double[] b1; final double[] w2; // verborgene Schicht → Ausgabe double b2; final double[] h; // die Ausgaben der verborgenen Schicht vom letzten predict
Net(int hidden, int inputs) { w1 = new double[hidden][inputs]; b1 = new double[hidden]; w2 = new double[hidden]; h = new double[hidden]; }
// zufällige Gewichte von -scale bis scale; die Biases bleiben 0 void randomize(Rng rng, double scale) { for (double[] row : w1) for (int i = 0; i < row.length; i++) row[i] = (rng.next() * 2 - 1) * scale; for (int j = 0; j < w2.length; j++) w2[j] = (rng.next() * 2 - 1) * scale; }
static double sigmoid(double z) { return 1 / (1 + Math.exp(-z)); }
double predict(double[] x) { double y = b2; for (int j = 0; j < w2.length; j++) { double z = b1[j]; for (int i = 0; i < x.length; i++) z += w1[j][i] * x[i]; h[j] = sigmoid(z); y += w2[j] * h[j]; } return y; }
double loss(double[][] xs, double[] ys) { double total = 0; for (int k = 0; k < xs.length; k++) { double e = predict(xs[k]) - ys[k]; total += e * e; } return total / xs.length; }
// ein Schritt: die mittlere Steigung über die Beispiele des Batches void step(double[][] xs, double[] ys, int[] batch, double rate) { int hidden = w2.length, inputs = xs[0].length; double[][] gw1 = new double[hidden][inputs]; double[] gb1 = new double[hidden], gw2 = new double[hidden]; double gb2 = 0; for (int k : batch) { double d = 2 * (predict(xs[k]) - ys[k]); gb2 += d; for (int j = 0; j < hidden; j++) { gw2[j] += d * h[j]; double dz = d * w2[j] * h[j] * (1 - h[j]); gb1[j] += dz; for (int i = 0; i < inputs; i++) gw1[j][i] += dz * xs[k][i]; } } int n = batch.length; b2 -= rate * gb2 / n; for (int j = 0; j < hidden; j++) { w2[j] -= rate * gw2[j] / n; b1[j] -= rate * gb1[j] / n; for (int i = 0; i < inputs; i++) w1[j][i] -= rate * gw1[j][i] / n; } }}Das Mischen ist Fisher–Yates: Von hinten nach vorn tauscht jede Stelle mit einer zufälligen Stelle vor ihr.
// Fisher–Yates: Jede Reihenfolge ist gleich wahrscheinlichstatic void shuffle(int[] order, Rng rng) { for (int i = order.length - 1; i > 0; i--) { int k = (int) (rng.next() * (i + 1)); int tmp = order[i]; order[i] = order[k]; order[k] = tmp; }}Sieh es dir an
Die Schleife und die Fehlerkurve
Rng rng = new Rng(42);Net net = new Net(4, 3);net.randomize(rng, 1.0);int size = 8;double rate = 0.05;int[] order = IntStream.range(0, trainX.length).toArray();System.out.println("Epoche Training Validierung");for (int epoch = 1; epoch <= 1000; epoch++) { shuffle(order, rng); for (int start = 0; start < order.length; start += size) { int end = Math.min(start + size, order.length); int[] batch = Arrays.copyOfRange(order, start, end); net.step(trainX, trainY, batch, rate); } if (epoch % 100 == 0) { double t = net.loss(trainX, trainY); double v = net.loss(validX, validY); System.out.printf("%6d%10.4f%13.4f%n", epoch, t, v); }}Schritt für Schritt, in Worten
- Seed 42: Jeder Durchlauf beginnt mit denselben zufälligen Gewichten. Vor dem Training liegt der Fehler bei 7,19, weit über dem Diagramm.
- Eine Epoche ist ein Durchgang durch alle 24 Trainingsbestellungen. Zuerst werden sie gemischt, mit demselben Generator.
- Ein Batch aus 8 Bestellungen: vorwärts, Fehler, rückwärts, ein Schritt. Drei Schritte pro Epoche, 3000 im ganzen Training.
- Epoche 100: Training 0,1365, Validierung 0,0756. Mit den Validierungsbestellungen wurde nie ein Schritt gemacht.
- Die Validierung ist bei Epoche 200 am niedrigsten: 0,0705. Danach steigt sie wieder, während das Training weiter fällt.
- Epoche 1000: Training 0,0381, Validierung 0,2371. Das Netz lernt drei schwere Pakete auswendig, die zufällig zu spät kamen.
- Das beste Netz liegt bei Epoche 200, nicht bei 1000. Man stoppt, wenn die Validierung nicht mehr fällt.
Die ganze Schleife: 1.000 Epochen, Batches von 8, Rate 0,05:
Rng rng = new Rng(42);Net net = new Net(4, 3);net.randomize(rng, 1.0);int size = 8;double rate = 0.05;int[] order = IntStream.range(0, trainX.length).toArray();System.out.println("Epoche Training Validierung");for (int epoch = 1; epoch <= 1000; epoch++) { shuffle(order, rng); for (int start = 0; start < order.length; start += size) { int end = Math.min(start + size, order.length); int[] batch = Arrays.copyOfRange(order, start, end); net.step(trainX, trainY, batch, rate); } if (epoch % 100 == 0) { double t = net.loss(trainX, trainY); double v = net.loss(validX, validY); System.out.printf("%6d%10.4f%13.4f%n", epoch, t, v); }}Epoche Training Validierung 100 0.1365 0.0756 200 0.1041 0.0705 300 0.0826 0.0776 400 0.0667 0.1048 500 0.0556 0.1350 600 0.0486 0.1718 700 0.0443 0.1967 800 0.0417 0.2202 900 0.0396 0.2243 1000 0.0381 0.2371Das Training fällt von 7,19 auf 0,0381. Die Validierung fällt bis Epoche 200 und steigt dann: Bei Epoche 1.000 ist sie 0,2371, mehr als dreimal schlechter als die beste. Das Netz passt sich weiter den Trainingsbestellungen an, auch dem, was in ihnen Zufall ist. Darauf kommen wir am Ende der Lektion zurück.
Probier es aus
Sag die Ausgabe voraus
Wenn der Batch 10 ist und es 24 Beispiele gibt, wie viele Schritte macht das Training in 1.000 Epochen?
int examples = 24, size = 10, steps = 0;for (int epoch = 1; epoch <= 1000; epoch++) for (int start = 0; start < examples; start += size) steps++;System.out.println("Schritte: " + steps);24 Beispiele zu je 10 sind drei Batches: 10, 10 und 4. Der letzte ist kleiner, aber ein Schritt wie die anderen, also sind es 3 pro Epoche und 3.000 insgesamt. In step wird die mittlere Steigung über die Zahl der Beispiele im Batch gebildet, also macht auch der kleine Batch einen Schritt normaler Größe.
Die Idee
Batches: alle, einer nach dem anderen oder 8
Wie viele Beispiele sollen in einen Schritt eingehen? Hier ist dasselbe Netz für 40 Epochen mit einem Batch von 24 (alle auf einmal), 8 und 1. Nach jedem Schritt misst das Programm den Fehler über alle 24 Bestellungen und zählt, wie oft der Schritt ihn erhöht hat:
for (int size : new int[] {24, 8, 1}) { Rng rng = new Rng(42); Net net = new Net(4, 3); net.randomize(rng, 1.0); int[] order = IntStream.range(0, trainX.length).toArray(); int steps = 0, up = 0; double before = net.loss(trainX, trainY); for (int epoch = 1; epoch <= 40; epoch++) { shuffle(order, rng); for (int start = 0; start < order.length; start += size) { int end = Math.min(start + size, order.length); net.step(trainX, trainY, Arrays.copyOfRange(order, start, end), 0.05); double after = net.loss(trainX, trainY); steps++; if (after > before) up++; before = after; } } System.out.printf("Batch %2d: %3d Schritte, %3d aufwärts, Fehler %.4f%n", size, steps, up, before);}Batch 24: 40 Schritte, 0 aufwärts, Fehler 0.2913Batch 8: 120 Schritte, 21 aufwärts, Fehler 0.1840Batch 1: 960 Schritte, 495 aufwärts, Fehler 0.1238- Alle auf einmal: Die Steigung ist die exakte Steigung des ganzen Fehlers, also erhöht kein Schritt ihn. Aber es gibt nur einen Schritt pro Epoche, und nach 40 Epochen ist das Netz am weitesten entfernt.
- Ein Beispiel nach dem anderen: 24 Schritte pro Epoche, also kommt es in 40 Epochen am tiefsten. Aber die Steigung eines Beispiels ist eine grobe Schätzung der Steigung aller: 495 von 960 Schritten erhöhen den Fehler.
- Ein Batch von 8: drei Schritte pro Epoche und nur 21 von 120 aufwärts.
Das nennt man Mini-Batch: ein Schritt um die mittlere Steigung weniger Beispiele. Der Batch ist der Kompromiss, den fast alle benutzen. Auf einer Grafikkarte werden die Beispiele eines Batches auf einmal berechnet, deshalb sind Batches in echten Projekten Dutzende oder Hunderte von Beispielen.
Probier es aus
Ergänze
Die Steigungen des Batches werden addiert, und der Schritt geht um die mittlere Steigung. Wodurch teilst du?
int n = ;
b2 -= rate * gb2 / n;Durch die Zahl der Beispiele im Batch: batch.length. Ohne die Division macht ein Batch von 24 einen 24-mal größeren Schritt als ein Batch von 1, und den Batch zu ändern ändert auch die Lernrate.
Die Idee
Eine schlechte Rate, ein schlechter Start
Die Kurve sagt dir, was nicht stimmt. Die drei Raten aus der Lektion über den Gradientenabstieg, aber in einem Netz. Der Fehler bei den Epochen 50, 100 und 200:
for (double rate : new double[] {0.005, 0.05, 2.0}) { Rng rng = new Rng(42); Net net = new Net(4, 3); net.randomize(rng, 1.0); int[] order = IntStream.range(0, trainX.length).toArray(); System.out.printf("Rate %.3f:", rate); for (int epoch = 1; epoch <= 200; epoch++) { trainEpoch(net, trainX, trainY, order, 8, rate, rng); double loss = net.loss(trainX, trainY); if (Double.isNaN(loss) || loss > 1e6) { System.out.print(" explodiert bei Epoche " + epoch); break; } if (epoch == 50 || epoch == 100 || epoch == 200) System.out.printf(" %.4f", loss); } System.out.println();}Rate 0.005: 0.3892 0.3201 0.2472Rate 0.050: 0.1698 0.1365 0.1041Rate 2.000: explodiert bei Epoche 2Mit 0,005 lernt das Netz, aber langsam: Bei Epoche 200 (0,2472) ist es schlechter als 0,05 bei Epoche 50 (0,1698). Die Kurve fällt sanft und hört nicht auf zu fallen, also kannst du die Rate erhöhen. Mit 2 schießt jeder Schritt über den tiefsten Punkt hinaus, und die nächste Steigung ist noch steiler. Der Fehler liegt schon in der zweiten Epoche über einer Million, und danach kommen unendlich und NaN. Deshalb prüft das Programm und bricht ab.
Auch der Start zählt:
Rng rng = new Rng(42);Net zeros = new Net(4, 3); // ohne randomize: Jedes Gewicht ist 0int[] order = IntStream.range(0, trainX.length).toArray();for (int epoch = 1; epoch <= 200; epoch++) trainEpoch(zeros, trainX, trainY, order, 8, 0.05, rng);System.out.printf("Nullen: Fehler %.4f%n", zeros.loss(trainX, trainY));for (int j = 0; j < zeros.w1.length; j++) System.out.printf(" Neuron %d: %.2f %.2f %.2f%n", j + 1, zeros.w1[j][0], zeros.w1[j][1], zeros.w1[j][2]);
for (double scale : new double[] {8, 1}) { rng = new Rng(42); Net net = new Net(4, 3); net.randomize(rng, scale); double first = net.loss(trainX, trainY); order = IntStream.range(0, trainX.length).toArray(); for (int epoch = 1; epoch <= 200; epoch++) trainEpoch(net, trainX, trainY, order, 8, 0.05, rng); System.out.printf("Gewichte ×%.0f: am Anfang %.4f, nach 200 Epochen %.4f%n", scale, first, net.loss(trainX, trainY));}Nullen: Fehler 0.1127 Neuron 1: 0.99 0.47 0.19 Neuron 2: 0.99 0.47 0.19 Neuron 3: 0.99 0.47 0.19 Neuron 4: 0.99 0.47 0.19Gewichte ×8: am Anfang 33.9817, nach 200 Epochen 0.2971Gewichte ×1: am Anfang 7.1855, nach 200 Epochen 0.1041- Nullen. Die vier verborgenen Neuronen starten identisch, bekommen identische Steigungen und machen identische Schritte. Nach 200 Epochen sind ihre Gewichte bis zur letzten Stelle gleich: Das Netz hat ein verborgenes Neuron, viermal wiederholt. Zufällige Startgewichte gibt es, damit die Neuronen verschieden werden.
- Gewichte ×8. Die Summen in der Sigmoid-Funktion sind groß, also ist sie für die meisten Bestellungen fast 0 oder fast 1, wo sie flach ist. Eine flache Kurve bedeutet eine fast null Steigung und winzige Schritte: Nach 200 Epochen ist der Fehler fast dreimal so groß wie mit Gewichten ×1.
Probier es aus
Finde den Fehler
Ein Kollege schreibt die Schleife auf seine Art und mischt selbst die Arrays statt der Indizes. Der Fehler bleibt über 0,6 und fällt nicht, egal wie viele Epochen er laufen lässt. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Die zwei Mischvorgänge nehmen verschiedene Zufallszahlen aus dem Generator, also stehen danach die Bestellungen und die Antworten nicht mehr in derselben Reihenfolge. Das Netz lernt zum Beispiel eine Bestellung, die 5 km entfernt ist, mit der Antwort einer Bestellung, die 410 km entfernt ist. Das Beste, was es tun kann, ist für alle den Durchschnitt zu sagen, und der Fehler bleibt etwa bei der Streuung der Antworten. Die Lösung ist wie in der Lektion: die Indizes (order) mischen und Bestellung und Antwort mit demselben Index nehmen.
Die Idee
Wann man aufhört
Was genau lernt das Netz auswendig? Unter den Trainingsbestellungen sind drei schwere Pakete (9,0 kg, 8,2 kg und 8,6 kg), die zufällig einen ganzen Tag zu spät waren. Hier sind die drei schweren Pakete aus der Validierung, die nicht zu spät waren:
Rng rng = new Rng(42);Net net = new Net(4, 3);net.randomize(rng, 1.0);int[] order = IntStream.range(0, trainX.length).toArray();for (int epoch = 1; epoch <= 1000; epoch++) { trainEpoch(net, trainX, trainY, order, 8, 0.05, rng); if (epoch == 200 || epoch == 1000) { System.out.println("Epoche " + epoch + ":"); for (int k : new int[] {1, 3, 6}) // die schweren Pakete aus der Validierung System.out.printf(" %.0f km, %.1f kg: %.2f (tatsächlich %.1f)%n", VALID[k][0], VALID[k][2], net.predict(validX[k]), validY[k]); }}Epoche 200: 22 km, 8.8 kg: 1.31 (tatsächlich 1.0) 95 km, 7.9 kg: 2.53 (tatsächlich 2.5) 300 km, 9.4 kg: 3.13 (tatsächlich 2.6)Epoche 1000: 22 km, 8.8 kg: 1.84 (tatsächlich 1.0) 95 km, 7.9 kg: 3.09 (tatsächlich 2.5) 300 km, 9.4 kg: 3.38 (tatsächlich 2.6)Bei Epoche 200 liegen die Vorhersagen nah an der Wahrheit. Bei Epoche 1.000 hat das Netz „Schweres reist langsamer“ gelernt: Das Paket in 22 km Entfernung bekommt 1,84 Tage statt 1,0. Eine Regel, die in den Daten nur Zufall ist, ist Überanpassung: Das Training wird besser, und neue Bestellungen werden schlechter.
Deshalb hörst du auf, wenn die Validierung nicht mehr fällt. Das ist Early Stopping. Das Programm prüft die Validierung alle 10 Epochen und behält einen Schnappschuss (eine Kopie) des besten Netzes. Gibt es 100 Epochen lang keine Verbesserung, hält es an und gibt den Schnappschuss zurück:
// eine Kopie der Gewichte: ein Schnappschuss des Netzes in diesem MomentNet copy() { Net c = new Net(w2.length, w1[0].length); for (int j = 0; j < w2.length; j++) { c.w1[j] = w1[j].clone(); c.b1[j] = b1[j]; c.w2[j] = w2[j]; } c.b2 = b2; return c;}Rng rng = new Rng(42);Net net = new Net(4, 3);net.randomize(rng, 1.0);int[] order = IntStream.range(0, trainX.length).toArray();// alle 10 Epochen prüfen; nach 100 ohne Verbesserung stoppendouble best = Double.MAX_VALUE;int bestEpoch = 0, epoch = 0;Net bestNet = net.copy();while (epoch < 1000 && epoch - bestEpoch < 100) { epoch++; trainEpoch(net, trainX, trainY, order, 8, 0.05, rng); if (epoch % 10 == 0) { double valid = net.loss(validX, validY); if (valid < best) { best = valid; bestEpoch = epoch; bestNet = net.copy(); } }}System.out.printf("niedrigste Validierung: %.4f bei Epoche %d%n", best, bestEpoch);System.out.println("gestoppt bei Epoche " + epoch);System.out.printf("ich behalte das Netz von Epoche %d: %.4f%n", bestEpoch, bestNet.loss(validX, validY));niedrigste Validierung: 0.0667 bei Epoche 230gestoppt bei Epoche 330ich behalte das Netz von Epoche 230: 0.0667Die beste Validierung ist 0,0667 bei Epoche 230, besser als jeder Punkt auf der Kurve oben, weil die Prüfung häufiger ist. Das Training hält bei Epoche 330 statt 1.000 an, und das behaltene Netz ist das von Epoche 230.
Die Validierung hat entschieden, wann man aufhört, also ist sie kein ehrliches Maß mehr für neue Bestellungen. Die Endnote wird auf einem dritten Teil der Daten genommen, dem Testsatz, der an keiner Entscheidung beteiligt war.
Die Idee
Wann es schadet
- Ein Netz für alles. 32 Bestellungen und drei Merkmale sind wenig Daten: Eine Gerade mit ein paar Merkmalen oder eine einfachere Formel reicht oft, ist leichter zu erklären und lernt nicht auswendig. Ein Netz lohnt sich, wenn die Regeln zu viele zum Aufschreiben sind und es genug Beispiele gibt.
- Ein Lauf. Ein anderer Seed liefert andere Startgewichte und eine andere Kurve. Ein Lauf kann Glück sein. Vergleiche Änderungen mit mehreren Seeds und schau auf den Durchschnitt.
- Eine kleine Validierungsmenge. Acht Bestellungen sind ein verrauschtes Maß: Eine seltsame Bestellung verschiebt die Kurve. Je weniger Daten du hast, desto vorsichtiger liest du einen einzelnen Punkt.
- Early Stopping repariert die Daten nicht. Die drei zufälligen Verspätungen sind immer noch da. Besser ist, sie zu finden und zu entfernen oder zu korrigieren, und wenn das Gewicht wirklich keinen Einfluss hat, es nicht als Merkmal zu geben.
- Die Kosten. Hier dauert das Training weniger als eine Sekunde. Echte Netze trainieren Stunden oder Wochen auf vielen Grafikkarten. Trainings-Cluster und der Betrieb eines Modells in der Produktion sind Infrastruktur und werden im DevOps-Kurs ZeroToOps unterrichtet.
Genauer, für Neugierige
Der Schritt als Formel. Der Batch hat B Beispiele, und η ist die Rate. Für jedes Gewicht w:
w ← w − η · (1/B) · Σ ∂L/∂w
In Worten: Addiere die Steigung des Fehlers nach w für jedes Beispiel im Batch, teile durch B, multipliziere mit der Rate und ziehe ab. ∂L/∂w ist die Steigung, die Backprop für ein Gewicht berechnet. Ist B gleich allen Beispielen, ist das der gewöhnliche Gradientenabstieg. Mit B = 1 heißt er stochastisch (SGD), und mit Batches Mini-Batch-SGD.
Warum 2 · (y − t). Der Fehler eines Beispiels ist (y − t)². Seine Steigung nach y ist 2 · (y − t): Liegt die Ausgabe 0,5 über der Wahrheit, ist die Steigung 1, und liegt sie 0,5 darunter, ist sie −1. Dort beginnt Backprop in step.
Die echten Werkzeuge. Die Praxis des maschinellen Lernens läuft meist in Python. In PyTorch ist dieselbe Schleife: DataLoader(..., batch_size=8, shuffle=True) liefert die Batches, optimizer.zero_grad() setzt die Steigungen zurück (unsere neuen Arrays am Anfang des Schritts), loss.backward() ist Backprop, und optimizer.step() bewegt die Gewichte. Der Seed ist torch.manual_seed(42). In C# erledigen dasselbe ML.NET oder TorchSharp, in Java DJL oder Tribuo, in JavaScript TensorFlow.js, während ONNX Runtime ein schon trainiertes Modell ausführt. Die Schleife ist dieselbe, nur die Matrizen werden auf einer Grafikkarte berechnet.
Klügere Schritte. Fast niemand benutzt den bloßen Schritt aus der Lektion. Momentum merkt sich die Richtung der letzten Schritte und geht ruhiger durch das Rauschen der Batches. Adam führt für jedes Gewicht eine eigene Rate. Die Rate wird oft im Lauf der Zeit gesenkt (ein Learning-Rate-Schedule): am Anfang große Schritte, am Ende kleine.
Die Startgewichte. Hier sind die zufälligen Gewichte von −1 bis 1. In größeren Netzen wird der Bereich aus der Zahl der Eingaben des Neurons berechnet, zum Beispiel ±1/√n (Xavier, He), damit die Summen schon in der ersten Schicht nicht zu groß sind.
Dieselben Zahlen in vier Sprachen. Die Additionen und Multiplikationen gehen hier in derselben Reihenfolge, also ist das Ergebnis bitgleich. Dass exp in jeder Sprache bis zum letzten Bit identisch ist, wird nicht versprochen, deshalb geben die Programme 4 Nachkommastellen aus. Die Ausgabe ist geprüft und in Java, C#, Python und JavaScript gleich.
System.out.printf schreibt die Zahl nach dem Standard-Locale: Auf einem Computer mit deutschen Spracheinstellungen siehst du 0,1365. Mit Locale.ROOT als erstem Argument steht immer ein Punkt.Übung
Early Stopping bei drei Raten
Löse die Aufgabe in deinem eigenen Editor.
Nimm das Early Stopping aus der Lektion und lass es für die Raten 0,02, 0,05 und 0,2 laufen, jedes Mal mit einem neuen Netz aus Seed 42. Gib für jede Rate die niedrigste Validierung, ihre Epoche und die Epoche aus, bei der das Training anhält. Eine Kopie des Netzes musst du nicht aufbewahren.
Rate 0.02: niedrigste Validierung 0.0683 bei Epoche 470, gestoppt bei Epoche 570Rate 0.05: niedrigste Validierung 0.0667 bei Epoche 230, gestoppt bei Epoche 330Rate 0.20: niedrigste Validierung 0.0628 bei Epoche 50, gestoppt bei Epoche 150Mit 0,2 erreicht das Netz seine niedrigste Validierung in 50 Epochen und hält bei 150 an. Mit 0,02 braucht es 470. Die größere Rate erreicht den tiefsten Punkt eher, aber sie dreht auch eher nach oben. Deshalb wird die Validierung oft geprüft, nicht nur am Ende.
Weitere Aufgaben
- Sechs Neuronen. Lass die Hauptschleife mit 6 verborgenen Neuronen laufen. Steigt die Validierung früher?
- Ohne das Gewicht. Nimm das Gewicht aus den Merkmalen (2 Eingaben statt 3) und lass die Hauptschleife laufen. Was passiert mit der Validierung bei Epoche 1.000?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Überanpassung: Das Modell, das auswendig lernt · Das Neuron · Schichten · Backpropagation
- Geht weiter in: Die Ziffern auf dem Kassenbon
- Aufgaben zum Thema: Ein neuronales Netz von innen