2, 5, 7, 10, 12 – und welche Zahl kommt als nächste? Michael Schilli testet, ob sich von Psychologen erdachte Intelligenztests mit modernen KI-Automaten knacken lassen.
Neuronale Netzwerke leisten Großes, wenn es darum geht, in Eingangsdaten verrauschte Muster zu erkennen und sie eindeutigen Ergebnissen zuzuordnen. Wenn ein Dutzend Personen die Buchstaben A oder B in unterschiedlichen Handschriften in ein Formular eintragen, fieselt ein trainiertes Netzwerk die richtige Zuordnung raus. Und eine Mustererkennung für Nummernschilder vorbeizischender Fahrzeuge extrahiert auf den Fotos die Autokennzeichen, damit der Verkehrsminister auch genau weiß, wer wann wohin gefahren ist.
Hat ein neuronales Netzwerk aber einmal ausgelernt, ordnet es den gleichen Eingangsdaten immer das gleiche Ergebnis zu. Bei Aufgaben, die sich darum drehen, aus Zeit-diskreten Wertefolgen den folgenden Wert zu ermitteln, schneiden sie oft nicht optimal ab, besonders falls das Eingangssignal Schwankungen unbekannter Periodizität unterliegt.
In neuronalen Netzwerken justiert der Lernalgorithmus interne Gewichte anhand der Trainingsdaten, die sich aber zur Laufzeit nicht mehr ändern, also keine temporalen Änderungen in den Eingangsdaten berücksichtigen können, da der Automat sich keinen Zustand merkt. Zwar führen in so genannten Recurrent Neural Networks (RNN) interne Verbindungen wieder zurück zum Eingang und beeinflussen so den nächsten Eingabevektor, doch reicht das bei simplen Netzwerken nicht aus, um zeitliche Muster zu erkennen, die sich über mehrere Zyklen erstrecken.
Beim Psychologen
Ein Beispiel zum Vorhersagen von Sequenzen sind die von Psychologen verwendeten Intelligenztests (Abbildung 1), bei denen es für eine Ziffernfolge zu ermitteln gilt, welche Ziffer als nächste folgt. Ein Schulkind kann sagen, dass nach 2, 4, 6 die 8 folgt, aber wie sieht es mit der Folge 2, 5, 7, 10, 12 aus?
![Abbildung 1: Ein Intelligenztest, bei dem der Kandidat eine Ziffernfolge ergänzen muss <a href="#artRef-i2">[2]</a>.](/wp-content/uploads/2017/10/iq-sequence-300x281.jpg)
Abbildung 1: Ein Intelligenztest, bei dem der Kandidat eine Ziffernfolge ergänzen muss [2].
Abbildung 2 zeigt zwei Lernschritte und einen Testschritt für ein “Long Short-Term Memory”-Netzwerk (LSTM), das lernen soll, welche Zahl nach der 12 kommt. Im ersten Lernschritt in der ersten Reihe der Matrix erfährt es, dass auf die Kombination »2,5,7« immer eine »10« folgt. Die zweite Reihe weist der um einen Schritt verschobenen Teilfolge »5,7,10« als Ergebnis die 12 zu. Mit diesen Trainingsdaten justiert das LSTM-Netzwerk die Parameter seiner internen Zellen (Abbildung 3).
Anders als beim neuronalen Netzwerk produziert nicht jeder Eingabewert einen Ausgabewert, sondern es merkt sich den aktuellen Zustand in einer Speicherzelle (Abbildung 4), um erst nach der – im vorliegenden Fall – dritten Eingabe und unter Auswertung des auf jedem Zeitschritt mitgeschleppten Zustands einen Ausgabewert (y(1)) zu produzieren.

