import tensorflow
import math
from math import exp
import matplotlib.pyplot as plt
import numpy as np
import time
5 Einführung in Keras
Im praktischen Teil werden wir hauptsächlich mit Keras - innerhalb von TensorFlow - arbeiten. Wir lernen, wie man Feedforward-Netze in Keras erstellt, trainiert und evaluiert. Wir starten mit den Iris-Daten und einem sehr einfachen Netz. Dann lernen wir die MNIST-Daten zur Erkennung von handgeschriebenen Ziffern kennen und erstellen und trainieren ein FFN mit mehreren Schichten.
Nach Abschluss des Kapitels können Sie
- Feedforwardnetze in Python mit Hilfe der Keras-Bibliothek erstellen, mit Daten aus Keras trainieren und den Trainingsverlauf visualisieren
- verschiedene Hyperparameter-Optionen (Lernrate, Batchgröße etc.) benennen und ihre Bedeutung in konkreten Trainingssituationen beurteilen
Datensatz
Name | Daten | Anz. Klassen | Klassen | Trainings-/Testdaten | Ort |
---|---|---|---|---|---|
Iris | Blütenblatt-Maße | 3 | Blumenklassen | 150 | |
MNIST | s/w-Bilder (28x28) | 10 | handgeschriebene Ziffern 0…9 | 60000/10000 | 5.4 |
Importe
5.1 Hintergrund
5.1.1 TensorFlow
TensorFlow ist eine Open-Source-Bibliothek für Python (mit Bestandteilen in C++) für den Themenbereich des Maschinellen Lernens, inklusive Neuronaler Netze. TensorFlow wurde ursprünglich vom Google Brain Team für Google-eigenen Projekte entwickelt, wurde aber 2015 unter einer Open-Source-Lizenz (Apache License 2.0) für die Allgemeinheit frei gegeben. Mittlerweile dürfte TensorFlow das mit Abstand das wichtigste Framework im Bereich Maschinelles Lernen sein.
Noch ein paar Worte zum Entwicklungsteam hinter TensorFlow: Google Brain begann 2011 als ein sogenanntes “Google X”-Projekt als Kooperation zwischen Google-Fellow Jeff Dean und Stanford-Professor Andrew Ng. Seit 2013 ist Deep-Learning-Pioneer Geoffrey Hinton als leitender Wissenschaftler im Team. Weitere bekannte Wissenschaftler bei Google Brain sind Alex Krizhevsky und Ilya Sutskever (Autoren von AlexNet), Christopher Olah (bekannt durch seinen Blog) und Chris Lattner (Erfinder von Apple’s Programmiersprache Swift).
5.1.2 Keras
Keras ist ebenfalls eine Open-Source-Bibliothek für Python von François Chollet, die erstmals 2015 als Python-API für verschiedene “Backends” (u.a. TensorFlow) veröffentlicht wurde. Es ist ein objektorientiertes Framework für das Erstellen, Trainieren und Evaluieren Neuronaler Netze. Das Buch Deep Learning with Python (Chollet 2021) ist vom Erfinder von Keras und daher von besonderer Relevanz (einschränkend muss man sagen, dass die Theorie in dem Buch ein wenig zu kurz kommt). Eine Idee von Keras ist, dass es auch als Interface für verschiedene Backends in anderen Kontexten (z.B. Robotik oder mobile) genutzt werden kann. Daher liegt ein Fokus von Keras auf einer intuitiven, modularen und erweiterbaren Systematik.
Keras wurde unabhängig von TensorFlow entwickelt, wurde dann aber 2017 in TensorFlow 1.4 als Teil der TensorFlow Core API aufgenommen, d.h. alle Konzepte und Daten von Keras stehen in TensorFlow zur Verfügung. François Chollet arbeitet seit 2015 für Google und hat dort auch die Einbindung in TensorFlow unterstützt.
5.1.3 PyTorch
PyTorch ist - wie TensorFlow - eine Open-Source-Bibliothek für Maschinelles Lernen in Python. PyTorch wurde ursprünglich von der KI-Gruppe bei Facebook, genannt FAIR - Facebook AI Research (oder oft nur “Facebook AI”) - entwickelt. PyTorch ist in letzter Zeit immer populärer geworden, weil es an manchen Stellen einen direkteren Eingriff in die Abläufe zulässt. In diesem Buch wird hauptsächlich Keras verwendet, aber PyTorch wird in den Anhängen E und F zumindest angerissen.
5.2 Einfaches Neuronales Netz
Wir beginnen mit dem einfachsten möglichen Neuronales Netz.
In Keras heißt die Basisklasse für Neuronale Netze Sequential
. Der Name weist darauf hin, dass ein Sequential-Objekt eine geordnete Reihe (Sequenz) von Schichten enthält.
Wir erstellen zunächst ein Neuronales Netz mit einer Schicht. Also stellen wir eine Instanz von Sequential
her, dies wäre unser Modell.
from tensorflow.keras.models import Sequential
= Sequential() model
5.2.1 Schichten Input und Dense
Jetzt kann man Schichten als Objekte dem Modell hinzufügen. Es gibt verschiedene Arten von Schichten.
Als erstes fügen wir die Neuronen für die Eingabe als Objekt der Klasse Input
hinzu. Diese “Schicht” hat keine Parameter, die zu lernen sind, und wird daher nicht als Schicht gezählt.
Unsere erste “richtige” Schicht ist ein Objekt vom Typ Dense
. Diese Schicht verbindet sich mit allen Neuronen der Vorgängerschicht. In anderen Kontexten nennt man solche Schichten auch fully connected layer oder FC layer. Unsere Dense-Schicht ist gleichzeitig auch die Ausgabeschicht und hat daher nur ein Neuron (der erste Parameter).
Bei unserer Dense-Schicht geben wir hier die Initialisierung von Gewichten mit kernel_initializer
und des Bias-Gewichts mit bias_initializer
an. In unserem Fall setzen wir alle Gewichte auf Null.
Mit activation
kann man die Aktivierungsfunktion spezifizieren. Wir wählen die Funktion sigmoid, also die logistische Funktion:
\[ g(z) = \frac{1}{1-e^{-z}} \]
Dies ist die typische Aktivierung für binäre Klassifikation.
from tensorflow.keras import Input
from tensorflow.keras.layers import Dense
= Sequential([
model =(2,)),
Input(shape1,
Dense(='zeros',
kernel_initializer='zeros',
bias_initializer='sigmoid')
activation ])
Unser Netz sieht derzeit so aus (das Bias-Neuron ist nicht grafisch repräsentiert):
Mit der Methode summary
geben wir uns eine Zusammenfassung unserer Netzwerk-Architektur aus, was später bei komplexeren Netzen hilfreich ist.
model.summary()
Model: "sequential_16"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense_20 (Dense) │ (None, 1) │ 3 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 3 (12.00 B)
Trainable params: 3 (12.00 B)
Non-trainable params: 0 (0.00 B)
Wir haben drei Parameter: \(w_1\), \(w_2\) und \(b\).
5.2.2 Parameter
Man kann das Netzwerkobjekt auch programmatisch untersuchen. Die Eigenschaft layers
enthält alle Schichtenobjekte in iterierbarer Form. Hier wenden wir die Methode get_config
auf jede Schicht an, die uns ein Dictionary zurückgibt, das wir hier ausgeben.
for l in model.layers:
for key in l.get_config():
print(f'{key}: {l.get_config()[key]}')
name: dense_20
trainable: True
dtype: {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}
units: 1
activation: sigmoid
use_bias: True
kernel_initializer: {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}
bias_initializer: {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}
kernel_regularizer: None
bias_regularizer: None
kernel_constraint: None
bias_constraint: None
Besonders interessant sind natürlich die Gewichte, zumindest bei kleineren Netzen (später werden die Gewichte nicht mehr handhabbar).
Mit der Methode get_weights
kann man sich die Gewichte und Bias-Gewichte aller Schichten ausgeben lassen.
= model.get_weights()
w, b print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [0.] [0.] BIAS [0.]
5.2.3 Iris-Datensatz
Wir möchten jetzt noch das Experiment mit den Iris-Daten mit einem Keras-Netz wiederholen.
Wir bereiten die Daten vor wie im Abschnitt 3.4.1.
from sklearn import datasets
= datasets.load_iris() iris
Wir reduzieren die Features und Zielwerte auf zwei Klassen.
= iris.data.T
features = features[[0,2]]
features = features.T
x_iris 5] # Testausgabe x_iris[:
array([[5.1, 1.4],
[4.9, 1.4],
[4.7, 1.3],
[4.6, 1.5],
[5. , 1.4]])
= x_iris[:100] # nur die ersten 100 Elemente
x_iris = iris.target[:100]
y_iris y_iris
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Jetzt schauen wir uns die Daten nochmal an:
0][:50], features[1][:50], color='red', marker='o', label='setosa')
plt.scatter(features[0][50:], features[1][50:], color='blue', marker='x', label='versicolor')
plt.scatter(features['sepal length')
plt.xlabel('petal length')
plt.ylabel(='upper left')
plt.legend(loc plt.show()
5.2.4 Training
Damit man ein Keras-Netzwerk trainieren kann, muss es zunächst mit der Methode compile
konfiguriert werden.
Konfiguration mit Compile
Mit compile
legen wir Hyperparameter wie die Lernrate und die Optimierungsmethode fest, aber auch Metriken wie die Art der Zielfunktion und die Evaluationsmaße.
Wir verwenden hier stochastic gradient descent (SGD) als Optimierungsmethode, mit einer Lernrate von 0.1. Als Zielfunktion wählen wir MSE (gemittelte Fehlerquadrate) und als Evaluationsmaß Accuracy.
from tensorflow.keras.optimizers import SGD
= SGD(learning_rate=0.1)
sgd
compile(optimizer=sgd,
model.='mean_squared_error',
loss=['acc'])
metrics
# Alternative:
# Hier benötigt man das Objekt in sgd nicht, kann allerdings auch
# nicht die Lernrate einstellen
#
# model_and.compile(optimizer='sgd', loss='mean_squared_error', metrics=['acc'])
Wie wir später noch sehen, muss man nicht unbedingt ein Objekt für den Optimizer herstellen, sondern kann auch einfach in der Methode compile optimizer=‘sgd’ angeben. Das Vorgehen hier benötigt man nur, wenn man den Optimizer konfigurieren will (wie hier mit der Lernrate).
Training mit Lernrate 0.1
Das eigentliche Training, also die Updates der Paramter mit Hilfe der Trainingsdaten, findet in der Methode fit
statt. Hier geben wir die Featurevektoren an, die Zielwerte und die Anzahl der Epochen, also die Trainingsdauer.
= model.fit(x_iris,
history
y_iris, =10) epochs
Epoch 1/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 0.6220 - loss: 0.2435
Epoch 2/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 0.8768 - loss: 0.2209
Epoch 3/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 0.9382 - loss: 0.1984
Epoch 4/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 0.5528 - loss: 0.1965
Epoch 5/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 0.9845 - loss: 0.1658
Epoch 6/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 1.0000 - loss: 0.1523
Epoch 7/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 1.0000 - loss: 0.1367
Epoch 8/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 1.0000 - loss: 0.1253
Epoch 9/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 1.0000 - loss: 0.1170
Epoch 10/10
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - acc: 1.0000 - loss: 0.1053
Ergebnis
Wir sehen uns die Entwicklung von Loss und Accuracy an.
Dazu definieren wir eine Hilfsfunktion:
def plot_loss_acc(history):
= range(len(history.history['acc']))
epochs = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
fig, ax 0].plot(epochs, history.history['loss'], 'r', label='Loss')
ax[0].set_xlabel('Epochen')
ax[0].set_ylabel('Loss')
ax[0].set_title('Loss')
ax[1].plot(epochs, history.history['acc'], 'b', label='Accuracy')
ax[1].set_xlabel('Epochen')
ax[1].set_ylabel('Accuracy')
ax[1].set_title('Accuracy')
ax[ plt.show()
plot_loss_acc(history)
Man sieht hier, dass das Netz mit Lernrate 0.1 bereits nach 6 Epochen 100% Accuracy erreicht.
Wir können uns noch die Gewichte ausgeben:
= model.get_weights()
w, b print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [-0.00348009] [0.25110418] BIAS [-0.01677915]
5.2.5 Vorhersage
Man kann mit predict
auch Vorhersagen mit dem trainierten Netz berechnen. Hier für 10 Werte aus der ersten Hälfte der 100 Datenpunkte (Klasse 0):
= model.predict(x_iris[:10])
pred pred
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 19ms/step
array([[0.3170182 ],
[0.31771424],
[0.29329982],
[0.3438687 ],
[0.3173662 ],
[0.39130548],
[0.31875828],
[0.34247664],
[0.31945428],
[0.34282467]], dtype=float32)
Da wir kontinuierliche Werte bekommen, legen wir eine Schwellwertfunktion an, um die Ausgabe auf die Werte {0, 1} zu zwingen.
1 if x>=0.5 else 0) for x in pred] [(
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Hier für Werte aus der zweiten Hälfte der Daten (Klasse 1):
= model.predict(x_iris[50:60])
pred 1 if x>=0.5 else 0) for x in pred] [(
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 21ms/step
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
5.3 Feedforward-Netze
Unser erstes Netz hatte lediglich eine parametrisierte Schicht und ein Ausgabeneuron.
Jetzt sehen wir uns an, wie man FNN mit Zwischenschichten erstellt, das für die Klassifikation von 10 Klassen geeignet ist.
5.3.1 Verlustfunktion in Keras
Bevor man ein Modell in Keras mit fit
trainiert, legt man mit compile
die Verlustfunktion fest (siehe dazu auch Abschnitt 4.2.1), z.B. so:
model.compile(loss='categorical_crossenropy', optimizer='sgd')
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.
Bislang haben wir die Labels immer in One-Hot-Encoding umgewandelt und dann categorical_crossentropy benutzt.
5.4 MNIST-Datensatz
Im MNIST-Datensatz geht es Erkennung von handgeschriebenen Ziffern von 0 bis 9. Es gibt zehn Klassen, die es vorherzusagen gilt: 0, 1, 2, … 9. Die Bilder haben die Auflösung 28x28 und liegen in Graustufen vor. Der Datensatz enthält 60000 Trainingsbeispiele und 10000 Testbeispiele.
Wir ziehen den Datensatz MNIST hinzu. MNIST steht für Modified National Institute of Standards and Technology (dataset). Siehe auch den Wikipedia-Artikel zu MNIST und die Keras-Doku zu MNIST.
Eine Einführung in die MNIST-Daten in Keras finden Sie im folgenden Video.
5.4.1 Daten einlesen
Die Daten kommen bereits separiert in Trainings- und Testdaten.
from tensorflow.keras.datasets import mnist
= mnist.load_data() (x_train, y_train), (x_test, y_test)
Wir sehen uns die Struktur an. Die Bilder sind als 28x28-Matrizen hinterlegt.
print('x_train: ', x_train.shape)
print('y_train: ', y_train.shape)
print('x_test: ', x_test.shape)
print('y_test: ', y_test.shape)
x_train: (60000, 28, 28)
y_train: (60000,)
x_test: (10000, 28, 28)
y_test: (10000,)
5.4.2 Daten visualisieren
Die Funktion imshow (image show) erlaubt uns das schnelle Betrachten der Bilder. Wir wählen als color map (cmap) Graustufen.
Erstes Trainingsbild:
0], cmap='gray') plt.imshow(x_train[
<matplotlib.image.AxesImage at 0x3060bce10>
Zehntes Trainingsbild:
9], cmap='gray') plt.imshow(x_train[
<matplotlib.image.AxesImage at 0x30606df50>
Hier geben wir von jeder Kategorie das erste Trainingsbeispiel aus:
= plt.subplots(nrows=2, ncols=5, sharex=True, sharey=True,)
fig, ax = ax.flatten()
ax for i in range(10):
= x_train[y_train == i][0]
img ='gray')
ax[i].imshow(img, cmap
0].set_xticks([]) # keine Achsenmarkierungen
ax[0].set_yticks([])
ax[
plt.tight_layout() plt.show()
5.4.3 Daten vorverarbeiten
Für unsere Zwecke möchten wir die Bildmatrizen linearisieren zu Vektoren der Länge 784 (= 28*28). Dazu verwenden wir reshape.
Außerdem möchten wir die Pixelwerte (0..255) auf das Interval [0, 1] normalisieren. Das erreichen wir, indem wir alle Werte durch 255.0 teilen.
= x_train.reshape(60000, 784)/255.0
x_train = x_test.reshape(10000, 784)/255.0 x_test
Kleiner Check, ob die Linearisierung geklappt hat:
print('x_train: ', x_train.shape)
print('x_test: ', x_test.shape)
x_train: (60000, 784)
x_test: (10000, 784)
5.4.4 One-Hot Encoding
Um die zehn Ziffern 0, , 9 in der Ausgabe zu kodieren, benutzen wir One-Hot Encoding, d.h. jede Ziffer wird durch einen Vektor der Länge 10 repräsentiert:
\[ \left( \begin{array}{c} 1 \\0 \\ \vdots \\ 0\end{array} \right) \quad \left( \begin{array}{c} 0 \\1 \\ \vdots \\ 0\end{array} \right) \quad \ldots\quad \left( \begin{array}{c} 0 \\0 \\ \vdots \\ 1\end{array} \right) \]
In Keras gibt es dafür die Funktion to_categorical
. Wir schreiben das Ergebnis in neue Variablen.
from tensorflow.keras.utils import to_categorical
= to_categorical(y_train, 10)
y_train_1hot = to_categorical(y_test, 10) y_test_1hot
Wir prüfen, ob die Label-Daten die richtige Form haben.
print("y_train: ", y_train_1hot.shape)
print("y_test: ", y_test_1hot.shape)
y_train: (60000, 10)
y_test: (10000, 10)
Das passt: Es sind Vektoren der Länge 10.
5.5 Netz mit zwei Schichten
Wir bauen jetzt ein Neuronales Netz mit einer “versteckten” Schicht (hidden layer) mit 60 Neuronen.
Das Netz benötigt für die MNIST-Daten eine Eingabeschicht mit 784 Neuronen und hat zwei parametrisierte Schichten:
- Versteckte Schicht mit 60 Neuronen
- Ausgabeschicht mit 10 Neuronen
Schematisch sieht das so aus (ohne Bias-Neuronen):
5.5.1 Modell
In Keras erstellen wir das Netz als Instanz der Klasse Sequential
.
Neben der Eingabe fügen dem Objekt zwei parametrisierte Schichten vom Typ Dense
(fully connected) hinzu. Wir wählen die Sigmoid-Funktion für die Aktivierung der versteckten Schicht. Bei der Ausgabeschicht geben wir an, dass wir die Softmax-Funktion anwenden möchten.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
= Sequential()
model =(784,)))
model.add(Input(shape60,
model.add(Dense(='sigmoid'))
activation10,
model.add(Dense(='softmax')) activation
5.5.2 Anzahl der Parameter
Wie viele Parameter hat das Netz? Die Gewichtsmatrix \(W^{(1)}\) von Input- zu versteckter Schicht hat 784x60 Einträge:
784*60
47040
Es kommen 60 Gewichte für das Bias-Neuron dazu. Insgesamt also 47100 Parameter. Die Gewichtsmatrix \(W^{(2)}\) von versteckter zu Ausgabeschicht enthält 10x60 = 600 Einträge plus den 10 Gewichten für das Bias-Neuron zur Ausgabe, macht 610 Parameter. Alles in allem also 47710 Parameter.
Wir schauen uns das Netz in der Kurzübersicht mit Hilfe der Methode summary
an. Dort steht auch nochmal die Anzahl der Parameter:
model.summary()
Model: "sequential_11"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense_12 (Dense) │ (None, 60) │ 47,100 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_13 (Dense) │ (None, 10) │ 610 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 47,710 (186.37 KB)
Trainable params: 47,710 (186.37 KB)
Non-trainable params: 0 (0.00 B)
Einen etwas technischeren Blick auf Ihr Modell mit allen Schichten erhalten Sie mit get_config
. Hier sehen Sie zum Beispiel verschiedene Einstellungen in den Schichten, z.B. zur Initialisierung der Gewichte.
model.get_config()
{'name': 'sequential_11',
'trainable': True,
'dtype': {'module': 'keras',
'class_name': 'DTypePolicy',
'config': {'name': 'float32'},
'registered_name': None},
'layers': [{'module': 'keras.layers',
'class_name': 'InputLayer',
'config': {'batch_shape': (None, 784),
'dtype': 'float32',
'sparse': False,
'ragged': False,
'name': 'input_layer_11'},
'registered_name': None},
{'module': 'keras.layers',
'class_name': 'Dense',
'config': {'name': 'dense_12',
'trainable': True,
'dtype': {'module': 'keras',
'class_name': 'DTypePolicy',
'config': {'name': 'float32'},
'registered_name': None},
'units': 60,
'activation': 'sigmoid',
'use_bias': True,
'kernel_initializer': {'module': 'keras.initializers',
'class_name': 'GlorotUniform',
'config': {'seed': None},
'registered_name': None},
'bias_initializer': {'module': 'keras.initializers',
'class_name': 'Zeros',
'config': {},
'registered_name': None},
'kernel_regularizer': None,
'bias_regularizer': None,
'kernel_constraint': None,
'bias_constraint': None},
'registered_name': None,
'build_config': {'input_shape': (None, 784)}},
{'module': 'keras.layers',
'class_name': 'Dense',
'config': {'name': 'dense_13',
'trainable': True,
'dtype': {'module': 'keras',
'class_name': 'DTypePolicy',
'config': {'name': 'float32'},
'registered_name': None},
'units': 10,
'activation': 'softmax',
'use_bias': True,
'kernel_initializer': {'module': 'keras.initializers',
'class_name': 'GlorotUniform',
'config': {'seed': None},
'registered_name': None},
'bias_initializer': {'module': 'keras.initializers',
'class_name': 'Zeros',
'config': {},
'registered_name': None},
'kernel_regularizer': None,
'bias_regularizer': None,
'kernel_constraint': None,
'bias_constraint': None},
'registered_name': None,
'build_config': {'input_shape': (None, 60)}}],
'build_input_shape': (None, 784)}
5.5.3 Gewichtsmatrizen und Biasvektoren
Mit get_weights
können wir auf die Parameter des Netzwerks zugreifen. Für jede Schicht gibt es jeweils
- eine Gewichtsmatrix und
- einen Bias-Vektor.
Wir lassen uns hier in einer Schleife jeweils die Dimensionen der Gewichtsmatrix und des Bias-Vektor für unsere zwei Schichten (Hidden + Output) ausgeben:
= model.get_weights()
weights for w in weights:
print(w.shape)
(784, 60)
(60,)
(60, 10)
(10,)
Wir sehen hier, dass die Gewichtsmatrix \(W^{(1)}\) von der Eingabeschicht zur versteckten Schicht die Dimensionen 784x60 hat. In unserer theoretischen Behandlung haben wir eine 60x784-Matrix verwendet, also die transponierte Version. Beide Varianten sind möglich und üblich.
Der Bias-Vektor von Input zu Hidden hat eine Länge von 60. Auch das ist plausibel, wenn Sie sich in der Abbildung oben ein Bias-Neuron in der Inputschicht vorstellen, dass mit allen 60 versteckten Neuronen verbunden ist. Dies muss offensichtlich 60 Gewichte haben.
5.5.4 Training
Beim Training wollen wir die ursprüngliche Variante des Backpropagation verwenden, wo alle Trainingsbeispiele vor einem Update durchlaufen werden. Wir nennen diese Variante Batchtraining (nicht zu verwechseln mit “Minibatch”, siehe unten).
In Keras spezifizieren wir dazu Stochastic Gradient Descent, kurz SGD, als Optimierungsmethode und geben als Batchgröße die Anzahl der Trainingsdaten an. Als Zielfunktion wählen wir wie im Theorieteil die Cross-Entropy-Funktion.
Lernrate
Damit wir die Lernrate einstellen können, erstellen wir für die Optimierungsmethode ein eigenes SGD-Objekt (stochastic gradient descent). Wir sehen uns mit get_config
die Voreinstellungen an.
from tensorflow.keras.optimizers import SGD
= SGD()
opt opt.get_config()
{'name': 'SGD',
'learning_rate': 0.009999999776482582,
'weight_decay': None,
'clipnorm': None,
'global_clipnorm': None,
'clipvalue': None,
'use_ema': False,
'ema_momentum': 0.99,
'ema_overwrite_frequency': None,
'loss_scale_factor': None,
'gradient_accumulation_steps': None,
'momentum': 0.0,
'nesterov': False}
Die Standardeinstellung für die Lernrate ist also 0.01. Wir erstellen ein neues Objekt mit unserer gewünschten Lernrate:
= SGD(learning_rate = 0.1)
opt opt.get_config()
{'name': 'SGD',
'learning_rate': 0.10000000149011612,
'weight_decay': None,
'clipnorm': None,
'global_clipnorm': None,
'clipvalue': None,
'use_ema': False,
'ema_momentum': 0.99,
'ema_overwrite_frequency': None,
'loss_scale_factor': None,
'gradient_accumulation_steps': None,
'momentum': 0.0,
'nesterov': False}
5.5.5 Compile
Mit compile
konfigurieren wir das Training zunächst nur und übergeben zum Beispiel die Optimiermethode als Objekt. Alternativ kann man als Parameter für optimizer
den String sgd
übergeben, dann werden für Lernrate etc. die Standardeinstellungen gewählt. Als Metrik für unsere Messungen wählen wir Accuracy, kurz acc
.
compile(optimizer=opt,
model.='categorical_crossentropy',
loss=['acc']) metrics
5.5.6 Fit
Das eigentliche Training wird mit fit
durchgeführt. Wichtig ist, dass fit
den Verlauf der Metriken pro Epoche in einem History-Objekt zurückgibt. Will man die Fehler- und Performance-Entwicklung später visualisieren, muss man dieses Objekt speichern.
Die Methode hat außer den Daten und Epochen noch ein paar Parameter:
- mit batch_size können Sie die Größe der Minibatches angeben (Standardmäßig 32)
- mit validation_data übergibt man Daten, auf denen nach jeder Epoche das Modell evaluiert wird; wir benutzen hier die echten Testdaten und keine speziellen Validierungsdaten
- mit verbose kann man die Ausgabe steuern (z.B. mit verbose=0 ausschalten).
Um Batchtraining zu realisieren, wählen wir eine Batchgröße von 60000, und bilden damit das originale Backpropagation nach, wo in einer Epoche alle Trainingsbeispiele verarbeitet werden, bevor ein Update der Gewichte durchgeführt wird (siehe auch Abschnitt 3.6).
Wir unterdrücken die Ausgabe (verbose=0), messen aber die Trainingsdauer mit Hilfe der Funktion time aus dem gleichnamigen Paket (gibt die Sekunden seit 1.1.1970 0:00 Uhr zurück).
= time.time() # Wir merken uns die Startzeit
start_time
= model.fit(x_train, y_train_1hot,
history1 =20,
epochs=60000,
batch_size=1,
verbose= (x_test, y_test_1hot))
validation_data
= time.time() - start_time # Wir berechnen die Dauer in Sek.
duration print(f'TRAININGSDAUER: {duration:.2f} Sek.')
Epoch 1/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 365ms/step - acc: 0.1263 - loss: 2.6268 - val_acc: 0.1396 - val_loss: 2.5047
Epoch 2/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 126ms/step - acc: 0.1390 - loss: 2.5075 - val_acc: 0.1486 - val_loss: 2.4209
Epoch 3/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 123ms/step - acc: 0.1485 - loss: 2.4239 - val_acc: 0.1586 - val_loss: 2.3608
Epoch 4/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 109ms/step - acc: 0.1595 - loss: 2.3639 - val_acc: 0.1774 - val_loss: 2.3165
Epoch 5/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 185ms/step - acc: 0.1747 - loss: 2.3196 - val_acc: 0.1989 - val_loss: 2.2828
Epoch 6/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 148ms/step - acc: 0.1938 - loss: 2.2860 - val_acc: 0.2239 - val_loss: 2.2563
Epoch 7/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 123ms/step - acc: 0.2166 - loss: 2.2596 - val_acc: 0.2496 - val_loss: 2.2348
Epoch 8/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 132ms/step - acc: 0.2389 - loss: 2.2382 - val_acc: 0.2746 - val_loss: 2.2168
Epoch 9/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 115ms/step - acc: 0.2609 - loss: 2.2202 - val_acc: 0.3000 - val_loss: 2.2011
Epoch 10/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 116ms/step - acc: 0.2862 - loss: 2.2046 - val_acc: 0.3245 - val_loss: 2.1871
Epoch 11/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 124ms/step - acc: 0.3078 - loss: 2.1907 - val_acc: 0.3444 - val_loss: 2.1742
Epoch 12/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 113ms/step - acc: 0.3283 - loss: 2.1780 - val_acc: 0.3628 - val_loss: 2.1622
Epoch 13/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 120ms/step - acc: 0.3477 - loss: 2.1661 - val_acc: 0.3819 - val_loss: 2.1508
Epoch 14/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 115ms/step - acc: 0.3681 - loss: 2.1548 - val_acc: 0.4023 - val_loss: 2.1398
Epoch 15/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 115ms/step - acc: 0.3889 - loss: 2.1440 - val_acc: 0.4267 - val_loss: 2.1292
Epoch 16/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 125ms/step - acc: 0.4130 - loss: 2.1334 - val_acc: 0.4552 - val_loss: 2.1187
Epoch 17/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 112ms/step - acc: 0.4423 - loss: 2.1231 - val_acc: 0.4839 - val_loss: 2.1084
Epoch 18/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 110ms/step - acc: 0.4717 - loss: 2.1130 - val_acc: 0.5097 - val_loss: 2.0982
Epoch 19/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 131ms/step - acc: 0.4985 - loss: 2.1029 - val_acc: 0.5344 - val_loss: 2.0881
Epoch 20/20
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 106ms/step - acc: 0.5211 - loss: 2.0930 - val_acc: 0.5524 - val_loss: 2.0781
TRAININGSDAUER: 3.06 Sek.
Die Trainingsdauer im Batchtraining ist mit 3 Sekunden für 20 Epochen extrem kurz.
5.5.7 Evaluation
Wir sehen uns die Entwicklung von Zielfunktion (Loss) und Accuracy an.
Dazu definieren wir vorab eine Hilfsfunktion.
def set_subplot(ax, y_label, traindata, testdata, ylim):
= range(1, len(traindata) + 1)
e_range 'b', label='Training')
ax.plot(e_range, traindata, 'g', label='Test')
ax.plot(e_range, testdata, 'Epochen')
ax.set_xlabel(
ax.set_ylabel(y_label)
ax.legend()
ax.grid()
ax.set_ylim(ylim) ax.set_title(y_label)
Achten Sie bei den Plots immer auf die Grenzen der y-Achse (im Code set_ylim). Diese werden immer so gewählt, dass man die Kurve möglichst gut sieht. Hier haben wir das Interval 0 bis 0.6 für die Accuracy. Weiter unten haben wir ein anderes Interval. Dies muss man beim Vergleich der Kurven natürlich beachten.
= plt.subplots(nrows=1, ncols=2, figsize=(15,4))
fig, ax
0], 'Loss', history1.history['loss'],
set_subplot(ax['val_loss'], [0, 3])
history1.history[1], 'Accuracy', history1.history['acc'],
set_subplot(ax['val_acc'], [0, 0.6])
history1.history[
plt.show()
Mit evaluate können wir die Performanz unseres Modells auf den Testdaten messen. Wir bekommen ein Tupel mit Fehler und Accuracy zurück.
= model.evaluate(x_test, y_test_1hot)
loss, acc print(f"Evaluation auf den Testdaten:\n\nLoss = {loss:.3f}\nAccuracy = {acc:.3f}")
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 411us/step - acc: 0.5153 - loss: 2.0928
Evaluation auf den Testdaten:
Loss = 2.078
Accuracy = 0.552
Mit Batchtraining erhalten wir nach 20 Epochen Training eine enttäuschende Accuracy von 55% auf den Testdaten.
5.6 Einfluss der Batchgröße
Mit Batchtraining (Batchgröße = 60000) haben wir ein enttäuschendes Ergebnis erzielt. Wir sehen uns jetzt das reine SGD und eine Variante von Minibatch-Training an. Beides wurde in Abschnitt 3.6 eingeführt.
5.6.1 Reines SGD (Batchgröße = 1)
Wenn wir die Batchgröße auf 1 setzen, erreichen wir “reines” Stochastic Gradient Descent, wo die Gewichte nach jedem Trainingsbeispiel angepasst werden.
Wir erzeugen eine neue Instanz des Netzwerks mit den gleichen Eigenschaften (20 versteckte Neuronen, gleiche Aktivierungsfunktionen). Anschließend trainieren wir es unter gleichen Bedingungen. Der einzige Unterschied ist die Batchgröße, die wir hier auf 1 setzen.
= Sequential()
model =(784,)))
model.add(Input(shape60,
model.add(Dense(='sigmoid'))
activation10, activation='softmax'))
model.add(Dense(
= SGD(learning_rate = 0.1)
opt
compile(optimizer=opt,
model.='categorical_crossentropy',
loss=['acc'])
metrics
= time.time()
start_time
= model.fit(x_train, y_train_1hot,
history2 =20,
epochs=1,
batch_size=1,
verbose= (x_test, y_test_1hot))
validation_data
= time.time() - start_time
duration print(f'TRAININGSDAUER: {duration:.2f} Sek.')
Epoch 1/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 415us/step - acc: 0.8890 - loss: 0.3600 - val_acc: 0.9579 - val_loss: 0.1419
Epoch 2/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 427us/step - acc: 0.9607 - loss: 0.1313 - val_acc: 0.9602 - val_loss: 0.1348
Epoch 3/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 411us/step - acc: 0.9680 - loss: 0.1009 - val_acc: 0.9672 - val_loss: 0.1119
Epoch 4/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 415us/step - acc: 0.9725 - loss: 0.0875 - val_acc: 0.9638 - val_loss: 0.1195
Epoch 5/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 439us/step - acc: 0.9776 - loss: 0.0722 - val_acc: 0.9678 - val_loss: 0.1107
Epoch 6/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 424us/step - acc: 0.9791 - loss: 0.0649 - val_acc: 0.9654 - val_loss: 0.1182
Epoch 7/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 418us/step - acc: 0.9790 - loss: 0.0646 - val_acc: 0.9691 - val_loss: 0.1167
Epoch 8/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 430us/step - acc: 0.9820 - loss: 0.0532 - val_acc: 0.9706 - val_loss: 0.1007
Epoch 9/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 415us/step - acc: 0.9820 - loss: 0.0533 - val_acc: 0.9713 - val_loss: 0.1050
Epoch 10/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 425us/step - acc: 0.9849 - loss: 0.0443 - val_acc: 0.9729 - val_loss: 0.1063
Epoch 11/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 439us/step - acc: 0.9864 - loss: 0.0408 - val_acc: 0.9686 - val_loss: 0.1206
Epoch 12/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 27s 453us/step - acc: 0.9866 - loss: 0.0383 - val_acc: 0.9701 - val_loss: 0.1169
Epoch 13/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 435us/step - acc: 0.9882 - loss: 0.0368 - val_acc: 0.9727 - val_loss: 0.1026
Epoch 14/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 27s 450us/step - acc: 0.9895 - loss: 0.0322 - val_acc: 0.9682 - val_loss: 0.1256
Epoch 15/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 440us/step - acc: 0.9905 - loss: 0.0306 - val_acc: 0.9723 - val_loss: 0.1112
Epoch 16/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 27s 444us/step - acc: 0.9893 - loss: 0.0304 - val_acc: 0.9729 - val_loss: 0.1156
Epoch 17/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 432us/step - acc: 0.9911 - loss: 0.0261 - val_acc: 0.9715 - val_loss: 0.1171
Epoch 18/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 25s 421us/step - acc: 0.9927 - loss: 0.0222 - val_acc: 0.9706 - val_loss: 0.1139
Epoch 19/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 28s 469us/step - acc: 0.9935 - loss: 0.0193 - val_acc: 0.9719 - val_loss: 0.1138
Epoch 20/20
60000/60000 ━━━━━━━━━━━━━━━━━━━━ 26s 429us/step - acc: 0.9950 - loss: 0.0156 - val_acc: 0.9747 - val_loss: 0.1144
TRAININGSDAUER: 518.57 Sek.
Wir sehen, dass das Training mit reinem SGD mit ca. 8 Minuten deutlich länger dauert als beim Batchtraining mit seinen 2 Sekunden - in beiden Fällen für 20 Epochen.
Das liegt daran, dass hier in jeder Epoche 60000 Mal sämtliche Gewichte angepasst werden, wohingegen vorher nur ein einziges Update pro Epoche stattfand.
Jetzt schauen wir uns die Performance an. Achten Sie auf den Wertebereich, der bei der Accuracy-Kurve zwischhen 0.9 und 1.0 liegt.
= plt.subplots(nrows=1, ncols=2, figsize=(15,4))
fig, ax
0], 'Loss', history2.history['loss'],
set_subplot(ax['val_loss'], [0, .5])
history2.history[1], 'Accuracy', history2.history['acc'],
set_subplot(ax['val_acc'], [0.9, 1])
history2.history[
plt.show()
Wir sehen einen typischen Verlauf der Accuracy: Auf den Trainingsdaten steigt die Kurve immer weiter an, aber auf den Testdaten bleibt die Kurve in einem bestimmten Bereich. Sobald die Kurve auf den Testdaten sich einpendelt, kann man das Training beenden, weil das Training anschließend Overfitting betreibt.
Wir schauen uns jetzt noch einmal den finalen Accuracy-Wert auf den Testdaten an:
= model.evaluate(x_test, y_test_1hot)
loss, acc print(f"Evaluation auf den Testdaten:\n\nLoss = {loss:.3f}\nAccuracy = {acc:.3f}")
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 406us/step - acc: 0.9691 - loss: 0.1334
Evaluation auf den Testdaten:
Loss = 0.114
Accuracy = 0.975
Mit 97% Accuracy auf den Testdaten erreicht das mit reinem SGD trainierte Netz einen deutlich besseren Wert als das vorigen Netz, dass wir mit Batchtraining trainiert haben.
5.6.2 Minibatch (Batchgröße = 32)
Nachdem wir Batchtraining (alle Trainingsbeispiele pro Update) und reines SGD (Batchgröße = 1) ausprobiert haben, setzen wir jetzt die eigentliche Idee von Minibatch um, dass eine bestimmte Anzahl von (zufällig ausgewählten) Trainingsdaten durchlaufen wird, bevor dann ein Update durchgeführt wird. Es handelt sich also um einen Kompromiss zwischen Batchtraining und reinem SGD.
Im Keras ist bei der Methode SGD eine Batchgröße von 32 standardmäßig eingestellt. Diese probieren wir jetzt aus. Den Parameter batch_size lassen wir weg, damit die Standardgröße von 32 genommen wird. Alles andere bleibt wie in den beiden anderen Versuchen gleich.
= Sequential()
model =(784,)))
model.add(Input(shape60,
model.add(Dense(='sigmoid'))
activation10, activation='softmax'))
model.add(Dense(
= SGD(learning_rate = 0.1)
opt
compile(optimizer=opt,
model.='categorical_crossentropy',
loss=['acc'])
metrics
= time.time()
start_time
= model.fit(x_train, y_train_1hot,
history3 =20,
epochs=1,
verbose= (x_test, y_test_1hot))
validation_data
= time.time() - start_time
duration print(f'TRAININGSDAUER: {duration:.2f} Sek.')
Epoch 1/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 614us/step - acc: 0.7758 - loss: 0.9306 - val_acc: 0.9147 - val_loss: 0.3091
Epoch 2/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 549us/step - acc: 0.9109 - loss: 0.3122 - val_acc: 0.9273 - val_loss: 0.2550
Epoch 3/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 476us/step - acc: 0.9259 - loss: 0.2600 - val_acc: 0.9368 - val_loss: 0.2267
Epoch 4/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 470us/step - acc: 0.9371 - loss: 0.2209 - val_acc: 0.9430 - val_loss: 0.2012
Epoch 5/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 507us/step - acc: 0.9430 - loss: 0.1975 - val_acc: 0.9484 - val_loss: 0.1818
Epoch 6/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 539us/step - acc: 0.9487 - loss: 0.1782 - val_acc: 0.9508 - val_loss: 0.1709
Epoch 7/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 483us/step - acc: 0.9520 - loss: 0.1671 - val_acc: 0.9534 - val_loss: 0.1565
Epoch 8/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 503us/step - acc: 0.9568 - loss: 0.1496 - val_acc: 0.9557 - val_loss: 0.1487
Epoch 9/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 473us/step - acc: 0.9606 - loss: 0.1377 - val_acc: 0.9599 - val_loss: 0.1398
Epoch 10/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 519us/step - acc: 0.9622 - loss: 0.1333 - val_acc: 0.9605 - val_loss: 0.1349
Epoch 11/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 535us/step - acc: 0.9649 - loss: 0.1197 - val_acc: 0.9610 - val_loss: 0.1303
Epoch 12/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 483us/step - acc: 0.9662 - loss: 0.1192 - val_acc: 0.9637 - val_loss: 0.1213
Epoch 13/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 474us/step - acc: 0.9693 - loss: 0.1076 - val_acc: 0.9666 - val_loss: 0.1159
Epoch 14/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 471us/step - acc: 0.9712 - loss: 0.1022 - val_acc: 0.9660 - val_loss: 0.1128
Epoch 15/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 480us/step - acc: 0.9723 - loss: 0.0973 - val_acc: 0.9670 - val_loss: 0.1109
Epoch 16/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 527us/step - acc: 0.9743 - loss: 0.0936 - val_acc: 0.9675 - val_loss: 0.1079
Epoch 17/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 500us/step - acc: 0.9745 - loss: 0.0912 - val_acc: 0.9686 - val_loss: 0.1067
Epoch 18/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 496us/step - acc: 0.9766 - loss: 0.0846 - val_acc: 0.9682 - val_loss: 0.1027
Epoch 19/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 500us/step - acc: 0.9771 - loss: 0.0815 - val_acc: 0.9684 - val_loss: 0.1002
Epoch 20/20
1875/1875 ━━━━━━━━━━━━━━━━━━━━ 1s 487us/step - acc: 0.9784 - loss: 0.0775 - val_acc: 0.9688 - val_loss: 0.0978
TRAININGSDAUER: 19.50 Sek.
Minibatch liegt mit einer Dauer von ca. 20 Sekunden zwischen Batch-Training (3 Sek.) und reinem SGD (8 Min.). Interessant ist, dass es doch erheblich schneller ist als reines SGD.
Wir sehen uns wieder die Performance an:
= plt.subplots(nrows=1, ncols=2, figsize=(15,4))
fig, ax
0], 'Loss', history3.history['loss'],
set_subplot(ax['val_loss'], [0, 1])
history3.history[1], 'Accuracy', history3.history['acc'],
set_subplot(ax['val_acc'], [0.8, 1])
history3.history[
plt.show()
= model.evaluate(x_test, y_test_1hot)
loss, acc print(f"Evaluation auf den Testdaten:\n\nLoss = {loss:.3f}\nAccuracy = {acc:.3f}")
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 424us/step - acc: 0.9643 - loss: 0.1109
Evaluation auf den Testdaten:
Loss = 0.098
Accuracy = 0.969
Die Performance für unser Minibatch-Training mit Batchgröße 32 ist mit einer 97% Accuracy auf den Testdaten praktisch gleich zur Performance mit reinem SGD.
5.6.3 Fazit
Schauen wir uns die Resultate nochmal im Vergleich an (unten sieht man auch die jeweilige Accuracy als Kurve über die Epochen).
Trainingsdauer | Accuracy (Test) | |
---|---|---|
Batchtraining | 3 Sek | 27% |
Reines SGD | 8 Min | 97% |
Minibatch (32) | 20 Sek | 97% |
Es scheint - zumindest bei diesen Daten - so zu sein, dass man mit Minibatch-Training mit der voreingestellten Batchgröße am besten fährt, da man eine hohe Accuracy bei tolerierbarer Trainingsdauer erzielt.
Wir sehen uns nochmal die Kurven der drei Ansätze nebeneinander an.
= plt.subplots(nrows=1, ncols=3, figsize=(15,4))
fig, ax
0], 'Batchgröße 60000 / Accuracy', history1.history['acc'], history1.history['val_acc'], [0, 1])
set_subplot(ax[1], 'Batchgröße 1 / Accuracy', history2.history['acc'], history2.history['val_acc'], [0.85, 1])
set_subplot(ax[2], 'Batchgröße 32 / Accuracy', history3.history['acc'], history3.history['val_acc'], [0.85, 1])
set_subplot(ax[
plt.show()
Wichtig ist immer, sich die Rahmenbedingungen des obigen “Experiments” vor Augen zu führen. Wir haben die Optimierungsmethode Stochastic Gradient Descent mit einer Lernrate von 0.1 verwendet und für 20 Epochen trainiert. Wir hatten ein FNN mit einer versteckten Schicht mit 60 Neuronen und haben auf den MNIST-Daten gearbeitet. Sollte sich einer dieser Umstände ändern, kann das die Ergebnisse verändern.
Zudem müsste man jede der drei Variante mehrfach testen (z.B. 10x) und dann den Mittelwert von Dauer und Accuracy nehmen, um statistische Glaubwürdigkeit zu erreichen. In unserem Beispiel sind die Unterschiede allerdings so deutlich, dass wir uns das ersparen.