

Discover more from Tinz Twins Tech Blog
Ein künstliches neuronales Netz von Grund auf verstehen und implementieren
Ein umfassender Leitfaden für alle angehenden Data Scientists
Künstliche Intelligenz ist ein Hype-Thema. Doch was steckt hinter den leistungsstarken Netzen der großen Tech-Unternehmen? Ein künstliches neuronales Netz (engl. Artificial Neural Network, kurz: ANN) wird normalerweise mit Frameworks wie TensorFlow, Keras oder PyTorch implementiert. Solche Frameworks sind für sehr komplexe ANNs geeignet. Als Data Scientist ist es jedoch wichtig, die Grundlagen hinter solchen Frameworks im Detail zu verstehen.
Dieser Artikel hilft dir zu verstehen, wie ein neuronales Netzwerk funktioniert. Zunächst stellen wir die Grundlagen eines ANN im Allgemeinen vor. Dann gehen wir auf die grundlegenden Konzepte von ANNs im Detail ein. Danach erklären wir, wie man ein neuronales Netzwerk mit NumPy am Beispiel einer binären Klassifikation implementiert. Wir werden auch einige Experimente durchführen, damit du verstehst, wie ein neuronales Netz funktioniert.
Künstliche neuronale Netze im Allgemeinen
Ein künstliches neuronales Netz verwendet die Biologie als Modell. Ein solches Netz besteht aus künstlichen Neuronen (auch Knoten genannt) und Verbindungen (auch Kanten genannt) zwischen diesen Neuronen. Ein neuronales Netz hat eine oder mehrere versteckte Schichten (engl. hidden layer), wobei jede Schicht aus mehreren Neuronen besteht. Jedes Neuron in jeder Schicht empfängt die Ausgabe jedes Neurons in der vorherigen Schicht als Eingabe. Jede Eingabe in das Neuron ist gewichtet. Die folgende Abbildung zeigt ein sogenanntes neuronales Feed Forward Netzwerk. In einem solchen Netz sind die Verbindungen zwischen den Knoten azyklisch.
Die wichtigsten Begriffe sind fett und kursiv geschrieben und werden in diesem Artikel ausführlicher behandelt. Feedforward ist der Fluss der Eingabedaten durch das neuronale Netz von der Eingabeschicht (engl. input layer) zur Ausgabeschicht (engl. output layer). Beim Durchlaufen der einzelnen Schichten des Netzes werden die Eingabedaten an den Kanten gewichtet und durch eine Aktivierungsfunktion normalisiert. Die Gewichtung und die Aktivierungsfunktion sind Teil eines Neurons. Die berechneten Werte am Ausgang des Netzes haben einen Fehler im Vergleich zu den wahren Ergebniswerten. In unseren Trainingsdaten kennen wir die wahren Ergebniswerte. Die Rückführung der Fehler wird als Backpropagation bezeichnet, wobei die Fehler pro Knoten berechnet werden. Um die Fehlerterme zu minimieren, wird in der Regel die Methode des Gradientenabstiegs verwendet. Das Training des Netzes mit Backpropagation wird so lange wiederholt, bis das Netz den kleinstmöglichen Fehler aufweist.
Dann kann das trainierte neuronale Netz für eine Vorhersage auf neuen Daten (Testdaten) verwendet werden. Die Qualität der Vorhersage hängt von vielen Faktoren ab. Als Data Scientist musst du beim Training eines Netzes auf die Datenqualität achten.
Konzept eines Perzeptrons
Das Konzept eines Perceptrons wurde erstmals 1957 von Frank Rosenblatt vorgestellt. Ein Perzeptron besteht aus einem künstlichen Neuron mit einstellbaren Gewichten und einem Schwellenwert. Zum besseren Verständnis erklären wir die Funktionsweise eines Perzeptrons anhand der folgenden Abbildung.
Die Abbildung veranschaulicht eine Eingabeschicht und ein künstliches Neuron. Die Eingabeschicht enthält den Eingabewert und x_0 als Bias. In einem neuronalen Netz ist ein Bias erforderlich, um die Aktivierungsfunktion entweder zur positiven oder zur negativen Seite hin zu verschieben. Das Perzeptron hat Gewichte an den Kanten. Anschließend wird die gewichtete Summe aus Eingabewerten und Gewichten berechnet. Dies wird auch als Aggregation bezeichnet. Das Ergebnis a dient schließlich als Eingabe für die Aktivierungsfunktion. In diesem einfachen Beispiel wird die Stufenfunktion als Aktivierungsfunktion verwendet. Hier werden alle Werte von a > 0 auf 1 und Werte a < 0 auf -1 abgebildet. Es gibt verschiedene Aktivierungsfunktionen. Ohne eine Aktivierungsfunktion wäre das Ausgangssignal nur eine einfache lineare Funktion. Eine lineare Gleichung ist zwar leicht zu lösen, aber sie ist in ihrer Komplexität sehr begrenzt und hat eine viel geringere Fähigkeit, eine komplexe Funktionsabbildung aus Daten zu lernen. Die Leistung eines solchen Netzes wäre stark eingeschränkt und würde schlechte Ergebnisse liefern.
Feedforward
Feedforward ist der Fluss der Eingangsdaten durch das neuronale Netz von der Eingabeschicht bis zur Ausgabeschicht. Die folgende Abbildung skizziert den Ablauf:
In diesem Beispiel haben wir genau eine versteckte Schicht. Zunächst fließen die Daten x in die Eingabeschicht. x ist ein Vektor mit den einzelnen Datenpunkten. Die einzelnen Datenpunkte werden mit den Gewichten der Kanten gewichtet. Es gibt verschiedene Verfahren zur Initialisierung der Ausgangsgewichte, auf die wir in diesem Artikel allerdings nicht eingehen werden. Dieser Schritt wird als Aggregation bezeichnet. Mathematisch sieht dieser Schritt wie folgt aus:
Das Ergebnis dieses Schrittes dient als Eingabe für die Aktivierungsfunktion. Die Formel lautet:
Wir bezeichnen die Aktivierungsfunktion mit f. Das Ergebnis der Aktivierungsfunktion dient schließlich als Eingabe für das Neuron in der Ausgabeschicht.
Dieses Neuron führt wieder eine Aggregation durch. Die Formel lautet wie folgt:
Das Ergebnis ist wieder die Eingabe für die Aktivierungsfunktion. Die Formel lautet:
Wir bezeichnen die Ausgabe des Netzes mit y. y ist ein Vektor mit allen y_k. Die Aktivierungsfunktion in der versteckten Schicht und in der Ausgabeschicht muss nicht identisch sein. In der Praxis werden je nach Anwendungsfall unterschiedliche Aktivierungsfunktionen eingesetzt.
Gradientenabstieg Methode
Die Methode des Gradientenabstiegs minimiert die Fehlerterme. In diesem Zusammenhang wird die Fehlerfunktion abgeleitet, um ein Minimum zu finden. Die Fehlerfunktion berechnet den Fehler zwischen berechneten und wahren Ergebniswerten.
Zunächst muss die Richtung bestimmt werden, in der die Kurve der Fehlerfunktion am stärksten abfällt. Das ist die negative Steigung. Ein Gradient ist eine mehrdimensionale Ableitung einer Funktion. Dann gehen wir ein Stück in Richtung der negativen Steigung und aktualisieren die Gewichte. Die folgende Formel veranschaulicht dieses Verfahren:
Das umgekehrte Dreieck ist das Nabla-Zeichen und wird verwendet, um Ableitungen von Vektoren zu verdeutlichen. Die Methode des Gradientenabstiegs benötigt noch eine Lernrate (Eta-Zeichen) als Übergabeparameter. Die Lernrate gibt an, wie stark die Gewichte angepasst werden. E ist die Fehlerfunktion, die abgeleitet wird. Dieser ganze Prozess wird so lange wiederholt, bis es keine signifikante Verbesserung mehr gibt.
Backpropagation
Die Backpropagation lässt sich im Prinzip in drei Schritte unterteilen.
Schritt: Konstruktion der Fehlerfunktion
Schritt: Berechnung des Fehlerterms für jeden Knoten der Ausgangs- und der verborgenen Schicht
Schritt: Aktualisierung der Gewichte an den Kanten
Konstruktion der Fehlerfunktion
Die berechneten Werte am Ausgang des Netzes haben einen Fehler im Vergleich zu den wahren Ergebniswerten. Eine Fehlerfunktion dient zur Berechnung dieses Fehlers. Mathematisch kann der Fehler auf verschiedene Arten berechnet werden. Die Fehlerfunktion enthält das Ergebnis aus der Ausgabeschicht als Eingabe. Das bedeutet, dass die gesamte Berechnung des Feedforwards als Eingabe für die Fehlerfunktion verwendet wird. Das n der Funktion E steht für den n-ten Datensatz. m steht für die Anzahl der Ausgangsneuronen.
Berechnung des Fehlerterms für jeden Knoten der Ausgangs- und der versteckten Schicht
Die Fehlerterme sind in der Backpropagation-Abbildung orange markiert. Um die Fehlerterme für die Ausgabeschicht zu berechnen, müssen wir die Fehlerfunktion entsprechend den jeweiligen Gewichten ableiten. Hierfür verwenden wir die Kettenregel. Mathematisch sieht das so aus:
Um die Fehlerterme für die versteckte Schicht zu berechnen, leiten wir die Fehlerfunktion nach a_j ab.
Aktualisierung der Gewichte an den Kanten
Die neuen Gewichte zwischen der versteckten und der Ausgabeschicht können nun mit den jeweiligen Fehlertermen und einer Lernrate berechnet werden. Durch das Minus (-) gehen wir ein wenig in die Richtung des Abstiegs.
Das Dreieck ist der griechische Buchstabe delta. In der Mathematik und Informatik wird dieser Buchstabe verwendet, um die Differenz anzugeben. Wir können nun die Gewichte zwischen der Eingabe- und der versteckten Schicht aktualisieren. Die Formel sieht wie folgt aus:
Die Deltas werden für die Aktualisierung der Gewichte verwendet, wie in der Abbildung zu Beginn des Abschnitts Backpropagation dargestellt.
Implementierung eines künstlichen neuronalen Netzes mit NumPy
1. Schritt - Technische Voraussetzungen
Folgenden Voraussetzungen sind notwendig:
Zugang zu einen Terminal (macOS, Linux oder Windows).
Installiertes Python (≥ 3.7)
Einen Python-Paketmanager deiner Wahl wie conda
Code-Editor deiner Wahl (Wir verwenden VSCode.)
Einrichtung der Umgebung:
Erstelle eine conda Umgebung (env):
conda create -n neural-network python=3.9.12
-> Beantworte die Frage Proceed ([y]/n)? mit y.Aktiviere die conda-Umgebung:
conda activate neural-network
Installiere die erforderlichen Bibliotheken mit folgendem Terminal Befehl:
pip install matplotlib==3.7.1 numpy==1.24.2 plotly==5.14.1 pandas==2.0.0 scikit-learn==1.2.2
2. Schritt - Importe
Im zweiten Schritt importieren wir alle notwendigen Anforderungen. Für die Visualisierungen verwenden wir matplotlib und Plotly. In der letzten Zeile stellen wir die Abbildungsgröße der matplotlib-Plots ein.
import numpy as np
import sklearn
import sklearn.datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, auc
from sklearn.metrics import accuracy_score
import pandas as pd
import plotly.express as px
import time
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib
matplotlib.rcParams['figure.figsize'] = (5.0, 4.0)
3. Schritt - Generieren eines Beispiel-Datensatzes
Wir verwenden den einfachen Spielzeugdatensatz sklearn.datasets.make_moons
. Weitere Informationen zu diesem Datensatz findest du auf der sklearn-Website.
Die Funktion hat die folgenden Parameter:
n_samples
: Gesamtzahl der erzeugten Punktenoise
: Standardabweichung des zu den Daten hinzugefügten Gaußschen Rauschensrandom_state
: Seed für eine reproduzierbare Ausgabe
n = 1000
data_seed = 4242 # choose seed value
X, y = sklearn.datasets.make_moons(
n_samples = n,
noise = 0.,
random_state = data_seed)
plt.scatter(
x = X[:,0],
y = X[:,1],
s = 40, # marker size
c = y)
Scatter-Plot:
Wir sehen zwei Halbkreise.
4. Schritt - Aufteilung des Datensatzes
In diesem Schritt verwenden wir die Funktion train_test_split()
des Pakets sklearn, um einen Trainings- und Testdatensatz mit Labels zu erstellen.
Die Funktion hat die folgenden Parameter:
X
: Der Mond-Datensatzy
: Die Labelstest_size
: Größe des Testdatensatzes in Prozentrandom_size
: Seed für reproduzierbare Ausgabe
test_size = 0.25
split_seed = 42 # choose split seed value
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size = test_size,
random_state = split_seed)
5. Schritt - Definition von Hilfsfunktionen
Aktivierungsfunktion: Die sigmoid Aktivierungsfunktion hat das offene Intervall (0,1) als Wertebereich. Außerdem ist die Sigmoidfunktion eine begrenzte und differenzierbare reelle Funktion. Die Formel lautet:
Die Ableitung ist:
Die Funktion sigmoid(z, derivation=FALSE)
berechnet für einen Wert x
das Ergebnis für Sigmoid oder die Ableitung von Sigmoid.
Kostenfunktion / Fehlerfunktion:
In der Funktion calculate_loss()
wird die quadratische Fehlerfunktion berechnet. Die Formel lautet:
n
ist die Anzahl der Klassen.
Vorhersagefunktion:
In der Vorhersagefunktion führen wir eine Feed-Forward Propagation durch.
# activation function (sigmod)
def sigmoid(z, derivation = False):
if derivation:
return sigmoid(z)*(1-sigmoid(z))
else:
return 1/(1+np.exp(-z))
# cost function (Mean squared error function)
def calculate_loss(model, X, y):
# extract model parameters
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# calculation of the estimated class probabilities
## calc hidden layer
a1 = X.dot(W1) + b1
h1 = sigmoid(a1)
## calc output layer
a2 = h1.dot(W2) + b2
probs = sigmoid(a2)[:,0]
# calculation of the cost function value
cost = np.power(y-probs,2)
cost = np.sum(cost)/2
return cost
# predict function
def predict(
model,
x,
proba = False,
decision_point = 0.5):
# extract model parameters
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# calculation of the estimated class probabilities (Forward Propagation)
## calc hidden layer
a1 = x.dot(W1) + b1
h1 = sigmoid(a1)
## calc output layer
a2 = h1.dot(W2) + b2
probs = sigmoid(a2)
if(proba):
return probs
return (probs>decision_point)*1
6. Schritt - Modeling
Jetzt können wir unser neuronales Netz designen. Wir erstellen ein Netz mit nur einer versteckten Schicht. Die Funktion build_neural_network()
erhält als Eingabevariablen unsere Trainingsdaten X_train
, die Labels y_train
, die Anzahl der Gradientenabstiegsiterationen, die learning_rate
, einen Seed zur Reproduzierbarkeit, die Anzahl der versteckten Knoten und einen Wert zur Initialisierung der Gewichte.
Wir haben die Form des Arrays immer als Kommentar hinter die einzelnen Codezeilen geschrieben, damit du es besser nachvollziehen kannst. Bitte gehe den Code im Detail durch, um zu verstehen, wie er funktioniert.
def build_neural_network(
X, # features
y, # target
iterations = 20000, # number of gradient descent iterations
learning_rate = 0.1, # learning rate of Gradient descent
random_state = None, # random seed of weights
hidden_nodes = 5, # number of hidden nodes
rand_range = 0.05): # initialise the weights
observations = X.shape[0] # number of observations
features = X.shape[1] # number of features
# initialise the parameters to random values:
np.random.seed(random_state)
W1 = np.random.uniform(low = -rand_range,
high = rand_range,
size= (features,hidden_nodes)) # (2,10)
b1 = np.zeros((1,hidden_nodes)) # (1,10)
W2 = np.random.uniform(low = -rand_range,
high = rand_range,
size= (hidden_nodes,1)) # (10, 1)
b2 = np.zeros((1,1)) # (1,1)
# this is what we return at the end
model = {}
# Gradient descent:
for i in range(0, iterations):
# Forward propagation
## calc hidden layer
a1 = X.dot(W1) + b1 # X: (750, 2), W1: (2,10), a1: (750, 10)
h1 = sigmoid(a1) # z1: (750, 10)
## calc output layer
a2 = h1.dot(W2) + b2 # z1: (750, 10), W2: (10, 1), a2: (750, 1)
probs = sigmoid(a2) # probs: (750, 1)
# Backpropagation
# y.reshape: (750,1), probs: (750, 1), delta1: (750, 1)
delta1 = (probs-y.reshape((observations,1))) * probs * (1 - probs) # g(a_k)*(1-g(a_k))*(g(a_k)-t_k^n)
dW2 = np.dot(h1.T, delta1) # z1.T: (10, 750), delta1: (750,1), dW2: (10,1)
db2 = np.sum(delta1, axis=0, keepdims=True) # delta1: (750,1), db2: (1, 1)
delta_j = delta1 * W2.T * h1 * (1 - h1) # delta1: (750,1), W2.T: (1,10), z1: (750,10), delta_j: (750,10)
dW1 = np.dot(X.T, delta_j) # X.T: (2, 750), delta_j: (750,10), dW1: (750,1)
db1 = np.sum(delta_j, axis=0) # delta_j: (750,10), db1: (10,1)
# Gradient descent parameter update
W1 += -learning_rate * dW1 # dW1: (750,1)
b1 += -learning_rate * db1 # db1: (10,1)
W2 += -learning_rate * dW2
b2 += -learning_rate * db2
# assign new parameters to the model
model = { 'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
if i % 100 == 0:
print("Loss after iteration %7d:\t%f" %(i, calculate_loss(model,X,y)))
return model
# Train the neural network
model = build_neural_network(X = X_train,
y = y_train,
iterations = 20000,
random_state = 4242,
hidden_nodes = 5,
learning_rate = 0.1,
rand_range = 0.1)
7. Schritt - Evaluation
Classification Report:
from sklearn.metrics import classification_report
predictions = predict(model, X_test)
print(classification_report(y_test, predictions))
# Output
# precision recall f1-score support
# 0 0.95 0.94 0.94 118
# 1 0.95 0.95 0.95 132
# accuracy 0.95 250
# macro avg 0.95 0.95 0.95 250
# weighted avg 0.95 0.95 0.95 250
Das trainierte neuronale Netz hat einen gewichteten durchschnittlichen Recall und eine Precision von 0,95.
ROC-Kurve mit dem Test Datensatz:
Die ROC-Kurve stellt den Trade-off zwischen der Rate der richtigen positiven Ergebnisse (y-Achse) und der Rate der falschen positiven Ergebnisse (x-Achse) dar. Ein zufälliger Klassifikator liegt auf der Winkelhalbierenden. Ein schlechter Klassifikator liegt unterhalb dieser Linie. Ein Klassifikator wird umso besser, je weiter er von der Winkelhalbierenden nach oben abweicht. Ein weiteres Kriterium für die ROC-Kurve ist der Wert für die Fläche unter der ROC-Kurve (AUC), wobei die Fläche unter der Kurve berechnet wird. Ein guter Klassifikator hat eine Fläche > 0,5.
Unser neuronales Netz hat einen AUC Score von 0,9879 erreicht. Ein sehr guter Wert.
Experimente
Standardeinstellungen für jedes Experiment:
iterations
: 10000hidden_nodes
: 5learning_rate
: 0.1
model = build_neural_network(X = X_train,
y = y_train,
iterations = 10000,
random_state = 4242,
hidden_nodes = 5,
learning_rate = 0.1,
rand_range = 0.1)
1. Änderung der Iterationen der Gradientenabstieg Methode
Im ersten Experiment ändern wir die Anzahl der Iterationen des Gradientenabstiegs. So bestimmen wir, wie oft der Fehler zurückgeführt werden soll. In Tabelle 1 siehst du die Ergebnisse für mehrere Durchläufe. Wir haben die folgenden Metriken für den Trainings- und Testdatensatz gemessen.
Es ist zu erkennen, dass der AUC-Wert und die Genauigkeit mit zunehmender Iterationszahl steigen. Außerdem nimmt die Fehlklassifizierungsrate mit zunehmender Anzahl von Iterationen stark ab. Mit zunehmender Anzahl von Iterationen werden die Gewichte an den Kanten des neuronalen Netzes mehr und mehr aktualisiert. Das Netz lernt durch die Aktualisierung der Gewichte die Strukturen in den Trainingsdaten. Wir sehen aber auch, dass sich die Leistung des Netzes ab 1000 Iterationen nur noch geringfügig verbessert. Je mehr Iterationen wir machen, desto länger dauert es, das Netz zu trainieren.
2. Änderung der Anzahl der versteckten Knoten
Im zweiten Experiment ändern wir die Anzahl der Knoten in der versteckten Schicht. Die Ergebnisse des Experiments kannst du in Tabelle 2 sehen.
Wir sehen, dass das Netz auch mit einer kleinen Anzahl von Neuronen gute Ergebnisse liefert. Die erforderliche Anzahl von Neuronen hängt von der Komplexität der Daten ab. Für komplexere Probleme braucht man mehr Schichten mit mehr Neuronen. Je mehr Neuronen wir verwenden, desto länger dauert es, das Netz zu trainieren.
3. Änderung der Lernrate
Im dritten Experiment ändern wir die Lernrate. Die Lernrate gibt an, wie stark die Gewichte angepasst werden.
Wir sehen, dass die Lernrate einen großen Einfluss auf die Leistung des Netzes haben kann. Mit einer Lernrate von 0,5 schnitt das Netz deutlich schlechter ab. Eine hohe Lernrate führt zu schlechten Ergebnissen, weil das neuronale Netz nicht das optimale Minimum findet. Die optimale Lernrate zu finden, ist eine Kunst. Außerdem bestimmt die Lernrate auch die Dauer des Trainingsprozesses.
Fazit
In diesem Artikel hast du erfahren, wie ein künstliches neuronales Netz funktioniert. Wir haben uns die zentralen Konzepte im Detail angesehen. Feedforward beschreibt den Fluss der Eingangsdaten durch das neuronale Netz. Backpropagation wird verwendet, um die Fehlerterme pro Neuron mit Hilfe der Gradientenabstiegsmethode zu berechnen. Diese Konzepte bilden auch die Grundlage für komplexere Netzarchitekturen. Darüber hinaus hast du gelernt, wie man ein künstliches neuronales Netz von Grund auf mit NumPy implementiert. In diesem Zusammenhang haben wir uns die Implementierung im Detail angesehen. Wir haben auch drei Experimente durchgeführt, um zu sehen, was passiert, wenn wir die Parameter während des Trainings ändern.
👉🏽 Du findest alle digitalen Produkte von uns in unserem Online Shop! Schaue gerne mal vorbei.
Dir gefällt unserer Content und wir konnten dir weiterhelfen? Dann unterstütze uns doch, indem du unsere Spendenoption auf Buy me a coffee nutzt oder unsere Artikel mit anderen teilst. Vergesse auch nicht, uns auf YouTube zu folgen. Vielen Dank für deine Unterstützung! 🙏🏽🙏🏽
Erfahre mehr über uns auf unserer About-Seite. Du kannst unseren Tech Blog auch gerne weiterempfehlen. Nutze hierfür einfach unser Empfehlungsprogramm und sichere dir Vorteile. Vielen Dank fürs Lesen.
Referenzen
KUBAT, Miroslav. An introduction to machine learning. Cham, Switzerland: Springer International Publishing, 2021.*
Hastie, Trevor, et al, The elements of statistical learning: data mining, inference, and prediction (2009). Vol. 2. New York: Springer.*
Schwaiger, R. and Steinwendner, J., 2020. Neuronale Netze programmieren mit Python. Rheinwerk Computing.*
*Affiliate-Link / Anzeige: Die Links sind Affiliate-Links, d.h. wir erhalten eine Provision, wenn du über diese Links einkaufst. Es entstehen keine zusätzlichen Kosten für dich.