#KI-Werkstatt

Worum geht es hier?

In meinen letzten Blogs habe ich die einzelnen Bausteine eines Convolutional Neuronal Networks beschrieben, das designtechnisch im Jahre 2018 angekommen ist.
Auf der Plattform Kaggle.com fand in den letzten Jahren ein Wettstreit statt, handgeschriebene Ziffern per Machine Learning zu erkennen. Der Rekord bei der Trefferrate liegt im Augenblick jenseits der 99,7% aber knapp unter 99,8%. Der Bereich oberhalb der 99,8% wird als unmöglich zu erreichen betrachtet. Lässt man sich die Kandidaten in diesem Grenzbereich einmal anzeigen, versteht man auch warum. Es sind so unordentlich geschriebene Ziffern, dass weder Mensch noch Maschine diese eindeutig bestimmen können.
Die Architektur, die ich hier nun aus allen Einzelteilen zusammenbaue, kommt zu einer Genauigkeit von 99,64% und ist somit unter den Top Kandidaten. Damit möchte ich zeigen, dass neuronale Hochleistungsnetze keine Zauberei, sondern solide Ingenieurskunst sind. Man muss also nicht in einem Geheimlabor der NSA 100 Meter unter der Erde arbeiten um das zu können, sondern kann sich mit viel Liebe zum Detail dem Ziel zügig nähern.
Da ich die Einzelteile ja bereits sehr ausführlich beschrieben habe, werde ich bis auf eine Ausnahme (CallBacks) nicht mehr viel beschreiben. Falls Dir also die dargereichten Erklärungen zu schmal scheinen, möchte ich dich bitten, in den vorherigen Blogs nachzulesen.

Die Bauanleitung

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
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import RMSprop
from keras import callbacks
#numpy
import numpy as np
#sklearn
from sklearn.metrics import confusion_matrix
#itertools
import itertools
#matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
#time
import time
#os
import os
#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.

Trainings und Testdaten besorgen

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

Samples und Features normieren und aufsplitten

#fraktion of the training to the validation samples
a_train = int(train_images_raw.shape[0] * 0.9)
images = train_images_raw.reshape((train_images_raw.shape[0], 28, 28, 1))
images = images.astype('float32') / 255 #255 different gray scales
train_images = images[ : a_train]
valid_images = images[a_train : ]
print("Amount of all images:{}".format(images.shape))
print("Amount of all training images:{}".format(train_images.shape))
print("Amount of all validation images:{}".format(valid_images.shape))
#convert labels into one hot representation
labels = to_categorical(train_labels_raw)
train_labels = labels[ : a_train]
valid_labels = labels[a_train : ]
print("Amount of all labels:{}".format(labels.shape))
print("Amount of all training labels:{}".format(train_labels.shape))
print("Amount of all validation labels:{}".format(valid_labels.shape))
test_images = test_images_raw.reshape((test_images_raw.shape[0], 28, 28, 1))
test_images = test_images.astype('float32') / 255 #255 different gray scales
test_labels = to_categorical(test_labels_raw)

Amount of all images:(60000, 28, 28, 1)
Amount of all training images:(54000, 28, 28, 1)
Amount of all validation images:(6000, 28, 28, 1)
Amount of all labels:(60000, 10)
Amount of all training labels:(54000, 10)
Amount of all validation labels:(6000, 10)

Das Netz zusammen bauen

Anmerkung: hier habe ich die Konfiguration des optimizers herausgezogen, um die anfängliche Lernrate lr explizit zu setzen

model = models.Sequential()
# Convolution Layers
model.add(layers.Conv2D(32, (3, 3), padding = 'same', activation = 'relu', input_shape = (28, 28, 1)))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(32, (3, 3), padding = 'same', activation = 'relu'))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(32, (5, 5), padding = 'same', activation = 'relu'))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(64, (3, 3), padding = 'same', activation = 'relu', input_shape = (28, 28, 1)))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(64, (3, 3), padding = 'same', activation = 'relu'))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(64, (5, 5), padding = 'same', activation = 'relu'))
model.add(layers.BatchNormalization())
model.add(layers.Conv2D(32, (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, activation='relu'))
model.add(layers.Dropout(0.4))
model.add(layers.Dense(10 , activation='softmax'))
optimizer = RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0)
model.compile(optimizer = optimizer , loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()

Layer (type) Output Shape Param #

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


batch_normalization_43 (Batc (None, 28, 28, 32) 128


conv2d_51 (Conv2D) (None, 28, 28, 32) 9248


batch_normalization_44 (Batc (None, 28, 28, 32) 128


conv2d_52 (Conv2D) (None, 28, 28, 32) 25632


batch_normalization_45 (Batc (None, 28, 28, 32) 128


conv2d_53 (Conv2D) (None, 28, 28, 64) 18496


batch_normalization_46 (Batc (None, 28, 28, 64) 256


conv2d_54 (Conv2D) (None, 28, 28, 64) 36928


batch_normalization_47 (Batc (None, 28, 28, 64) 256


conv2d_55 (Conv2D) (None, 28, 28, 64) 102464


batch_normalization_48 (Batc (None, 28, 28, 64) 256


conv2d_56 (Conv2D) (None, 28, 28, 32) 18464


dropout_22 (Dropout) (None, 28, 28, 32) 0


flatten_8 (Flatten) (None, 25088) 0


dropout_23 (Dropout) (None, 25088) 0


dense_15 (Dense) (None, 128) 3211392


dropout_24 (Dropout) (None, 128) 0


dense_16 (Dense) (None, 10) 1290

Total params: 3,425,386
Trainable params: 3,424,810
Non-trainable params: 576


Zum Augmentieren der Daten: Der ImageDataGenerator

datagen = ImageDataGenerator(
# Wieviel Grad [0, 180] darf sich das Bild hin und her drehen
rotation_range = 10,
# Um wieviel darf das Bild nach links und rechts bewegt werden
width_shift_range=0.1,
# Um wieviel darf das Bild nach Oben und Unten bewegt werden
height_shift_range=0.1,
# Die Varianz von dunkel zu hell
#brightness_range = (0.1, 2),
# In welchem Bereich darf das Bild abgeschert werden
shear_range = 0.1,
# In welchem Bereich dar das Bild die größe ändern
zoom_range = 0.1,
# Bei Zahlen sollte man auf das Spiegeln eher verzichten :)
horizontal_flip = False,
# Bei Zahlen sollte man auf das Spiegeln eher verzichten :)
vertical_flip = False)

Callbacks zum Überwachen

Das ist eine Technik, die ich vorher noch nicht beschrieben habe. Mit Callbacks kann man das Training des Modells überwachen und bei Bedarf eingreifen. Die nun folgenden drei Callbacks tun genau dies

Überwachen der Lernrate

Die Lernrate ist die Schrittweite, mit der das Gradienten Verfahren bei der Suche nach dem Minima im Hypothesenraum den nächsten Schritt macht.
Das Bild, das man dazu oft findet, ist der Bergsteiger, der den Weg hinunter ins Tal sucht. Wenn der Bergsteiger im Vergleich zu den Bergen zu klein ist, wird er mit seinen Trippelschrittchen einfach in die nächstbeste Delle laufen und diese für das absolute Minimum halten.
Ist der Bergsteiger aber riesengroß, dann besteht die Gefahr, dass er über das entscheidende Tal einfach drübersteigt.
Ein Indikator für das aktuelle Gelände, durch das der Bergsteiger gerade läuft, ist die Verlustrate der letzten Validierungsschritte. Ändert diese sich kaum noch, so ist der Bergsteiger anscheinend in einem Tal angekommen. Hier zeigt die Erfahrung, dass es sinnvoll ist, die Lernrate zu verkleinern und genau das macht der ReduceLROnPlateau Callback im laufenden Betrieb.

learning_rate_reduction = callbacks.ReduceLROnPlateau(
# val_acc zeigt die letzten verlusraten
monitor='val_acc',
# wie lange halten wir die Fuesse still, bevor wir die Lernrate anpassen
patience=3,
verbose=1,
# Die lernrate wird halbiert
factor=0.5,
# ab wann brechen wir ab
min_lr=0.00001)

Aufhören, wenn es nichts mehr bringt

Wenn sich die Genauigkeit nicht mehr ändert, können wir mit dem Training auch aufhören. Damit entledigt man sich des Problems die Anzahl der Lernraten sinnvoll zu belegen.

early_stopping = callbacks.EarlyStopping(
# über welchen Monitor beobachtet man die Änderung der letzten Iterationen?
monitor = 'acc',
# wie lange halten wir die Fuesse still, bis wir das Lernen abbrechen
patience = 3)

Nichts wegschmeißen

Wenn man sich in das perfekte Modell hineindreht, überläuft man eigentlich immer den optimalen Punkt, an dem das Modell die beste Genauigkeit hatte. Leider ist dieser Punkt mit der nächsten Epoche unwiederbringlich verloren, da die Gewichte bereits geändert wurden. Um diesem Dilemma vorzubeugen, speichere ich das jeweils beste Modell ab. Hat sich die Qualität also im Vergleich zu den vorherigen Berechnungen verbessert, schreibe ich das Modell auf die Festplatte. Somit habe ich immer das beste Modell eines Laufs gerettet.

model_checkpoint = callbacks.ModelCheckpoint(
# Dateiname mit Pfad relativ zu dieserm Code
filepath = 'persist_model/augmenting_cnn_MNIST_model.h5',
# Welcher Monitor soll beobachtet werden, um über die äbderung der Qualität zu entscheiden
monitor = 'val_loss',
# Nur speciern, wenn sich auch was verbessert hat
save_best_only = True)

Los geht’s

Wenn du deine GPU Unterstützung nicht aktiviert hast, solltest du hier aufhören, oder es wird ein sehr langes Wochenende, bis dein Rechner aus der Versenkung wieder auftaucht. Das Normalisieren der Ergebnisse dauert lange.

start = time.time()
epochs = 200
batch_size = 256
history = model.fit_generator(
datagen.flow(train_images, train_labels, batch_size = batch_size),
steps_per_epoch = train_images.shape[0] // batch_size,
epochs = epochs,
validation_data = (valid_images, valid_labels),
callbacks=[learning_rate_reduction, early_stopping, model_checkpoint]
)
print("It took:", time.time() - start)

Epoch 1/200
210/210 [==============================] – 36s 173ms/step – loss: 0.9621 – acc: 0.7322 – val_loss: 0.1943 – val_acc: 0.9547
Epoch 2/200
210/210 [==============================] – 32s 153ms/step – loss: 0.2421 – acc: 0.9325 – val_loss: 0.0697 – val_acc: 0.9847
Epoch 3/200
210/210 [==============================] – 32s 155ms/step – loss: 0.1665 – acc: 0.9560 – val_loss: 0.0503 – val_acc: 0.9880
Epoch 4/200
210/210 [==============================] – 35s 167ms/step – loss: 0.1309 – acc: 0.9662 – val_loss: 0.0506 – val_acc: 0.9885s: 0.1319 – acc
Epoch 5/200
210/210 [==============================] – 52s 249ms/step – loss: 0.1078 – acc: 0.9714 – val_loss: 0.0393 – val_acc: 0.9912
Epoch 6/200
210/210 [==============================] – 93s 442ms/step – loss: 0.0984 – acc: 0.9746 – val_loss: 0.0304 – val_acc: 0.9928
Epoch 7/200
210/210 [==============================] – 93s 442ms/step – loss: 0.0914 – acc: 0.9766 – val_loss: 0.0345 – val_acc: 0.9902
Epoch 00007: ReduceLROnPlateau reducing learning rate to 0.0005000000237487257.
Epoch 8/200
210/210 [==============================] – 89s 422ms/step – loss: 0.0618 – acc: 0.9844 – val_loss: 0.0289 – val_acc: 0.9930
Epoch 9/200
210/210 [==============================] – 86s 412ms/step – loss: 0.0606 – acc: 0.9852 – val_loss: 0.0211 – val_acc: 0.9948
Epoch 10/200
210/210 [==============================] – 51s 243ms/step – loss: 0.0562 – acc: 0.9851 – val_loss: 0.0279 – val_acc: 0.9937
Epoch 00010: ReduceLROnPlateau reducing learning rate to 0.0002500000118743628.
Epoch 11/200
210/210 [==============================] – 50s 239ms/step – loss: 0.0454 – acc: 0.9877 – val_loss: 0.0203 – val_acc: 0.9953
Epoch 12/200
210/210 [==============================] – 60s 285ms/step – loss: 0.0451 – acc: 0.9884 – val_loss: 0.0201 – val_acc: 0.9948
Epoch 00012: ReduceLROnPlateau reducing learning rate to 0.0001250000059371814.
Epoch 13/200
210/210 [==============================] – 54s 256ms/step – loss: 0.0378 – acc: 0.9901 – val_loss: 0.0208 – val_acc: 0.9953
Epoch 14/200
210/210 [==============================] – 65s 311ms/step – loss: 0.0353 – acc: 0.9910 – val_loss: 0.0208 – val_acc: 0.9958
Epoch 15/200
210/210 [==============================] – 93s 442ms/step – loss: 0.0346 – acc: 0.9904 – val_loss: 0.0230 – val_acc: 0.9947
Epoch 00015: ReduceLROnPlateau reducing learning rate to 6.25000029685907e-05.
Epoch 16/200
210/210 [==============================] – 86s 408ms/step – loss: 0.0339 – acc: 0.9910 – val_loss: 0.0195 – val_acc: 0.9955
Epoch 17/200
210/210 [==============================] – 78s 372ms/step – loss: 0.0322 – acc: 0.9911 – val_loss: 0.0191 – val_acc: 0.9955
Epoch 00017: ReduceLROnPlateau reducing learning rate to 3.125000148429535e-05.
Epoch 18/200
210/210 [==============================] – 47s 222ms/step – loss: 0.0294 – acc: 0.9916 – val_loss: 0.0192 – val_acc: 0.9958
Epoch 00018: ReduceLROnPlateau reducing learning rate to 1.5625000742147677e-05.
Epoch 19/200
210/210 [==============================] – 45s 212ms/step – loss: 0.0301 – acc: 0.9920 – val_loss: 0.0183 – val_acc: 0.9962TA: 4s – loss:
Epoch 20/200
210/210 [==============================] – 66s 316ms/step – loss: 0.0306 – acc: 0.9915 – val_loss: 0.0182 – val_acc: 0.9958
Epoch 00020: ReduceLROnPlateau reducing learning rate to 1e-05.
Epoch 21/200
210/210 [==============================] – 45s 213ms/step – loss: 0.0286 – acc: 0.9923 – val_loss: 0.0181 – val_acc: 0.9960- ETA: 0s – loss: 0.0287 – acc: 0.
Epoch 22/200
210/210 [==============================] – 45s 214ms/step – loss: 0.0313 – acc: 0.9919 – val_loss: 0.0181 – val_acc: 0.9962
Epoch 23/200
210/210 [==============================] – 72s 345ms/step – loss: 0.0276 – acc: 0.9924 – val_loss: 0.0186 – val_acc: 0.9963
Epoch 24/200
210/210 [==============================] – 88s 418ms/step – loss: 0.0299 – acc: 0.9921 – val_loss: 0.0186 – val_acc: 0.9962
Epoch 25/200
210/210 [==============================] – 88s 418ms/step – loss: 0.0325 – acc: 0.9913 – val_loss: 0.0182 – val_acc: 0.9962
Epoch 26/200
210/210 [==============================] – 88s 418ms/step – loss: 0.0266 – acc: 0.9927 – val_loss: 0.0186 – val_acc: 0.9962
Epoch 27/200
210/210 [==============================] – 45s 215ms/step – loss: 0.0295 – acc: 0.9920 – val_loss: 0.0183 – val_acc: 0.9963loss: 0
Epoch 28/200
210/210 [==============================] – 45s 213ms/step – loss: 0.0278 – acc: 0.9924 – val_loss: 0.0185 – val_acc: 0.9960
Epoch 29/200
210/210 [==============================] – 45s 214ms/step – loss: 0.0296 – acc: 0.9917 – val_loss: 0.0188 – val_acc: 0.9962
It took: 1805.24138712883

Speichern

Ich speichere hier das Modell noch einmal explizit ab, da in meinem Fall das letzte Ergebnis das Beste ist, obwohl sich die Verlustrate nicht gebessert hat.

# serialize model to JSON
model.save('persist_model/final_augmenting_cnn_MNIST_model.h5')

Trainingsverlauf interpretieren

Man sieht, dass die Kurve hier einen absoluten Lehrbuchcharakter hat. Beide Verläufe schmiegen sich mehr und mehr aneinander an und streben gemeinsam asymptotisch die Horizontale an. Ein mathematisches Happy End 🙂

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

Anstieg der Genauigkeit über die Epochen
Abb 1: Anstieg der Genauigkeit über die Epochen
Abnahme des Fehlers über die Epochen
Abb 2: Abnahme des Fehlers über die Epochen

Model Testen

Für den Test verwende ich wirklich das abgespeicherte Modell um alle Seiteneffekte auszuschließen und um mir sicher zu sein, dass ich das Ergebnis später auch nutzen kann.

main_dir = os.path.normpath(os.getcwd() + os.sep + os.pardir)
mod_dir = os.path.join(main_dir,'train_model', 'persist_model')
test_model = models.load_model(os.path.join(mod_dir, 'final_augmenting_cnn_MNIST_model.h5'))
(test_loss, test_acc) = test_model.evaluate(test_images, test_labels)
print("Loss: ", test_loss)
print("Accuracy: ", test_acc)

10000/10000 [==============================] – 5s 526us/step
Loss: 0.012790212325054472
Accuracy: 0.9964

Fehleranalyse

Es gibt eine sehr schöne Grafik, um die Ausreißer lokalisieren zu können: Die Confusion Matrix. Nach rechts sind dabei die Ziffern angetragen, die das Modell erkannt hat und nach oben die Ziffern, die das Modell hätte erkennen sollen. Somit ist es Ziel der Übung möglichst alle Werte auf die Diagonale zu treiben und den Rest zu Null zu trimmen.
Die Ausreißer habe ich in der Grafik rot markiert. Dadurch springt ins Auge, dass wie zu erwarten die Vier und die Neun den meisten Ärger machen.

# Look at confusion matrix
#Note, this code is taken straight from the SKLEARN website, an nice way of viewing confusion matrix.
def plot_confusion_matrix(confusion_matrix,
labels,
title='Confusion matrix',
cmap=plt.cm.Blues):
plt.imshow(confusion_matrix, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(labels))
plt.xticks(tick_marks, labels, rotation=45)
plt.yticks(tick_marks, labels)
thresh = confusion_matrix.max() / 2.
for i, j in itertools.product(range(confusion_matrix.shape[0]), range(confusion_matrix.shape[1])):
plt.text(j, i, confusion_matrix[i, j],
horizontalalignment="center",
color="white" if confusion_matrix[i, j] > thresh else "black",
bbox = None if confusion_matrix[i , j] == 0 or i == j else dict(boxstyle="square",
ec=(1., 0.5, 0.5),
fc=(1., 0.8, 0.8),
)
)
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
# Predict the values from the validation dataset
labels_pred = test_model.predict(test_images)
# Convert predictions classes to one hot vectors
labels_pred = np.argmax(labels_pred, axis = 1)
# Convert validation observations to one hot vectors
labels_true = np.argmax(test_labels, axis = 1)
# compute the confusion matrix
confusion_mtx = confusion_matrix(labels_true, labels_pred)
# plot the confusion matrix
plot_confusion_matrix(confusion_mtx, labels = range(10))
plt.show()
print('Abb 3: Confusion Matrix')Confusion Matrix
Abb 3: Confusion Matrix
Hier sind alle falsch erkannten Vier - Neun Kombinationen zu sehen
```python
examples = []
wrong = 0
for index, pred in enumerate(labels_pred):
real = labels_true[index]
if pred != real:
wrong += 1
if pred in (4,9):
if real in (4,9) and real != pred:
examples.append((real, pred, test_images[index]))
print('{} von {} wurden falsch erkannt.'.format(wrong, len(labels_pred)))
fig = plt.figure(figsize=(10, 10))
for index, (real, pred, image) in enumerate(examples):
ax = fig.add_subplot(1, len(examples), index + 1)
ax.axis('off')
ax.title.set_text('pred = {}\nreal = {}'.format(pred,real))
ax.imshow(image.reshape(28, 28), cmap=plt.get_cmap('gray'))
plt.show()
print('Abb 4: Schwierigkeiten zwischen 4 und 9')

36 von 10000 wurden falsch erkannt.
Schwierigkeiten zwischen 4 und 9
Abb 4: Schwierigkeiten zwischen 4 und 9

Schluss

Es gibt nun noch zwei Werkzeuge / Techniken, mit denen man das Ergebnis noch verbessern könnte. Das eine ist ein Tool zum automatischen und systematischen justieren der Metaparameter, damit man eine möglichst optimale Konfiguration der Metaparameter erhält. Zum Anderen kann man ein ganzes Ensemble von Modellen trainieren und deren Ergebnisse gemeinsam nutzen. Beides braucht naturgemäß während des Trainings extrem viel Rechenzeit, dennoch kann ein wirklich ausgereiftes und produktiv genutztes Modell nicht darauf verzichten. Bei unserem Modell würden wir sicherlich die letzten beiden Vieren aus der obigen Abbildung noch richtig erkannt bekommen und könnten somit in einen produktiven Betrieb gehen.
Nichts desto trotz ist das Ergebnis schon sehr zufriedenstellend, wenn man bedenkt, dass von 10.000 Ziffern nur 36 falsch erkannt wurden und dass unter diesen 36 auch noch Ziffern sind, die man nicht richtig erkennen kann, wie zum Beispiel die zweite, dritte und vierte Ziffer aus dem obigen Beispiel.

Categories:

Tags:

4 Kommentare

  1. Sehr gutes Tutorial!
    Allerdings bin ich etwas verwirrt im letzten Schritt:
    „confusion_matrix“ ist doch eine Funktion aus der sklearn.metrics Library. Demnach kann Python gar nicht .shape() auf sie anwenden.
    Zeile 17:
    for i, j in itertools.product(range(confusion_matrix.shape[0])
    Wenn ich davon ausgehe, dass es ein Tippfehler ist und statt „confusion_matrix“ „confusion_mtx“ gemeint ist, komme ich trotzdem nicht auf das gewünschte Ergebnis, da „confusion_mtx“ erst nach der for Schleife definiert wird.
    Ich hoffe ihr könnt mich ein wenig erleuchten.
    Beste Grüße!

    • Hallo Tobias,
      tatsächlich wird in Zeile 16 nicht die Funktion confusion_matrix(…) aufgerufen, sondern der Parameter confusion_matrix der Funktion plot_confusion_matrix(…), den wir in Zeile 3 definiert haben.
      Wenn wir dann in Zeile 39 die Funktion plot_confusion_matrix(confusion_mtx,…) aufrufen, übergeben wir die Variable confusion_mtx.
      Hinter confusion_mtx verbirgt sich der Datentyp numpy.ndarray, den wir vorher in Zeile 37 (confusion_mtx = confusion_matrix(labels_true, labels_pred)) erstellen haben.
      Der Datentyp numpy.ndarray bietet uns zahlreiche Attribute und Funktion [https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html], darunter auch das Attribut shape.
      Ich hoffe ich konnte dir weiterhelfen und einen kleinen Überblick verschaffen.

  2. Hallo
    Durch den Kommentar von Tobias ist mir aufgefallen, dass das Bild der Konfusion Matrix versehentlich nicht mit eingebettet war. Jetzt haben wir es noch mit eingefügt.
    Gruß
    Johannes

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.