Tutorial - Das Perzeptron - Teil 2


Mit Python und Numpy das Perzeptron verstehen

Nun kann es richtig losgehen

Im ersten Teil dieses Tutorials haben wir uns darauf eingelassen, grundlegende Ideen der künstlichen neuronalen Netze kennen zu lernen und haben die mathematischen Kniffe der Delta-Lernregel untersucht. Wir haben ein Ausgabeneuron geschaffen, das - immerhin - schon beliebig viele Eingabeneuronen haben kann. Das werden wir uns gleich auch zunutze machen.

Eine taktische Einschränkung haben wir damals noch gemacht: die Eingabeneuronen sollten zunächst nur binäre Aktivierung haben. Das war aber vor allen Dingen ein lerntaktischer Trick, um das Rechnen im Kopf einfach zu halten, wenn man versucht, die Schritte nachzuvollziehen.

Wir können diese Regelung getrost aufbrechen, und die Lernregel hält immer noch: in eben dieser Regel multiplizieren wir ja mit dem passenden Eingabeneuron i_n - und eine kleine Aktivierung dieses Eingabeneurons wird auch weiterhin einen eher kleinen Anteil am Netzfehler haben als eine große Aktivierung dieses Neurons. Wir könnten also auch bspw. für “Freunde haben Zeit” einen Wert von 0.7 eintragen, wenn nur ein Teil der Freunde Zeit gehabt hätte, und damit ganz neue Trainings-Horizonte eröffnen. 1

Was vorerst gleich bleibt, ist, das wir mit einem Ausgabeneuron weiterarbeiten und dieses auch weiterhin nur mit 1 aufleuchten oder mit 0 dunkel lassen, also binär aktivieren wollen, wie gehabt mit der schönen Treppenfunktion.

Kann man damit denn bis jetzt auch schon etwas Sinnvolles tun? Wir versuchen es!

Kurze Wiederholung und Vertiefung

Was wir tun wollen

Ein unter Data Scientisten mittlerweile bekannter Datensatz sind Aufzeichnungen eines Sonars, also das, was von den ausgesendeten Schallwellen wieder zurückreflektiert und sensorisch erfasst wurde. Dabei gibt es genau zwei Klassen in diesem Experiment: das eine sind Aufzeichnungen, die die Reflektionsmuster eines Felsens wiedergeben, das andere sind Beispiele für Echos von einem metallischen Zylinder. Insgesamt gibt es 111 Beispiele für Felsen und 97 Beispiele für Metallzylinder, und jedes Beispiel umfasst sagenhafte 60 verschiedene Merkmale. Jedes davon steht für die Intensität einer Frequenz, die wieder als Echo zurückkam.

Wir wollen herausfinden, ob wir mit unserem Perzeptron in der Lage dazu sind, diese Muster zu unterscheiden: können wir also nur anhand solcher Daten wie in der Abbildung hier unter dem Text vorhersagen, ob das Sonar auf einen metallischen Zylinder oder einen Fels getroffen ist?

Echo von Felsen vs. Echo von Metallzylinder

Was wir verinnerlichen müssen

Bevor wir fortfahren, sollten wir uns nochmal vergewissern, dass wir die wichtigen Bausteine parat haben, und machen uns das Leben noch ein Stückchen einfacher.

Wir hatten bei der Aktivierung des Ausgabeneurons zwei Teile zu kombinieren. Zunächst der Teil, der zuerst gemacht wird, das Aufsummieren aller Eingaben in das Ausgabeneuron einwärts.

i_0 * w_0 + i_1 * w_1 + i_2 * w_2 + ... + i_n * w_n

Das hatten wir als Skalarprodukt identifiziert.

Mathe ist ja nunmal kein Laberfach, und kein mathematisch arbeitender Mensch würde nun bspw. im Falle unsere oben erwähnten Sonardaten 60 mal ein i, 60 mal ein w, 60 mal ein * und 59 mal ein + schreiben wollen.

Im Grunde genommen wissen wir ja auch schon, dass es eben eine Liste mit Werten für i_0, i_1, ..., i_n gibt, und der mathematisch passende Begriff dafür wäre ein Vektor. Den kann man einfach i nennen, ohne die kleingestellte Zahl, und damit sind dann alle Werte gemeint, die in i vorhanden sind. In der Schule hat man vermutlich die Vektoren mit einem Pfeil gekennzeichnet, also bspw. so \vec{i} . Das sparen wir uns jetzt aber auch mal, denn alle anderen lassen die ollen Pfeile ja schließlich auch weg (mal von den Physikern abgesehen).

