Über die Integration symbolischer Inferenz in tiefe neuronale Netze

Tiefe neuronale Netze waren in den letzten Jahren eine enorme Erfolgsgeschichte. Viele Fortschritte im Bereich der KI, wie das Erkennen von Objekten, die fließende Übersetzung natürlicher Sprache oder das Spielen von GO auf Weltklasseniveau, basieren auf tiefen neuronalen Netzen. Über die Grenzen dieses Ansatzes gab es jedoch nur wenige Berichte. Eine dieser Einschränkungen ist die Unfähigkeit, aus einer kleinen Anzahl von Beispielen zu lernen. Tiefe neuronale Netze erfordern in der Regel eine Vielzahl von Trainingsbeispielen, während der Mensch aus nur einem einzigen Beispiel lernen kann. Wenn Sie eine Katze einem Kind zeigen, das noch nie zuvor eine gesehen hat, kann es eine weitere Katze anhand dieser einzigen Instanz erkennen. Tiefe neuronale Netze hingegen benötigen Hunderttausende von Bildern, um zu erlernen, wie eine Katze aussieht. Eine weitere Einschränkung ist die Unfähigkeit, Rückschlüsse aus bereits erlerntem Allgemeinwissen zu ziehen. Beim Lesen eines Textes neigen Menschen dazu, weitreichende Rückschlüsse auf mögliche Interpretationen des Textes zu ziehen. Der Mensch ist dazu in der Lage, weil er Wissen aus sehr unterschiedlichen Bereichen abrufen und auf den Text anwenden kann.

Diese Einschränkungen deuten darauf hin, dass in tiefen neuronalen Netzen noch etwas Grundsätzliches fehlt. Dieses Etwas ist die Fähigkeit, symbolische Bezüge zu Entitäten in der realen Welt herzustellen und sie in Beziehung zueinander zu setzen. Symbolische Inferenz in Form von formaler Logik ist seit Jahrzehnten der Kern der klassischen KI, hat sich jedoch als spröde und komplex in der Anwendung erwiesen. Gibt es dennoch keine Möglichkeit, tiefe neuronale Netze so zu verbessern, dass sie in der Lage sind, symbolische Informationen zu verarbeiten? Tiefe neuronale Netzwerke wurden von biologischen neuronalen Netzwerken wie dem menschlichen Gehirn inspiriert. Im Wesentlichen sind sie ein vereinfachtes Modell der Neuronen und Synapsen, die die Grundbausteine des Gehirns ausmachen. Eine solche Vereinfachung ist, dass statt mit zeitlich begrenzten Aktionspotenzialen nur mit einem Aktivierungswert gearbeitet wird. Aber was ist, wenn es nicht nur wichtig ist, ob ein Neuron aktiviert wird, sondern auch, wann genau. Was wäre, wenn der Zeitpunkt, zu dem ein Neuron feuert, einen relationalen Kontext herstellt, auf den sich diese Aktivierung bezieht? Nehmen wir zum Beispiel ein Neuron, das für ein bestimmtes Wort steht. Wäre es nicht sinnvoll, wenn dieses Neuron jedes Mal ausgelöst würde, wenn das Wort in einem Text erscheint? In diesem Fall würde das Timing der Aktionspotenziale eine wichtige Rolle spielen. Und nicht nur das Timing einer einzelnen Aktivierung, sondern auch das Timing aller eingehenden Aktionspotenziale eines Neurons relativ zueinander wäre wichtig. Dieses zeitliche Muster kann verwendet werden, um eine Beziehung zwischen diesen Eingangsaktivierungen herzustellen. Wenn beispielsweise ein Neuron, das ein bestimmtes Wort repräsentiert, eine Eingabesynapse für jeden Buchstaben in diesem Wort hat, ist es wichtig, dass das Wort Neuron nur dann ausgelöst wird, wenn die Buchstabenneuronen in der richtigen Reihenfolge zueinander abgefeuert wurden. Konzeptionell könnten diese zeitlichen Unterschiede als Relationen zwischen den Eingangssynapsen eines Neurons modelliert werden. Diese Relationen definieren auch den Zeitpunkt, zu dem das Neuron selbst im Verhältnis zu seinen Eingangsaktivierungen feuert. Aus praktischen Gründen kann es sinnvoll sein, der Aktivierung eines Neurons mehrere Slots zuzuordnen, wie z.B. den Anfang und das Ende eines Wortes. Andernfalls müssten Anfang und Ende eines Wortes als zwei getrennte Neuronen modelliert werden. Diese Relationen sind ein sehr mächtiges Konzept. Sie ermöglichen es, die hierarchische Struktur von Texten einfach zu erfassen oder verschiedene Bereiche innerhalb eines Textes miteinander in Beziehung zu setzen. In diesem Fall kann sich ein Neuron auf eine sehr lokale Information beziehen, wie z.B. einen Buchstaben, oder auf eine sehr weitreichende Information, wie z.B. das Thema eines Textes.

Eine weitere Vereinfachung im Hinblick auf biologische neuronale Netze besteht darin, dass mit Hilfe einer Aktivierungsfunktion die Feuerrate eines einzelnen Neurons angenähert wird. Zu diesem Zweck nutzen klassische neuronale Netze die Sigmoidfunktion. Die Sigmoidfunktion ist jedoch symmetrisch bezüglich großer positiver oder negativer Eingangswerte, was es sehr schwierig macht, ausssagenlogische Operationen mit Neuronen mit der Sigmoidfunktion zu modellieren. Spiking-Netzwerke hingegen haben einen klaren Schwellenwert und ignorieren alle Eingangssignale, die unterhalb dieses Schwellenwerts bleiben. Daher ist die ReLU-Funktion oder eine andere asymmetrische Funktion eine deutlich bessere Annäherung für die Feuerrate. Diese Asymmetrie ist auch für Neuronen unerlässlich, die relationale Informationen verarbeiten. Das Neuron, das ein bestimmtes Wort repräsentiert, muss nämlich für alle Zeitpunkte, an denen das Wort nicht vorkommt, völlig inaktiv bleiben.

Ebenfalls vernachlässigt wird in tiefen neuronalen Netzwerken die Tatsache, dass verschiedene Arten von Neuronen in der Großhirnrinde vorkommen. Zwei wichtige Typen sind die bedornte Pyramidenzelle, die in erster Linie eine exzitatorische Charakteristik aufweist, und die nicht bedornte Sternzelle, die eine hemmende aufweist. Die inhibitorischen Neuronen sind besonders, weil sie es ermöglichen, negative Rückkopplungsschleifen aufzubauen. Solche Rückkopplungsschleifen finden sich normalerweise nicht in einem tiefen neuronalen Netzwerk, da sie einen inneren Zustand in das Netzwerk einbringen. Betrachten wir das folgende Netzwerk mit einem hemmenden Neuron und zwei exzitatorischen Neuronen, die zwei verschiedene Bedeutungen des Wortes “August” darstellen.

Beide Bedeutungen schließen sich gegenseitig aus, so dass das Netzwerk nun zwei stabile Zustände aufweist. Diese Zustände können von weiteren Eingangssynapsen der beiden exzitatorischen Neuronen abhängen. Wenn beispielsweise das nächste Wort nach dem Wort ‘August’ ein potenzieller Nachname ist, könnte eine entsprechende Eingabesynapse für das Entitätsneuron August-(Vorname) das Gewicht dieses Zustands erhöhen. Es ist nun wahrscheinlicher, dass das Wort “August” als Vorname und nicht als Monat eingestuft wird. Aber bedenken Sie, dass beide Zustände evaluiert werden müssen. In größeren Netzwerken können viele Neuronen durch negative oder positive Rückkopplungsschleifen verbunden sein, was zu einer großen Anzahl von stabilen Zuständen im Netzwerk führen kann.

Aus diesem Grund ist ein effizienter Optimierungsprozess erforderlich, der den besten Zustand in Bezug auf eine Zielfunktion ermittelt. Diese Zielfunktion könnte darin bestehen, die Notwendigkeit der Unterdrückung stark aktivierter Neuronen zu minimieren. Diese Zustände haben jedoch den enormen Vorteil, dass sie es erlauben, unterschiedliche Interpretationen eines bestimmten Textes zu berücksichtigen. Es ist eine Art Denkprozess, in dem verschiedene Interpretationen bewertet werden und die jeweils stärkste als Ergebnis geliefert wird. Glücklicherweise lässt sich die Suche nach einem optimalen Lösungszustand recht gut optimieren.

Der Grund, warum wir in diesen Rückkopplungsschleifen hemmende Neuronen benötigen, ist, dass sonst alle gegenseitig unterdrückenden Neuronen vollständig miteinander verbunden sein müssten. Das würde zu einer quadratisch zunehmenden Anzahl von Synapsen führen.

Durch die negativen Rückkopplungsschleifen, d.h. durch einfaches Verbinden einer negativen Synapse mit einem ihrer Vorläuferneuronen, haben wir plötzlich den Bereich der nichtmonotonen Logik betreten. Die nichtmonotone Logik ist ein Teilgebiet der formalen Logik, in dem Implikationen nicht nur zu einem Modell hinzugefügt, sondern auch entfernt werden. Es wird davon ausgegangen, dass eine nichtmonotone Logik erforderlich ist, um Schlussfolgerungen für viele Common Sense Aufgaben ziehen zu können. Eines der Hauptprobleme der nichtmonotonen Logik ist, dass sie oft nicht entscheiden kann, welche Schlussfolgerungen sie ziehen soll und welche eben nicht. Einige skeptische oder leichtgläubige Schlussfolgerungen sollten nur gezogen werden, wenn keine anderen Schlussfolgerungen wahrscheinlicher sind. Hier kommt die gewichtete Natur neuronaler Netze zum Tragen. In neuronalen Netzen können nämlich eher wahrscheinliche Zustände weniger wahrscheinliche Zustände unterdrücken.

Beispielimplementierung innerhalb des Aika-Frameworks

An dieser Stelle möchte ich noch einmal das Beispielneuron für das Wort ‘der’ vom Anfang aufgreifen. Das Wort-Neuron besteht aus drei Eingabesynapsen, die sich jeweils auf die einzelnen Buchstaben des Wortes beziehen. Über die Relationen werden die Eingabesynapsen nun zueinander in eine bestimmte Beziehung gesetzt, so dass das Wort ‘der’ nur erkannt wird, wenn alle Buchstaben in der korrekten Reihenfolge auftreten.
Als Aktivierungsfunktion des Neurons wird hier der im negativen Bereich abgeschnittene (rectified) hyperbolische Tangens verwendet. Dieser hat gerade bei einem UND-verknüpfenden Neuron den Vorteil, dass er selbst bei sehr großen Werten der gewichteten Summe auf den Wert 1 begrenzt ist. Alternativ kann auch die ReLU-Funktion (Rectified Linear Unit) verwendet werden. Diese eignet sich insbesondere für ODER-verknüpfende Neuronen, da sie die Eingabewerte unverzerrt weiterleitet.
Im Gegensatz zu herkömmlichen neuronalen Netzen gibt es hier mehrere Bias Werte, einen für das gesamte Neuron (in diesem Fall auf 5.0 gesetzt) und einen für jede Synapse. Intern werden diese Werte zu einem gemeinsamen Bias aufsummiert. Es ist schon klar, dass dieses Aufteilen des Bias nicht wirklich gut zu Lernregeln wie der Delta-Rule und dem Backpropagation passt, allerdings eignen sich diese Lernverfahren eh nur sehr begrenzt für diese Art von neuronalem Netzwerk. Als Lernverfahren kommen eher von den natürlichen Mechanismen Langzeit-Potenzierung und Langzeit-Depression inspirierte Ansätze in Betracht.

Fazit

