Bernd Thomas
Beck et al.
Published in
9 min readNov 11, 2019

--

5.2 Transfer Learning Modell und Training eines Gesamt-Modells für Vielfache von 3 (und 9)

Wir bauen nun ein NN Modell auf, dass Vielfache von 3 zu erkennen lernt. Dazu wird erst das Modell aus 5.1 zur Bestimmung von einfachen Quersummen erstellt und vor-trainiert. Im trainierten Zustand, d.h. mit den so fixierten Gewichten, wird das Quersummen-Modell mehrfach hintereinader geschaltet (recurrent layers), indem der Output eines Layers als (in Ziffern gewandelten) Input für den nächsten Layer weitergeleitet wird. Die Anzahl der Layer wird als Hyper-Parameter so festgelegt, dass für alle Zahlen des Trainingsets die einstellige Quersumme (deep checksum) bestimmt wird. Der Output des Quersummen-Stacks wird dann einer Aufgaben-spezifischen Schicht zugeführt; hier eine, die lernt einstellige Vielfache von 3 zu erkennen.

Trainiert werden also nur noch die Parameter der letzten Schicht, die vorausgehenden bleiben im trainierten Zustand fest. Ein typisches Schema für Transfer Learning. Dem Gesamtmodell werden für Training und Test Zahlen und deren (Target-) Klassifikation vorgelegt. Quersummen (wie in 5.1) oder andere “Zwischenziele” spielen im Data Set keine Rolle.

Wir setzen die Problem-Parameter und definieren Helferfunktionen wie in 5.1 für das Quersummen-Modell. Für die one-hot-Kodierung bietet keras die Funktion to_categorical, die in der Import-Section importiert wird.

5.2.0 Import Section, Problem-Parameter und Helfer-Funktionen

# Import Section, Problem Paramters and Helper Functions

import numpy as np
import random as rd
import matplotlib.pyplot as plt # Für plots verwenden wir die mathplotlib
from keras.models import Sequential
from keras.utils import to_categorical
from keras.layers import Dense
from keras.optimizers import sgd, adam # stochastic gradient descent or adam

n_pos = 4
nmin,nmax = 0,10000
n_cases = 200
prange = range(10) # Standard range for print output

# def pad_zeros(n_pos,s): -- wie in 5.1

# def to_string(X,n_pos): -- wie in 5.1

# def to_digits(X,n_pos): -- wie in 5.1

def to_class(y):
''' Round y to nearest integer
providing the predicted checksum as integer
Return 1darray of same structure as X_Train
'''
a = np.rint(y)
b = np.array([list(map(int,list(x))) for x in a])
b = b.ravel()
return b

5.2.1 Generierung von Datasets für Training und Test

Zufallszahlen X werden klassifizert nach Rest bei Division durch 3. Dabei wird y=0 gesetzt, wenn die Division aufgeht, y=1, wenn nicht. Anders als bei dem Quersummen-Teil-Modell, wo das Training mittels Zahl : Quersumme erfolgt, wird hier mit den Daten für die Klassifikationsaufgabe trainiert: Zahl : Klasse.

# Generating Datasets for multiples of 3 Learning

X = np.random.randint(nmin,nmax,size=(n_cases))
y = np.array(list(x%3 for x in X)) # Target-Klassifikationen
y = np.where(y>0,1,y) # Restklassen > 0 werden als y=1 klassifiziert

# Now convert numbers to digit vectors (just for illustration)
D = to_digits(X,n_pos)
print(X.shape,X[:10])
print(list((list(D[i]),y[i]) for i in prange))
# Set 25% test data set
n_train = 150
X_train = X[:n_train]
y_train = y[:n_train]
n_test = n_cases - n_train
X_test = X[n_train:]
y_test = y[n_train:]
['2766' '6203' '9414' '6356' '0336' '1656' '4874' '3310' '9598' '4840']
(200,) [2766 6203 9414 6356 336 1656 4874 3310 9598 4840]
[([2, 7, 6, 6], 0), ([6, 2, 0, 3], 1), ([9, 4, 1, 4], 0), ([6, 3, 5, 6], 1), ([0, 3, 3, 6], 0), ([1, 6, 5, 6], 0), ([4, 8, 7, 4], 1), ([3, 3, 1, 0], 1), ([9, 5, 9, 8], 1), ([4, 8, 4, 0], 1)]

5.2.2 Die Basis-Modellschicht für Quersummen wird erstellt und trainiert

Das Pre-Training des Quersummen-Modells wiederholen wir hier nicht. Alle Details dazu findet man in 5.1. Wir unterstellen, dass mit mqs das trainierte Quersummen-Modell exitiert.

5.2.3 Das NN Modell mit Transfer Learning

Das Bestimmen der Quersumme wurde schon durch das Modell mqs oben gelernt. Wir verwenden das als pre-trained model. Die prinzipielle Mehrstufigkeit der Quersummenbildung wird über mehrere Schichten (hier) des geichen Modells abgebildet. Die Modellschichten werden über direkte Output-Input-Beziehung verbunden, d.h. die Aktivierungsfunktion ist die Identität (activation = 'linear').

Die noch zu trainierende Modell-Schicht ist die, die lernt, eine einstellige Zahl als Vielfaches von 3 zu erkennen. Sie wird im Folgenden als msd3 definiert und compiliert.

# Model for the final task needs to be generated: single digit classification

msd3 = Sequential() # Model Single Digit 3 (msd3)

# Add dense layer which takes input as one 10-dim unit vector,
# which represents the one-hot coded output
# of the previous model step
msd3.add(Dense(1, activation='linear',use_bias=False,
input_shape=(10,)))
msd3.summary()

# Compile Model msd3
sgd1 = sgd(lr= 0.01) # sgd with learning rate
msd3.compile(optimizer=sgd1,loss='mean_squared_error',
metrics=['accuracy'])
msd3.weights
print(msd3.get_weights()) # Show initial weights
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_3 (Dense) (None, 1) 10
=================================================================
Total params: 10
Trainable params: 10
Non-trainable params: 0
_________________________________________________________________
[array([[-0.32700732],
[ 0.07160836],
[-0.5548037 ],
[ 0.5244499 ],
[ 0.04687846],
[-0.70941854],
[-0.06985933],
[ 0.6809017 ],
[ 0.63807553],
[-0.5876057 ]], dtype=float32)]

Das komplette Modell mit den pre-trained Quersummen-Schichten und der Schicht zur Bestimmung von einstelligen Vielfachen von 3.

Die Anzahl der Quersummen-Schichten ist ein Hyper-Parameter, d.h. gerade so viele, bis alle Quersummen nur noch einstellig sind. Man kann die Struktur auch als recurrent NN (RNN) sehen, wobei der Quersummen-Layer mehrfach durchlaufen wird, so oft wie nötig. Hier wird jeder Layer durch Aufruf der predict Methode des pre-trained Modells msq realisiert. Der Effekt ist der gleiche, wie wenn die Quersummen-Schichten mit msq.fit eingesetzt würden und dabei die (bereits trainierten) Gewichte von msq vom Training ausgenommen würden.

Da wie keine NN-Schicht für die Umwandlung von Zahlen in Ziffernfolgen zur Verfügung haben, ist die Verwendung von predict ohnehin die geeignete Möglichkeit, die Rekursion abzubilden.

# Sequence of mqs models written as loop 
# until deep checksum is reached
# Using predict of mqs is equivalent to include mqs
# in training with trainable=False

global mqs # Trained simple checksum model

max_loops = 4 # Task parameter specific (related to n_pos)
loops = 0
T = X_train
# We use a pre-defined number of checksum layers
while loops < max_loops:
loops +=1
T = to_digits(T,n_pos)
# to_digits prints out first 10 items in T
pred = mqs.predict(T) # Deploying pre-trained checksum model
T = to_class(pred) # 'Prediction' of mqs is a float
# as a result of weighted sums

print('Pre-trained checksum model depth:',loops)
print(T.shape,T[:10])

# Now connect last output T as input to msd3 and train msd3

T = to_categorical(T) # First convert to one-hot encoded input
print(T[:10])

ep = 100 # Set number of epochs
bs = 10 # Set bacth size
hist1 = msd3.fit(T,y_train,epochs=ep,batch_size=bs,verbose=0)

print([hist1.history['loss'][i] for i in range(0,ep,5)])
['2766' '6203' '9414' '6356' '0336' '1656' '4874' '3310' '9598' '4840']
['0021' '0011' '0018' '0020' '0012' '0018' '0023' '0007' '0031' '0016']
['0003' '0002' '0009' '0002' '0003' '0009' '0005' '0007' '0004' '0007']
['0003' '0002' '0009' '0002' '0003' '0009' '0005' '0007' '0004' '0007']
Pre-trained checksum model depth: 4
(150,) [3 2 9 2 3 9 5 7 4 7]
[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]
[0.9408645411332448, 0.640995450814565, 0.4393147389094035, 0.30285641153653464, 0.21012657831112544, 0.14683404713869094, 0.10346050995091598, 0.0735355194658041, 0.05275927285353343, 0.03822185794512431, 0.028005709437032542, 0.020695983059704305, 0.015466335291663805, 0.011680751371507843, 0.0089111994796743, 0.0068555643937240045, 0.005321688205003739, 0.004164334336140504, 0.0032840543387768167, 0.002602849012085547]

Die Ausgabe illustriert die Abfolge der Quersummenbildung für die ersten 10 Zahlen durch die mqs Layer. Zuviele mqs Schichten stören nicht (hier hätten evtl. auch 3 gereicht). Anschließend wird der final output des Quersummen-Stapels gezeigt (wieder für die ersten 10 Trainingszahlen) und die one-hot Codierung. Als letztes die Loss-Entwicklung aus der Training-History.

Konvergenz der Loss-Funktion tut sich zum Teil schwer, aber mit genügend Trainingsepochen sieht es gut aus. Batches scheinen besser als Gesamt-Set. Je kleiner die Batchgrösse, desto besser scheint die Konvergenz zu sein. Bei batch_size=2 reichen 50 Epochen oder weniger. Hier die Plots dazu:

# Plot development of Accuracy
# Plot mse, accuracy und die Entwicklung der Gewichte
plt.plot(hist1.history['acc'],'k-')
plt.show()
plt.plot(hist1.history['loss'],'g--')
plt.show()
Abb. 1: Accuracy-Ewicklung (batch_size = 2)
Abb. 2: Konvergenz der Loss-Funktion (batch_size = 2)

Die trainierten Gewichte entsprechen den erwarteten, theoretischen Werten:

# Anzeige der trainierten Gewichteprint(mqs.get_weights())
print(msd3.get_weights())
[array([[0.99999917],
[0.9999989 ],
[0.9999991 ],
[0.9999992 ]], dtype=float32)]
[array([[-0.32700732],
[ 0.8469427 ],
[ 0.9769042 ],
[ 0.00952307],
[ 0.92951787],
[ 0.96896285],
[-0.00516534],
[ 0.97640705],
[ 0.9901842 ],
[-0.00584184]], dtype=float32)]

Die Gewichte der Quersummen-Layer sind unverändert aus dem Pre-Training. Die trainierten Gewichte der letzten Schicht zeigen ein alternierendes Muster auf 3, d.h. nahe Null für 3, 6, 9 und nahe 1.0 für die anderen. Eine Ausnahme macht das Gewicht w_0: der Wert scheint irgendwie unbestimmt, zumindest nicht nahe Null. Den Grund findet man darin, dass es bei der Quersummen-bildung nie zu einem Endwert 0 kommen kann - außer für die Zahl 0 selbst. Daher "ignoriert" das ML-Modell dieses Gewicht, d.h. es ist dem Modell schlichtweg "egal".

Evaluation mit dem Test-Dataset und “Forward Propagation” der predict Ergebnisse.

# Prediction for test data and test targets

T = X_test

# Apply check sum layers
while not (T < 10).all():
loops +=1
T = to_digits(T,n_pos)
pred = mqs.predict(T)
T = to_class(pred)

# Apply single digit layer
T = to_categorical(T)
pred = msd3.predict(T)
y_pred = to_class(pred)

acc = 1.0 - np.sum(np.abs(y_test - y_pred))/n_test
print('Accuracy Testset:',acc)
for i in range(n_test):
print(X_test[i],y_pred[i],y_test[i])
['6069' '1528' '8789' '2964' '2399' '2228' '5688' '5169' '3602' '7030']
['0021' '0016' '0032' '0021' '0023' '0014' '0027' '0021' '0011' '0010']
['0003' '0007' '0005' '0003' '0005' '0005' '0009' '0003' '0002' '0001']
Accuracy Testset: 1.0
6069 0 0
1528 1 1
8789 1 1
2964 0 0
2399 1 1
2228 1 1
5688 0 0
5169 0 0
...
480 0 0
4901 1 1
8522 1 1
4696 1 1
7120 1 1

Es gelingt also perfekt, mit diesem NN Modell-Ansatz (RNN, Tansfer Learning) Vielfache von 3 zu klassifizieren.

3.2.4 Lernen, Vielfache von 9 klassifizieren

Unabhängigkeit vom Lernziel und Robustheit des Lernverfahrens lassen sich ebenfalls wieder leicht nachweisen. Wir wenden das gleiche Verfahren auf die Lernaufgabe der Klassifikation von Vielfachen von 9 an. Dazu sind lediglich die Trainingsdaten entsprechend anzupassen, insbesondere die Vorgabe-Klassifikation.

# Generating Datasets for multiples of 9

n_cases = 450
X = np.random.randint(nmin,nmax,size=(n_cases))

y = np.array(list(x%9 for x in X))
y = np.where(y>0,1,y) # Restklassen > 0 zu y=1
# We now have a dataset X that consists of the digits
# of the random numbers generated above
# The y's now hold the classifiaction:
# multiple of 9 (y=0) or not (y=1)
# Die Zuordnung der Klassifikation y könnte iterativ
# über Quersummenbildung gehen, oder auch per
# Modulo-Funktion. Da es hier nur auf die Erzeugung von
# Trainingsdaten ankommt, machen wir das
# per mod-9 Funktion
n_train = 400
X_train = X[:n_train]
y_train = y[:n_train]
n_test = n_cases - n_train
X_test = X[n_train:]
y_test = y[n_train:]
['2508' '1514' '5368' '2807' '1619' '9006' '3692' '4947' '6585' '1568']
(450,) [2508 1514 5368 2807 1619 9006 3692 4947 6585 1568]
[(array([2, 5, 0, 8]), 1), (array([1, 5, 1, 4]), 1), (array([5, 3, 6, 8]), 1), (array([2, 8, 0, 7]), 1), (array([1, 6, 1, 9]), 1), (array([9, 0, 0, 6]), 1), (array([3, 6, 9, 2]), 1), (array([4, 9, 4, 7]), 1), (array([6, 5, 8, 5]), 1), (array([1, 5, 6, 8]), 1)]

Damit wird der gleiche Trainings- und Evaluations-Ablauf wie oben durchgeführt.

Die Gewichte der Quersummen-Layer sind unverändert aus dem Pre-Training übernommen (Transfer Learning Prinzip). Die trainierten Gewichte der letzten Schicht zeigen einen Wert nahe Null für die 9 und nahe 1.0 für alle anderen Ziffern.

Und auch hier gelingt es perfekt, mit dem gleichen NN Modell-Ansatz Vielfache von 9 im Test-Set korrekt zu klassifizieren.

Weiter lesen: 5.3 Ein NN mit Transfer Learning lernt Vielfache von 7 erkennen

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