Bernd Thomas
Nov 15 · 8 min read

5.6 Das NN-Modell zur Klassifikation von Vielfachen von 3 als vollständiges Deep NN

Mit dem Layer-Stack zur Bestimmung der einfachen Quersumme einer Zahl, können wir versuchen, das Modell aus 5.2 für die Klassifikation von Vielfachen von 3, vollständig durch keras/Tensorflow Schichten zu spezifizieren. Wie in 5.2 werden wir die Schichten für die Quersummen-Bildung im "vor-trainierten" Zustand verwenden, so dass das Training des Gesamt-NNs sich wieder nur auf das Klassifizieren von einstelligen Vielfachen von 3 beschränkt. (S. dazu auch die Ausführungen am Ende von 5.5)

5.6.1 Modell-Architektur

NN Modell-Architektur: Kompletter Layer-Stack für ein NN zur Klassifikation von Vielfachen von 3

Sequential Model utilizing a custom layer

Das insgesamt noch sequentielle Modell besteht aus 3 Gruppen von Layern zur sukzessiven Quersummen-Bestimmung (#1 — #9) und zwei Layern, die dem Lernen der Klassifikation einstellige Zahlen in 5.2 entsprechen. Drei reichen aus, um für maximal vierstellige Zahlen die endgültige, einstellige Quersumme zu bestimmen. Die Aktivierungsfunktionen custom_intund custom_round sind wie in 5.6. definiert.

Zur Initialisierung der Gewichte verwenden wir die aus 5.5 bekannten “trainierten” Gewichte:

Für Training und Test generieren wir zunächst Daten und Ziel-Klassifikationen:

# Imports and Data generation for models in #5.6import numpy as np
import random as rd
import matplotlib.pyplot as plt
from keras.models import Model
from keras.layers import Dense, Input, Lambda, Flatten
from keras.optimizers import sgd, adam
from keras import backend as K
n_pos = 4
nmin,nmax = 0,10000 # nmax = Anzahl der Ziffern (0..9)
n_cases = 200
X = np.random.randint(nmin,nmax,size=(n_cases)) # Generate data set and convert to digits
y = np.array(list(x%3 for x in X)) # Generate target classification multiple of 3
y = np.where(y>0,1,y) # Remainder > 0 are classified as c3 = 1
print(X[:10])
print(y[:10])
# Aufteilung in Traings- und Testdaten - simple partitioning
n_train = 150
X_train = X[:n_train]
y_train = y[:n_train]
X_test = X[n_train:]
y_test = y[n_train:]
# y_train = np.reshape(y_train,(n_train,1,1))
# Obsolet wg Flatten. S. Anmerkung unten
# Print examples ...[7950 6552 3592 3316 2418 3641 7051 7818 4926 7846]
[0 0 1 1 0 1 1 0 0 1]

5.6.2 Das Modell mm3 (multiple of 3):

Modell-Summary und die Kernel-Weigths nach Compilation sowie ein Feed-Forward-Lauf mit einem Testdaten-Satz werden dargestellt. (Die dreifache Quersumme ist hier für max 4-stellige Zahlen sicher einstellig.)

# Deep NN for Learning Multiple of 3 Clssification by Checksum Rule
# Concatenate checksum stack in pipe fashion to generate
# deep checksum (one-digit checksum)
# Additionally, add a layer that transforms the output of
#the deep checksum stack
# (which is a float x with 0.0 <= x < 10.0)
# into a one-hot coding (dim = 10)
# Then add layer for deciding single-digit multiples of 3 as in 5.2
inp = Input(shape=(1,),name='Input_integer')# First level checksum
x = Dense(4,name = 'Dense_Divide',activation=custom_int,
use_bias=False,kernel_initializer=div_init,
trainable=False)(inp)
x = Dense(4,name = 'Dense_Digits',activation=custom_round,
use_bias=False,kernel_initializer=digit_init,
trainable=False)(x)
hand_over = Dense(1,name='Dense_sum',
activation='linear',use_bias=False,
kernel_initializer=sum_init,trainable=False)(x)
#Second level checksum
x = Dense(4,name = 'Dense_Divide_1',activation=custom_int,
use_bias=False,kernel_initializer=div_init,
trainable=False)(hand_over)
x = Dense(4,name = 'Dense_Digits_1',activation=custom_round,
use_bias=False,kernel_initializer=digit_init,
trainable=False)(x)
hand_over = Dense(1,name='Dense_sum_1',activation='linear',
use_bias=False,kernel_initializer=sum_init,
trainable=False)(x)
#3rd level checksum
x = Dense(4,name = 'Dense_Divide_2',activation=custom_int,
use_bias=False,kernel_initializer=div_init,
trainable=False)(hand_over)
x = Dense(4,name = 'Dense_Digits_2',activation=custom_round,
use_bias=False,kernel_initializer=digit_init,
trainable=False)(x)
hand_over = Dense(1,name='Dense_sum_2',activation='linear',
use_bias=False,kernel_initializer=sum_init,
trainable=False)(x)
# Cast checksum output to uint8 and 1-hot coding -
# defining a custom layer
out0 = Lambda(lambda x: K.one_hot(K.cast(x,'uint8'),10))(hand_over)
# Flatten to shape of y_train
out0 = Flatten()(out0)
# Add dense trainable layer which takes input as
# one 10-dim unit vector which represents the
# one-hot coded output of the previous model steps
out = Dense(1,name='Dense_out', activation='linear',use_bias=False,
trainable=True)(out0)
mm3 = Model(inputs=[inp],outputs=[out])
mm3.summary()
# Optimizer and lr for training
lrate= 0.01 # sgd with learning rate
opt = sgd(lrate) # alternative: sgd with learning rate
opt = adam(lrate)
mm3.compile(optimizer=opt,loss='mean_squared_error',metrics=['accuracy'])
print(mm3.get_weights())pred = mm3.predict(X_test)
print(X_test[:10])
print(pred[:10])
# Intermediate feed-forward outputs from layers can be
# generated setting the `out` tensor as layer output
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Input_integer (InputLayer) (None, 1) 0
_________________________________________________________________
Dense_Divide (Dense) (None, 4) 4
_________________________________________________________________
Dense_Digits (Dense) (None, 4) 16
_________________________________________________________________
Dense_sum (Dense) (None, 1) 4
_________________________________________________________________
Dense_Divide_1 (Dense) (None, 4) 4
_________________________________________________________________
Dense_Digits_1 (Dense) (None, 4) 16
_________________________________________________________________
Dense_sum_1 (Dense) (None, 1) 4
_________________________________________________________________
Dense_Divide_2 (Dense) (None, 4) 4
_________________________________________________________________
Dense_Digits_2 (Dense) (None, 4) 16
_________________________________________________________________
Dense_sum_2 (Dense) (None, 1) 4
_________________________________________________________________
Lambda_One_Hot (Lambda) (None, 1, 10) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 10) 0
_________________________________________________________________
Dense_out (Dense) (None, 1) 10
=================================================================
Total params: 82
Trainable params: 10
Non-trainable params: 72

