16 Minuten Lesezeit (3133 Worte)

KI & Mensch: Warum sollte Intelligenz vor der Tastatur sitzen?

Ich beginne mit einem Zitat von Andriy Burkov aus seinem Buch "The Hundred-Page Machine Learning Book":

"Machines don't learn. What a typical "learning machine" does, is finding a mathematical formula, which, when applied to a collection of inputs (called "training data"), produces the desired outputs. This mathematical formula also generates the correct outputs for most other inputs (distinct from the training data) on the condition that those inputs come from the same or a similar statistical distribution as the one the training data was drawn from." [Burkov, 2019, S. xvii]

Was das bedeutet, werde ich im Rahmen dieses Artikels anhand eines einfachen Beispiels zeigen.

Ein Anwendungsfall für KI ist das Lesen von handgeschriebenen Texten. Ein Mensch lernt das Lesen üblicherweise im ersten Schuljahr. Er beginnt damit, einzelne Buchstaben und Ziffern zu lesen und im Laufe des Jahres lernt er das Lesen ganzer Wörter und Zahlen. Nach einem Jahr kann er es noch nicht perfekt, aber gut genug, um einfache Texte zu lesen und zu verstehen.

Auch Maschinen kann das Lesen beigebracht werden. Um den Rahmen das Artikels nicht zu sprengen, wird das Problem vereinfacht. Statt ganzer Texte, werden im folgenden Beispiel nur einzelne handgeschriebene Ziffern gelesen. Dazu werde ich ein einfaches Modell trainieren und zeigen, dass es gute Ergebnisse liefert, wenn die Eingabedaten den Trainingsdaten ähneln. Anschließend werde ich zeigen, wie unzuverlässig das Ergebnis ist, wenn die tatsächlichen Eingaben deutlich von den Daten abweichen, mit denen das Modell trainiert wurde.

An dieser Stelle kommt der Mensch ins Spiel. Bereits beim Erstellen des Modells ist er gefordert, mögliche Ausnahmen und Abweichungen vorherzusehen und zu berücksichtigen. Durch Fachwissen, Kreativität und Intelligenz kann er Systeme entwickeln, die sich dann scheinbar intelligent verhalten.

Für die Beispiele wird das MNIST-Dataset [MNIST] verwendet. Dieses enthält Bilder der handgeschriebenen Ziffern 0-9. Die hier gezeigten Modelle werden in Python mit Keras und TensorFlow 2 erstellt. Für das Verständnis des Artikels ist der Code nicht zwingend notwendig, er kann beim Lesen übersprungen werden. 

Das MNIST-Dataset

Die MNIST-Daten wurden ursprünglich vom National Institute of Standards and Technology (NIST) veröffentlicht. Das MNIST-Dataset ist eine modifizierte Teilmenge dieser Daten, die oft für Beispiele und Übungen verwendet wird. Die Keras API enthält eine Funktion, um dieses "Toy"-Dataset [Keras-Datasets] zu laden.

from tensorflow import keras

# MNIST Trainings- und Testdaten laden
# x_train bzw. x_test enthalten die Bilder (Features)
# y_train bzw. y_test enthalten die erwarteten Ziffern (Label)
mnist = keras.datasets.mnist
(X_train_raw, y_train_raw), (X_test_raw, y_test_raw) = mnist.load_data()
# Datentypen und Dimensionen ausgeben

print('Variable     |  Dimension      | Kommentar')         
print('-------------+-----------------+--------------------------')
print(f'X_train_raw  | {X_train_raw.shape} | Bilder der Trainingsdaten')
print(f'X_test_raw   | {X_test_raw.shape} | Bilder der Testdaten')
print(f'y_train_raw  | {y_train_raw.shape}        | Label der Trainingsdaten')
print(f'y_test_raw   | {y_test_raw.shape}        | Label der Testdaten') 

Variable     | Dimension       | Kommentar
-------------+-----------------+--------------------------
X_train_raw  | (60000, 28, 28) | Bilder der Trainingsdaten
X_test_raw   | (10000, 28, 28) | Bilder der Testdaten
y_train_raw  | (60000,)        | Label der Trainingsdaten
y_test_raw   | (10000,)        | Label der Testdaten


Das Dataset besteht aus 60.000 Bildern für das Training und weiteren 10.000 Bildern für den Test. Die Bilder selbst haben eine Größe von 28 * 28 Pixeln und 256 Graustufen. Zusätzlich enthält das Dataset die erwarten Werte 0-9 (Label) für die Trainings- und Testdaten. Mit dem folgenden Code werden einige der Bilder zusammen mit dem erwarteten Wert (Label) angezeigt.

import matplotlib.pyplot as plt
import math

# Funktion zur Ausgabe der ersten n Bilder (images)
# Zusätzlich wird die erwartete Ziffer (labels)
# und falls vorhanden, die vorhergesagte Ziffer (predictions) ausgegeben
def plot_samples(n, images, labels, predictions=None, fig_title=None):
    cols = 10
    rows = math.ceil(n / cols)
    fig, ax = plt.subplots(rows, cols, figsize=(30, 8))

    col = 0
    row = 0
    for i in range(0, n):
        ax[row][col].axis('off')
        ax[row][col].imshow(images[i], cmap=plt.cm.gray)

        title = str(labels[i])
        title_color = 'black'
        if predictions is not None:
            title = title + " / " + str(predictions[i])
            if labels[i] != predictions[i]:
                title_color = 'red'

        ax[row][col].set_title(title, color=title_color, fontsize=36)

        col += 1
        if cols <= col:
            col = 0
            row += 1
    fig.suptitle(fig_title, fontsize=38)
    fig.tight_layout()             
plot_samples(20, X_test_raw, y_test_raw) 

Daten normalisieren

Bevor mit TensorFlow und der Keras-API ein Modell erstellt wird, werden die Daten in ein geeignetes Format gebracht werden. Die einzelnen Bilder sind aktuell zweidimensionale Arrays (Matrizen). Für das Modell werden diese in eindimensionale Arrays (Vektoren) umgewandelt. Zusätzlich wird der Wertebereich der Pixel (Features) auf den Bereich von 0 bis 1 normalisiert.

# Wichtige Konstanten
# Anzahl der verschiedenen Ziffern (Klassen)
CLASSES = 10
# Anzahl der Features (Pixel) je Bild
PIXEL=28*28 
# Funktion zum Normalisieren der Daten
# reshape wandelt das 28 x 28 große zweidimensionale Array in ein eindimensionales Array mit 784 Elementen um
# anschließend werden die Werte durch 255 geteilt, um den Wertebereich auf Werte von 0 bis 1 zu begrenzen
def normalize_X(X_in):
    num_samples = X_in.shape[0]
    num_features = X_in.shape[1] * X_in.shape[2]
    
    X_out = X_in.reshape(num_samples, PIXEL)
    X_out = X_out.astype('float32')
    X_out /= 255
    return X_out 
# Trainings- und Testdaten normalisieren
X_train = normalize_X(X_train_raw)
X_test = normalize_X(X_test_raw) 

Als Ergebnis werden die Ziffern 0 bis 9 erwartet. Auch die erwarteten Werte (Label) werden konvertiert. Dazu wird das One-Hot-Encoding verwendet. Dabei wird für jeden der möglichen 10 Werte eine einzelne Spalte angelegt. Eine dieser Spalten enthält dann immer eine 1, während alle anderen eine 0 enthalten.

import tensorflow as tf

# Funktion für die One-Hot-Kodierung der Label
def one_hot_y(y_in):
    y_out = tf.keras.utils.to_categorical(y_in, CLASSES)
    return y_out 
# One-hot representation of the labels.
y_train = one_hot_y(y_train_raw)
y_test = one_hot_y(y_test_raw) 

Zur Verdeutlichung wird das Bild der Ziffer 7, der Wert des Labels im Original und die Werte nach der On-Hot-Kodierung ausgegeben.

print(f'Original Label:   {y_test_raw[0]}')
print(f'One-Hot-Encoding: {y_test[0]}')
plt.axis('off')
plt.imshow(X_test_raw[0], cmap=plt.cm.gray) 

Original Label: 7
One-Hot-Encoding: [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]

Modell erstellen und trainieren

Als nächstes wird mit der Keras API ein Modell erstellt und mit den Trainingsdaten trainiert. Die Qualität des Modells wird mit den Testdaten ermittelt. Als Kennzahl wird hier die Genauigkeit (Accuracy) verwendet. Das ist der Prozentsatz der Datensätze, die vom trainierten Modell richtig erkannt (klassifiziert) wurden.

# Funktion zum Erstellen und Kompilieren des Modells
def build_model():
    mdl = tf.keras.models.Sequential()
    mdl.add(keras.layers.Dense(CLASSES,
                               input_shape=(PIXEL,),
                               name='dense_layer',
                               activation='softmax'))
    mdl.compile(optimizer='SGD',
                loss='categorical_crossentropy',
                metrics=['accuracy'])
    return mdl 
model1 = build_model() 
# Funktion zum Trainieren des Modells
def train_model(mdl, X, y):
    EPOCHS = 50
    BATCH_SIZE = 128
    VERBOSE = 1
    
    # Training the model.
    mdl.fit(X, y,
            batch_size=BATCH_SIZE,
            epochs=EPOCHS,
            verbose=VERBOSE) 
train_model(model1, X_train, y_train) 

Train on 60000 samples
Epoch 1/50
60000/60000 [==============================] - 1s 12us/sample - loss: 1.2746 - accuracy: 0.6949
Epoch 2/50
60000/60000 [==============================] - 1s 12us/sample - loss: 0.7139 - accuracy: 0.8406
...
Epoch 50/50
60000/60000 [==============================] - 1s 14us/sample - loss: 0.3093 - accuracy: 0.9140

# Qualität des Modells überprüfen
_, m1_acc = model1.evaluate(X_test, y_test)
print(f'Genauigkeit Testdaten: {m1_acc}') 

10000/10000 [==============================] - 0s 28us/sample - loss: 0.2987 - accuracy: 0.9171
Genauigkeit Testdaten: 0.9171000123023987

Das Modell hat eine Genauigkeit von ca. 92%. Für eine zuverlässige Handschrifterkennung ist dieser Wert noch zu gering. Für den geringen Aufwand, der bis jetzt in die Entwicklung des Modells gesteckt wurde, ist dies bereits ein sehr guter Wert.

Durch Optimierungen an den Daten (Feature-Engineering), am Modell und an den verwendeten Parametern (Hyperparameter-Optimierung) kann dieser Wert noch deutlich gesteigert werden. Auf der Keras-Homepage gibt es ein Beispiel, das mit dem MNIST-Dataset eine Genauigkeit von ca. 99% erreicht [MNIST-99%]. Um mit einem Modell einen so guten Wert zu erreichen, wird dann wieder der Mensch mit seiner Intelligenz und Kreativität benötigt.

In diesem Artikel geht es nicht darum, das bestmögliche Ergebnis zu erzielen! Vielmehr soll gezeigt werden, wie sich die Ergebnisse verändern, wenn ein Modell mit unerwarteten Daten konfrontiert wird.

Bilder klassifizieren

Mit dem folgenden Code werden einige Bilder klassifiziert. Die erwarteten und die vorhergesagten Werte werden zusammen mit dem Bild ausgegeben. Bei einer falschen Klassifikation wird der Text in Rot ausgegeben.

import numpy as np

# Funktion zur Vorhersage von Werten
def predict(model, X_values, n):
    X_in = X_values[0:n]
    P_out = np.empty(n, dtype = int)
    predictions = model.predict(X_in)
    for i in range(0, n):
        P_out[i] = int(np.argmax(predictions[i]))
    return P_out 
# Anzahl der Beispiele
n = 20
# Vorhersage
P_test = predict(model1, X_test, n)
# Ausgabe der Ziffern mit Vorhersage und erwartetem Ergebnis
plot_samples(n, X_test_raw, y_test_raw, P_test, fig_title="Modell 1: Label / Vorhersage") 

Für diese Beispieldaten wurde die Ziffer 5 einmal nicht korrekt erkannt. Hier hat das Modell eine 6 statt einer 5 vorhergesagt. Ein Fehler bei 20 Vorhersagen entspricht einer Genauigkeit von 95% und war bei der zuvor berechneten Genauigkeit von ca. 92% zu erwarten.

Abweichende Daten

Was passiert jetzt, wenn sich die Eingabedaten auf unerwartete Weise ändern?

Wie würde zum Beispiel ein Schüler der zweiten Klasse reagieren, wenn er einen Text lesen soll, der auf dem Kopf steht? Vermutlich dauert es einen Moment, aber einfache Texte wird er sicherlich nach kurzer Zeit lesen können. Auf keinen Fall wird er ein weiteres Jahr zur Schule gehen müssen, um es zu lernen.

Aber wie reagiert das Modell, wenn es mit umgedrehten Bildern konfrontiert wird?

Bilder um 180-Grad drehen

Für das Experiment werden die Bilder der Testdaten gedreht und normalisiert. Die gedrehten Bilder werden dann verwendet, um mit dem ersten Modell die Genauigkeit zu berechnen und einige Bilder zu klassifizieren. Zur Erinnerung: Das Modell wurde nur mit den nicht gedrehten Bildern trainiert.

# Bilder um 180 Grad drehen
n =  X_test_raw.shape[0]
X_test_raw_180 = np.empty(X_test_raw.shape)
for i in range(0, n):
    X_test_raw_180[i] = np.rot90(X_test_raw[i], 2) 
# Daten normalisieren
X_test_180 = normalize_X(X_test_raw_180)
# Genauigkeit mit dem alten Modell berechnen
m1_loss_180, m1_acc_180 = model1.evaluate(X_test_180, y_test)
print('Genauigkeit original:', m1_acc)
print('Genauigkeit gedreht :', m1_acc_180) 

10000/10000 [==============================] - 0s 20us/sample - loss: 5.1361 - accuracy: 0.1948

Genauigkeit original: 0.9165
Genauigkeit gedreht : 0.1948

Während das Modell bei den Originalbildern noch eine Genauigkeit von ca. 92% hatte, liegt die Genauigkeit für die gedrehten Bilder nur noch bei ca. 20%.
# Vorhersage für einige gedrehte Bilder berechnen und anzeigen
P_test_180 = predict(model1, X_test_180, 20)
plot_samples(20, X_test_raw_180, y_test_raw, P_test_180, fig_title="Modell 1: Label / Vorhersage (gedreht)") 

Anhand des Plots der Zahlen sind einige Vorhersagen des Modells leicht nachvollziehbar. Bei der 6 und bei der 9 ist es auch für einen Menschen schwer, den richtigen Wert zu erkennen. Hier fällt aber auf, dass eine 6 als 3 und die 9 zweimal als 4 klassifiziert wurde.

Bei der 0 und der 1 ist es eigentlich egal, ob Sie gedreht werden. Eine 1 wird vom Modell aber als 8 klassifiziert.

Die Ergebnisse für diese 1 werden jetzt genauer untersucht.

TensorFlow berechnet für jede mögliche Klasse (0-9) die Wahrscheinlichkeit, dass das Bild dieser Klasse entspricht. Diese Werte werden als nächstes für das originale und das gedrehte Bild der 1 ausgegeben.

# Plot Image und Wahrscheinlichkeiten für die 10 verschiedenen Klasen (0-9)
def plot_images_and_predictions(images, predictions, titles):
    n = len(predictions)
    fig, ax = plt.subplots(2, n, figsize=(3 * n , 3 * 2))
    fig.suptitle("Wahrscheinlichkeiten der Klassen 0-9\nLabel=1", fontsize=16)
    for i in range(n):
        ax[0][i].imshow(images[i], cmap=plt.cm.gray)
        ax[0][i].set_title(titles[i], fontsize=16)
        ax[0][i].set_xticks([])
        ax[0][i].set_yticks([])
        ax[1][i].bar(range(10), predictions[i])
        ax[1][i].set_xticks(range(10))
        ax[1][i].set_ylim(0, 1)
        ax[1][i].set_ylim(0, 1)
        ax[1][i].grid(axis='y', linestyle=':') 
SAMPLE_IMAGE = 14
images = np.array([X_test_raw[SAMPLE_IMAGE], np.rot90(X_test_raw[SAMPLE_IMAGE], 2)])

images_norm = normalize_X(images)
predictions = model1.predict(images_norm)

titles = np.array(['original', 'gedreht'])
plot_images_and_predictions(images, predictions, titles) 

Beim originalen Bild ist das Modell zu fast 100% sicher, dass es eine 1 ist. Beim gedrehten Bild, sind die Werte nicht so eindeutig. Hier hat das Modell für die 8 eine Wahrscheinlichkeit von ca. 60% berechnet, während die 1 auf ca. 30% kommt.

Im direkten Vergleich ist bei den Bildern kein großer Unterschied erkennbar. Insbesondere haben die Bilder keine Ähnlichkeit mit einer 8.

Warum sieht die Maschine das anders?

Um diese Frage zu beantworten, werden zuerst einige Trainingsbilder der 8 ausgegeben.

image_ids = np.array([17, 31, 41, 6816, 8785, 16282, 18884, 19541, 19948, 22320, 25018, 25418, 31800, 32018, 34328, 34758, 41218, 44806, 49088, 47317])
row=0
col=0
fig, ax = plt.subplots(2, 10, figsize=(30, 8))
for i in range(0, image_ids.size):
    ax[row][col].imshow(X_train_raw[image_ids[i]], cmap=plt.cm.gray)
    ax[row][col].set_title("id=" + str(image_ids[i]), fontsize=24)
    col += 1
    if 10 <= col:
        col = 0
        row += 1
fig.suptitle("Trainingsdaten mit Label 8", fontsize=30) 

Text(0.5, 0.98, 'Trainingsdaten mit Label 8')

Einige dieser Bilder haben eine starke Ähnlichkeit mit der 1 aus dem obigen Beispiel.

Die genauen Ursachen für die falsche Klassifizierung sind komplex und deren detaillierte Analyse würde den Rahmen dieses Artikels sprengen.

Eine stark vereinfachte Erklärung ist, dass das Modell beim Training Beispiele für die verschiedenen Ziffern auswendig gelernt hat. Die einzelnen Trainingsbilder enthalten aber keine Information darüber, wie "gut" oder "schlecht" eine abgebildete Ziffer ist. Beim Klassifizieren der gedrehten, "schlechten" 1 erkennt das Modell jetzt eine Ähnlichkeit mit der 8 und berechnet für die 8 eine höhere Wahrscheinlichkeit als für die 1.

An dieser Stelle ist der Mensch gefordert, die Ergebnisse zu interpretieren und das Problem zu lösen.

Training mit gedrehten Bildern

Das Modell zusätzlich mit den gedrehten Bildern zu trainieren, ist eine mögliche Lösung. Mit dem folgenden Code werden die Daten entsprechend vorbereitet und es wird ein zweites Modell erstellt und trainiert.

# Trainingsdaten um 180 Grad drehen und normalisieren
n =  X_train_raw.shape[0]
X_train_raw_180 = np.empty(X_train_raw.shape)
for i in range(0, n):
    X_train_raw_180[i] = np.rot90(X_train_raw[i], 2)
X_train_180 = normalize_X(X_train_raw_180) 
# Neue Arrays mit original und gedrehten Bildern erstellen
X2_train = np.concatenate((X_train, X_train_180), axis=0)
X2_test = np.concatenate((X_test, X_test_180), axis=0)

# Neue Arrays mit den Labeln erstellen
# Die Label sind für die gedrehten Bilder identisch
y2_train = np.concatenate((y_train, y_train), axis=0)
y2_test = np.concatenate((y_test, y_test), axis=0)

# 2. Modell erstellen
model2 = build_model() 
# 2. Modell mit den originalen und den gedrehten Bildern trainieren
train_model(model2, X2_train, y2_train) 

Train on 120000 samples
Epoch 1/50
120000/120000 [==============================] - 1s 12us/sample - loss: 1.3864 - accuracy: 0.6104
Epoch 2/50
120000/120000 [==============================] - 1s 12us/sample - loss: 0.9902 - accuracy: 0.7213
...
Epoch 50/50
120000/120000 [==============================] - 1s 11us/sample - loss: 0.6452 - accuracy: 0.8144


Für das erste und das zweite Modell wird jetzt die Genauigkeit ermittelt. In beiden Fällen werden die gedrehten Bilder zusammen mit den originalen Bildern zur Berechnung verwendet.

# Genauigkeit berechnen
_, m1_orig_180_acc = model1.evaluate(X2_test, y2_test)
print('Genauigkeit Modell 1: ', m1_orig_180_acc)
print()
_, m2_orig_180_acc = model2.evaluate(X2_test, y2_test)
print('Genauigkeit Modell 2: ', m2_orig_180_acc) 

20000/20000 [==============================] - 0s 24us/sample - loss: 2.6978 - accuracy: 0.5566

Genauigkeit Modell 1: 0.55655

20000/20000 [==============================] - 0s 24us/sample - loss: 0.6193 - accuracy: 0.8183
Genauigkeit Modell 2: 0.8183

Mit dem zweiten Modell werden jetzt einige gedrehte und nicht gedrehte Bilder klassifiziert und ausgegeben.
# Vorhersage von gedrehten Bildern berechnen und anzeigen
P2_test_180 = predict(model2, X_test_180, 20)
plot_samples(20, X_test_raw_180, y_test_raw, P2_test_180, fig_title="Modell 2: Label / Vorhersage (gedreht)") 
# Vorhersage von nicht gedrehten Bildern berechnen und anzeigen
P2_test = predict(model2, X_test, 20)
plot_samples(20, X_test_raw, y_test_raw, P2_test, fig_title="Modell 2: Label / Vorhersage (original)") 

Das erste Modell erreicht eine Genauigkeit von ca. 56%, wenn sowohl die originalen als auch die gedrehten Bilder berücksichtigt werden. Die Ergebnisse des zweiten Modells sind mit ca. 82% deutlich besser. Im Vergleich mit dem ersten Modell und den nicht gedrehten Bildern ist die Genauigkeit aber um ca. 10% schlechter geworden.

Die verbesserte Genauigkeit spiegelt sich auch in den Ergebnissen bei der Klassifizierung wider. Mit dem zweiten Modell wird die gedrehte 1 auch als 1 klassifiziert. Weiterhin schafft es das neue Modell, die gedrehten Neunen richtig zu erkennen. Von den insgesamt 40 Beispielen wurden 34 richtig erkannt, was 85% entspricht.

Das Modell zusätzlich mit gedrehten Bildern zu trainieren, ist nur eine mögliche Lösung. Es gibt noch viele weitere Optionen:

  • Es werden zwei Modelle trainiert. Eines mit gedrehten und eines mit nicht gedrehten Ziffern. Das bessere Ergebnis wird als Vorhersage verwendet.
  • Es wird ein Modell erstellt, dass die Orientierung der Ziffern erkennt. Gedrehte Bilder werden dann richtig gedreht und an das erste Modell (für nicht gedrehte Bilder) übergeben.
  • Das Neuronale Netz wird um weitere Schichten (Layer) erweitert, so dass es bessere Vorhersagen liefert.
  • Es werden die einzelnen Wahrscheinlichkeiten gegen einen Schwellwert geprüft. Wenn dieser nicht erreicht wird, dann gibt es keine Vorhersage, sondern eine Fehlermeldung.


Hier sind der Kreativität kaum Grenzen gesetzt. Das weitere Vorgehen hängt stark vom konkreten Anwendungsfall ab. Wenn nur einzelne Ziffern erkannt werden sollen, dann ist es sehr schwer, die Orientierung zu ermitteln. Bei längeren Ziffernfolgen oder ganzen Texten ist dies, unter der Voraussetzung, dass alle Zeichen dieselbe Orientierung haben, deutlich einfacher.

Fazit

Anhand des MNIST-Datasets wurde gezeigt, dass es sehr einfach ist ein Modell zu trainieren, das in der Lage ist, handgeschriebene Ziffern zu erkennen.

Es wurde aber auch deutlich, dass das Modell für Daten, die von den Trainingsdaten stark abweichen, sehr viele falsche Ergebnisse liefert. Das Problem aus diesem Artikel ist etwas konstruiert, kommt in der Praxis in vergleichbarer Form aber häufig vor. Ein konkretes Beispiel ist die KI in einem Auto, die auf ein neu eingeführtes Verkehrsschild reagieren muss.

Der Mensch wird bei weiteren Fortschritten beim Maschinellen Lernen ein wichtiger Faktor bleiben. Nur durch die Kombination von menschlicher und künstlicher Intelligenz können zuverlässige KI-Systeme entwickelt werden.

Sprechen Sie uns an, wenn Sie für Ihr KI-Projekt auf der Suche nach menschlicher Intelligenz sind. Wir können Ihnen weiterhelfen.

ki-und-mensch
156 kb
{loadmoduleid 179}
hat noch keine Informationen über sich angegeben
 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Dienstag, 23. April 2024

Sicherheitscode (Captcha)

×
Informiert bleiben!

Bei Updates im Blog, informieren wir per E-Mail.

Weitere Artikel in der Kategorie