Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| Beide Seiten der vorigen Revision Vorhergehende Überarbeitung Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
| ef:ki:nn [2026/03/27 10:03] – andristn | ef:ki:nn [2026/04/23 14:17] (aktuell) – andristn | ||
|---|---|---|---|
| Zeile 1: | Zeile 1: | ||
| - | Nikolai, Adam | + | ====== Neuronale Netzwerke ====== |
| + | |||
| + | //Nikolai, Adam// | ||
| {{ : | {{ : | ||
| - | ** | + | |
| - | Einleitung | + | ===== Einleitung |
| Viele kennen neuronale Netzwerke als Diagramme mit Punkten, die in Spalten angeordnet sind und durch Linien miteinander verbunden werden. Auf den ersten Blick wirkt das ziemlich technisch, aber die Grundidee dahinter ist eigentlich leicht zu verstehen. Ein neuronales Netzwerk versucht, Muster in Daten zu erkennen, so wie wir Menschen Dinge wiedererkennen, | Viele kennen neuronale Netzwerke als Diagramme mit Punkten, die in Spalten angeordnet sind und durch Linien miteinander verbunden werden. Auf den ersten Blick wirkt das ziemlich technisch, aber die Grundidee dahinter ist eigentlich leicht zu verstehen. Ein neuronales Netzwerk versucht, Muster in Daten zu erkennen, so wie wir Menschen Dinge wiedererkennen, | ||
| - | In unserem Beispiel besteht das Netzwerk aus drei Schichten. Die erste Schicht hat 784 Eingabewerte. Das können zum Beispiel | + | In unserem Beispiel besteht das Netzwerk aus drei Schichten. Die erste Schicht hat 784 Eingabewerte. Das sind die einzelnen Pixel eines 28×28 Pixel grossen |
| So zeigt das Diagramm sehr anschaulich, | So zeigt das Diagramm sehr anschaulich, | ||
| - | **Weiter gehts** | + | ===== Vom Pixel zum Input ===== |
| - | Wenn du dir die Grafik anschaust, siehst du auf der linken Seite die lila Kreise. Jeder dieser Kreise ist ein einzelner | + | Wenn du dir die Grafik anschaust, siehst du auf der linken Seite die lila Kreise. Jeder dieser Kreise ist ein einzelnes |
| + | <code python> | ||
| inputVector = 1.0 - arr.flatten() / 255.0 | inputVector = 1.0 - arr.flatten() / 255.0 | ||
| + | </ | ||
| - | Weiss wird zu 0.0, schwarz zu 1.0. Und weil das Bild 28x28 Pixel gross ist, entstehen genau 784 Werte, deshalb steht in der Grafik auch x₁, x₂ bis x₇₈₄. | + | Weiss wird zu 0.0, schwarz zu 1.0. Und weil das Bild 28×28 |
| - | Schau jetzt auf die Linien zwischen den lila und den grünen Kreisen. Jede einzelne Linie ist ein Gewicht. Es verbindet jeden Input-Neuron mit jedem Hidden-Neuron und jede Verbindung hat einen eigenen Wert, der bestimmt wie stark dieser Input Einfluss hat. Im Code stecken alle diese Gewichte zusammen in einer Matrix: | + | |
| + | ===== Gewichte und gewichtete Summe ===== | ||
| + | |||
| + | Schau jetzt auf die Linien zwischen den lila und den grünen Kreisen. Jede einzelne Linie ist ein **Gewicht**. Sie verbindet jeden Input-Neuron mit jedem Hidden-Neuron, | ||
| + | |||
| + | <code python> | ||
| self.W1 = np.random.randn(inputSize, | self.W1 = np.random.randn(inputSize, | ||
| + | </ | ||
| - | 784 Inputs mal 128 Hidden | + | 784 Inputs mal 128 Hidden-Neuronen ergibt |
| - | Die grünen Kreise in der Mitte, h₁ bis h₁₂₈, sind die Hidden Layer. Jeder dieser Neuronen nimmt alle 784 Eingabewerte, | + | |
| - | z=(x1⋅w1)+(x2⋅w2)+…+(x784⋅w784)+b | + | Die grünen Kreise in der Mitte, h₁ bis h₁₂₈, sind die **Hidden Layer**. Jedes dieser Neuronen nimmt alle 784 Eingabewerte, |
| - | Und im Code passiert das für alle 128 Neuronen auf einmal: | + | < |
| + | z = (x₁ · w₁) + (x₂ · w₂) + … + (x₇₈₄ · w₇₈₄) + b | ||
| + | </ | ||
| + | Oder kompakter als Summe: | ||
| + | |||
| + | < | ||
| + | 784 | ||
| + | z = | ||
| + | i=1 | ||
| + | </ | ||
| + | |||
| + | Und im Code passiert das für alle 128 Neuronen auf einmal, dank der Matrix-Multiplikation: | ||
| + | |||
| + | <code python> | ||
| self.z1 = X @ self.W1 + self.b1 | self.z1 = X @ self.W1 + self.b1 | ||
| + | </ | ||
| + | |||
| + | Dieses z ist noch nicht das finale Ergebnis des Neurons. Es ist die rohe gewichtete Summe, auch // | ||
| + | |||
| + | ===== Aktivierungsfunktion: | ||
| - | Dieses z ist noch nicht das finale Ergebnis des Neurons. Es ist die rohe gewichtete Summe, auch pre-activation genannt. Ob der Neuron jetzt wirklich etwas weitergibt, entscheidet ReLU. Ist z grösser als 0, kommt der Wert durch. Ist er kleiner, wird er auf 0 gesetzt: | + | Ist z grösser als 0, kommt der Wert durch. Ist er kleiner |
| + | <code python> | ||
| self.a1 = self.relu(self.z1) | self.a1 = self.relu(self.z1) | ||
| + | </ | ||
| - | Was danach rauskommt fliesst zu den orangen Kreisen rechts in der Grafik, der Output Layer. Dort passiert genau dasselbe nochmal, mit Gewichten W₂ und Bias b₂: | + | Was danach rauskommt, fliesst zu den orangen Kreisen rechts in der Grafik, der **Output Layer**. Dort passiert genau dasselbe nochmal, mit Gewichten W₂ und Bias b₂: |
| + | <code python> | ||
| self.z2 = self.a1 @ self.W2 + self.b2 | self.z2 = self.a1 @ self.W2 + self.b2 | ||
| + | </ | ||
| - | Nur der letzte Schritt ist anders. Statt ReLU kommt jetzt Softmax, | + | Nur der letzte Schritt ist anders. Statt ReLU kommt jetzt //Softmax//, die die Rohwerte in Wahrscheinlichkeiten umwandelt. Am Ende gibt jeder der 52 orangen Neuronen eine Wahrscheinlichkeit aus, und der Buchstabe mit dem höchsten Wert ist die Vorhersage des Netzes. |
| + | |||
| + | ===== ReLU und Softmax im Detail ===== | ||
| Wenn du dir nochmal die Grafik anschaust, steht unter den grünen Kreisen "ReLU Aktivierung" | Wenn du dir nochmal die Grafik anschaust, steht unter den grünen Kreisen "ReLU Aktivierung" | ||
| - | Fangen wir mit ReLU an. ReLU steht für Rectified Linear Unit, auf Deutsch in etwa " | ||
| + | ==== ReLU ==== | ||
| + | |||
| + | ReLU steht für //Rectified Linear Unit//, auf Deutsch in etwa „gleichgerichtete lineare Einheit" | ||
| + | |||
| + | <code python> | ||
| def relu(z): | def relu(z): | ||
| return np.maximum(0, | return np.maximum(0, | ||
| - | | + | </ |
| Mathematisch sieht das so aus: | Mathematisch sieht das so aus: | ||
| - | f(z)={zwenn z> | + | < |
| + | ⎧ z wenn z > 0 | ||
| + | f(z) = max(0, z) = ⎨ | ||
| + | ⎩ 0 sonst | ||
| + | </code> | ||
| - | Warum macht man das überhaupt? Ohne eine Aktivierungsfunktion wäre das ganze Netz nur eine einzige grosse lineare Gleichung, egal wie viele Schichten man draufpackt. ReLU bringt die Nichtlinearität rein, die das Netz braucht um wirklich komplexe Muster zu lernen. Ein Neuron | + | Warum macht man das überhaupt? Ohne eine Aktivierungsfunktion wäre das ganze Netz nur eine einzige grosse lineare Gleichung, egal wie viele Schichten man draufpackt. ReLU bringt die **Nichtlinearität** rein, die das Netz braucht, um wirklich komplexe Muster zu lernen. Ein Neuron, das 0 ausgibt, ist quasi stumm. Es gibt nichts weiter. Ein Neuron, das einen positiven Wert ausgibt, feuert und beeinflusst die nächste Schicht. |
| - | Bei der Output Layer ist ReLU aber nicht mehr geeignet. Dort brauchen wir keine stummen Neuronen, wir brauchen Wahrscheinlichkeiten. Genau das macht Softmax. Er nimmt alle 52 Rohwerte der Output-Neuronen auf einmal und rechnet sie so um, dass sie zusammen 1.0 ergeben: | + | ==== Softmax |
| + | Bei der Output Layer ist ReLU aber nicht mehr geeignet. Dort brauchen wir keine stummen Neuronen, wir brauchen Wahrscheinlichkeiten. Genau das macht Softmax. Sie nimmt alle 52 Rohwerte der Output-Neuronen auf einmal und rechnet sie so um, dass sie zusammen 1.0 ergeben: | ||
| + | |||
| + | <code python> | ||
| def softmax(z): | def softmax(z): | ||
| e = np.exp(z - np.max(z, axis=1, keepdims=True)) | e = np.exp(z - np.max(z, axis=1, keepdims=True)) | ||
| return e / e.sum(axis=1, | return e / e.sum(axis=1, | ||
| - | + | </ | |
| - | Mathematisch passiert folgendes, jeder Rohwert wird mit der Exponentialfunktion hochgerechnet, | + | |
| - | σ(zi)=ezi∑j=152ezj\sigma(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{52} e^{z_j}}σ(zi)=∑j=152ezjezi | + | Mathematisch passiert Folgendes: jeder Rohwert wird mit der Exponentialfunktion hochgerechnet und dann durch die Summe aller hochgerechneten Werte geteilt: |
| - | Das sorgt dafür, dass ein hoher Rohwert eine hohe Wahrscheinlichkeit bekommt und ein tiefer eine kleine. Am Ende schaut das Netz einfach welcher der 52 Neuronen die höchste Wahrscheinlichkeit hat, und das ist dann der vorhergesagte Buchstabe: | + | < |
| + | e^(zᵢ) | ||
| + | σ(zᵢ) | ||
| + | | ||
| + | Σ e^(zⱼ) | ||
| + | j=1 | ||
| + | </ | ||
| + | Das sorgt dafür, dass ein hoher Rohwert eine hohe Wahrscheinlichkeit bekommt und ein tiefer eine kleine. Am Ende schaut das Netz einfach, welcher der 52 Neuronen die höchste Wahrscheinlichkeit hat, und das ist dann der vorhergesagte Buchstabe: | ||
| + | |||
| + | <code python> | ||
| labels[np.argmax(probs)] | labels[np.argmax(probs)] | ||
| + | </ | ||
| ReLU und Softmax haben also ganz verschiedene Rollen. ReLU filtert innerhalb des Netzes und schafft die Nichtlinearität. Softmax macht am Schluss aus rohen Zahlen eine lesbare Aussage, nämlich wie sicher das Netz ist, welchen Buchstaben es gerade sieht. | ReLU und Softmax haben also ganz verschiedene Rollen. ReLU filtert innerhalb des Netzes und schafft die Nichtlinearität. Softmax macht am Schluss aus rohen Zahlen eine lesbare Aussage, nämlich wie sicher das Netz ist, welchen Buchstaben es gerade sieht. | ||
| - | Ein Perzeptron ist eigentlich der Vorläufer von allem was wir bisher besprochen haben. Es ist das einfachste denkbare künstliche Neuron, erfunden in den 1950er Jahren. Die Idee dahinter ist genau dieselbe wie bei unserem Neuron, Inputs | + | ===== Perzeptron und Feedforward-Netz ===== |
| - | Das Problem dabei ist schnell klar. Mit nur 0 und 1 kann man keine Wahrscheinlichkeiten ausdrücken und man kann auch keine komplexen Muster lernen. Ein einzelnes Perzeptron kann zum Beispiel nicht unterscheiden ob ein Pixel oben links oder unten rechts liegt, es sieht alles als eine einzige Entscheidung. | + | |
| - | Genau deshalb wurde das Feedforward-Netz entwickelt. Die Idee ist simpel, man nimmt nicht ein Perzeptron sondern viele, stapelt sie in Schichten übereinander und verbindet sie. Das ist genau das was du in der Grafik siehst. Die Informationen fliessen dabei immer nur in eine Richtung, von links nach rechts, von der Input Layer durch die Hidden Layer bis zur Output Layer. Kein Rückweg, kein Kreis, nur vorwärts. Daher der Name Feedforward. | + | Ein **Perzeptron** ist eigentlich der Vorläufer von allem, was wir bisher besprochen haben. Es ist das einfachste denkbare künstliche Neuron, erfunden in den 1950er Jahren |
| + | |||
| + | Mathematisch sieht ein Perzeptron so aus: | ||
| + | |||
| + | < | ||
| + | ⎧ 1 wenn Σ wᵢ·xᵢ + b > 0 | ||
| + | y = ⎨ | ||
| + | ⎩ 0 sonst | ||
| + | </ | ||
| + | |||
| + | Das Problem dabei ist schnell klar. Mit nur 0 und 1 kann man keine Wahrscheinlichkeiten ausdrücken, und man kann auch keine komplexen Muster lernen. Ein einzelnes Perzeptron kann zum Beispiel nicht unterscheiden, ob ein Pixel oben links oder unten rechts liegt. Es sieht alles als eine einzige Entscheidung. | ||
| + | |||
| + | Genau deshalb wurde das **Feedforward-Netz** entwickelt. Die Idee ist simpel: man nimmt nicht ein Perzeptron, sondern viele, stapelt sie in Schichten übereinander und verbindet sie. Das ist genau das, was du in der Grafik siehst. Die Informationen fliessen dabei immer nur in eine Richtung, von links nach rechts, von der Input Layer durch die Hidden Layer bis zur Output Layer. Kein Rückweg, kein Kreis, nur vorwärts. Daher der Name //Feedforward//. | ||
| + | |||
| + | Im Code ist dieser Vorwärtsdurchlauf die '' | ||
| + | |||
| + | <code python> | ||
| + | def forward(self, | ||
| + | self.z1 = X @ self.W1 + self.b1 | ||
| + | self.a1 = self.relu(self.z1) | ||
| + | self.z2 = self.a1 @ self.W2 + self.b2 | ||
| + | self.a2 = self.softmax(self.z2) | ||
| + | return self.a2 | ||
| + | </ | ||
| + | |||
| + | Man sieht es schön: zuerst Input Layer zu Hidden Layer, dann Hidden Layer zu Output Layer. Schritt für Schritt nach vorne. Jede Schicht nimmt das Ergebnis der vorherigen, verarbeitet es weiter und gibt es an die nächste weiter. | ||
| + | |||
| + | Der entscheidende Unterschied zum einzelnen Perzeptron ist die Hidden Layer in der Mitte. Dort lernt das Netz nicht mehr nur eine einzige Entscheidung, | ||
| + | |||
| + | ===== Ein einfaches Beispiel: das AND-Gatter ===== | ||
| + | |||
| + | Um das Ganze mal ganz konkret zu machen, schauen wir uns ein Perzeptron an, das die logische // | ||
| + | |||
| + | ^ x₁ ^ x₂ ^ y (AND) ^ | ||
| + | | 0 | 0 | 0 | | ||
| + | | 0 | 1 | 0 | | ||
| + | | 1 | 0 | 0 | | ||
| + | | 1 | 1 | 1 | | ||
| + | |||
| + | Ein einzelnes Perzeptron mit zwei Inputs reicht, um das zu lernen. Wir brauchen nur passende Gewichte und einen Bias. Zum Beispiel: | ||
| + | |||
| + | < | ||
| + | w₁ = 1, w₂ = 1, b = -1.5 | ||
| + | </ | ||
| + | |||
| + | Rechnen wir das für alle vier Fälle durch: | ||
| + | |||
| + | * x₁ = 0, x₂ = 0: z = 0·1 + 0·1 − 1.5 = −1.5 | ||
| + | * x₁ = 0, x₂ = 1: z = 0·1 + 1·1 − 1.5 = −0.5 | ||
| + | * x₁ = 1, x₂ = 0: z = 1·1 + 0·1 − 1.5 = −0.5 | ||
| + | * x₁ = 1, x₂ = 1: z = 1·1 + 1·1 − 1.5 = 0.5 | ||
| + | |||
| + | Das Perzeptron gibt also genau dann 1 aus, wenn beide Inputs 1 sind. Die **Entscheidungsgrenze** ist hier die Gerade x₁ + x₂ = 1.5. Alles darüber wird als 1 klassifiziert, | ||
| + | |||
| + | ===== Gewichtete Summe, Schwellenwert und Entscheidungsgrenze ===== | ||
| + | |||
| + | Die **gewichtete Summe** ist der erste Schritt, den jedes Neuron macht. Es nimmt alle seine Inputs, multipliziert jeden mit seinem zugehörigen Gewicht und addiert alles zusammen. Das Gewicht entscheidet dabei, wie wichtig ein Input ist. Ein Pixel, der für die Erkennung eines bestimmten Buchstabens sehr relevant ist, bekommt ein hohes Gewicht, ein unwichtiger Pixel ein niedriges. Im Code landet das alles in dieser einen Zeile: | ||
| + | |||
| + | <code python> | ||
| + | self.z1 = X @ self.W1 + self.b1 | ||
| + | </ | ||
| + | |||
| + | Das Ergebnis z ist einfach eine Zahl, die zusammenfasst, | ||
| + | |||
| + | Jetzt kommt der **Schwellenwert** ins Spiel. Man kann sich das vorstellen wie eine Eingangstür. Nur wenn die gewichtete Summe gross genug ist, also den Schwellenwert überschreitet, | ||
| + | |||
| + | <code python> | ||
| + | self.a1 = self.relu(self.z1) | ||
| + | </ | ||
| + | |||
| + | Der Schwellenwert ist also nicht irgendeine komplizierte Einstellung, | ||
| + | |||
| + | Und genau da kommen wir zur **Entscheidungsgrenze**. Das ist das, was das ganze Netz letztendlich lernt. Durch das Training mit vielen Buchstabenbildern passen sich die Gewichte so an, dass das Netz den Raum aller möglichen Inputs aufteilt. Auf der einen Seite der Grenze liegt zum Beispiel ein " | ||
| + | |||
| + | ===== Backpropagation ===== | ||
| + | |||
| + | Fangen wir ganz klein an, mit einem einzigen Neuron. Stell dir vor, das Netz sagt " | ||
| + | |||
| + | ==== Schritt 1: Fehler messen ==== | ||
| + | |||
| + | Der erste Schritt ist, den Fehler zu messen. Das macht die **Loss Function**. Davon gibt es natürlich mehrere Varianten, die zwei wichtigsten sind //Mean Squared Error// und //Cross Entropy Loss//. | ||
| + | |||
| + | Der **Mean Squared Error** (MSE) ist der Klassiker und wird zum Beispiel auch von 3Blue1Brown in seiner bekannten YouTube-Serie über neuronale Netzwerke verwendet. Er misst den Fehler, indem er für jedes Output-Neuron die Differenz zwischen Vorhersage und richtigem Wert quadriert und dann den Durchschnitt bildet: | ||
| + | |||
| + | < | ||
| + | 1 | ||
| + | MSE = ─── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Wir haben uns aber für **Cross Entropy Loss** entschieden, | ||
| + | |||
| + | <code python> | ||
| + | def crossEntropyLoss(probs, | ||
| + | return -np.mean(np.sum(yOnehot * np.log(np.clip(probs, | ||
| + | </ | ||
| + | |||
| + | Mathematisch gesehen berechnet er, wie weit die vorhergesagte Wahrscheinlichkeit vom richtigen Wert entfernt ist: | ||
| + | |||
| + | < | ||
| + | L = − Σ yᵢ · log(ŷᵢ) | ||
| + | i | ||
| + | </ | ||
| + | |||
| + | Der grosse Vorteil von Cross Entropy gegenüber MSE ist, dass er starke Fehler viel härter bestraft. Sagt das Netz mit 99% Sicherheit " | ||
| + | |||
| + | War die Vorhersage für " | ||
| + | |||
| + | ==== Schritt 2: Gradient und Gradientenabstieg ==== | ||
| + | |||
| + | Hier kommt der **Gradient** ins Spiel. Ein Gradient sagt uns für jedes einzelne Gewicht, in welche Richtung und wie stark es den Loss beeinflusst. Man kann sich das wie einen Hügel vorstellen. Der Loss ist die Höhe, und wir wollen den tiefsten Punkt finden. Der Gradient zeigt uns die steilste Richtung bergauf, also gehen wir genau die entgegengesetzte Richtung bergab. Das nennt sich **Gradientenabstieg**. | ||
| + | |||
| + | Um den Gradienten zu berechnen, müssen wir verstehen, wie ein Gewicht überhaupt den Loss beeinflusst. Das Gewicht w ist ja nicht direkt im Loss drin, sondern es gibt eine ganze Kette von Zwischenschritten: | ||
| + | |||
| + | < | ||
| + | w ──► | ||
| + | | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Das Gewicht beeinflusst zuerst die gewichtete Summe z, diese wird dann durch Softmax zur Vorhersage ŷ, und ŷ fliesst in den Loss L. Um zu wissen, wie stark w den Loss verändert, müssen wir diese Kette komplett durchrechnen. Genau dafür gibt es die **Kettenregel** aus der Differentialrechnung: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Wir leiten also drei Mal einzeln ab und multiplizieren das Ergebnis. Schauen wir uns jeden Teil einzeln an. | ||
| + | |||
| + | **1. Ableitung des Loss nach der Vorhersage** | ||
| + | |||
| + | Cross Entropy Loss ist L = −Σ yᵢ · log(ŷᵢ). Leiten wir ihn nach ŷ ab: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | **2. Ableitung der Softmax nach der gewichteten Summe** | ||
| + | |||
| + | Hier wird es etwas komplizierter. Softmax ist nämlich eine Funktion, bei der jeder Output ŷᵢ von //allen// z-Werten gleichzeitig abhängt (wegen der Summe im Nenner). Die vollständige Ableitung ist deshalb eine sogenannte Jacobi-Matrix und würde den Rahmen hier sprengen. Wir sparen uns den detaillierten Zwischenschritt und halten einfach fest: es gibt eine wohldefinierte Ableitung. | ||
| + | |||
| + | **3. Ableitung der gewichteten Summe nach dem Gewicht** | ||
| + | |||
| + | Die gewichtete Summe ist z = w·x + b. Leiten wir nach w ab, bleibt einfach x übrig: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | **Alles einsetzen** | ||
| + | |||
| + | Wenn man nun alle drei Teile in die Kettenregel einsetzt und sauber ausrechnet (das ist der Punkt, an dem sich die komplizierte Softmax-Ableitung mit der −y/ŷ aus dem Cross Entropy Loss gegenseitig auffrisst), bleibt am Ende dieser wunderschön einfache Ausdruck übrig: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Das ist genau das, was wir vorhin als " | ||
| + | |||
| + | **Gradient für den Bias** | ||
| + | |||
| + | Für den Bias b läuft dieselbe Kettenregel, | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Also ist der Gradient für den Bias einfach der Fehler selbst, ohne Multiplikation mit dem Input: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | **Update-Regel** | ||
| + | |||
| + | Jetzt wo wir beide Gradienten kennen, können wir Gewicht und Bias in die entgegengesetzte Richtung anpassen: | ||
| + | |||
| + | < | ||
| + | w_neu = w_alt − η · (∂L / ∂w) = w_alt − η · δ · x | ||
| + | |||
| + | b_neu = b_alt − η · (∂L / ∂b) = b_alt − η · δ | ||
| + | </ | ||
| + | |||
| + | Dabei ist η (eta) die **Lernrate**, | ||
| + | |||
| + | <code python> | ||
| + | self.lr = learningRate | ||
| + | </ | ||
| + | |||
| + | Ist die Lernrate zu gross, springt das Netz über das Minimum hinweg. Ist sie zu klein, braucht das Training ewig. 0.01 ist ein guter Startwert für unser Netz. | ||
| + | |||
| + | **Das Ganze als Matrix** | ||
| + | |||
| + | Bisher haben wir so getan, als hätten wir nur ein einziges Gewicht w und einen einzigen Input x. In Wirklichkeit ist w natürlich die ganze **Gewichtsmatrix W** und x der ganze **Input-Vektor**. Schauen wir uns ein Mini-Beispiel mit 3 Inputs und 2 Hidden-Neuronen an: | ||
| + | |||
| + | < | ||
| + | X = [ x₁ x₂ x₃ ] | ||
| + | |||
| + | |||
| + | ⎡ w₁₁ | ||
| + | W = ⎢ w₂₁ | ||
| + | ⎣ w₃₁ | ||
| + | |||
| + | |||
| + | b = [ b₁ b₂ ] | ||
| + | </ | ||
| + | |||
| + | Die gewichtete Summe ist dann eine Matrix-Multiplikation: | ||
| + | |||
| + | < | ||
| + | z = X · W + b | ||
| + | |||
| + | = [x₁·w₁₁ + x₂·w₂₁ + x₃·w₃₁ + b₁ , x₁·w₁₂ + x₂·w₂₂ + x₃·w₃₂ + b₂] | ||
| + | </ | ||
| + | |||
| + | Für die Ableitung gilt genau dasselbe Prinzip wie vorher, nur eben für alle Gewichte auf einmal. Statt δ · x schreiben wir die Matrix-Version: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Das Xᵀ (X transponiert) sorgt dabei einfach dafür, dass die Dimensionen zusammenpassen. Im Code ist genau das diese Zeile, die du schon von oben kennst: | ||
| + | |||
| + | <code python> | ||
| + | self.W1 -= self.lr * (X.T @ d1) / N | ||
| + | </ | ||
| + | |||
| + | Der Vorteil der Matrix-Schreibweise ist enorm: statt 100' | ||
| + | |||
| + | ==== Schritt 3: Fehler zurück durch das Netz ==== | ||
| + | |||
| + | Soweit so gut für einen einzelnen Neuron. Aber jetzt kommt die eigentliche Herausforderung. | ||
| + | |||
| + | In unserem Netz hat kein Gewicht in der Hidden Layer einen direkten Kontakt zum Fehler am Ausgang. Die Hidden Layer sieht den Fehler nicht direkt, sie hat ihn nur indirekt mitverursacht. Backpropagation löst genau dieses Problem, indem es den Fehler rückwärts durch das Netz schickt, Schicht für Schicht. | ||
| + | |||
| + | Zuerst wird der Fehler an der Output Layer berechnet. Das ist noch einfach: Vorhersage minus richtige Antwort: | ||
| + | |||
| + | <code python> | ||
| + | d2 = self.a2 - yOnehot | ||
| + | </ | ||
| + | |||
| + | Mathematisch: | ||
| + | |||
| + | < | ||
| + | δ₂ = ŷ − y | ||
| + | </ | ||
| + | |||
| + | Jetzt muss dieser Fehler zurück durch die Gewichte W₂ in die Hidden Layer propagiert werden. Das klingt kompliziert, | ||
| + | |||
| + | < | ||
| + | z₁ ──► | ||
| + | | ||
| + | </ | ||
| + | |||
| + | z₁ wirkt also über mehrere Zwischenschritte auf den Loss. Die Kettenregel sagt uns: | ||
| + | |||
| + | < | ||
| + | | ||
| + | ──── | ||
| + | | ||
| + | </ | ||
| + | |||
| + | Schauen wir uns jeden Teil an: | ||
| + | |||
| + | * **∂L / ∂z₂** | ||
| + | * **∂z₂ / ∂a₁** | ||
| + | * **∂a₁ / ∂z₁** | ||
| + | |||
| + | Setzen wir alles zusammen: | ||
| + | |||
| + | < | ||
| + | δ₁ = (δ₂ · W₂ᵀ) | ||
| + | </ | ||
| + | |||
| + | Das ⊙ steht dabei für " | ||
| + | |||
| + | Das ReLU' | ||
| + | |||
| + | <code python> | ||
| + | d1 = (d2 @ self.W2.T) * self.reluDerivative(self.z1) | ||
| + | </ | ||
| + | |||
| + | Jetzt kennt jede Schicht ihren Fehleranteil. Damit können alle Gewichte und Biases aktualisiert werden: | ||
| + | |||
| + | <code python> | ||
| + | self.W2 -= self.lr * (self.a1.T @ d2) / N | ||
| + | self.b2 -= self.lr * d2.mean(axis=0) | ||
| + | self.W1 -= self.lr * (X.T @ d1) / N | ||
| + | self.b1 -= self.lr * d1.mean(axis=0) | ||
| + | </ | ||
| + | |||
| + | Mathematisch passiert hier für jede Schicht dasselbe wie beim einzelnen Neuron, nur jetzt für alle Gewichte gleichzeitig: | ||
| + | |||
| + | < | ||
| + | | ||
| + | W₂ = W₂ − η · ───────── | ||
| + | N | ||
| + | |||
| + | Xᵀ · δ₁ | ||
| + | W₁ = W₁ − η · ───────── | ||
| + | N | ||
| + | </ | ||
| + | |||
| + | Die Division durch N ist dabei wichtig: sie mittelt den Fehler über alle Trainingsbeispiele im Batch, damit kein einzelnes Bild zu viel Einfluss hat. | ||
| + | |||
| + | Dieser ganze Prozess – Forward Pass, Loss berechnen, Fehler zurückpropagieren, | ||
| + | |||
| + | <code python> | ||
| + | for epoch in range(epochs): | ||
| + | for start in range(0, N, batchSize): | ||
| + | probs = self.forward(Xs[start: | ||
| + | epochLoss += self.crossEntropyLoss(probs, | ||
| + | self.backward(Xs[start: | ||
| + | </ | ||
| + | |||
| + | Nach hunderten von Epochen haben sich die Gewichte so weit angepasst, dass das Netz die meisten Buchstaben richtig erkennt. Backpropagation ist also im Grunde nur eine sehr clevere Art, die Schuld für einen Fehler auf alle Beteiligten aufzuteilen und jeden ein kleines Stück besser zu machen. | ||
| + | |||
| + | ===== Die drei Schichten im Überblick ===== | ||
| + | |||
| + | ==== Input Layer ==== | ||
| + | |||
| + | Die **Input Layer** ist der Eingang des Netzes. Hier kommt das Bild rein, nichts weiter. Jeder Pixel wird zu einem Neuron, und weil unsere Bilder 28×28 Pixel gross sind, hat diese Schicht genau 784 Neuronen. Sie rechnet nichts, sie bewertet nichts, sie gibt einfach die rohen Pixelwerte weiter. In der Grafik sind das die lila Kreise ganz links, beschriftet mit x₁ bis x₇₈₄. | ||
| + | |||
| + | ==== Hidden Layer ==== | ||
| + | |||
| + | Die **Hidden Layer** ist dort, wo das eigentliche Lernen passiert. Sie liegt in der Mitte, zwischen Input und Output, und ist von aussen nicht direkt sichtbar. Daher der Name. Ihre 128 Neuronen nehmen alle 784 Eingabewerte, | ||
| + | |||
| + | <code python> | ||
| + | self.W1 = np.random.randn(inputSize, | ||
| + | </ | ||
| + | |||
| + | ==== Output Layer ==== | ||
| + | |||
| + | Die **Output Layer** ist die letzte Schicht und gibt die finale Antwort des Netzes. Sie hat genau 52 Neuronen, eines pro Buchstabe von a bis Z. Jedes dieser Neuronen gibt eine Wahrscheinlichkeit aus, wie sicher das Netz ist, dass der gesehene Buchstabe dieser Klasse entspricht. Der Buchstabe mit der höchsten Wahrscheinlichkeit ist dann die Vorhersage. In der Grafik sind das die orangen Kreise ganz rechts. Im Code: | ||
| + | |||
| + | <code python> | ||
| + | self.W2 = np.random.randn(hiddenSize, | ||
| + | </ | ||
| + | |||
| + | ===== Flache und tiefe Netze ===== | ||
| - | | + | Ein **tiefes neuronales Netz**, auf Englisch //Deep Neural Network//, ist im Grunde nichts anderes als ein Netz mit mehreren Hidden Layers hintereinander. Statt einer einzigen mittleren Schicht wie in unserem Code hat ein tiefes Netz vielleicht fünf, zehn oder sogar hunderte davon. Jede Schicht lernt dabei etwas ein bisschen Abstrakteres als die vorherige. Die erste Hidden Layer erkennt vielleicht einfache Kanten und Striche, die zweite kombiniert diese zu Kurven und Ecken, die dritte erkennt daraus ganze Buchstabenteile und so weiter. Je tiefer das Netz, desto komplexere Konzepte kann es verstehen. |
| - | | + | |
| - | | + | |
| - | | + | |
| - | * Aktivierungsfunktion | + | |
| - | * Perzeptron | + | |
| - | * Feedforward-Netz | + | |
| - | * gewichtete Summe | + | |
| - | * Schwellenwert | + | |
| - | * Entscheidungsgrenze | + | |
| - | * Backpropagation | + | |
| - | * Input Layer | + | |
| - | * Hidden Layer | + | |
| - | * Output Layer | + | |
| - | * Tiefe neuronale Netze (Deep Neural Networks) | + | |
| - | * flache vs. tiefe Netze | + | |
| - | Evtl. ein ganz simples Beispiel zeigen | + | Genau da liegt der Unterschied zu einem **flachen Netz**. Ein flaches Netz hat nur eine oder sehr wenige Hidden Layers, so wie unseres mit genau einer. Es kann durchaus funktionieren, |
| + | Ein tiefes Netz löst das, indem es die Arbeit auf viele Schichten verteilt. Jede Schicht baut auf der vorherigen auf und abstrahiert weiter. Das ist auch der Grund, warum moderne KI-Modelle für Bilderkennung, | ||
| + | Der Nachteil dabei ist, dass tiefe Netze viel mehr Daten, viel mehr Rechenleistung und viel mehr Zeit zum Trainieren brauchen. Unser flaches Netz mit 784 Inputs, 128 Hidden-Neuronen und 52 Outputs hat knapp 102' | ||
| + | ===== Zusammenfassung der wichtigsten Begriffe ===== | ||
| + | * **künstliches Neuron** – Grundbaustein, | ||
| + | * **Input** – rohe Eingabewerte, | ||
| + | * **Gewicht** – bestimmt, wie stark ein Input Einfluss hat | ||
| + | * **Bias** – verschiebt die Aktivierungsschwelle unabhängig vom Input | ||
| + | * **Aktivierungsfunktion** – entscheidet, | ||
| + | * **Perzeptron** – einfachstes künstliches Neuron, gibt nur 0 oder 1 aus | ||
| + | * **Feedforward-Netz** – Informationen fliessen nur in eine Richtung, von Input zu Output | ||
| + | * **gewichtete Summe** – z = Σ wᵢ·xᵢ + b, das Zwischenergebnis eines Neurons | ||
| + | * **Schwellenwert** – Grenze zwischen feuern und nicht feuern (bei ReLU: 0) | ||
| + | * **Entscheidungsgrenze** – hochdimensionale Trennfläche, | ||
| + | * **Backpropagation** – Verfahren, das den Fehler rückwärts durchs Netz schickt und Gewichte anpasst | ||
| + | * **Input Layer** – erste Schicht, nimmt die rohen Inputs entgegen | ||
| + | * **Hidden Layer** – mittlere Schicht, lernt Teilmuster | ||
| + | * **Output Layer** – letzte Schicht, gibt die Vorhersage als Wahrscheinlichkeiten aus | ||
| + | * **Tiefe neuronale Netze (Deep Neural Networks)** – Netze mit vielen Hidden Layers | ||
| + | * **flache vs. tiefe Netze** – eine vs. viele Hidden Layers, Komplexität und Rechenaufwand steigen | ||
| + | ===== Quellen ===== | ||
| + | ==== Videos ==== | ||
| + | Die Videoserie von // | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | ==== Wikipedia ==== | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | ==== Weiterführende Literatur ==== | ||
| + | * Michael Nielsen: //Neural Networks and Deep Learning// – kostenloses Online-Buch unter [[http:// | ||
| + | * Ian Goodfellow, Yoshua Bengio, Aaron Courville: //Deep Learning// – frei verfügbar unter [[https:// | ||
| + | ==== Bibliotheken und Tools ==== | ||
| + | * [[https:// | ||
| + | * [[https:// | ||