Kapitel K1: Keras

Updates dieser Seite:

Überblick

In diesem Kapitel sind praxisrelevante Keras-Themen versammelt, die bislang vernachlässigt wurden, weil die Zeit fehlte oder sie konzeptionell nicht passten. Die Inhalte hier sind nicht prüfungsrelevant, sondern sollen Ihnen in der Praxis mit Keras helfen.

1 Training

1.1 Loss-Funktion (binary, categorical, sparse)

Bevor man ein NN mit fit trainiert, legt man mit compile die Loss-Funktion fest, z.B.

model.compile(loss='categorical_crossenropy', optimizer='adam')

Die Loss-Funktion ist in der Regel eine von diesen (die Begiffe sind mit der Keras-Dokumentation verlinkt):

Hier nochmal eine kurze Erklärung, wann Sie welche nehmen:

Sie verwenden binary_crossentropy für binäre Klassifikation, wo Ihre Labels als Liste mit Nullen und Einsen vorliegen.

Auf categorical_crossentropy greifen Sie bei der Klassifikation mit multiplen Klassen zurück (Beispiel MNIST mit Klassen 0 bis 9). Hier müssen die Labels in One-Hot-Encoding vorliegen.

Die sparse_categorical_crossentropy verwenden Sie ebenfalls bei der Klassifikation mit multiplen Klassen, aber hier liegen die Labels als Integer-Werte vor, also z.B. [1, 5, 3, 0] etc. Achten Sie darauf, dass die Indizes der Klassen bei 0 beginnen.

1.2 Lernrate dynamisch anpassen

Mit LearningRateScheduler kann man die Lernrate dynmisch während des Trainings anpassen. Oft möchte man mit einer hohen Lernrate wie 0.1 oder 0.01 beginnen und später die Lernrate reduzieren, z.B. um Faktor 10 verkleinern. Die Idee ist, dass man sich in der Fehlerlandschaft erst schnell dem Minimum nähert und in der Nähe des Minimums langsamer wird, um nicht um das Minimum herum zu oszillieren.

Funktion zur Anpassung der Lernrate

In Keras definiert man zunächst eine Funktion mit zwei Parametern. Parameter 1 ist die aktuelle Epochenzahl, Parameter 2 die aktuelle Lernrate. Die Funktion gibt die neue Lernrate zurück.

Hier ein Beispiel, wo die Lernrate bei Epoche 10, 15, 20 ... um jeweils Faktor 10 veringert wird (also z.B. 0.1, 0.01, 0.001...)

def scheduler(epoch, lr):
    if epoch >= 10 and epoch%5 == 0:
        return lr/10.0
    else:
        return lr

Überwachungsobjekt

Die obige Funktion wird in ein Objekt des Typs LearningRateScheduler (Paket keras.callbacks) gepackt. Als zweites Argument kann man einen Wert für verbose angeben (0: ohne Meldungen, 1: mit Meldungen).

Sie müssen dazu den LearningRateScheduler importiert haben:

from keras.callbacks import LearningRateScheduler

Anschließend kann man es für das Training mit fit der Liste von Callback-Funktion hinzufügen. Hier mit verbose=1:

model.fit(..., callbacks=[LearningRateScheduler(scheduler, 1)])

Keras-Doku

https://keras.io/api/callbacks/learning_rate_scheduler

1.3 Regularisierung und Weight Decay

Wir haben L2-Regularisierung in Kapitel 4 als Teil der Fehlerfunktion definiert. Dort wurde bereits angemerkt, dass dies dazu dient, Overfitting zu mildern. Regularisierung bedeutet, dass hohe Gewicht ganz allgemein "bestraft" werden. So wird erreicht, dass keine zu großen Unterschiede unter den Gewichten entstehen. Wenn in einer Publikation von weight decay gesprochen wird, ist damit Regularisierung gemeint.

In Keras kann man L1- oder L2-Regularisierung verwenden oder beides gleichzeitig. Die relevanten Klassen finden sich im Paket keras.regularizers:

Man kann auch eigene Regularisierer über eine Funktion und eine Unterklasse von Regularizer definieren.

Die Regularisierung für jede Schicht spezifiziert. Man kann einen String Identifier übergeben:

layer = Dense(30, kernel_regularizer='l2')

Alternativ als Objekt, dann kann man den Regularisierungsfaktor (bei uns $\lambda$) angeben:

layer = Dense(30, kernel_regularizer=l2(l2=0.01))

Bei l1_l2 hat man zwei Argumente:

layer = Dense(30, kernel_regularizer=l1_l2(l1=0.01, l2=0.01))