Obwohl tiefe neuronale Netze bereits einen langen Weg zurückgelegt haben und mittlerweile beeindruckende Ergebnisse liefern, kann es sich doch lohnen, einen weiteren Blick auf das Original, das menschliche Gehirn und seine Schaltkreise zu werfen. Wenn eine so inhärent komplexe Struktur wie das menschliche Gehirn als Blaupause für ein neuronales Modell verwendet werden soll, müssen vereinfachende Annahmen getroffen werden. Allerdings ist bei diesem Prozess Vorsicht geboten, da sonst wichtige Aspekte des Originals verloren gehen können.

Referenzen

  1. Der Aika-Algorithm
    Lukas Molzberger
  2. Neuroscience: Exploring the Brain
    Mark F. Bear, Barry W. Connors, Michael A. Paradiso
  3. Neural-Symbolic Learning and Reasoning: A Survey and Interpretation
    Tarek R. Besold, Artur d’Avila Garcez, Sebastian Bader; Howard Bowman, Pedro Domingos, Pascal Hitzler, Kai-Uwe Kuehnberger, Luis C. Lamb, ; Daniel Lowd, Priscila Machado Vieira Lima, Leo de Penning, Gadi Pinkas, Hoifung Poon, Gerson Zaverucha
  4. Deep Learning: A Critical Appraisal
    Gary Marcus
  5. Nonmonotonic Reasoning
    Gerhard Brewka, Ilkka Niemela, Mirosław Truszczynski

IIIb. Einführung in TensorFlow: Realisierung eines Perzeptrons mit TensorFlow

In [1]:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

# Reset des TensorFlows
tf.reset_default_graph() 

Daten laden und eigene Definitionen

In [2]:
data = pd.read_csv('data_train.csv')
input_X = data[['x0', 'x1']]
input_y = data.y

data_test = pd.read_csv('data_test.csv')
test_X = data_test[['x0', 'x1']]
test_y = data_test.y

Damit unser Modell schneller lernt, teilen wir unseren Datensatz in Stapel ein. Dafür erstellen wir eine Funktion, welche unseren Datensatz in Stapel teilt!

Je nach Datensatz und Modell empfehlt sich eine andere Stapelgröße.

In [3]:
def stapel_erstellen(X, Y, stapel_grosse, p_index):
    return X[stapel_grosse * p_index: stapel_grosse * (p_index + 1)], Y[stapel_grosse * p_index: stapel_grosse * (p_index + 1)]

Erstellen des Graphen

Formen der Tensoren

In [4]:
# Anzahl der Ergebnissspalten
anz_unit = 1
# Anzahl der Eingänge bzw. Merkmale 
anz_ein = 2
# Anzahl der Ausgänge
anz_aus = 1

Parameter zur Steuerung des Graphen

Die richtige Wahl der Parameter zur Steuerung des Graphen sind entscheidend, wenn es darum geht, wie schnell ein Modell lernt. Wenn wir zum Beispiel anz_stapel=10 statt anz_stapel=5 nutzen, dann brauch unser Modell länger um eine Genauigkeit von 100 % zu erreichen, wenn überhaupt.

In [5]:
# Lernrate
eta = 0.1
# Anzahl der der Pakete mit den zu analysierenden Datenwerte
anz_stapel = 5
# Anzahl der zu analysierenden Datenwerte
stapel_grosse = int(len(input_X)/anz_stapel)
# Anzahl der Wiederholungen
epochen = 50

Relevante Größen

In [6]:
# Eingangssignal
x = tf.placeholder(tf.float32, shape=[None, anz_ein],name='Input')  # Stapelgröße(k) x 2
# Ausgangssignal
y_true = tf.placeholder(tf.float32, shape=None, name='Labels')  # Stapelgröße(k) x 1
# Gewichte
w = tf.Variable(tf.random_normal([anz_ein, anz_unit]), name='Weights')  # 2x1

Berechnungsgleichungen

In der Theorie sind wir immer nur einen Datenpunkt in Betracht gezogen. In TensorFlow wollen wir jedoch einen Stapel betrachten. Dadurch ändert sich die Berechnung ein wenig. Wir berechnen für alle Punkte eine Fehlerfunktion. Der Mittelwert aller Fehlerfunktionen, die Kostenfunktion, soll dann optimiert werden.

In [7]:
# z = xw
z = tf.matmul(x, w, name='Z')
# H = y * -log(sigmoid(z)) + (1 - y) * -log(1 - sigmoid(z)) -> Kreuzentropie
err = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true, logits=z),name='Costfunction')
# Minimieren der Fehlerfunktion
opt = tf.train.GradientDescentOptimizer(learning_rate=eta).minimize(err)

# Berechnung der Genauigkeit
eins = tf.reshape(tf.round(tf.sigmoid(z)),[len(test_X), 1])
zwei = tf.reshape(y_true,[len(test_X), 1])
acc = tf.equal(eins, zwei)
acc = tf.reduce_mean(tf.cast(acc, tf.float32), name='Accuracy')

Ausführung des Graphen

Bei der Ausführung ist es wichtig, dass wir die Variablen initialisieren. Auch ist es vorteilhaft, wenn wir die Session mit with starten.

In [8]:
# Größen zur späteren Datenvisualisierung
W_set = []
Err_set = []
Acc_set = []
# Initialisierung der Variablen
init = tf.global_variables_initializer()
# Ausführung des Graphen
with tf.Session() as sess:
    # Wichtig für TensorBoard
    writer = tf.summary.FileWriter('./graphs/perceptron', sess.graph)
    sess.run(init)
    # Schleife für Epoche
    for e in range(epochen):
        # Schleife für Stapel
        for i in range(anz_stapel):
            # Einteilen unserer Daten in Stapel
            stapel_x, stapel_y = stapel_erstellen(X=input_X,
                                                  Y=input_y,
                                                  stapel_grosse=stapel_grosse,
                                                  p_index=i)
            # Ausführung der Berechnung
            Z, W, _, Err = sess.run([z, w, opt, err],
                                    feed_dict={x: stapel_x, y_true: stapel_y})

        # Datenspeicherung für Visualisierung über die Epochen
        W_set.append(W)
        Err_set.append(np.mean(Err))
        Acc = sess.run([acc],
                       feed_dict={x: test_X, y_true: test_y})
        Acc_set.append(Acc)
        print('{:}. Epoche Genauigkeit: {:.2f} %'.format(e, Acc[0]*100))
    sess.close()
0. Epoche Genauigkeit: 50.75 %
1. Epoche Genauigkeit: 65.00 %
2. Epoche Genauigkeit: 80.75 %
3. Epoche Genauigkeit: 93.00 %
4. Epoche Genauigkeit: 97.75 %
5. Epoche Genauigkeit: 98.75 %
6. Epoche Genauigkeit: 99.75 %
7. Epoche Genauigkeit: 100.00 %
8. Epoche Genauigkeit: 100.00 %
9. Epoche Genauigkeit: 100.00 %
10. Epoche Genauigkeit: 100.00 %
11. Epoche Genauigkeit: 100.00 %
12. Epoche Genauigkeit: 100.00 %
13. Epoche Genauigkeit: 100.00 %
14. Epoche Genauigkeit: 100.00 %
15. Epoche Genauigkeit: 100.00 %
16. Epoche Genauigkeit: 100.00 %
17. Epoche Genauigkeit: 100.00 %
18. Epoche Genauigkeit: 100.00 %
19. Epoche Genauigkeit: 100.00 %
20. Epoche Genauigkeit: 100.00 %
21. Epoche Genauigkeit: 100.00 %
22. Epoche Genauigkeit: 100.00 %
23. Epoche Genauigkeit: 100.00 %
24. Epoche Genauigkeit: 100.00 %
25. Epoche Genauigkeit: 100.00 %
26. Epoche Genauigkeit: 100.00 %
27. Epoche Genauigkeit: 100.00 %
28. Epoche Genauigkeit: 100.00 %
29. Epoche Genauigkeit: 100.00 %
30. Epoche Genauigkeit: 100.00 %
31. Epoche Genauigkeit: 100.00 %
32. Epoche Genauigkeit: 100.00 %
33. Epoche Genauigkeit: 100.00 %
34. Epoche Genauigkeit: 100.00 %
35. Epoche Genauigkeit: 100.00 %
36. Epoche Genauigkeit: 100.00 %
37. Epoche Genauigkeit: 100.00 %
38. Epoche Genauigkeit: 100.00 %
39. Epoche Genauigkeit: 100.00 %
40. Epoche Genauigkeit: 100.00 %
41. Epoche Genauigkeit: 100.00 %
42. Epoche Genauigkeit: 100.00 %
43. Epoche Genauigkeit: 100.00 %
44. Epoche Genauigkeit: 100.00 %
45. Epoche Genauigkeit: 100.00 %
46. Epoche Genauigkeit: 100.00 %
47. Epoche Genauigkeit: 100.00 %
48. Epoche Genauigkeit: 100.00 %
49. Epoche Genauigkeit: 100.00 %
In [9]:
w_0, w_1 = zip(*W_set)
fig, ax = plt.subplots(3,1, figsize=(15,30), sharex='all')
ax[0].plot(range(len(W_set)), w_0, label='w0')
ax[0].plot(range(len(W_set)), w_1, label='w1')
ax[0].legend()
ax[0].grid()
ax[0].set_title('Gewichte')

ax[1].plot(range(len(W_set)), Err_set, c='r', label='err')
ax[1].legend()
ax[1].set_title('Fehlerfunktion')
ax[1].grid()

ax[2].plot(range(len(W_set)), Acc_set, c='g', label='acc')
ax[2].legend()
ax[2].set_title('Genauigkeit')
ax[2].set_xlabel('Epoche')
ax[2].grid()

Zusammenfassung

Nun haben wir unser Perzeptron erfolgreich mit TensorFlow realisiert. Um ein Gefühl zu bekommen, könnt ihr gerne mit den "Parameter zur Steuerung des Graphen" herumexperimentieren. Je nach Auswahl der Parameter ändert sich die Optimierung und sogar die Genauigkeit unseres Modells. Bei so einfachen Daten, sollte unser Modell definitiv 100% Genauigkeit erreichen. Dies ist jedoch nur möglich, wenn wir die richtigen Parameter wählen. Probiert es also einfach mal aus.

PS: Wenn ihr die Trainings- und Testdaten sucht, dann werdet ihr auf Github fündig.

IIIa. Einführung in TensorFlow: Realisierung eines Perzeptrons mit TensorFlow

1. Einleitung

1.1. Was haben wir vor?

Im zweiten Artikel dieser Serie sind wir darauf eingegangen, wie man TensorFlow prinzipiell nutzt. Wir wollen das Gelernte an einem einfachen Modell anwenden. Bevor wir dies jedoch tun, müssen wir die Theorie hinter dem Modell verstehen um TensorFlow richtig anwenden zu können.

Dafür bietet sich ein Adaline-Perzeptron sehr gut an. Es ist ein einfaches Modell mit nur einer Schicht, wo die Theorie verständlich ist.

1.2. Aufgabenstellung

Abb.1 Trainingsdaten: Grün \rightarrow Label 0, Rot
\rightarrow Label 1

In Abb.1 sehen wir unsere Trainingsdaten, die
zufällig generiert wurden. Alle grün markierten Datenpunkte haben das Label 0 und die rot markierten Punkte erhalten das Label 1. 

Wir möchten einen Adaline-Perzeptron entwickeln, der unsere Daten  je nach Position in die richtige Klasse zuordnet. Somit haben wir eine Aufgabe mit binärer Klassifikation

2. Grundlagen

2.1. Funktionsweise eines Perzeptrons

Ein Perzeptron ist ein mathematisches Modell, welches eine Nervenzelle beschreiben soll.

Abb.2 Schematische Darstellung einer Nervenzelle und ihren Bestandteilen

Vereinfacht funktioniert eine Nervenzelle, auch Neuron genannt, folgendermaßen: Eine Vielzahl von Reizen bzw. Eingabesignalen wird von den Dendriten aufgenommen, die dann im Kern verarbeitet werden. Wenn die verschiedenen Eingabesignale die ’richtige’ Dosis an Reizen erreichen und einen Schwellwert erreichen, dann feuert das Neuron ab und leitet ein Signal weiter. 

Für eine detaillierte Beschreibung, wie ein Perzeptron mathematisch beschrieben wird, möchte ich auf diesen Artikel hinweisen.

Wir wollen uns in diesem Artikel auf den Adaline-Algorithmus (ADAptive LINear Element) konzentrieren. Dieser ist eine Weiterentwicklung des Perzeptron. Die Besonderheit an diesem Algorithmus liegt darin, dass das Konzept der Fehlerminimierung durch Minimierung der Straffunktion der berechneten und der tatsächlichen Ergebnisse enthält. Ein weiter wesentlicher Unterschied zu einem einfachen Perzeptron ist vor allem, dass wir bei Adaline keine einfache Sprungfunktion als Aktivierungsfunktion haben, sondern eine stetige Funktion nutzen und somit eine Differenzierung/Ableitung der Aktivierungsfunktion durchführen können. Dieser Punkt ist für die Optimierung der Gewichte und des Lernens unseres Modells ein entscheidender Vorteil.

Das Schema in Abb.3 zeigt uns die Funktionsweise, wie unser Adaline-Algorithmus funktionieren soll.

Abb.3 Schematische Darstellung des Adaline-Perzeptrons

  1. Eingang: In dieser Schicht werden unsere Daten ein gepfangen und weitergeleitet
  2. Die Gewichte geben an, welchen Einfluss unsere Eingangssignale haben. Sie sind auch unsere Größe, die in unserem Algorithmus optimiert werden.
  3. Die Nettoeingabefunktion wird durch die Zusammenführung von Eingangssignalen und Gewichten erzeugt. Je nachdem wie die Eingänge und Gewichte verbunden sind,  müssen diese mathematisch korrekt multipliziert werden.
  4. Die Nettoeingabe wird dann, in die Aktivierungsfunktion eingebunden. Je nachdem welche Aktivierungsfunktion man nutzt, ändert sich die Ausgabe nach der Aktivierungsfunktion. 
  5.  In der Fehlerrückgabe werden die vorhergesagten Ausgaben mit den tatsächlichen Werten/Labels verglichen. Auch hier gibt es verschiedene Verfahren, um eine Fehlerfunktion zu bilden. 
  6. In der Optimierung werden dann auf Basis der Fehlerfunktion die Gewichte so optimiert, dass der Fehler zwischen unseren Label und den vorhergesagten Werten minimiert wird.
  7. Der Quantisierer ist ein optionales Element. Bei einer kategorischen Problemstellung bekommen wir nach der Aktivierungsfunktion eine Wahrscheinlichkeit zu der die Daten zu welchem Label zugeteilt werden. Der Quantisierer wandelt diese Wahrscheinlichkeiten zu Labeln um. Zum Beispiel haben wir einen Datensatz und unser Modell sagt voraus, dass dieser Datensatz zu 88 % das Label 1 hat. Je nachdem welche Grenze dem Quantisierer gegeben wird, teilt dieser dann den Datensatz in die entsprechende Klasse ein. Wenn wir sagen die Grenze soll 50% sein, dann sagt der Quantisierer, dass unser Datensatz Label 1 ist.

2.2. Aktivierungsfunktionen

Die Aktivierungsfunktion ist ein sehr wichtiger Bestandteil bei neuronalen Netzen. Diese bestimmen, wie sich das Ausgangssignal verhält. Es gibt eine Vielzahl von Aktivierungsfunktionen, die ihre Vor- und Nachteile haben. Wir wollen uns erstmal auf die Sigmoidfunktion konzentrieren.

Eigentlich haben wir bei der Sprungfunktion alles was wir brauchen. Wenn wir einen Schwellenwert erreichen z \geq 0, dann feuert die Sprungfunktion und das sehr abrupt. Die Sigmoidfunktion hingegen hat einen sanfteren und natürlicheren Verlauf als die Sprungfunktion. Außerdem ist sie eine stetig und differenzierbare Funktion, was sehr vorteilhaft für das Gradientenverfahren (Optimierung) ist. Daher wollen wir die Sigmoidfunktion für unsere Problemstellung nutzen.

    \begin{align*} \text{sig}(z) = \frac{1}{1 + e^{-z}}\end{align*}

Abb.4 Sigmoid-Funktion mit ihrer Ableitung und deren Sättigungsbereichen

2.3. Optimierungsverfahren

2.3.1. Fehlerfunktion

Die wohl am häufigsten genutzten Fehlerfunktionen (oder auch Ziel-, Kosten-, Verlust-, Straffunktion) sind wohl der mittlere quadratische Fehler bei Regressionen und die Kreuzentropie bei kategorischen Daten.

In unserem Beispiel haben wir Daten kategorischer Natur und eine binäre Thematik, weshalb wir uns auf die Kreuzentropie in Kombination mit der Sigmoidfunktion konzentrieren wollen.

Aus der Matrizenrechnung t (z =\boldsymbol{xw}^T) erhalten wir ein Skalar (eindimensional). Geben wir diese in die Sigmoidfunktion ein, kommen wir auf folgende Gleichung.



    \begin{align*} \text{sig}(z=\boldsymbol{xw}^T) = \frac{1}{1 + e^{-\boldsymbol{xw}^T}} \end{align*}


Hinweis: Wie in Abb.4 kann die Sigmoidfunktion nur Werte zwischen 0 und 1 erreichen, ohne diese jemals zu erreichen. Außerdem ändert sich die Funktion bei sehr großen Beträgen nur noch minimal, man spricht auch von Sättigung. Dieser Fakt ist sehr wichtig, wenn um die Optimierung der Gewichte geht. Wenn wir unsere Nettoeingabe nicht skalieren, dann kann es passieren, dass unser Modell sehr langsam lernt, da der Gradient der Sigmoidfunktion bei großen Beträgen sehr klein ist.

Bei Aufgaben mit binärer Klassifizierung hat sich die Kreuzentropie als Fehlerfunktion etabliert. Sie ist ein Maß für die Qualität eines Modells, welche eine Wahrscheinlichkeitsverteilung angibt. Je kleiner diese Größe ist, desto besser unser Modell. Es gilt also unsere Fehlerfunktion zu minimieren!

Wir wollen in einem separaten Artikel genauer auf die Kreuzentropie eingehen. Für den jetzigen Zeitpunkt soll es reichen, wenn wir die Formel vor Augen haben und was sie grob bedeutet.

P = \{p_1,p_2,\dots,p_N\} sei die ‘wahre’ Wahrscheinlichkeitsverteilung aus der Menge X = \{x_1,x_2,\dots,x_N\}, in unserem Fall, die Wahrscheinlichkeitsverteilung, ob ein Datenpunkt dem Label 0 oder 1 zugehört. Wenn wir nun unser Eingangssignal durch die Aktivierungsfunktion fließen lassen, dann erhalten wir ebenfalls eine ‘berechnete’ Wahrscheinlichkeitsverteilung die Q = \{q_1,q_2,\dots,q_N\} genannt werden soll. Um die Wahrscheinlichkeitsverteilungen p und q zu vergleichen, nutzen wir die Kreuzentropie, welche wie folgt für diskrete Daten definiert ist:

    \begin{align*}\log_2{x}&= \operatorname{ld}(x) \\H(P;Q) &= - \sum{P \cdot \operatorname{ld}(Q)}\\H(P;Q) &= -p_1 \operatorname{ld}(q_1) - p_2  \operatorname{ld}(q_2)\end{align*}

Beispiel einer binären Problemstellung. Wir haben unsere Label 0 und 1. p1 ist die Wahrscheinlichkeit, inwiefern unser Datenpunkt das Label 0 hat. Da wir die Trainingsdaten kennen, wissen wir auch das dieser Punkt zu 100 %, welches Label hat. Unser Modell hat zum Beispiel im ersten Durchgang eine Wahrscheinlichkeit von 0.8 und später 0.9 berechnet.

Fall I : P = Q Die Wahrscheinlichkeitsverteilungen P und Q sind identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_0 &= \{q_1 = 1.0, q_2 = 0.0 \} \\ \\H_{0}(P;Q_I) &= -1.0 \operatorname{ld}(1) -0.0 \operatorname{ld}(0.0) = 0.0\\\end{align*}

Fall II: P \neq Q Die Wahrscheinlichkeitsverteilungen P und Q sind nicht identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_{1} &= \{q_1 = 0.8, q_2 = 0.2 \} \\ Q_{2} &= \{q_1 = 0.9, q_2 = 0.1 \} \\ Q_{3} &= \{q_1 = 0.99, q_2 = 0.01 \} \\ \\H_{1}(P;Q_{1}) &= -1.0 \operatorname{ld}(0.8) -0.0 \operatorname{ld}(0.2) = 0.3219 \\H_{2}(P;Q_{2}) &= -1.0 \operatorname{ld}(0.9) -0.0 \operatorname{ld}(0.1) = 0.1520 \\ H_{3}(P;Q_{3}) &= -1.0 \operatorname{ld}(0.99) -0.0 \operatorname{ld}(0.01) = 0.0144\\\end{align*}

In der oberen Berechnung haben wir zum einfachen Verständnis der Kreuzentropie ein einfaches Beispiel. p_1 ist eine 100 % ige  Wahrscheinlichkeit, dass zum Beispiel unser Datensatz das Label 0 hat. Unser perfektes Modell mit Q_0 hat eine Kreuzentropie-Wert von 0. Unser zweites Modell  H_1(P;Q1) hat eine gewisse Unbestimmtheit, die sich durch eine größere Kreuzentropie H_1 = 0.1520 bemerkbar macht. Je mehr sich also unser Modell von den wirklichen Daten abweicht, desto größer ist die Kreuzentropie.

2.3.2. Optimierung nach dem Gradientenverfahren

Wenn wir es also schaffen die Kreuzentropie zu minimieren, dann erhalten wir auch ein besseres Modell! Bei der Optimierung nach dem Gradientenverfahren versuchen wir uns schrittweise an das Minimum zu bewegen.

    \begin{align*}H(P;Q) &= H(y; \varPhi(z)) \\            &= H(y; \text{sig}(z))\\             &= H(y; \text{sig}(xw))\\H' &= \frac{\partial H}{\partial w} \rightarrow Min.\end{align*}

Ziel der Optimierung ist es, dass unsere Gewichte so angepasst werden, dass sich der Fehler in unserer Fehlerfunktion minimiert. Wir leiten also die Fehlerfunktion nach w ab. 

Diese Aufgabe wird zum Glück von TensorFlow übernommen und wir müssen die Randbedingungen nur dem System geben.

Neben dem Gradientenverfahren, gibt es auch noch eine Menge anderer Optimierer, auf die wir später nochmal eingehen werden.

3. Zusammenfassung

Bevor wir TensorFlow nutzen, ist es wichtig, dass wir unser Modell verstehen. TensorFlow ist wie vieles nur ein Werkzeug, wenn man die Grundlagen nicht verstanden hat. Daher haben wir uns in diesem Artikel erstmal auf die Theorie konzentriert und ich habe dabei versucht mich auf das Wesentliche zu beschränken. 

Im nächsten Artikel werden wir dann unser Modell in TensorFlow realisieren.

PS: In einem separaten Artikel wollen später nochmal detaillierter auf Aktivierungsfunktion, Kreuzentropie und das Gradientenverfahren eingehen.

Language Detecting with sklearn by determining Letter Frequencies

Of course, there are better and more efficient methods to detect the language of a given text than counting its lettes. On the other hand this is a interesting little example to show the impressing ability of todays machine learning algorithms to detect hidden patterns in a given set of data.

For example take the sentence:

“Ceci est une phrase française.”

It’s not to hard to figure out that this sentence is french. But the (lowercase) letters of the same sentence in a random order look like this:

“eeasrsçneticuaicfhenrpaes”

Still sure it’s french? Regarding the fact that this string contains the letter “ç” some people could have remembered long passed french lessons back in school and though might have guessed right. But beside the fact that the french letter “ç” is also present for example in portuguese, turkish, catalan and a few other languages, this is still a easy example just to explain the problem. Just try to guess which language might have generated this:

“ogldviisnntmeyoiiesettpetorotrcitglloeleiengehorntsnraviedeenltseaecithooheinsnstiofwtoienaoaeefiitaeeauobmeeetdmsflteightnttxipecnlgtetgteyhatncdisaceahrfomseehmsindrlttdthoaranthahdgasaebeaturoehtrnnanftxndaeeiposttmnhgttagtsheitistrrcudf”

While this looks simply confusing to the human eye and it seems practically impossible to determine the language it was generated from, this string still contains as set of hidden but well defined patterns from which the language could be predictet with almost complete (ca. 98-99%) certainty.

First of all, we need a set of texts in the languages our model should be able to recognise. Luckily with the package NLTK there comes a big set of example texts which actually are protocolls of the european parliament and therefor are publicly availible in 11 differen languages:

  •  Danish
  •  Dutch
  •  English
  •  Finnish
  •  French
  •  German
  •  Greek
  •  Italian
  •  Portuguese
  •  Spanish
  •  Swedish

Because the greek version is not written with the latin alphabet, the detection of the language greek would just be too simple, so we stay with the other 10 languages availible. To give you a idea of the used texts, here is a little sample:

“Resumption of the session I declare resumed the session of the European Parliament adjourned on Friday 17 December 1999, and I would like once again to wish you a happy new year in the hope that you enjoyed a pleasant festive period.
Although, as you will have seen, the dreaded ‘millennium bug’ failed to materialise, still the people in a number of countries suffered a series of natural disasters that truly were dreadful.”

Train and Test

The following code imports the nessesary modules and reads the sample texts from a set of text files into a pandas.Dataframe object and prints some statistics about the read texts:

Above you see a sample set of random rows of the created Dataframe. After removing very short text snipplets (less than 200 chars) we are left with 56481 snipplets. The function clean_eutextdf() then creates a lower case representation of the texts in the coloum ‘ltext’ to facilitate counting the chars in the next step.
The following code snipplet now extracs the features – in this case the relative frequency of each letter in every text snipplet – that are used for prediction:

Now that we have calculated the features for every text snipplet in our dataset, we can split our data set in a train and test set:

After doing that, we can train a k-nearest-neigbours classifier and test it to get the percentage of correctly predicted languages in the test data set. Because we do not know what value for k may be the best choice, we just run the training and testing with different values for k in a for loop:

As you can see in the output the reliability of the language classifier is generally very high: It starts at about 97.5% for k = 1, increases for with increasing values of k until it reaches a maximum level of about 98.5% at k ≈ 10.

Using the Classifier to predict languages of texts

Now that we have trained and tested the classifier we want to use it to predict the language of example texts. To do that we need two more functions, shown in the following piece of code. The first one extracts the nessesary features from the sample text and predict_lang() predicts the language of a the texts:

With this classifier it is now also possible to predict the language of the randomized example snipplet from the introduction (which is acutally created from the first paragraph of this article):

The KNN classifier of sklearn also offers the possibility to predict the propability with which a given classification is made. While the probability distribution for a specific language is relativly clear for long sample texts it decreases noticeably the shorter the texts are.

Background and Insights

Why does a relative simple model like counting letters acutally work? Every language has a specific pattern of letter frequencies which can be used as a kind of fingerprint: While there are almost no y‘s in the german language this letter is quite common in english. In french the letter k is not very common because it is replaced with q in most cases.

For a better understanding look at the output of the following code snipplet where only three letters already lead to a noticable form of clustering:

 

Even though every single letter frequency by itself is not a very reliable indicator, the set of frequencies of all present letters in a text is a quite good evidence because it will more or less represent the letter frequency fingerprint of the given language. Since it is quite hard to imagine or visualize the above plot in more than three dimensions, I used a little trick which shows that every language has its own typical fingerprint of letter frequencies:

What more?

Beside the fact, that letter frequencies alone, allow us to predict the language of every example text (at least in the 10 languages with latin alphabet we trained for) with almost complete certancy there is even more information hidden in the set of sample texts.

As you might know, most languages in europe belong to either the romanian or the indogermanic language family (which is actually because the romans conquered only half of europe). The border between them could be located in belgium, between france and germany and in swiss. West of this border the romanian languages, which originate from latin, are still spoken, like spanish, portouguese and french. In the middle and northern part of europe the indogermanic languages are very common like german, dutch, swedish ect. If we plot the analysed languages with a different colour sheme this border gets quite clear and allows us to take a look back in history that tells us where our languages originate from:

As you can see the more common letters, especially the vocals like a, e, i, o and u have almost the same frequency in all of this languages. Far more interesting are letters like q, k, c and w: While k is quite common in all of the indogermanic languages it is quite rare in romanic languages because the same sound is written with the letters q or c.
As a result it could be said, that even “boring” sets of data (just give it a try and read all the texts of the protocolls of the EU parliament…) could contain quite interesting patterns which – in this case – allows us to predict quite precisely which language a given text sample is written in, without the need of any translation program or to speak the languages. And as an interesting side effect, where certain things in history happend (or not happend): After two thousand years have passed, modern machine learning techniques could easily uncover this history because even though all these different languages developed, they still have a set of hidden but common patterns that since than stayed the same.

Einstieg in Natural Language Processing – Teil 2: Preprocessing von Rohtext mit Python

Dies ist der zweite Artikel der Artikelserie Einstieg in Natural Language Processing.

In diesem Artikel wird das so genannte Preprocessing von Texten behandelt, also Schritte die im Bereich des NLP in der Regel vor eigentlichen Textanalyse durchgeführt werden.

Tokenizing

Um eingelesenen Rohtext in ein Format zu überführen, welches in der späteren Analyse einfacher ausgewertet werden kann, sind eine ganze Reihe von Schritten notwendig. Ganz allgemein besteht der erste Schritt darin, den auszuwertenden Text in einzelne kurze Abschnitte – so genannte Tokens – zu zerlegen (außer man bastelt sich völlig eigene Analyseansätze, wie zum Beispiel eine Spracherkennung anhand von Buchstabenhäufigkeiten ect.).

Was genau ein Token ist, hängt vom verwendeten Tokenizer ab. So bringt NLTK bereits standardmäßig unter anderem BlankLine-, Line-, Sentence-, Word-, Wordpunkt- und SpaceTokenizer mit, welche Text entsprechend in Paragraphen, Zeilen, Sätze, Worte usw. aufsplitten. Weiterhin ist mit dem RegexTokenizer ein Tool vorhanden, mit welchem durch Wahl eines entsprechenden Regulären Ausdrucks beliebig komplexe eigene Tokenizer erstellt werden können.

Üblicherweise wird ein Text (evtl. nach vorherigem Aufsplitten in Paragraphen oder Sätze) schließlich in einzelne Worte und Interpunktionen (Satzzeichen) aufgeteilt. Hierfür kann, wie im folgenden Beispiel z. B. der WordTokenizer oder die diesem entsprechende Funktion word_tokenize() verwendet werden.

Stemming & Lemmatizing

Andere häufig durchgeführte Schritte sind Stemming sowie Lemmatizing. Hierbei werden die Suffixe der einzelnen Tokens des Textes mit Hilfe eines Stemmers in eine Form überführt, welche nur den Wortstamm zurücklässt. Dies hat den Zweck verschiedene grammatikalische Formen des selben Wortes (welche sich oft in ihrer Endung unterscheiden (ich gehe, du gehst, er geht, wir gehen, …) ununterscheidbar zu machen. Diese würden sonst als mehrere unabhängige Worte in die darauf folgende Analyse eingehen.

Neben bereits fertigen Stemmern bietet NLTK auch für diesen Schritt die Möglichkeit sich eigene Stemmer zu programmieren. Da verschiedene Stemmer Suffixe nach unterschiedlichen Regeln entfernen, sind nur die Wortstämme miteinander vergleichbar, welche mit dem selben Stemmer generiert wurden!

Im forlgenden Beispiel werden verschiedene vordefinierte Stemmer aus dem Paket NLTK auf den bereits oben verwendeten Beispielsatz angewendet und die Ergebnisse der gestemmten Tokens in einer Art einfachen Tabelle ausgegeben:

Sehr ähnlich den Stemmern arbeiten Lemmatizer: Auch ihre Aufgabe ist es aus verschiedenen Formen eines Wortes die jeweilige Grundform zu bilden. Im Unterschied zu den Stemmern ist das Lemma eines Wortes jedoch klar als dessen Grundform definiert.

Vokabular

Auch das Vokabular, also die Menge aller verschiedenen Worte eines Textes, ist eine informative Kennzahl. Bezieht man die Größe des Vokabulars eines Textes auf seine gesamte Anzahl verwendeter Worte, so lassen sich hiermit Aussagen zu der Diversität des Textes machen.

Außerdem kann das auftreten bestimmter Worte später bei der automatischen Einordnung in Kategorien wichtig werden: Will man beispielsweise Nachrichtenmeldungen nach Themen kategorisieren und in einem Text tritt das Wort „DAX“ auf, so ist es deutlich wahrscheinlicher, dass es sich bei diesem Text um eine Meldung aus dem Finanzbereich handelt, als z. B. um das „Kochrezept des Tages“.

Dies mag auf den ersten Blick trivial erscheinen, allerdings können auch mit einfachen Modellen, wie dem so genannten „Bag-of-Words-Modell“, welches nur die Anzahl des Auftretens von Worten prüft, bereits eine Vielzahl von Informationen aus Texten gewonnen werden.

Das reine Vokabular eines Textes, welcher in der Variable “rawtext” gespeichert ist, kann wie folgt in der Variable “vocab” gespeichert werden. Auf die Ausgabe wurde in diesem Fall verzichtet, da diese im Falle des oben als Beispiel gewählten Satzes den einzelnen Tokens entspricht, da kein Wort öfter als ein Mal vorkommt.

Stopwords

Unter Stopwords werden Worte verstanden, welche zwar sehr häufig vorkommen, jedoch nur wenig Information zu einem Text beitragen. Beispiele in der beutschen Sprache sind: der, und, aber, mit, …

Sowohl NLTK als auch cpaCy bringen vorgefertigte Stopwordsets mit. 

Vorsicht: NLTK besitzt eine Stopwordliste, welche erst in ein Set umgewandelt werden sollte um die lookup-Zeiten kurz zu halten – schließlich muss jedes einzelne Token des Textes auf das vorhanden sein in der Stopworditerable getestet werden!

POS-Tagging

POS-Tagging steht für „Part of Speech Tagging“ und entspricht ungefähr den Aufgaben, die man noch aus dem Deutschunterricht kennt: „Unterstreiche alle Subjekte rot, alle Objekte blau…“. Wichtig ist diese Art von Tagging insbesondere, wenn man später tatsächlich strukturiert Informationen aus dem Text extrahieren möchte, da man hierfür wissen muss wer oder was als Subjekt mit wem oder was als Objekt interagiert.

Obwohl genau die selben Worte vorkommen, bedeutet der Satz „Die Katze frisst die Maus.“ etwas anderes als „Die Maus frisst die Katze.“, da hier Subjekt und Objekt aufgrund ihrer Reihenfolge vertauscht sind (Stichwort: Subjekt – Prädikat – Objekt ).

Weniger wichtig ist dieser Schritt bei der Kategorisierung von Dokumenten. Insbesondere bei dem bereits oben erwähnten Bag-of-Words-Modell, fließen POS-Tags überhaupt nicht mit ein.

Und weil es so schön einfach ist: Die obigen Schritte mit spaCy

Die obigen Methoden und Arbeitsschritte, welche Texte die in natürlicher Sprache geschrieben sind, allgemein computerzugänglicher und einfacher auswertbar machen, können beliebig genau den eigenen Wünschen angepasst, einzeln mit dem Paket NLTK durchgeführt werden. Dies zumindest einmal gemacht zu haben, erweitert das Verständnis für die funktionsweise einzelnen Schritte und insbesondere deren manchmal etwas versteckten Komplexität. (Wie muss beispielsweise ein Tokenizer funktionieren der den Satz “Schwierig ist z. B. dieser Satz.” korrekt in nur einen Satz aufspaltet, anstatt ihn an jedem Punkt welcher an einem Wortende auftritt in insgesamt vier Sätze aufzuspalten, von denen einer nur aus einem Leerzeichen besteht?) Hier soll nun aber, weil es so schön einfach ist, auch das analoge Vorgehen mit dem Paket spaCy beschrieben werden:

Dieser kurze Codeabschnitt liest den an spaCy übergebenen Rohtext in ein spaCy Doc-Object ein und führt dabei automatisch bereits alle oben beschriebenen sowie noch eine Reihe weitere Operationen aus. So stehen neben dem immer noch vollständig gespeicherten Originaltext, die einzelnen Sätze, Worte, Lemmas, Noun-Chunks, Named Entities, Part-of-Speech-Tags, ect. direkt zur Verfügung und können.über die Methoden des Doc-Objektes erreicht werden. Des weiteren liegen auch verschiedene weitere Objekte wie beispielsweise Vektoren zur Bestimmung von Dokumentenähnlichkeiten bereits fertig vor.

Die Folgende Übersicht soll eine kurze (aber noch lange nicht vollständige) Übersicht über die automatisch von spaCy generierten Objekte und Methoden zur Textanalyse geben:

Diese „Vollautomatisierung“ der Vorabschritte zur Textanalyse hat jedoch auch seinen Preis: spaCy geht nicht gerade sparsam mit Ressourcen wie Rechenleistung und Arbeitsspeicher um. Will man einen oder einige Texte untersuchen so ist spaCy oft die einfachste und schnellste Lösung für das Preprocessing. Anders sieht es aber beispielsweise aus, wenn eine bestimmte Analyse wie zum Beispiel die Einteilung in verschiedene Textkategorien auf eine sehr große Anzahl von Texten angewendet werden soll. In diesem Fall, sollte man in Erwägung ziehen auf ressourcenschonendere Alternativen wie zum Beispiel gensim auszuweichen.

Wer beim lesen genau aufgepasst hat, wird festgestellt haben, dass ich im Abschnitt POS-Tagging im Gegensatz zu den anderen Abschnitten auf ein kurzes Codebeispiel verzichtet habe. Dies möchte ich an dieser Stelle nachholen und dabei gleich eine Erweiterung des Pakets spaCy vorstellen: displaCy.

Displacy bietet die Möglichkeit, sich Zusammenhänge und Eigenschaften von Texten wie Named Entities oder eben POS-Tagging graphisch im Browser anzeigen zu lassen.

Nach ausführen des obigen Codes erhält man eine Ausgabe die wie folgt aussieht:

Nun öffnet man einen Browser und ruft die URL ‘http://127.0.0.1:5000’ auf (Achtung: localhost anstatt der IP funktioniert – warum auch immer – mit displacy nicht). Im Browser sollte nun eine Seite mit einem SVG-Bild geladen werden, welches wie folgt aussieht

Die Abbildung macht deutlich was POS-Tagging genau ist und warum es von Nutzen sein kann wenn man Informationen aus einem Text extrahieren will. Jedem Word (Token) ist eine Wortart zugeordnet und die Beziehung der einzelnen Worte durch Pfeile dargestellt. Dies ermöglicht es dem Computer zum Beispiel in dem Satzteil “der grüne Apfel”, das Adjektiv “grün” auf das Nomen “Apfel” zu beziehen und diesem somit als Eigenschaft zuzuordnen.

Nachdem dieser Artikel wichtige Schritte des Preprocessing von Texten beschrieben hat, geht es im nächsten Artikel darum was man an Texten eigentlich analysieren kann und welche Analysemöglichkeiten die verschiedenen für Python vorhandenen Module bieten.

II. Einführung in TensorFlow: Grundverständnis für TensorFlow

o. Installation von TensorFlow

Bevor wir richtig durchstarten können, müssen wir natürlich TensorFlow erstmal installieren. Auf dieser Seite findet ihr eine ausführliche Anleitung, wie man TensorFlow auf allen möglichen Systemen installiert. Die nächsten Schritte beschränken sich auf die Installation auf Windows.

o.1.  Installation mit pip

Um TensorFlow zu nutzen, müssen wir diesen Framework auch erstmal installieren. Am einfachsten ist die Installation, wenn ihr Python in reiner Form auf euren Rechner habt. Dann ist es vollkommen ausreichend, wenn ihr folgenden Befehl in eure Eingabeaufforderung(Windows: cmd) eingebt:

Stellt bei dieser Installation sicher, dass ihr keine ältere Version von Python habt als 3.5.x. Außerdem ist es erforderlich, dass ihr pip installiert habt und Python bei euch in der PATH-Umgebung eingetragen ist.Besitzt ihr eine NVIDIA® Grafikkarte so könnt ihr TensorFlow mit GPU Support nutzen. Dazu gebt ihr statt des oben gezeigten Befehls folgendes ein:

o.2. Installation mit Anaconda

Ein wenig aufwendiger wird es, wenn ihr die beliebte Anaconda Distribution nutzt, weil wir da eine Anaconda Umgebung einrichten müssen. Auch hier müssen wir wieder in den Terminal bzw. in die Eingabeaufforderung und folgenden Befehl eingeben:

Tauscht das mit eurer genutzten Version aus.(= 5, 6) Danach aktiviert ihr die erstellte Umgebung:

Nun installieren wir TensorFlow in unsere erstellte Umgebung. Ohne GPU Support

mit GPU Support

Es sei erwähnt, dass das Conda package nur von der Community unterstützt wird, jedoch nicht offiziell seitens Google.

o.3.  Validierung der Installation

Der einfachste Weg um zu überprüfen ob unsere Installation gefruchtet hat und funktioniert können wir anhand eines einfachen Beispiels testen. Dazu gehen wir wieder in den/die Terminal/Eingabeaufforderung und rufen python auf, indem wir python eingeben.


 

1. Grundverständnis für TensorFlow

1.1. Datenstrom-orientierte Programmierung

In diesem Artikel wollen wir näher auf die Funktionsweise von TensorFlow eingehen. Wie wir aus dem ersten Artikel dieser Serie wissen, nutzt TensorFlow das datenstrom-orientierte Paradigma. In diesem wird ein Datenfluss-Berechnungsgraph erstellt, welcher aus Knoten und Kanten besteht. Ein  Datenfluss-Berechnungsgraph, Datenflussgraph oder auch Berechnungsgraph kann mehrere Knoten haben, die wiederum durch die Kanten verbunden sind. In TensorFlow steht jeder Knoten für eine Operation, die Auswirkungen auf eingehende Daten haben.

Abb.1: Knoten und Kanten: Das Eingangssignal wird durch Kanten in den Knoten eingespeist, verändert und ausgegeben

Abb. 1.5: Achterbahn mit fehlender Verbindung [Quelle]

Analogie-Beispiel: Stellt euch vor ihr seid in einem Freizeitpark und habt Lust eine Achterbahn zu fahren. Am Anfang seid ihr vielleicht ein wenig nervös, aber euch geht es noch sehr gut. Wie jeder von euch weiß, hat eine Achterbahn verschiedene Fahrelemente eingebaut, die unsere Emotionen triggern und bei manchen vielleicht sogar auf den Magen schlagen. Diese Elemente sind äquivalent unsere Knoten. Natürlich müssen diese Elemente auch verbunden sein, sonst wäre eine Fahrt mit dieser Achterbahn in meinen Augen nicht empfehlenswert. Diese Verbindungsstücke sind unsere Kanten und somit sind wir die Daten/Signale, die von Knoten zu Knoten durch die Kanten weitergeleitet werden. Schauen wir uns Abb. 2 an, in der eine schematische Darstellung einer fiktiven Achterbahn zu sehen ist, welche mit 4 Fahrelementen dienen kann.

Abb. 2: Oben: Schematische Darstellung eines Datenflussgraphen anhand unserer fiktiven Achterbahn Unten: Unsere fiktive Achterbahn

  1. Airtime-Hügel: Ein Airtime-Hügel erzeugt bei der Überfahrt Schwerelosigkeit und in manchen Fällen ein Abheben aus dem Sitz. Ein guter Einstieg für die Mitfahrer, wie ich finde.
  2. Klassischer Looping: Wir kennen ihn alle, den Looping. Mit hoher Geschwindigkeit geht es in einen vertikalen Kreis hinein und man sich am höchsten Punkt kopfüber befindet.  Für Leute mit nicht so starken Nerven fragen sich spätestens jetzt, warum sie überhaupt mitgefahren sind.
  3. Korkenzieher/Schraube: Der Korkenzieher kann als auseinander gezogener Looping beschrieben werden.
  4. Schraubel-Looping : Und zu guter Letzt kombinieren wir  einen Looping mit einer Schraube! Ein Teil unserer Mitfahrer sucht den nächsten Busch auf, ein anderer Teil will am liebsten nochmal fahren und der Rest wird jetzt einen Pause brauchen.

Fakt ist, dass die Fahrelemente/Knoten unsere anfänglichen Emotionen/Eingangsdatensignale geändert haben.

1.2. Genereller Ablauf in TensorFlow

Anhand unser fiktiven Achterbahn haben wir das Prinzip der datenstrom-orientierten Programmierung eingefangen. Damit wir aber erst einmal Achterbahn fahren können, müssen wir diese konstruieren. Das gilt auch in TensorFlow und können die Arbeit in zwei wesentliche Phasen unterteilen:

  1. Erstellen eines Berechnungsgraphen: Wie auch bei einer Achterbahn müssen wir unser Modell erst einmal modellieren. Je nachdem welche Ressourcen uns zur Verfügung gestellt werden, welche Bedingungen wir folgen müssen, können wir unser Modell darauf aufbauen und gestalten.
  2. Ausführung des Berechnungsgraphen: Nachdem wir das Modell/den Graph fertig konstruiert haben, führen wir diese nun aus, d.h. für unsere Achterbahn, dass wir den Strom anschalten und losfahren können.

2. Erstellung eines Graphen

2.1. TensorFlow-Operatoren

Wie bereits erwähnt können Knoten verschiedene Operationen in sich tragen. Das können z.B. Addition, Substraktion oder aber auch mathematische Hyperbelfunktionen  à la Tangens Hyperbolicus Operatoren sein. Damit TensorFlow mit den Operatoren arbeiten kann, müssen wir diese mit den zur Verfügung gestellten Operatoren von TensorFlow auskommen. Eine vollständige Dokumentation findet ihr hier.

2.2. Platzhalter

Wenn in TensorFlow Daten aus externen Quellen in den Berechnungsgraph integriert werden sollen, dann wird eine eigens dafür entwickelte Struktur genutzt um die Daten einzulesen; dem Platzhalter. Ihr könnt euch den Platzhalter als Wagon unserer Achterbahn vorstellen, der die Mitfahrer (Daten bzw. Tensoren) durch die Achterbahn (Berechnungsgraph) jagt.

Es ist bei der Modellierung eines Berechnungsgraphen nicht notwendig, die Daten am Anfang einzuspeisen. Wie der Name schon sagt, setzt TensorFlow eine ‘leere Größe’ ein, die in der zweiten Phase gefüllt wird.

Eine Frage, die ich mir damals gestellt habe war, warum man einen Platzhalter braucht? Dazu können wir uns wieder unsere Achterbahn nehmen. Bei 2-3 Fahrgästen besteht kein Problem; wir hätten genug Platz/Ressourcen um diese unterzubringen. Aber was machen wir, wenn wir 10.000 Gäste haben, wie es auch in der Realität ist ? Das ist auch bei neuronalen Netzen der Fall, wenn wir zu viele Daten haben, dann stoßen wir irgendwann an unser Leistungslimit. Wir teilen unsere Daten/Gäste so auf, dass wir damit arbeiten können.

2.3. Variable

Stellen wir uns folgendes Szenario vor: Wir haben eine Achterbahn fertig konstruiert – wahrscheinlich die beste und verrückteste Achterbahn, die es jemals gegeben hat. Je nachdem welchen Effekt wir mit unserer Achterbahn erzielen wollen; z.B. ein einfacher Adrenalinschub, ein flaues Gefühl im Magen oder den vollständigen Verlust jeglicher Emotionen aus purer Angst um das eigene Leben, reicht es nicht nur ein schönes Modell zu bauen. Wir müssen zusätzlich verschiedene Größen anpassen um das Erlebnis zu maximieren. Eine wichtige Größe für unsere Achterbahn wäre die Geschwindigkeit (in neuronalen Netzen sind es die Gewichte), die über den Fahrspaß entscheidet. Um die optimale Geschwindigkeit zu ermitteln, müssen viele Versuche gemacht werden (sei es in der Realität oder in der Simulation) und nach jedem Test wird die Geschwindigkeit nach jedem Test angepasst. Zu diesem Zweck sind die Variablen da. Sie passen sich nach jedem Versuch an.

2.4. Optimierung

Damit die Variablen angepasst werden können, müssen wir TensorFlow Anweisungen geben, wie er die Variablen optimiert werden soll. Dafür müssen wir eine Formel an TensoFlow übermitteln, die dann optimiert wird. Auch hat man die Auswahl von verschiedenen Optimierer, die die Aufgabe anders optimieren. Die Wahl der richtigen Formel und des passenden Optimierer ist jedoch eine Sache, die ohne weiteres nicht zu beantworten ist. Wir wollen ein anderes Mal Bezug auf diese Frage nehmen.

3. Ausführung eines Graphen

Wie die Ausführung des Graphen von statten läuft, schauen wir uns im nächsten Abschnitt genauer an. Es sei so viel gesagt, dass um eine Ausführung einzuleiten wir den Befehl tf.Session() benötigen. Die Session wird mit tf.Session().run()gestartet und am Ende mit tf.Session().close() geschlossen. In der Methode .run()müssen die ausgeführten Größen stehen und außerdem der Befehl feed_dict= zum Befüllen der Platzhalter.

4. Beispiel: Achterbahn des Grauens – Nichts für schwache Nerven

4.1 Erklärung des Beispiels

Wir haben jetzt von so vielen Analogien gesprochen, dass es alles ein wenig verwirrend sein kann. Daher nochmal eine Übersicht zu den wesentlichen Punkten:

TensorFlow Neuronales Netz Achterbahn
Knoten Neuron Fahrelement
Variable Gewichte, Bias Geschwindigkeit
Kanten Signale Zustand der Fahrer
Platzhalter Wagon
Tab.1: Analogie unser fiktiven Achterbahn

 

Nun haben wir so viel Theorie gehört, jetzt müssen auch Taten folgen! Weshalb wir unsere Achterbahn modellieren wollen. Zu unserem Beispiel: Wir wollen eine Achterbahn bauen, welche ängstlichen Mitfahrer noch ängstlicher machen soll und diese sollen am Ende der Fahrt sich wünschen nie mitgefahren zu sein. (Es wird natürlich eine stark vereinfachte Variante werden, die aber auf all unsere Punkte eingehen soll, die wir im oberen Teil angesprochen haben.)

Wie im bereits beschrieben, unterteilt sich die Arbeit in TensorFlow in zwei Phasen:

  1. Erstellung des Graphen: In unserem Falle wäre das die Konstruktion unserer Achterbahn.
  2. Ausführung des Graphen: In dieser Phase lassen wir unsere Insassen einfach los und schauen mal was passiert.

Um die Zahlen zu verstehen, möchte ich euch zudem erklären, was überhaupt das Ziel unseres Modells ist. Wir haben 8 Probanden mit verschiedenen Angstzuständen. Der Angstzustand ist in unserem Beispiel ein quantitativer Wert, Menge der ganzen Zahlen  und je größer dieser Wert ist, desto ängstlicher sind unsere Probanden. Unser Ziel ist es alle Probanden in Angst und Schrecken zu versetzen, die einen Angstzustand >5 haben und sich nach der Fahrt wünschen unserer Achterbahn nie mitgefahren zu sein! Die Größe die wir dabei optimieren wollen, ist die Geschwindigkeit. Wenn die Geschwindigkeit zu schnell ist, dann fürchten sich zu viele, wenn wir zu langsam fahren, dann fürchtet sich womöglich niemand. Außerdem benötigen wir noch eine Starthöhe, die wir dem Modell zugeben müssen.

Wir haben somit eine Klassifikationsaufgabe mit dem Ziel die Geschwindigkeit und die Starthöhe zu optimieren, damit sich Fahrgäste mit einem Angstzustand > 5 so eine schlechte Erfahrung machen, dass sie am liebsten nie mitgefahren wären.

Wir benötigen außerdem für unser Beispiel folgende Module:

4.2. Eingangssignale: Zustände der Gäste

Wir sehen hier zwei Vektoren bzw. Tensoren die Informationen über unsere Gäste haben.

  • x_input ist der Angstzustand unserer Gäste
  • y_input ist unser gewünschtes Ausgangsssignal: 0  normal, 1  Wunsch nicht mitgefahren zu sein

4.3. Erstellung unseres Graphen: Konstruktion der Achterbahn

Nun konstruieren wir unsere Achterbahn des Grauens:

Eine Gleichrichter-Aktivierungsfunktion (engl. rectifier) mit einer Matrizenmultiplikation aus einem Vektor und einem Skalar mit anschließender Fehleroptimierung! MuhahahahaHAHAHAHA!

Auf den ersten Blick vielleicht ein wenig verwirrend, weshalb wir alles Schritt für Schritt durchgehen:

  • wag = tf.placeholder(tf.float32, shape = [8, 1]) ist unser Wagon, welcher die Achterbahn auf und ab fährt. Gefüllt mit unseren Probanden. Die Daten der Probanden (x_input)sind externe Daten und damit geeignet für einen Platzhalter.
    • Wichtig bei Platzhalter ist, dass ihr den Datentyp angeben müsst!
    • Optional könnt ihr auch die Form angeben. Bei einem so überschaubaren Beispiel machen wir das auch. (Form unseres Vektors: 8×1)
  • y_true = tf.placeholder(tf.float32, shape = [8, 1]) ist der gewünschte Endzustand unserer Gäste, den wir uns für die Probanden erhoffen, d.h. es ist unser y_input. Auch hier kommen die Daten von außerhalb und daher wird der Platzhalter genutzt.
  • v, h sind Geschwindigkeit und Starthöhe, die optimiert werden müssen; perfekt für eine Variable!
    • Variablen brauchen am Anfang immer einen Initialisierungswert. Für v soll es 1 sein und für h soll es -2 sein. Außerdem liegen diese Größen als Skalare (1×1) vor.

Abb.2: Schematische Darstellung unseres Berechnungsgraphen

Nun zum zweiten Teil der Modellierung in dem wir ein klein wenig Mathematik benötigen. Schauen wir uns folgende Gleichung an:

  • z = tf.matmul(wag, v) + h: ist unsere Matrizenmultiplikation -> Da unsere Größen in Vektoren/Tensoren vorliegen, können wir diese nicht einfach multiplizieren, wie z.B. 2*2 = 4. Bei der Multiplikation von Matrizen oder Vektoren müssen bestimmte Bedingungen herrschen, damit diese überhaupt multipliziert werden können. Eine ausführlichere Erklärungen soll demnächst folgen.
  • y_pred = tf.nn.relu(z): Für all diejenigen, die sich bereits mit neuronalen Netzen beschäftigt haben; relu ist in unserem Fall die Aktivierungsfunktion. Für alle anderen, die mit der Aktivierungsfunktion noch nichts anfangen können: Die Kombination (Matrizenmultiplikation) aus dem Angstzustand und der Geschwindigkeit ist der Wert Z. Je nachdem welche Aktivierungsfunktion genutzt wird, triggert der Wert unsere Emotionen, so dass wir den Wunsch verspüren, die Bahn nie gefahren zu sein.
  • err = tf.square(y_true - y_pred):Quadriert die Differenz der tatsächlichen und der ermittelten Werte. -> die zu optimierende Funktion
  • opt = tf.train.AdamOptimizer(learning_rate=0.01).minimize(err)Unser gewählter Optimierer mit der Lernrate 0.01.
  • init = tf.global_variables_initializer() Initialisierung der Variablen

Abb. 3: Aktivierungsfunktion ReLu

4.4. Ausführung des Graphen: Test der Achterbahn

Wenn wir den unten stehenden Code mal grob betrachten, dann fällt vor allem die Zeile mit dem with-(Python)Operator und dem tf.Session()-(TensorFlow)Operator auf. Der tf.Session()-Operator leitet unsere Ausführung ein. Warum wir with nutzen hat den Grund, dass dieser Operator uns das Leben einfacher macht, da dieser die nachfolgenden Befehle wieder schließt und damit wieder Leistungsressourcen frei werden. Werden zum Beispiel Daten aus externen Quellen benötigt – sei es eine Excel- oder eine SQL-Tabelle – dann schließt uns der with Operator die geöffneten Dateien, nachdem er alle unsere Befehle durchgeführt hat.

Durch die Methode .run() werden dann die in der Klammer befindenden Größen bearbeitet. Mit dem Parameter feed_dict= füllen wir den Graphen mit unseren gewünschten Dateien.

Wir lassen das Ganze 100 mal Testfahren um die optimalen Variablen zu finden. In Abb. 4 sehen wir die Verläufe der Fehlerfunktion, der Geschwindigkeit und der Höhe.

 

In Tab.2 sind nun zwei Fahrgäste zu sehen, die sich wünschen, die Bahn nie gefahren zu sein! Deren Angstlevel () ist über 0 und damit wird der Wunsch getriggert wurde; so wie wir es auch beabsichtigt haben!

Angstlvl berechnet: Fehler: Geschwindigkeit: Starthöhe:
 [0.       ] [0.        ] [0.4536] [-2.5187]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.2060 ] -> Wunsch getriggert  [0.6304]
 [1.5685] -> Wunsch getriggert  [0.3231]
Tab.2: Endergebnisse der letzten Runde

Abb.4: Verläufe der Fehler-, Geschwindigkeits- und Höhenfunktion durch Optimierung

5. Zusammenfassung und Ausblick

Zugegeben ist dieser ganze Aufwand für ein mehr oder weniger linearen Zusammenhang etwas übertrieben und bestimmt ist dem einen oder anderen aufgefallen, dass unser Beispiel mit der Achterbahn an manchen Stellen hinkt. Dennoch hoffe ich, dass ich mit der Analogie das Verständnis von TensorFlow rüberbringen konnte. Lasst uns daher nochmal die wichtigsten Punkte zusammenfassen:

Die Arbeit mit TensorFlow unterteilt sich in folgende Phasen:

  1. Erstellung des Graphen: In dieser Phase konzentrieren wir uns darauf einen Berechnungsgraphen zu erstellen, welcher  so konzipiert wird, dass er uns am Ende das Ergebnis ausgibt, welches wir uns wünschen.
    • Platzhalter: Eine der wichtigsten Sturkturen in TensorFlow ist der Platzhalter. Er ist dafür zuständig, wenn es darum geht externe Daten in unseren Graph einfließen zu lassen. Bei der Erstellung eines Platzhalters müssen wir zumindest den Datentypen angeben.
    • Variable: Wenn es darum geht Größen für ein Modell zu optimieren, stellt TensorFlow Variablen zur Verfügung. Diese benötigen eine Angabe, wie die Form des Tensors aussehen soll.
  2. Ausführung des Graphen: Nachdem wir unseren Graphen entwickelt haben, ist der nächste Schritt diesen auszuführen.
    • Dies machen wir mit dem Befehl tf.Session() und führen diesen dann mit der Methode .run() aus
    • Ebenfalls hat die Optimierung einen wichtigen Bestand in dieser Phase
    • Um unseren Graphen mit den Daten zu füllen, nutzen wir den wird den Parameter feed_dict=

Um diesen Artikel nicht in die Länge zu ziehen, wurden die Themen der Matrizenmultiplikation, Aktivierungsfunktion und Optimierung erstmal nur angerissen. Wir wollen in einem separaten Artikel näher darauf eingehen. Für den Anfang genügen wir uns damit, dass wir von diesen Elementen wissen und dass sie einen wichtigen Bestandteil haben, wenn wir neuronale Netze aufbauen wollen.

In nächsten Artikel werden wir dann ein Perzeptron erstellen und gehen auch näher auf die Themen ein, die wir in diesem Teil nur angerissen haben. Bleibt gespannt!

6. Bonus-Material

Mit Tensorboard ist es möglich unseren entwickelten Graphen auch plotten und auszugeben zu lassen. So sieht unser Graph aus:

Abb.5.: Tensorboard Berechnungsgraphausgabe

Den Programmiercode könnt ihr in diesem Link auch als Ganzes betrachten.

Einstieg in Natural Language Processing – Teil 1: Natürliche vs. Formale Sprachen

Dies ist Artikel 1 von 4 der Artikelserie Einstieg in Natural Language Processing – Artikelserie.

Versuche und erste Ansätze, Maschinen beizubringen menschliche Sprache zu verstehen, gibt es bereits seit den 50er Jahren. Trotz der jahrzehntelangen Forschung und Entwicklung gelingt dies bis heute nicht umfassend. Woran liegt dies?

Um diese Frage zu beantworten, hilft es, sich die Unterschiede zwischen „natürlichen“, also sich selbstständig entwickelnden, typischerweise von Menschen gesprochenen Sprachen und den von Computern interpretieren formalen Sprachen klar zu machen. Formale Sprachen, wie zum Beispiel Python zum Ausführen der Codebeispiele in dieser Artikelserie, HTML (Hyper Text Markup Language) zur Darstellung von Webseiten und andere typische Programmier- und Skriptsprachen, sind üblicherweise sehr streng strukturiert.

Alle diese Sprachen weisen eine Reihe von Gemeinsamkeiten auf, welche es Computern einfach machen, sie korrekt zu interpretieren (also den Informationsinhalt zu “verstehen”). Das vermutlich auffälligste Merkmal formaler Sprachen ist eine relativ strikte Syntax, welche (wenn überhaupt) nur geringe Abweichungen von einem Standard erlaubt. Wie penibel die jeweilige Syntax oft einzuhalten ist, wird am ehesten deutlich, wenn diese verletzt wird:

Solche so genannten “Syntax Error”  gehören daher zu den häufigsten Fehlern beim Schreiben von Quellcode.

Ganz anders dagegen sieht es in der Kommunikation mit natürlichen Sprachen aus. Zwar fördert falsche Komma-Setzung in der Regel nicht die Leserlichkeit eines Textes, jedoch bleibt dieser in der Regel trotzdem verständlich. Auch macht es keinen Unterschied ob ich sage „Es ist heiß heute.“ oder „Heute ist es heiß.“. Genau wie in der deutschen Sprache funktioniert dieses Beispiel auch im Englischen sowie in anderen natürlichen Sprachen. Insbesondere Spanisch ist ein Beispiel für eine Sprache mit extrem variabler Satzstellung. Jedoch kann in anderen Fällen eine andere Reihenfolge der selben Worte deren Bedeutung auch verändern. So ist „Ist es heute heiß?“ ganz klar eine Frage, obwohl exakt die selben Worte wie in den Beispielsätzen oben vorkommen.

Ein weiterer wichtiger, hiermit verwandter Unterschied ist, dass es bei formalen Sprachen in der Regel einen Ausdruck gibt, welcher eine spezifische Bedeutung besitzt, während es in natürlichen Sprachen oft viele Synonyme gibt, die ein und dieselbe Sache (oder zumindest etwas sehr ähnliches) ausdrücken. Ein wahrer boolscher Wert wird in Python als

geschrieben. Es gibt keine andere Möglichkeit, diesen Wert auszudrücken (zumindest nicht ohne irgend eine Art von Operatoren wie das Doppelgleichheitszeichen zu benutzen und damit z. B. “0 == 0” zu schreiben).  Anders hingegen zum Beispiel in der Deutschen Sprache: Wahr, richtig, korrekt, stimmt, ja,

Um einen Vorstellung davon zu bekommen, wie verbreitet Synonyme in natürlichen Sprachen sind, lässt sich die Internetseite https://www.openthesaurus.de verwenden. Beispielshalber findet man dutzende Synonyme für das Wort „schnell“ hier: https://www.openthesaurus.de/synonyme/schnell

Eine weitere große Schwierigkeit, welche in den meisten natürlichen Sprachen und nahezu allen Arten von Texten zu finden ist, stellen verschiedene grammatikalische Formen eines Wortes dar. So sind die Worte bin, wäre, sind, waren, wirst, werden… alles Konjugationen desselben Verbs, nämlich sein. Eine durchaus beeindruckende Übersicht über die verwirrende Vielfalt von Konjugationen dieses kleinen Wörtchens, findet sich unter: https://www.verbformen.de/konjugation/sein.htm.

Dieses Problem wird um so schwerwiegender, da viele Verben, insbesondere die am häufigsten genutzten, sehr unregelmäßige Konjugationsformen besitzen und damit keiner generellen Regel folgen. Daher ist computerintern oft ein Mapping für jede mögliche Konjugationsform bei vielen Verben die einzige Möglichkeit, an die Grundform zu kommen (mehr dazu in Teil 3 dieser Artikelserie).

Die Liste der sprachlichen Schwierigkeiten beim computergestützten Auswerten natürlicher Sprache ließe sich an diesem Punkt noch beliebig weiter fortsetzen:

  • Rechtschreibfehler
  • falsche Grammatik
  • Smileys
  • der „Substantivverkettungswahn“ im Deutschen
  • mehrdeutige Worte und Abkürzungen
  • abwegige Redewendungen (z. B. “ins Gras beißen”)
  • Ironie
  • und, und, und …

Ob und welche Rolle jede dieser Schwierigkeiten im einzelnen spielt, hängt natürlich sehr stark von den jeweiligen Texten ab und kann nicht pauschalisiert werden – ein typischer Chatverlauf wird ganz andere Probleme bereithalten als ein Wikipedia-Artikel. Wie man einige dieser Probleme in der Praxis vereinfachen oder sogar lösen kann und welche Ansätze und Methoden zur Verfügung stehen und regelmäßig zur Anwendung kommen wird im nächsten Teil dieser Artikelserie an praktischen Codebeispielen genauer unter die Lupe genommen.

NLTK vs. Spacy – Eine kurze Übersicht

Möchte man einen (oder auch einige) Text(e) mit den Methoden des natural language processings untersuchen um die darin verwendete Sprache auswerten oder nach bestimmten Informationen suchen, so sind insbesondere die Pakete NLTK und spaCy zu empfehlen (bei sehr vielen Texten sieht das schon wieder anders aus und wird am Ende der Artikelserie mit dem Paket gensim vorgestellt); beide bieten eine unglaubliche Vielzahl von Analysemöglichkeiten, vorgefertigten Wortsets, vortrainierte Stemmer und Lemmatiser, POS Tagger und, und, und…

Ist man vor allem an den Ergebnissen der Analyse selbst interessiert, so bietet sich spaCy an, da hier bereits mit wenigen Zeilen Code viele interessante Informationen generiert werden können.

Wer dagegen gerne selber bastelt oder wissen möchte wie die einzelnen Tools und Teilschritte genau funktionieren oder sich seine eigenen Stemmer, Tagger ect. trainieren will, ist vermutlich mit NLTK besser beraten. Zwar ist hier oft mehr Quellcode für das gleiche Ergebnis notwendig, allerdings kann das Preprocessing der Texte hierbei relativ einfach exakt den eigenen Vorstellungen angepasst werden. Zudem bietet NLTK eine Vielzahl von Beispieltexten und bereits fertig getagte Daten, mit welchen eigene Tagger trainiert und getestet werden können.

Einstieg in Natural Language Processing – Artikelserie

Unter Natural Language Processing (NLP) versteht man ein Teilgebiet der Informatik bzw. der Datenwissenschaft, welches sich mit der Analyse und Auswertung , aber auch der Synthese natürlicher Sprache befasst. Mit natürlichen Sprachen werden Sprachen wie zum Beispiel Deutsch, Englisch oder Spanisch bezeichnet, welche nicht geplant entworfen wurden, sondern sich über lange Zeit allein durch ihre Benutzung entwickelt haben. Anders ausgedrückt geht es um die Schnittstelle zwischen unserer im Alltag verwendeten und für uns Menschen verständlichen Sprache auf der einen, und um deren computergestützte Auswertung auf der anderen Seite.

Diese Artikelserie soll eine Einführung in die Thematik des Natural Language Processing sein, dessen Methoden, Möglichkeiten, aber auch der Grenzen . Im einzelnen werden folgende Themen näher behandelt:

1. Artikel – Natürliche vs. Formale Sprachen
2. Artikel – Preprocessing von Rohtext mit Python
3. Artikel – Möglichkeiten/Methoden der Textanalyse an Beispielen (erscheint demnächst…)
4. Artikel – NLP, was kann es? Und was nicht? (erscheint demnächst…)

Zur Verdeutlichung der beschriebenen Zusammenhänge und Methoden und um Interessierten einige Ideen für mögliche Startpunkte aufzuzeigen, werden im Verlauf der Artikelserie an verschiedenen Stellen Codebeispiele in der Programmiersprache Python vorgestellt.
Von den vielen im Internet zur Verfügung stehenden Python-Paketen zum Thema NLP, werden in diesem Artikel insbesondere die drei Pakete NLTK, Gensim und Spacy verwendet.

I. Einführung in TensorFlow: Einleitung und Inhalt

 

 

 

1. Einleitung und Inhalt

Früher oder später wird jede Person, welche sich mit den Themen Daten, KI, Machine Learning und Deep Learning auseinander setzt, mit TensorFlow in Kontakt geraten. Für diejenigen wird der Zeitpunkt kommen, an dem sie sich damit befassen möchten/müssen/wollen.

Und genau für euch ist diese Artikelserie ausgelegt. Gemeinsam wollen wir die ersten Schritte in die Welt von Deep Learning und neuronalen Netzen mit TensorFlow wagen und unsere eigenen Beispiele realisieren. Dabei möchten wir uns auf das Wesentlichste konzentrieren und die Thematik Schritt für Schritt in 4 Artikeln angehen, welche wie folgt aufgebaut sind:

  1. In diesem und damit ersten Artikel wollen wir uns erst einmal darauf konzentrieren, was TensorFlow ist und wofür es genutzt wird.
  2. Im zweiten Artikel befassen wir uns mit der grundlegenden Handhabung von TensorFlow und gehen den theoretischen Ablauf durch.
  3. Im dritten Artikel wollen wir dann näher auf die Praxis eingehen und ein Perzeptron – ein einfaches künstliches Neuron – entwickeln. Dabei werden wir die Grundlagen anwenden, die wir im zweiten Artikel erschlossen haben.
  4. Im vierten Artikel werden wir dann endlich unser erstes neuronales Netz aufbauen. Auch hier bilden die vorherigen Artikel ein gutes Fundament der Verständlichkeit um die kommende Aufgabe zu meistern.

Wenn ihr die Praxisbeispiele in den Artikeln 3 & 4 aktiv mit bestreiten wollt, dann ist es vorteilhaft, wenn ihr bereits mit Python gearbeitet habt und die Grundlagen dieser Programmiersprache beherrscht. Jedoch werden alle Handlungen und alle Zeilen sehr genau kommentiert, so dass es leicht verständlich bleibt.

Neben den Programmierfähigkeiten ist es hilfreich, wenn ihr euch mit der Funktionsweise von neuronalen Netzen auskennt, da wir im späteren Verlauf diese modellieren wollen. Jedoch gehen wir vor der Programmierung  kurz auf die Theorie ein und werden das Wichtigste nochmal erwähnen.

Zu guter Letzt benötigen wir für unseren Theorie-Teil ein Mindestmaß an Mathematik um die Grundlagen der neuronalen Netze zu verstehen. Aber auch hier sind die Anforderungen nicht hoch und wir sind vollkommen gut  damit bedient, wenn wir unser Wissen aus dem Abitur noch nicht ganz vergessen haben.

2. Ziele dieser Artikelserie

Diese Artikelserie ist speziell an Personen gerichtet, welche einen ersten Schritt in die große und interessante Welt von Deep Learning wagen möchten, die am Anfang nicht mit zu vielen Details überschüttet werden wollen und lieber an kleine und verdaulichen Häppchen testen wollen, ob dies das Richtige für sie ist. Unser Ziel wird sein, dass wir ein Grundverständnis für TensorFlow entwickeln und die Grundlagen zur Nutzung beherrschen, um mit diesen erste Modelle zu erstellen.

3. Was ist TensorFlow?

Viele von euch haben bestimmt von TensorFlow in Verbindung mit Deep Learning bzw. neuronalen Netzen gehört. Allgemein betrachtet ist TensorFlow ein Software-Framework zur numerischen Berechnung von Datenflussgraphen mit dem Fokus maschinelle Lernalgorithmen zu beschreiben. Kurz gesagt: Es ist ein Tool um Deep Learning Modelle zu realisieren.

Zusatz: Python ist eine Programmiersprache in der wir viele Paradigmen (objektorientiert, funktional, etc.) verwenden können. Viele Tutorials im Bereich Data Science nutzen das imperative Paradigma; wir befehlen Python also Was gemacht und Wie es ausgeführt werden soll. TensorFlow ist dahingehend anders, da es eine datenstrom-orientierte Programmierung nutzt. In dieser Form der Programmierung wird ein Datenfluss-Berechnungsgraph (kurz: Datenflussgraph) erzeugt, welcher durch die Zusammensetzung von Kanten und Knoten charakterisiert wird. Die Kanten enthalten Daten und können diese an Knoten weiterleiten. In den Knoten werden Operationen wie z. B. Addition, Multiplikation oder auch verschiedenste Variationen von Funktionen ausgeführt. Bekannte Programme mit datenstrom-orientierten Paradigmen sind Simulink, LabView oder Knime.

Für das Verständnis von TensorFlow verrät uns der Name bereits erste Informationen über die Funktionsweise. In neuronalen Netzen bzw. in Deep-Learning-Netzen können Eingangssignale, Gewichte oder Bias verschiedene Erscheinungsformen haben; von Skalaren, zweidimensionalen Tabellen bis hin zu mehrdimensionalen Matrizen kann alles dabei sein. Diese Erscheinungsformen werden in Deep-Learning-Anwendungen allgemein als Tensoren bezeichnet, welche durch ein Datenflussgraph ‘fließen’. [1]

Abb.1 Namensbedeutung von TensorFlow: Links ein Tensor in Form einer zweidimensionalen Matrix; Rechts ein Beispiel für einen Datenflussgraph

 

4. Warum TensorFlow?

Wer in die Welt der KI einsteigen und Deep Learning lernen will, hat heutzutage die Qual der Wahl. Neben TensorFlow gibt es eine Vielzahl von Alternativen wie Keras, Theano, Pytorch, Torch, Caffe, Caffe2, Mxnet und vielen anderen. Warum also TensorFlow?

Das wohl wichtigste Argument besteht darin, dass TensorFlow eine der besten Dokumentationen hat. Google – Herausgeber von TensorFlow – hat TensorFlow stets mit neuen Updates beliefert. Sicherlich aus genau diesen Gründen ist es das meistgenutzte Framework. Zumindest erscheint es so, wenn wir die Stars&Forks auf Github betrachten. [3] Das hat zur Folge, dass neben der offiziellen Dokumentation auch viele Tutorials und Bücher existieren, was die Doku nur noch besser macht.

Natürlich haben alle Frameworks ihre Vor- und Nachteile. Gerade Pytorch von Facebook erfreut sich derzeit großer Beliebtheit, da die Berechnungsgraphen dynamischer Natur sind und damit einige Vorteile gegenüber TensorFlow aufweisen.[2] Auch Keras wäre für den Einstieg eine gute Alternative, da diese Bibliothek großen Wert auf eine einsteiger- und nutzerfreundliche Handhabung legt. Keras kann man sich als eine Art Bedienoberfläche über unsere Frameworks vorstellen, welche vorgefertigte neuronale Netze bereitstellt und uns einen Großteil der Arbeit abnimmt.

Möchte man jedoch ein detailreiches und individuelles Modell bauen und die Theorie dahinter nachvollziehen können, dann ist TensorFlow der beste Einstieg in Deep Learning! Es wird einige Schwierigkeiten bei der Gestaltung unserer Modelle geben, aber durch die gute Dokumentation, der großen Community und der Vielzahl an Beispielen, werden wir gewiss eine Lösung für aufkommende Problemstellungen finden.

 

Abb.2 Beliebtheit von DL-Frameworks basierend auf Github Stars & Forks (10.06.2018)

 

5. Zusammenfassung und Ausblick

Fassen wir das Ganze nochmal zusammen: TensorFlow ist ein Framework, welches auf der datenstrom-orientierten Programmierung basiert und speziell für die Implementierung von Machine/Deep Learning-Anwendungen ausgelegt ist. Dabei fließen unsere Daten durch eine mehr oder weniger komplexe Anordnung von Berechnungen, welche uns am Ende ein Ergebnis liefert.

Die wichtigsten Argumente zur Wahl von TensorFlow als Einstieg in die Welt des Deep Learnings bestehen darin, dass TensorFlow ausgezeichnet dokumentiert ist, eine große Community besitzt und relativ einfach zu lesen ist. Außerdem hat es eine Schnittstelle zu Python, welches durch die meisten Anwender im Bereich der Datenanalyse bereits genutzt wird.

Wenn ihr es bis hier hin geschafft habt und immer noch motiviert seid den Einstieg mit TensorFlow zu wagen, dann seid gespannt auf den nächsten Artikel. In diesem werden wir dann auf die Funktionsweise von TensorFlow eingehen und einfache Berechnungsgraphen aufbauen, um ein Grundverständnis von TensorFlow zu bekommen. Bleibt also gespannt!

Quellen

[1] Hope, Tom (2018): Einführung in TensorFlow: DEEP-LEARNING-SYSTEME PROGRAMMIEREN, TRAINIEREN, SKALIEREN UND DEPLOYEN, 1. Auflage

[2] https://www.marutitech.com/top-8-deep-learning-frameworks/

[3] https://github.com/mbadry1/Top-Deep-Learning

[4] https://www.bigdata-insider.de/was-ist-keras-a-726546/

How To Remotely Send R and Python Execution to SQL Server from Jupyter Notebooks

Introduction

Did you know that you can execute R and Python code remotely in SQL Server from Jupyter Notebooks or any IDE? Machine Learning Services in SQL Server eliminates the need to move data around. Instead of transferring large and sensitive data over the network or losing accuracy on ML training with sample csv files, you can have your R/Python code execute within your database. You can work in Jupyter Notebooks, RStudio, PyCharm, VSCode, Visual Studio, wherever you want, and then send function execution to SQL Server bringing intelligence to where your data lives.

This tutorial will show you an example of how you can send your python code from Juptyter notebooks to execute within SQL Server. The same principles apply to R and any other IDE as well. If you prefer to learn through videos, this tutorial is also published on YouTube here:


 

Environment Setup Prerequisites

  1. Install ML Services on SQL Server

In order for R or Python to execute within SQL, you first need the Machine Learning Services feature installed and configured. See this how-to guide.

  1. Install RevoscalePy via Microsoft’s Python Client

In order to send Python execution to SQL from Jupyter Notebooks, you need to use Microsoft’s RevoscalePy package. To get RevoscalePy, download and install Microsoft’s ML Services Python Client. Documentation Page or Direct Download Link (for Windows).

After downloading, open powershell as an administrator and navigate to the download folder. Start the installation with this command (feel free to customize the install folder): .\Install-PyForMLS.ps1 -InstallFolder “C:\Program Files\MicrosoftPythonClient”

Be patient while the installation can take a little while. Once installed navigate to the new path you installed in. Let’s make an empty folder and open Jupyter Notebooks: mkdir JupyterNotebooks; cd JupyterNotebooks; ..\Scripts\jupyter-notebook

Create a new notebook with the Python 3 interpreter:

 

To test if everything is setup, import revoscalepy in the first cell and execute. If there are no error messages you are ready to move forward.

Database Setup (Required for this tutorial only)

For the rest of the tutorial you can clone this Jupyter Notebook from Github if you don’t want to copy paste all of the code. This database setup is a one time step to ensure you have the same data as this tutorial. You don’t need to perform any of these setup steps to use your own data.

  1. Create a database

Modify the connection string for your server and use pyodbc to create a new database.

  1. Import Iris sample from SkLearn

Iris is a popular dataset for beginner data science tutorials. It is included by default in sklearn package.

  1. Use RecoscalePy APIs to create a table and load the Iris data

(You can also do this with pyodbc, sqlalchemy or other packages)

Define a Function to Send to SQL Server

Write any python code you want to execute in SQL. In this example we are creating a scatter matrix on the iris dataset and only returning the bytestream of the .png back to Jupyter Notebooks to render on our client.

Send execution to SQL

Now that we are finally set up, check out how easy sending remote execution really is! First, import revoscalepy. Create a sql_compute_context, and then send the execution of any function seamlessly to SQL Server with RxExec. No raw data had to be transferred from SQL to the Jupyter Notebook. All computation happened within the database and only the image file was returned to be displayed.

While this example is trivial with the Iris dataset, imagine the additional scale, performance, and security capabilities that you now unlocked. You can use any of the latest open source R/Python packages to build Deep Learning and AI applications on large amounts of data in SQL Server. We also offer leading edge, high-performance algorithms in Microsoft’s RevoScaleR and RevoScalePy APIs. Using these with the latest innovations in the open source world allows you to bring unparalleled selection, performance, and scale to your applications.

Learn More

Check out SQL Machine Learning Services Documentation to learn how you can easily deploy your R/Python code with SQL stored procedures making them accessible in your ETL processes or to any application. Train and store machine learning models in your database bringing intelligence to where your data lives.

Other YouTube Tutorials: