Bernd Thomas
Oct 14 · 9 min read

3.2 Ein erfolgreiches NN für ein Teilproblem der Gerade/Ungerade Klassifikationsaufgabe

wikiwand.com

Wir haben gesehen (in 3.1), dass das Herumraten nach einer geeigneten NN Architektur “top down” nicht klappt. Wir brauchen offenbar mehr Einsicht in das Problem. Daher versuchen wir nun ein einfacheres NN eine etwas einfachere Teil-Aufgabe lernen zu lassen: Die “Einer” einer Zahl zu identifizieren. Vielleicht lässt sich ja das “gerade/ungerade” Lernen aus zwei Komponenten zusammensetzen: Die relevante Stelle (hier: Einer) einer Zahl zu identifizieren und dort zwischen 2, 4, 6, 8, 0 und den anderen Ziffern zu unterscheiden. So zunächst die Hoffnung. Da es jeweils um das Erkennen von Mustern geht, sollte das für NN-Verfahren prinzipiell möglich sein. Immerhin können NN's (bzw. CNN's) Katzen- und Hundebilder unterscheiden lernen - warum nicht auch die einstelligen geraden und ungeraden Zahlen?

Dies würde der statistischen Methode in Teil 2 entsprechen, wo der Chi-Squared Test a) Auffälligkeiten in der Verteilung der Dezimalziffern in den einzelnen Stellen erkennt und b) “trennbare” Verteilungen der Ziffern in der Einer-Stelle als signifikant (p<0.05) befindet.

In diesem Abschnitt werden wir zunächst ein NN entwicklen, dass das erste Lern-Teilproblem lösen kann: Das Erkennen einer bestimmten Stelle einer Zahl.

Problem 1: Die Einer-Stelle einer Zahl (Ziffernfolge) erkennen

Die Lern-Aufgabe kann wie folgt definiert werden: Dem Modell soll wie in 3.1 ein Trainingsset mit 4-stelligen Zahlen vorgelegt werden. Die “richtige Antwort” ist jeweils die Ziffer an der Einer-Stelle. Damit haben wir eine Aufgabe des supervised learning, aber anders als in 3.1. ist das target nicht binär (0 oder 1).

Zum besseren Verständnis des Modells verwenden wir hier nicht eines der Learning-Pakete sondern entwickeln das Modell “von Hand”:

Algorithmus: Ein einfaches NN mit Input-Layer und fully connected 1-Neuron Output Layer

Input Layer: 4 Zeichen, x_0, ..., x_3 (Einer)

  • Gewichte: w = (w_0,...,w_3), ohne bias, Startwerte w_k = 1.0
  • Net input: z = w*x (dot-Produkt):
  • Activation: relu bzw. linear (Identität): a = phi(z) = z (relu: a = 0, wenn z <= 0)
  • Output: y = a
  • Loss: mean square error (mse) err = sum_i((y_i - z_i)**2)/N
  • Gradient des mse für w: deriv err_k = 2/N * (sum_i((y_i - z_i)*x_ik) für k = 0,1,2,3
  • Schrittweiten-Faktor (Learning Rate): eta = 0.01 (nicht adaptiv)
  • Korrektur: delta_w = eta * grad

Dieses NN ist ein schlichtes einschichtiges lineares Perzeptron.

Generieren der Daten: Zunächst werden genau wie in 3.1 die Data-Sets und die Vorgabe-Werte generiert und die Daten dann in Training- und Test-Set aufgeteilt. Die Vorgabe-Werte (targets) sind jetzt die Ziffern der Einer-Stelle (y = x_3). Die one-hot-Codierung von y wird nicht benötigt, da wir nicht nach Kategorien klassifizieren.

'''Wir erzeugen Datasets und die Kategorisierung
als numpy arrays: Ein 1-dimensionales Z mit
Zufallszahlen zwischen 0 und 9999.
y-Werte sind die Ziffern an der Einer-Stelle'''
import numpy as np
import random as rd
import matplotlib.pyplot as plt
nmin,nmax = 0,10000
n_cases = 200
N = n_cases
n_pos = 4 # Anzahl Stellen, beliebig wählbar
Z = np.random.randint(nmin,nmax,size=(n_cases,1))
y = Z[:] % 10 # Hier "isolieren" wir die Einer-Stelle aus Z
print(' Ein Beispiel: Zahl: ',Z[10,0],' Zielwert: ',y[10,0])# Weiter wie in 3.1 ....''' Wir erzeugen die 4-Zeichen Strings zu den Zahlen in Z
inklusive Padding vorlaufender Nullen '''
...'''Erzeugen eines Input Arrays aus den Zeichenketten in D
Shape ist z.B. (200,4) d.h. 200 Inputs je 4 digits '''
...''' Wir verzichten hier auf die One-Hot Codierung für y, da wir den numerischen Werte von y anpassen'''''' Aufteilen der Daten X in ein Trainingset (80%)
und ein Testset (20%). X enthält die Ziffern der
Zahlen als Vektor.
Zufallsauswahl mit gleichverteilten Kategorien'''
...Ein Beispiel: Zahl: 5275 Zielwert: 5
Die ersten 10 Trainingdaten:
[('1975', 5), ('9546', 6), ('1672', 2), ('8137', 7), ('7367', 7), ('0696', 6), ('7806', 6), ('0360', 0), ('9844', 4), ('5998', 8)]
(200, 4)

Die folgende Code-Zelle ist für die unten erwähnten Experimente zur Robustheit des Verfahrens gedacht. Hiermit werden ggf. Fehler in die Trainingsdaten eingeführt. Das Code-Stück kann übersprungen werden, oder mit dem Default-Aufruf von data_error()durchlaufen werden, wenn keine Fehler eingeführt werden sollen.

# Ggf. Einfügen von Klassifikationsfehlern. 
# Keine Fehler: q_err = 0.0.
# Fehlertyp: Verrauschen err_type = -1 (default)
# Verwechseln der Stelle err_type = err_pos (z.B. =2 für die
# Zehnerstelle)
def data_error(q_err = 0.0,err_type = -1):
'''Einführen von Datenfehlern in Trainingset: Verrauschen bzw
Verwechseln '''

global X_train, y_train # "in-place" Änderung

err_pos = err_type if err_type >= 0 else None
_n_cases = X_train.shape[0]
_n_err = int(q_err * _n_cases) # Anzahl der Fehler
# Zufallsauswahl der Fälle mit Fehlklassifikation
sel = np.random.choice(_n_cases,_n_err)
if err_type < 0: # Default: -1
y_train[sel,0] = np.random.randint(10,size=_n_err)
# Verrauschen der Klassifikation
else:
for i in sel: # Verwechseln von Stellen
y_train[i,0] = X_train[i,err_pos]

return _n_err
# Fehlereinführung: Anwendung auf X_train, y_train.
n_err = data_error() # Default: keine Daten-Fehler
# Beispiel-Situationen (auskommentiert)
# n_err = data_error(q_err = 0.1)
# Verrauschen mit Fehlerquote 0.1
# n_err = data_error(q_err = 0.1,err_type = 2)
# Verwechseln mit Pos 2 (Zehner) mit Fehlerquote 0.1
print('Fehler in Trainingsdaten:',n_err)Fehler in Trainingsdaten: 0

Das Modell gestaltet sich sehr übersichtlich. Wir definieren wieder die Schritte:

  • Input
  • Net Input und Aktivierung
  • Output
  • Fehlerberechnung
  • Fehler-Backpropagation

und dazu die fit-Methode model_fit und den Predictor model_predict.

''' Problem- und Start-Settings '''n_pos = 4        # Anzahl Stellen, beliebig wählbar
n_epochs = 100 # Anzahl der Durchläufe mit dem vollen Trainingsset
''' Modell-Komponenten '''def net_input(X,w):
return X.dot(w) # Input gewichtet mit w liefert den "Net Input"
def phi(z): # Aktivierungsfunktion. Hier relu oder Identität
return z
def err(y_true,y_calc): # Berechnung der Fehlerfunktion (Loss)
return ((y_true-y_calc)**2).mean() # Hier: Mean Square Error
def err_gradient(X,w,y_true,y_calc): # Gradient bzgl. w
return 2.0/X.shape[0]* (y_true[:,0]-y_calc).dot(X)
# w selbst kommt nicht explizit vor. Lineares Modell!
def model_fit(X,y,n_epochs): # Fit-Funktion mit Ausgabe
# Iteration über n_epochs
# Alternativ: Abbruch bei Konvergenz
global n_pos # Problem-Parameter 'Anzahl Stellen'

w_start = np.ones(n_pos) # Initialisierung der Gewichte
w = w_start

for ep in range(n_epochs): # Loop über Epochen
print(ep,'--------------')

z = net_input(X,w) # Berechnung net input
y_pred = phi(z) # Berechnung Output y_pred

mse = err(y,y_pred) # Berechnung des Fehlers
print('Mean Square Error:',mse)

grad = err_gradient(X,w,y,y_pred) # Berechnung ErrorGradient
print ('mse gradient:',grad)

eta = 0.01 # Learning coefficient eta, ggf adaptiv
w = w + eta*grad # Korrektur Gewichtsvektor w
print('New weights:',w)

return w # fit liefert finalen Gewichtsvektor
def model_fit_and_plot(X,y,n_epochs): # Fit-Funktion (mit plot)
global n_pos # Problem-Parameter 'Anzahl Stellen'

w_start = np.ones(n_pos) # Initialisierung der Gewichte
w = w_start

mse_list = list() # Listen für Plot
w_list = list()

for ep in range(n_epochs):

z = net_input(X,w) # Berechnung net input
y_pred = phi(z) # Berechnung Output y_pred

mse = err(y,y_pred) # Berechnung des Fehlers
mse_list.append(mse)

grad = err_gradient(X,w,y,y_pred) # Berechnung ErrorGradient

eta = 0.01 # Learning coefficient
w = w + eta*grad # Korrektur Gewichtsvektor w
w_list.append([w[k] for k in range(n_pos)])

# Plot mse und die Entwicklung der Gewichte
plt.plot(mse_list,'k-')
plt.show()
g = ['b--','m--','g--','r-']
for k in range(n_pos):
plt.plot([w_list[i][k] for i in range(n_epochs)],g[k])
plt.show()

return w # Fit liefert finalen Gewichtsvektor
def model_predict(X,y,w): # Prediction für Test-Set

precision = 0.01 # Alt. als Parameter
score = 0
z = net_input(X,w) # Schritte bei model_fit
y_pred = phi(z)
y_true = y[:,0]

score = (np.abs(y_true - y_pred) < precision).sum()

# Print Output Einzelergebnisse
for i in range(X.shape[0]):
print('%4d' % i,X[i],y_true[i],'%7.3f' % y_pred[i])

return float(score)/X.shape[0]

Training

Das Modell kann prinzipiell mit Teilmengen (Batches) des Trainingsets trainiert werden (s. “stochastic gradient”, “mini batches”, “full training set”). Dazu müsste die simple Iteration ergänzt werden durch eine Zufallsauswahl und eine Loop über Batches. Da sich zeigt, dass wir bei der vorliegenden Lernaufgabe keinerlei Probleme mit der Konvergenz haben, sind diese Erweiterungen nicht implementiert. Es wir das Gesamt-Trainingset über alle “Epochen” verwendet.

Das Model wird mit den Trainingsdaten und model_fit() trainiert. Die fit Funktion gibt es hier in zwei Varianten: eine mit print-Output, eine mit entsprechenden Grafiken.

In der plot-Version zeigen die Kurven das Konvergenz-Verhalten: oben das des Mean Square Errors, unten das der Gewichte. Dabei in rot die Entwicklung des Gewichts für die Einerstelle, in grün, magenta, blau die der anderen Stellen.

Offenbar wird die Konvergenz schnell erreicht. Dargestellt sind 100 “Epochen” (Durchläufe mit allen Trainingsdaten).

# Training: Fit model - with plots of mse and weightsw_fit = model_fit_and_plot(X_train,y_train,n_epochs)             
Training Konvergenzüber 100 Epochen. Links: Loss (mse). Rechts: Gewichte

Die Plots zeigen zum einen den raschen Abstieg der Fehlerfunktion (oben), zum anderen die (alternierende) Konvergenz des “Einer-Gewichtes” (w_3, rot) gegen 1.0 und der anderen Gewichte (andere Farben) gegen Null.

Das Konvergenzverhalten hängt u.a. auch von der Learning Rate ab, die wir hier konstant auf eta = 0.01 belassen, die aber durchaus von Epoche zu Epoche angepasst werden könnte, um die Konvergenz noch zu verbessern.

Prediction Tests

Das trainierte Modell wird mit Testdatensätzen überprüft. Der score gibt die relative Anzahl der korrekten Predictions an. Genauer: die relative Anzahl der Prediction-Werte, die um weniger als der precision Parameter vom wahren Wert abweichen.

# Single Prediction
test_input = np.array([1,2,3,6])
y_pred = test_input.dot(w_fit)
print('Prediction test for',test_input,' :',y_pred)
# Run Test-Set to check quality of fit
print()
print('Prediction für Test-Set:')
pred_score = model_predict(X_test,y_test,w_fit)
print('\nPrediction Score:',pred_score)Prediction test for [1 2 3 6] : 5.999999999998648Prediction für Test-Set:
0 [7 6 7 1] 1 1.000
1 [2 8 3 2] 2 2.000
2 [6 0 7 6] 6 6.000
3 [5 1 2 8] 8 8.000
4 [5 0 7 5] 5 5.000
5 [7 6 3 2] 2 2.000
6 [9 4 3 4] 4 4.000
7 [3 6 5 4] 4 4.000
8 [2 0 9 7] 7 7.000
9 [2 0 1 8] 8 8.000
10 [1 4 6 0] 0 -0.000
...

36 [3 6 8 8] 8 8.000
37 [7 0 5 3] 3 3.000
38 [6 2 1 5] 5 5.000
39 [6 8 0 0] 0 0.000
Prediction Score: 1.0

Das Ergebnis zeigt die Nr des Testfalls, die Testzahl in Ziffern (Beispiel: [8 3 0 3] entspricht 8303), den wahren Wert (d.i. die Einer-Stelle) und den vorhergesagten Wert, der als eine float-Zahl errechnet wird. Mit einer precision von 0.01 wird jede Vorhersage als richtig gewertet, die weniger als 0.01 abweicht. Hier ist der Score z.B. 1.0, das System hat also perfekt gelernt.

Während die Anzahl der Epochen und die Learning Rate eta die Qualität der Anpassung (fit) bestimmen, legt precision fest, wie genau die Vorhersage sein muss, um akzeptiert zu werden. Bei einem Training über nur 80 statt 100 Epochen bleibt eine gewisse Ungenauigkeit in den erlernten Gewichten, die bei einer geforderten precision von 0.01 nur etwa 67% der Testfälle richtig verhersagt.

Robustheit des Modells

Wie in Teil 2 kann man auch hier die Robustheit des Verfahrens experimentell bestimmen, indem man die Trainingsdaten “verrauscht”, d.h. ab und zu einen falschen Zielwert y in die Trainingsdaten einschleust.

Dazu wird nach der Datendefinition eine Funktion data_error(q_err= , err_type= ) auf das Trainingset X_train, y_train angewendet. q_err setzt die Fehlerqoute, err_type setzt die Fehlerart auf "Verrauschen" (=0) oder "Verwechseln der Position" (z.B. =2 für Zehnerstellen).

Ergebnisse der Robustheitsexperimente

Verrauschen: Werden die Klassifikationsdaten im Training zu nur 10% zufällig verfälscht, konvergieren die Gewichte zwar weiterhin sehr schnell, erreichen aber nicht die Zielwerte des ungestörten Falls. Das Testset liefert nur noch einen Score von 0.025 bei dem gesetzten precision level von 0.01. Gerundet auf ganze Zahlen stimmen die vorhergesagten Klassifikationen jedoch weitgehend mit den wahren Werten (des Testsets) überein. Auch bei einer Error-Quote von 5% liefert das Testset nur einen Score von 0.05, bei 1% Fehler liegt der Score bei 0.4.

Das Verfahren erscheint also recht sensibel gegen Verfälschungen in der Klassifikation der Trainingsdaten.

Stellen-Verwechslung: Bei 10% der Trainingsdaten wurde die Klassifikation anstelle der Einer-Stelle durch die Zehner-Stelle ersetzt. Das Verfahren konvergiert rasch und es zeigt sich, dass die Gewichte für die Einerstelle und die Zehnerstelle nur wenig von den “ungestörten” Werten abweichen: w3 = 0.85 (Einer) und w2 = 0.14 (Zehner). Das ist zwar zu erwarten, aber die “Superposition” doch überraschend.

D.h. das Verfahren erkennt die Beiträge der verschiedenen Stellen auch in einem “Gemisch” von Trainingsdaten — analog einer Frequenzanalyse.

Der Test-Score — der ja auf die Einer-Stelle ausgerichtet ist — liefert entprechend schlechtere Werte. Bei einer Error-Rate von 0.05 liegt der Score bei 0.075, bei einer Rate von 0.01 bei 0.1.

Bei allen Beispielen kann der Test-Score variieren, da das konkrete Ergebnis immer auch vom zufällig erzeugten Trainings-Dataset abhängt.

Unabhängigkeit vom Lernziel

Wie in Teil 2 für starkes Lernen gefordert, ist das NN unabhängig vom Lernziel, d.h. das Lernziel (Einer-Stelle Finden) ist nicht im Verfahren versteckt implementiert. Gibt man in den Testdaten die Ziffern einer anderen Stelle als target vor, wird diese andere Stelle “erlernt”.

Weiter lesen: 3.3 Das zweite Teilproblem: Gerade-/Ungerade-Klassifikation einstelliger Zahlen

Zurück auf Anfang

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

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