Updates dieser Seite:

  • 20.03.2022: v1 neues Semester

Ü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 Grundlegende Themen

1.1 Wahl der Loss-Funktion (binär vs. multiple Klassen)

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

  • Architektur
  • Gewichte
  • Trainingskonfiguration (das, was man bei 'compile' festlegt: loss und metrics)
  • Optimierungszustand

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.

1.3 Regularisierung (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:

  • l1
  • l2
  • l1_l2 (verwendet sowohl L1 als auch L2)

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

  • bias_regularizer: Regularizer to apply a penalty on the layer's bias
  • activity_regularizer: Regularizer to apply a penalty on the layer's output

Keras-Doku

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

Artikel: Keras Weight Decay Hack

1.4 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).

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

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.

In [1]:
s1 = 'Harry ging nach Berlin, ich nicht.'
s2 = 'Ich ging nach Hause?'

texte = [s1, s2]

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

In [2]:
from tensorflow.keras.preprocessing.text import Tokenizer

tok = Tokenizer(num_words=4)
tok.fit_on_texts(texte)

tok.word_index
Out[2]:
{'ging': 1,
 'nach': 2,
 'ich': 3,
 'harry': 4,
 'berlin': 5,
 'nicht': 6,
 'hause': 7}

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.

In [3]:
tok.texts_to_sequences(['Ich ging nicht dorthin.'])
Out[3]:
[[3, 1]]

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

In [4]:
tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='binary')
Out[4]:
array([[0., 1., 0., 1.]])

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

In [7]:
tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='count')
Out[7]:
array([[0., 2., 0., 1.]])

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").

In [8]:
tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='freq')
Out[8]:
array([[0.        , 0.66666667, 0.        , 0.33333333]])

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.

In [5]:
seq = tok.texts_to_sequences(texte)
seq
Out[5]:
[[1, 2, 3], [3, 1, 2]]

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

In [6]:
from tensorflow.keras.preprocessing import sequence

sequence.pad_sequences(seq, maxlen=2)
Out[6]:
array([[2, 3],
       [1, 2]], dtype=int32)

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

In [7]:
sequence.pad_sequences(seq, maxlen=5)
Out[7]:
array([[0, 0, 1, 2, 3],
       [0, 0, 3, 1, 2]], dtype=int32)

3 Fortgeschrittene Themen

In diesem Abschnitt finden Sie Themen, die eigentlich mehr Hintergrund benötigen und hier nur angerissen werden. Es wird auf externe Quellen verwiesen, wo Sie das jeweilige Thema vertiefen können.

3.1 Batch Normalization

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:

  • Batch normalization uses weights as usual but does NOT add a bias term. This is because its calculations include gamma and beta variables that make the bias term unnecessary.

  • We add the normalization before calling the activation function.

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.

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.