Das Modell mm3 Summary zeigt die Liste der Layers und ihren Typ. Insgesamt gibt es 82 Parameter (Gewichte), davon 10 (von Dense_out) trainierbar. Der Custom Layer Lambda_One_Hot hat keine Parameter, das entspricht seiner Rolle im Modell.

out0 = Lambda(lambda x: K.one_hot(K.cast(x,'uint8'),10))(hand_over) ist quasi das Interface vom Quersummen-Teil zum letzten Layer (Dense_out), der one-hot codierten Input erwartet. Dazu gibt es die keras backend Funktion K.one_hot(). Die wiederum erwartet den Datentyp integer, die finale Quersumme wird aber als float abgeliefert. Daher erfolgt zunächst die Umwandlung mit K.cast(x,'uint8'). ('uint8' bezeichnet einen "kurzen" Integer-Datentyp ohne Vorzeichen.)

Wir schließen einen Flatten Layer an, um für das Training den Output des Modells (out0, out) im gleichen Format (shape) zu bekommen wie die Trainings-Targets y_train.

Damit ist das Modell mm3 durchgängig als Multilayer-NN definiert.

Die initialen Gewichte erkennt man wieder als die der 3 Quersummen-Layer-Gruppen und des Dense_outLayers.

[array([[0.001, 0.01 , 0.1  , 1.   ]], dtype=float32), 
array([[ 1., -10., 0., 0.],
[ 0., 1., -10., 0.],
[ 0., 0., 1., -10.],
[ 0., 0., 0., 1.]], dtype=float32),
array([[1.],
[1.],
[1.],
[1.]], dtype=float32),
array([[0.001, 0.01 , 0.1 , 1. ]], dtype=float32),
array([[ 1., -10., 0., 0.],
[ 0., 1., -10., 0.],
[ 0., 0., 1., -10.],
[ 0., 0., 0., 1.]], dtype=float32),
array([[1.],
[1.],
[1.],
[1.]], dtype=float32),
array([[0.001, 0.01 , 0.1 , 1. ]], dtype=float32),
array([[ 1., -10., 0., 0.],
[ 0., 1., -10., 0.],
[ 0., 0., 1., -10.],
[ 0., 0., 0., 1.]], dtype=float32),
array([[1.],
[1.],
[1.],
[1.]], dtype=float32),
array([[ 0.22243696],
[ 0.6670005 ],
[ 0.7182825 ],
[ 0.5713195 ],
[-0.5804445 ],
[-0.43882123],
[ 0.6084803 ],
[-0.5653434 ],
[-0.5541681 ],
[ 0.3179726 ]], dtype=float32)]

