#KI-Werkstatt

Worum es hier geht

In meinem letzten Blog Einstieg in neuronale Netze mit Keras habe ich recht umfassend beschrieben, wie man sich eine „Werkbank“ für die Arbeit mit einfachen neuronalen Netzen zusammen baut. Als Beispiel habe ich die MNIST Datenbank für handschriftliche Ziffern verwendet.
Die Genauigkeit der Vorhersage war mit ungefähr 96,5% zwar schon beeindruckend, für die Realität aber völlig unbrauchbar. Die Vorhersagegenauigkeit von Menschen liegt bei diesem Datensatz etwa bei 99,7%. Somit gibt es also noch viel Luft nach oben.
Ich will in diesem Blog versuchen mit sehr einfachen Mitteln auf 99,1% Genauigkeit zu kommen. Dies schafft man mit etwas Glück mit einer CNN Netzarchitektur.

Was sind CNN?

Convolutions, also Faltungen sind eine von zwei gängigen Architekturen die ein Netz tief machen (Die andere Architektur sind die RNN, Recurrent Nuron Networks, über die ich in einem weitern Blog schreiben werde). Die Tiefe bezeichnet ja die Anzahl der Schichten in einem Netz. Dabei fasst man aber mehrere Schichten zu funktionsbereichen zusammen. Hier ist der besondere Funktionsbereich das Falten von Bildern.
Die ganz einfachen, Fully Connected Neuronalen Netze sind zwar ein sehr mächtiges Tool, sie reagieren aber empfindlich auf zu große Hypothesenräume (kleine Erinnerungsstütze: Ein Datensatz heißt Sample, die Attribute eines Samples heißen Features ==> Die Anzahl der Features bestimmt die Anzahl der Dimensionen im NN ==> Das bezeichnet man als Hypothesenraum).
Umgangssprachlich kann man sagen, dass zu viele beschreibende Attribute das Netz verwirren. Da funktionieren wir Menschen übrigens ganz ähnlich. Versuch doch mal aus folgendem Satz die Emotion abzuleiten: „Ach, ich weiß auch nicht. Es regnet, der Himmel ist grau und ich bin heute irgendwie mit dem linken Fuß aufgestanden.“ Es gelingt zwar, aber folgende zwei Zeichen zeigen die gleiche Emotion und sind um ein vielfaches schneller und unmissverständlicher zu erfassen :(.
Genau das machen die Convolution Layer vor den Fully Connected Layern. Sie erkennen in einer großen Menge von Features eindeutige Muster und heben diese hervor.
Nehmen wir zur Veranschaulichung das Bild einer Katze. Convolution Layer gehen dabei von den ganz feinen Strukturen also einem kleinen Strich, einem Punkt oder einer Farbe zu immer größeren Mustern, wie etwa einem Katzenohr, einer Katzennase, Katzenaugen. Die Fully Connected Layers bekommen danach also nicht mehr eine Menge von Bildpunkten zu sehen, sondern sie bekommen gesagt: „Auf dem Bild ist ein Katzenkopf“. Dadurch reduziert sich die Anzahl an Features, aus denen der Fully Connected Layers Teil des Netzes seine Antwort ableiten muss, was ihm die Arbeit leichter macht.
Es gibt ein wirklich phantastisches YouTube Video, das ganz anschaulich in die Mathematik hinter den CNN führt. Unbedingt anschauen. Es macht aus vielen ? ein ! im Kopf. A friendly introduction to Convolutional Neural Networks and Image Recognition

Vom Daten besorgen bis zum Feature Engineering

In meinem letzten Blog habe ich sehr detailliert beschrieben, wie man die Samples fürs Trainieren und Testen in seine „Werkbank einspannt“. Das kann ich hier also überspringen und nur noch die Codeblocks aufführen:

Boiler Plate

#tensorflow
import tensorflow as tf
from tensorflow.python.client import device_lib
#keras
import keras
from keras.backend.tensorflow_backend import set_session
from keras.datasets import mnist
from keras import models
from keras import layers
from keras import regularizers
from keras.utils import to_categorical
from keras import backend as K
#numpy
import numpy as np
#matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
#scipy
from scipy import ndimage
from scipy.ndimage.filters import gaussian_filter
#time
import time
#Force GPU support with growing memory
config = tf.ConfigProto()
config.gpu_options.allow_growth = True # dynamically grow the memory used on the GPU
sess = tf.Session(config=config)
set_session(sess)

Using TensorFlow backend.

Daten besorgen

#keras / tensorflow has already the full MNIST dataset
(train_images_raw, train_labels_raw), (test_images_raw, test_labels_raw) = mnist.load_data()

Feature Engineering

Hier ist dann doch noch eine Kleinigkeit anders als beim letzten Mal.
Ich konvertiere den 3D Bildtensor in einen 4D Tensor, da Keras das so für seine Convolutional Layers als Input braucht:
* 1. Dimension: Die Nummer des Samples
* 2. Dimension: Die Bildzeile
* 3. Dimension: Die Bildspalte
* 4. Dimension: Die Farbkanäle (hier haben wir nur einen, da es sich ja um Graustufenbilder handelt. Sonst sind es drei für Rot, Grün und Blau)
Ich teile alle Grauwerte durch ihren Maximalwert (255), um sie in den Wertebereich zwischen Null und Eins zu trimmen. Dieser Wertebereich ist für die Aktivierungsfunktionen bekömmlicher.

#convert samples into (60.000, 28,28, 1) norm - tesnsors
train_images = train_images_raw.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255 #255 different gray scales
test_images = test_images_raw.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255 #255 different gray scales
#convert labels into one hot representation
train_labels = to_categorical(train_labels_raw)
test_labels = to_categorical(test_labels_raw)

Training

Das Modell Konfigurieren

Ich habe mich hier für eine extrem schlanke Konfiguration meines Netzes entschieden. Man findet zum Beispiel auf Kaggle wesentlich leistungsstärkere, aber eben auch größere Konfigurationen.
Allerdings kommt mir die reduzierte Komplexität später wieder zugute, wenn ich ein paar interessante Details des Netzes herausheben möchte. Zudem verfügt das Netz über alle wichtigen Bestandteile, die man für seine Experimente braucht.

Die Convolutions

Über models.Sequential()hole ich mir von Keras ein frisches Modell. Dann füge ich den ersten Convolutional Layer hinzu. Dabei sagt die 32 und die (3, 3), dass ich 32 verschiedene Filter der Größe 3 auf 3 Pixel trainieren möchte. In Aktion fährt dann ein 3 x 3 großes Fenster über das Bild und wendet alle 32 Filter pro Ausschnitt an. Jeder dieses Filter konzentriert sich auf ein bestimmtes kleines Detail wie zum Beispiel einen waagrechten schwarzen Strich. Findet der Filter diesen Strich meldet er das Ergebnis weiter. Findet er ihn nicht, bleibt der Filter dunkel. Das Attribute padding gibt an, ob die Ränder mit beachtet werden sollen oder nicht. Dabei bedeutet ‚same‘, dass sie beachtet werden sollen. Als Aktivierungsfunktion habe ich hier die relu verwendet. Die ist ein wenig performanter als die sigmoid und skaliert über den Wertebereich linear, was sigmoid ja genau nicht macht. Du solltest beim tuning verschiedene Funktionen versuchen.

Des Pooling

Danach kommt der erste Pooling Layer, der das Ergebnis des Filters um den Faktor 2 in jede Richtung reduziert. Bei den traditionellen CNN verwendet man Pooling Layer, man geht aber mehr und mehr dazu über, das Ergebnis zu normalisieren, statt zu konzentrieren. Das liefert oft bessere Vorhersagen, ist aber um einiges langsamer beim Training. Du kannst ja die layers.MaxPooling2D(2, 2)durch layers. BatchNormalization() austauschen und die Ergebnisse miteinander vergleichen.

Ein Paar ist nicht genug

Ich staple insgesamt vier Convolution Layer übereinander und reduziere die Anzahl der Filter zum Schluss von 32 auf 16. Das ist eher unüblich. Eigentlich bläht man die Anzahl der Filter nach hinten auf. Man fängt zum Beispiel mit 32 Filtern an, und erweitert dann auf 64. Ich mache hier aber aus zwei Gründen genau das Gegenteil. Zum einen sollen nachher in den Filtern eindeutige Merkmale für zehn verschiedene Ziffern freigestellt werden. Dazu sollte ein Raum von 16 drei auf drei großen Matrizen mehr als ausreichend sein. Zum anderen möchte ich nachher visualisieren, wie die Convolutions die einzelnen Ziffern codieren. Das zeigt sich an einer übersichtlichen Menge leichter.

An der Grenze wird gerade gebogen

Am Übergang vom CNN zum Fully Connected Nuron Network, biegt der Flatten() Layer den Output 3D Tensor aus dem Layer der Convolutions in einen 1D Tensor auf. 16 Filter x 3 auf 3 Pixel macht also einen Vektor der Länge 144.

Dropout

Ich habe im Modell an einigen Stellen Dropout Layer hinzugefügt. Das ist ein sehr simples Mittel um overfitting zu vermeiden. Die 0,4 sagt dabei, dass 40% der Ergebnisse pro Iteration zufällig ausgewählt und verworfen werden. Da pro Epoche immer wieder die selben Samples zum Trainieren verwendet werden, vermeidet man so, dass die Entscheidung (aka Gewichtung) des Netzes an einigen wenigen prominenten Features festgemacht wird. Neuronale Netze verhalten sich da ähnlich wie wir Menschen auch. Sie wählen immer den einfachsten Weg. Mit den Dropout Layern verstellt man die einfachen Wege und zwingt Das Netz auch in anderen Dimensionen nach Minima zu suchen.

Regularizers

Beim ersten Dense Layer habe ich noch den Parameter kernel_regularizer = regularizers.l2(0.007)hinzugefügt. Regularizer nähern sich dem Problem Overfitting von der anderen Seite. Sie verhindern, dass das Netz einzelne Gewichte überinterpretiert. Zu laute Töne werden damit also heruntergeregelt. Auch hier gibt es eine Analogie: In der Klasse von Harry Potter meldet sich immer Hermine. Da sie zudem auch meist die richtige Antwort kennt, wird sie nicht mehr so oft aufgerufen. Man regelt sie also herunter, damit die anderen auch drankommen. Der Faktor 0.007 gibt an, wie stark man herunterregeln möchte.

Was man noch machen könnte

Ein Mittel für das Verbessern des Modells habe ich hier weggelassen: Das Augmentieren der Daten. Dabei werden die einzelnen Samples verzerrt, um mehr Testdaten zu erhalten. Eigentlich ist das nicht sonderlich schwer hinzuzufügen und es bringt die Lösung weit nach vorne. Um genau zu sein augmentieren die top MNIST Modelle in Kaggle alle. Allerdings bläht mir das den Python Code um einige Generatoren auf. Diese zusätzlich Komplexität möchte ich hier weglassen, und mich auf das Wesentliche konzentrieren.

model = models.Sequential()
# Convolution Layers
model.add(layers.Conv2D(32, (3, 3), padding = 'same', activation = 'relu', input_shape = (28, 28, 1)))
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.Conv2D(32, (3, 3), padding = 'same', activation = 'relu'))
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.Conv2D(32, (3, 3), padding = 'same', activation = 'relu'))
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.Conv2D(16, (3, 3), padding = 'same', activation = 'relu'))
model.add(layers.Dropout(0.4))
# Fully connected Layers
model.add(layers.Flatten())
model.add(layers.Dropout(0.4))
model.add(layers.Dense(128, kernel_regularizer = regularizers.l2(0.007), activation='relu'))
model.add(layers.Dropout(0.4))
model.add(layers.Dense(10 , activation='softmax'))
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