Abbildung 4: Zeitlich aufeinanderfolgende Eingabewerte verändern zunächst nur den aktuellen Status und produzieren alle drei Zeitschritte eine Ausgabe.
Matrizen umkneten
Zur Implementierung des LSTM-Netzwerks greift Listing 1 auf die Python-Library »Keras« zu [3]. Da viele ihrer Funktionen Daten in Form von Matrizen entgegennehmen, bietet sich eine Erläuterung der Funktion »reshape()« aus der Numpy-Array-Library an. Einen eindimensionalen Numpy-Array, also einen Vektor, wandelt »reshape()«, wie Abbildung 5 zeigt, in Matrizen voreingestellter Dimensionen um.
Listing 1
iq
01 #!/usr/bin/python3
02 import numpy as np
03 from sklearn.preprocessing \
04 import StandardScaler
05 from keras.models import Sequential
06 from keras.layers import Dense, Activation
07 from keras.layers import LSTM
08 import os
09
10 os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
11
12 def window(npa, n=2):
13 for start in range(npa.size-n+1):
14 yield npa[start:start+n:1]
15
16 input_size=3
17
18 seq= np.array(
19 [2,5,7,10,12]).astype('float64')
20
21 print("learn input: " + str(seq))
22
23 scaler = StandardScaler()
24 seq = seq.reshape(-1,1)
25 seq = scaler.fit_transform(seq)
26 seq = seq.reshape(-1)
27
28 X=np.array([])
29 y=np.array([])
30
31 for chunk in window(seq, n=input_size+1):
32 X=np.append(X, chunk[:-1])
33 y=np.append(y, chunk[-1])
34
35 X=X.reshape((-1,input_size,1))
36 y=y.reshape((-1,1))
37
38 model = Sequential()
39 model.add(LSTM(5,
40 input_shape=(input_size,1)))
41 model.add(Dense(1))
42 model.add(Activation("linear"))
43 model.compile(loss="mean_squared_error",
44 optimizer="rmsprop")
45 model.fit(X,y, epochs=500, verbose=0)
46
47 print("\nresults:")
48 for input in X:
49 input=input.reshape(1,input_size,1)
50 pred=model.predict(input)
51 print(scaler.inverse_transform(
52 input.reshape(-1,1)))
53 print(scaler.inverse_transform(
54 pred.reshape(-1,1)))
55
56 test = seq[-input_size::1]
57 print(scaler.inverse_transform(
58 test.reshape(-1,1)))
59 test=test.reshape(1,input_size,1)
60 y1=model.predict(test)
61 print(scaler.inverse_transform(
62 y1.reshape(-1,1)))
Dabei gibt der erste Parameter nach »reshape()« die Anzahl der Elemente der ersten Dimension an, gefolgt von der Anzahl der zweiten und so weiter. Da die Anzahl der Elemente in der ersten Dimension implizit feststeht, nachdem tiefere Dimensionen festgelegt sind, wird Erstere oft als »-1« angegeben. Dann füllt die Library die Matrix mit den übrigen Elementen auf.
Mit nur einem Parameter aufgerufen (»reshape(-1)«), macht die Methode aus einer verschachtelten Array-Struktur wieder einen eindimensionalen Vektor.
Ene mene muh
Mit einem Array wie »[3,4,5,6,7]« aufgerufen (Abbildung 6) produziert das Skript relativ genau die auf die Sequenz folgende Zahl (7,84 statt 8). Listing 1 zerlegt die Zahlenreihe mit der ab Zeile 12 definierten Funktion »window« in wandernde 4er Blöcke ([3,4,5,6][4,5,6,7]) und legt jeweils die ersten drei Elemente im Eingabevektor »X« und das letzte Element im Ergebnisvektor »y« ab.
Damit dem LSTM-Netzwerk nicht seine Gewichte um die Ohren fliegen, normalisiert der aus der Sklearn-Library stammende »StandardScaler« die Werte auf kleine sowohl positive als auch negative Floatingpoint-Zahlen um den Nullpunkt herum – sowohl für den Eingabe- als auch den Ergebnisvektor, der die zum Supervised Learning notwendigen erwarteten richtigen Ergebnisse enthält.
Die Methode »fit_transform()« standardisiert die Daten. Weiter unten, wenn es an die Ausgabe der Ergebnisse geht, dreht »inverse_transform()« den Spieß um und holt zur erbaulichen Betrachtung die Originaldaten wieder hervor.
Die Zeilen 38 bis 45 stapeln die einzelnen Layer des LSTM-Netzwerks aufeinander. Als Erstes kommt in Zeile 39 der LSTM-Core-Layer mit fünf internen Neuronen hinzu. Es folgen der Ausgabe-Layer unter dem Namen »Dense« und die Activation-Funktion, die die Antwortkurve der intern verwendeten Neuronen auf »linear« setzt, da dies beim Testen die besten Ergebnisse brachte.
Auf los geht’s los
Anschließend macht »compile()« das Lernmodell startfertig und bestimmt außerdem als Lernparameter »mean_squared_error« (Abweichungen vom optimalen Lernerfolg werden nach der Methode der mittleren Quadrate gemessen) und als Optimizer den Algorithmus »rmsprop«, ein bei neuronalen Netzwerken gängiges Verfahren.
Zeile 45 ruft nun die Methode »fit()« des Modells auf, übergibt ihr die Lerndaten und verlangt mit »epoch=500« entsprechend viele Lerndurchgänge. Im Test zeigten sich bei kürzeren Lernphasen schlechtere Ergebnisse, doch auch größere Werte für »epoch« halfen nicht zu mehr Lernerfolg, da sich das System danach auspendelte und der Lernprozess, sichtbar am gleichbleibenden Wert für die »loss«-Funktion, stagnierte.
Der Abschnitt von Listing 1 ab Zeile 47 gibt dann die mit dem austrainierten Modell erzielten Vorhersagen aus, sowohl für die Trainingsdaten als auch für die Fortsetzung der Folge, die das System rein aus vorher Gelerntem ableiten muss, da sich in den Trainingsdaten kein entsprechender Präzedenzfall findet.
Abbildung 7 zeigt, dass das Netzwerk auch bei zyklischen Daten (1,2,3,1,2,3) eine gute Figur macht, übrigens unabhängig davon, wie lang die Periode des Signals ist. Das ist ein wesentlicher Vorteil gegenüber klassischen neuronalen Netzwerken, denen man die Periodizität vorher zustecken muss, damit sie zuverlässige Vorhersagen machen.

Abbildung 7: Bei einer zyklischen Sequenz sagt der Algorithmus den jeweils nächsten Wert ungefähr richtig voraus.
Schnell wie die Schnecke
Um die »keras«-Library auf den Rechner zu holen, sind folgende Bibliotheken aus dem Python-Fundus zu installieren:
pip3 install --user keras pandas tensorflow sklearn numpy sudo apt-get install python-tk
Gerade das von »keras« verwendete Tensorflow-Backend ist kein Geschwindigkeitsmonster, auf meinem fünf Jahre alten PC dauerte es schon gute 10 Sekunden, bis das Programm überhaupt loslegte.
Beim Intelligenztest machte das Netzwerk dann aber keine so gute Figur (Abbildung 8). Bei der Serie »2,5,7,10,12« sind, wie Ratefüchse sicher erkannt haben, jeweils abwechselnd 3 und 2 zu den Zahlen hinzuzuaddieren. Da der Sprung von 10 auf 12 zwei Einheiten lang war, muss die nächste Zahl also drei Werte vorne liegen, 15 ist das richtige Ergebnis. Das Netzwerk tendierte eher zu 14, kann also mit menschlicher Intelligenz (noch) nicht mithalten. (uba)

Abbildung 8: Beim Intelligenztest schneidet das LSTM-Netzwerk nicht gut ab, da es die unterschiedlichen Zuwachsraten nicht erkennt.
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2017/10/plus
Infos
-
Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2017/10/snapshot/
-
Number Sequence Test: https://www.fibonicci.com/numerical-reasoning/number-sequences-test/easy/
-
Jason Brownlee, “Long Short-Term Memory Networks with Python”: Machine Learning Mastery, 2017, https://machinelearningmastery.com/lstms-with-python/











