Machine Learning – ein Appetizer!

Deep Learning, Neuronale Netze, Decision Trees — immer mehr Begriffe aus dem Gebiet des maschinellen Lernens schaffen es aktuell in die Medien. Tatsächlich werden regelmäßig erstaunliche Anwendungen präsentiert – zuletzt etwa der unerwartete Sieg des Programms AlphaGo über einen europäischen Profi des beliebten ostasiatischen Brettspiels Go.

Abseits der gesteigerten medialen Aufmerksamkeit verrichten maschinelle Lernalgorithmen allerdings schon seit Jahren zuverlässig ihren Dienst, ohne dass wir es besonders beachten: Etwa bei der Spam-Erkennung, bei Suchvorschlägen und Übersetzungsdiensten sowie bei Produktvorschlägen des Online-Händlers unserer Wahl. Ob bei der Analyse von Marketingprozessen und Produktstrategien im E-Commerce oder bei der Planung von Lagerbeständen in der Logistik: Immer häufiger werden Lernalgorithmen verwendet, um vorgegebene Fragestellungen durch statistische Analyse vorhandener Daten automatisiert zu beantworten und zu optimieren. Die Grundlage hierfür bilden die automatisch erfassten Daten, die etwa bei Verkaufsprozessen anfallen.

Immer mehr Open Source Tools verfügbar

Eine bemerkenswerte Entwicklung findet allerdings gerade auf einem anderen Terrain statt: Immer mehr Bibliotheken für maschinelles Lernen werden unter Open Source-Lizenzen veröffentlicht. So haben im letzten Jahr sowohl Microsoft mit CNTK als auch Google mit Tensorflow Ergebnisse ihrer Forschungsabteilungen der Allgemeinheit zur Verfügung gestellt. Für Data Scientists ist das natürlich ein Glücksfall: So ist es heute einfach geworden, schnell und effizient einen Prototyp zur Lösung eines gegebenen Problems zu entwickeln. Gleichzeitig ist man nicht an teure Lizenzprodukte gebunden.

„We live in a golden age of machine learning, where very powerful algorithms are available as black boxes that produce good results“  – Russ Thompson – Senior Research Scientist Alexa

Nehmen wir dieses Zitat doch einfach mal ernst: In diesem Beitrag wollen wir anhand eines einfachen Beispiels demonstrieren, wie schnell sich Lernalgorithmen heute anwenden lassen. Wir verwenden dazu frei verfügbare Python-Bibliotheken wie Scikit-Learn und wollen versuchen, auf einer Autobahn fahrende LKWs auf Webcam-Bildern zu erkennen. Genauer: Wir möchten der Maschine beibringen, für ein solches Bild zu entscheiden, ob auf ihm ein LKW zu sehen ist oder nicht. In der Fachterminologie spricht man hier von einem binärem Klassifikationsproblem.

Beispiel: LKWs erkennen

Solche Webcam-Bilder werden zum Beispiel vom Landesbetrieb Straßen.NRW auf verkehr.nrw zur Verfügung gestellt. Die Bilder sind laut Landesdatenschutzbeauftragtem unbedenklich, weder Kennzeichen noch Insassen lassen sich aufgrund der geringen Auflösung auf ihnen erkennen. Untersucht man den Quellcode der Webseite (besonders einfach mit den Firefox-Entwickler-Tools), so findet man schnell die direkte URL für eine der Webcams. Wir wählen ein Exemplar in der Nähe unseres Standorts in Essen, denn hier überprüfe ich häufig, ob ich einen Stau umfahren sollte.

Leere Autobahn A52

Es wäre natürlich schön, wenn die Autobahn immer so leer wäre… aber zurück zum Thema: Mittels

urllib.request.urlretrieve(url, path)

lässt sich in Python 3 sehr einfach ein Bild aus einer URL speichern. Wir brauchen zum Trainieren eines Lernalgorithmus natürlich einige Bilder, mehrere hundert sollten es schon sein. Hier muss also etwas gesammelt werden. Wir gehen im Folgenden davon aus, dass genügend viele Bilder vorhanden sind. Diese sollten aufgeteilt werden: ein Verzeichnis mit mehreren hundert Trainingsbildern, die wir von Hand klassifizieren müssen und ein Verzeichnis mit Testbildern, auf die wir unseren Klassifikator später zur Vorhersage anwenden wollen.

Arbeiten mit IPython, Scikit-Learn und Scikit-Image

