Blog

Keras: Mehr-Label-Klassifizierung mit ImageDataGenerator

Aktualisiert Oktober 21, 2025
11 Minuten

Die Multi-Label-Klassifizierung ist eine nützliche Funktion von tiefen neuronalen Netzen. Ich habe diese Funktion kürzlich in Keras ImageDataGenerator integriert, um auf Daten zu trainieren, die nicht in den Speicher passen. Dieser Blogbeitrag zeigt die Funktionalität und führt ein komplettes Beispiel mit dem VOC2012-Datensatz durch.

Seien Sie still und zeigen Sie mir den Code!

Stadt

In freier Wildbahn aufgenommene Bilder sind äußerst komplex. Um ein Bild wirklich zu "verstehen", spielen viele Faktoren eine Rolle, z. B. die Anzahl der Objekte im Bild, ihre Dynamik, die Beziehung zwischen den Bildern, die Positionen der Objekte usw. Um KI in die Lage zu versetzen, Bilder in freier Wildbahn so zu verstehen wie wir, müssen wir KI mit all diesen Fähigkeiten ausstatten. Diese Befähigung kann auf unterschiedliche Weise erfolgen, z.B. durch Mehrklassen-Klassifizierung, Multi-Label-Klassifizierung, Objekterkennung (Bounding Boxes), Segmentierung, Posenschätzung, optischen Fluss usw.

Nach einer kleinen Diskussion mit den Mitarbeitern des Pakets keras-preprocessing haben wir beschlossen, den Benutzern von Keras einige dieser Anwendungsfälle über die bekannte Klasse ImageDataGenerator zu ermöglichen. Insbesondere dank der von @Vijayabhaskar hinzugefügten Flexibilität der Klasse DataFrameIterator sollte dies möglich sein.

Während unseres letzten GDD-Freitags bei Xebia beschloss ich dann, den Anwendungsfall der Mehrklassen-Klassifizierung hinzuzufügen.1 Das Endergebnis war dieser PR.

Aber zuerst... Was ist eine Multi-Label-Klassifizierung?

Nicht zu verwechseln mit der Mehrklassen-Klassifikation. Bei einem Multi-Label-Problem können einige Beobachtungen 2 oder mehr Klassen zugeordnet werden.

ANMERKUNG

Diese Funktionalität wurde erst gestern in der Version keras-preprocessing 1.0.6 in der PyPI veröffentlicht . Sie können keras aktualisieren, um die neueste Version zu erhalten, indem Sie:

pip install -U keras

Lernen Sie Spark oder Python in nur einem Tag

Entwickeln Sie Ihre Data Science-Fähigkeiten. **Online**, unter Anleitung am 23. oder 26. März 2020, 09:00 - 17:00 CET.

1-tägige Live-Schulungen

Mehrklassen-Klassifizierung in 3 Schritten

In diesem Teil wird die Verwendung von ImageDataGenerator für die Mehrklassen-Klassifizierung schnell demonstriert.

1. Bild-Metadaten in Pandas-Datenrahmen

Nehmen Sie die Metadaten des Mehrklassenproblems in einen Pandas-Datenrahmen auf. Die Beschriftungen für jede Beobachtung sollten in einer Liste oder einem Tupel vorliegen. Die Dateinamen der Bilder können auf zwei Arten in den Datenrahmen aufgenommen werden, wie in der Abbildung unten gezeigt.

  • Relative Pfade: Wenn Sie nur die Dateinamen der Bilder angeben, müssen Sie später das Argument directory verwenden, wenn Sie die Methode flow_from_dataframe aufrufen.
  • Absolute Pfade: In diesem Fall können Sie das Argument directory weglassen. 2
Datenrahmen

2. Instanziieren Sie DataFrameIterator

Erstellen Sie den Generator für die Bildstapel. Dies geschieht durch die Instanziierung von DataFrameIterator über die Methode flow_from_dataframe von ImageDataGenerator. Angenommen, wir haben die Dateinamen als relative Pfade eingegeben, dann würde die einfachste Instanziierung wie folgt aussehen:3

von keras.preprocessing.image importieren ImageDataGenerator

img_iter = ImageDataGenerator().flow_from_dataframe(
    img_metadata_df,
    Verzeichnis=/home/rodrigo/.keras/datasets'.,
    x_col='Dateiname',
    y_col='Etiketten',
    Klasse_Modus='kategorisch'
)

