Es gibt sehr simple Probleme, in denen lineare Modelle nicht gut funktionieren:
| $x_1$ | $x_2$ | $y$ |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
XOR
Mit komplexeren Funktionen als Hypothese klappt das:
$f(x) = \sigma(W_2 \sigma(W_1 x + b_1) + b_2)$,
mit: $\sigma(z) = \frac{1}{1 + e^{-z}}$
\[ W_1 = \begin{bmatrix} 20 & -20 \\ -20 & 20 \end{bmatrix}, \quad b_1 = \begin{bmatrix} -10 \\ -10 \end{bmatrix}\\ W_2 = \begin{bmatrix} 20 \\ 20 \end{bmatrix}, \quad b_2 = -10 \]
| $x$ | $\sigma(W_1 x + b_1)$ | $\sigma(W_2 \sigma(W_1 x + b_1) + b_2)$ |
|---|---|---|
| $[0,0]^T$ | $[4.5\cdot 10^{-5},4.5\cdot 10^{-5}]^T$ | $4.5\cdot 10^{-5}$ |
| $[0,1]^T$ | $[9.4\cdot 10^{-14},0.999]^T$ | $0.999$ |
| $[1,0]^T$ | $[0.999,9.4\cdot 10^{-14}]^T$ | $0.999$ |
| $[1,1]^T$ | $[4.5\cdot 10^{-5},4.5\cdot 10^{-5}]^T$ | $4.5\cdot 10^{-5}$ |
Bei unserer Hypothese kombinieren wir zwei logistische Regressionen
Wie finden wir die richtigen Parameter $W_1, W_2, b_1, b_2$?
Wie berechnen wir z.B. den Gradienten $\nabla_{W_1}$?
Kettenregel: $f(g(x))' = f'(g(x)) \cdot g'(x)$
Backpropagation Algorithmus
Wir müssen für jede einzelne Operation die Ableitung kennen,
dann erhalten wir
mechanisch die Ableitung der ganzen Operation.
Frameworks, wie PyTorch, TensorFlow, Jax, etc. bieten genau dieses Tooling.
Mit diesem Schema könnten wir beliebig viele logistische Regressionen aufeinander stapeln:
Das ist ein neuronales Netzwerk mit $n$ Layern.
Anders als bei linearen Modellen gibt es keine Garantie, dass wir irgendwann zur optimalen Lösung kommen.
In der Praxis sind lokale Optima kein echtes Problem.
Fallstrick bei Gradient Descent:
Was passiert, wenn wir alle Gewichte mit 0 initialisieren?
Lösung: Initialisiere Gewichte zufällig.
import numpy as np
W1 = np.random.randn(n1, n0) * 0.01
b1 = np.zeros([n1, 1])
Unterschiedliche Starts führen zu unterschiedlichen Lösungen.
In der Praxis macht es keinen großen Unterschied für die Lösung.
Ein paar Fragen müssen wir noch klären:
Muss jeder Layer eine logistische Regression sein?
Versuchen wir es mal mit einer linearen Regression:
$\Rightarrow a_2 = W_2(W_1 x + b_1) + b_2$
$= \underbrace{W_2 W_1}_{W'} x + \underbrace{W_2 b_1 + b_2}_{b'}$
Da jede Operation linear ist, kollabieren die Gleichungen.
Das Modell ist immer noch eine Gerade / Ebene. Die Zusatzlayer bringen uns nichts.
Die logistische Funktion $\sigma$ hat im anderen Fall eine Nichtlinearität eingebracht.
Diese ist essentiell, damit mehrere Layer einen Nutzen haben.
$\Rightarrow$ Aktivierungsfunktion
Muss die Aktivierungsfunktion die logistische Funktion sein?
Nein, aber die Funktion muss folgende Eigenschaften erfüllen:
Sigmoid
$g(z) = \frac{1}{1+e^{-z}}$
Tangens hyperbolicus
$g(z) = \operatorname{tanh}(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}$
Rectified Linear Unit (ReLU)
$g(z) = \max(z, 0) = \begin{cases} z & \text{if } z > 0\\ 0 & \text{else} \end{cases}$
Leaky ReLU
$g(z) = \max(z, 0.01z) = \begin{cases} z & \text{if } z > 0\\ 0.01 z & \text{else} \end{cases}$
Üblicher Ansatz: Starte mit ReLU für alle außer dem letzten Layer.
Aktivierungsfunktion des letzten Layers hängt vom Problem ab:
Die Loss Funktion müssen wir passend wählen:
| Wertebereich | Akt.funktion | Loss Funktion |
|---|---|---|
| $y \in \mathbb{R}$ | Linear | Mean Squared Error |
| $y \in \{0,1\}$ | Sigmoid | Binary Cross Entropy |
| $y \in \{0,1,\ldots,K\}$ | Soft-max | Cross Entropy |
Interpretation bleibt (ungefähr) erhalten:
Als nächstes wollen wir ein neuronales Netzwerk praktisch trainieren.
Als Framework verwenden wir TensorFlow
Grundidee: Wir spezifizieren den Compute Graph und das Framework berechnet die Gradienten.
Beispiel: Minimiere $w^2 - 10 w + 25$
JupyterEs gibt diverse Modifikationen von Gradient Descent, die besser funktionieren können.
Häufig sehr gut funktioniert ADAM (Adaptive Moment Estimation)
In Tensorflow tauschen wir
tf.keras.optimizers.SGD
durch
tf.keras.optimizers.Adam
aus.
Je standardisierter unsere Anwendung ist, umso mehr Helfer bietet uns das Framework.
Wir können immer auf die Low Level APIs ausweichen, wenn wir speziellere Ansprüche haben.
Beispiel: XOR
Hier nutzen wir ein reguläres neuronales Netzwerk. Das können wir mit der Keras.Sequential API zusammenkonfigurieren.
JupyterData Pipelines
Effizientes Training bedeutet, dass wir dem Modell die Trainingsdaten effizient zufüttern
Tensorflow Datasets bieten eine API dafür an.
Relevante Operationen:
Wenn wir viele kleine Werte aufmultiplizieren wird das Ergebnis schnell sehr klein: \[ \underbrace{0.5 \cdot 0.5 \cdot 0.5 \cdot \ldots}_{50\text{ mal}} = 0.5^{50} \\\approx 0.00000000000000089 \]
Analog wachsen Werte $>1$ sehr schnell: \[ 1.5 ^ {50} \approx 637621500 \]
Mit vielen Layern wird ein neuronales Netz im Training leicht instabil
Vanishing / exploding gradients
Exploding Gradients können durch gradient clipping "gepatched" werden.
Vanishing gradients sind meist eher das Problem
Um das Training gut zu starten wollen versuchen wir die initialen Gewichte günstig zu wählen.
Idee: Skaliere die zufälligen Gewichte auf eine Standard Abweichung von 1
\[ W = \text{np.random.randn(n, m)} \cdot \sqrt{\frac{1}{m}} \]
Von dieser Idee gibt es verschiedene Spielarten, z.B.:
Ordentliche Initialisierung hilft natürlich vor allem zum Trainingsstart.
Für Eingangsfeatures ist es günstig, wenn sie Mittelwert 0 und Standardabweichung 1 haben
Wie schaut es mit den hidden Layers aus?
Batch Normalization
Idee: Normalisiere die Outputs jedes Layers innerhalb jedes Batches
Jeder Layer kann sich besser auf seine Inputs verlassen.
Batch normalization kann das Training deutlich stabiler machen und zu schnellerer Konvergenz führen.
Gerade mit tiefen neuronalen Netzwerken haben wir schnell sehr viele trainierbare Gewichte:
Ein Layer mit 500 Eingangs und 500 Ausgangsfeatures hat 250.500 Gewichte
Overfitting wird schnell zu einem Problem.
Wie bei den linearen Modellen können wir in der Zielfunktion große Gewichte bestrafen.
L2 / L1 Regularisierung
In Keras / Tensorflow können wir einem Layer mitgeben, wie Gewichte bestraft werden sollen:
kernel_regularizer=regularizers.L1L2(l1=1e-5, l2=1e-4)
Dropout Regularisierung
Idee: Wir setzten zufällig den Output mancher Neuronen auf 0.
Praktisch trainieren wir in jedem Beispiel ein kleineres Teilnetzwerk.
Beachte: Das Netzwerk ist ein anderes für jeden Datenpunkt
Neuer Hyperparameter: $0 \leq \text{keep\_prob} \leq 1$
Die Wahrscheinlichkeit, dass wir einen Output nicht auf 0 setzen.
import numpy as np
d_i = np.random.rand(a_i.shape[0], a_i.shape[1]) < keep_prob
a_i = np.multiply(a_i, d_i)
a_i /= keep_prob
Wir rescalen das Ergebnis am Ende ("inverted dropout")
Dropout wird nur zur Trainingszeit verwendet, nicht für Inferenz.
Warum funktioniert das?
|
|
Wir können unterschiedliche keep_prob Hyperparameter für jeden Layer haben.
Wenn ein Layer nur wenige Outputs hat, sollten wir diese nicht auch noch auf 0 setzen.
Wenn es kein Overfitting Problem gibt, brauchen wir auch keinen Dropout Layer.