Bernd Thomas
Beck et al.
Published in
7 min readOct 30, 2019

--

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 spaces
v_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]
Abb. 1: Hex-Zahlen. Embedding Dim 8— Konvergenz und trainierte Gewichte

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

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