6.5 Das sin² Problem revisited
Bei der Fragestellung in Teil 1, “Kann KI lernen, gerade und ungerade Zahlen zu unterscheiden?” versuchten wir den Ansatz einer Transformation der Eingangsdaten mittels einer sin² Funktion. Dabei stellte sich heraus, dass die Lösung nur eine scheinbare ist, da die Eigenschaft einer Zahl, Vielfaches von 2 zu sein, bereits als Parameter in der Transformationsfunktion vorgegeben war.
Die Idee war dann, diesen Parameter “lernen zu lassen”, d.h. über ein ML Verfahren und Vorgabedaten zu “trainieren”. Das misslang und es wurde auch dargestellt, warum. Wir wollen das Problem hier noch einmal aufgreifen unter dem Aspekt der universellen Approximationseigenschaft von Neuronalen Netzen.
Dabei werden wir nebenbei ein einfaches Minimum Jumping Verfahren entwickeln, mit dem bei erratischem Konvergenzverhalten im Training eine determinierte Verbesserung erzwungen werden kann.
In einem Artikel von R. Mehran findet sich die Konstruktion eines NN, das uns hier möglicherweise weiterhilft, weil sie von der universellen Approximationseigenschaft von NN Gebrauch macht. Das Ziel des Artikels ist allerdings ein anderes, nämlich ein Performancevergleich verschiedener Approximationsverfahren anhand von Testfunktionen — unter anderem der “gedämpften Sinus-Produkt-Funktion”:
f(x,y) = sin(x)*sin(y)/(x*y)
Diese Funktion sieht über einem Quadrat von -10.0
bis 10.0
in x- und y-Richtung so aus:
import numpy as np
import random as rd
import matplotlib.pyplot as plt
%matplotlib inline
from numpy import pi, sin
from matplotlib import cm
from mpl_toolkits import mplot3d
# Plot RM's function
def Phi(x,y):
return sin(x)*sin(y)/(x*y)
# 3-d Plot für Phi
x_low = -10
x_high = -x_low
y_low = x_low
y_high = x_high
x = np.linspace(x_low, x_high, 50)
y = np.linspace(y_low, y_high, 50)
R, S = np.meshgrid(x, y)
T = Phi(R,S)
fig = plt.figure(figsize=(10,6))
ax = plt.axes(projection='3d')
ax.contour3D(R, S, T, 50) #, cmap='binary')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x,y)')
Dies ist die nach außen hin “gedämpfte” Version der einfachen sin-Produktfunktion g(x,y) = sin(x)*sin(y)
:
def Phi(x,y):
return sin(x)*sin(y)
Wenn wir hier beide Argumente gleichsetzen x=y
, erhalten wir die gedämpfte sin^2
Funktion:
bzw. die Funktion, die wir als Transformation in Teil 1 verwendet haben, sin(x*w)**2
mit w = pi/2
hier für -4 <= x <= 4
.
Das Neuronale Netz von Mehran
Mehran findet experimentell als bestes Modell für seinen 2-dim Fall ein NN mit 9 oder 10 Neuronen im Hidden Layer (hier in keras
nachgebaut).
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import sgd, adam
from keras.initializers import zeros, ones, constantact = 'tanh' # Several alternatives tested in addition
mf = Sequential()
mf.add(Dense(10, activation=act,input_shape=(2,)))
mf.add(Dense(1, activation=act))
mf.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_3 (Dense) (None, 10) 30
_________________________________________________________________
dense_4 (Dense) (None, 1) 11
=================================================================
Total params: 41
Trainable params: 41
Non-trainable params: 0
Wenn das NN mit 10 hidden Neuronen die 2-dim Aufgabe ziemlich gut löst (bei Mehran: mse(Train)~0.005, mse(Test)~0.006
), gehen wir davon aus, dass es das einfachere Problem als Spezialfall ebenso gut löst.
Wir wenden daher dieses NN auf unser sin**2
-Problem an, d.h. für die Frage, kann das NN so trainiert werden, dass es die sin**2
Funktion approximiert - sogar so gut, dass das trainierte NN anschließend die sin**2
Funktion ersetzen kann?
Dazu bauen wir Training und Test-Evaluation wie üblich auf, setzen aber stets x=y
, d.h. betrachten nur Zahlenpaare, die in der Fläche auf der "Diagonalen" liegen. Wir wählen x von -2
bis 2
in äquidistanten Schritten (alternativ zufallsverteilt). Bei 0.02 Schrittweite bekommen wir 201 Trainingsdaten mit y=sin(x*pi/2)**2
als Zielwerte.
# Train with (x,x) with x from -2 to 2 in steps of 0.02
T = np.arange(-2.0,2.02,0.02)
T = np.column_stack((T,T))
X = T
n_cases = X.shape[0]
def fsin2(z):
x,y = z
return sin(x*pi/2)*sin(y*pi/2)
y = np.array(list(fsin2(z) for z in X))
plt.plot(X[:,0],y,'b')
plt.show
Die Grafik zeigt sin(x*pi/2)**2 = sin(x*pi/2)*sin(x*pi/2)
zwischen -2
und 2
.
Training des NN
Wir trainieren mit Zahlenpaaren (x,x)
.
sgd1 = sgd(lr= 0.03)
mf.compile(optimizer=sgd1,loss='mean_squared_error') ep = 400
hstep = 10
hist1 = mf.fit(X,y,epochs=ep,batch_size=1,verbose=0)
print([hist1.history['loss'][i] for i in range(0,ep,hstep)])plt.plot(hist1.history['loss'],'g--')
plt.show()Loss-Entwicklung:
[0.14980399444664103, 0.13072902722879054, ...., 0.09456077068401765]
Der mse
geht über 400 Epochen von anfangs 0.2 auf 0.002 herunter, was ziemlich gut den Experimenten von Mehran enstpricht [1]. Schauen wir uns dazu den Vergleich von wahren Funktionswerten (y_true=y
, blau) und den für die Trainigspunkte vom NN berechneten Werten y_pred
(rot) an.
# Test Evaluation with (x,x) with x from -2 to 2ind steps of 0.02S = np.arange(-2.2,2.22,0.02)
S = np.column_stack((S,S))
y_pred = mf.predict(S)
y_true = np.array(list(fsin2(z) for z in S))
plt.plot(X[:,0],y_pred,'r')
plt.plot(X[:,0],y_true,'b')
plt.show
print('Plot true(blue) and trained(red) function: sinc(x,y)=sin(x)sin(y) with y=x in[-pi,pi]')
Das sieht bemerkenswert gut aus. Allerdings finden wir hier wieder den typischen “Interpolationseffekt”. Für Werte über den Trainingsbereich hinaus sieht das Bild nicht so gut aus. Etwa für x-Werte zwischen -4 und 4:
# Test Evaluation with (x,x) with x from -4 to 4 ind steps of 0.02
S = np.arange(-4.2,4.22,0.02)
S = np.column_stack((S,S))
...
(Für Kölner sieht das Bild trotzdem schön aus.) Das NN zeigt sich jedoch offensichtlich unbrauchbar außerhalb des Trainingsbereichs.
Erweitern wir also den Trainingsbereich entsprechend auf -4 <= x<= 4
und trainieren erneut wie zuvor. Nach 100 Trainingsepochen ergibt sich typischerweise folgendes Bild:
Man erkennt den “guten Willen”, daher verdoppeln die Epochenzahl auf 200.
Grafisch erscheint die Approximation nur geringfügig verbessert. Tatsächlich ergeben sich bei Verlängerung des Trainings keine Verbesserungen, die der Qualität der Approximation zwischen -2 und 2 oben nahe kommt.
Das lässt sich durch einen Blick auf die mse-loss
-Entwicklung über die nächsten 400 Epochen verstehen.
Die loss
-Funktion bewegt sich mit größerer Epochenzahl insgesamt zwar weiter nach unten, aber die erratischen Ausschläge zeigen, dass wir keine gesicherte Verbesserung der NN-Approximation erwarten können.
ep = 400
hstep = 10
hist1 = mf.fit(X,y,epochs=ep,batch_size=1,verbose=0)
print([hist1.history['loss'][i] for i in range(0,ep,hstep)])
plt.plot(hist1.history['loss'],'g--')
plt.show()Loss-Entwicklung:
[0.09722117241235681, 0.10116305655876755, ...., 0.08659219566520236]
Es ist also schwer vorherzusagen, ob ein Training des NN eine gute Approximation liefert oder nicht.
Auch die Form der Anpassung an das Zielbild variiert stark, was damit zusammen hängt, dass wir bei diesem Modell die initialen Gewichte per Default und zufällig erzeugen und damit die Entwicklung der Gewichte beim Training natürlich auch nicht gleich verläuft. Das folgende Bild zeigt ein extremes Ergebnis eines Trainingslaufs mit 100 Epochen.
Man beobachtet nämlich, dass die Gewichte der 2. Dense-Schicht stets dazu neigen teilweise gegen Null zu gehen. Im obigen Fall waren nach dem Training alle diese Gewichte nahe Null. Damit erklärt sich das Ergebnis.
Optimizer Tuning
Das erratische Verhalten der loss
-Funktion bei insgesamt noch absteigendem Trend lässt darauf schließen, dass die Korrekturen der Gewichte beim gewählten Optimizer-Verfahren (und deren Parametrisierung) "überschießen". Wir prüfen daher einen anderen keras
-Optimizer, der diesen Effekt adaptiv auffangen kann: adadelta
(s. keras
-Dokumentation).
D.h. wir ersetzen den Parameter optimizer=sgd
durch adadelta
:
# Compile mf using adadelta optimizer
opt = adadelta()
mf.compile(optimizer=opt,loss='mean_squared_error')
Das typische Annäherungsverhalten über die Epochen bei adadelta
ist glatter (loss
) langsamer und gleicht im Ergebnis dem sgd
-Ergebnis. Hier das Bild eines Trainingslaufs nach 1200 Epochen.
Weitere Verbesserungen durch mehr Epochen ergeben sich typischerweise nicht.
Minimum Jumping
Die erratischen loss
-Kurven zeigen, dass der Fehler bei höheren Epochenzahlen neben der mittleren Tendenz starke Ausschläge nach oben und unten zeigt. Wir können also versuchen, dem Training ein Verfahren zu überlagern, dass man als "Minimum Jumping" bezeichnen kann. Dabei springt das Verfahren von Loss-Minimum zum nächsten besseren Loss-Minimum und merkt sich die dazu gehörigen Gewichte-Sets. Das kann man über mehrere Perioden führen, wobei jede Periode mit dem aktuellen Loss-Minimum und dem zugehörigen Gewichte-Set startet.
Auf diese Weise wird von Periode zu Periode der Fehler verbessert, sofern er sich überhaupt noch nach unten bewegt. Entprechend verbessert sich die Anpassung des NN an die Zielfuntion.
Ausgehend vom untrainierten Zustand (initiale Gewichte), der folgendes Bild ergibt (Epoche 0)
bekommen wir per Minimum Jumping nach insgesamt 750 Epochen tatsächlich eine sehr gute Annäherung:
Anwendung auf die Transfomationsmethode zum Lernen der Gerade/Ungerade-Klassifikation
Das Problem bei der Transformation mit sin(x*w)^2
in Teil 1 war, dass wir den Parameter w=pi/2
setzen mussten und nicht aus Trainingsdaten lernen konnten. Damit war die Gerade/Ungerade-Klassifikation von Zahlen x
bereits in die Transformation "eingebaut". Kein Wunder also, dass ein SVM
-Verfahren dann die Klassen trennen konnte.
Haben wir etwas gewonnen dadurch, dass wir die Transformation durch ein NN approximieren können? In Abb. 10b sehen wir, dass eine Trennlinie auf Höhe 0.5
die geraden Zahlen des Trainingsbereichs (-4, -2, 0, 2, 4) und die ungeraden (-3, -1, 1-,3) sauber klassifizieren kann.
Um beliebige 4-stellige ganze Zahlen zu klassifizieren müsste man jedoch entweder
a) das NN über den gesamten Zahlenbereich approximieren, was praktisch kaum möglich ist, oder
b) die Zahlen vorher in den vorgegebenen Trainingsbereich, z.B. -4 bis 4, transformieren. Besipiel: 4365 -> 4365–4362 = 3. Die Transformation wäre zulässig, weil sin^2
periodisch mit Periode pi
ist, d.h. für x=4365
den gleichen Wert liefert wie für x=3
.
Allerdings ist beides nicht zielführend: zum Einen trainieren wir für die Approximation nicht mit ganzen Zahlen und zugehörigen Klassen und zudem müssen wir in b) eine passende gerade Zahl subtrahieren, also schon wissen, welche Zahlen gerade und ungerade sind.
Eine Simulation des Trainings mit ganzen Zahlen lässt sich prinzipiell machen, indem als Trainingsdaten für x
nur ganze Zahlen vorgegeben werden. Das Ergebnis eines solchen Versuchs mit Zahlen von -10 bis 10 nach 4500 Epochen zeigt die folgende Grafik:
In Bezug auf die Methoden in Teil 1 bietet das Approximationtheorem offenbar keine neuen Möglichkeiten, gerade und ungerade Zahlen unterscheiden zu lernen.
Weiter lesen: 6.6 Boolesche Funktionen und Quanten-Gates — Neuronale Netze lernen Logik
bernhard.thomas@becketal.com
www.becketal.com