Updates dieser Seite:
In diesem Kapitel wird gezeigt, wie man in Keras die Suche nach "guten" Hyperparametern automatisieren kann. Hyperparameter sind z.B. die Anzahl der Schichten, der Neuronen pro Schicht oder die Lernrate. Man nennt dies Hyperparameter Tuning. Diese Methode erfordert naturgemäß hohe Rechenkapazitäten.
Wenn man ein Neuronales Netz erstellt und später trainiert, muss man verschiedene Entscheidungen treffen:
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
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.
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
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:
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:
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
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.
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)
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>)
Keras bietet verschiedene Suchmethoden an, um den Raum der Hyperparameter zu durchlaufen:
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.
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.
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}
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.
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 _________________________________________________________________
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
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 _________________________________________________________________
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()
show_loss_acc(history.history)
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:
Weitere Hyperparameter, die evtl. untersuchenswert sind: