Bernd Thomas
Beck et al.
Published in
9 min readOct 28, 2019

--

4.3 Embedding — Das Gerade-/Ungerade-Problem als Text-Analyse

Mit den elementaren NN-Architekturen und keras lassen sich leicht verschiedene Modell-Varianten und Trainingsmethoden am gleichen Problem ausprobieren: Unterschiedliche Schichten- und Knotenanzahl, Gewicht-Presets, Aktivierungs- und Loss-Funktionen, Optimierungsverfahren, und sonstige Hyper-Parameter.

Stattdessen wollen wir noch eine andere Repräsentation des Gerade-/Ungerade-Lernproblems behandeln. In Teil 1, wurden die Zahlen als numerische Größe behandelt, in den folgenden Teilen dagegen als Zeichenketten aus Ziffern. Eine weitere Möglichkeit besteht darin, die Zahlen als Text zu verstehen. Beispiel: eine vierstellige Zahl -> ein Satz bestehend aus 4 Wörtern (Ziffern oder Zahlworte), wobei die syntaktische Bedeutung der Satzbestandteile hier “Tausender” oder “Zehner” usw. sind.

Modell-Architektur

Vierstellige Zahlen werden als Sätze mit vier Wörtern dargestellt. Statt der klassischen one-hot Kodierung als 10-dim Einheitsvektoren bietet Embedding eine dichtere Kodierung in Form von Vektoren in einem geringer-dimensionalen Raum.

Mit der one-hot Kodierung wird der Input von einer k-stelligen Zahl in eine k x 10 Matrix gewandelt, in der nur k Elemente einen Wert ungleich 0 haben (nämlich 1.0). Ein Trainingsset wird damit zu einem extrem dünn-besetzten 3-dim Array. Embedding (auch word embedding genannt) verdichtet diese Darstellung und reduziert die Dimensionalität: Statt Einheitsvektoren werden voll-besetzte, geringer-dimensionale Vektoren erzeugt. Üblicherweise wird Embedding für Textanalysen eingesetzt; wir interpretieren daher k-stellige Zahlen als Sätze mit k Wörtern aus einem Vokabular mit 10 Wörtern und das Datenset als Text aus n_case Sätzen.

Das Modell besteht dann aus folgenden keras Layern:

  • Embedding
  • Flatten
  • Dense (Output).

Anm.: Die Liste der Imports von keras wird etwas länger, daher geben wir diese und die Schritte der Erzeugung und Vorbereitung von Trainings- und Testdaten gekürzt wieder.

import random as rd
import numpy as np
from keras import initializers
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.embeddings import Embedding
from keras.initializers import Constant, Zeros

# rd.seed(42) # Für Repoduzierbarkeit

nmin,nmax = 0,10000
n_cases = 200
N = n_cases
n_test = 50
''' Trainingsdaten Z und y und Testdaten Z_test, y_test in der
Ausgangsform werden wie üblich erzeugt '''
Z = np.random.randint(nmin,nmax,size=(n_cases))
y = Z[:] % 2

Z_test = np.random.randint(nmin,nmax,size=(n_test))
y_test = Z_test[:] % 2
''' Wir erzeugen zu den Zahlen in Z und Z_test "Sätze" zu je 4
Worten (= Ziffern), inklusive Padding vorlaufender Nullen '''

v_pad = np.vectorize(pad_zeros)
D = v_pad(4,Z)
X = list(D)

D = v_pad(4,Z_test)
X_test = list(D)

print([[X[i],y[i]] for i in range(10)])

''' Umwandlung der Training- und Test-Zahlen in ein n_case x 4
np.array von einstelligen integers. Damit kann das embedding
coding per Identität erfolgen, d.h. das encoding sind die
Ziffern der Zahl'''

npX = np.zeros((n_cases,4),dtype='int32')
for i in range(n_cases):
l=[int(a) for a in list(X[i])]
npX[i]= np.array(l)

npX_test = np.zeros((n_test,4),dtype='int32')
for i in range(n_test):
l=[int(a) for a in list(X_test[i])]
npX_test[i]= np.array(l)

print('npX_test:',type(npX_test),npX_test.shape)

['8141', '0174', '7877', '5776', '4665', '2693', '0644', '5584', '9244', '9014']
[['8141', 1], ['0174', 0], ['7877', 1], ['5776', 0], ['4665', 1], ['2693', 1], ['0644', 0], ['5584', 0], ['9244', 0], ['9014', 0]]
npX_test: <class 'numpy.ndarray'> (50, 4)

Kodierung

Die Kodierung wandelt die Wörter des Vokabulars zu Codes (Zahlen). Da unser Vokabular aus den Dezimalziffern besteht, ist hier eine gesonderte (Um-)Kodierung unnötig. Wir verwenden die “natürliche” Kodierung und damit die npX, npX_test direkt.

Padding

Hierbei werden die “Sätze” auf gleiche Länge gebracht. Auch das ist schon erledigt, da wir ggf vorlaufende Nullen ge-padded haben.

Definition des Modells

Die Parameter “Satzlänge” und Größe der “Vokabulars” sind in unserem Fall festgelegt (ein Vokabular mit mehr als 10 Wörtern ist möglich, macht aber keinen Sinn, da es nicht mehr als 10 “Wörter” gibt):

  • mx_length = 4
  • vocab_size = 10

Die Dimension des Einbettungsraums embedding_dim kann noch experimentell variiert werden. Dimensionen größer 10 machen keinen Sinn, Dimensionen kleiner 10 bedeuten Reduktion der Input-Dimensionalität, damit der Anzahl Gewichte und - im Optimum - des Trainingsaufwands.

Das Modell besteht aus den 3 keras-Schichten

  • Embedding
  • Flatten
  • Dense (Output-Schicht)

Um den Embedding-Effekt darzustellen, compilieren wir das Modell zunächst ohne die Output Schicht:

# Modell-Spezifikation und -Generierungvocab_size = 10          
max_length = 4
embedding_dim = 6 # Dimension des Embedding Raums. Ausprobieren.
print('Embedding Parameter')
print('Vokabular:',vocab_size,' Satzlänge:', max_length,' Embedding Dimension:',embedding_dim)
model = Sequential() # Die einzelnen Layers:
model.add(Embedding(vocab_size, embedding_dim,
input_length=max_length))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])# Modell Summary
print(model.summary())
# Anzeigen der Trainingsdaten in der embedded Form
embeddings = model.predict(npX)
print('Embedding:', embeddings.shape)
print(npX[0],npX[1])
print(embeddings)
Embedding Parameter
Vokabular: 10 Satzlänge: 4 Embedding Dimension: 6
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 4, 6) 60
=================================================================
Total params: 60
Trainable params: 60
Non-trainable params: 0
_________________________________________________________________
Embedding: (200, 4, 6)
Die ersten zwei Zahlen embedded:
[8 1 4 1]
[[[-0.03774507 -0.01465297 0.01155975 0.04355851 0.04296576
0.02256625]
[ 0.01738021 0.00026632 -0.0142792 -0.04681025 -0.02145786
0.03311464]
[-0.04529729 0.04828927 -0.0422636 0.03005159 -0.00677324
0.00681709]
[ 0.01738021 0.00026632 -0.0142792 -0.04681025 -0.02145786
0.03311464]]
[0 1 7 4]
[[ 0.02428028 0.01560793 0.04396329 0.02770181 -0.0367776
-0.01221422]
[ 0.01738021 0.00026632 -0.0142792 -0.04681025 -0.02145786
0.03311464]
[ 0.0273521 -0.00808902 0.03356317 0.04947278 -0.04241119
0.00869002]
[-0.04529729 0.04828927 -0.0422636 0.03005159 -0.00677324
0.00681709]]
...

Mit embedding_dim = 6 wird jeder "Satz" als 4 Vektoren der Länge 6 dargestellt. Flatten macht daraus jeweils einen Vektor der Länge 24:

# Modell-Spezifikation und -Generierung - mit Flattenvocab_size = 10          
max_length = 4
embedding_dim = 6 # Dimension des Embedding Raums. Ausprobieren.
print('Embedding Parameter')
print('Vokabular:',vocab_size,' Satzlänge:', max_length,' Embedding Dimension:',embedding_dim)
model = Sequential()# Die einzelnen Layers:
model.add(Embedding(vocab_size, embedding_dim,
input_length=max_length))
model.add(Flatten())
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])# Modell Summary
print(model.summary())
# Anzeigen der Trainingsdaten in der embedded Form
embeddings = model.predict(npX)
print('Embedding:', embeddings.shape)
print(npX[0],npX[1])
print(embeddings)
Embedding Parameter
Vokabular: 10 Satzlänge: 4 Embedding Dimension: 6
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 4, 6) 60
_________________________________________________________________
flatten_1 (Flatten) (None, 24) 0
=================================================================
Total params: 60
Trainable params: 60
Non-trainable params: 0
_________________________________________________________________
Embedding: (200, 24)
[1 0 8 8] [4 0 7 2]
[[ 0.02225088 -0.04969155 0.01920274 ... 0.03442291 -0.03308008
-0.00746533]
[-0.01414711 0.00633459 -0.00714425 ... 0.00363339 0.03462604
0.00384744]
...

Wenn man genau hinschaut, sieht man, dass, wie erwartet, gleiche Ziffern in den Input-Tupeln durch gleiche Vektoren der Länge 6 repräsentiert werden. Das entspricht den Einheitsvektoren der one-hot Kodierung in 4.1. Weiter unten, beim Variieren der Embedding Dimension, ist das noch klarer zu sehen.

Das Training findet mit dem kompletten Modell auf Basis der embedded Daten statt:

# Modell-Spezifikation und -Generierung - Vollständiges Modell

vocab_size = 10
max_length = 4
embedding_dim = 6 # Dimension des Embedding Raums. Ausprobieren.

print('Embedding Parameter')
print('Vokabular:',vocab_size,' Satzlänge:', max_length,' Embedding Dimension:',embedding_dim)

model = Sequential()

# Die einzelnen Layer:
model.add(Embedding(vocab_size, embedding_dim,
input_length=max_length))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid',
kernel_initializer=initializers.Constant(0.5),
bias_initializer='zeros'))

model.compile(optimizer='adam', loss='binary_crossentropy',
metrics=['acc'])

# Modell Summary
print(model.summary())
Embedding Parameter
Vokabular: 10 Satzlänge: 4 Embedding Dimension: 6
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_2 (Embedding) (None, 4, 6) 60
_________________________________________________________________
flatten_1 (Flatten) (None, 24) 0
_________________________________________________________________
dense_9 (Dense) (None, 1) 25
=================================================================
Total params: 85
Trainable params: 85
Non-trainable params: 0
_________________________________________________________________

Das Modell-Summary zeigt Anzahl und Typ der Layers, Shape des Layer-Outputs und Anzahl Gewichte (gesamt und anpassbare). Flatten macht nur aus dem 2-dim array einen Vektor, hat keine eigenen Gewichte.

Training des Modells

Neben den Hyper-Paramtern oben (vocab_size und embedding_dim) lässt sich das Training noch auf unterscheidliche Weise steuern durch die Hyper-Parameter batch_size (von 1 bis n_cases), Anzahl der Epochen je Batch und noch (künstlich) durch Wiederholung der Durchläufe (runs)

import matplotlib.pyplot as plt# Train the model. Batch size set to 10%.
training_epochs = 200
training_batches = int(n_cases * 0.1)
hist1 = model.fit(npX, y, epochs=training_epochs,
batch_size=training_batches, verbose=0)
print('Training')
print('Anz. Epochen:',training_epochs,' Batch Size:',training_batches,' Trainng Set:',n_cases)
print('Accuracy')
print([hist1.history['acc'][i] for i in range(0,200,10)])

Training
Anz. Epochen: 200 Batch Size: 20 Trainng Set: 200
Accuracy (in 10er Schritten)
[0.585000005364418, 0.7499999940395355, 0.8400000095367431, 0.9150000035762786, 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]

Man sieht hier schon, dass die Accuracy nach ca. 50 Epochen gegen 1.0 konvergiert. Loss und Accuracy, sowie die Entwicklung der (60+24+1) Gewichte zeigen die Plots der history und der weights im trainierten Zustand:

# Get current weights
w = model.get_weights()[0]
w_size = w.shape[0]*w.shape[1]
w = w.reshape((w_size))
v = model.get_weights()[1]
# Plot history of loss & acc
...
# Plot embedding layer weigths w
...
# Plot output layer weights v
...

Mit model.get_weights hat man Zugriff auf die (trainierten) Gewichte der verschiedenen Layer. (Den Code für die Plots lassen wir wieder weg.)

Abb. 1: Embedding Dim 6 — Konvergenz und trainierte Gewichte

Ein Blick auf die trainierten Gewichte zeigt wieder ein interessantes Muster! Man beachte, dass die Anzahl der blauen Punkte je Gruppe gerade der embedding_dim enstpricht und die roten ebenfalls so gruppiert sind. Die trainierten Embedding Layer Gewichte repräsentieren offensichtlich gruppenweise die "Gerade/Ungerade-Klassifikation" der Dezimalziffern. Im Output Layer zeigen die Gewichte wieder, dass nur die Einer-Position (bzw. das letze "Wort" im "Satz") für die Klassifikation der Zahl (des "Satzes") ausschlaggebend ist. Es ist interessant zu sehen, was passiert, wenn wir das Embedding variieren (s. unten). Zunächst aber die

