Bernd Thomas
Beck et al.
Published in
36 min readDec 23, 2019

--

Bonustrack A: Formel-basierte Neuronale Netze zur Multiplikation

Foto pixabay.com

Dass das Multiplizieren Lernen für NNs keine einfache Aufgabe ist, hatten wir in 6.4 Multiplizieren Lernen — Problematisch für NN erläutert.

Wir hatten dort zunächst das Multiplizieren Lernen naiv als Interpolationsaufgabe für ein NN formuliert in der Erwartung, dass die Universelle Approximationseigenschaft von NNs das Produkt als Funktion von zwei Faktoren darstellen kann. Das Ergebnis ist allerdings nicht sehr zurfieden stellend, weil das Netz die Multiplikation nur im Bereich der Trainingsdaten einigermaßen zuverlässig erlernt (Interpolation) und dann nur über vorherige Skalierung von beliebigen Zahlen in Input-Faktoren aus dem Trainingsbereich Produkte berechnen kann. Das wiederum bedeutet, dass bereits außerhalb des NN Multiplikationen stattfinden müssen.

Wir hatten dann in 6.4.3–6.4.5 andere, *formelbasierte* NN konstruiert, ausgehend von einer Idee von M. Tegmark in seinem Buch “Leben 3.0”.

In diesem Bonustrack wollen wir diese, ganz anders strukturierte NN-Modelle mit dem Multiplikationsproblem konfrontieren, in der Hoffnung, bessere universelle Lern-Ergebnisse zu erzielen.

Hier die Modellansätze, die wie im Folgenden detaillierter explorieren:

  • Eine einfachere “Einstiegsversion” des 5-Neuronen-Modells von Tegmark
  • Das Modell von Tegmark zur Multiplikation beliebiger Zahlen
  • Ein nahezu perfektes Modell für die Multiplikation als Spezialfall
  • Ein perfektes Modell abgeleitet aus den Binomischen Formeln

Wir werden sehen, dass auch diese NN-Modelle ihre Einschränkungen haben, sowohl bei der Berechnung eines Produkts als auch, insbesondere, in der Trainierbarkeit. Die beste Version stellt das einfache, aus der Anwendung der 1. Binomischen Formel abgeleitete NN dar.

Alle Modelle basieren auf mathematischen Ausdrücken (Formeln), die sich auf einfache Neuronale Netze abbilden lassen. Insofern sind diese NNs als Berechnungsverfahren eigentlich obsolet. Bedeutung bekommt das NN lediglich als “lernendes” NN, d.h. wenn es auf die korrekte Multiplikation hin trainierbar ist.

A1 Ein 5-Neuronen Modell für die Multiplikation (nach Tegmark [1] [2])

