Zum Inhalt
wannacode

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:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
Console.WriteLine($"Fehler (MSE): {net.Loss(trainX, trainY):F4}");
foreach (int k in new[] { 0, 12, 23 })
Console.WriteLine($"{train[k][0]} km: das Netz sagt {net.Predict(trainX[k]):F2}, " +
$"tatsächlich {trainY[k]:F1} Tage");
Ausgabe
Fehler (MSE): 7.1855
5 km: das Netz sagt -0.23, tatsächlich 1.0 Tage
150 km: das Netz sagt -0.17, tatsächlich 2.3 Tage
520 km: das Netz sagt 0.18, tatsächlich 3.0 Tage

Eine 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:

  1. Startgewichte: kleine Zufallszahlen aus einem Generator mit Seed.
  2. Epoche: ein Durchgang durch alle Trainingsbeispiele. Vor jeder Epoche werden die Beispiele gemischt, damit das Netz ihre Reihenfolge nicht lernt.
  3. 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.
  4. 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 Lieferung
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 sieht
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 kg
static double[][] Inputs(double[][] orders) =>
orders.Select(o => new[] { o[0] / 100, o[1], o[2] / 10 }).ToArray();
static double[] Targets(double[][] orders) =>
orders.Select(o => o[3]).ToArray();

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(long seed)
{
long state = seed;
// die nächste Zahl zwischen 0 und 1; ein Seed liefert immer dieselben Zahlen
public double Next()
{
state = state * 48271 % 2147483647;
return (double)state / 2147483647;
}
}
new Random(42) in C# 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
{
public readonly double[][] W1; // Eingabe → verborgene Schicht
public readonly double[] B1;
public readonly double[] W2; // verborgene Schicht → Ausgabe
public double B2;
readonly double[] h; // die Ausgaben der verborgenen Schicht vom letzten predict
public Net(int hidden, int inputs)
{
W1 = new double[hidden][];
for (int j = 0; j < hidden; j++)
W1[j] = new double[inputs];
B1 = new double[hidden];
W2 = new double[hidden];
h = new double[hidden];
}
// zufällige Gewichte von -scale bis scale; die Biases bleiben 0
public void Randomize(Rng rng, double scale)
{
foreach (var row in 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) => 1 / (1 + Math.Exp(-z));
public 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;
}
public 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
public void Step(double[][] xs, double[] ys, int[] batch, double rate)
{
int hidden = W2.Length, inputs = xs[0].Length;
var gw1 = new double[hidden, inputs];
double[] gb1 = new double[hidden], gw2 = new double[hidden];
double gb2 = 0;
foreach (int k in 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 wahrscheinlich
static void Shuffle(int[] order, Rng rng)
{
for (int i = order.Length - 1; i > 0; i--)
{
int k = (int)(rng.Next() * (i + 1));
(order[i], order[k]) = (order[k], order[i]);
}
}

Sieh es dir an

Die Schleife und die Fehlerkurve

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.WriteLine("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 = 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);
Console.WriteLine($"{epoch,6}{t,10:F4}{v,13:F4}");
}
}
Schritt für Schritt, in Worten
  1. Seed 42: Jeder Durchlauf beginnt mit denselben zufälligen Gewichten. Vor dem Training liegt der Fehler bei 7,19, weit über dem Diagramm.
  2. Eine Epoche ist ein Durchgang durch alle 24 Trainingsbestellungen. Zuerst werden sie gemischt, mit demselben Generator.
  3. Ein Batch aus 8 Bestellungen: vorwärts, Fehler, rückwärts, ein Schritt. Drei Schritte pro Epoche, 3000 im ganzen Training.
  4. Epoche 100: Training 0,1365, Validierung 0,0756. Mit den Validierungsbestellungen wurde nie ein Schritt gemacht.
  5. Die Validierung ist bei Epoche 200 am niedrigsten: 0,0705. Danach steigt sie wieder, während das Training weiter fällt.
  6. Epoche 1000: Training 0,0381, Validierung 0,2371. Das Netz lernt drei schwere Pakete auswendig, die zufällig zu spät kamen.
  7. 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:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.WriteLine("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 = 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);
Console.WriteLine($"{epoch,6}{t,10:F4}{v,13:F4}");
}
}
Ausgabe
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.2371

Das 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++;
Console.WriteLine($"Schritte: {steps}");
Antwort

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:

foreach (int size in new[] { 24, 8, 1 })
{
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.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, order[start..end], 0.05);
double after = net.Loss(trainX, trainY);
steps++;
if (after > before) up++;
before = after;
}
}
Console.WriteLine($"Batch {size,2}: {steps,3} Schritte, {up,3} aufwärts, Fehler {before:F4}");
}
Ausgabe
Batch 24: 40 Schritte, 0 aufwärts, Fehler 0.2913
Batch 8: 120 Schritte, 21 aufwärts, Fehler 0.1840
Batch 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;

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:

foreach (double rate in new[] { 0.005, 0.05, 2.0 })
{
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
Console.Write($"Rate {rate:F3}:");
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)
{
Console.Write($" explodiert bei Epoche {epoch}");
break;
}
if (epoch is 50 or 100 or 200)
Console.Write($" {loss:F4}");
}
Console.WriteLine();
}
Ausgabe
Rate 0.005: 0.3892 0.3201 0.2472
Rate 0.050: 0.1698 0.1365 0.1041
Rate 2.000: explodiert bei Epoche 2

Mit 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:

var rng = new Rng(42);
var zeros = new Net(4, 3); // ohne Randomize: Jedes Gewicht ist 0
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 200; epoch++)
TrainEpoch(zeros, trainX, trainY, order, 8, 0.05, rng);
Console.WriteLine($"Nullen: Fehler {zeros.Loss(trainX, trainY):F4}");
for (int j = 0; j < zeros.W1.Length; j++)
Console.WriteLine($" Neuron {j + 1}: " + string.Join(" ", zeros.W1[j].Select(x => x.ToString("F2"))));
foreach (double scale in new double[] { 8, 1 })
{
rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, scale);
double first = net.Loss(trainX, trainY);
order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 200; epoch++)
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
Console.WriteLine($"Gewichte ×{scale}: am Anfang {first:F4}, nach 200 Epochen {net.Loss(trainX, trainY):F4}");
}
Ausgabe
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.19
Gewichte ×8: am Anfang 33.9817, nach 200 Epochen 0.2971
Gewichte ×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 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:

var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
for (int epoch = 1; epoch <= 1000; epoch++)
{
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch is 200 or 1000)
{
Console.WriteLine($"Epoche {epoch}:");
foreach (int k in new[] { 1, 3, 6 }) // die schweren Pakete aus der Validierung
Console.WriteLine($" {valid[k][0]} km, {valid[k][2]:F1} kg: " +
$"{net.Predict(validX[k]):F2} (tatsächlich {validY[k]:F1})");
}
}
Ausgabe
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 Moment
public Net Copy()
{
var c = new Net(W2.Length, W1[0].Length);
for (int j = 0; j < W2.Length; j++)
{
W1[j].CopyTo(c.W1[j], 0);
c.B1[j] = B1[j];
c.W2[j] = W2[j];
}
c.B2 = B2;
return c;
}
var rng = new Rng(42);
var net = new Net(4, 3);
net.Randomize(rng, 1.0);
int[] order = Enumerable.Range(0, trainX.Length).ToArray();
// alle 10 Epochen prüfen; nach 100 ohne Verbesserung stoppen
double best = double.MaxValue;
int bestEpoch = 0, epoch = 0;
var bestNet = net.Copy();
while (epoch < 1000 && epoch - bestEpoch < 100)
{
epoch++;
TrainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch % 10 == 0)
{
double loss = net.Loss(validX, validY);
if (loss < best)
{
best = loss;
bestEpoch = epoch;
bestNet = net.Copy();
}
}
}
Console.WriteLine($"niedrigste Validierung: {best:F4} bei Epoche {bestEpoch}");
Console.WriteLine($"gestoppt bei Epoche {epoch}");
Console.WriteLine($"ich behalte das Netz von Epoche {bestEpoch}: {bestNet.Loss(validX, validY):F4}");
Ausgabe
niedrigste Validierung: 0.0667 bei Epoche 230
gestoppt bei Epoche 330
ich behalte das Netz von Epoche 230: 0.0667

Die 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.

:F4 schreibt die Zahl nach der Kultur des Systems. Auf einem Computer mit deutscher Kultur siehst du 0,1365, während die Prüfung der Lektion mit der invarianten Kultur und einem Punkt läuft.

Ü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.

Ausgabe
Rate 0.02: niedrigste Validierung 0.0683 bei Epoche 470, gestoppt bei Epoche 570
Rate 0.05: niedrigste Validierung 0.0667 bei Epoche 230, gestoppt bei Epoche 330
Rate 0.20: niedrigste Validierung 0.0628 bei Epoche 50, gestoppt bei Epoche 150

Mit 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

1. Was ist eine Epoche?
2. Der Trainingsfehler fällt die ganze Zeit, während die Validierung bei Epoche 200 am niedrigsten ist und dann wächst. Welches Netz behältst du?
3. Nach der zweiten Epoche ist der Fehler über einer Million, und dann wird er NaN. Was ist am wahrscheinlichsten?
4. Alle Gewichte sind am Anfang 0. Was passiert mit den vier verborgenen Neuronen?
5. Warum trainiert man meist mit Batches, nicht mit allen Beispielen auf einmal oder mit einem nach dem anderen?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·