Die eigentliche Logik der Erstellung der Stapel und der Handhabung der Datenerweiterung wird von der Klasse DataFrameIterator verwaltet. Weitere verfügbare Argumente können Sie hier nachschlagen.

3. Trainieren Sie das Modell

Trainieren Sie das Modell mit der Methode fit_generator.4

Modell.fit_generator(img_iter)

So erhalten Sie Batches direkt von der Festplatte und können mit viel mehr Daten trainieren, als in Ihren Speicher passen.

Das war's!5

Rundown Beispiel mit VOC2012

In diesem Teil führe ich Sie Schritt für Schritt durch ein Mehrklassen-Klassifizierungsproblem. Das Beispiel verwendet den VOC2012-Datensatz, der aus ~17.000 Bildern und 20 Klassen besteht.

Wenn Sie sich die Bilder unten ansehen, können Sie schnell feststellen, dass es sich um einen recht vielfältigen und schwierigen Datensatz handelt. Perfekt! Je näher an einem realen Beispiel, desto besser.

VOC2012 Bilder

Beginnen wir damit, die Daten von hier aus in ~/.keras/datasets herunterzuladen.

~/.keras/Datensätze/VOC2012
├── Anmerkungen
   ├── 2010_000002.xml
   ├── 2010_000003.xml
   ├── 2011_000002.xml
   └── ...
├── ImageSets
   ├── Aktion
   ├── Layout
   ├── Haupt
   └── Segmentierung
├── JPEGImages
   ├── 2010_000002.jpg
   ├── 2010_000003.jpg
   ├── 2011_000002.jpg
   └── ...
├── SegmentierungKlasse
   ├── 2010_000002.png
   ├── 2010_000003.png
   └── 2011_000003.png
└── SegmentierungObjekt
    ├── 2010_000002.png
    ├── 2010_000003.png
    └── ...

Wir werden das Verzeichnis Annotations verwenden, um die Metadaten der Bilder zu extrahieren. Jedes Bild kann auch wiederholte zugehörige Beschriftungen haben. Das Argument unique_labels der Funktion unten regelt, ob wir wiederholte Beschriftungen behalten. Das werden wir nicht tun, glauben Sie mir, das Problem ist schwer genug.

importieren xml.etree.ElementTree als ET
von pathlib importieren Pfad

def xml_to_labels(xml_data, einzigartige_etiketten):
    Wurzel = ET.XML(xml_data)
    Etiketten = einstellen.() wenn einzigartige_etiketten sonst []
    Etiketten_hinzufügen = Etiketten.hinzufügen wenn einzigartige_etiketten sonst Etiketten.anhängen. # Beschleunigt die Suche nach Methoden
    für i, Kind in aufzählen.(Wurzel):
        wenn Kind.Tag == 'Dateiname':
            img_filename = Kind.Text
        wenn Kind.Tag == 'Objekt':
            für Unterkind in Kind:
                wenn Unterkind.Tag == 'Name':
                    Etiketten_hinzufügen(Unterkind.Text)
    return img_filename, Liste(Etiketten)

def get_labels(annotations_dir, einzigartige_etiketten=True):
    für annotation_file in annotations_dir.iterdir():
        mit öffnen Sie(annotation_file) als f:
            Ertrag xml_to_labels(f.lesen(), einzigartige_etiketten)

annotations_dir = Pfad(~/.keras/datasets/VOC2012/Anmerkungen'.).expanduser()
img_metadaten = pd.DataFrame(get_labels(annotations_dir), Spalten=['Dateiname', 'Etiketten'])

Nach der Extraktion erhalten wir einen Datenrahmen mit relativen Pfaden, wie unten gezeigt.

VOC2012 Bilder

Die Dateinamen sind dann relativ zu

images_dir = Pfad(~/.keras/datasets/VOC2012/JPEGImages'.).expanduser()

Scannen Sie den Datensatz

Werfen wir nun einen kurzen Blick darauf, wie die Beschriftungen über den Datensatz verteilt sind. Diese Zählungen können leicht mit einem Counter Objekt berechnet werden.

von Sammlungen importieren Zähler
Etiketten_Zahl = Zähler(Etikett für lbs in img_metadaten['Etiketten'] für Etikett in lbs)

Von hier aus können wir ganz einfach die class_weights zur späteren Verwendung berechnen.

gesamt_Zahl = Summe(Etiketten_Zahl.Werte())
Klasse_Gewichte = {cls: gesamt_Zahl / zählen für cls, zählen in Etiketten_Zahl.Artikel()}

Lassen Sie uns nun die Anzahl der Etiketten aufzeichnen.

VOC2012 Bilder

Nein bueno, überhaupt nicht bueno! Es gibt zwei Arten von Ungleichgewichten in dem Datensatz. Unausgewogenheit zwischen verschiedenen Klassen und Unausgewogenheit zwischen positiven und negativen Beispielen in einigen Klassen. Die erste Art des Ungleichgewichts kann zu einer Überanpassung an stark vertretene Klassen führen, in diesem Fall person. Das letztgenannte Ungleichgewicht kann dazu führen, dass eine Klasse immer als negativ gekennzeichnet wird, d.h. wenn cow immer als negativ gekennzeichnet wird, ergibt dies eine Genauigkeit von 97% für diese Klasse.

Was können wir also dagegen tun?... beten. Ich werde nicht ins Detail gehen, aber eine Möglichkeit, den Ungleichgewichten entgegenzuwirken, ist eine Kombination aus Klassengewichten und Probengewichten.6

Der nächste Schritt ist die Betrachtung der Form- und Größenverteilung in den verschiedenen Bildern.

VOC2012 Bilder

Wie oben dargestellt, enthält der Datensatz Bilder mit unterschiedlichen Höhen und Breiten. Ich werde hier nicht ins Detail gehen, aber das ist nicht wirklich ein Problem, wenn am Ende der Merkmalsextraktion über Faltungsschichten eine globale Pooling-Schicht angewendet wird. Leider gibt es noch ein weiteres Problem: Wenn Sie flow_from_dataframe verwenden, müssen alle Bilder auf dieselbe Breite und Höhe standardisiert werden.7 Dies wird über den Parameter target_size festgelegt.

Die untere Histogrammdarstellung ist gut, weil sie uns einen ungefähren Hinweis auf die maximale Stapel- und Warteschlangengröße geben kann, die unser Speicher bei Verwendung des Generators aufnehmen kann. In diesem Beispiel verwende ich das Diagramm jedoch nicht wirklich.

Das Modell trainieren

Zunächst müssen wir ImageDataGenerator instanziieren. Ich werde dies mit einem einfachen Setup tun, das lediglich die Pixelwerte normalisiert. Ich habe auch einen Validierungssplit eingefügt, um ihn für Validierungsstatistiken während des Trainings nach jeder Epoche zu verwenden.

img_gen = ImageDataGenerator(neu skalieren=1/255, validation_split=0.2)

Wir können nun die Trainings- und Validierungsdaten DataFrameIterator erstellen, indem wir subset als "training" bzw. "validation" angeben. Im Falle einer Multi-Label-Klassifizierung sollte class_mode "categorical" (der Standardwert) sein.

img_iter = img_gen.flow_from_dataframe(
    img_metadaten,
    Shuffle=True,
    Verzeichnis=images_dir,
    x_col='Dateiname',
    y_col='Etiketten',
    Klasse_Modus='kategorisch',
    ziel_größe=(128, 128),
    chargen_größe=20,
    Teilmenge='Ausbildung'
)

img_iter_val = img_gen.flow_from_dataframe(
    img_metadaten,
    Shuffle=False,
    Verzeichnis=images_dir,
    x_col='Dateiname',
    y_col='Etiketten',
    Klasse_Modus='kategorisch',
    ziel_größe=(128, 128),
    chargen_größe=200,
    Teilmenge='Überprüfung'
)

Ich verwende in diesem Beispiel das vortrainierte Modell ResNet50. Ich werde die letzten vollständig verbundenen Schichten des Netzwerks durch eine Ausgabeschicht mit 20 Neuronen ersetzen, eine für jede Klasse.8 Achten Sie außerdem auf die Aktivierungsfunktion der Ausgabe. Ich werde nicht ins Detail gehen, aber für die Klassifizierung in mehrere Klassen sollte die Wahrscheinlichkeit jeder Klasse unabhängig sein, daher die Verwendung der Funktion sigmoid und nicht der Funktion softmax, die für Mehrklassenprobleme verwendet wird.

Basis_Modell = ResNet50(
    einschließen_oben=False,
    Gewichte='imagenet',
    input_shape=Keine,
    Zusammenlegung='avg'
)
für Ebene in Basis_Modell.Ebenen:
    Ebene.trainierbar = False

Vorhersagen = Dichtes(20, Aktivierung='sigmoid')(Basis_Modell.Ausgabe)
Modell = Modell(Eingaben=Basis_Modell.Eingabe, Ausgaben=Vorhersagen)

Als Nächstes kompilieren wir das Modell mit "binary_crossentropy" loss. Warum binäre Cross-Entropie und nicht kategorische Cross-Entropie, werden Sie sich fragen? Nun, auch hier werde ich nicht ins Detail gehen, aber wenn Sie categorical_crossentropy verwenden, bestrafen Sie im Grunde genommen nicht für falsch-positive Ergebnisse (wenn Sie eher ein Code-Mensch als ein Mathe-Mensch sind , hier ist es).

Modell.kompilieren(
    Verlust=binäre_Querentropie'.,
    Optimierer='adam'
)

HINWEIS: Auch wenn ich gerade gesagt habe, dass die Mathematik für multi-label Sigmoid und binäre Kreuzentropie vorschreibt, gibt es Fälle, in denen Softmax und kategorische Kreuzentropie besser funktionieren. Wie in diesem Fall.

Trainieren Sie das Modell bereits! Noch nicht... Geduld,"lento pero seguro". Lassen Sie uns über Metriken für ein Multi-Label-Problem wie dieses sprechen. Ich hoffe, es ist klar, dass Genauigkeit nicht der richtige Weg ist. Lassen Sie uns stattdessen f1_score, recall_score und precision_score verwenden. Es gibt allerdings ein kleines Problem - ja, das Leben ist ein Miststück - diese Metriken wurden aus gutem Grund aus den Keras-Metriken entfernt.

Der richtige Weg, diese Metriken zu implementieren, besteht darin, eine Callback-Funktion zu schreiben, die sie am Ende jeder Epoche anhand der Validierungsdaten berechnet. Etwa so:

von itertools importieren Abschlag  # endlich! Ich habe etwas Nützliches für sie gefunden 
von sklearn importieren Metriken

Klasse Metriken(Rückruf):
    def __init__(selbst, validierung_generator, validierung_schritte, Schwellenwert=0.5):
        selbst.validierung_generator = validierung_generator
        selbst.validierung_schritte = validierung_schritte oder len(validierung_generator)
        selbst.Schwellenwert = Schwellenwert

    def on_train_begin(selbst, Protokolle={}):
        selbst.val_f1_scores = []
        selbst.val_recalls = []
        selbst.val_präzisionen = []

    def on_epoch_end(selbst, Epoche, Protokolle={}):
        # Generator duplizieren, um sicherzustellen, dass y_true und y_pred aus denselben Beobachtungen berechnet werden
        gen_1, gen_2 = Abschlag(selbst.validierung_generator)
        y_true = np.vstack(nächste(gen_1)[1] für _ in Reichweite(selbst.validierung_schritte)).astype('int')
        y_pred = (selbst.Modell.predict_generator(gen_2, Schritte=selbst.validierung_schritte) > selbst.Schwellenwert).astype('int')
        f1 = Metriken.f1_score(y_true, y_pred, Durchschnitt='gewichtet')
        Präzision = Metriken.präzision_score(y_true, y_pred, Durchschnitt='gewichtet')
        Rückruf = Metriken.recall_score(y_true, y_pred, Durchschnitt='gewichtet')
        selbst.val_f1_scores.anhängen.(f1)
        selbst.val_recalls.anhängen.(Rückruf)
        selbst.val_präzisionen.anhängen.(Präzision)
        drucken(f" - val_f1_score: {f1:.5f} - val_precision: {Genauigkeit:.5f} - val_recall: {Rückruf:.5f}")
        return

Endlich! Wir sind bereit, das Modell zu trainieren. Zu Ihrer Information: Ich habe mich wenig bis gar nicht um die Optimierung des Modells bemüht.

Metriken = Metriken(img_iter_val, validierung_schritte=50)
Geschichte = Modell.fit_generator(
    img_iter,
    Epochen=5,
    Schritte_pro_Epoche=250,
    Klasse_Gewicht=Klasse_Gewichte,
    Rückrufe=[Metriken]
)

Während der Trainingszeit sollten Sie am Ende jeder Epoche Validierungsmetriken sehen, etwa so:

Epoche 1/5
250/250 [==============================] - 261s 1s/Schritt - Verlust: 4.2584 - val_f1_score: 0.28155 - val_precision: 0.21952 - val_recall: 0.40316

Wenn Ihr Gedächtnis während des Trainings schmilzt, reduzieren Sie die batch_size, die target_size oder die max_queue_size Parameter.

Post-mortem Untersuchung

Bei einem Mehrklassenproblem ist es bereits eine große Hilfe, die Konfusionsmatrix zu zeichnen. Auf diese Weise können wir sehr deutlich erkennen, wo das Modell eine Klasse mit einer anderen "verwechselt" und die Probleme direkt angehen. Da es sich um ein Multi-Label-Problem handelt, macht es keinen Sinn, das Gleiche zu tun. Stattdessen kann eine Verwechslungsmatrix pro Klasse überprüft werden. 9

Diese Funktion ist nur in der Entwicklungsversion von scikit-learn enthalten, die Sie über

pip install git+https://www.github.com/scikit-learn/scikit-learn.git --upgrade

Danach sollten Sie in der Lage sein

von sklearn.metrics importieren multilabel_confusion_matrix

Ich habe eine Wrapper-Plot-Funktion plot_multiclass_confusion_matrix um multilabel_confusion_matrix herum geschrieben, die Sie im Code finden können. Die Ausgabe der Funktion sieht folgendermaßen aus:

VOC2012 Bilder

Das war's, Leute! Wie Sie sehen können, ist das Modell schlecht. Ihr Auftrag, falls Sie ihn annehmen wollen...

VOC2012 Bilder

Adios

Ich hoffe, Sie fanden diesen Blogbeitrag nützlich. Ich habe viele Konzepte recht schnell abgehandelt, aber ich denke, es sind einige wertvolle Tipps dabei.

Sie können den Code hier finden.

Wenn Sie weitere Fragen haben, pingen Sie mich einfach auf Twitter an @rragundez.

Xebia stellt ein

Möchten Sie Keras selbst ausprobieren?

Nehmen Sie an einem dreitägigen Kurs über Deep Learning teil. Sie lernen nicht nur die Theorie hinter Deep Learning und die für Deep Learning verwendeten Algorithmen kennen, sondern erhalten auch umfangreiche praktische Erfahrungen mit Keras.


  1. Das war schon vorher möglich, aber auf eine nicht sehr API-freundliche Weise. Sie können hier darüber lesen.
  2. Das absolute Pfadformat bietet Ihnen mehr Flexibilität, da Sie einen Datensatz aus mehreren Verzeichnissen erstellen können.
  3. Im Falle einer Mehrklassen-Klassifizierung sollten Sie class_mode='categorical' verwenden .
  4. Für die Mehrklassen-Klassifizierung stellen Sie sicher, dass die Ausgabeschicht des Modells eine sigmoid Aktivierungsfunktion hat und dass die Verlustfunktion binary_crossentropy ist .
  5. Ich hoffe, Sie wissen die Einfachheit zu schätzen :)
  6. Beispielgewichte sind noch nicht in flow_from_dataframe implementiert. Ich warte auf diese Person, aber wenn Sie dazu beitragen möchten, tun Sie es bitte!
  7. Dies ist eine Voraussetzung, da jeder Stapel von Bildern in ein Numpy-Array geladen wird. Daher sollte jedes geladene Bild die gleichen Array-Abmessungen haben. Außerdem wäre dies eine großartige Funktion. Eine PR wäre zwar ziemlich umständlich, aber machen Sie es!
  8. Die Ausgabeschicht von ResNet50, wenn include_top=False eine Größe von 2048 hat, würde ich normalerweise nicht mit einer voll verknüpften Schicht von 20 Neuronen folgen, aber für dieses Beispiel ist es ausreichend, um die Funktionalität zu zeigen. Normalerweise versuche ich, die Ausgabeeinheiten auf jeder Schicht um 1/3 oder 1/10 zu verringern, wenn 1/3 nicht ausreicht.
  9. Es gibt einige Dinge, die eine Verwirrungsmatrix pro Klasse nicht berücksichtigt, aber es ist ein guter erster Ansatz.

Contact

Let’s discuss how we can support your journey.