[1] M. Tegmark: Leben 3.0 (Ullstein 2017) und [2] Tegmark M., H. Lin, D. Rolnick: “Why does deep and cheap learning work so well” (http://arxiv.org/abs/1608.08225)

In seinem Buch [1] schreibt Tegmark:

“…(Die Abbildung) zeigt, wie nur fünf Neuronen zwei beliebige Zahlen miteinander multiplizieren können …”

Wir wollen sehen, ob das tatsächlich funktioniert und wir damit ein ML-System haben, das das allgemeine Multiplizieren lernt.

Im Artikel [2] wird beschrieben, wie man durch eine elegante mathematische Herleitung zu diesem 5-Neuronen-Modell kommt.

Um den Trick zu verstehen, vereinfachen wir das Problem der Multiplikation zweier Zahlen zunächst auf die Multiplikation zweier gleicher Zahlen, also der Berechnung von Quadratzahlen, die wir in 6.2 bereits per NN-Interpolationsansatz zu lernen versucht haben.

A1.1 Zunächst ein einfacheres Modell: Zwei gleiche Faktoren multiplizieren

Schauen wir uns dazu folgendes keras-Modell an und versuchen zu verstehen, was es (mathematisch) bedeutet.

inp = Input(shape=(1,),name='Input_h')
x = (Dense(3,activation='sigmoid',use_bias=True,name='Dense_1',
trainable=False,
kernel_initializer=init_d1,bias_initializer=ones()))(inp)
out = (Dense(1,activation='linear',use_bias=False,
name='Dense_2',trainable=False,
kernel_initializer=init_d2))(x)

msq = Model(inp,out)
msq.summary()
msq.compile(optimizer='adam',loss='mean_squared_error',metrics=['accuracy'])

_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input_h (InputLayer) (None, 1) 0
_________________________________________________________________
Dense_1 (Dense) (None, 3) 6
_________________________________________________________________
Dense_2 (Dense) (None, 1) 3
=================================================================
Total params: 9
Trainable params: 0
Non-trainable params: 9

Die Gewichte werden initialisiert (und nicht trainiert.) Die Aktivierungs-funktion des ersten Layers soll nicht-linear sein (hier sigmoid). Die Grafik illustriert das Modell samt Gewichten, die im keras Code durch die vorher definierten initializer-Funktionen init_d1, init_d2 sowie ones() für die Bias-Gewichte von Dense_1 gesetzt werden (d.h. a=1 ).

Abb. 1: Das obige keras-Modell als NN-Grafik

Wir wollen uns ansehen, was dieses Modell im Feed Forward Durchlauf berechnen würde.

Input h wird über die drei Neuronen gewichtet und mit Bias versehen zu 1*h+a, 0*h+a, -1*h+a. Die Aktivierungsfunktion f macht daraus f(a+h), f(a), f(a-h). Diese werden, gewichtet, im nächsten Layer summiert und über die lineare Aktivierung zum Output: f(a+h)-2*f(a)+f(a-h).

Man ahnt hier schon — zumindest, wenn man sich an den Mathematikunterricht erinnert — eine Taylorreihen-Entwicklung. Der Output des Modells ist eine Näherung für die zweite Ableitung von f, multipliziert mit h-Quadrat, also

(F1) f(a+h)-2*f(a)+f(a-h) = f''(a)*h^2 + R(h^4)

(f''(a) bezeichnet die 2. Ableitung von f an der Stelle a.)

wenn h klein genug ist, d.h. a+h, a-h ganz nahe bei a liegen. Der Rest (Approximationsfehler) R(h^4) ist 4. Ordnung und wird dann so klein, dass man ihn vernachlässigen kann.

Wozu das Ganze? Nun, wenn wir das Quadrat von h durch ein NN berechnen wollten, würde das obige 4-Neuronen-Modell das können, sofern wir den Output noch durch f''(a) teilen. Das lässt sich in das NN integrieren durch Einfügen einer weiteren 1-Neuron-Schicht nach der Summation, mit der Gewichtung g = 1/f''(a). Anders als bei Tegmark (s.u.) nutzen wir hier eine NN-Komponente statt einem externen Postprocessing.

Abb. 2: Das vollständige NN für h²

Das geht natürlich nur, wenn die Aktivierungsfunktion f zweimal stetig differenzierbar ist und f''(a) nicht Null ist. Das a (Bias der ersten Schicht) ist im Prinzip beliebig, mit a=0.0 haben beliebte Aktivierungsfunktionen wie sigmoid oder tanh als 2. Ableitung allerdings den Wert Null.

Anm.: Die mathematische Herleitung ist einfach: Man schreibt das f(a+h) und das f(a-h) als Taylorentwicklung an der Stelle a bis einschließlich h^3 (dritte Ableitung) und addiert die beiden Ausdrücke. Die linearen Summanden h*f'(a) und die kubische Summandend h^3*f'''(a) heben sich dabei auf und durch Umstellen der Gleichung kommt man auf den obigen Ausdruck.

Wir wählen hier z.B. die sigmoid-Funktion als Aktivierung und a=1.0. Damit ist f''(1.0)=-0.090857 (berechnet mit wolfram alpha ), und wir setzen den kernel_initializer von Dense_3 entsprechend.

# Extension of model to include 
# compensation of f''(a)

inp = Input(shape=(1,),name='Input_h')
x = (Dense(3,activation='sigmoid',use_bias=True,name='Dense_1',
kernel_initializer=init_d1,bias_initializer=init_b1,
trainable=False))(inp)
x = (Dense(1,activation='linear',use_bias=False,name='Dense_2',
kernel_initializer=init_d2,trainable= False))(x)
out = (Dense(1,activation='linear',use_bias=False,name='Dense_3',
kernel_initializer=init_d3,trainable= False))(x)

msq = Model(inp,out)
msq.summary()
msq.compile(optimizer='adam',loss='mean_squared_error',metrics=['accuracy'])

_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input_h (InputLayer) (None, 1) 0
_________________________________________________________________
Dense_1 (Dense) (None, 3) 6
_________________________________________________________________
Dense_2 (Dense) (None, 1) 3
_________________________________________________________________
Dense_3 (Dense) (None, 1) 1
=================================================================
Total params: 10
Trainable params: 0
Non-trainable params: 10

Damit bildet das 5-Neuronen Modell die mathematische Taylorreihen-Approximation ab und das 6-Neuronen-Modell berechnet daraus das x^2 zum Input x. (Wir bezeichnen jetzt die zu quadrierende Zahl mit x .)

Wir wollen sehen, wie gut.

Als Input wählen wir die ganzen Zahlen von 0 bis 10. Da die Näherungsformel nur für ganz kleine Werte genau genug wird, müssen wir den Input x mit einem Faktor 1/sc auf hinreichend kleine Werte skalieren und das Ergebnis anschließend mit dem Quadrat von sc wieder auf die richtige Größenordnung bringen.

# Calculations with scaling 
for k in range(4,0,-1):
for x in range(11):
sc = 10**k
h = x/sc
pred_sqr = msq.predict([h])*sc**2
print('%8.6f %12.10f %12.10f' %(h,pred_sqr,x**2))
Ergebnisse für verschiedene Skalierungen: x pred x^2 true x^2
--------------------------------------
Scaling: 0.001
0.000000 0.0000000000 0.0000000000
1.000000 1.9680646801 1.0000000000 *
2.000000 4.5921510718 4.0000000000 *
3.000000 9.1843021437 9.0000000000
4.000000 16.4005396073 16.0000000000
5.000000 24.9288204941 25.0000000000
6.000000 36.0811864084 36.0000000000
7.000000 49.8576409882 49.0000000000 *
8.000000 64.2901140964 64.0000000000
9.000000 82.0026980364 81.0000000000 *
10.00000 99.7152819764 100.0000000000 *

x pred x^2 true x^2
--------------------------------------
Scaling: 0.01
0.000000 0.0000000000 0.0000000000
1.000000 0.9971528053 1.0000000000
2.000000 4.0082917213 4.0000000000
3.000000 9.0071763992 9.0000000000
4.000000 16.0003662109 16.0000000000
5.000000 25.0009822845 25.0000000000
6.000000 35.9893455505 36.0000000000
7.000000 48.9785690308 49.0000000000
8.000000 63.9555473328 64.0000000000
9.000000 80.9268264771 81.0000000000
10.00000 99.8924102783 100.0000000000

x pred x^2 true x^2
--------------------------------------
Scaling: 0.1
0.000000 0.0000000000 0.0000000000
1.000000 0.9989241362 1.0000000000
2.000000 3.9819200039 4.0000000000
3.000000 8.9089040756 9.0000000000
4.000000 15.7135543823 16.0000000000
5.000000 24.3054695129 25.0000000000 *
6.000000 34.5718154907 36.0000000000 *
7.000000 46.3800697327 49.0000000000 *
8.000000 59.5814590454 64.0000000000 *
9.000000 74.0128784180 81.0000000000 *
10.00000 89.5027313232 100.0000000000 *

Man sieht, dass für 1/sc mit zunehmender Größenordnung die NN-Näherung für x^2 schlechter wird. In der Größenordnung 1/100 ist das Egebnis halbwegs korrekt. Das entspricht den Erwartungen.

Für kleinere Skalierungsfaktoren 1/sc und damit kleiner h=x/sc werden die (relativen) Abweichungen aber wieder größer. Warum das so ist, greifen wir später im Absatz Fehleranalyse noch einmal auf.

Hier sei nur schon erwähnt, dass mit kleinerem h zwar (gemäß Formel (F1)) der Approximationsfehler reduziert wird, dabei aber gleichzeitig die Auswirkung von Fehlern in der Berechnung (Maschinengenauigkeit, Rundungsfehler, Auslöschung) verstärkt werden. Daher kann man nicht erwarten, dass man mit immer kleinerm h immer bessere Resultate bekommt.

Hinzu kommt der Effekt des Hochskalierens! Wir haben nämlich wieder das Problem wie in 6.3, dass eine beliebige Zahl x nur dann mit diesem NN quadriert werden kann, wenn sie vorher auf die richtige Größenordnung transformiert wird, etwa x = 568 auf h = x/s = 0.00568 mit einem Skalierungswert von etwa s = 10^5 oder größer. Um auf x^2 zu kommen, muss dann das NN-Ergebnis wieder mit s^2 multipliziert werden!

(Anm.: Wir bezeichnen im Folgenden die Größe s als Skalierungswert im Gegensatz zum Skalierungsfaktor 1/s.)

Wir können aber die Auswirkungen der Rechenfehler bei kleinem h = x/s dadurch deutlich mildern, dass wir die Genauigkeit der Gleitkommazahlen-Darstellung "im Rechner" erhöhen. Tensorflow/keras arbeiten standardmäßig mit "einfacher Genauigkeit" (Datentyp float32), da das für alle typischen ML-Probleme, z.B. Klassifikation von Bildern, ausreicht. Man kann aber "Doppelte Genauigkeit" (float64) gleich zu Beginn des Codes erzwingen, durch

# Set floating point precision for keras / Tensorflowfrom keras import backend as K#prc = 'float32'   
prc = 'float64'
K.set_floatx(prc)

Mit dem gleichen Modell berechnen wir nun wieder (feed-forward) die Quadrate für die Zahlen von 0–10 und gehen dabei mit dem Skalierungsfaktor bis auf 1/10^6 herunter.

x         pred x^2     true x^2    
--------------------------------------
Scaling: 0.00001
0.000000 0.0000000000 0.0000000000
1.000000 0.9999953113 1.0000000000
2.000000 4.0000179032 4.0000000000
3.000000 8.9999944596 9.0000000000
4.000000 15.9999860772 16.0000000000
5.000000 25.0000049755 25.0000000000
6.000000 36.0000022770 36.0000000000
7.000000 49.0000024204 49.0000000000
8.000000 64.0000054057 64.0000000000
9.000000 80.9999990135 81.0000000000
10.000000 100.0000076826 100.0000000000

x pred x^2 true x^2
--------------------------------------
Scaling: 0.0001
0.000000 0.0000000000 0.0000000000
1.000000 1.0000000768 1.0000000000
2.000000 4.0000000629 4.0000000000
3.000000 8.9999998361 9.0000000000
4.000000 15.9999997629 16.0000000000
5.000000 24.9999993546 25.0000000000
6.000000 35.9999986112 36.0000000000
7.000000 48.9999974104 49.0000000000
8.000000 63.9999953858 64.0000000000
9.000000 80.9999927817 81.0000000000
10.000000 99.9999886205 100.0000000000

x pred x^2 true x^2
--------------------------------------
Scaling: 0.001
0.000000 0.0000000000 0.0000000000
1.000000 0.9999998862 1.0000000000
2.000000 3.9999981885 4.0000000000
3.000000 8.9999908268 9.0000000000
4.000000 15.9999710022 16.0000000000
5.000000 24.9999292021 25.0000000000
6.000000 35.9998531911 36.0000000000
7.000000 48.9997280201 49.0000000000
8.000000 63.9995360126 64.0000000000
9.000000 80.9992567843 81.0000000000
10.000000 99.9988672234 100.0000000000

Dargestellt sind hier die Ergebnisse für Skalierungen von 1/10^5 bis 1/10^3. Die besten Werte findet man (nach Augenschein) bei Skalierungen zwischen 0.0001 und 0.00001. Die Näherungswerte pred x^2 sind deutlich besser als bei float32 und als Ergebnis durchaus akzeptabel.

Mit 1/10^6 oder kleiner verschlechtern sich die Ergebnisse, aus gleichem Grund wie oben. Mit 0.001 und größer wächst der Approximationsfehler wieder. Eine genauere Darstellung gibt es im Absatz Fehleranalyse.

A1.2 Das Tegmark Modell für die Multiplikation

Das Modell von Tegmark sieht in [1] schematisch wie folgt aus:

Abb. 3: Das Tegmark-NN (Originalabbildung aus [1], S. 114)

Wir verwenden wieder die Darstellung wie bei der Quadratberechnung (in [1] sind allerdings die die Gewichte etwas anders definiert, s.u.), die sich aus folgender Taylor-Approximation ableitet (s. [2]):

(F2) f(x0+u+v)+f(x0-u-v)-f(x0+u-v)-f(x0-u+v) = u*v*(4*f''(x0) + R(u^2+v^2))

wobei u,v hier die Rolle von h in (F1) spielen, also so klein sind, dass der Rest R gegenüber 4*f''(x0) vernachlässigbar wird. Die linke Seite entspricht den ersten Schichten des Modells aus (F1). Die Division durch 4*f''(x0) entspricht dem Neuron mit Gewicht g. Damit haben wir das Produkt aus zwei (sehr kleinen) Zahlen u*v approximiert.

Abb. 4: Das Tegmark-Modell als NN mit festen Gewichten gemäß (F2)

Anm.: In [2] ist x0=0 gesetzt, daher muß f''(0)!=0 sein, was z.B. für einige klassische Aktivierungsfunktionen nicht zutrifft (sigmoid,tanh, relu).

Die Formel (F2) kann man sich selbst herleiten: Man setzt die vier u,v Ausdrücke in die Formel (F1) oben anstelle von h ein und diese dann in die linke Seite der Formel (F2). Mit etwas Rechnen und Vereinfachen ergibt sich dann (F2).

Um “zwei beliebige Zahlen” a,b damit zu "multiplizieren", müssen wir diese wieder auf kleine Werte u=a/s und v=b/s skalieren, damit der Approximationsfehler klein genug wird. Das Ergebnis u*v der skalierten Werte müssen wir anschließend wieder mit s^2 multiplizieren, um auf das Ziel-Produkt a*b zu kommen.

Wir skalieren hier beide Faktoren der Einfachheit halber mit dem gleichen Faktor 1/s. Man kann ebenso gut mit unterschiedlichen Werten r,s für die jeweiligen Faktoren arbeiten. Die abschließende Hochskalierung nutzt dann r*s statt s^2.

Im keras Modell können wir die Skalierungen, sowie die Division durch 4*f''(x0), durch gesonderte Lambda-Custom Layer einfügen. Damit ergibt sich folgende Modell-Struktur (Die Variable sc_in entspricht hier dem 1/s.)

# Multiplication NN implementing the Tegmark Model [1]
# *** Variant A. Down-/Up-Scaling in custom layers ***

import numpy as np
from keras.models import Model
from keras.layers import Dense, Input, Lambda
from keras.initializers import zeros, ones, constant
from keras import backend as K

prc = 'float64' # Set precision to double
K.set_floatx(prc)
def sigmoid2(x): # 2nd derivative of sigmoid
e = np.exp(x)
e = -(e*(e - 1))/(e + 1)**3
return e
x0 = 1.0 # a in text and graphics
act = 'sigmoid' # Activation function (f in text)
sig2 = sigmoid2(x0) # 2nd derivative of sigmoid for x=1.0

sc_in = 1/500 # Set Input scaling factor
sc_out = 1/(4*sig2*sc_in**2) # Output scaling factor including 2nd
# derivative

# Define theoretical weights for initializationdef dense1_init(shape,dtype=None):
w = 1.0
a = np.array([[w,-w,w,-w],[w,-w,-w,w]])
a = np.reshape(a,shape)
return a
def dense2_init(shape,dtype=None):
w = 1.0
a = np.array([w,w,-w,-w])
a = np.reshape(a,shape)
return a
inp = Input(shape=(2,),name='Input')

# Input scaled down by factor 'sc_in' as Lambda layer
x = Lambda(lambda s: s*sc_in, name='Lambda_In') (inp)
x = Dense(4,name='Dense_1',activation=act,
bias_initializer=constant(x0),
kernel_initializer=dense1_init,trainable=False)(x)
x = Dense(1,name='Dense_2',activation='linear',use_bias=False,
kernel_initializer=dense2_init,trainable=False)(x)
# Output scaled up by 'sc_out' as Lambda layer
out = Lambda(lambda s: s*sc_out,name='Lambda_out')(x)

m = Model(inp,out)
m.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input (InputLayer) (None, 2) 0
_________________________________________________________________
Lambda_In (Lambda) (None, 2) 0
_________________________________________________________________
Dense_1 (Dense) (None, 4) 12
_________________________________________________________________
Dense_2 (Dense) (None, 1) 4
_________________________________________________________________
Lambda_out (Lambda) (None, 1) 0
=================================================================
Total params: 16
Trainable params: 0
Non-trainable params: 16
_________________________________________________________________

Dabei setzen wir die initializer die Gewichte auf [[1,-1,1,-1],[1,-1,-1,1]] für die 8 Kernel-Gewichte in Dense_1 bzw. [1,1,-1,-1] in Dense_2. Die Bias-Werte von Dense_1 entsprechen dem x0 in der Formel und müssen mit dem Wert übereinstimmen, für den die 2. Ableitung von f berechnet wird.

Anm.: Das originale Tegmark-NN in [1] wendet die Skalierung nicht auf die Inputs und Outputs an, sondern steckt die Skalierung in die Gewichte des NN; d.h. statt auf [[1,-1,1,-1],[1,-1,-1,1]] werden die Dense_1 Gewichte z.B. auf [[1/500,-1/500,1/500,-1/500],[1/500,-1/500,-1/500,1/500]] gesetzt, wenn der Skalierungsfaktor 1/500 gewählt wird. Das ist aber mathematisch äquivalent. Bias-Werte tauchen in [1] nicht auf, da implizit x0=0 angenommen wird.

Um uns nicht wieder erst mit ungenauen Ergebnissen wie beim Quadrat-Modell oben befassen zu müssen, setzem wir die Default-Genauigkeit für keras/Tansorflow von vornherein auf doppelte Genauigkeit (float64).

Ergebnisse aus Berechnungen (Feed Forward)

Für eine “Versuchsreihe” werden zunächst zehn ganzzahlige Zahlenpaare a,b von -10 bis 10 per Zufall ausgewählt. Das Modell berechnet dafür dann für eine Serie von Skalierungsfaktoren 1/s in Zehnerpotenzen von 1/10 bis 1/10^7 die Produkte a*b. Analog werden Quadrate berechnet, d.h. a=b gesetzt.

Das beste Ergebnis der Serie wurde bei der Skalierung mit s=100000 erreicht:

Scaling: 1e-05
X_test:
a b a*b true predict err
4.00 7.00 28.00000000 28.00000007 0.000000
4.00 2.00 8.00000000 8.00000220 0.000002
6.00 7.00 42.00000000 42.00000164 0.000002
5.00 9.00 45.00000000 45.00000285 0.000003
7.00 6.00 42.00000000 42.00000164 0.000002
8.00 2.00 16.00000000 15.99999830 0.000002
3.00 6.00 18.00000000 18.00000114 0.000001
7.00 6.00 42.00000000 42.00000164 0.000002
9.00 2.00 18.00000000 17.99999808 0.000002
6.00 9.00 54.00000000 54.00000036 0.000000
max-err: 0.00000285 min-err: 0.00000007
X_sqr:
a a**2 predict err
-4.00 16.00 16.00000135 0.000001
5.00 25.00 24.99999887 0.000001
-5.00 25.00 24.99999887 0.000001
-7.00 49.00 48.99999937 0.000001
9.00 81.00 81.00000207 0.000002
7.00 49.00 48.99999937 0.000001
-10.00 100.00 100.00000157 0.000002
-4.00 16.00 16.00000135 0.000001
-8.00 64.00 63.99999930 0.000001
-4.00 16.00 16.00000135 0.000001
max-err: 0.00000207 min-err: 0.00000063

Wie bei der Quadratberechnung nach (F1) oben bereits gesehen, tritt auch hier der Trade-off zwischen Approximationsgenauigkeit und Rundungsfehler auf. Grössere und kleinere Skalierungsfaktoren verschlechtern das Ergebnis. Zur Orientierung sind jeweils der größte und kleinste absolute Fehler der zehn Berechnungen angegeben. Ein genauere Fehleranalyse behandeln wir in A1.3 unten.

Äquivalenz und Vergleich der Modellvarianten

Die folgenden drei mathematisch äquivalenten Modell-Varianten liefern in der Versuchsreihe bei gleicher Skalierung im Wesentlichen gleiche Werte. Abweichungen finden sich erst im “Fehlerbereich”, d.h. in den Stellen, die man ohnehin außer Acht lassen müsste.

Variante A: Das obige komplette NN-Modell, bei dem die Skalierung durch Lambda Layer realisiert wird. Hier sind die Gewichte unabhängig von den Input-Größen festgelegt.

Variante B: Das Modell, dass der Grafik in [1] entspricht, d.h. die Gewichte werden mit der Skalierung der Eingangswerte angepasst.

Variante C: Das Modell wie in A (mit festen Gewichten), aber ohne die Lambda Layer. Der Input wird vorher auf die richtige Größenordnung standardisiert (Down-Scaling) und der Output wird komplementär hoch-skaliert (Up-Scaling). Skalierung findet also außerhalb des NN statt.

Anm.: Die Quadrierung eine Zahl kann mit den Multiplikationsmodellen einfach dadurch berechnet werden, dass die zwei Faktoren im Input gleich gesetzt werden. Wir vergleichen das mit der Quadrierung im einfachen Modell oben.

Die die keras-Spezifikation der Variante B unterscheidet sich von A (s. oben) wie folgt:

# Define theoretical weights for initialization
# Including scaling
# *** Variant B - See graphic in [1] ***
x0 = 1.0
sc_in = 1/500 # Input scaling factor
sig2 = sigmoid2(x0) # 2nd derivative of sigmoid for x=1.0
sc_out = 1/(4*sig2*sc_in**2) # Output scaling factor including 2nd derivative

def dense1_init(shape,dtype=None):
w = sc_in
a = np.array([[w,-w,w,-w],[w,-w,-w,w]])
a = np.reshape(a,shape)
return a
def dense2_init(shape,dtype=None):
w = sc_out # sc_out includes f''(a) term
a = np.array([w,w,-w,-w])
a = np.reshape(a,shape)
return a

act = 'sigmoid'

inp = Input(shape=(2,),name='Input')

# Scaling down by weights for Dense_1
x = Dense(4,name='Dense_1',activation=act,
bias_initializer=constant(x0),
kernel_initializer=dense1_init,trainable=False)(inp)
out = Dense(1,name='Dense_2',activation='linear',use_bias=False,
kernel_initializer=dense2_init,trainable=False)(x)
# Output scaled up by weights for Dense_2

m = Model(inp,out)
m.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input (InputLayer) (None, 2) 0
_________________________________________________________________
Dense_1 (Dense) (None, 4) 12
_________________________________________________________________
Dense_2 (Dense) (None, 1) 4
=================================================================
Total params: 16
Trainable params: 0
Non-trainable params: 16

Die Äquivalenz der Modellvarianten A und B zeigt sich auch in den Berechnungsergebnissen, die sowohl bei float64 als auch float32 bei gleicher Skalierung gleiches Fehlerverhalten liefern. Wir verzichten hier auf die Wiedergabe der Ergebnissdaten.

Bei der Variante C erfolgt die Skalierung des Inputs und die Korrektur der Outputs mit sc_out außerhalb des Modells. Das Modell berechnet also das Produkt von zwei hinreichend kleinen Input-Werten u und v. Die keras-Spezifikation entspricht der Variante B mit w=1.0 in den Initialisierungs-funktionen, d.h. ohne Skalierungsfaktor.

Insgesamt zeigt das Tegmark-Modell bei — je nach Maschinengenauigkeit — optimaler Skalierung brauchbar bis sehr gute Werte für das Produkt zweier Zahlen.

Dabei müssen die Multiplikationsergebnisse jeweils in einer passenden Größenordnung liegen. Das Beispiel zeigt die Ergebnisse (Modell A) bei unterschiedlichen Größenordnungen der Faktoren und gleicher Skalierung mit 1/10000. (Siehe dazu auch Kritik, unten.)

A1.3 Fehleranalyse

In der Numerischen Mathematik gehören Fehleranalysen für numerische Verfahren zum Handwerkszeug [3]. Sie entscheiden letztlich, ob ein Algorithmus brauchbar ist oder nicht. Bei ML-Verfahren sind solche Betrachtungen eher unüblich, da es meist nicht um numerische Berechnungen geht.

[3] P.Deuflhard, A. Hohmann: Numerische Mathematik 1 (3. Auflage 2002, de Gruyter)

Wir wollen hier aber ein wenig in das Fehlerverhalten des Multiplikationsverfahrens von [1] eintauchen. Diesen Abschnitt kann getrost überspringen, wer sich für die mathematische Fehleranalyse nicht interessiert. Man sollte aber wenigstens einen Blick auf die Fehlerkurven werfen.

Die Fehlerentwicklung über die oben beschriebene Skalierungsserie zeigt die folgende log-log Grafik:

Abb. 5: Skalierungsabhänge Abweichung der NN-Berechnung

Die Grafik (rote, grüne Linien) zeigt die Minimal- bzw Maximal-Werte des (absoluten) Fehlers zwischen wahrem Multiplikationsergebnis a*b und dem Berechnungsergebnis des Modells; jeweils bei einfacher (gestrichelt) und doppelter Rechengenauigkeit. Die Fehler für die Multiplikationen der verwendeten Zahlenpaare a,b liegen jeweils zwischen den roten und grünen Werten. Man sieht deutlich, dass der Fehlerkorridor mit kleiner werdendem Skalierungsfaktor wieder ansteigt; ebenso, wenn der Skalierungsfaktor wieder größer wird.

Das entspricht eigentlich nicht der Erwartung, wenn man die Formel (F2) ansieht. Der Fehler müsste immer kleiner werden, wenn die u,v Ausdrücke kleiner werden, d.h. mit immer kleineren Skalierungsfaktor. Darauf basiert die Aussage in [1], dass das 5-Neuron-Netz beliebige Zahlen multiplizieren könne. Praktisch wird die Genauigkeit der Berechnung aber immer durch Rechenfehler gestört. Das kann man wie folgt verstehen:

Je kleiner u und v sind, desto genauer stimmen in (F2) die Werte mit postiven Vorzeichen mit denen überein, die ein negatives Vorzeichen haben. Die Summation dieser Werte im NN (oder in der Formel) führt dazu, dass ziemlich genau gleich große Werte subtrahiert werden (+/- Vorzeichen). Damit gehen im Ergebnis eine ganze Reihe von Stellen "verloren", d.h. werden zu Null. Dieser Effekt bei numerischen Berechnungen wird Auslöschung genannt und kann für die Gesamtberechung äußerst gefährlich werden: Das Ergebnis hat nur noch wenige "richtige" Stellen, gefolgt von Stellen, die aufgrund der beschränkten Zahldarstellung im Rechner (Maschinengenauigkeit) nichts mehr mit dem wirklichen Rechenergebnis zu tun haben. Tritt dieser Effekt an einer Stelle im Verfahren (Modell) auf, zieht er sich von da an durch alle weiteren Berechnungsschritte. Im Tegmark-Modell wird das verfälschte Ergebnis zudem mit dem Quadrat des Saklierungswerts - also einer sehr großen Zahl, wenn der Skalierungfaktor sehr klein war - wieder "hoch-skaliert".

Damit erklärt sich das Fehlerverhalten, das man in der Grafik sieht. Bei geringer Skalierung (rechts von der “Senke”) dominiert der Approximationsfehler, bei größerer Skalierung (links der Senke) dominiert der Effekt des Auslöschungfehlers.

Dieser Effekt ist prinzipiell auch nicht durch höhere Maschinen-Genauigkeit zu eliminieren. Die gestrichelte Linie zeigt die gleiche Fehlerdarstellung aus der Berechnungen mit einfacher Genauigkeit (float32, Standard für keras). Abhängig von der Maschinengenauigkeit gibt damit immer nur einen beschränkten "optimalen" Bereich an Skalierungsgrößen - optimal bzgl. der Gültigkeit der Ergebnisse des NN.

A1.4 Trainierbarkeit des Tegmark-Modells

Wenn das NN das Multiplizieren nach Tegmark “lernen” soll, muss es die Formel (F2) — genauer, die Koeffizienten — entwickeln. Das entspricht einem Training der Gewichte, ausgehend von einem zufälligen Anfangszustand, auf die theoretisch richtigen Werte hin.

Mit zufälligen Anfangswerten der Gewichte ergibt sich eine weitere Fehlerart zusätzlich zu den Fehlereffekten aus der Fehleranalyse (A1.3): das NN arbeitet mit einer “falschen Formel”, die nicht mehr der Taylor-Approximation entspricht.

Wir wollen sehen, ob das Multiplizieren in diesem Sinne gelernt werden kann. Dazu “befreien” wir das Modell, indem wir trainable = True setzen und die Gewichte durch "gestörte" Werte ersetzen, etwa durch normal-verteilte Abweichungen von den theoretischen Werten.

Anm.: Wir haben für das Feed-Forward Modell eine eher ungewöhnliche Abhängigkeit zwischen Gewichten. Der Bias-Wert x0 in der ersten Schicht muß das x0 in der Berechnung von f''(x0) in Dense_3 sein. Das lässt sich aber innerhalb eines NN-Modells nicht standardmäßig einrichten. Bliebe also zu hoffen, dass sich diese Beziehung im Zuge des Trainings "von selbst" einstellt. Andernfalls müsste man die üblichen Back-Propagation- und Korrektur-Verfahren um eine "Randbedingung" ergänzen und z.B. beim Gradient Descent explizit berücksichtigen. Im Modell wird der Bias-Wert von Dense_1 wie oben mit 1.0 initialisiert, ist aber als Parameter des Layers Dense_1 auch veränderbar.

Wir testen die Trainierbarkeit mit der Modellvariante A (komplettes NN-Modell mit Lambda-Layer).

# Kernel initializer allowing variation of start values
# As normal distribution or determined fixed size deviations

from numpy.random import normal,seed
seed(42) # Set fixed random seed

def dense1_init(shape,dtype=None):
a = np.array([[1,-1,1,-1],[1,-1,-1,1]])
a = a + normal(0,0.01,(2,4))
# Alternative:
#a = np.array([[1.01,-1.01,1.01,-1.01],[1.01,-1.01,-1.01,1.01]])
a = np.reshape(a,shape)
return a
def dense2_init(shape,dtype=None):
a = np.array([1,1,-1,-1])
a = a + normal(0,0.001,(4))
# Alternative:
#a = np.array([1.01,1.01,-1.01,-1.01])
a = np.reshape(a,shape)
return a

Trainierbarkeitstests — Zusammenfassung

Das Modell wurde in verschiedenen Varianten mit den Daten aus der Feed-Forward Berechnung versuchsweise trainiert. Wie üblich unter Verwendung von fit und predict. Die Hyperparamter sind dabei: adam als Optimizer, Batch size 1 bis 4, Epochen variiernd zwischen 20 und 300.

Die folgende Listen geben einen Eindruck von der erreichbaren Güte der Ergebnisse. (Trainings- und Testddaten Zufallswahl aus -10 bis 10. X_Test: Testdaten, X_sqr: Testdaten für Quadrat-Berechnung). Oben ist die Standardabweichung in der Störung der Gewichte 0.001, unten 0.01. Die Zeilen geben jeweils an: Die beiden Faktoren (Input), das wahre Produkt (numpy), das Ergebnis des trainierten Modells. (Bei X_sqr ist nur der eine Faktor angegeben.)

Modell-Variante A (NN-Modell mit Lambda Layer)
Gleitkomma-Genauigkeit float64
Input-Range: -10 bis 10

Skalierung 1/1000
Normalverteilte Störung: normal(0.0,0.001)
Epochen 150 (bestes Ergebnis)

X_test: 10
-10.00 4.00 -40.000000 -39.953881
-4.00 6.00 -24.000000 -24.012154
-9.00 -2.00 18.000000 18.037683
-2.00 2.00 -4.000000 -4.027686
-5.00 -5.00 25.000000 25.006847
9.00 9.00 81.000000 80.957058
-8.00 6.00 -48.000000 -47.974869
6.00 1.00 6.000000 5.949707
5.00 4.00 20.000000 19.948922
2.00 -10.00 -20.000000 -20.008371
--------------------------------------------
X_sqr: 10
6.00 36.00 35.949885
4.00 16.00 15.949982
-7.00 49.00 49.031622
6.00 36.00 35.949885
0.00 0.00 -0.036356
-8.00 64.00 64.045385
3.00 9.00 8.951666
3.00 9.00 8.951666
-7.00 49.00 49.031622
-5.00 25.00 25.006847

Normalverteilte Störung: normal(0.0,0.01)
Epochen 200 (bestes Ergebnis)

X_test: 10
1.00 6.00 6.000000 6.543644
-1.00 3.00 -3.000000 -2.697889
6.00 7.00 42.000000 42.184640
8.00 6.00 48.000000 47.846886
3.00 -2.00 -6.000000 -6.965157
-3.00 -9.00 27.000000 25.933364
2.00 -4.00 -8.000000 -9.127362
-2.00 0.00 0.000000 -0.025275
3.00 -7.00 -21.000000 -22.622185
2.00 -9.00 -18.000000 -19.743128
--------------------------------------------
X_sqr: 10
-6.00 36.00 35.794081
-5.00 25.00 24.748646
-1.00 1.00 0.680745
-2.00 4.00 3.680509
-9.00 81.00 80.997295
-6.00 36.00 35.794081
1.00 1.00 0.715936
-5.00 25.00 24.748646
-9.00 81.00 80.997295
2.00 4.00 3.750950

Zum Vergleich ein Trainings-/Testlauf mit einfacher Rechengenauigkeit.

Gleitkomma-Genauigkeit float32
Skalierung 1/100
Normalverteilte Störung: normal(0.0,0.001)
Epochen 90 (bestes Ergebnis)

X_test: 10
-5.00 -10.00 50.000000 49.575554
4.00 -10.00 -40.000000 -40.020599
6.00 -10.00 -60.000000 -59.898052
4.00 -9.00 -36.000000 -36.031986
3.00 -3.00 -9.000000 -9.051458
-3.00 8.00 -24.000000 -23.936588
-4.00 8.00 -32.000000 -31.930212
-5.00 -5.00 25.000000 24.753336
-8.00 -4.00 32.000000 31.697325
8.00 -3.00 -24.000000 -23.908709
--------------------------------------------
X_sqr: 10
-1.00 1.00 0.900390
4.00 16.00 16.062689
-10.00 100.00 99.200310
-4.00 16.00 15.800281
-5.00 25.00 24.753336
-6.00 36.00 35.694138
-7.00 49.00 48.619404
-1.00 1.00 0.900390
-1.00 1.00 0.900390
-5.00 25.00 24.753336

Die Trainingsreihe in doppelter Genauigkeit (float64, oben) erreicht beste Ergebnisse bei einer Skalierung mit 1/1000 für Input Zahlen zwischen -10 und 10. Allerdings darf die Abweichung der initialen Gewichtswerte gegenüber den theoretischen Zielwerten nur äußerst gering sein. Schon bei der Standardabweichung von 0.01 (zweiter Block) sieht das Trainingsergebnis deutlich schlechter aus als bei 0.001. Die feste Skalierung führt bei unterschiedlichen Größenordnungen der Ergebniswerte erwartungsgemäß zu unterschiedlicher Genauigkeit.

Zum Vergleich: bei einfacher Genauigkeit muß man für beste Ergebnisse die Skalierung “schwächer” wählen (1/100), um bei extrem geringer Störung in den Ausgangswerten der Gewichte (Standardabweichung 0.001) halbwegs brauchbares Trainingsergebnis zu erzielen (untere Liste).

Zu bemerken ist auch, dass die dargestellen “besten” Ergebnisse hier nur bei einer bestimmten Anzahl Epochen erreicht werden. Experimentell etwa bei 150 im float64-Fall und bei 90 im Fall float32. Vorher bleiben loss (mean squared error) und Ergebniswerte schlechter. Bei größerer Epochenanzahl zeigt der loss-Wert erratisches Rauschen, eine Zeichen, dass der Fehler nicht weiter verbessert werden kann.

Die Gewichte bewegen sich durch Training nur sehr wenig “in Richtung” der theoretisch richtigen Werte und lassen sich auch durch mehr Epochen nicht darüber hinaus bewegen. Ein Beispiel: Der durchschnittliche absolute Fehler gegenüber den theoretischen Werten ist

für die Startwerte:

Dense_1 Kernel: 0.07025 Bias: 0.0
Dense_2 Kernel: 0.04852

und für die trainierten Werte:

Dense_1 Kernel: 0.06124 Bias: 0.01643
Dense_2 Kernel: 0.03897

Trainingparameter: float32, s = 1/100, normal(0,0.1), ep=150. Die Ergebnisse für die Testdaten sind dabei wegen der relativ großen Standarabweichung der Störung unbrauchbar (max 1 Stelle). Wir untersuchen das näher in A3, unten.

Ergebnis: Der Modell-Ansatz nach [1] ist generell nur sehr beschränkt trainierbar. Das Ziel, die reale Multiplikation der Inputzahlen zu “erlernen”, wird nur unter starken Einschränkungen näherungsweise erreicht.

  • Mit einer Default-Initialisierung der keras-Modelle lässt sich überhaupt kein Konvergenzverhalten erzielen.
  • Ausgehend von den theoretischen Werten kann man durch minimale Störung der Gewichte ein beschränktes Konvergenzverhalten im loss-Parameter erzielen, wobei es darauf ankommt, welche Dense-Schicht man trainable sein lässt, bzw. ob beide trainierbar sind.
  • Die Störung kann durch kleine “Auslenkung” von den theoretischen Werten oder auch durch um die theoretischen Werte normalverteilte Zufallswerte realisiert werden. Je näher die gestörten Werte bei der theoretischen Verteilung liegen, etwa Auslenkung 0.001 bzw. Standardabweichung 0.001, desto schneller werden die Produkte der Inputzahlen durch eine kurze Konvergenzphase angenähert; die Näherung wird dabei nicht merklich besser. Die Testmethoden mit keras/Tensroflow variierten: Es wurden einzelne oder beide Layer trainierbar gesetzt, Abweichungen in den Startgewichten variierten von 0.1 - 0.0001.
  • Die realen Produkt-Werte von Trainings- und Testdaten werden dabei von sehr schlecht bis auf max. 2–4 Stellen Genauigkeit erreicht. Die Ergebnisse sind erwartungsgemäß deutlich schlechter als in der Feed-Forward Berechnung mit den theoretisch richtigen Gewichten.
  • Setzt man die theoretischen Werte als Startwerte für die Gewichte, erreicht man je nach Skalierung nach Training über wenige Epochen (20–30) in etwa die Werte der Feed-Forward Berechnung, wobei sich die Gewichte offenbar im Rahmen der Fehlerquellen (s. A1.3) verändern.
  • Die trainierten Gewichte verändern sich im Vergleich zu den Startweretn nur minimal und das offenbar nur in Abhängigkeit von der Stärke der Störung und der Skalierung.

Es ist allerdings nicht ausgeschlossen, dass mit anderen Methoden des Optimizers, variabler Learning Rate, Gradient Clipping, Loss Regularisierung und anderen, tiefer gehenden Optionen das Modell besser trainiert werden kann. Mit einigen dieser Varianten wurden die Testserien wiederholt: Optimizer sgd,adam mit verschiedenen Learning Rates, adadelta im default, Gradient Norm-Clipping, größere Training-Sets, nicht ganzzahlige Training-Sets. Die Lern-Ergebnisse waren meist schlechter, ansonsten ohne nennenswerte Verbesserung.

A1.5 Kritische Analyse — Multiplizieren Lernen bleibt ein Problem

1. Im Unterschied zum Universal Approximation Modell (Interpolation) in 6.4.1 basiert das Tegmark-Modell auf einer Taylorreihen-Approximation für u*v (aus der mathematischen Analysis). D.h. es basiert auf Berechnung einer Approximationsformel im Gegensatz zu einem Trainingset von Funktionspunkten.

  • Die Taylor-Approximation ist theoretische beliebig genau, wenn u,v hinreichend klein sind.
  • In der Praxis realer Modelle kann man diese Werte nicht über eine Grenze hinaus klein machen, wie wir gesehen haben. Hohe Genauigkeit für die Taylor-Approximation wird durch die beschränkte Gleitkomma-Rechengenauigkeit konterkariert. Die Rechenfehler überlagern die Approximationsgenauigkeit.(Rundungsfehler, Auslöschung, s. Details unten.)

2. Das 5-Neuronen-Modell mit den festen Gewichten 1.0 bzw -1.0 ist de facto eine reine Formelauswertung für die linke Seite von Gleichung (F2). Es ist damit zunächst nur eine grafische Darstellung (als Netzwerk) des Berechnungsvorgangs. Was sollte also Besonderes daran sein, das als Neuronales Netz zu verstehen?

  • Mit dem “Mehrwert” eines NN-Modells verbindet man implizit die Trainierbarkeit für eine bestimmte Aufgabe oder Aufgabenklasse.
  • D.h. die exakte Taylor-Approximation (Formel) sollte durch Training (Anpassung des Gewichte) so gut wie möglich approximierbar sein.
  • Aktivierungsfunktionen spielen in Neuronalen Netzen in der Regel eine bestimmte Rolle, z.B. vorwärts-propagierte Signale in bestimmter Weise für die nächste Neuronenschicht zu “konzentrieren”. Diese Rolle ist hier gegenstandslos; f(x) kann jede beliebige Funktion mit stetiger 2. Ableitung sein, sofern diese nicht identisch Null ist. Wir kommen darauf in A2 zurück.

3. Die Trainierbarkeit des 5-Neuronen-Modells für die Aufgabe, die Taylor-Approximation für u*v zu realisieren, ist offenbar stark eingeschränkt.

  • Hier muss man unterscheiden zwischen der Genauigkeit, die Ziel-Gewichte anzunähern, und der Genauigkeit der Approximation des Produkts durch die Taylor-Formel durch möglichst kleine u,v.
  • Eine halbwegs brauchbare Annäherung an die Ziel-Gewichte ist nur bei ganz geringen Abweichungen der Startwerte möglich. Ein “Trainingseffekt” ist dann zwar zu verzeichnen (Konvergenz der loss-Funktion), aber die Zielwerte und das Zielergebnis (wahres Produkt) werden kaum besser als auf 2 bis 3 Stellen Genauigkeit erreicht.

4. Das Modell setzt — auch für die Feed-Forward-Berechnung — implizit die Multiplikation bereits voraus, also das, was es eigentlich lernen soll. Entweder als Komponente des Modells oder per externem Pre- und Postprocessing (Skalierung) der Daten.

  • Das erinnert an die “Fake-Lösung” für das Gerade/Ungerade Lernen (in Teil 1).
  • Wird die Skalierung auf hinreichend kleine Input-Werte (und die Hochskalierung der Ergebnisse) in die Gewichte verlagert wie im Original-Text [2], hätten wir “Input-spezifische Gewichte”. Es ist zumindest unüblich bei Neuronalen Netzen, dass die — trainierten oder vorgegebenen Gewichte — anhängig vom jeweiligen Einzel-Input geändert werden.
  • Denkbar wäre, einen festen Skalierungsfaktor “über alles” zu verwenden, der für “zwei beliebige Zahlen” als Input diese hinreichend klein skaliert. Das würde aber bedeuten, dass einerseits einige Zahlen so klein skaliert werden, dass sie wegen der Rundungfehlerproblematik kein brauchbares Ergebnis liefern, und andererseits andere Input-Zahlen noch zu groß sind, um auch nach Skalierung einen hinreichend kleinen Approximationfehler in der Taylor-Formel zu generieren.
  • Erfolgt dagegen die Skalierung als individuelles Pre-/Postprocessing der Faktoren, so dass passend kleine Werte als Input in das Modell eingehen, findet die Multiplikation der echten Faktoren zum Teil bereits außerhalb des Modells statt. Im NN werden dabei dann nur zwei Zahlen aus einem beschränkten Bereich kleiner Zahlen miteinander multipliziert.
  • Auch hier kann man den paradoxen Fall wie in 6.4.1 konstruieren, dass man als (faktorenweise) Skalierung r=d/a und s=d/b wählt, mit z.B. d=0.001. Das ergibt dann als Modell-Input stets u=0.001, v=0.001. Das Modell müsste daher lediglich eine Multiplikationsaufgabe lösen, nämlich d*d. Anschließend würde das Ergebnis mit 1/(r*s) "hochskaliert" werden, d.h. mit a*b multipliziert werden.

A2 Ein nahezu perfektes Neuronales Netz für die Multiplikation

In [2] wird die Taylorentwicklung (F2) für eine allgemeine (zweimal stetig differenzierbare) Funktion f(x) zur Grundlage für das 5-Neuronen-Modell in [1].

(F2) f(x0+u+v)+f(x0-u-v)-f(x0+u-v)-f(x0-u+v) = u*v*(4*f''(x0) + R(u^2+v^2))

In den Berechnungen in A1.2 haben wir für f die für NN-Modelle typische Aktivierungsfunktion sigmoid gewählt - allerdings ohne besonderen Grund oder Effekt. Wir hätten auch jede andere einsetzen können, die an einer Stelle x0 die zweite Ableitung ungleich Null hat.

Wir können damit f sogar so wählen, dass der Rest-Fehler Null wird: ein Polynom 2. Grades. Mit f(x)=1/8*x**2 und x0=0 wird so aus (F2) die Identität

(F3) f(u+v)+f(-u-v)-f(u-v)-f(-u+v) = u*v,

da 4*f''(0)=1 und alle höheren Ableitungen Null sind. Oder — unter Verwendung der binomischen Formeln

f(u+v)+f(-u-v)-f(u-v)-f(-u+v) = 2/8*(u+v)**2 - 2/8*(u-v)**2 = u*v

Mit (F3) haben wir eine Formel für die Multiplikation, die

  • keine Skalierung benötigt, da der Restterm immer schon Null ist
  • daher beliebige Zahlen multipliziert
  • keine Modifikation der Gewichte benötigt und
  • als Fehlerquelle nur die Maschinengenauigkeit hat.

Die Formel (F3) als NN und Beispielrechnungen

Das NN entspricht der Grafik in Abb. 4 mit a=0 und g=1, ist damit ein "spezielles" Tegmark-Modell.

Die keras-Spezifikation sieht wie folgt aus:

# Function used as "activation" in NN
def sig_sqr(x):
return x**2/8.0
# 2nd derivative of sig_sqr
def sig_sqr2(x):
return 1/4.0
# Use single precision
from keras import backend as K
prc = 'float32'
#prc = 'float64'
K.set_floatx(prc)
# Define theoretical weights for initialization
def dense1_init(shape,dtype=None):
a = np.array([[1,-1,1,-1],[1,-1,-1,1]])
a = np.reshape(a,shape)
return a
def dense2_init(shape,dtype=None):
a = np.array([1,1,-1,-1])
a = np.reshape(a,shape)
return a

act = sig_sqr
x0 = 0.0
# 2nd deriv of sig_sqr is 1/4 - hence 4*f"(0.0) is 1.0
# No need for division by this term

inp = Input(shape=(2,),name='Input') # Input for a,b
x = Dense(4,name='Dense_1',activation=act,
bias_initializer=constant(x0),
kernel_initializer=dense1_init,trainable=False)(inp)
out = Dense(1,name='Dense_2',activation='linear',use_bias=False,
kernel_initializer=dense2_init,trainable=False)(x)

m = Model(inp,out)
m.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input (InputLayer) (None, 2) 0
_________________________________________________________________
Dense_1 (Dense) (None, 4) 12
_________________________________________________________________
Dense_2 (Dense) (None, 1) 4
=================================================================
Total params: 16
Trainable params: 0
Non-trainable params: 16

Die 16 Parameter (Gewichte) werden wie in A1.2 inititalisiert. Die Berechnung von Produkten erfolgt für zufällige Inputzahlen zwischen -100 und 1000 bzw. 10000 für Quadrate-Berechnung

y_pred = m.predict(X_test)[:,0]
sqr_pred = m.predict(X_sqr)[:,0]

Ergebnis-Beipiel (bei float32):

Scaling: n/a
X_test: x*y true pred err
877.00 47.00 41219.0000 41219.0000 0.000000
341.00 50.00 17050.0000 17050.0000 0.000000
619.00 452.00 279788.0000 279788.0000 0.000000
370.00 362.00 133940.0000 133940.0000 0.000000
-61.00 189.00 -11529.0000 -11529.0000 0.000000
872.00 919.00 801368.0000 801368.0000 0.000000
368.00 904.00 332672.0000 332672.0000 0.000000
-4.00 177.00 -708.0000 -708.0000 0.000000
923.00 444.00 409812.0000 409812.0000 0.000000
531.00 742.00 394002.0000 394002.0000 0.000000
max-err: 0.00000000 min-err: 0.00000000
--------------------------------------------
X_sqr: x^2 true pred err
4675.00 21855625.00 21855624.0000 1.000000
7445.00 55428025.00 55428024.0000 1.000000
636.00 404496.00 404496.0000 0.000000
6149.00 37810201.00 37810200.0000 1.000000
8181.00 66928761.00 66928760.0000 1.000000
2098.00 4401604.00 4401604.0000 0.000000
142.00 20164.00 20164.0000 0.000000
795.00 632025.00 632025.0000 0.000000
4961.00 24611521.00 24611520.0000 1.000000
9674.00 93586276.00 93586272.0000 4.000000
max-err: 4.00000000 min-err: 0.00000000

Das Modell liefert exakte Produkte und Quadrate beliebiger Zahlen ohne Skalierung, solange die Maschinengenauigkeit nicht überschritten wird. Das ist bei der Liste der Quadrate mit einfacher Genauigkeit der Fall, weshalb sich hier (einstellige) Fehler in der 8. Stelle ergeben. Es ist damit deutlich besser als das NN im vorigen Abschnitt.

Trainierbarkeit

Auch hier ist wieder zu prüfen, ob und wie gut das Modell Multiplizieren lernt.

Wir trainieren das Modell mit den gleichen Bedingungen wie in A1.4, allerdings kommen wir hier ohne Skalierung aus, da (F3) keinen Approximationsfehler hat.

Es wurden wieder Trainingsläufe unter verschiedenen Bedingungen und Hyperparameter-Werten durchgeführt. Unter anderem

  • Genauigkeit: float32, float64
  • Trainingsset: n=500 mit Zufallszahlen ganzzahlig sowie Gleitkommezahlen
  • Training-Range: Zahlen zwischen 0 - 10, 0 - 100
  • Testsets: Multiplikation (-10) - 10, (-10) - 100, Quadrate dto.
  • Initialisierung der Gewichte: (a) Theoretische Werte, gestört durch normalverteilte Abweichungen mit Standardabweichung 0.1, 1.0 - also sehr groß im Vergleich zu den Versuchen in A2. (b) Keine Vorgaben, d.h. Zufallswerte per Default. Jeweils mit use_bias = False.
  • Optimizer: adam, sgd, adadelta
  • Konvergenz: loss = mse
  • Batch size: 1, 4
  • Epochs: Abhängig vom Konvergenzverhalten 18 - 200

Wie bei anderen Aufgabenstellungen und Modellen zuvor schauen wir auch auf die Entwicklung der Gewichte. Die Konvergenz der Loss-Funktion sagt nichts über die Entwicklung der Gewichte bzgl. bekannter theoretischer Werte aus.

Die Vielzahl der Varianten zeigt den experimentellen Charakter der Trainingsläufe. A priori war nicht einzuschätzen, welche der Einstellungen gute oder schlechte Trainingsergebnisse liefert. Einige plausible Erkenntnisse sind weiter unten aufgeführt.

Zunächst einige “beste” Ergebnis-Beispiele: Trainings-Setting und Ergebnisse für Test-Sets. (Die Zeilen zeigen die Input-Zahlen, das “wahre” Ergebnis (y_true) und das Ergenbis des trainierten NN (y_pred)). nmax=10, n=500 bedeutet hier 500 Traningsdaten(-Paare) mit zufälligen Gleitkommazahlen zwischen 0 und nmax=10.

Training Settings: 
float32, nmax 10, n=500, init norm, std-dev 0.1,
opt=adam, ep=200, bs=4, test -10,100

X_test:
63.00 28.00 1764.0000 1764.0608
-2.00 -3.00 6.0000 6.0000
0.00 47.00 0.0000 -0.0263
43.00 44.00 1892.0000 1892.0040
66.00 26.00 1716.0000 1716.0698
17.00 -7.00 -119.0000 -118.9985
15.00 -4.00 -60.0000 -59.9988
55.00 31.00 1705.0000 1705.0398
43.00 51.00 2193.0000 2192.9946
-6.00 32.00 -192.0000 -192.0087
---------------------------------------------------
X_sqr:
36.00 1296.00 1296.0031
86.00 7396.00 7396.0298
96.00 9216.00 9216.0381
51.00 2601.00 2601.0081
71.00 5041.00 5041.0186
80.00 6400.00 6400.0249
97.00 9409.00 9409.0410
28.00 784.00 784.0011
49.00 2401.00 2401.0076
20.00 400.00 399.9999
>>> Loss starts fluctuating from epoch 50Alternative Training Settings:float64 nmax 10, n=500, init norm, std-dev 0.1,
opt=adam, ep=200, bs=4, test -10,100:
>>> Same quality of results
float32 nmax 10, n=500, init default,
opt=adam, ep=200, bs=4, test -10,100
X_test:
90.00 46.00 4140.0000 4140.8911
-7.00 77.00 -539.0000 -539.2595
-3.00 59.00 -177.0000 -177.1428
84.00 6.00 504.0000 504.5891
34.00 -4.00 -136.0000 -135.9191
12.00 11.00 132.0000 132.0184
64.00 9.00 576.0000 576.3649
23.00 56.00 1288.0000 1288.0330
93.00 56.00 5208.0000 5208.9829
31.00 86.00 2666.0000 2666.0239
---------------------------------------------------
X_sqr:
91.00 8281.00 8282.0176
25.00 625.00 625.0786
6.00 36.00 36.0046
44.00 1936.00 1936.2402
38.00 1444.00 1444.1797
4.00 16.00 16.0019
56.00 3136.00 3136.3872
58.00 3364.00 3364.4155
89.00 7921.00 7921.9731
0.00 0.00 -0.0005
>>> Smooth loss development
>>> Same quality of results for float64

float32 nmax 100, n=500, init default,
opt=adam, ep=200, bs=3 test -10,100
X_test: 10
88.00 60.00 5280.0000 5279.9961
93.00 47.00 4371.0000 4370.9922
16.00 39.00 624.0000 623.9702
3.00 18.00 54.0000 53.8134
11.00 -9.00 -99.0000 -99.3247
9.00 42.00 378.0000 377.9389
34.00 4.00 136.0000 135.8927
97.00 38.00 3686.0000 3685.9731
97.00 51.00 4947.0000 4946.9692
62.00 62.00 3844.0000 3844.0613
---------------------------------------------------
X_sqr: 10
65.00 4225.00 4225.0503
99.00 9801.00 9800.7412
17.00 289.00 288.8943
88.00 7744.00 7743.8784
56.00 3136.00 3136.0745
-3.00 9.00 8.6238
74.00 5476.00 5476.0024
41.00 1681.00 1681.0596
71.00 5041.00 5041.0210
73.00 5329.00 5329.0083

Aus den Ergebnissen der Trainingsexperimente lassen sich eine Reihen von Schlüssen ziehen (ohne Beweis!):

  • Die Trainingsergebnisse bei doppelt-genauer Rechnung unterscheiden sich kaum von denen bei float32. D.h. größere Rechengenauigkeit führt nicht zu besseren Trainingsergebnissen. Da wir bei diesem Modell auch keinen Einfluß eines Approximationsfehlers wie bei A2 haben, bedeutet das, dass die NN-Verfahren in der Nähe der theoretischen Werte in lokale Minima oder in quasi-indifferente Bereiche laufen. Das zeigt auch die Loss-Entwicklung (s. Abb. 6), die nach anfänglicher Konvergenz in Fluktuationen übergeht. (Beispiel aus dem ersten Test oben.)
Abb. 6: Loss-Werte ab ca. Epoche 110 (0 = epoch 50)
  • Wie schon in A1 zeigen die Gewichte anfänglich eine Entwicklung in Richtung der theoretisch richtigen Werte, die sie aber nicht beliebig gut annähern — selbst wenn die Lossfunktion sher kleine Werte annimmt (z.B. bis 10**(-8)).
  • Der adelta Optimizer liefert final eher schlechtere Trainingsergebnisse als adam. sgd mit verschiedenen Learning Rates liefert generell schlechtere Ergebnisse.
  • Der trainierte Zustand hängt stark von der Anzahl der Iterationen (epochs) ab. Mehr Epochen führen meist nicht zu besseren Ergebnissen. Auch wenn die Beispiele jeweil für 200 Epochen engegeben sind, kann die Loss-Funktion u.U. schon ab 50 fluktuieren.
  • Batch size 4 scheint generell besser als bs=1.
  • Bemerkenswert ist der Unterschied bzgl. der “Störung” der Startwerte. In A1 bekommt man über eine Standardabweichung von 0.001 hinaus keine guten Trainingsergebnisse mehr. Hier dagegen lassen sich mit Standardabweichung 1.0 oder Default-Zufallszahlen ohne weiteres gute bis sehr gute Ergebnisse erzielen. Ursache könnte hier sein, dass es keinen Approximationfehler in der Formel (F3) gibt.

A3 Ein 4-Neuronen-Modell für die Multiplikation abgeleitet aus den Binomischen Formeln

Das folgende NN liefert die besten Ergebnisse bei Berechnung und Training für die Multiplikation. Die Herleitung ist eine Vereinfachung der Formel (F3) in A2. Mit f(x)=x^2 bekommen wir mit der 1. Binomischen Formel

(F4) f(u+v) = f(u) + f(v) + 2*u*v

oder

(F4a) 2*u*v = (u+v)^2 - u^2 - v^2

Mit f als "Aktivierungsfunktion" nach dem ersten Layer ergibt sich daraus ein NN, das grafisch wie folgt aussient:

Abb. 7: Das 4-Neuronen Netz abgeleitet aus der 1. Binomischen Formel

Das keras-Modell dazu ist:

def sig_sqr(x):
return x**2
# Define theoretical weights for initializationdef dense1_init(shape,dtype=None):
a = np.array([[1,1,0],[0,1,1]])
a = np.reshape(a,shape)
return a
def dense2_init(shape,dtype=None):
a = np.array([-1,1,-1])*0.5
a = np.reshape(a,shape)
return a
act = sig_sqr inp = Input(shape=(2,),name='Input') # Input for a,b
x = Dense(3,name='Dense_1',activation=act,use_bias=False,
kernel_initializer=dense1_init,trainable=False)(inp)
out = Dense(1,name='Dense_2',activation='linear',use_bias=False,
kernel_initializer=dense2_init,trainable=False)(x)
m = Model(inp,out)
m.summary()

opt = 'adam'
m.compile(optimizer=opt,loss='mean_squared_error',
metrics=['accuracy'])
Layer (type) Output Shape Param #
=================================================================
Input (InputLayer) (None, 2) 0
_________________________________________________________________
Dense_1 (Dense) (None, 3) 6
_________________________________________________________________
Dense_2 (Dense) (None, 1) 3
=================================================================
Total params: 9
Trainable params: 0
Non-trainable params: 9
_________________________________________________________________

Die Imports und das Precision Setting, hier float32, sowie die Generierung von Trainings- und Test-Daten sind wie in A2.

Berechnung

Das Modell liefert perfekte Berechnungsergebnisse. Zwei Beispiele:

float32  X_test randint -10,10 
X_test:
-7.00 7.00 -49.0000 -49.0000
0.00 -7.00 0.0000 0.0000
-10.00 -3.00 30.0000 30.0000
-5.00 -2.00 10.0000 10.0000
5.00 -2.00 -10.0000 -10.0000
6.00 7.00 42.0000 42.0000
9.00 -1.00 -9.0000 -9.0000
9.00 -2.00 -18.0000 -18.0000
0.00 3.00 0.0000 0.0000
7.00 -7.00 -49.0000 -49.0000

float32 X_test randint -100,1000
X_test: 10
308.00 900.00 277200.0000 277200.0000
940.00 200.00 188000.0000 188000.0000
935.00 967.00 904145.0000 904145.0000
96.00 686.00 65856.0000 65856.0000
944.00 611.00 576784.0000 576784.0000
925.00 25.00 23125.0000 23125.0000
232.00 508.00 117856.0000 117856.0000
874.00 313.00 273562.0000 273562.0000
141.00 547.00 77127.0000 77127.0000
983.00 577.00 567191.0000 567191.0000

Dies wird verständlich aufgrund der Abwesenheit von Fehlerquellen (s. Analyse in A1, oben): Wir haben keinen Approximationfehler, (F3) und F(4) sind Identitäten. Da wir nicht skalieren müssen, sind die rechten Seiten numerisch stabil, d.h. die Subtraktionen führen nicht zu Auslöschung, da die Summanden nicht nahezu gleich sind.

Trainierbarkeit

Zunächst einige Beispiele aus Trainingsläufen.

Mit einmal erzeugten Trainings- und Testdaten und gleichen Trainingsparametern unterscheiden sich wiederholte Läufe dadurch, dass die Start-Gewichte als normalverteilte Zufallszahlen unterscheiden. Das sollte im Idealfall eines konvergenten Verfahrens keine Rolle spielen.

Als Paramter bei wiederholten Läufen setzen wir z.B. nmax=10 (Trainingsset Gleitkommezahlen zwischen 0 zund 10), size=500, optimizer=adam, batch size=4, Anzahl Epochen ep=200, Standardabweichung der normalverteilten Störungen 1.0, Testsetzahlen zwischen -10 und 100. Angegeben sind die “Predictions” für 10 Test-Inputs (ganzzahlig).

# 1. Lauf     -9.00      -7.00        63.0000        63.0000
-9.00 6.00 -54.0000 -54.0000
-1.00 8.00 -8.0000 -8.0000
-7.00 -1.00 7.0000 7.0000
1.00 -7.00 -7.0000 -7.0000
1.00 4.00 4.0000 4.0000
-1.00 -7.00 7.0000 7.0000
-7.00 -5.00 35.0000 35.0000
-10.00 -7.00 70.0000 70.0000
-8.00 3.00 -24.0000 -24.0000

# 2. Lauf: gleiches Ergebnis
# 3. Lauf -9.00 -7.00 63.0000 63.0001
-9.00 6.00 -54.0000 -53.9995
-1.00 8.00 -8.0000 -7.9997
-7.00 -1.00 7.0000 7.0001
1.00 -7.00 -7.0000 -6.9998
1.00 4.00 4.0000 4.0000
-1.00 -7.00 7.0000 7.0002
-7.00 -5.00 35.0000 35.0001
-10.00 -7.00 70.0000 70.0002
-8.00 3.00 -24.0000 -23.9997

Die Testwerte ergeben hier perfekte oder nahezu perfekte Ergebnisse nach Training über 200 Epochen. Und obwohl wir nur mit Zahlen zwischen 0 und 10 trainiert haben, sind die Testmultiplikationen im Bereich -10 bis 100 gut. Ein Zeichen, dass wir es hier nicht mit einem Interpolations-Training zu tun haben.

Die Ergebnisse hängen aber tatsächlich nahezu chaotisch von den Startwerten für die Gewichte und der Anzahl der Trainingsschritte (Epochen) ab. Das Training mit gleichem Trainingset und gleichen Parametern über 250 Epochen endet bei gerade mal 1 Stelle Genauigkeit bei den Test-Multiplikationen, bei 150 (und natürlich anderen Startgewichten) nahezu perfekt, bei 160 Epochen wiederum nur bei 3–4 richtigen Stellen.

Ausschlaggebend für die Güte der Tests sind die Gewichte des NN am Ende des Trainings. Die zeigen, wie schon oben erwähnt, auch hier keine Konvergenz gegen die theoretischen Werte. In den Beispielen des 1. und des 2. Laufs, die gleich gute Testergebnisse liefern, sehen die trainierten Gewichte völlig unterschiedlich aus:

1. Lauf:
Dense_1[[ 0.15184368, 0.13011439, -0.02215194],
[-0.88960737, 1.3022488 , 1.4670174 ]]
Dense_2[[-1.2727597 ],
[ 1.7599896 ],
[-0.91881484]]
2. Lauf
Dense_1[[ 1.478678 , -0.67404723, -0.8627163 ],
[-0.20385504, 2.7070034 , 1.2825133 ]]
Dense_2[[ 0.2405687 ],
[ 0.18224077],
[-0.8179722 ]]

Das Gleiche zeigt sich für alle anderen Trainingsläufe, unabhängig von der Güte des Trainings.

Die unterschiedliche Entwicklung (Konvergenz) der Gewichte über die ersten 50 Epochen zeigen die Grafiken von zwei Läufen mit den gleichen Paramtern wie oben. (Legende: Blau — das Gewichte-Set von u ausgehend, Grün - das von v ausgehende Set, Rot - Die Gewichte der Ausgabe-Schicht. Die Linienart deutet die jeweiligen Einzelgewichte an, in der Grafik von oben nach unten: ----, ....., -.-.-.-..)

Abb. 8a: Training 1 — Konvergenz Gewichte
Abb. 8b: Training 2 — Konvergenz Gewichte

Dafür gibt es eine — zugegeben etwas versteckte — Erklärung, die man bei diesem NN relative einfach überprüfen kann. Diese Erklärung gilt auch für die vorigen NN-Modelle, ist dort aber etwas schwieriger nachzuweisen.

Dass die Gewichte eines erfolgreich trainierten NN andere Werte aufweisen als die theoretische erwarteten, haben wir schon bei anderen Modellen gesehen.

Fasst man die Gewichte eines “erfolgreichen” Trainingslaufs als Lösung der Aufgabe auf, beliebige Inputzahlen korrekt zu multiplizieren, dann gibt es viele Lösungen, nicht nur die im initializer, die wir auch theoretische Gewichte nennen. Mehr noch, die Lösungen liegen dicht im Raum der Gewichtskombinationen, der hier 9-dimensional ist. Das bedeutet, dass in "unmittelbarer Nähe" einer Lösung weitere Lösungen zu finden sind. Bildlich: in der Umgebung von z.B. der theoretischen Lösung liegen beliebig nahe beliebig viele weitere Lösungen. Details dazu s. Anmerkung am Ende dieses Abschnitts.

Das erklärt auch, warum bei unseren Modellen das Training zu immer anderen Lösungen führt und die Loss-Funktion mit zunehmender Epochenzahl nicht mehr besser wird, sondern fluktuiert. In der Nähe einer Lösung führen weitere Trainingsschritte zunächst von der Lösung weg und ggf. wieder auf eine andere in der Nähe hin.

Nun ja, das kann uns eigentlich egal sein, solange das NN mit irgendeinem Gewichte-Set das Multiplizieren richtig hinkriegt.

Anm.: Wie kann man das nachprüfen? Prinzipiell einfach: die Gewichte einer Lösung, etwa die von Lauf 1 und 2 oben, erfüllen drei nichtlinieare Gleichungen, die man ableiten kann, wenn man das NN als Formel (wie (F4)) mit den Gewichtsvariablen schreibt. Man kann dann die Formel so umformen, dass sie wie in (F4a) nach (u*v)^2, u^2, v^2 "sortiert" ist. Man erkennt dann, dass eine Gewichtekombination immer eine Lösung ist, wenn die Ausdrücke vor u^2 und v^2 Null ergeben und der vor (u*v) 1 ergibt. Damit haben wir die drei Gleichungen, in denen alle Gewichte vorkommen. Im Prinzip könnte man durch Lösen der Gleichungen alle Gewichte-Kombinationen errechnen, die eine Lösung der Multiplikationsaufgabe ergeben. Das ist uns hier dann doch zuviel der Mathematik.

Was wir aber tun können, ist zu prüfen, ob die trainierten Gewichte eines erfolgreichen Trainings die Gleichungen erfüllen! Dabei ergibt sich zum einen, dass natürlich die theoretischen Gewichte die Gleichungen erfüllen. Kein Wunder, so haben wir das NN ja konstruiert. Und tatsächlich erfüllen auch die trainierten Gewichte z.B. der beiden Läufe oben die Gleichungen ebenfalls!

Falls man das nachprüfen möchte, hier ein Code-Snippet, dem auch die Formeln der Gleichungen und die Gewichte zu entnehmen sind.

# Get trained weights and check paramter equations

w = m.get_weights()[0]
v = m.get_weights()[1]
a,c,e = w[0]
b,d,g = w[1]
r,s,t = v[:,0]
print(a,c,e)
print(b,d,g)
print(r,s,t)

# Eq 1: Coeff of u**2: should be 0
eq1 = r*a**2+s*c**2+t*e**2

# Eq 2: Coeff of v**2: should be 0
eq2 = r*b**2+s*d**2+t*g**2

# Eq 3: Coeff of u*v: should be 1
eq3 = 2*(r*a*b+s*c*d+t*e*g)

print('eq1:',eq1)
print('eq2:',eq2)
print('eq3:',eq3)

Die Gewichte nach den Trainingsläufen 1 und 2, oben, ergeben damit folgende Werte für eq1, eq2, eq3:

# Lauf 1:
# Gewichte
0.15184368 0.13011439 -0.02215194
-0.88960737 1.3022488 1.4670174
-1.2727597 1.7599896 -0.91881484

eq1: -6.641760698697765e-08
eq2: -9.150373081467933e-08
eq3: 0.9999998092913226

# Lauf 2:
# Gewichte
1.478678 -0.67404723 -0.8627163
-0.20385504 2.7070034 1.2825133
0.2405687 0.18224077 -0.8179722

eq1: 6.779593852979104e-08
eq2: -2.0896636732103957e-07
eq3: 1.0000000318134639

Beide Trainingsläufe führen zu perfekt trainierten Multiplikations-NN. Jedoch mit völlig unterschiedlichen Gewichte-Sets, die aber die Lösungsbedingungen eq1, eq2, eq3 gleichermaßen erfüllen.

Ein schönes Beispiel für die (numerische) Instabilität der “Lösungslandschaft”, d.h. der Gewichte-Sets, die die kritischen Gleichungen eq1 = 0, eq2 = 0, eq3 = 1 erfüllen, zeigt das Ergebnis des folgenden Laufs. Die Trainingsparameter und Ausgangsbedingungen sind die gleichen wie oben. Die Trainingsdaten sind natürlich andere Zufallszahlen, ebenso wie die initialen Gewichte (hier normalverteilt mit Standardabweichung 1.0 um die theoretischen Werte.) Das Training geht über 150 Epochen. Die Loss-Enwtwicklung unten ist zwischen Epoche 60 und 90 dargestellt, die Gewichte über den ganzen Trainingslauf. Oben stehen die trainierten Gewichte nach 150 Epochen. Die kritischen Gleichungen werden erfüllt ebenso wie die Testvorgaben (jeweils im Rahmen der Genauigkeitsgrenzen).

Abb. 9: Trainierte Gewichte, Verlauf der Gewichte. Loss von 60 bis 90 (0+60)

Bei etwa Epoche 70 springt hier die anfängliche Konvergenz um auf eine andere Lösung. Dabei wird auch die Lossfunktion schlagartig kleiner. In anderen Fällen springt sie auch erst auf höhere Werte, um dann wieder zu konvergieren.

Anm.: Die Modelle haben nicht nur das sonst bei NN übliche Problem, dass es neben einem globalen Optimum weitere, lokale, nicht so gute Optima gibt. Hier liegen die Gewichte-Sets, die eine Lösung für das Multiplikationsproblem liefern sogar “dicht an dicht”. Das kann man wie folgt nachprüfen.

Die Gleichungen eq1 - eq3 bilden ein System von 3 (nicht-linearen) Gleichungen für 9 "Unbekannte". Durch Umformen kann man die Gleichungen u.a. in eine Form bringen, die aus zwei entkoppelten Teilen besteht.

Mit den Abkürzungen

A = s*c**2+t*e**2
B = s*d**2+t*g**2
C = s*c*d+t*e*g - 1/2

sind eq1 - eq3 immer erfüllbar, wenn

A*B = C**2 ist und - unabhängig davon
b = a*B/C und
r = -C/(a*b)
wobei a frei wählbar ist (außer Null).

Wenn wir ein Set von Gewichten haben wie oben, das die eq1- eq3 erfüllen, dann ist auch A*B = C**2 erfüllt und man kann z.B. das a frei variieren, b und r damit berechnen, und bekommt wieder eine Lösung. Das zeigt die Beispielrechnung ausgehend von den Gewichten oben aus Lauf 1.

# Proof that solutions are dense:
# Given that A,B,C relate as A*B=C^2
# Then set a to any value, calculate b an r
# The eq's 1-3 will be satisfied
# Hence the new set of weights provide a solution
A = s*c**2+t*e**2
B = s*d**2+t*g**2
C = s*c*d+t*e*g - 1/2
print(A,B,C)
print(A*B-C**2) # A*B = C^2
# Gestaffelte Gleichung:
# A*B-C^2 = 0
# a beliebig
# b = a*B/C
# r = -C/(ab)
delta = 0.01 # Variation for a
a = a + delta
b = a*B/C
r = -C/(a*b)
# Calculate eq1, eq2, eq3 ...0.029345321622199317 1.0072635549917628 -0.17192607525499404
-1.0237303274540643e-07
eq1: -1.0163480276462579e-07
eq2: 0.0
eq3: 1.0

A4 Lassen wir die 3 NN-Modellansätze für die Multiplikation Revue passieren:

A1 Tegmark 5-Neuronen-Modell

Die “Aktivierungsfunktion” f ist beliebig, solange die 2. Ableitung an einer Stelle x0 nicht Null ist. (Die Stelle muss gleichzeitig der Bias-Wert der ersten dense-Schicht sein.) Sie sorgt für die Nichtlinearität, die man braucht, um mit dem Trick der Taylor-Entwicklung einen Ausdruck für x*y zu finden, der als NN berechenbar ist. Wegen des Fehlerterms in der Taylor-Entwicklung müssen die Faktoren sehr klein sein, was man durch eine passende Skalierung z.B. in den Gewichten, erreichen kann. Die korrekte Berechnung von x*y wird dabei durch Rundungsfehler (Auslöschung) und Approximationfehler verfälscht. Beim Training zusätzlich durch das Näherungsverfahren (optimizer und Hyperparameter).

A2 Spezialfall eines Tegmark-Modells ohne Fehlerterm

Da die Aktivierungsfunktion beliebig und ansonsten ohne Bedeutung für das NN ist, können wir ein f(x) wählen, bei dem die Taylor-Entwicklung nach dem Term mit f''(x) abbricht, d.h. alle weiteren Ableitungen Null sind: f ist eine quadratische Funktion. Damit entfällt die Verfälschung durch den Approximationfehler. Die Faktoren müssen nicht mehr nur sehr klein sein, sondern sind tatsächlich beliebig. Die Berechnung von x*y wird nur noch durch die Maschinengenauigkeit beschränkt, da wegen der Grössenordnung der Summanden in der Formel auch Auslöschung vermieden wird. Für das Training gilt nach wie vor der Verfahrensfehler, der Trainingsbereich (Start-Gewichte) ist aber deutlich größer als in A1.

A3 Minimales NN abgeleitet aus der Binomischen Formel

Wir vereinfachen weiter und setzen als Nichtlinearität gleich die Quadratfunktion an, kümmern uns nicht um Taylor-Entwicklung sondern entwicklen eine als NN darstellbare Formel aus der 1. Binomischen Formel. Damit wird die Berechnung von x*y noch robuster und einfacher, erzielt teilweise sogar bessere Ergebnisse als A2. Genauigkeitsbeschränkung Trainingsgenauigkeit sind die gleichen wir bei A2.

Anm.: Auch dieses Modell lässt sich als Spezialfall des Tegmark-Ansatzes darstellen. Wer mag, kann das selbst herleiten.

Bei A2 und A3 wird deutlich, dass quasi das Multiplizieren durch Quadrieren ersetzt wird! Beim allgemeinen Tegmark-Modell A1 wird Multiplizieren — nicht so offensichtlich — durch eine andere Nichtlinearität ersetzt, z.B. durch die Sigmoid-Funktion.

Man fragt sich, wenn offensichtlich das Multiplizieren durch Quadrieren in einem NN ersetzt wird, wo liegt denn da der Clou? Kann man nicht gleich das Produkt als Nichtlinearität einsetzen und damit ein triviales NN für das Multiplikationsproblem konstruieren?

Der Clou ist, dass üblicherweise in NN die Aktivierungsfunktion nur ein Argument hat, etwa die gewichtete Summe eines Neurons. Im Gegensatz zum Produkt (2 Argumente) ist die Quadratfunktion (1 Argument) damit in der Rolle als Aktivierungsfunktion einsetzbar. Das ist bei A2 und A3 der Fall und das Training der Gewichte des kompletten NN führt tatsächlich zum Lernerfolg (unter geringen Einschränkungen).

Ein noch einfacheres Modell ist daher nicht möglich. Es sei denn, man verwendet die Multiplikation direkt innerhalb des NN. In der Rolle einer einfachen Aktivierungsfunktion ist das, wie gesagt, nicht möglich. Man kann nur zwei “Äste” des NN mit einer Multiplikation vereinigen. Das entspricht aber genau dem einfachen multiplikativen NN Modell in 6.4.2. Dort wird aber, wie wir gesehen haben, nicht die Multiplikation gelernt sondern nur eine passende Gewichtung der Faktoren.

Insgesamt bleibt die Multiplikation — in dieser rein numerischen Form — für NN eine problematische Aufgabe. Die bisherigen Ansätze machen alle irgendwie Abstriche an Verwendbarkeit, Genauikeit, Robustheit, Aufbereitungsbedarf der Faktoren und, vor allem, Trainierbarkeit. Beste Ergebnisse mit geringsten Einschränkungen gelingen mit Modell A3, wobei die eigentliche Multiplikation durch Quadrieren ersetzt wird.

Wir werden bei Gelegenheit andere Ansätze untersuchen, die vielleicht mehr die Vorstellung abbilden, die man vom Multiplizieren Lernen in der Schule hat.

Zurück auf 6.4: Multiplizieren Lernen — problematisch für NN

Weiter lesen: 6.5 Das sin**2 Problem revisited

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