Neuronale Netze

Es gibt sehr simple Probleme, in denen lineare Modelle nicht gut funktionieren:

$x_1$$x_2$$y$
000
011
101
110

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

  • $a_1 = \sigma(W_1 x + b_1)$
  • $a_2 = \sigma(W_2 a_1 + b_2)$

Wie finden wir die richtigen Parameter $W_1, W_2, b_1, b_2$?

  • Keine analytische Lösung möglich
  • Gradient descent!

Wie berechnen wir z.B. den Gradienten $\nabla_{W_1}$?

Kettenregel: $f(g(x))' = f'(g(x)) \cdot g'(x)$

Backpropagation Algorithmus

  • Berechne die Ableitung der Loss Funktion in Richtung von $a_2$
  • Berechne die weiteren Ableitungen von außen nach innen

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:

  • $a_1 = \sigma(W_1 x + b_1)$
  • $a_2 = \sigma(W_2 a_1 + b_2)$
  • $\ldots$
  • $a_n = \sigma(W_n a_{n-1} + b_n)$

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?

  • Gradienten werden 0
  • Komplette Symmetrie

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?
  • Wie gut funktioniert Gradient Descent, wenn wir viele Layer / Daten haben?
  • Wie implementieren wir das alles praktisch?
  • Was machen wir mit speziellen Daten (z.B. Bilder oder Text)?

Muss jeder Layer eine logistische Regression sein?

Versuchen wir es mal mit einer linearen Regression:

  • $a_1 = W_1 x + b_1$
  • $a_2 = W_2 a_1 + b_2$

$\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:

  • nicht linear
  • differenzierbar (fast überall)
  • Gradient ist (großteils) nicht null

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:

  • Reellwertige Zahl: Linear
  • Binäre Klassifikation: Sigmoid
  • Mehrere Klassen: Soft-max

Die Loss Funktion müssen wir passend wählen:

WertebereichAkt.funktionLoss Funktion
$y \in \mathbb{R}$LinearMean Squared Error
$y \in \{0,1\}$SigmoidBinary Cross Entropy
$y \in \{0,1,\ldots,K\}$Soft-maxCross Entropy

Interpretation bleibt (ungefähr) erhalten:

  • Bei reellwertigen Problemen haben wir einen normalverteilten Fehler.
  • Bei Klassifikationen sagen wir Wahrscheinlichkeiten je Klasse vorher.

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$

Jupyter

Es gibt diverse Modifikationen von Gradient Descent, die besser funktionieren können.

Häufig sehr gut funktioniert ADAM (Adaptive Moment Estimation)

  • Weiche nicht zu stark von der letzten Richtung ab (Momente)
  • Normalisiere die Länge der Gradienten (RMSProp)

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.

Jupyter

Data Pipelines

Effizientes Training bedeutet, dass wir dem Modell die Trainingsdaten effizient zufüttern

Tensorflow Datasets bieten eine API dafür an.

Relevante Operationen:

  • Daten laden
  • Batching
  • Shuffling
  • Preprocessing
  • Augmentation
  • Caching
Jupyter

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

  • LeCun normal (Faktor $\sqrt{\frac{1}{m}}$)
  • He normal (Faktor $\sqrt{\frac{2}{m}}$)
  • Glorot / Xavier normal (Faktor $\sqrt{\frac{2}{n+m}}$)
  • ...

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?

  • Das Netzwerk ist zur Trainingszeit kleiner
  • Jeder Layer verlässt sich weniger auf jeden einzelnen Input -> mehr Redundanz

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.