Krasses Modell mit 60 Eingabeneuronen für Sonardaten

Für die Gewichte w_0, w_1, ..., w_n gilt das gleiche, für diesen Vektor können wir einfach w schreiben. Damit reduziert sich auch mal wieder das ganze Geschreibsel für das Skalarprodukt, das wir gebraucht haben, um die gewichtete Summe aller Eingaben zu bilden, auf eine knackige Formel:

i \bullet w

Ja, man darf da ruhig einen dicken Punkt dazwischen machen (ein normaler Malpunkt reicht wohl auch; hier im Tutorial werde ich jedoch den dicken Punkt verwenden, damit man von weitem schon das Skalarprodukt riechen kann). Im Englischen heisst das Skalarprodukt auch dot product, also “Punktprodukt”.

Wer unsicher geworden ist, was das alles mit dem neuronalen Netz zu tun hatte, sollte noch mal in Teil 1 vorbeischauen.

Das Ergebnis dieses Skalarproduktes haben wir dann wieder als Parameter in eine Aktivierungsfunktion gesteckt, in unserem letzten Tutorial war das die Heaviside-Funktion.

H[x]=\begin{cases} 0, & x < 0, \\ 1, & x \ge 0, \end{cases}

oder, wer das lieber als Python-Code lesen mag

1
2
3
4
5
def heaviside(x):
if x < 0:
return 0
else:
return 1

Sofern wir also unser H streng wie oben definiert haben, darf man schreiben:

H(i \bullet w)

Sprich: Das Ergebnis der Skalarmultiplikation von Eingabe-Vektor i und Gewichts-Vektor w wird durch die Heaviside-Funktion durchgejagt. Oder auch für die Angeber: H von i mal w. Das ist quasi alles, was man wissen muss, um ein Ausgabeneuron zu füttern und ggf. aufleuchten zu lassen.

Wenn man mal gar nicht weiter weiß, schreibt man einfach den griechischen Großbuchstaben Phi \Phi als Aktivierungsfunktion. Das sieht nicht nur supercool aus und beeindruckt alle Deine Freunde, sondern erlaubt es auch, eigentlich jede gültige Aktivierungsfunktion an dieser Stelle zu verwenden, sofern diese nicht woanders definiert wurde:

\Phi(i \bullet w)

Das zumindestens wird man in vielen Lehrbüchern über neuronale Netze so wiederentdecken können. Aber wir schweifen ab.

Delta-Regel - da war doch was

Nun noch einmal schauen, dass wir die Lernegel wirklich drauf haben:

  • wir ändern jedes Gewicht
    • bezogen auf die Richtung des anliegenden Fehlers, den wir delta ( \delta_1 ) genannt haben, und das berechnet sich aus dem erwarteten Wert minus dem gelieferten Wert
    • bezogen darauf, wie stark das zum Gewicht gehörende Eingabeneuron aktiviert war, das war das Input-Neuron i_n
    • bezogen auf eine Lernrate, die wir alpha ( \alpha ) getauft haben. In der Regel ist diese Zahl irgendwas zwischen 0.0001 und 0.1

Das führte uns zur knackigen Delta-Regel:

w_n \leftarrow w_n + \delta_1 * i_n * \alpha

Die Lern-Regel-Bande d.i.a.

Mit numpy schneller zum Ziel

Im letzten Tutorial hatten wir uns noch selbst eine Funktion skalarprodukt(liste1, liste2) geschrieben, und auch die Heaviside-Funktion selbst implementiert. Das hatte sicherlich einen pädagogischen Effekt, aber im Grunde wurde jedes rechentechnische Problem schon mal von irgendwem auf der Welt gelöst.

In der Python-Welt gibt es dafür beispielsweise numpy. numpy ist nicht nur elegant, sondern auch rasend schnell, weil die darunterliegenden Operationen nativ gegen den Prozessor gebaut werden, und Python fungiert nur noch als Schnittstelle zum Menschen. Keine Angst, sobald wir die Grundlagen alle draufhaben, machen wir uns auch an TensorFlow, falls jemand auf das Stichwort gewartet hat.

Wer es noch nicht installiert hat, kann es jetzt tun, bspw. wenn man in einem Jupyter Notebook folgende Codezeile ausführt:

1
!pip install numpy

Man beachte das Ausrufezeichen am Anfang. Damit wird im Grunde genommen alles, was nach dem Ausrufezeichen kommt, als Befehl auf dem darunterliegenden Betriebssystem ausgeführt.

Dann sollten wir numpy importieren können, es hat sich eingebürgert, dabei dem Modul numpy den Kurznamen np zu geben, und das sieht so aus:

1
import numpy as np

Mittlerweile gibt es eigentlich kein Jupyter-Notebook mehr, in dem ich nicht als allererstes wie im Tran import numpy as np eingebe, auch wenn ich es gar nicht brauche.

Ein Vektor ist auch nur eine Liste

Was wir zunächst mal brauchen, ist ein Vektor. In numpy ist das auch erstmal nur eine Liste oder besser gesagt ein Array. Nehmen wir doch mal unsere Inputs wie folgt:

1
i = np.array([1.0, 0.0, 1.0]) # an erster Position das Bias-Neuron

Und dazu noch ein paar Gewichte, die wir bspw. woanders trainiert haben:

1
w = np.array([-0.2, 0.3, 0.3])

Nun können wir auch schon das dot product bzw. das Skalarprodukt von numpy berechnen lassen:

1
i.dot(w)

Ist das nicht geradezu phantastisch? Da wir nun keine herkömmliche Liste mehr haben, sondern ein numpy.array, können wir auf diesem Ding einfach .dot(...) aufrufen und bekommen das Skalarprodukt frei Haus geliefert.

Für den Moment scheint es, als hätten wir noch nicht allzu viel gewonnen, denn es fehlt ja noch die heaviside-Funktion. Aber auch die steht in numpy schon bereit, mutet uns aber auf den ersten Blick erstmal seltsam an, wegen der 1.0, die wir noch reinstecken:

1
o = np.heaviside(i.dot(w), 1.0)

Der zweite Parameter, der hier mit 1.0 angegeben ist, ist der Wert, den die heaviside-Funktion an der Stelle 0 annehmen soll. Wir hatten ja oben gesagt, dass alles größer oder gleich 0 auch schon 1 sein soll, so dass wir das hier eintragen.

Jetzt haben wir es geschafft, ein einzelnes Output-Neuron mit dem Namen o mit numpy zu aktivieren.

Die Sonardaten einlesen

Junge, Junge. Jetzt haben wir doch mal was gelernt. Es wird höchste Zeit, dass wir uns mal diese Sonar-Daten genauer anschauen. Was genau bekommen wir denn da geliefert?

Um besser reinschauen zu können, sollten wir auf jedenfall die matplotlib am Start haben. Falls noch nicht geschehen, kann man die jetzt installieren:

1
!pip install matplotlib

Zunächst mal müssen wir jedoch die Daten in den Speicher laden. Was wir da bekommen, ist eine CSV-Datei, also Werte, die mit Komma getrennt werden (Comma-Separated Values, kommaseparierte Werte), wenn man so will, eine Excel-Datei für Arme.

Die erste Zeile in der Datei sieht so aus:

    0.0200,0.0371,0.0428,0.0207,0.0954,0.0986,0.1539,0.1601,0.3109,0.2111,0.1609,0.1582,0.2238,0.0645,0.0660,0.2273,0.3100,0.2999,0.5078,0.4797,0.5783,0.5071,0.4328,0.5550,0.6711,0.6415,0.7104,0.8080,0.6791,0.3857,0.1307,0.2604,0.5121,0.7547,0.8537,0.8507,0.6692,0.6097,0.4943,0.2744,0.0510,0.2834,0.2825,0.4256,0.2641,0.1386,0.1051,0.1343,0.0383,0.0324,0.0232,0.0027,0.0065,0.0159,0.0072,0.0167,0.0180,0.0084,0.0090,0.0032,R

Man darf ruhig zugeben, dass das auf den ersten Blick völlig nichtssagend aussieht. Interessant ist aber der Buchstabe, das R, da ganz am Ende der Zeile. Das ‘R’ steht für ‘rock’ - nein, nicht der aus der Mode gekommene Damenrock, sondern das englische Wort für Felsen. Wenn man sich die ganze Datei anschaut, bspw. wenn man diese in LibreOffice Calc öffnet, wird man feststellen, dass in der letzten Spalte immer ein Buchstabe ist, entweder ein ‘R’ für ‘rock’ oder ein ‘M’ für ‘metal’, also die metallischen Zylinder.

Unsere Daten laden wir uns jetzt erstmal in Ruhe in den Speicher:

1
2
3
4
5
6
data = np.genfromtxt(
'./data/sonar-mine-rock.csv',
delimiter=',',
converters={
-1: lambda s: 1.0 if s == b'M' else 0.0
})

Mit der Funktion genfromtxt bitten wir numpy, uns diese Textdatei zu öffnen. Mit dem delimiter=',' legen wir fest, dass die Werte tatsächlich kommasepariert vorliegen, und für die converters setzen wir für die letzte Spalte (also da, wo R oder M drin steht) eine Konvertierung in Gang: Immer, wenn der Buchstabe M da auftaucht, soll eine 1.0 zurückgegeben werden, ansonsten eine 0.0, das betrifft dann also die Zeilen, die auf R enden. Genaueres kann man in der numpy-Doku nachschlagen. Dazu muss man nicht mal das Jupyter Notebook verlassen, einfach folgende Zelle im Notebook ausführen:

1
help(np.genfromtxt)

Diese Konvertierung ist notwendig, da ein Neuronales Netz zunächst mal intern nur Zahlen verarbeiten kann - oder habt ihr schon mal ein Skalarprodukt gesehen, in dem mit etwas anderem als Zahlen gerechnet wird? Mit “M” oder “FOOBAR” oder “30 Dollar” kann das Netz so rein gar nix anfangen, wenn man diese Werte nicht irgendwie in eine vernünftige Zahl packen kann. Da wir ja weiterhin ein binäres Klassifikationsproblem haben, weil wir nur zwischen Fels und Metall unterscheiden, reichen uns die beiden Zustände 0 und 1 ja vollkommen aus.

Wir schauen uns mal an, was wir da reingeladen haben:

1
print(data.shape)

Als Ausgabe bekommen wir (208, 61) geliefert, der erste Wert ist die Anzahl der Zeilen, die wir geladen haben, und 61 ist die Anzahl der Spalten. Die letzte Spalte ist das, was wir eigentlich mit dem Perzeptron trainieren wollen, also ob die Daten Fels oder Metall repräsentieren. Die ersten 60 Spalten sind dann folgerichtig die Merkmale, die wir zur Verfügung haben.

Daten sichten

Den ersten Datensatz schauen wir uns wie folgt an:

1
2
3
4
import matplotlib.pyplot as plt
%matplotlib inline

plt.plot(data[0,0:60]) # Plotte die erste Zeile von Spalte 0-60

Die erste Zeile Felsendaten

Wir schauen nochmal nach, ob das eine 0=Fels oder eine 1=Metall ist:

1
print(data[0,-1]) # die erste Zeile, und die letzte Spalte

Die -1 ist übrigens eine Möglichkeit, mit numpy vom anderen Ende her zu schauen, und -1 ist dann nach dem Komma also die letzte Spalte, -2 wäre die vorletzte Spalte gewesen usw. Das haben wir oben beim Laden der Daten schon genutzt, als wir einen Konverter für die letzte Spalte gebaut haben.

Ja, da kommt eine 0.0. So sieht also ein Sonarbild von einem Felsen aus. Wer hätte das gedacht? Da die Daten sortiert eingelesen wurden, können wir uns auch mal das letzte Sonarbild anschauen, denn das ist eines von einem Metallzylinder:

1
plt.plot(data[-1,0:60])

Glattes Metall

Das sieht nicht ganz so schroff aus wie das Echobild von einem Felsen, das wir uns zuerst angesehen haben.

Wir merken uns auf jeden Fall, dass wir mit numpy prima die Daten selektieren können: mit [Zeile, Spalte], wobei sowohl Zeile als auch Spalte entweder ein einzelner Wert oder eine Range sein kann, wobei eine Range dann mit Start:Ende notiert wird. Mit negativen Werten können wir von hinten nach vorne wandern.

Lernen vs. Auswendig Lernen

Es soll ja Studiengänge geben, wo auch heute noch großer Wert darauf gelegt wird, ob man dazu in der Lage ist, in kürzester Zeit möglichst viel auswendig zu lernen. Aber in unserem Fall Daten einfach auswendig zu lernen, ist das wirklich das, was wir wollen? Ist das denn, um das Wort endlich mal zu benutzen, nachhaltig?

Wir könnten ja auch einfach eine Datenbank anlegen, in der wir die 60 Werte des Sonars zusammen mit dem richtigen Ergebnis speichern. Dann müssten wir nur noch die richtige Zeile in der Datenbank finden und könnten das richtige Ergebnis wieder hervorzaubern. Das wäre auswendig lernen. So ist das also mit diesen Auswendiggesängen-Studiengängen. Im Grunde degradieren diese unser wunderbares Gehirn zu einer profanen SQL-Tabelle.

Viel lieber aber wollen wir eben nicht auswendig lernen, sondern in der Lage sein, auch solche Muster zu erkennen, die wir nie zuvor gesehen haben. Wir wollen also ein Modell trainieren, dass die Fähigkeit dazu hat, zu generalisieren.

Daten vorbereiten und Trainings- und Testdaten splitten

Um der Gefahr zu entlaufen, dass unser kleines, aber feines neuronales Netz einfach nur auswendig lernt, legen wir einen kleinen Teil der Daten zur Seite, die wir auf gar keinen Fall für das Training verwenden dürfen. Den Löwenanteil der Daten nutzen wir dann für das Training, und einen kleineren Anteil der Daten nutzen wir dann zum Testen des trainierten Netzes. Erst, wenn auch die Vohersagequalitäten des KNNs auf dem Testdatensatz gut genug ist, lassen wir uns überzeugen, dass eine Fähigkeit zum Generalisieren enthalten ist.

Eine typische Grundregel ist, rund 80-90% der Daten für das Training zu verwenden, den Rest für die Validierung des Trainings aufzusparen.

Bei der Gelegenheit können wir auch gleich noch unser Bias-Neuron wieder einschleusen, das wir ja im letzten Tutorial schon eingeführt haben, um den Schwellwert des Neurons auf eine gemeinsame Seite zu bekommen und es wie ein herkömmliches Gewicht zu behandeln.

An der ersten Position des jeweiligen i -Vektors soll nun unser Bias-Neuron stehen. Dazu nutzen wir eine numpy Funktion namens insert, mit der wir am Anfang noch eine Spalte hinzufügen und mit 1.0 auffüllen:

1
2
3
4
5
print(data.shape, "data original")

fulldata = np.insert(data, 0, 1, axis=1)

print(fulldata.shape, "data mit bias")

Wir sehen, fulldata hat nun eine Spalte mehr, und die erste Spalte besteht auch tatsächlich nur aus 1en:

1
print(fulldata[:,0]) # Alle Zeilen, von den Spalten nur 0

Jetzt sollten wir diese Daten nochmal gründlich durchmischen, damit wir beim Splitten in Trainings- und Testdaten nicht versehentlich die Felsdaten im Training haben, aber nur noch Metalldaten im Test. Wir nutzen dazu np.random.shuffle, und dabei werden auch nur die Zeilen der Daten durcheinandergewürfelt, aber natürlich nicht die Spalten der Daten (letzteres wäre eine Katastrophe!).

1
np.random.shuffle(fulldata)

Jetzt können wir schon einen Teil der Daten abknapsen für das Training, und einen anderen Teil für die Testdaten.

1
2
3
4
5
testdata = fulldata[0:31,:]
training = fulldata[31:,:]

print(testdata.shape, " sind Testdaten")
print(training.shape, " sind Trainingsdaten")

Da das Ganze vom Zufall abhängt, kann man natürlich auch gehörig Pech haben, zum Beispiel die einfachen Muster landen im Trainingsdatensatz und die komplizierten und ausgefallenen Muster landen im Testdatensatz. In späteren Tutorials schauen wir uns dieses Problem nochmal genauer an. Falls Du gleich jedoch zu einem unbefriedigenden Ergebnis kommen solltest, versuche nochmal einen weiteren Lauf ab dem Laden der Daten oben.

Jetzt kann eigentlich auch schon fast das Training starten. Damit wir gleich den Überblick behalten, installieren wir uns noch eine nette Bibliothek, die uns erlaubt, einen Fortschrittsbalken darzustellen:

1
!pip install tqdm

Nun initialisieren wir wieder die Gewichte mit zufälligen Werten, legen die Anzahl der Epochen fest und wie groß unsere Lernrate alpha sein soll.

1
2
3
weights = np.random.rand(61) # 1 für das Bias-Neuron, 60 für Merkmale
epochen = 1000
alpha = 0.001