Neben kernel_regularizer gibt es noch

Keras-Doku

https://keras.io/api/layers/regularizers

Artikel: Keras Weight Decay Hack

1.4 Batch Normalization

Batch Normalization ist ein wirkungsvolles Verfahren, um Overfitting abzuschwächen. Man kann es auch als Alternative zu Dropout betrachten.

Wir haben gesehen, dass es sinnvoll ist, die Featurevektoren unserer Trainingsdaten zu skalieren, entweder durch Normalisierung (auf den Bereich [0, 1]) oder durch Standardisierung (mit Hilfe von Mittelwert und Varianz).

Die Grundidee von Batch Normalization ist es nun, etwas ähnliches für die Ausgabewerte jeder Schicht zu tun. Das heißt zum Beispiel für Schicht $l$, dass wir alle berechneten Werte $z^l$ (oder $a^l$) nehmen und zunächst skalieren, bevor sie in die nächste Schicht $l+1$ gehen, so dass die weitere Verarbeitung (d.h. das Lernen der Gewichte) schneller/kontrollierter vonstatten geht. Man nimmt tatsächlich die Rohinputs $z^l$ (nicht die Aktivierungen $a^l$) und skaliert mit Hilfe von Mittelwert und Varianz. Diese Mittelwerte werden in der Praxis auf Mini-Batches berechnet und nicht dem gesamten Trainingsdatensatz. Wichtig ist noch, dass für das Verfahren zwei neue Parameter pro Schicht hinzukommen: Gamma $\gamma$ und Beta $\beta$. Durch den Parameter $\beta$ wird das entsprechende Bias-Neuron der jeweiligen Schicht überflüssig, so dass bei Anwendung von Batch Normalization die Bias-Gewichte weggelassen werden.

Das Thema wurde angeregt durch einen Artikel von Abhijeet Kumar (CNN-Netz für den Datensatz CIFAR-10): Achieving 90% accuracy in Object Recognition Task on CIFAR-10 Dataset with Keras

Die Methode Batch Normalization kommt von Ioffe und Szegedy (2015), beide von Google. Hier das Abstract des Papers (Hervorhebungen von mir):

Training Deep Neural Networks is complicated by the fact that the distribution of each layer’s inputs changes during training, as the parameters of the previous layers change. This slows down the training by requiring lower learning rates and careful parameter initialization, and makes it notoriously hard to train models with saturating nonlinearities. We refer to this phenomenon as internal covariate shift, and address the problem by normalizing layer inputs. Our method draws its strength from making normalization a part of the model architecture and performing the normalization for each training mini-batch. Batch Normalization allows us to use much higher learning rates and be less careful about initialization. It also acts as a regularizer, in some cases eliminating the need for Dropout. Applied to a state-of-the-art image classification model, Batch Normalization achieves the same accuracy with 14 times fewer training steps, and beats the original model by a significant margin. Using an ensemble of batch-normalized networks, we improve upon the best published result on ImageNet classification: reaching 4.9% top-5 validation error (and 4.8% test error), exceeding the accuracy of human raters.

Hier ist ein guter Artikel, inklusive eines Benchmarking: One simple trick to train Keras model faster with Batch Normalization

Der Autor weist auf Folgendes hin:

Er zeigt dann, wie das für FC-Schichten und Konv-Schichten aussehen muss:

FC-Schicht normalerweise:

model.add(layers.Dense(64, activation='relu'))

FC-Schicht mit Batch Normalization:

model.add(layers.Dense(64, use_bias=False))
model.add(layers.BatchNormalization())
model.add(Activation("relu"))

Konv-Schicht normalerweise:

model.add(layers.Conv2D(64, (3, 3), activation='relu'))

Konv-Schicht mit Batch Normalization:

model.add(layers.Conv2D(64, (3, 3), use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.Activation("relu"))

Anmerkung: In dem Artikel Achieving 90% accuracy in Object Recognition Task on CIFAR-10 Dataset with Keras wird teils anders vorgegangen - unbedingt vergleichen.

Formal

Die Standardisierung wird über einen Batch vorgenommen. Ein Batch ist einfach eine Teilmenge der Trainingsdaten. Mit $m$ bezeichnen wir die Größe des Batches. Die Ausgangslage besteht also aus $m$ Aktivierungsvektoren, z.B. $z^1, \ldots, z^{32}$ für den ersten Batch bei einer Batchgröße von 32. Wir bezeichnen mit $B$ die Menge der Indizes eines Batches und berechnen zunächst Mittelwert und Varianz - nur für diesen einen Batch:

$$ \begin{align} \mu &= \frac{1}{m} \sum_{k \in B} z^k \\[3mm] \sigma^2 & =\frac{1}{m} \sum_{k \in B} (z^k - \mu_B)^2 \end{align} $$

Im Grunde tut man dies für jede Komponente eines Vektors $z$ separat, d.h. wir können auch die Komponenten der Vektoren betrachten. Wir nennen $n$ die Anzahl der Neuronen in der aktuellen Schicht. Dann gilt für $i \in {1, \ldots, n}$:

$$ \begin{align} \mu_i &= \frac{1}{m} \sum_{k \in B} z_i^k \\[3mm] \sigma_i^2 & =\frac{1}{m} \sum_{k \in B} (z_i^k - \mu_i)^2 \end{align} $$

Jetzt verändern wir die Aktivierungswerte $z_i^k$ für alle $k$ in dem Batch $B$ und nennen sie $\hat{z}_i$:

$$ \hat{z}_i^k = \frac{x_i^k - \mu_i}{\sqrt{\sigma_i^2 + \epsilon}} $$

Der endgültige neue Wert $\tilde{z}_i^k$ wird noch einmal skaliert - mit Faktor $\gamma$ - und verschoben - mit dem Wert $\beta$.

$$ \tilde{z}_i^k = \gamma \hat{z}_i^k + \beta = BN_{\gamma, \beta}(x_i^k) $$

Bei der Forward Propagation werden nach Verarbeitung eines Batch alle Aktivierungen $z$ durch $\tilde{z}$ ausgetauscht. Im Lernschritt werden auch $\gamma$ und $\beta$ mit Hilfe von Gradientenabstieg angepasst, d.h. die Parameter $\gamma$ und $\beta$ werden auch gelernt.

Keras-Doku

https://keras.io/api/layers/normalization_layers/batch_normalization

Videos

Andrews Ng beschreibt das Verfahren in den folgenden drei Videos:

Publikation

Sergey Ioffe, Christian Szegedy (2015) Batch normalization: accelerating deep network training by reducing internal covariate shift. In: Proceedings of the 32nd International Conference on International Conference on Machine Learning (ICML), pp. 448–456.

2 Arbeiten mit Text (NLP)

Keras stellt ein paar wichtige Funktionen bereit, um Textdaten aufzubereiten. Wir stellen hier insbesondere den Tokenizer vor.

Siehe auch: https://keras.io/api/preprocessing/text

2.1 Tokenizer

Ein Tokenizer ist ein Programm, das Texte in logische Einheiten zerlegt, z.B. in Wörter. Diese Einheiten nennt man dann Tokens. Der Begriff kommt sowohl aus dem Bereich natürlicher Sprachen (NLP) als auch aus dem Bereich Computersprachen/Compilerbau. In unserem Fall sind die Tokens alle Wörter, wobei Groß-/Kleinschreibung ignoriert wird. Satzzeichen sind keine Tokens und werden ignoriert.

Ein Tokenizer wird auf einer Menge von Texten "trainiert" mit fit_on_words. Der Tokenizer baut einen Wortindex auf: jedes Wort (= Token) wird mit einer eindeutigen Zahl assoziiert.

Man gibt dem Tokenizer i.d.R. über num_words eine Grenze $M$ mit, wie viele Tokens aufgenommen werden sollen. Es werden dann nur die $M$ häufigsten Tokens verwendet (im Wortindex sind dennoch alle Wörter vertreten, aber später in den Sequenzen nicht mehr).

Hier sehen wir, dass "Ich" und "nicht" repräsentiert werden, aber nicht "nicht", weil dieses Wort nicht unter den 4 häufigsten Wörtern war.

Jetzt können wir das als Bag-of-words-Repräsentation ausgeben. Dies ist ein Vektor der Länge 4. Nur bei solchen Indizes, bei denen das entsprechende Wort im Satz vorkommt, steht eine Zahl (z.B. 1), bei allen anderen Null.

Im Modus binary steht bei den vorkommeneden Wörtern eine 1, egal wie häufig sie vorkommen. Hier sehen wir zwei Einträge für "ich" und "ging".

Im Modus count steht die Häufigkeit der Vorkommen als absolute Häufigkeit (hier die 2 für "ging").

Im Modus freq wird die relative Häufigkeit genommen, d.h. die Zahl der Vorkommen von Wort $w$ wird durch die Gezamtzahl aller vorkommenden Wörter geteilt (hier einmal 2/3 für "ging" und einmal 1/3 für "ich").

2.2 Padding bei Sequenzen

Um eine fixe Sequenzlänge zu erzwingen, gibt es die Keras-Funktion pad_sequences.

Eine Sequenz ist hier eine Reihe von Zahlen. Wir stellen zwei Sequenzen mit Hilfe unseres Tokenizers her.

Hier kürzen wir die Sequenzen auf Länge 2.

Hier erweitern wir die Sequenzen, so dass sie Länge 5 haben.

Eine gute Darstellung findet man hier: https://machinelearningmastery.com/data-preparation-variable-length-input-sequences-sequence-prediction

3 Speichern und Laden von Netzen

Da Sie mittlerweile wissen, wie lang es dauern kann, ein Netz zu trainieren, kann es sinnvoll sein, trainierte Netze routinemäßig zu speichern. Auch die Trainingshistorie ist i.d.R. interessant, muss aber separat gespeichert werden.

Siehe auch den Artikel How to Save and Load Your Keras Deep Learning Model von Jason Brownlee (2019).

Netz speichern und laden

Beim Speichern muss man unterscheiden: will man nur die Netzwerkstruktur speichern (also ungefähr das, was man sieht, wenn man summary auf dem Modell aufruft) oder will man das trainierte Netz mit allen Parametern (insbes. die Gewichte) speichern.

Es gibt grundsätzlich drei Speicherformate: JSON, YAML und HDF5.

Siehe auch die Keras-Doku Model saving & serialization APIs.

JSON und YAML (Netzstruktur)

JSON und YAML sind allgemeine und recht bekannte Text-basierte Formate, d.h. sie sind auch menschenlesbar. Bei diesen Formaten wird aber nur die Netzstruktur gespeichert, also nicht die trainierten Parameter. Für diese Formate gibt es die Methoden to_json() und to_yaml() der Klasse Sequential (model), die einen String zurückgeben. Diesen kann man dann in eine Datei schreiben:

with open('model.json', 'w') as json_file:
    json_file.write(model.to_json(indent=3))

Durch das indent=3 wird das JSON eingerückt gespeichert, was die Menschlesbarkeit deutlich erhöht.

Insbesondere JSON ist ein weit verbreitetes und intuitiv lesbares Fileformat. Es bildet relativ offensichtlich eine assoziative Liste ab. Eventuell lohnt es sich, neben Netzstruktur und Parametern (s.u.) auch noch weitere Infos wie Lernrate, Optimizer etc. in einem eigenen JSON-Format.

Mehr zu JSON in Python: https://stackabuse.com/reading-and-writing-json-to-a-file-in-python

HDF5 (Netzstruktur und trainierte Parameter)

Will man auch die trainierten Parameter speichern, wählt man HDF5. Dies ist ein binäres Format. Dazu ruft man auf dem Modell (Sequential) die Methode save (Pfad als Parameter) auf. Die Dateiendung ist .h5.

model.save('model.h5')

Umgekehrt lädt man ein Modell mit Methode load_model (und Pfad) und bekommt ein Modell (Sequential) zurück.

from tensorflow.keras.models import load_model
m = load_model(model_fn)
m.summary()

Besonders interessant ist, dass in der HDF5-Datei folgende Infos gespeichert werden:

Das heißt, man könnte mit einem geladenen Netz das Training fortführen.

Trainingshistorie speichern

Wenn Sie das Modell mit fit trainieren, bekommen Sie ein History-Objekt zurück, dass in der Instanzvariablen history einen assoziativen Array mit allen Trainingsverläufen (acc, loss, etc.) enthält. Wenn es darum geht, verschiedene Netzarchitekturen oder Trainingsvarianten (Hyperparameter) zu vergleichen, ist gerade die Historie wichtig. Daher liegt es nahe, auch diese zu speichern.

Diesen Array können Sie mit pickle als Python-Objekt speichern (binäres Format):

import pickle
pickle.dump(history.history, open('history.pickle', "wb" ))

Das Laden funktioniert ähnlich:

hist = pickle.load(open('history.pickle', 'rb'))

Achten Sie darauf, dass hier das Array (history.history) zurückkommt und nicht das Histroy-Objekt.

Siehe auch die Python-Wike-Doku Using Pickle.

Alle Information in einem Verzeichnis

Wenn Sie ausführliche Experimente fahren, lohnt es sich, darüber nachzudenken, ob Sie nicht alle Infos zu einem Netz in einem Verzeichnis speichern, wo Sie z.B. Netzstruktur in JSON, trainiertes Netz in HDF5 und Historie als Pickle-Datei speichern. Zusätzlich könnten Sie einige Meta-Daten in einem eigenen JSON-Format dort ablegen. Überlegen Sie sich ein einfaches Namensschema, um das Netz/Verzeichnis zu benennen.