Wir arbeiten außerdem mit dem IPython Notebook, um uns die Bilder direkt grafisch anzeigen lassen zu können. IPython (bzw. neuerdings Jupyter) Notebook ist ein vielseitiges Hilfsmittel für die explorative Datenanalyse und interaktive Berechnungen — ohne Installation kann es auch online bei Wakari ausprobiert werden. Neben Scikit-Learn verwenden wir auch Scikit-Image zur Verarbeitung der Bilder.

Logos: IPython, Scikit-Learn, Scikit-Image

Unser IPython Notebook beginnt mit folgenden Vorbereitungen:

# Vorbereitungen
# Plots und Bilder direkt im IPython Notebook anzeigen:
%pylab inline 

# Importe
import skimage
from skimage import io
import sklearn
import math
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import time
import os

# Lokale Pfade der Webcam-Bilder
trainings_bilder_pfad="~/Skripte/webcam_a52/train_data/"
test_bilder_pfad= "~/Skripte/webcam_a52/test_data/"

Anschließend definieren wir eine hilfreiche Funktion um Bilder anzeigen zu können. Außerdem müssen wir für den Trainingsteil der gesammelten Bilder Label vergeben: 1=ja (LKW drauf zu sehen), 0=nein (kein LKW). Auch hierfür schreiben wir uns eine kleine helfende Funktion:

from IPython import display

# Zeigt nacheinander Bilder an und fragt nach Labels:
def manual_label_images(images, default_label=0):
    labels=[]
    for image in images:
        io.imshow(image)
        display.clear_output(wait=True)
        display.display(plt.gcf())
        my_label=input("Label (Enter="+str(default_label)+"):")
        try:
            my_label_int=int(my_label)
        except ValueError:
            my_label_int=default_label
        labels.append(my_label_int)
    return labels

# Zeige viele Bilder als Bilder-Matrix an,  columns=0 für nur eine Zeile"""
def show_images(images, titles=None, columns=0, fontsize=26):
    n_ims = len(images)
    if columns==0:
        rows=1
        columns=n_ims
    else:
        rows=int(math.ceil(n_ims / columns))
    
    if titles is None: titles = ['({%d})'.format(i) for i in range(1,n_ims + 1)]
    fig = plt.figure()
    k = 1
    for image,title in zip(images, titles):
        a = fig.add_subplot(rows, columns, k)  
        if image.ndim == 2: # Is image grayscale?
            plt.gray()
        plt.imshow(image)
        a.set_title(title, size=fontsize)
        k += 1
    fig.set_size_inches(np.array(fig.get_size_inches()) * columns)
    plt.show()

Wir wollen die Bilder noch komfortabel aus den Verzeichnissen einlesen können. Sehr einfach geht das mit der Funktion imread_collection aus skimage.io. Wir nutzen glob um die Bilder über Muster wie „*.jpg“ einlesen zu können:

import glob

def images_from_dir(glob_pattern):
    filenames=glob.glob(glob_pattern)
    return np.array(filenames), io.imread_collection(filenames).concatenate()

Auch die Labels möchten wir, wenn wir sie einmal vergeben haben, zusammen mit den Dateinamen in einer csv-Datei speichern, welche wir wieder für das Einlesen verwenden. Dafür nutzen wir übrigens einen Pandas DataFrame und die Methoden to_csv und read_csv:

def import_labels(label_csv_file):
    df=pd.read_csv(label_csv_file)
    return pd.DataFrame({'labels':list(df.iloc[:,1])},index=df.iloc[:,0])

def export_labels(df, label_csv_file):
    df.to_csv(label_csv_file)

Die Bilder sollten zumindest ein wenig aufbereitet werden. Die unten definierte Funktion prepare_images schneidet die Bilder etwas kleiner, wandelt sie in Graustufen um und erhöht die Kontraste (equalize_hist). Scikit-Image stellt uns hier die benötigten Methoden zur Verfügung. Mehr Auf- und Vorbereitung der Bilddaten wollen wir an dieser Stelle nicht betreiben, auch wenn es hier sicher genügend Verbesserungsmöglichkeiten gibt.

Für unsere minimalistische Vorgehensweise wird das Bild nur noch in ein eindimensionales Array umgewandelt, um es als Trainingsdatensatz/-vektor verwenden zu können. Hier hilft NumPy’s reshape Funktion. Damit wir es später auch wieder anzeigen können, fügen wir noch eine Funktion recreate_image hinzu, die ein zweidimensionales Array aus dem eindimensionalen Datensatz erzeugt.

from skimage.color import rgb2gray
from skimage.exposure import equalize_hist

def prepare_images(img_coll):
    # sehr einfache Aufbereitung: 
    # in Graustufen umwandeln und Helligkeit mitteln
    image_array=np.array([equalize_hist(rgb2gray(x[50:,:-30,:]))
        for x in img_coll ])
    w,h = original_shape = tuple(image_array[0].shape)
    return np.array([np.reshape(x,(w*h)) for x in image_array])

# um später aus 1-dimensionalen Arrays wieder Bilder zu machen:
def recreate_image(image_1d_array, w, h):
    image = np.zeros((w, h))
    k = 0
    for i in range(w):
        for j in range(h):
            image[i][j] = image_1d_array[k]
            k += 1
    return image

Labeln, Trainieren, Lernen

Nachdem wir jetzt eine Sammlung kleiner Helfer beisammen haben, können wir mit dem eigentlichen Prozess loslegen. Wir lesen erst einmal unsere Bilder ein:

# Trainingsbilder einlesen
training_filenames,training_color_images=images_from_dir(
    trainings_bilder_pfad+"2016*.jpg")
training_images=prepare_images(training_color_images)
# Testbilder einlesen
test_filenames, test_color_images=images_from_dir(
    test_bilder_pfad+"2016*.jpg")
test_images=prepare_images(test_color_images)

Die manuelle Vergabe der Labels für unsere Trainingsbilder läuft dann etwa wie folgt ab:

label_file=os.path.join(trainings_bilder_pfad,"labels.csv")
labels1=manual_label_images(training_color_images[0:50])
...
man_labels=labels1+labels2+labels3+labels4+labels5+labels6+labels7+labels8
l_new=pd.DataFrame({'labels':list(man_labels)}, index=training_filenames)
export_labels(l_new, label_file)

Wie man sieht, habe ich die Trainingsbilder in separaten Untermengen manuell klassifiziert, und abschließend das Ergebnis zusammengefügt. Die Vergabe der Label müssen wir natürlich nur einmal machen. Bei zukünftigen Aufrufen wollen wir die Label einfach aus der csv-Datei laden. Das funktioniert via

label_file=os.path.join(trainings_bilder_pfad,"labels.csv")
if os.path.isfile(label_file):
    print('Lade')
    labels=import_labels(label_file)
else:
    labels=pd.DataFrame()
# Reihenfolge angleichen, indem wir Labels und Trainingsbilder 
# in einen DataFrame mit den Dateinamen als Index packen.    
df_images=pd.DataFrame({'images': list(training_images)},
    index=training_filenames)
train_data=pd.concat((labels,df_images),axis=1)
# Zur Kontrolle lassen wir uns das Anzeigen:
show_images([recreate_image(x,190,290) 
    for x in train_data['images'][:10]],
    titles=train_data['labels'][:10], columns=3)

Kommen wir nun zum spannenden Teil, dem Lernalgorithmus. Wir verwenden hier die lineare Support Vector Maschine, so wie sie in Scikit-Learn vorkonfiguriert ist. An den Parametern des Algorithmus ändern wir nichts:

import time
from sklearn import svm
clf = svm.LinearSVC()
t0 = time.time()
clf.fit(list(train_data['images']), list(train_data['labels'])) 
print("Lerndauer:", time.time()-t0)

Das Lernen dauert bei 380 Trainingsbildern ca. 32 Sekunden. Nun wollen wir sehen, was die Maschine für unsere Testbilder vorhersagt. Wir lassen uns die Ergebnisse mitsamt den zugehörigen Bildern für die ersten 20 Testbilder anzeigen:

new_labels = clf.predict(test_images)
start=0
stop=20
show_images([recreate_image(x,190,290) 
    for x in test_images[start:stop]],
    new_labels[start:stop], columns=5)

Hier die Vorhersage für die ersten 20 Testbilder:
20_test_images

Das Ergebnis ist trotz des simplen Ansatzes gar nicht so schlecht: In den ersten 20 Testbildern sind beide LKWs erkannt worden. Für 3 Bilder hat die Vorhersage-Funktion LKWs erkannt, obwohl keine zu sehen sind. Die Fehlerquote auf allen Testbildern liegt bei ca 20%. Wenn alle Bilder (Trainings- und Testbilder) bei wolkigem Wetter aufgenommen wurden, ist das Ergebnis sogar besser. Das liegt wahrscheinlich an den dann nicht vorhandenen, im Tagesverlauf „wandernden“, Baum- und Böschungsschatten.

Natürlich gibt es hier jede Menge Möglichkeiten zur Optimierung. Für professionelle Bildanalyse und Objekterkennung sind noch einmal andere Herangehensweisen und Algorithmen notwendig. Für eine kleine Spielerei sind die Ergebnisse aber erstaunlich gut und zeigen das große Potenzial der verwendeten Python-Bibliotheken.

Beitragsbild Quelle: Wikipedia, CC BY-SA 3.0

Schreibe einen Kommentar

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