Ansonsten hat sich nicht viel geändert an unserem krassen Lernalgorithmus. Da wir nun numpy haben, können wir die Gewichtsaktualisierung ein wenig knapper aufschreiben.

Beim letzten Mal sind wir noch mit einer for-Schleife durch die Gewichte und Inputs iteriert und haben jedes Gewicht einzeln angepasst. Wenn man jedoch den *-Operator auf zwei numpy.arrays anwendet, dann multiplziert numpy auch tatsächlich jede Zahl der einen Liste mit jeder Zahl der anderen Liste und gibt diese Zwischenergebnisse als neue Liste wieder zurück. Und nutzt man den *-Operator mit einer Liste und einem einfachen Wert, dann wird jedes Element der Liste mit dem Wert multipliziert.

Und, wir kriegen auch noch einen schönen Fortschrittsbalken!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from tqdm import tqdm # Für den Fortschrittsbalken

errors = [] # Wir merken uns den Netzfehler für die Epochen

# Einmal über alle Epochen iterieren...
# dank tqdm kriegen wir einen Fortschrittsbalken angezeigt.
for epoche in tqdm(range(0, epochen)):

error = 0 # Der Fehler in dieser Epoche zurückgesetzt

# Wisst ihr noch, warum wir das gemacht haben im
# ersten Tutorial?
np.random.shuffle(training) # Nochmal gut durchmischen

# Dem Netz jeden Trainingsdatensatz einmal präsentieren...
for i in range(0, len(training)):

inputs = training[i, 0:61] # Die ersten 61 Spalten sind Bias + 60 Merkmale
target = training[i, -1] # Die letzte Spalte ist 1 für Metall oder 0 für Fels

# Feuert das Perzeptron?
output = np.heaviside(inputs.dot(weights), 1.0)

# Was wurde erwartet, was geliefert?
delta = target - output

# Fehler zählen, falls Fehler da
# und Gewichte aktualisieren
if (delta != 0):
error += 1
weights += delta * inputs * alpha # Ja, numpy kann das so

errors.append(error)

Nachdem das Training über die Epochen abgeschlossen ist, lassen wir uns einmal ausgeben, wie viele Beispiele das Netz im Training richtig erkannt hat. Da sehen wir auch, dass die meisten Fehler schon in den ersten Epochen korrigiert werden.

1
2
3
plt.figure(figsize=(12,5))
plt.plot(errors)
_ = plt.title("Trainingsfehler über die Epochen", fontsize=16)

Hier ist ein exemplarisches Bild aus einem meiner Läufe, durch den Zufall sieht das aber jedes Mal ein bisschen anders aus:

Gar nicht mal übel - so ein Bodensatz von Datensätzen, die nicht richtig zugeordnet werden können, bleibt aber auch erhalten. Wenn man möchte, kann man den Code für das Training oben nochmal ausführen, dann werden weitere 1000 Epochen trainiert. Manchmal schafft es das Netz dann auch, die Daten fehlerfrei im Training zu erkennen - das ist aber gar nicht das, was wir unbedingt anstreben! Viel wichtiger ist es, dass unser kleines Netz nun dazu in der Lage ist, die Beispiele richtig zu klassifizieren, die wir ihm vorenthalten haben!

Das schauen wir uns jetzt nochmal an:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
anzahl_fehler = 0

# Wir präsentieren alle Zeilen der Testdaten
for test in testdata:

# Was kommt zurück, wenn wir die Testdaten mit den
# oben gelernten Gewichten durch die Perzeptron-
# Aktivierung jagen?
o = np.heaviside(test[0:61].dot(weights), 1.)

# in der letzten Spalte von test steht der erwartete Wert
delta = test[-1] - o

# Fehler aufgetreten?
if delta != 0.0:
anzahl_fehler += 1

# Die Güte prozentual ausrechnen.
anzahl_daten = len(testdata)
erkannte_daten = (anzahl_daten - anzahl_fehler ) / anzahl_daten

print ("Güte im Test: {:0.2f}".format(erkannte_daten))

So sieht eine exemplarische Ausgabe aus:

Güte im Test: 0.77

Fazit

  • Lasse Deine Modelle niemals auswendig lernen, sondern viel wichtiger ist es, dass diese auch unbekannte Daten richtig zuordnen können. Die Fähigkeit zum Generalisieren soll gegeben sein.

  • Wenn die Trainingsgüte sehr sehr hoch ist, die Testgüte allerdings eher schlecht, ist das ein Signal für eine Überanpassung, d. h., das Netz hat die Trainingsdaten auswendig gelernt, ist aber nicht in der Lage, mit neuen Daten gut umzugehen.

Und das war es auch für heute für diesen 2. Teil des Tutorials.

Nicht traurig sein, wenn hier zum Schluss in der Güte kein hoher Wert rausgekommen ist. Wir sind ja auch noch ganz am Anfang, und das Netz erkennt die Muster auch besser, als wenn es einfach nur geraten hätte (dann müsste ja ungefähr eine Güte von 0.5 rauskommen)! Wir sind also deutlich besser als ein Affe mit Dartpfeilen, und wenn es dafür ein Abzeichen gäbe, könnten wir uns das jetzt annähen.

Je nachdem, wie die Daten im Laufe des Codes zufällig durcheinandergepurzelt sind, kann aber zum Schluss durchaus eine Güte von 0.8 entstanden sein. Das bedeutet ja immerhin, dass knapp 4 von 5 Beispielen richtig erkannt werden. Aber auch davon sollte man sich nicht blenden lassen, da wir ja je nach Laune des Würfels mal mehr oder weniger Güte haben würden. Die Lösung für dieses Problem bauen wir aber noch früh genug in unseren Werkzeugkasten mit ein.

Je nach Anwendungszweck kann das auch schon gut genug sein. Warum sollte man mehr Technik auffahren, wenn ein einfaches Perzeptron auch schon reicht? Schnell auszurechnen geht es allemal.

Eigene Experimente

  • Alles, was hier geschah, hing stark vom Zufall ab. Versuche, den Python-Code mehrfach laufen zu lassen und vergleiche die Ergebnisse. Achte dabei darauf, dass die Daten geladen, in Trainings- und Testdaten gesplittet und die Gewichte wieder initialisiert werden.

  • Versuche dabei mal, für die Lernrate alpha unterschiedliche Werte einzusetzen. Wie verhält sich dann der Plot für den Trainingsfehler über die Epochen?

  • Wenn man bedenkt, wie einfach unser Modell ist (nämlich so einfach wie H(i \bullet w) ), kann sich die Leistung doch durchaus sehen lassen - zumindestens ist es besser als geraten. Setze mal die Gewichte wieder auf zufällige Werte und führe den Teil des Skriptes aus, der die Testdaten validiert.

  • Man kann ein Netz auch “nachtrainieren”, also die Schleife über alle Epochen mit der Gewichtsanpassung nochmal laufen lassen. Oft wird dann die Erkennung der Trainingsdaten auch noch ein Stückchen besser, aber gilt das auch für die Testdaten?

Fortsetzung

Hier geht es zum nächsten Teil des Tutorials:

Das Perzeptron - Teil 3

Downloads

Das gesamte Tutorial ist dazu gedacht, interaktiv im Jupyter Notebook ausgeführt zu werden, daher kann man sich dieses auch via Github herunterladen und lokal ausführen:

https://github.com/dannybusch/neuromant.de-Tutorials/blob/master/notebooks/Tutorial_Das-Perzeptron-Teil-2.ipynb

Im Repository sind auch die Daten enthalten, die man für dieses Tutorial braucht, daher am besten gleich das ganze Repo clonen:

neuromant.de-Tutorials auf Github

Schlusswort

Herzlichen Glückwunsch! Ich freue mich wie immer über Feedback zu diesem Tutorial. Wer eine Frage hat, kann diese auch gerne hier unter dem Artikel über das angebotene Forum stellen. Wer einen Fehler findet, ist ebenso herzlich dazu aufgerufen, diesen kundzutun.

Bis bald zum nächsten Tutorial!

Fußnoten


  1. 1.Dabei muss man allerdings auch ein bisschen aufpassen: wenn man bspw. die Freunde einfach zählt und dann "7" in das Netz einspeist, wenn 7 Freunde anwesend waren, aber weiterhin eine 0.8, wenn die Sonne ein bisschen getrübt war, dann stimmen die Skalen der Werte ja nicht überein. Das ist zunächst mal kein Problem, da die zu lernenden Gewichte ja den Effekt der Skalenverschiebung auch selbst durch den Lernalgorithmus korrigieren würden, allerdings könnte das Training länger dauern und umständlicher ablaufen als notwendig. Daher ist es immer gut, die Merkmale auf Werte bspw. zwischen 0 und 1 zu normalisieren. Nur, damit man das schon mal gehört hat.