Anhang C: Keras: Hyperparameter-Tuning

In diesem Kapitel lernen wir, wie man in Keras die Suche nach “guten” Hyperparametern automatisieren kann. Zur Erinnerung: Hyperparameter sind z.B. die Anzahl der Schichten, der Neuronen pro Schicht oder die Lernrate. Man nennt das automatisierte und systematische Suchen nach guten Hyperparametern Hyperparameter Tuning. Diese Methode erfordert viel Rechenpower und Zeit.

C.1 Motivation

Wenn man ein Neuronales Netz erstellt und später trainiert, muss man verschiedene Entscheidungen treffen:

  • Anzahl der Schichten
  • Typ einer Schicht (z.B. Dense vs. Conv2D)
  • Anzahl der Neuronen pro Schicht
  • Einfügen einer Dropout-Schicht (oder nicht)
  • Optimierungsmethode (SGD, Adam etc.)
  • Lernrate (zu Trainingsbeginn)

Diese Aspekte fallen alle unter den Begriff Hyperparameter, weil dies Parameter sind, die sich während des Trainings nicht ändern.

Wie trifft man diese Entscheidungen? In der Regel orientiert man sich an Netzen, die man schonmal gesehen hat oder die in wissenschaftlichen Publikationen dargestellt sind, und anschließend “probiert” man mehr oder weniger systematisch, an den Hyperparametern zu drehen (und protokolliert hoffentlich die jeweiligen Outcomes). Natürlich kommt man schnell auf die Idee, diesen Prozess zu automatisieren. Das nennt man auch Hyperparameter Tuning oder Hyperparameter Optimization.

Siehe auch: https://keras.io/guides/keras_tuner/getting_started

C.2 Keras-Paket

In Keras gibt es dazu das Paket keras-tuner, das hier vorgestellt wird.

Zunächst muss keras-tuner installiert sein - das behandeln wir hier nicht.

Importe

Wir benötigen die folgenden Importe:

import keras_tuner
from kerastuner import HyperModel
/var/folders/lm/zb9jvwpj09551m21zr3dd8440000gn/T/ipykernel_62551/2350946801.py:2: DeprecationWarning: `import kerastuner` is deprecated, please use `import keras_tuner`.
  from kerastuner import HyperModel

Objekt vom Typ HyperParameters

Wir erstellen ein Objekt vom Typ HyperParameters, das später Werte für unsere Hyperparameter (z.B. Anzahl der Neuronen einer Schicht).

hp = keras_tuner.HyperParameters()

Ein Objekt hp vom Typ HyperParameters hat mehrere Funktionen. Bei diesen Funktionen wird ein Wert vom entsprechenden Typ generiert:

  • hp.Int()
  • hp.Float()
  • hp.Boolean()

Beim Funktionsaufruf wird auch ein Bezeichner (z.B. “units”) übergeben, damit das Objekt die verschiedenen Werte unterscheiden kann (z.B. Neuronenanzahl für Schicht 2 und Schicht 3).

print(hp.Int("units_2", min_value=32, max_value=512, step=32))
32

Bei der folgenden Funktion wird eine Menge von Werten mit einer Liste definiert:

  • hp.Choice()

Beispiel:

hp.Choice("activation", ["relu", "tanh"])
'relu'

Die komplette Liste von Funktionen und ihrer Parameter finden Sie hier: https://keras.io/api/keras_tuner/hyperparameters

C.3 Modell

Wir spielen alle Schritte anhand eines einfachen Versuchs durch, wo wir in einem Netz lediglich die Größe der versteckten Schicht ändern. Wir haben also einen einzigen Hyperparameter.

C.3.1 Daten (CIFAR-10)

Wir nehmen die CIFAR-10-Daten für unsere Versuche.

CIFAR-10 enthält 60000 Farbbilder (32x32x3) mit Abbildungen aus 10 Kategorien (= Label), z.B “ship” oder “dog”.

import numpy as np
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.datasets import cifar10

(train_x, train_y), (test_x, test_y) = cifar10.load_data()

train_x = train_x/255.0
test_x = test_x/255.0

train_y = to_categorical(train_y, 10)
test_y = to_categorical(test_y, 10)

print(train_x.shape, test_x.shape)
print(test_y.shape)
(50000, 32, 32, 3) (10000, 32, 32, 3)
(10000, 10)
train_x[0].shape
(32, 32, 3)

C.3.2 Modell-Erzeugungsfunktion

Wir definieren eine Funktion, die ein Modell mit Hilfe eine HyperParameter-Objekts baut. Das Objekt trifft die Entscheidung über die Hyperparameter des generierten Modells.

Wir spezifizieren den Input hier explizit, weil wir sonst die Funktion summary nicht aufrufen können.

from tensorflow.keras import Input
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten

def build_model(hp):
    model = Sequential()
    model.add(Input(shape=train_x[0].shape))
    model.add(Flatten())
    model.add(Dense(hp.Int("num_hidden", min_value=50, max_value=1000, step=50),
                    activation="relu"))
    model.add(Dense(10, 
                    activation="softmax"))
    model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["acc"])
    return model

Sanity check: Wird ein Modell erzeugt und wie sieht es aus?

model = build_model(hp)
model.summary()
Metal device set to: Apple M1 Max
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten (Flatten)           (None, 3072)              0         
                                                                 
 dense (Dense)               (None, 50)                153650    
                                                                 
 dense_1 (Dense)             (None, 10)                510       
                                                                 
=================================================================
Total params: 154,160
Trainable params: 154,160
Non-trainable params: 0
_________________________________________________________________
2022-06-21 18:08:26.965512: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:305] Could not identify NUMA node of platform GPU ID 0, defaulting to 0. Your kernel may not have been built with NUMA support.
2022-06-21 18:08:26.965646: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:271] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 0 MB memory) -> physical PluggableDevice (device: 0, name: METAL, pci bus id: <undefined>)

C.4 Tuning

C.4.1 Suchmethode

Keras bietet verschiedene Suchmethoden an, um den Raum der Hyperparameter zu durchlaufen:

  • RandomSearch
  • BayesianOptimization
  • Hyperband

RandomSearch

Wir wählen die einfachste Methode RandomSearch. Zunächst wird ein Objekt mit Konfigurationsdaten erstellt. Ein “Versuch” (trial) bezieht sich auf ein spezifisches Modell mit festgelegten Hyperparametern.

  • objective: Metrik, anhand der die Modelle bewertet werden sollen
  • max_trials: Maximale Anzahl der Versuche
  • executions_per_trial: Für jeden Versuche kann man mehrere Läufe anstellen
  • overwrite: Wenn man eine Suche fortsetzen möchte, kann man hier False angeben
  • directory: Hier werden Log-Informationen hineingeschrieben
tuner = keras_tuner.RandomSearch(
    hypermodel=build_model,
    objective="val_acc",
    max_trials=6,
    executions_per_trial=1,
    overwrite=True,
    directory="hyperparameter_tuning",
    project_name="simple",
)

Uns reichen 6 Versuche. Bei jedem Versuch wird nur 1x trainiert. Wie viele Epochen pro Versuch durchlaufen werden, wird später festgelegt.

Suchraum

Man kann sich jetzt den Suchraum anzeigen lassen. Hier sieht man im Grunde, an welchen Stellen man einen Hyperparameter von Keras wählen lässt.

Wir sehen, dass es nur einen Hyperparameter gibt, der variiert wird.

tuner.search_space_summary()
Search space summary
Default search space size: 1
num_hidden (Int)
{'default': None, 'conditions': [], 'min_value': 50, 'max_value': 1000, 'step': 50, 'sampling': None}

C.4.2 Suche durchführen

Jetzt können wir die Suche starten. Für die Anzahl der Epochen nimmt man einen relativ niedrigen Wert, z.B. 4 oder 10, damit man auch viele Durchläufe in realistischer Zeit schafft. Wir haben wir nur wenige Durchläufe (6 Versuche). In einem realistischen Beispiel würde man die Anzahl der Versuche erhöhen. Eine Epochenzahl von 4-10 ist auch bei größeren Versuchen realistisch.

Während der Suche wird immer der bislang beste Wert angezeigt. Die Suche dauert natürlich etwas…

tuner.search(train_x, train_y, 
             epochs=4, 
             validation_data=(test_x, test_y))
Trial 6 Complete [00h 00m 48s]
val_acc: 0.44450002908706665

Best val_acc So Far: 0.4547000229358673
Total elapsed time: 00h 04m 39s
INFO:tensorflow:Oracle triggered exit

