KI-Werkbank¶
MNIST 99,64% - Mit einem einfachen Convolutional Neuronal Network (CNN) unter die Top 10%¶
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.
#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_1 (Conv2D) (None, 28, 28, 32) 320 _________________________________________________________________ batch_normalization_1 (Batch (None, 28, 28, 32) 128 _________________________________________________________________ conv2d_2 (Conv2D) (None, 28, 28, 32) 9248 _________________________________________________________________ batch_normalization_2 (Batch (None, 28, 28, 32) 128 _________________________________________________________________ conv2d_3 (Conv2D) (None, 28, 28, 32) 25632 _________________________________________________________________ batch_normalization_3 (Batch (None, 28, 28, 32) 128 _________________________________________________________________ conv2d_4 (Conv2D) (None, 28, 28, 64) 18496 _________________________________________________________________ batch_normalization_4 (Batch (None, 28, 28, 64) 256 _________________________________________________________________ conv2d_5 (Conv2D) (None, 28, 28, 64) 36928 _________________________________________________________________ batch_normalization_5 (Batch (None, 28, 28, 64) 256 _________________________________________________________________ conv2d_6 (Conv2D) (None, 28, 28, 64) 102464 _________________________________________________________________ batch_normalization_6 (Batch (None, 28, 28, 64) 256 _________________________________________________________________ conv2d_7 (Conv2D) (None, 28, 28, 32) 18464 _________________________________________________________________ dropout_1 (Dropout) (None, 28, 28, 32) 0 _________________________________________________________________ flatten_1 (Flatten) (None, 25088) 0 _________________________________________________________________ dropout_2 (Dropout) (None, 25088) 0 _________________________________________________________________ dense_1 (Dense) (None, 128) 3211392 _________________________________________________________________ dropout_3 (Dropout) (None, 128) 0 _________________________________________________________________ dense_2 (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')
# serialize model to JSON
model_json = model.to_json()
with open("persist_model/final_augmenting_cnn_MNIST_model.json", "w") as json_file:
json_file.write(model_json)
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("")
Abb 1: Anstieg der Genauigkeit ü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')
Abb 3: Confusion Matrix
Hier sind alle falsch erkannten Vier - Neun Kombinationen zu sehen
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.
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.
SCHLAGWÖRTER¶
CNN, Convolution, KI, AI, ML, Keras, TensorFlow, DNN, Deep Learning, Neuronale Netze, Tutorial, MNIST, Dens Layer, Conv2D, Maschinelles Sehen, #KI-Werkbank
BESCHREIBUNG¶
Wie kann man mit einfachsten Mitteln zu den Top MNIST Modellen kommen.
KATEGORIEN¶
Architektur, BI & DWH, Blog, Business Intelligence, KI