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
  • 10.04.2025: Kapitel umstrukturiert

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

import tensorflow
import math
from math import exp
import matplotlib.pyplot as plt
import numpy as np
import time

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

model = Sequential()

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

model = Sequential([
    Input(shape=(2,)),
    Dense(1,
          kernel_initializer='zeros',
          bias_initializer='zeros',
          activation='sigmoid')
])

Unser Netz sieht derzeit so aus (das Bias-Neuron ist nicht grafisch repräsentiert):

Perzeptron in Keras

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.

w, b = model.get_weights()
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

iris = datasets.load_iris()

Wir reduzieren die Features und Zielwerte auf zwei Klassen.

features = iris.data.T
features = features[[0,2]]
x_iris = features.T
x_iris[:5] # Testausgabe
array([[5.1, 1.4],
       [4.9, 1.4],
       [4.7, 1.3],
       [4.6, 1.5],
       [5. , 1.4]])
x_iris = x_iris[:100] # nur die ersten 100 Elemente
y_iris = iris.target[:100]
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:

plt.scatter(features[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.xlabel('sepal length')
plt.ylabel('petal length')
plt.legend(loc='upper left')
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 = SGD(learning_rate=0.1)

model.compile(optimizer=sgd, 
              loss='mean_squared_error', 
              metrics=['acc'])

# 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'])
Hinweis

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.

history = model.fit(x_iris, 
                    y_iris, 
                    epochs=10)
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):
    epochs = range(len(history.history['acc']))
    fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
    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')
    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:

w, b = model.get_weights()
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):

pred = model.predict(x_iris[:10])
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):

pred = model.predict(x_iris[50:60])
[(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

MNIST kurz gefasst

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

(x_train, y_train), (x_test, y_test) = mnist.load_data()

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:

plt.imshow(x_train[0], cmap='gray')
<matplotlib.image.AxesImage at 0x3060bce10>

Zehntes Trainingsbild:

plt.imshow(x_train[9], cmap='gray')
<matplotlib.image.AxesImage at 0x30606df50>

Hier geben wir von jeder Kategorie das erste Trainingsbeispiel aus:

fig, ax = plt.subplots(nrows=2, ncols=5, sharex=True, sharey=True,)
ax = ax.flatten() 
for i in range(10):
    img = x_train[y_train == i][0]
    ax[i].imshow(img, cmap='gray')

ax[0].set_xticks([]) # keine Achsenmarkierungen
ax[0].set_yticks([])
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 = x_train.reshape(60000, 784)/255.0
x_test = x_test.reshape(10000, 784)/255.0

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

y_train_1hot = to_categorical(y_train, 10)
y_test_1hot = to_categorical(y_test, 10)

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

Netz mit 20 versteckten Neuronen in Keras

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

model = Sequential()
model.add(Input(shape=(784,)))
model.add(Dense(60, 
                activation='sigmoid'))
model.add(Dense(10,
                activation='softmax'))

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:

weights = model.get_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

opt = SGD()
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:

opt = SGD(learning_rate = 0.1)
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.

model.compile(optimizer=opt,
              loss='categorical_crossentropy',
              metrics=['acc'])

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

start_time = time.time() # Wir merken uns die Startzeit

history1 = model.fit(x_train, y_train_1hot, 
                     epochs=20,
                     batch_size=60000,
                     verbose=1,
                     validation_data = (x_test, y_test_1hot))

duration = time.time() - start_time # Wir berechnen die Dauer in Sek.
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):
    e_range = range(1, len(traindata) + 1)
    ax.plot(e_range, traindata, 'b', label='Training')
    ax.plot(e_range, testdata, 'g', label='Test')
    ax.set_xlabel('Epochen')
    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.

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15,4))

set_subplot(ax[0], 'Loss', history1.history['loss'], 
            history1.history['val_loss'], [0, 3])
set_subplot(ax[1], 'Accuracy', history1.history['acc'], 
            history1.history['val_acc'], [0, 0.6])

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.

loss, acc = model.evaluate(x_test, y_test_1hot)
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.

model = Sequential()
model.add(Input(shape=(784,)))
model.add(Dense(60, 
                activation='sigmoid'))
model.add(Dense(10, activation='softmax'))

opt = SGD(learning_rate = 0.1)

model.compile(optimizer=opt,
              loss='categorical_crossentropy',
              metrics=['acc'])

start_time = time.time() 

history2 = model.fit(x_train, y_train_1hot, 
                     epochs=20,
                     batch_size=1,
                     verbose=1,
                     validation_data = (x_test, y_test_1hot))

duration = time.time() - start_time 
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.

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15,4))

set_subplot(ax[0], 'Loss', history2.history['loss'], 
            history2.history['val_loss'], [0, .5])
set_subplot(ax[1], 'Accuracy', history2.history['acc'], 
            history2.history['val_acc'], [0.9, 1])

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:

loss, acc = model.evaluate(x_test, y_test_1hot)
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.

model = Sequential()
model.add(Input(shape=(784,)))
model.add(Dense(60, 
                activation='sigmoid'))
model.add(Dense(10, activation='softmax'))

opt = SGD(learning_rate = 0.1)

model.compile(optimizer=opt,
              loss='categorical_crossentropy',
              metrics=['acc'])

start_time = time.time() 

history3 = model.fit(x_train, y_train_1hot, 
                     epochs=20,
                     verbose=1,
                     validation_data = (x_test, y_test_1hot))

duration = time.time() - start_time 
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:

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15,4))

set_subplot(ax[0], 'Loss', history3.history['loss'], 
            history3.history['val_loss'], [0, 1])
set_subplot(ax[1], 'Accuracy', history3.history['acc'], 
            history3.history['val_acc'], [0.8, 1])

plt.show()

loss, acc = model.evaluate(x_test, y_test_1hot)
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.

fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15,4))

set_subplot(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])

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.