4.4 Just for fun! — Gerade-/ Ungerade-Klassifikation in anderen Zahlensystemen
Bei den Neuronalen Netzen für die Klassifikation von geraden und ungeraden Zahlen spielte die Dezimaldarstellung der Zahlen eine Rolle. Zum Einen in der one-hot Kodierung, zum anderen in der Architektur beim Output-Layer mit 10 Neuronen. Das führt zu der Frage, ob wir hier eine problemspezifische Abhängigkeit haben, die wir bei starkem Lernen ja vermeiden wollen.
Daher betrachten wir als Ergänzung das Gerade/Ungerade-Problem auf Basis einer beliebigen Zahldarstellung. Bekannte Systeme in der Informatik sind das Hexadezimalsystem, das Oktalsystem, das Binärsystem, weniger häufig das Ternärsystem (Basis 3) oder andere auf ungeraden Zahlen basierende Zahlensysteme.
Da es dabei nur um die “Darstellung” geht, als das Muster, in dem eine Zahl dargestellt wird, sollten NN’s das Lernproblem unabhängig vom zugrunde liegenden Zahlensystem lösen können.
Voraussetzung ist, dass das Prinzip, nach dem man Gerade und Ungerade unterscheiden kann, das Gleiche bleibt. Das ist leider nicht ganz der Fall.
Man kann sich überlegen, dass man Gerade / Ungerade an der letzten Stelle der Zahl entscheiden kann, wenn die Basis geradzahlig ist. Beispiel: Oktalzahl 104 endet auf 4 -> 64+0+4 = 68 dezimal
ist gerade, Hexadezimal 10b
endet auf b -> 256+0+11 = 267 dezimal
ist ungerade.
Bei ungeradzahliger Basis wie 3 (ternär) oder 5 (quinär) gilt eine etwas komplexere Regel: Eine Zahl ist gerade, wenn ihre Quersumme gerade ist. Quinärzahl 104 -> Quersumme 10 (dezimal: 5) -> 125+0+4 = 129
dezimal ist ungerade, (204 -> QS 11 -> 2\*125+0+4 = 254
gerade).
Wir können das “text-analytische” NN mit embedding unverändert einsetzen für die Gerade-/Ungerade-Klassifikation von z.B. Hexdezimalzahlen. Wir müssen nur dafür sorgen, die Data-Sets für Training und Test mit Hex-Zahlen zu erzeugen und als Input für das Modell zu codieren.
Anm.: Es werden nur Code Snippets gezeigt, die gegenüber 4.3 neu oder geändert sind.
#--- Imports wie in 4.3nmin,nmax = 0,0x10000 # hex 10000
n_cases = 200
n_test = 50
n_pos = 4 # Anzahl hex Stellen, beliebig wählbar#--- Generieren der Zahlen-Sets Z, Z_test wie in 4.3# Umwandlung in Hexadezimalzahlenv_hex = np.vectorize(hex)
Z = v_hex(Z)
Z_test = v_hex(Z_test)
print(' Testset - Ein Beispiel: Zahl: ',Z[10],' Kategorie: ',y[10])# Umwandlung in hex-Zeichenketten (ohne '0x')def pad_zeros(k,s):
t = str(s)[2:] # 0x im string weglassen
while len(t) < k:
t = '0' + t
return t # A sentence of k one-character words, separated by spacesv_pad = np.vectorize(pad_zeros) D = v_pad(4,Z)
X = list(D)
D = v_pad(4,Z_test)
X_test = list(D) # Daten-Beispiele
print([D[i] for i in range(10)])
print([[X[i],y[i]] for i in range(10)])
Ein Beispiel: Zahl: 62486 Kategorie: 0
Ein Beispiel: Zahl: 0xf1fc Kategorie: 0
['a848', '59d5', 'baff', '1a54', '18ba', '4a2b', '3b57', 'd36a', '8818', '14b6']
[['a848', 0], ['59d5', 1], ['baff', 1], ['1a54', 0], ['18ba', 0], ['4a2b', 1], ['3b57', 1], ['d36a', 0], ['8818', 0], ['14b6', 0]]
Kodierung
Die Kodierung wandelt die Wörter des Vokabulars zu Codes (Zahlen). Da unser Vokabular aus den Hexadezimalziffern besteht, also Buchstaben enthält, ist hier eine gesonderte Umkodierung nötig. Wir verwenden die “natürliche” Kodierung durch Dezimalzahlen und erzeugen damit die Input-Arrays npX, npX_test.
Padding
Hierbei werden die “Sätze” auf gleiche Länge gebracht. Auch das ist schon erledigt, da wir ggf vorlaufende Nullen ge-padded haben.
'''Umwandlung der Training- und Test-Zahlen in ein
n_case x 4 np.array von hex digits.
Damit kann das embedding coding per Identität erfolgen,
d.h. das encoding sind die Dezimal-Repräsentationen
der hex-Ziffern der Zahl'''
hti = dict() # Vocabulary - hex Code Dictionary
for i in range(10):
hti[str(i)] = str(i)
hti['a'] = '10'
hti['b'] = '11'
hti['c'] = '12'
hti['d'] = '13'
hti['e'] = '14'
hti['f'] = '15'# Codierung der hex-Zahlen (Zeichenketten) in das input-array Format
npX = np.empty((n_cases,n_pos))
for i in range(n_cases):
l = list(X[i])
l = [hti[c] for c in l]
npX[i]= np.array(l)
v_int = np.vectorize(int)
npX = npX.astype(int)
print(npX.shape)
print([[npX[i],y[i]] for i in range(10)])
npX_test = np.empty((n_test,n_pos))
for i in range(n_test):
l = list(X_test[i])
l = [hti[c] for c in l]
npX_test[i]= np.array(l)
npX_test = npX_test.astype(int)
print(npX_test.shape)
print([[npX_test[i],y_test[i]] for i in range(10)])
(200, 4)
[[array([10, 8, 4, 8]), 0], [array([ 5, 9, 13, 5]), 1], [array([11, 10, 15, 15]), 1], [array([ 1, 10, 5, 4]), 0], [array([ 1, 8, 11, 10]), 0], [array([ 4, 10, 2, 11]), 1], [array([ 3, 11, 5, 7]), 1], [array([13, 3, 6, 10]), 0], [array([8, 8, 1, 8]), 0], [array([ 1, 4, 11, 6]), 0]]
(50, 4)
[[array([12, 6, 7, 2]), 0], [array([7, 7, 2, 3]), 1], [array([12, 14, 7, 11]), 1], [array([5, 3, 8, 6]), 0], [array([ 4, 12, 4, 6]), 0], [array([12, 4, 8, 7]), 1], [array([11, 14, 8, 5]), 1], [array([11, 0, 8, 8]), 0], [array([ 4, 1, 0, 15]), 1], [array([ 7, 0, 10, 9]), 1]]
Definition des Modells
Die Parameter “Satzlänge” und Größe der “Vokabulars” sind in unserem Fall festgelegt :
mx_length = 4
vocab_size = 16
(!!!)
Die Dimension des Einbettungsraums embedding_dim
kann noch experimentell variiert werden. Dimensionen größer 16 machen keinen Sinn, Dimensionen kleiner 16 bedeuten Reduktion der Imput-Dimensionalität, damit der Anzahl Gewichte und - im Optimum - des Trainingsaufwands.
Das Modell besteht wieder aus den 3 keras
-Schichten
- Embedding
- Flatten
- Dense (Output-Schicht)
# Modell-Spezifikation und -Generierung
vocab_size = 16
max_length = 4
embedding_dim = 8 # Dimension des Embedding Raums. Ausprobieren.
print('Embedding Parameter')
print('Vokabular:',vocab_size,' Satzlänge:', max_length,' Embedding Dimension:',embedding_dim)#--- Spezifikation der Layer, Generierung und Summary wie in 4.3# Modell Summary
print(model.summary())
Embedding Parameter
Vokabular: 16 Satzlänge: 4 Embedding Dimension: 8
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_3 (Embedding) (None, 4, 8) 128
_________________________________________________________________
flatten_3 (Flatten) (None, 32) 0
_________________________________________________________________
dense_3 (Dense) (None, 1) 33
=================================================================
Total params: 161
Trainable params: 161
Non-trainable params: 0
_________________________________________________________________
None
Training erfolgt mit dem gleichen Code wie in 4.3. Die Linien-Plots zeigen wieder die Entwicklung von Accuracy und Loss im Trainingsverlauf. Die Scatter-Plots zeigen die trainierten Gewichte.
Training
Anz. Epochen: 200 Batch Size: 20 Trainng Set: 200
Accuracy
[0.5, 0.7549999952316284, 0.85, 0.9099999964237213, 0.9899999976158143, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
Der trainierte Zustand des NN Modells zeigt wieder die auffällige Anordnung der Gewichte in Gruppen von 8, der gewählten embedding_dim
. Es gibt 16 blaue Gruppen, die zwischen -0.4 und 0.4 alternieren, die damit 'gerade' (-0.4) bzw. 'ungerade' signalisieren. Für den Output Layer zeigen die 4 roten Gruppen die "Relevanz" der Stellen für die Gerade-/Ungerade-Klassifikation an: nur die vierte Gruppe ("Einer-Stelle") nehmen deutlich positive Werte an.
Die Evaluation mit dem Test-Set zeigt stets korrekte Ergebnisse:
# Evaluation mittels Predict über das Testsetyy = model.predict(npX_test) #--- Output, wie in 4.3hex pred y y_true
[0 2 0 e] : 0.006 0 0
[2 3 1 6] : 0.005 0 0
[8 c 1 1] : 0.996 1 1
[7 7 8 e] : 0.005 0 0
[f c 0 d] : 0.997 1 1
[a 7 3 8] : 0.004 0 0
[6 4 3 c] : 0.006 0 0
[b e 8 e] : 0.005 0 0
[5 3 1 e] : 0.005 0 0
[9 d c 1] : 0.994 1 1
[3 a e 4] : 0.003 0 0
[5 2 6 3] : 0.993 1 1
[c d e 5] : 0.997 1 1
[f e f 5] : 0.996 1 1
[4 6 6 8] : 0.004 0 0
[e f 0 2] : 0.018 0 0
[e a 7 1] : 0.996 1 1
[f 7 6 6] : 0.004 0 0
[d 8 3 6] : 0.005 0 0
[a 8 b 5] : 0.997 1 1
Accuracy: 100.000
In anderen Zahlensysteme mit geradzahliger Basis lässt sich die Gerade-/Ungerade-Klassifikation mit der gleichen NN Architektur lernen. Ggf. muss man, wie hier beim Hexadezimal-System, den Umfang des Ziffern-Raumes (das Vokabular) erweitern.
Der Unterschied zum Fall des Dezimalsystems (in 4.3) besteht nur in der Vorbereitung:
- Erweiterung des Ziffernbereichs (auf
0,...,9,a,b,c,d,e,f
) - Generierungsroutine für Trainings- und Testdaten
- Umcodierung der Buchstaben für die one-hot Codierung
Das NN Modell bleibt gleich, erfordert lediglich die Anpassung des Paramters vocab_size
und damit (in keras
) die erneute Compilierung.
Im Beispiel war die embedding_dim
auf 8 gesetzt. Wie in 4.3 kann man auch hier die Embedding Dimension auf das extreme Minimum setzen: embedding_dim = 1
. Das NN Modell konvergiert etwas langsamer, aber es konvergiert. Die trainierten Gewichte zeigen das gleiche Muster, allerdings bestehen die "Gruppen" jeweils nur noch aus einem Punkt.
Für ungeradzahlige Basis erfordert die Gerade-/Ungerade-Erkennung das Lernen der Quersummen-Bildung. Das lernen wir in Teil 5.
Weiter lesen: 4.5 Vertrauensfragen — 5 gerade sein lassen
bernhard.thomas@becketal.com
www.becketal.com