Anmerkung: Der Output von mm3 würde das shape (n_cases,1,1) haben, verursacht durch die one-hot Codierung im Lambda Layer. Damit im Training die loss Funktion richtig ausgewertet werden kann, müssen der Output und die Zielwerte y_train die gleiche Gestalt haben. Das erreicht man entweder mit einem "re-shape" im Anschluß an die Datenengenerierung (oben)

y_train = np.reshape(y_train,(n_train,1,1))

aber sinnvoller noch durch Einziehen eines Flatten Layers nach der one-hot- Codierung.

5.6.3 Training und Evaluation

Die Plots zeigen das erwartet gute Konvergenzverhalten in Loss und Accurracy wie in 5.2

# Now try training of the full model - 
# where only the last layer weights are trainable
ep = 100 # Set number of epochs
bs = 10 # Set bacth size
hist1 = mm3.fit(X_train,y_train,epochs=ep,batch_size=bs,verbose=0)
Training mm3: Loss
Training mm3: Accuracy

Für die Evaluation mit dem Testset und der Accuracy-Auswertung mit den Test-Zielwerten “shapen” wir den mm3 Output (float Werte) auf die Form der Zielwerte y_test. Das Ergebnis ist wieder perfekt wie in 5.2.

# Prediction for test data and test targets

pred = mm3.predict(X_test)
print(pred.shape,X_test.shape)
print(X_test[:10],y_test[:10])
y_pred = np.reshape(pred,(n_test))
y_pred = np.round(y_pred)
print(pred[:10],y_pred[:10])
acc = 1.0 - np.sum(np.abs(y_test - y_pred))/n_test
print('Accuracy Testset:',acc)
(50, 1) (50,)
[6831 7806 2627 1502 8117 5589 5493 9572 9370 3292]
[0 0 1 1 1 0 0 1 1 1]
[[4.4641961e-35]
[3.1688953e-24]
[9.9999988e-01]
[9.9999988e-01]
[9.9999988e-01]
[4.4641961e-35]
[3.1688953e-24]
[1.0000000e+00]
[9.9999988e-01]
[9.9999952e-01]]
[0. 0. 1. 1. 1. 0. 0. 1. 1. 1.]
Accuracy Testset: 1.0

5.6.4 Trainierbarkeit des Modells

Ein zusätzlicher Custom Layer nach einer Quersummen Layer-Gruppe hilft bei der am Ende von 5.5 diskutierten Problematik. Beim Übergang von einem Dense_sum Layer zum nächsten Layer (nächstes Dense_Divide oder Lambda_One_Hot) wird der Output damit auf die "richtige" nächste ganze Zahl gerunded, so dass z.B. eine 16.9994 tatsächlich als 17.0000 an den nächsten Layer übergeben wird. Damit könnte das Modell mit echt "vor-trainierten" Gewichten arbeiten (statt mit den gesetzten, theoretisch exakten Gewichten). Ausschnitt aus dem Code:

# .....hand_over = Dense(1,name='Dense_sum',activation='linear',use_bias=False,
kernel_initializer=sum_init,trainable=False)(x)
hand_over = Lambda(lambda s: custom_round(s))(hand_over)
# .....

Dieser Layer hat natürlich keine Paramter und blockiert ein Back-Propagating — was aber in Ordnung ist, da die die Layer der Quersummen-Gruppen im Gesamtmodell ohnehin im “austrainierten” Zustand eingesetzt werden (transfer learning).

Das Training des Gesamtmodells mm3 kann wegen der Custom-Komponenten nicht durchgängig erfolgen, sehr wohl aber iterativ als transfer learning. Das sei hier kurz skizziert ohne Durchführung.

Nehmen wir also an, dass wir die theoretisch exakten Gewichte für mm3 nicht kennen bzw. nicht apriori per kernel_initializer in der Layer-Defintition setzen wollen.

Dense_Divide: Dieser Layer kann als einfaches Modell definiert werden und mit den geeigneten Trainingsdaten trainiert werden, indem zunächst als Aktivierungsfunktion nicht custom_int sondern eine differenzierbare Standard Aktivierung verwendet wird, z.B. linear. Mit d_div_trained_weights = mm3.layers[1].get_weights() rettet man die für diesen Layer trainierten Gewichte. Wenn anschließend der Dense_Divide Layer mit der Custom-Aktivierung weiter verwendet werden sollen, setzt man trainable=False und "transferiert" den trainierten Zustand (die d_div_trained_weights) im Anschluß an die Modell Compilation mit mm3.layers[1].set_weights(d_div_trained_weights).

Anm. Eine Liste aller Layer eines Modells bekommt man z.B. mit layers_list = mm3.layers. Damit kann man einzelne Layer über den passenden List-Index referenzieren.

Teilmodell Zahl zu Digits: Hier verwendet man die Dense_Divide Schicht mit der custom activation und trainable=False. Man lässt Dense_Digits trainable und ohne Custom-Aktivierung, und sorgt für die passenden Trainingsdaten (Zahlen, 4 Ziffern). Nach der Modell Compilation werden die d_div_trained_weights dann für die Dense_Divide Schicht gesetzt, wie zuvor beschrieben. Nach dem Training des Teilmodells sichert man die trainierten Gewichte von Dense_Digits für spätere Verwendung.

Teilmodell Quersumme erste Stufe: Hierzu ist nur noch die Dense_sum Schicht zu trainieren, die beiden vorigen Layer werden im trainierten Zustand verwendet, indem deren Gewichte nach dem compile mit den vorher trainierten Gewichten je Layer aktualisiert werden. Wie wir gesehen haben (s. Anmerkung am Ende von 5.5) müssen wir nun tatsächlich sicher stellen, dass der Output von Dense_sum auf die nächste ganze Zahl "gerundet" wird - wozu wir den eben definierten Layer nachschalten.

Teilmodell Vollständige Quersumme: Hier ist nichts weiter zu tun, als für die Schichten aller drei Quersummengruppen nach dem Compilieren des mm3 die Gewichte der 9 Dense Layer jeweils mit den 3 entsprechenden trainierten Gewichtsmatrizen zu setzen. (Die Schichten sind alle auf "nicht-trainierbar" zu setzen, die kernel_initializer können optional weg gelassen werden, da sie keine Rolle spielen.)

Gesamtmodell mm3: Für das Gesamtmodell ist nur noch die letzte Schicht zu trainieren (Dense_out). Soll das Modell im Sinne von transfer learning für weitere Zwecke (s. 5.7) eingestzt werden, kann man auch die trainierten Gewichte dieser Schicht, oder die des Modells mm3 insgesamt speichern.

Weiter lesen: 5.7 Zwei mal Drei ist Sechs — Ein keras/tf Modell lernt Vielfache von 6 und mehr

Zurück auf Anfang

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

Beck et al.

*Arbeiten ist Zusammenarbeiten*. Von Menschen, Daten, Infrastrukturen. Dafür stehen wir.

Bernd Thomas

Written by

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

Beck et al.

*Arbeiten ist Zusammenarbeiten*. Von Menschen, Daten, Infrastrukturen. Dafür stehen wir.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade