Zum Inhalt
wannacode

Schichten

Kurz gesagt: Schichten verbinden Neuronen hintereinander: Eine verborgene Schicht lässt das Netz Regeln lernen, die eine einzelne Gerade nicht zeichnen kann, zum Beispiel XOR.

Problem

Die Regel, die eine Gerade nicht zeichnen kann

Das Betrugsteam will eine neue Regel: Eine Zahlung ist verdächtig, wenn das Land der Karte und das Land der Verbindung (nach IP-Adresse) nicht übereinstimmen. Eine bulgarische Karte von einer ausländischen IP: Jemand benutzt im Ausland eine gestohlene Karte. Eine ausländische Karte von einer bulgarischen IP: Eine gestohlene Karte wird hier benutzt. Eine bulgarische Karte aus Bulgarien und eine ausländische Karte aus dem Ausland (ein Kunde, der dort lebt) sind in Ordnung.

Es gibt zwei Merkmale, je 0 oder 1, wobei 1 Ausland bedeutet. Es gibt vier Fälle, und (1, 0) und (0, 1) sind verdächtig. Das Programm probiert 15.000 Neuronen aus: jedes Gewicht von −6 bis 6 in Schritten von 0,5 und einen Bias von −5,75 bis 5,75. Für jedes zählt es, wie viele Fälle bei einem Schwellenwert von 0,5 richtig sind:

double[][] cases = {{0, 0}, {1, 0}, {0, 1}, {1, 1}};
int[] labels = {0, 1, 1, 0}; // 1 = verdächtig
int tried = 0, best = 0;
double[] bestW = {0, 0};
double bestB = 0;
for (int i = -12; i <= 12; i++) {
for (int j = -12; j <= 12; j++) {
for (int k = -12; k <= 11; k++) {
double[] w = {i / 2.0, j / 2.0};
double b = k / 2.0 + 0.25;
tried++;
int right = 0;
for (int n = 0; n < 4; n++) {
int guess = neuron(cases[n], w, b) >= 0.5 ? 1 : 0;
if (guess == labels[n]) right++;
}
if (right > best) {
best = right;
bestW = w;
bestB = b;
}
}
}
}
System.out.printf("probierte Neuronen: %d, bestes Neuron: %d von 4%n", tried, best);
System.out.printf("w = [%.2f, %.2f], b = %.2f%n", bestW[0], bestW[1], bestB);
for (int n = 0; n < 4; n++) {
double p = neuron(cases[n], bestW, bestB);
boolean right = (p >= 0.5 ? 1 : 0) == labels[n];
System.out.printf("%.0f %.0f: p = %.3f %s%n", cases[n][0], cases[n][1], p,
right ? "richtig" : "falsch");
}
Ausgabe
probierte Neuronen: 15000, bestes Neuron: 3 von 4
w = [-6.00, 0.50], b = -0.25
0 0: p = 0.438 richtig
1 0: p = 0.002 falsch
0 1: p = 0.562 richtig
1 1: p = 0.003 richtig

Das beste trifft 3 von 4, und es nennt eine ausländische Karte von einer bulgarischen IP „ok“. Keines der 15.000 trifft alle vier. Und das liegt nicht daran, dass das Raster der Gewichte zu grob ist: Die nächste Karte zeigt, warum.

Die Idee

Warum eine Gerade nicht reicht

Zeichne die Fälle als Punkte in einem Quadrat: Die Karte ist die waagerechte Achse, die Verbindung die senkrechte. Die verdächtigen (1, 0) und (0, 1) liegen in zwei gegenüberliegenden Ecken, die anderen beiden sind in Ordnung.

Das Neuron sagt „ja“, wenn z über null liegt. Die Grenze z = 0, also w₁·Karte + w₂·IP + b = 0, ist eine Gerade. Auf der einen Seite sagt das Neuron „ja“, auf der anderen „nein“. Und eine Gerade, die die beiden verdächtigen Ecken auf einer Seite lässt, lässt mindestens eine der anderen beiden bei ihnen. Probier es mit einem Bleistift: Es geht nicht.

Die logistische Regression aus der Lektion über Klassifikation hat dieselbe Grenze, also kann sie es auch nicht. Es braucht zwei Geraden.

Die Idee

Zwei Neuronen in der Mitte

Zwischen Eingabe und Ausgabe setzen wir zwei Neuronen. Jedes zieht seine eigene Gerade:

  • h1: „mindestens eins kommt aus dem Ausland“. Gewichte 6 und 6, Bias −3: z liegt über null, sobald mindestens eine Eingabe 1 ist.
  • h2: „beide kommen aus dem Ausland“. Gewichte 6 und 6, Bias −9: z liegt nur über null, wenn beide Eingaben 1 sind.

Die Ausgabe liest h1 und h2 mit den Gewichten 10 und −10 und einem Bias von −5: „mindestens eins, aber nicht beide“. Der verdächtige Bereich ist der Streifen zwischen den beiden Geraden.

Die Neuronen in der Mitte sind eine verborgene Schicht. Verborgen, weil ihnen niemand eine richtige Antwort vorgibt: Die Eingabe kommt aus den Daten, die Ausgabe hat ein Label, und was die dazwischen berechnen, entscheidet das Netz selbst. Hier sind die Gewichte von Hand gewählt, damit sie etwas bedeuten, das du in Worte fassen kannst. Gelernte lassen sich meist nicht so leicht in Worte fassen.

Von links nach rechts zu rechnen, Schicht für Schicht, ist der Vorwärtsdurchlauf:

static final double[][] W = {{6, 6}, {6, 6}};
static final double[] B = {-3, -9};
static final double[] V = {10, -10};
static final double C = -5;
static double forward(double[] x) {
// mindestens eins kommt aus dem Ausland
double h1 = neuron(x, W[0], B[0]);
// beide kommen aus dem Ausland
double h2 = neuron(x, W[1], B[1]);
double[] h = {h1, h2}; // die verborgene Schicht
return neuron(h, V, C);
}

Sieh es dir an

Der Vorwärtsdurchlauf, Zahl für Zahl

Ein Fall: eine ausländische Karte von einer bulgarischen IP, (1, 0). Jeder Schritt gibt sein z und seine Ausgabe aus:

double[] x = {1, 0}; // eine ausländische Karte, eine IP aus Bulgarien
double z1 = weightedSum(x, W[0], B[0]);
double h1 = sigmoid(z1);
System.out.printf("h1: z = %.2f, h1 = %.4f%n", z1, h1);
double z2 = weightedSum(x, W[1], B[1]);
double h2 = sigmoid(z2);
System.out.printf("h2: z = %.2f, h2 = %.4f%n", z2, h2);
double[] h = {h1, h2};
double z = weightedSum(h, V, C);
double p = sigmoid(z);
System.out.printf("Ausgabe: z = %.2f, p = %.4f%n", z, p);
Ausgabe
h1: z = 3.00, h1 = 0.9526
h2: z = -3.00, h2 = 0.0474
Ausgabe: z = 4.05, p = 0.9829

h1: 6 × 1 + 6 × 0 − 3 = 3, und die Sigmoid-Funktion ergibt 0,9526. h2: 6 × 1 + 6 × 0 − 9 = −3, das sind 0,0474. Die Ausgabe: 10 × 0,9526 − 10 × 0,0474 − 5 = 4,05, und die Sigmoid-Funktion ergibt 0,9829. Verdächtig.

Die vier Fälle mit forward:

double[][] cases = {{0, 0}, {1, 0}, {0, 1}, {1, 1}};
System.out.println("Karte IP p");
for (double[] x : cases) {
double p = forward(x);
String verdict = p >= 0.5 ? "verdächtig" : "ok";
System.out.printf("%5.0f %3.0f %7.4f %s%n", x[0], x[1], p, verdict);
}
Ausgabe
Karte IP p
0 0 0.0107 ok
1 0 0.9829 verdächtig
0 1 0.9829 verdächtig
1 1 0.0107 ok

Vier von vier. Dieselben Gewichte, dieselbe Sigmoid-Funktion, nur eine Schicht mehr.

Sieh es dir an

Zwei Geraden und das Signal durch das Netz

static final double[][] W = {{6, 6}, {6, 6}};
static final double[] B = {-3, -9};
static final double[] V = {10, -10};
static final double C = -5;
static double forward(double[] x) {
// mindestens eins kommt aus dem Ausland
double h1 = neuron(x, W[0], B[0]);
// beide kommen aus dem Ausland
double h2 = neuron(x, W[1], B[1]);
double[] h = {h1, h2}; // die verborgene Schicht
return neuron(h, V, C);
}
Schritt für Schritt, in Worten
  1. Vier Fälle: Karte und IP, 1 bedeutet Ausland. (1, 0) und (0, 1) sind verdächtig.
  2. Das beste einzelne Neuron zieht eine Gerade und trifft 3 von 4: (1, 0) bleibt auf der falschen Seite.
  3. Die verborgene Schicht: zwei Geraden, h1 „mindestens eins“ und h2 „beide“. Das Verdächtige liegt dazwischen.
  4. Der Vorwärtsdurchlauf für eine ausländische Karte von einer bulgarischen IP: x = (1, 0).
  5. h1: 6 × 1 + 6 × 0 − 3 = 3,00, σ ergibt 0,9526. h2: 6 × 1 + 6 × 0 − 9 = −3,00, σ ergibt 0,0474.
  6. Die Ausgabe liest h: 10 × 0,9526 − 10 × 0,0474 − 5 = 4,05, σ ergibt 0,9829.
  7. 0,9829 liegt über 0,5: verdächtig.
  8. Die vier Fälle durch dasselbe Netz: 0,0107; 0,9829; 0,9829; 0,0107. Vier von vier.

Oben stehen die vier Fälle. Eine Gerade lässt immer einen falsch. Die zwei Geraden der verborgenen Schicht schließen einen Streifen, und die Ausgabe sagt nur darin „verdächtig“.

Probier es aus

Sag die Ausgabe voraus

Dieselbe Regel mit ReLU statt der Sigmoid-Funktion: h1 = ReLU(Karte + IP), h2 = ReLU(Karte + IP − 1), Ausgabe h1 − 2·h2. Was wird für die vier Fälle ausgegeben?

double[][] cases = {{0, 0}, {1, 0}, {0, 1}, {1, 1}};
double[] out = new double[4];
for (int n = 0; n < 4; n++) {
double[] x = cases[n];
double h1 = relu(x[0] + x[1]);
double h2 = relu(x[0] + x[1] - 1);
out[n] = h1 - 2 * h2;
}
System.out.printf("%.0f %.0f %.0f %.0f%n", out[0], out[1], out[2], out[3]);
Antwort

Probier es aus

Finde den Fehler

Ein Kollege schreibt forward selbst. Das Netz antwortet wie ein einzelnes Neuron: drei von vier. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Ergänze

Was bekommt das Ausgabeneuron?

double[] h = {h1, h2};
return neuron(, V, C);

Die Idee

Wann eine verborgene Schicht hilft und wann sie schadet

  • Sie hilft, wenn die Merkmale sich gegenseitig beeinflussen. „Ausländische Karte“ allein bedeutet nichts, und „ausländische IP“ auch nicht. Die Kombination schon. Eine verborgene Schicht lernt solche Kombinationen, ohne dass man es ihr sagt.
  • Wenn du die Kombination kennst, schreib sie hin. Das Merkmal „Karte ≠ Verbindung“ ist eine Zeile Code. Damit reicht ein Neuron oder ein if. Ein Netz, das etwas lernt, das du hinschreiben könntest, ist langsamer, braucht Daten und macht mehr Fehler.
  • Mehr Neuronen sind mehr Gewichte. Hier sind es 9 Zahlen. Eine verborgene Schicht mit 100 Neuronen über 20 Merkmalen sind 2.201 Zahlen. Mehr Gewichte bedeuten mehr Daten, oder das Netz lernt auswendig.
  • Jede Schicht kostet bei jeder Zahlung. Der Vorwärtsdurchlauf geht bei jeder Anfrage durch alle Gewichte. Bei einem großen Netz und vielen Zahlungen sind das Zeit und Geld.
  • Verborgene Neuronen müssen nichts bedeuten. Hier tun sie es, weil die Gewichte von Hand gewählt sind. Gelernte sind meist eine Mischung, die sich nicht in Worte fassen lässt.
Genauer, für Neugierige

Mit Formeln. Die verborgene Schicht ist h = σ(W·x + b): W ist eine Tabelle (eine Matrix) aus 2 × 2 Gewichten, b enthält die zwei Biases, und σ wird auf jedes Element angewendet. Die Ausgabe ist p = σ(v·h + c). Der Vorwärtsdurchlauf sind diese zwei Zeilen hintereinander. Deshalb ist eine Schicht in echten Bibliotheken eine Matrix mal ein Vektor.

Deep Learning. Ein Netz mit vielen verborgenen Schichten heißt tief. Jede Schicht baut auf der vorherigen auf: Die ersten erfassen einfache Kombinationen, die nächsten kombinieren diese.

Wie viele Neuronen reichen. Es ist bewiesen, dass eine verborgene Schicht mit genug Neuronen jede stetige Funktion (auf einem beschränkten Bereich) beliebig genau annähern kann. Aber „genug“ kann sehr viel sein, und niemand sagt, wie man die Gewichte findet. Deshalb gibt es in der Praxis mehrere Schichten.

Ein bisschen Geschichte. 1969 zeigten Minsky und Papert, dass eine einzelne Schicht (ein Perzeptron) genau diese Regel (XOR, „das eine oder das andere“) nicht lernen kann. Das Interesse an neuronalen Netzen sank für Jahre. Die Lösung mit einer verborgenen Schicht war bekannt, nicht aber, wie man ihre Gewichte lernt. Das kam mit der Backpropagation.

Übung

Eine Schicht für alle

Löse es in deinem Editor.

Das forward der Lektion weiß, dass es zwei verborgene Neuronen gibt. Schreibe ein allgemeines layer(x, w, b): Es bekommt die Eingaben, eine Tabelle von Gewichten (eine Zeile für jedes Neuron) und die Biases und gibt ein Array mit der Ausgabe jedes Neurons zurück. Baue dann dasselbe Netz als zwei Schichten: die verborgene mit den Gewichten aus der Lektion und die Ausgabeschicht mit einem einzigen Neuron (Gewichte 10 und −10, Bias −5). Lass die vier Fälle durch layer(layer(x, w1, b1), w2, b2) laufen.

Ausgabe
Karte IP p
0 0 0.0107 ok
1 0 0.9829 verdächtig
0 1 0.9829 verdächtig
1 1 0.0107 ok

Dieselbe Tabelle wie in der Lektion: layer ist der allgemeine Teil, und das Netz sind zwei Zeilen mit Gewichten.

Weitere Aufgaben

  • Ein drittes verborgenes Neuron. Füge der verborgenen Schicht ein Neuron mit den Gewichten 0 und 0 und einem Bias von 0 hinzu (und ein Gewicht von 0 dafür an der Ausgabe). Die Ausgabe darf sich nicht ändern.
  • ReLU in der verborgenen Schicht. Gib layer die Aktivierung als Parameter und baue das Netz aus „Sag die Ausgabe voraus“.

Test

Teste dich

1. Warum kann ein einzelnes Neuron nicht lernen, dass „Karte und Verbindung aus verschiedenen Ländern kommen“?
2. Was ist eine verborgene Schicht?
3. Was ist der Vorwärtsdurchlauf?
4. Die verborgene Schicht hat zwei Neuronen mit einer Sigmoid-Funktion, und die Ausgabe ist eine Sigmoid-Funktion. Wie viele Zahlen lernt das Netz?
5. Du weißt, dass verdächtig ist, wenn die beiden Länder verschieden sind. Was ist am sinnvollsten?

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