Bildklassifizierung mit CNNs

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“:

Schachbrettmuster

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“:

Horizontal - Augmentiert

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.

Preprocessed (Vertikal)

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.

CNN visualisiert

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.

Bildklassifikation mit CNNs - Performance

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! 💪

Bildklassifikation mit CNNs - Vorhersagen

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:

  1. Datenerstellung: Wir haben drei verschiedene Klassen von Bildern mit der DALL-E 2 API generiert.
  2. Augmentierung: Um die Robustheit des Modells zu erhöhen, haben wir die Daten mit Augmentierungstechniken erweitert.
  3. Preprocessing: Die Farbe der Bilder wurde in Graustufen umgewandelt.
  4. Modelltraining: Wir haben ein CNN erstellt und mit Early Stopping trainiert.
  5. Modellevaluation: Wir haben uns Plots zur Performance unseres Modells angeschaut.
  6. 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.

Ähnliche Beiträge

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert