Beispielprojekt: Bildklassifizierung mit CNNs
Ein bedeutendes Feld der künstlichen Intelligenz (KI) und des maschinelles Lernens (ML) ist Computer Vision, ein Teilgebiet, das sich unter anderem mit der Bildklassifizierung auseinander setzt. Bildklassifizierung spielt eine zentrale Rolle in verschiedenen Anwendungen wie z.B. der Gesichtserkennung in Kameras oder in der medizinischen Bildanalyse. Dort wird häufig eine spezielle Form von Algorithmen aus dem Deep Learning genutzt, nämlich Convolutional Neural Networks (CNNs). In diesem Blogpost werden wir solch eine Bildklassifizierung mit CNNs durchführen. Wir bauen dazu ein CNN auf, welches zwischen verschiedenen Mustertypen unterscheiden soll. Dabei verwenden wir Python als Programmiersprache und TensorFlow zusammen mit Keras als Bibliotheken, um das Modell zu trainieren und anzuwenden.
Einführung in das Projekt
In diesem Projekt möchten wir eine Bildklassifizierung mit CNNs durchführen. Dazu werden wir ein CNN aufbauen und trainieren, um es später auf neue Daten anzuwenden. Die Bilder repräsentieren drei verschiedene Muster-Klassen, entweder ein Schachbrettmuster, horizontale Linien oder vertikale Linien. Eine Herausforderung bei ML-Projekten liegt oft in der Beschaffung von Trainingsdaten. In unserem Fall lösen wir dieses Problem etwas anders: Anstatt Bilder selbst zu fotografieren oder aus dem Internet zu scrapen, erstellen wir sie mit einem generativen Modell. Wir nutzen dazu die OpenAI DALL-E 2 API, mit der wir Bilder basierend auf Textprompts erstellen können. Nachdem das Modell mit den generierten Trainingsdaten trainiert wurde, verwenden wir es für Vorhersagen auf neu generierten Bildern.
Im Folgenden gehen wir Schritt für Schritt durch das Projekt, inklusive Python-Code.
Importe
Ich nehme die Importe der Bibliotheken vorweg, sodass sie im Skript oben stehen. Die Kernbibliotheken sind openai
für die OpenAI-API, tensorflow
und keras
für das Erstellen des CNN, PIL
für Bildbearbeitung, matplotlib.pyplot
für das Erstellen von Graphen und ein paar weitere wie z.B. numpy
und io
.
Außerdem definieren wir einige Konstanten, darunter die Auflösung der Bilder, der Farbmodus (wir verwenden der Einfachheit halber nur Graustufen), sowie die drei Kategorien.
from base64 import b64decode from PIL import Image from uuid import uuid4 from pathlib import Path from typing import Tuple import openai import tensorflow as tf from keras.utils.image_utils import img_to_array, load_img, save_img from keras import layers, models from keras.preprocessing.image import ImageDataGenerator from keras.callbacks import EarlyStopping import matplotlib.pyplot as plt import io import random import numpy as np # Konstanten im gesamten Projekt IMG_HEIGHT = 256 IMG_WIDTH = 256 IMG_DIM = (IMG_WIDTH, IMG_HEIGHT) IMG_DALLE_API_STR = str(IMG_WIDTH) + "x" + str(IMG_HEIGHT) COLOR_MODE = "grayscale" CLASSES = ["checkers", "horizontal", "vertical"]
Datenerstellung mit DALL-E 2
Die Generierung unserer Trainingsdaten wird mithilfe der DALL-E 2 API von OpenAI durchgeführt. DALL-E 2 ist ein generatives Modell, welches Bilder basierend auf Textprompts erstellt. In unserem Fall nutzen wir das, um drei verschiedene Musterklassen zu erzeugen: „Schachbrettmuster“, „horizontale Linien“ und „vertikale Linien“. Für jede dieser Klassen generieren wir 100 Bilder.
⚠️ Achtung: Das Nutzen der API ist mit Kosten verbunden. Ein Bild mit 256×256 Pixeln kostet $0.016. Vielleicht passt du die Konstante N_IMAGES_PER_TYPE
an.
Zuerst setzen wir unseren API-Key, um Zugang zur DALL-E 2 API zu haben. Dann definieren wir unsere Prompts und die Anzahl der Bilder, die wir für jede Klasse generieren möchten.
# API-Key setzen openai.api_key = "<MEIN API KEY>" # Prompts und Anzahl der Prompts PROMPTS = [ "a chess or checkers pattern", "a pattern with horizontal lines", "a pattern with vertical lines", ] N_IMAGES_PER_TYPE = 100
Der nächste Teil des Codes ist die Funktion generate_image
, welche die DALL-E 2 API nutzt, um ein Bild basierend auf einem gegebenen Prompt zu erstellen:
def generate_image(prompt: str, folder: str) -> None: response = openai.Image.create( prompt=prompt, n=1, size=IMG_DALLE_API_STR, response_format="b64_json", ) json_data = response["data"][0]["b64_json"] image_bytes = b64decode(json_data) file_name = str(uuid4()) + ".png" png_path = Path.cwd() / "responses" / folder / file_name with open(png_path, mode="wb") as png: png.write(image_bytes)
Die Funktion generate_image
erstellt einen Request an die API mit Prompt und der gewünschten Bildgröße und speichert das resultierende Bild dann in einem bestimmten Ordner. Die Bilder werden als PNG-Dateien gespeichert, wobei jeder Dateiname eine einzigartige UUID enthält, um Überschreibungen zu vermeiden.
Die Funktionen nutzen wir nun und iterieren durch die Muster-Klassen. Für jede Klasse erstellen wir mit generate_image
Bilder:
for prompt, folder in zip(PROMPTS, CLASSES): path_responses = Path.cwd() / "responses" / folder path_responses.mkdir(parents=True, exist_ok=True) for _ in range(N_IMAGES_PER_TYPE): generate_image(prompt, folder)
Nach diesem Abschnitt haben wir 3×100 Bilder, welche in den drei entsprechenden Ordnern als PNG abgespeichert sind.
Hier ein Beispiel für generierte Bilder der ersten Kategorie „checkers“:

Daten augmentieren
Insgesamt sind 300 Bilder noch etwas wenig, um ein CNN zu trainieren. Also erweitern wir den Datensatz, indem wir die Daten augmentieren. Das bedeutet: Wir ändern bestehende Bilddaten etwas ab und speichern sie, sodass wir die Vielfalt der Daten erhöhen und somit ein robusteres Modell erzeugen können. Wir verwenden folgende Transformationen: Drehung, Spiegelung und Helligkeitsanpassung.
# Rotation def rotate_image( image: Image, base_file_name: str, save_path: Path, angles: Tuple[int] ) -> None: for angle in angles: rotated_img = image.rotate(angle) new_file_name = f"{base_file_name}_rotated_{angle}.png" rotated_img.save(save_path / new_file_name)
In der rotate_image
-Funktion wird das Eingabebild um eine Liste von gegebenen Winkeln gedreht und das gedrehte Bild dann unter einem neuen Dateinamen gespeichert.
# Spiegelung def flip_image( image: Image, base_file_name: str, save_path: Path, direction: str ) -> None: if direction == "horizontal": flipped_img = image.transpose(Image.FLIP_LEFT_RIGHT) elif direction == "vertical": flipped_img = image.transpose(Image.FLIP_TOP_BOTTOM) else: return new_file_name = f"{base_file_name}_{direction}_flip.png" flipped_img.save(save_path / new_file_name)
Die flip_image
-Funktion spiegelt das Eingabebild entweder horizontal oder vertikal und speichert das gespiegelte Bild.
# Helligkeit def adjust_brightness( image: Image, base_file_name: str, save_path: Path, brightness_range: Tuple[float] ) -> None: for brightness_factor in brightness_range: brightened_img = image.point(lambda p: p * brightness_factor) new_file_name = f"{base_file_name}_brightness_{brightness_factor}.png" brightened_img.save(save_path / new_file_name)
Die adjust_brightness
-Funktion passt die Helligkeit des Bildes an und speichert das modifizierte Bild.
Die Funktionen für die Transformationen sind definiert. Entsprechend wenden wir sie nun auf alle Bilder an:
# Parameter AUG_PARAMS = { "rotation_angles": [-10, -5, 5, 10], "brightness_values": [0.8, 1.2], } # Durch die drei Kategorie-Ordner iterieren for folder in CLASSES: path_responses = Path.cwd() / "responses" / folder path_augmented = Path.cwd() / "augmented" / folder path_augmented.mkdir(parents=True, exist_ok=True) # Durch Dateien iterieren for file_path in path_responses.glob("*.png"): base_file_name = file_path.stem img = Image.open(file_path) # Speichern der Originaldatei img.save(path_augmented / file_path.name) # Bild transformieren rotate_image(img, base_file_name, path_augmented, AUG_PARAMS["rotation_angles"]) flip_image(img, base_file_name, path_augmented, "horizontal") flip_image(img, base_file_name, path_augmented, "vertical") adjust_brightness( img, base_file_name, path_augmented, AUG_PARAMS["brightness_values"] )
Wir iterieren durch die Klassen, öffnen das Originalbild (welches wir sofort im „augmented“-Ordner speichern) und wenden die Transformationen an (welche auch im „augmented“-Ordner gespeichert werden). Nun haben wir statt 100 Bilder je Klasse 900, insgesamt also 2700 Bilder. Das ist noch nicht wahnsinnig viel, aber sollte für unser Beispiel ausreichen.
Hier ein Beispiel für verschiedene Varianten eines Bildes für die Klasse „horizontal“:

Preprocessing
Die Datengrundlage ist vorhanden. Wir bereiten die Bilder in diesem Schritt nun so vor, dass sie anschließend für das Modelltraining verwendet werden können. Dazu konvertieren wir jedes Bild in ein standardisiertes Format. Mit load_img
aus der Keras-Bibliothek laden wir jedes Bild. Dabei geben wir die Farbmodus und Zielgröße an. Anschließend konvertieren wir das geladene Bild in ein Numpy-Array mit der Funktion img_to_array
und speichern es mit save_img
.
# Vorbearbeitung eines Bildes def preprocess_image( file_path: Path, output_path: Path, color_mode: str, target_size: Tuple[int] ) -> None: img = load_img(file_path, color_mode=color_mode, target_size=target_size) img_array = img_to_array(img) save_img(output_path / file_path.name, img_array)
Die Funktion ist erstellt und wird nun angewendet:
# Bearbeitung je Ordner for folder in CLASSES: path_augmented = Path.cwd() / "augmented" / folder path_preprocessed = Path.cwd() / "preprocessed" / folder path_preprocessed.mkdir(parents=True, exist_ok=True) for file_path in path_augmented.glob("*.png"): preprocess_image(file_path, path_preprocessed, COLOR_MODE, IMG_DIM)
Wir iterieren wieder durch die Klassen. Als Grundpfad für die verarbeiteten Bilder wählen wir „preprocessed“. Die Zielgröße der Bilder bleibt gleich (256×256 Pixel) und als Farbmodus wählen wir Graustufen.

Nun können wir das CNN trainieren!
Modelltraining
Um eine Bildklassifizierung mit CNNs durchzuführen, brauchen wir ein entsprechendes Modell. In diesem Schritt erstellen wir unser Convolutional Neural Network. Es besteht aus mehreren Schichten (Layer), welche für CNNs typisch sind: Convolutional Layer, Pooling-Layer, Dropout-Layer, Fully Connected Layer. Da wir so wenige Daten haben und die generierten Bilder vermutlich sehr ähnlich sind, verwenden wir Early Stopping, um Overfitting zu vermeiden. Diese Logik stoppt das Training, wenn der Verlust (Loss) auf unseren Validierungsdaten nicht mehr reduziert wird.
Als erstes definieren wir ein paar Konstanten: den Basis-Pfad für die Daten, die Batch-Größe und die Anzahl der Epochen.
# Konstanten BASE_PATH = "preprocessed" BATCH_SIZE = 32 EPOCHS = 15
Im nächsten Schritt definieren wir eine Funktion, die das Modell erstellt:
# Modellerstellung def create_model() -> models.Sequential: model = models.Sequential( [ layers.Conv2D( 16, 3, padding="same", activation="relu", input_shape=(IMG_HEIGHT, IMG_WIDTH, 1), ), layers.MaxPooling2D(), layers.Conv2D(32, 3, padding="same", activation="relu"), layers.MaxPooling2D(), layers.Conv2D(64, 3, padding="same", activation="relu"), layers.Dropout(0.5), layers.MaxPooling2D(), layers.Flatten(), layers.Dense(128, activation="relu"), layers.Dense(3), ] ) model.compile( optimizer="adam", loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True), metrics=["accuracy"], ) return model
Unser Netzwerk besteht aus mehreren Convolutional und Max Pooling Layern. Der Dropout-Layer ist dazu da, Overfitting zu verhindern, indem es während des Trainings zufällig eine Anzahl von Neuronen „ausschaltet“. Die Flatten- und Dense-Layer am Ende unseres Modells sind dazu da, die extrahierten Muster zu klassifizieren.

Als nächstes definieren wir Trainings- und Validierungsdaten:
# Image Generator image_generator = ImageDataGenerator(rescale=1.0 / 255, validation_split=0.2) # Trainingsdaten train_data_gen = image_generator.flow_from_directory( batch_size=BATCH_SIZE, directory=BASE_PATH, shuffle=True, target_size=(IMG_HEIGHT, IMG_WIDTH), class_mode="categorical", color_mode=COLOR_MODE, subset="training", ) # Validierungsdaten val_data_gen = image_generator.flow_from_directory( batch_size=BATCH_SIZE, directory=BASE_PATH, shuffle=True, target_size=(IMG_HEIGHT, IMG_WIDTH), class_mode="categorical", color_mode=COLOR_MODE, subset="validation", )
Wir nutzen ImageDataGenerator
, welcher durch die Methode flow_from_directory
automatisch die Bilder aus unserem BASE_PATH einliest und für das Training unseres Modells vorbereitet. Wir skalieren die Pixelwerte auf einen Bereich zwischen 0 und 1 und teilen unsere Daten in einen Trainings- und einen Validierungssatz auf (mit 20% der Daten für Validierung).
Schließlich trainieren wir unser Modell:
# Early Stopping gegen Overfitting early_stopping = EarlyStopping(monitor="val_loss", patience=3) # Modelltraining model = create_model() history = model.fit( train_data_gen, steps_per_epoch=train_data_gen.samples // BATCH_SIZE, epochs=EPOCHS, validation_data=val_data_gen, validation_steps=val_data_gen.samples // BATCH_SIZE, callbacks=[early_stopping], )
Dabei geben wir unsere Trainings- und Validierungsdaten sowie die EarlyStopping-Callback-Funktion an. Die Funktion model.fit
gibt eine Trainingshistorie zurück, welche den Loss und die Accuracy (Genauigkeit) während des Trainings und der Validierung enthält. Dies kann später zur Analyse des Trainingsprozesses genutzt werden.
Wir haben nun ein trainiertes CNN-Modell! Dieses können wir lokal abspeichern, um es später weiter zu verwenden:
# Modell speichern model.save("cnn")
Modellperformance
Das Training ist durch und wir möchten sehen, wie die Performance so ist. Eine typische Methode ist die visuelle Darstellung des Losses und der Accuracy während des Trainings und der Validierung.
# Training visualisieren plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(history.history["loss"], label="Trainings-Loss") plt.plot(history.history["val_loss"], label="Validierungs-Loss") plt.legend() plt.subplot(1, 2, 2) plt.plot(history.history["accuracy"], label="Trainings-Accuracy") plt.plot(history.history["val_accuracy"], label="Validierungs-Accuracy") plt.legend() plt.show()
Wir nutzen die Bibliothek matplotlib
für die visuelle Darstellung von Loss und Accuracy. Zuerst plotten wir den Trainings- und Validierungs-Loss. Im zweiten Chart plotten wir die Trainings- und Validierungs-Accuracy. Grundsätzlich strebt man einen geringen Loss und eine hohe Accuracy an. Wichtig ist aber vor allem der Blick auf die Validierungs-Metriken, da die Validierungsdaten nicht für das Modelltraining genutzt wurden. Ein extrem kleiner Loss auf den Trainingsdaten lässt Overfitting vermuten – meistens ist die Performance auf den Validierungsdaten entsprechend schwach. Genau das selbe Prinzip gilt für die Accuracy.

Ich hatte das Early Stopping übrigens eingebaut, weil ich genau in den Plots gesehen habe, dass das Modell overfitted und die Validierungs-Metriken mit weiteren Epochen schlechter wurden.
Modellanwendung
Wir haben nun ein trainiertes Modell, welches hoffentlich gut generalisiert (= auf neuen Daten eine gute Performance abliefert). Jetzt „gehen wir live“ und testen die Bildklassifizierung mit CNNs auf vollkommen neuen Daten.
Dazu generieren wir ein neues Bild mit der DALL-E 2 API und klassifizieren es anschließend mit unserem Modell:
# Modell laden model = models.load_model("cnn") # Neues Bild generieren def get_image(prompt: str) -> Image: response = openai.Image.create( prompt=prompt, n=1, size=IMG_DALLE_API_STR, response_format="b64_json", ) json_data = response["data"][0]["b64_json"] image_bytes = b64decode(json_data) img = Image.open(io.BytesIO(image_bytes)).convert("L") return img # Bild klassifizieren def predict_class(img: Image, model: models.Sequential) -> str: img_array = np.array(img) / 255.0 img_array = np.expand_dims(img_array, axis=[0, -1]) prediction = model.predict(img_array) class_idx = np.argmax(prediction) class_name = CLASSES[class_idx] return class_name # Erstelle neues Bild prompt = random.choice(PROMPTS) img = get_image(prompt) # Vorhersage prediction = predict_class(img, model) # Bild anzeigen plt.imshow(img, cmap="gray") plt.title(f"Vorhersage: {prediction}") plt.show()
Wenn wir das Skript in einem Durchgang ausführen, müssten wir das Modell nicht neu laden. Aber der Vollständigkeit halber ist es mit drin, um zu verdeutlichen, dass wir es nun jederzeit einsatzbereit haben. Das Modell laden wir mit load_model
.
Danach definieren wir eine Funktion get_image
, um ein neues Bild basierend auf einem der drei Text-Prompts zu generieren. Die Auswahl der Kategorie passiert hier zufällig. Das generierte Bild wird anschließend mit predict_class
klassifiziert. In dieser Funktion wird das Bild zuerst in ein Array umgewandelt und dann skaliert, bevor es an das Modell weitergegeben wird. Das Modell gibt dann eine Vorhersage in Form von Wahrscheinlichkeiten für jede Klasse zurück. Die Klasse mit der höchsten Wahrscheinlichkeit ist unsere finale Vorhersage.
Wir visualisieren das Bild und nutzen die Vorhersage der Klasse als Titel. Den Code unter dem Kommentar „# Erstelle neues Bild“ können wir beliebig oft ausführen, um unser Modell „live“ zu testen.
Hier die ersten vier Live-Tests – alle richtig klassifiziert! 💪

Fazit
Es hat gut funktioniert! In diesem Post haben wir erfolgreich eine Bildklassifizierung mit CNNs durchgeführt. Dazu haben wir ein entsprechendes Modell erstellt, welches mit generierten Bildern von DALL-E 2 trainiert wurde und anschließend mit neuen Bildern getestet wurde.
Hier eine kleine Zusammenfassung der Schritte:
- Datenerstellung: Wir haben drei verschiedene Klassen von Bildern mit der DALL-E 2 API generiert.
- Augmentierung: Um die Robustheit des Modells zu erhöhen, haben wir die Daten mit Augmentierungstechniken erweitert.
- Preprocessing: Die Farbe der Bilder wurde in Graustufen umgewandelt.
- Modelltraining: Wir haben ein CNN erstellt und mit Early Stopping trainiert.
- Modellevaluation: Wir haben uns Plots zur Performance unseres Modells angeschaut.
- Modellanwendung: Das Modell wurde dann auf neue Daten angewendet, um Vorhersagen zu machen.
Diese Bildklassifizierung mit CNNs ist sehr basic, zeigt aber doch, dass man relativ schnell ein CNN bauen und damit rumspielen kann. Man könnte hier noch viele Dinge ändern bzw. hinzufügen: weitere Daten generieren oder Daten anders sammeln, andere Augmentierungstechniken anwenden, die Modellarchitektur anpassen, usw. Viel Spaß beim ausprobieren!
Wenn du ein Wunschthema hast oder mir Feedback geben willst, schreibe gerne einen Kommentar oder schicke eine Mail an mail@thorejohannsen.de.