Ein Bot lernt fahren... ========================= Eines der größten Probleme bei diesem Projekt war die Erstellung von geeigneten Trainingsdaten. In diesem Fall von Bildern, die verschiedene Fälle wie "geradeaus Fahren" oder "stehen bleiben" abbilden. Dabei besteht die Schwierigkeit, dass der Roboter einen geeigneten Untergrund benötigt. Dieser soll es ihm ermöglichen, die prägnantesten visuellen Merkmale der Straße zu erlernen. Bietet der Untergrund keinen ausreichenden Kontrast, so kann er keine Merkmale erlernen. Um diesem Anspruch gerecht zu werden, benutzen wir Lego-Straßen. Die Lego-Platten haben in der Mitte eine graue Fahrbahn, die außen durch einen Grünstreifen abgegrenzt wird. Mit diesem kontrastreichem Bild sollten ausreichend Merkmale erlernbar sein. Szenario 1: Binäre Klassifikation -------------------------------------- .. raw:: html

Download: Beispieldaten + Code

In diesem ersten Szenario möchten wir zwei Fälle unterscheiden: - Der Bot erkennt eine leere Straße und fährt geradeaus. .. image:: res/fw1.png - Der Bot erkennt ein Hindernis und bleibt stehen (Kollisionsvermeidung). .. image:: res/stop1.png Wir importieren alle nötigen Module, definieren die Dimensionen der verwendeten Bilder und verweisen auf die Pfade der Trainings- und Validierungsdaten: .. code-block:: python import os import numpy as np from time import time from keras.callbacks import TensorBoard from keras.models import Sequential from keras.layers import Activation, Dropout, Flatten, Dense from keras.preprocessing.image import ImageDataGenerator from keras.layers import Convolution2D, MaxPooling2D, ZeroPadding2D from keras import optimizers from keras import applications from keras.models import Model # Dimensionen der Bilder img_width, img_height = 160,120 # Pfadangaben der Trainings- und Validierungsdaten train_data_dir = './data/train' validation_data_dir = './data/validation' Im nächsten Schritt werden die Bilder vorverarbeitet. Wir reduzieren die Pixelwerte, die im RGB-Farbraum zwischen 0 und 255 liegen zu einem Intervall von 0 bis 1 und legen die Batch Size fest. Wir werden später untersuchen, ob die Größe der `Batch Size `_ eine Auswirkung auf das trainierte Modell hat. Anschließend holen wir die Bilder und ihre Klassen. .. code-block:: python datagen = ImageDataGenerator(rescale=1./255) batch_size = 128 train_generator = datagen.flow_from_directory( train_data_dir, target_size=(img_width, img_height), batch_size=batch_size, class_mode='binary') validation_generator = datagen.flow_from_directory( validation_data_dir, target_size=(img_width, img_height), batch_size=batch_size, class_mode='binary') Wir schreiben einen einfachen Stapel von drei convolutionellen Layern mit einer ReLU `Aktivierungsfunktion `_, gefolgt von `Max-Pooling Layern `_. Anschließend kompilieren wir das Modell und legen dabei die Parameter fest: Die Kostenfunktion ist eine binary_crossentropy, der Optimizer ist "rmsprop", metrics ist "accuracy". .. code-block:: python model = Sequential() model.add(Convolution2D(32, (3, 3), \ input_shape=(img_width, img_height,3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Convolution2D(32, (3, 3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Convolution2D(64, (3, 3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(64)) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(1)) model.add(Activation('sigmoid')) model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) Nach diesem dreier Stapel von Convolutions folgt die eigentliche Klassifizierungsarbeit. Flatten überführt die mehrdimensionalen Vektoren aus den Convolutions in eindimensionale Vektoren. `Dropout `_ reduziert das Overfitting. Abschließend legen wir fest, wieviele Epochen wir berechnen möchten, wie viele Trainings- und Validierungsdaten wir haben und starten dann ein `TensorBoard `_, um die Berechnungen zu visualisieren. In der letzten Zeile speichern wir das Modell. Auf dem verwendeten Rechner (AMD RyZen 1700X mit 16 logischen Kerne @ 3,9 GHZ und 32GB DDR4) rechnet das Modell ca. 10 Minuten. TensorFlow ist für die Instruktionen `SSE `_, `AVX `_ und `FMA `_ kompiliert. Wenn TensorFlow diese Befehlssätze benutzen kann, verringert sich die Rechenzeit enorm. .. code-block:: python epochs = 50 train_samples = 1342 validation_samples = 403 tensorboard = TensorBoard(log_dir="logs/{}".format(time())) model.fit_generator( train_generator, steps_per_epoch=train_samples // batch_size, epochs=epochs, validation_data=validation_generator, validation_steps=validation_samples // batch_size, callbacks=[tensorboard]) model.save('./data/models/beispiel1.h5') **Ergebnisse** **50 Epochen** Batch Size = 128 .. image:: res/acc1.png .. image:: res/valacc1.png .. image:: res/loss1.png .. image:: res/valloss1.png **Vorhersage eines Bildes** .. code-block:: python from keras.models import load_model from keras.preprocessing import image import numpy as np img_width, img_height = 160, 120 model = load_model('./data/models/beispiel1.h5') model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) img = image.load_img('stop2.png', target_size=(img_width, img_height)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) classes = model.predict_classes(x, batch_size=128) print(classes) **200 Epochen** Wie bereits nach 50 Epochen zu erkennen ist, ändern sich die berechneten Werte nicht mehr. Sie erreichen eine Genauigkeit von ca. 98%. Trotzdem haben wir einen Test mit 200 Epochen durchlaufen lassen. Dieser bestätigt das bereits ermittelte Verhalten. Die Batch Size haben wir mit 128 eingestellt. .. image:: res/acc1_200.png .. image:: res/valacc1_200.png .. image:: res/loss1_200.png .. image:: res/valloss1_200.png **Modell Plotten** .. code-block:: python from keras.utils import plot_model plot_model(model, to_file='model.png', show_shapes=True) Die letzten beiden Zeilen plotten das verwendete TensorFlow Modell: .. image:: res/model1.png :height: 500px :width: 250px **Batch Sizes** Alle Graphen werden im TensorBoard mit Smoothing = 0.6 dargestellt. Die ausgeblichenen Graphen im Hintergrund stellen die Graphen ohne Smoothing dar. Betrachten wir die Graphen für die unterschiedlichen Batch Sizes mit 50 Epochen: - Orange: 128 - Lila: 256 .. image:: res/acc_256.png .. image:: res/val_acc_256.png .. image:: res/loss_256.png .. image:: res/val_loss_256.png Nun nehmen wir noch die Graphen für die Batch Size = 10 in die Auswertung: - Dunkel Blau: 10 - Orange: 128 - Lila: 256 .. image:: res/acc_10.png .. image:: res/val_acc_10.png .. image:: res/loss_10.png .. image:: res/val_loss_10.png Es lässt sich ablesen, dass sehr kleine und sehr große Batch Sizes zu großen Ausreißern in den Graphen führen. Dennoch erreichen alle drei Batch Sizes ähnlich gute Ergebnisse und würden sich für ein zuverlässiges Modell eignen. Es ist jedoch festzuhalten, dass ein mittlerer Wert, wie es 128 ist, zu einem ausgewogenen Verlauf führt. **ReLu vs. Sigmoid** Wir ändern die Aktivierungsfunktionen in den Convolutions von ReLu zu Sigmoid: .. code-block:: python model = Sequential() model.add(Convolution2D(32, (3, 3), \ input_shape=(img_width, img_height,3))) model.add(Activation('sigmoid')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Convolution2D(32, (3, 3))) model.add(Activation('sigmoid')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Convolution2D(64, (3, 3))) model.add(Activation('sigmoid')) model.add(MaxPooling2D(pool_size=(2, 2))) .. image:: res/acc_sig.png .. image:: res/val_acc_sig.png .. image:: res/loss_sig.png .. image:: res/val_loss_sig.png Die eingesetzten Sigmoidaktivierungsfunktionen sorgen dafür, dass das Training wesentlich länger dauert. Das Netz erreicht erst nach 200 Epochen ähnliche Werte wie bei der Verwendung von ReLu. **Anzahl der "Stapel"** Bislang haben wir das Netz mittels drei Stapeln der Abfolge 1. Convolution2D, 2. Activation, 3. MaxPooling2D trainiert. Benutzen wir diese Abfolge häufiger bzw. geringer, ergibt dies folgenden Verlauf: -Orange: Dreier Stapel -Lila: Fünfer Stapel -Braun: Einer Stapel .. image:: res/acc_3_5.png .. image:: res/val_acc_3_5.png .. image:: res/loss_3_5.png .. image:: res/val_loss_3_5.png Wird nur ein Stapel benutzt, so zeigt der Graph deutliche Ausreißer. Der Unterschied zwischen fünf und drei Stapeln ist hingegen marginal. Dies zeigt, warum Bilderkennung auf "Deep Learning", also viele aufeinander folgende Schichten angewiesen ist. Szenario 2: Multi-Class Klassifikation ------------------------------------------- .. raw:: html

Download: Beispieldaten + Code

Im zweiten Szenario möchten wir drei Fälle unterscheiden: - Der Bot erkennt eine leere Straße und fährt geradeaus. .. image:: res/fw1.png - Der Bot erkennt eine Links- oder Rechtskurve und fährt in die entsprechende Richtung. .. image:: res/left.png .. image:: res/right.png Für eine Multi-Klassifikation muss der bisherige Code ein wenig abgeändert werden: - Wir setzen den class_mode beim Generator auf "categorical", - die vorletzte Aktivierungsfunktion ist vom Typ "ReLu", - die letzte Aktivierungsfunktion ist vom Typ "Softmax", - wir setzten nb_classes = 3 und - kompilieren das Modell mit der Kostenfunktion = "categorical_crossentropy". Des Weiteren benutzen wir einen Stapel von drei convolutionellen Layern und trainieren 100 Epochen. .. code-block:: python import os import numpy as np from time import time from keras.callbacks import TensorBoard from keras.models import Sequential from keras.layers import Activation, Dropout, Flatten, Dense from keras.preprocessing.image import ImageDataGenerator from keras.layers import Convolution2D, MaxPooling2D, ZeroPadding2D from keras import optimizers from keras import applications from keras.models import Model from keras.utils.np_utils import to_categorical img_width, img_height = 160,120 train_data_dir = './data/train' validation_data_dir = './data/validation' datagen = ImageDataGenerator(rescale=1./255) batch_size = 128 train_generator = datagen.flow_from_directory( train_data_dir, target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical') validation_generator = datagen.flow_from_directory( validation_data_dir, target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical') model = Sequential() model.add(Convolution2D(32, (3, 3), input_shape=(img_width, img_height,3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model = Sequential() model.add(Convolution2D(32, (3, 3), input_shape=(img_width, img_height,3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model = Sequential() model.add(Convolution2D(32, (3, 3), input_shape=(img_width, img_height,3))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(64)) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(3)) model.add(Activation('softmax')) model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy']) nb_epochs = 100 nb_train_samples = 3000 nb_validation_samples = 600 nb_classes = 3 tb = TensorBoard(log_dir="logs/{}".format(time())) model.fit_generator( train_generator, steps_per_epoch=nb_train_samples // batch_size, epochs=nb_epochs, validation_data=validation_generator, validation_steps=nb_validation_samples // batch_size, callbacks=[tb] ) model.save('./data/models/beispiel2.1.h5') **Ergebnisse** Das Ergebnis hat uns überrascht: Auch dieses Modell erlangt eine Genauigkeit von ca. 99%! Für die drei Klassen haben wir jeweils 1000 Bilder hinterlegt und jeweils 200 davon für die Validierung benutzt. Die Berechnung dauerte hierbei stolze 27 Minuten. Der Videobeweis ist auf der Startseite zu sehen. .. image:: res/acc99.png .. image:: res/vall_acc99.png .. image:: res/loss99.png .. image:: res/val_loss99.png Fazit ---------------------------------------- In dieser Arbeit haben wir gezeigt, dass mit Hilfe von Convolutional Neural Networks die Umsetzung eines autonomen Fahrzeugs möglich ist. Die größte Schwierigkeit war hierbei nicht die Programmierleistung, sondern das Erzeugen von geeigneten Trainingsbildern. Die Bilder müssen markante Unterschiede aufweisen, um erlernbar zu sein. Des Weiteren ist die Wahl der für diesen Fall richtigen Aktivierungsfunktionen und Kostenfunktionen entscheidend. Die Berechnungen für das autonome Fahren werden aufgrund der Rechenzeit nicht auf dem Auto, sondern auf dem Desktop-Rechner ausgeführt. Die Daten werden dazu über WLAN kommuniziert. Dennoch dauert die Berechnung der Vorhersage eine gewisse Zeit, sodass das Auto für die im Video dargestellten drei Runden ca. eine Stunde Fahrtzeit benötigt hat. Es ist erstaunlich, wie gut das Auto die Strecke bewältigt und auch begangene Fehler korrigiert. Dabei können wir jedoch nicht beurteilen, ob dies tatsächlich Fahrfehler sind oder ob wir diese Fehler dem Auto "beigebracht" haben. .. raw:: html