Projekt zur Implementierung von Dynamic SOLO (SOLOv2) mit TensorFlow vorgestellt.
Ein besseres Verständnis von Computer Vision: Dynamic SOLO (SOLOv2) mit TensorFlow Hinweis: Dieses Projekt ist noch nicht für die Produktion bereit. Warum ich es von Grund auf neu implementiert habe Wenn man etwas nicht versteht, taucht man tief in das Problem ein, um Antworten zu finden und sein Wissen in diesem Bereich zu erweitern. Ein solches Vorgehen ermöglicht es, die Theorie hinter einem Ansatz oder Prinzip zu verstehen und dann technische Werkzeuge zu nutzen, um diesen Ansatz umzusetzen. Indem man etwas von Grund auf neu implementiert, gewinnt man ein besseres Verständnis für den Aufwand, die Zeit und die Ressourcen, die in solche Aufgaben investiert werden. Dies hilft auch bei der genauen Abschätzung von Kosten und dem Werten ähnlicher Arbeiten, einschließlich Vor- und Nachbereitung, Forschung, technischer Umsetzung und Dokumentation. Das Modell SOLO (Segmenting Objects by Locations) ist ein Modell zur Lösung von Computer-Vision-Aufgaben, insbesondere für die Instanzsegmentierung. Es ist ein vollständig ohne Ankerpunkte (anchor-free) arbeitendes Framework, das Masken ohne Begrenzungsrahmen (Bounding Boxes) vorhersagt. Die Autoren des Papers haben mehrere Varianten des Modells vorgestellt: Vanilla SOLO, Decoupled SOLO, Dynamic SOLO und Decoupled Dynamic SOLO. Ich begann mit der Implementierung von Vanilla SOLO, da es die einfachste Variante ist, entschied mich aber, den Code nicht zu veröffentlichen, da zwischen Vanilla und Dynamic SOLO im Hinblick auf die Implementierung nur geringe Unterschiede bestehen. Das Modell ist flexibel: von der Anzahl der FPN-Schichten bis hin zur Anzahl der Parameter in den Schichten. Die grundlegende Idee besteht darin, das gesamte Bild in Zellen zu unterteilen, wobei eine Gitterzelle nur eine Instanz darstellen kann: bestimmte Klasse plus Segmentationsmaske. Backbone Als Backbone wählte ich ResNet50, da es ein leichtgewichtiges Netzwerk ist, das sich gut für den Einstieg eignet. Ich nutzte keine vorab trainierten Parameter für ResNet50, da ich neben dem ursprünglichen COCO-Datensatz auch mit anderen Datensätzen experimentierte. Für den ursprünglichen COCO-Datensatz können jedoch vorab trainierte Parameter verwendet werden, um die Trainingszeit zu verkürzen und die Leistung zu verbessern. python backbone = ResNet50(weights='imagenet', include_top=False, input_shape=input_shape) backbone.trainable = False Neck Die FPN (Feature Pyramid Network) wird als "Neck" verwendet, um Merkmale auf verschiedenen Skalen zu extrahieren. Innerhalb der FPN werden alle Ausgänge C2, C3, C4, C5 aus den entsprechenden Residualblöcken von ResNet50 genutzt, wie in der FPN-Paper beschrieben. Jede FPN-Ebene repräsentiert eine spezifische Skala und hat ihr eigenes Gitter. Für kleine benutzerdefinierte Datensätze, bei denen alle Objekte etwa die gleiche Skala haben, sollte man nicht alle FPN-Ebenen verwenden, da dies unnötige Parameter und GPU-Ressourcen verbraucht. In diesem Fall muss man den Datensatz anpassen, sodass er nur Ziele für eine Skala zurückgibt, nicht für alle vier. Head Die Ausgänge der FPN-Schichten dienen als Eingänge für die Schichten, in denen die Instanzklasse und ihre Maske bestimmt werden. Der Head enthält zwei parallele Zweige: den Klassifikationszweig und den Mask-Kernel-Zweig. Für kleine benutzerdefinierte Datensätze kann man sogar eine einzelne Menge für beide Zweige verwenden, um unnötige Parameter zu vermeiden. Mask Feature Der Mask-Feature-Zweig wird mit dem Mask-Kernel-Zweig kombiniert, um die endgültige vorhergesagte Maske zu erzeugen. Diese Schicht fusioniert multi-skalare FPN-Merkmale, um eine einheitliche Mask-Feature-Karte zu produzieren. Die Autoren des Papers bewerteten zwei Ansätze zur Implementierung des Mask-Feature-Zweigs: einen spezifischen Mask-Feature für jede FPN-Ebene oder ein einheitliches Mask-Feature für alle FPN-Ebenen. Ich entschied mich für den letzten Ansatz. Die Kombination der beiden Zweige erfolgt über eine dynamische Faltung (dynamic convolution). Datensatz Datenverstärkung (Data Augmentation): Um den Datensatz zu erweitern, wurden verschiedene Bildtransformationen angewendet, um neue Proben zu generieren, die sich von den Originalen unterscheiden. Dies ist besonders wichtig für kleine Datensätze. Methoden wie horizontales Spiegeln, Helligkeitsanpassung, zufällige Skalierung und zufälliges Cropping wurden verwendet, um sicherzustellen, dass die Maske des modifizierten Bildes zu dessen neuen (verstärkten) Bild passt. Zielkonvertierung (Target Conversion): Das SOLO-Modell erwartet ein spezifisches Datenformat für das Ziel. Es nimmt ein normiertes Bild als Eingabe, erwartet aber für das Ziel komplexere Daten. Datenspeicherung: Es gibt zwei Hauptoptionen für die Datenspeicherung: Speichern von Proben im Arbeitsspeicher oder dynamisches Generieren von Daten. Obwohl das Speichern im Arbeitsspeicher viele Vorteile hat, wählte ich bewusst das dynamische Generieren von Daten mit tf.data.Dataset.from_generator. Dies ermöglicht es, mit großen Datenmengen und hoher Auflösung zu arbeiten, was bei realen Problemen oft der Fall ist. Dynamisches Generieren von Daten ist etwas komplexer zu implementieren, bietet aber mehr Flexibilität. Trainingsprozess Verlustfunktion (Loss Function): SOLO verfügt nicht über eine standardmäßige Verlustfunktion, die in TensorFlow eingebaut ist. Ich implementierte daher die Verlustfunktion selbst: [ L = L_{cate} + \lambda L_{mask} ] Wo: [ L_{mask} = \frac{1}{N_{pos}} \sum_k \mathbb{1}_{p^{i,j} > 0} d{mask}(m_k, m^_k) ] Hierbei ist ( L_{Dice} = 1 - D(p, q) ) und ( D(p, q) = \frac{2 \sum_{x,y} (p_{x,y} \cdot q_{x,y})}{\sum_{x,y} p^2_{x,y} + \sum_{x,y} q^2_{x,y}} ) der Dice-Koeffizient, der die Pixelwerte ( p_{x,y} ) und ( q_{x,y} ) des vorhergesagten und des Ground-Truth-Masken an Position ( (x, y) ) vergleicht. Alle Details der Verlustfunktion sind in Abschnitt 3.3.2 des ursprünglichen SOLO-Papers beschrieben. Fortsetzen von einem Checkpoint: Bei der Nutzung eines leistungsschwachen GPUs kann das Training des gesamten Modells in einem einzigen Durchgang unpraktisch sein. Um die trainierten Gewichte nicht zu verlieren und den Trainingsprozess fortzusetzen, bietet dieses Projekt ein System zum Fortsetzen von einem Checkpoint. Dies ermöglicht es, das Modell alle ( n ) Epochen (wobei ( n ) konfigurierbar ist) zu speichern und das Training später wieder aufzunehmen. Um dies zu aktivieren, setzt man load_previous_model auf True und gibt model_path in config.py an. python self.load_previous_model = True self.model_path = './weights/coco_epoch00000001.keras' Evaluationsprozess Um die Effektivität des trainierten Modells und seine Leistung auf bisher unbekannten Bildern zu prüfen, wird ein Evaluationsprozess verwendet. Dieser besteht aus folgenden Schritten: Laden eines Testdatensatzes. Vorbereiten des Datensatzes, sodass er mit der Modell-Eingabe kompatibel ist. Füttern der Daten in das Modell. Unterdrücken von Masken mit tiefer Wahrscheinlichkeit für dieselbe Instanz. Visualisierung des ursprünglichen Testbildes mit der endgültigen Maske und der vorhergesagten Klasse für jede Instanz. Die größte Herausforderung hierbei war die Implementierung von Matrix NMS (non-maximum suppression), wie in Abschnitt 3.3.4 des ursprünglichen SOLO-Papers beschrieben. NMS eliminiert redundante Masken, die dieselbe Instanz mit niedriger Wahrscheinlichkeit darstellen. Um mehrfache Vorhersagen derselben Instanz zu vermeiden, müssen diese doppelten Masken unterdrückt werden. Die Autoren stellten Python-Pseudo-Code für Matrix NMS zur Verfügung, den ich in TensorFlow umsetzen musste. Hier ist meine Implementierung: ```python def matrix_nms(masks, scores, labels, pre_nms_k=500, post_nms_k=100, score_threshold=0.5, sigma=0.5): seg_masks = tf.cast(masks >= 0.5, dtype=tf.float32) mask_sum = tf.reduce_sum(seg_masks, axis=[1, 2]) num_masks = tf.shape(scores)[0] if pre_nms_k is not None: num_selected = tf.minimum(pre_nms_k, num_masks) else: num_selected = num_masks topk_indices = tf.argsort(scores, direction='DESCENDING')[:num_selected] seg_masks = tf.gather(seg_masks, topk_indices) labels_sel = tf.gather(labels, topk_indices) scores_sel = tf.gather(scores, topk_indices) mask_sum_sel = tf.gather(mask_sum, topk_indices) N = tf.shape(seg_masks)[0] seg_masks_flat = tf.reshape(seg_masks, (N, -1)) intersection = tf.matmul(seg_masks_flat, seg_masks_flat, transpose_b=True) mask_sum_matrix = tf.tile(mask_sum_sel[tf.newaxis, :], [N, 1]) union = mask_sum_matrix + tf.transpose(mask_sum_matrix) - intersection iou = intersection / (union + 1e-6) iou = tf.linalg.band_part(iou, 0, -1) - tf.linalg.band_part(iou, 0, 0) labels_matrix = tf.tile(labels_sel[tf.newaxis, :], [N, 1]) same_class = tf.cast(tf.equal(labels_matrix, tf.transpose(labels_matrix)), tf.float32) same_class = tf.linalg.band_part(same_class, 0, -1) - tf.linalg.band_part(same_class, 0, 0) decay_iou = iou * same_class max_iou_per_col = tf.reduce_max(decay_iou, axis=0) comp_matrix = tf.tile(max_iou_per_col[..., tf.newaxis], [1, N]) decay_matrix = tf.exp(-((decay_iou ** 2 - comp_matrix ** 2) / sigma)) decay_coeff = tf.reduce_min(decay_matrix, axis=0) decay_coeff = tf.where(tf.math.is_inf(decay_coeff), 1.0, decay_coeff) new_scores = scores_sel * decay_coeff keep_mask = new_scores >= score_threshold new_scores = tf.where(keep_mask, new_scores, tf.zeros_like(new_scores)) if post_nms_k is not None: num_final = tf.minimum(post_nms_k, tf.shape(new_scores)[0]) else: num_final = tf.shape(new_scores)[0] final_indices = tf.argsort(new_scores, direction='DESCENDING')[:num_final] final_indices = tf.boolean_mask(final_indices, tf.greater(tf.gather(new_scores, final_indices), 0)) kept_indices = tf.gather(topk_indices, final_indices) return kept_indices ``` Hier ist ein Beispiel für Bilder mit überlagerten Masken, die vom Modell für ein Bild vorausgesagt wurden, das es noch nie gesehen hat. Tipps für die Neuimplementierung Welche Daten zu welcher Funktion?: Es ist sehr wichtig, sicherzustellen, dass man die richtigen Daten in das Modell füttert. Die Daten sollten dem entsprechen, was in jeder Schicht erwartet wird, und jede Schicht sollte die Eingabedaten so verarbeiten, dass die Ausgabe für die nächste Schicht geeignet ist. Denn letztlich berechnet man die Verlustfunktion auf Basis dieser Daten. Papier recherchieren: Man kann nicht umhin, das Papier zu lesen, auf dem man sein Modell basiert. Obwohl es offensichtlich ist, müssen die Prinzipien verstanden werden. Oftmals müssen andere Papiere und Arbeiten gelesen und verstanden werden, bevor man das eigene Papier vollständig erfassen kann. Aber meist basieren selbst die neuesten Arbeiten auf Prinzipien, die schon länger bekannt sind. Es gibt viele Materialien im Internet, die diese Prinzipien klar erklären. Man kann LLM-Programme verwenden, um die Informationen zusammenzufassen, Beispiele zu geben und das Verständnis zu fördern. Mit kleinen Schritten beginnen: Beim Implementieren eines Computer-Vision-Modells mit Millionen von Parametern sollte man im Entwicklungsstadium nicht unnötige Zeit auf Training, Datensatzvorbereitung, Evaluierung usw. verschwenden, wenn man sich nicht sicher ist, ob das Modell korrekt funktioniert. Wenn man ein leistungsschwaches GPU nutzt, dauert der Prozess noch länger. Man kann das Modell zu Beginn sogar mit einem kleinen Datensatz und wenigen Parametern überfitzen, um sicherzustellen, dass die Daten richtig auf die Ziele des Modells abgebildet sind. Code debuggen: Durch das Debuggen des Codes kann man sicherstellen, dass man das erwartete Verhalten und die Datenwerte auf jedem Schritt erreicht. Insbesondere beim Bau von Modellen, der Implementierung von Verlustfunktionen und der Vorbereitung von Datensätzen für Eingaben und Ziele, interagiert man häufig mit mathematischen Operationen und Tensoren, was increased Attention erfordert. Zusammenfassung Dies ist eine kurze Beschreibung des Projekts ohne technische Details, um ein allgemeines Bild zu vermitteln und Leseermüdung zu vermeiden. Offensichtlich kann eine Projektbeschreibung, die sich mit einem Computer-Vision-Modell befasst, in einem einzigen Artikel nicht vollständig umfassend sein. Falls ich Interesse von Lesern an dem Projekt sehe, werde ich eine detailliertere Analyse mit technischen Details schreiben. Vielen Dank fürs Lesen!