Wir sehen, dass 6 Versuche in 4 Minuten unternommen wurden und die beste Performance 45% betrug.

C.5 Auswertung

C.5.1 Beste Modelle

Die Modelle werden gespeichert. Wir sehen uns die besten zwei an.

models = tuner.get_best_models(num_models=2)
models
[<keras.engine.sequential.Sequential at 0x316508c10>,
 <keras.engine.sequential.Sequential at 0x316508280>]
models[0].summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten (Flatten)           (None, 3072)              0         
                                                                 
 dense (Dense)               (None, 950)               2919350   
                                                                 
 dense_1 (Dense)             (None, 10)                9510      
                                                                 
=================================================================
Total params: 2,928,860
Trainable params: 2,928,860
Non-trainable params: 0
_________________________________________________________________
models[1].summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten (Flatten)           (None, 3072)              0         
                                                                 
 dense (Dense)               (None, 900)               2765700   
                                                                 
 dense_1 (Dense)             (None, 10)                9010      
                                                                 
=================================================================
Total params: 2,774,710
Trainable params: 2,774,710
Non-trainable params: 0
_________________________________________________________________

Was wurde probiert?

Mit der Funktion results_summary können Sie ausgeben, welche Versuche unternommen wurden, wie die jeweiligen Parameter gewählt wurden (hier nur “num_hidden”) und welche Performance erzielt wurde.

tuner.results_summary()
Results summary
Results in hyperparameter_tuning/simple
Showing 10 best trials
<keras_tuner.engine.objective.Objective object at 0x314e09820>
Trial summary
Hyperparameters:
num_hidden: 950
Score: 0.4547000229358673
Trial summary
Hyperparameters:
num_hidden: 900
Score: 0.44450002908706665
Trial summary
Hyperparameters:
num_hidden: 250
Score: 0.44050002098083496
Trial summary
Hyperparameters:
num_hidden: 450
Score: 0.42970001697540283
Trial summary
Hyperparameters:
num_hidden: 200
Score: 0.4097000062465668
Trial summary
Hyperparameters:
num_hidden: 100
Score: 0.3987000286579132

C.5.2 Bestes Modell trainieren und evaluieren

Jetzt haben wir ja in der Suche nur für eine reduzierte Anzahl von Epochen trainiert.

Wir wählen jetzt das beste Modell und trainieren es mit einer Epochenzahl, die uns angemessen erscheint.

best_hps = tuner.get_best_hyperparameters(3)
best_hps
[<keras_tuner.engine.hyperparameters.HyperParameters at 0x31636ed30>,
 <keras_tuner.engine.hyperparameters.HyperParameters at 0x3165031c0>,
 <keras_tuner.engine.hyperparameters.HyperParameters at 0x314e134f0>]
model = build_model(best_hps[0])
model.summary()
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten_1 (Flatten)         (None, 3072)              0         
                                                                 
 dense_2 (Dense)             (None, 950)               2919350   
                                                                 
 dense_3 (Dense)             (None, 10)                9510      
                                                                 
=================================================================
Total params: 2,928,860
Trainable params: 2,928,860
Non-trainable params: 0
_________________________________________________________________

Training

Wir trainieren das Modell über 20 Epochen.

history = model.fit(train_x, train_y, 
                    epochs=20, 
                    validation_data=(test_x, test_y))
Epoch 1/20
  17/1563 [..............................] - ETA: 10s - loss: 4.7808 - acc: 0.1195
2022-06-21 18:13:07.739950: I tensorflow/core/grappler/optimizers/custom_graph_optimizer_registry.cc:112] Plugin optimizer for device_type GPU is enabled.
1557/1563 [============================>.] - ETA: 0s - loss: 1.8898 - acc: 0.3299
2022-06-21 18:13:18.659267: I tensorflow/core/grappler/optimizers/custom_graph_optimizer_registry.cc:112] Plugin optimizer for device_type GPU is enabled.
1563/1563 [==============================] - 13s 8ms/step - loss: 1.8898 - acc: 0.3301 - val_loss: 1.7331 - val_acc: 0.3798
Epoch 2/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.6864 - acc: 0.3961 - val_loss: 1.6275 - val_acc: 0.4234
Epoch 3/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.6193 - acc: 0.4248 - val_loss: 1.5797 - val_acc: 0.4350
Epoch 4/20
1563/1563 [==============================] - 12s 7ms/step - loss: 1.5763 - acc: 0.4397 - val_loss: 1.5650 - val_acc: 0.4419
Epoch 5/20
1563/1563 [==============================] - 12s 7ms/step - loss: 1.5497 - acc: 0.4474 - val_loss: 1.5508 - val_acc: 0.4528
Epoch 6/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.5273 - acc: 0.4575 - val_loss: 1.5335 - val_acc: 0.4501
Epoch 7/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.5064 - acc: 0.4664 - val_loss: 1.5794 - val_acc: 0.4388
Epoch 8/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4854 - acc: 0.4728 - val_loss: 1.5184 - val_acc: 0.4626
Epoch 9/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4703 - acc: 0.4776 - val_loss: 1.5376 - val_acc: 0.4590
Epoch 10/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4584 - acc: 0.4841 - val_loss: 1.5514 - val_acc: 0.4569
Epoch 11/20
1563/1563 [==============================] - 12s 7ms/step - loss: 1.4416 - acc: 0.4875 - val_loss: 1.4930 - val_acc: 0.4751
Epoch 12/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4325 - acc: 0.4898 - val_loss: 1.4912 - val_acc: 0.4717
Epoch 13/20
1563/1563 [==============================] - 12s 7ms/step - loss: 1.4229 - acc: 0.4952 - val_loss: 1.4855 - val_acc: 0.4759
Epoch 14/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4129 - acc: 0.4969 - val_loss: 1.4816 - val_acc: 0.4794
Epoch 15/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.4076 - acc: 0.4974 - val_loss: 1.4736 - val_acc: 0.4832
Epoch 16/20
1563/1563 [==============================] - 12s 7ms/step - loss: 1.3984 - acc: 0.5030 - val_loss: 1.4813 - val_acc: 0.4827
Epoch 17/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.3901 - acc: 0.5057 - val_loss: 1.5041 - val_acc: 0.4756
Epoch 18/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.3801 - acc: 0.5083 - val_loss: 1.5288 - val_acc: 0.4658
Epoch 19/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.3723 - acc: 0.5114 - val_loss: 1.5389 - val_acc: 0.4637
Epoch 20/20
1563/1563 [==============================] - 12s 8ms/step - loss: 1.3672 - acc: 0.5136 - val_loss: 1.4764 - val_acc: 0.4809
import matplotlib.pyplot as plt

def show_loss_acc(h):
    plt.figure(figsize=(12, 8))

    plt.subplot(2, 2, 1)
    plt.plot(h['loss'], label='train loss')
    plt.plot(h['val_loss'], label='val loss')
    plt.legend()

    plt.subplot(2, 2, 2)
    plt.plot(h['acc'], label='train acc')
    plt.plot(h['val_acc'], label='val acc')
    plt.legend()
    plt.show()

Visualisierung

show_loss_acc(history.history)

C.6 Fazit

Wir haben also herausgefunden, dass ein Netz mit 950 Neuronen besser performt als ein Netz mit weniger Neuronen. Weil wir die Suche auf 6 Trials beschränkt hatten, wurde ein Netz mit 1000 Neuronen gar nicht ausprobiert. Und natürlich gibt es eine Reihe von anderen Möglichkeiten, die Netzarchitektur oder das Training zu ändern, etwa mehrere Schichten oder eine andere Lernrate.

Mit dem gefundenen Netz haben wir ca. 48% auf den Testdaten erzielt.

Ganz allgemein können Sie mit Hyperparameter-Tuning praktisch alle Hyperparameter addressieren, sowohl für Feedforward-Netze als auch für Konvolutionsnetze.

Zu den wichtigsten Hyperparametern gehören sicherlich:

  • Anzahl der Schichten
  • Typ und Reihenfolge der Schichten (insbes. bei CNN: ConvLayer, Dense, Pooling)
  • Anzahl der Neuronen einer Schicht (bei Konv-Schichten auch Filtergröße und Filteranzahl, bei Pooling-Schichten Filtergröße und Stride)
  • Lernrate

Weitere Hyperparameter, die evtl. untersuchenswert sind:

  • Optimierungsmethode (Adam, RMSprop etc.)
  • Batchgröße
  • Momentum