Layer (type) Output Shape Param #

conv2d_1 (Conv2D) (None, 28, 28, 32) 320


max_pooling2d_1 (MaxPooling2 (None, 14, 14, 32) 0


conv2d_2 (Conv2D) (None, 14, 14, 32) 9248


max_pooling2d_2 (MaxPooling2 (None, 7, 7, 32) 0


conv2d_3 (Conv2D) (None, 7, 7, 32) 9248


max_pooling2d_3 (MaxPooling2 (None, 3, 3, 32) 0


conv2d_4 (Conv2D) (None, 3, 3, 16) 4624


dropout_1 (Dropout) (None, 3, 3, 16) 0


flatten_1 (Flatten) (None, 144) 0


dropout_2 (Dropout) (None, 144) 0


dense_1 (Dense) (None, 128) 18560


dropout_3 (Dropout) (None, 128) 0


dense_2 (Dense) (None, 10) 1290

Total params: 43,290
Trainable params: 43,290
Non-trainable params: 0


In der Zusammenfassung sieht man sehr anschaulich, wie das Pärchen aus Convolution Layer Conv2D und Pooling Layer MaxPooling2D zusammenarbeitet. Aus dem ersten Conv2D kommen beispielsweise 32 gefilterte Bilder der Größe 28 auf 28 Pixel heraus. Der MaxPooling2D Layer konzentriert diese auf die Hälfte je Richtung. Also auf 14 mal 14 Pixel.
Der Flatten Layer biegt den 3x3x16 Tensor letztendlich zu einem Vektor der länge 144 auf, um die freigestellten Merkmale in den Fully Connected Teil des Netzes zu pumpen.

Das Modell trainieren

start = time.time()
history = model.fit(
train_images, train_labels,
epochs= 15,
batch_size= 162,
shuffle = True,
validation_split = 0.1)
print("It took:", time.time() - start)

Train on 54000 samples, validate on 6000 samples
Epoch 1/15
54000/54000 [==============================] – 6s 112us/step – loss: 1.0720 – acc: 0.7441 – val_loss: 0.3139 – val_acc: 0.9468
Epoch 2/15
54000/54000 [==============================] – 4s 71us/step – loss: 0.3658 – acc: 0.9182 – val_loss: 0.1452 – val_acc: 0.9757
Epoch 3/15
54000/54000 [==============================] – 4s 68us/step – loss: 0.2454 – acc: 0.9421 – val_loss: 0.1299 – val_acc: 0.9738
Epoch 4/15
54000/54000 [==============================] – 4s 69us/step – loss: 0.2036 – acc: 0.9523 – val_loss: 0.0965 – val_acc: 0.9822
Epoch 5/15
54000/54000 [==============================] – 4s 69us/step – loss: 0.1733 – acc: 0.9592 – val_loss: 0.0880 – val_acc: 0.9830
Epoch 6/15
54000/54000 [==============================] – 4s 71us/step – loss: 0.1543 – acc: 0.9645 – val_loss: 0.0770 – val_acc: 0.9870
Epoch 7/15
54000/54000 [==============================] – 4s 69us/step – loss: 0.1407 – acc: 0.9677 – val_loss: 0.0819 – val_acc: 0.9855
Epoch 8/15
54000/54000 [==============================] – 4s 69us/step – loss: 0.1307 – acc: 0.9707 – val_loss: 0.0685 – val_acc: 0.9880
Epoch 9/15
54000/54000 [==============================] – 4s 69us/step – loss: 0.1209 – acc: 0.9729 – val_loss: 0.0648 – val_acc: 0.9892
Epoch 10/15
54000/54000 [==============================] – 4s 73us/step – loss: 0.1107 – acc: 0.9760 – val_loss: 0.0722 – val_acc: 0.9875
Epoch 11/15
54000/54000 [==============================] – 4s 70us/step – loss: 0.1061 – acc: 0.9760 – val_loss: 0.0655 – val_acc: 0.9875
Epoch 12/15
54000/54000 [==============================] – 4s 71us/step – loss: 0.1023 – acc: 0.9778 – val_loss: 0.0621 – val_acc: 0.9880
Epoch 13/15
54000/54000 [==============================] – 4s 70us/step – loss: 0.0992 – acc: 0.9784 – val_loss: 0.0706 – val_acc: 0.9878
Epoch 14/15
54000/54000 [==============================] – 4s 72us/step – loss: 0.0940 – acc: 0.9796 – val_loss: 0.0567 – val_acc: 0.9902
Epoch 15/15
54000/54000 [==============================] – 4s 70us/step – loss: 0.0896 – acc: 0.9802 – val_loss: 0.0595 – val_acc: 0.9887
It took: 59.30845069885254

Modell testen

(test_loss, test_acc) = model.evaluate(test_images, test_labels)
print("Loss: ", test_loss)
print("Accuracy: ", test_acc)

10000/10000 [==============================] – 1s 93us/step
Loss: 0.04145347746014595
Accuracy: 0.9926
Beim Trainieren des Modells darf man Eines nicht vergessen: Wir haben Gevatter Zufall mit zur Party eingeladen: Die initialen Filter und Gewichte werden zufällig ausgewählt. Der Dropout Layer schlägt zufällig zu. Die Samples werden vor jeder Epoche neu gemischt. Deshalb unterscheidet sich jeder Lauf von den anderen.
Dennoch sollte man nicht auf ein Wunder hoffen. In diesem Fall habe ich eine Testgenauigkeit von 99,26% erzielt. Wenn ich das Modell tausende Male trainiere befindet sich dennoch kein Modell mit 99,5% darunter, da das die Konfiguration einfach nicht hergibt.

Speichern

# serialize model to JSON
model_json = model.to_json()
with open("persist_model/convolutional_MNIST_model.json", "w") as json_file:
json_file.write(model_json)
# serialize weights to HDF5
model.save_weights("persist_model/convolutional_MNIST_model.h5")
print("Saved model to disk")

Saved model to disk

Trainingsverlauf interpretieren

Man sieht, dass die Kurve über den Trainingsverlauf in diesem Fall sehr unstet ist. Das ist ein Hinweis dafür, dass sich die einzelnen Epochen anscheinend in sehr unterschiedliche Minima optimiert haben. Das kann man als Fingerzeig werten, dass die Lernrate der Optimierungsfunktion falsch gewählt ist. Keras belegt die Lernrate zwar mit einem ausgewogenen Default vor, der zwar auf viele, aber natürlich nicht auf jedes Problem passt. Man kann hier entweder die Lernrate von Hand nachjustieren oder mit einem Optimizer von Keras arbeiten.
Da ich mich hier aber auf die Convolutions konzentrieren möchte, belasse ich die Kurve so wie sie ist. Es gibt hierzu übrigens einen sehr gut strukturierten Blog, der als gutes Hilfmittel für die richtige Auswahl des Optimizers dienen kann How to pick the best learning rate for your machine learning project

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.show()
print("Abb 1: Anstieg der Genauigkeit über die Epochen")
print("")
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
print("Abb 2: Abnahme des Fehlers über die Epochen")
print("")

Training und Validierung Accuracy
Abb 1: Anstieg der Genauigkeit über die Epochen
Training und Validierung Verlust
Abb 2: Abnahme des Fehlers über die Epochen

Modell bewerten

Visualisierung der Filter

Es stellt sich die Frage, wie man einen Filter visualisiert. Eigentlich gibt es da ja nicht viel mehr zu sehen, als einen kleinen Tensor mit Gewichten. Man kann aber zeigen, wie ein Filter arbeitet. Man könnte beispielsweise das Bild einer Ziffer durch den Filter leiten und zeigen, was der Filter aus dem Bild macht. Ich finde das Ergebnis zwar nicht sonderlich erhellend, aber dennoch sehenswert. Ich habe hier für eine Ziffer die ersten neun Filter pro Layer aktiviert.
Wichtig zu wissen ist, was ein heller Punkt bedeutet: Ein heller Punkt heißt, dass hier der Filter aktiviert wurde. Quasi sagt der Filter: „Hier an dieser 3 auf 3 Pixel großen Fläche habe ich mein Muster gefunden und kenzeichne die Stelle mit einem hellen Punkt.

my_image = test_images[4]
my_image_raw = test_images_raw[4]
#
# This will draw for the given layer all filter results
# layer_index := number of the layer
# interpolation := kind of interpolation (https://matplotlib.org/gallery/images_contours_and_fields/interpolation_methods.html)
def draw_example(layer_index = 0, interpolation = 'sinc'):
print('Layer Index {}'.format(layer_index))
# we add a axis to make it digestable for Keras
image = np.expand_dims(my_image, axis=0)
layer = model.get_layer(index = layer_index)
print(type(layer).__name__)
convolutions = K.function(model.inputs, [layer.output])([image])
#kick out empty axes
convolutions = np.squeeze(convolutions)
#rearange the order so that we can iterate over the images
convolutions = np.transpose(convolutions)
width = 10
fig = plt.figure(figsize=(15, 15))
# draw the original pic first
ax = fig.add_subplot(1, width, 1)
ax.axis('off')
ax.title.set_text("Original")
ax.imshow(my_image_raw, interpolation= interpolation , cmap='gray')
# draw some examples for filter results
for index, filter in enumerate(convolutions):
if index == width - 1:
break
ax = fig.add_subplot(1, width, index + 2)
ax.axis('off')
ax.title.set_text(index)
ax.imshow(filter.transpose(), interpolation = interpolation, cmap='gray')
plt.show()
plt.close()
# we draw just the outcome of the convolution layer
draw_example(layer_index = 0)
draw_example(layer_index = 2)
draw_example(layer_index = 4, interpolation = 'none')
draw_example(layer_index = 6, interpolation = 'none')
print("Abb 3: Das Ergebnis von neun verschiedenen Filtern je Layer, wenn sie auf eine 4 blicken.")

Layer Index 0
Conv2D
KI muss die Zahl vier in verschiedenen Schreibweisen erkennen
Layer Index 2
Conv2D
KI erkennt die Zahl 4
Layer Index 4
Conv2D

Layer Index 6
Conv2D

Abb 3: Das Ergebnis von neun verschiedenen Filtern je Layer, wenn sie auf eine 4 blicken.
Ich finde das Ergebnis irreführend, da man intuitiv annehmen könnte, dass das Ergebnis von einem Layer zum anderen immer schlechter wird. Im ersten Layer kann man noch annehmen, dass der Filter Konturen, Schatten und Flächen findet und dann verschwindet das Ergebnis mehr und mehr, bis nur noch ein paar Punkte übrig bleiben.
In Wirklichkeit ist es aber genau anders herum. Im ersten Layer erkennt man die Zahl noch relativ genau, weil sehr viele sehr kleine Filtermerkmale gefunden werden. In den tieferen Schichten werden die Bilder immer dunkler, da die Filter immer größere Muster suchen und diese nicht finden. Nochmal: Ein heller Punkt bedeutet nur, dass der Filter sein Muster gefunden hat, nicht aber, was sein Muster ist.

Träume im weißen Rauschen

Deshalb greife ich hier zu einem Trick. Ich nehme nicht das Bild einer Ziffer, sondern einfach nur ein Rauschen. Also eine 28 auf 28 Pixel große Fläche mit ganz zufällig gewählten grauen Pixeln. Das ist dann nämlich so, als würde ich mit weißem Stift auf weißes Papier schreiben. In dem Rauschen verstecken sich alle möglichen Zahlen, da der Filter ja nur prüft, ob sein Muster da ist, nicht aber, ob dies exklusiv der Fall ist. (Exklusiv würde hier bedeuten: „Wenn ich im Raum bin ist kein Platz für andere hier“)

#get a random picture this just gray scale pixels on it
my_image = np.random.random((28,28,1))
my_image_raw = np.squeeze(my_image)
#
# This will draw for the given layer all filter results
# layer_index := number of the layer
def draw_one_filter(layer_index = 0, interpolation = 'sinc'):
print('Layer Index {}'.format(layer_index))
# we add a axis to make it digestable for Keras
image = np.expand_dims(my_image, axis=0)
layer = model.get_layer(index = layer_index)
convolutions = K.function(model.inputs, [layer.output])([image])
print("{} {}".format(type(layer).__name__, convolutions[0].shape))
#kick out empty axes
convolutions = np.squeeze(convolutions)
#rearange the order so that we can iterate over the images
convolutions = np.transpose(convolutions)
# length and depth of the picture array
square = convolutions.shape[0] + 1
square = int(np.ceil(np.sqrt(square)))
fig = plt.figure(figsize=(10, 10))
fig.subplots_adjust(top= 1.2)
ax = fig.add_subplot(square, square, 1)
ax.axis('off')
ax.title.set_text("Original")
ax.imshow(my_image_raw, interpolation= interpolation , cmap='gray')
# Specify the layer to want to visualize
for index, filter in enumerate(convolutions):
ax = fig.add_subplot(square, square, index + 2)
ax.axis('off')
ax.title.set_text(index)
ax.imshow(filter.transpose(), interpolation = interpolation, cmap='gray')
plt.show()
plt.close()
# we draw just the outcome of the convolution layer
draw_one_filter(layer_index = 0)
draw_one_filter(layer_index = 2)
draw_one_filter(layer_index = 4)
draw_one_filter(layer_index = 6, interpolation = 'none')
print("Abb 4: Visualisierung, was die verschiedenen Filter einer Schicht aus einem Rauschen filtern.")

Layer Index 0
Conv2D (1, 28, 28, 32)

Layer Index 2 Conv2D (1, 14, 14, 32)

Abb. 3: Layer Index 2 Conv2D (1, 14, 14, 32)

Layer Index 4 Conv2D (1, 7, 7, 32)

Abb. 4: Layer Index 4 Conv2D (1, 7, 7, 32)

Layer Index 6 Conv2D (1, 3, 3, 16)

Abb. 5: Layer Index 6 Conv2D (1, 3, 3, 16)

Visualisierung, was die verschiedenen Filter einer Schicht aus einem Rauschen filtern

Abb 6: Visualisierung, was die verschiedenen Filter einer Schicht aus einem Rauschen filtern

Im ersten Layer erkennt man nun deutlich, wie ganz gleichmäßig verteilt kleine Muster erkannt werden. Dadurch entsteht die Struktur, die die Filter der nächsten Schicht aufnehmen.
Im Layer 4 erkennt man dann eindrucksvoll, wie komplexere Strukturen aus kleineren zusammengebaut wurden. Es gibt dazu gerade eine eigene Forschungsbewegung, mit dem Namen DeepDream.
Im letzten Layer ist nun das Erkannte so codiert, dass es von einem Fully Connected Netz leicht klassifiziert werden kann.
Nerds aufgepasst: Interessanterweise erkennt das trainierte Model das Rauschen als 8. Dies veranschaulicht, dass eben nur nach dem gesucht wird, was da ist, nicht aber, was nicht da sein sollte. Das ist ein aktuell kaum gelöstes Problem, dem man sehr oft begegnet. Sucht man zum Beispiel in Google nach „Gewinde von Schrauben, die nicht metrisch sind“ bekommt man fast ganz oben Artikel über metrische Schrauben. Wenn ich hingegen zu meinem Sohn sage: „Räume nicht dein Zimmer auf“ Klappt das ganz hervorragend ;-)

results = model.predict([np.expand_dims(my_image, axis=0)])[0]
for index, value in enumerate(results):
print("{} ==> {:2.2f}%".format(index, value*100))

0 ==> 0.45%
1 ==> 0.01%
2 ==> 0.14%
3 ==> 0.01%
4 ==> 0.02%
5 ==> 0.38%
6 ==> 1.20%
7 ==> 0.00%
8 ==> 97.77%
9 ==> 0.02%

Was bekommt der Fully Connected Layer?

Was aus dem letzten Convolution Layer herauskommt wird ja als Feature Vektor in den Fully Connected Teil des DNN geschoben.
Um zu zeigen, dass die Komplexität jetzt viel geringer geworden ist, habe ich zwei verschiedene 4-en und zwei 6-en herausgesucht und dafür alle Filter des letzten Layers ausgedruckt. Als Referenz sieht man in der ersten Spalte das Ergebnis für das Rauschen.

# function block
def get_convolutions(index = -1):
if index == -1:
_image = my_image
_image_raw = my_image_raw
else:
_image = test_images[index]
_image_raw = test_images_raw[index]
# we add a axis to make it digestable for Keras
image = np.expand_dims(_image, axis=0)
convolutions = K.function(model.inputs, [model.get_layer(index = 6).output])([image])
convolutions = np.squeeze(convolutions)
convolutions = np.transpose(convolutions)
return (_image_raw, convolutions, _image)
def add_image(pos, image, interpolation = 'none'):
ax = fig_col.add_subplot(height, width, pos)
ax.axis('off')
ax.imshow(image, interpolation='none', cmap='gray')
#execution block
conv_random = get_convolutions()
conv_4_1 = get_convolutions(index = 4)
conv_4_2 = get_convolutions(index = 6)
conv_6_1 = get_convolutions(index = 11)
conv_6_2 = get_convolutions(index = 21)
width = 7
height = 1
fig_col = plt.figure(figsize=(6,6))
add_image(1, conv_random[0])
add_image(3, conv_4_1[0])
add_image(4, conv_4_2[0])
add_image(6, conv_6_1[0])
add_image(7, conv_6_2[0])
for index in range(len(conv_random[1])):
fig_col = plt.figure(figsize=(6,6))
add_image(1, conv_random[1][index])
add_image(3, conv_4_1[1][index])
add_image(4, conv_4_2[1][index])
add_image(6, conv_6_1[1][index])
add_image(7, conv_6_2[1][index])
plt.show()
plt.close()
print("Abb 6: Zwei Beispiele für codierte Ziffern")













Zwei Beispiele für codierte Ziffern
Zwei Beispiele für codierte Ziffern
Zwei Beispiele für codierte Ziffern
Zwei Beispiele für codierte Ziffern

Zwei Beispiele für codierte Ziffern

Abb 6: Zwei Beispiele für codierte Ziffern

Ich finde, man erkennt sehr deutlich, dass die Ziffern – Paare nahezu gleich codiert wurden, obwohl sie ja etwas unterschiedlich geschrieben wurden. Das CNN hat die handschriftlichen Ziffern quasi in seinen eigenen QR Code übersetzt. – großartig –

Schluss

Mit diesem Blog ist der Einstieg zum maschinellen Sehen und Hören geschafft. Es fehlen noch ein paar Techniken und erweiterte Konfigurationen, um mit seinem Wissen und Können auf einem handwerklich soliden Fundament zu stehen.
Der nächste Blog wird sich nach den Fully Connected und Convolutional NN mit der dritten großen Deep Neuron Network DNN Architektur beschäftigen, den Recurrent NN.

Jetzt teilen auf:

Jetzt kommentieren