Bernd Thomas
Beck et al.
Published in
8 min readJan 24, 2020

--

6.5 Das sin² Problem revisited

weltderphysik.de

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)')
Abb. 1a: Die Testfunktion von R. Mehran

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)
Abb. 1b: Die ungedämpfte Funktion

Wenn wir hier beide Argumente gleichsetzen x=y, erhalten wir die gedämpfte sin^2Funktion:

Abb. 2a: Die gedämpfte sin² 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.

Abb. 2b: Die sin² Funktion aus Teil 1

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, constant
act = '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
Abb. 2c: Die Trainingskurve

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]
Abb. 3: Der mse im Verlauf von 400 Trainingsepochen

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]')
Abb. 4: Grafischer Vergleich Zielfunktion (blau) und NN (rot)

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))

...
Abb.. 5: NN-Berechnung (rot) über den Interpolationsbereich hinaus

(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:

Abb. 6a: Vergleichsplot nach 100 Trainingsepochen

Man erkennt den “guten Willen”, daher verdoppeln die Epochenzahl auf 200.

Abb. 6b: Vergleichplot nach 200 Trainingsepochen

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]
Abb. 7: Die loss-Entwicklung über weitere 400 Epochen

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.

Abb. 8a: Ein extremes Trainingsergebnis

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.

Abb. 8b: Die Konvergenz sieht gut aus, aber man beachte die Gewichte der zweiten Schicht

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.

Abb. 9: Optimizer adadelta — ein Ergebnis 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)

Abb. 10a: NN Approximation mit initialen Gewichten

bekommen wir per Minimum Jumping nach insgesamt 750 Epochen tatsächlich eine sehr gute Annäherung:

Abb. 10b: Sehr gute Anpassung durch “minimum jumping”

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:

Abb. 11: Versuch der Approxination mit ganzen Zahlen von -10 bis 10.

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

Zurück auf Anfang

bernhard.thomas@becketal.com
www.becketal.com

--

--

Bernd Thomas
Beck et al.

Dr. Bernhard Thomas — Mathematics, Theor. Biology, Computational Sciences, AI and advanced Technologies for the Enterprise. Beck et al. Consultant