Evaluation mit Testset

Die Ausgabe zeigt die Testzahl (als kodierter “Satz”), die Vorhersage des trainierten Modells, die Umwandlung in 0 / 1 (Vorhersage-Ergebnis gerade/ungerade) und die wahre Klassifikation. Und am Ende die Accuracy über das Testset.

# Evaluation durch Predicts über das Testset


yy = model.predict(npX_test) # Modell-Ausgabe
print(yy.shape)
y_pred = np.round(yy) # Vorhersage Klassifikation
acc = 0
for i in range(len(yy)):
if i<20:
print(npX_test[i],': %6.3f %d %d'
%(yy[i][0],int(y_pred[i][0]),y_test[i]))
if int(y_pred[i][0]) == y_test[i]:
acc += 1

loss, accuracy = model.evaluate(npX_test, y_test, verbose=0)
print('Accuracy: %6.3f' % (accuracy*100))
(50, 1)
[4 2 9 2] : 0.008 0 0
[3 1 7 7] : 0.995 1 1
[1 7 5 4] : 0.004 0 0
[4 5 0 8] : 0.006 0 0
[9 5 0 6] : 0.004 0 0
[6 6 9 5] : 0.994 1 1
[1 4 8 8] : 0.005 0 0
[2 0 5 3] : 0.993 1 1
[1 8 9 1] : 0.995 1 1
[7 2 9 4] : 0.004 0 0
...
Accuracy: 100.000

Die Korrekt-Klassifikation ist damit auch für das Testset gegeben. Man kann die Evaluation auch über alle maximal vierstelligen Zahlen von 0 bis 9999 führen.

Variation der Embedding Dimension

Zunächst überraschend, kann man das Modell bzgl. der embedding_dim variieren, sogar stark variieren, ohne dass es seine Trainierbarkeit verliert. Das Konvergenz-Profil von Loss und Accuracy sieht zwar leicht anders aus (z.B. Accuracy 1.0 wird nach mehr oder weniger Epochen erreicht), die Testset-Evaluation liefert nach 200 Epochen Training aber immer korrekte Klassifikationen. Mit Blick auf die Anzahl der trainierbaren Parameter erscheint es aber wieder plausibel: in 4.1 konnten wir ein (minimales) NN perfekt trainieren, das mit 14 Gewichten auskam (ohne Bias).

Experimente (“Epochen” bedeutet die Anzahl Epochen bis die Accuracy bei 1.0 liegt. “ + 1” bezieht sich auf den Bias-Parameter des einzigen Knoten im Output Layer.)

Die folgenden Grafiken zeigen Konvergenz und Gewichte im trainierten Zustand für die Embeddings mit Dimension 4, 2, und 1 (!).

Abb. 2: Embedding Dim 4
Abb. 3: Embedding Dim 2
Abb. 4: Extrem-Fall Embedding Dim 1

Das extreme Embedding mit dim=1 zeigt ein Gewichte-Muster, das dem aus dem Minimal-Modell in 4.1 entspricht. Embedding liefert aber eine andere Repräsentation des Inputs. In 4.1 werden die Daten durch einen “4er-Vektor” mit Dezimalziffern repräsentiert, hier dagegen durch einen Vektor mit float-Werten nahe Null, was zudem die Konvergenz verbessert. Ähnliche Überlegungen gelten für den Fall embedding_dim = 10. Zwischen 10 und 1 zeigt das Embedding-Modell eine Art Übergangsverhalten. Prinzipiell zeigt uns dieser Modellierungsansatz eine Strategie, zu einem Minimal-Modell zu gelangen.

Gerade-/Ungerade-Klassifikation von ganzen Zahlen erfolgreich mit NN’s gelernt!

Wir haben damit auch 3 Neural-Network-Architekturen entwickelt, die die Gerade/Ungerade-Klassifikation ganzer Zahlen lernen. Damit schließen wir die Untersuchungen zu diesem Problemtyp ab und befassen uns in Teil 5 mit dem Erlernen weiterer exemplarischer arithmetischer Kenntnisse. In Teil 6 wird das Gerade-/Ungerade-Problem noch einmal vor dem Hintergrund der Universal Approximation Property von NN’s aufgegriffen.

Weiter lesen: 4.4 Gerade/Ungerade Unterscheidung in anderen Zahlensystemen

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