7.15. Einen eigenen schreiben

Wenn der Katalog ein Modell nicht abdeckt – ein Forschungsnetz, dessen Ausgabelayout maßgeschneidert ist, eine Anpassung an eine bestehende Architektur, ein Tensor, dessen semantische Interpretation anwendungsspezifisch ist –, stellt die Anwendung ihren eigenen Post-Prozessor bereit. Das Protokoll ist schlicht: ein Callable, das (model, inputs, outputs) entgegennimmt und das zurückgibt, was die Anwendung von predict() erwartet.

Eine Klasse mit __call__ ist die übliche Form:

class MyPostprocessor:
    def __init__(self, threshold=0.5):
        self.threshold = threshold

    def __call__(self, model, inputs, outputs):
        ...
        return result

Eine einfache Funktion funktioniert ebenfalls – die Engine prüft nur, ob das Objekt aufrufbar ist.

7.15.1. Einbinden

Zwei Anbindungspunkte. Das kwarg postprocess= am Konstruktor bindet das Callable für jeden Aufruf von predict() am Modell:

model = ml.Model("/rom/my_model.tflite",
                 postprocess=MyPostprocessor())

Um die Bindung für einen einzelnen Aufruf zu überschreiben – Dekoder austauschen, ohne das Modell neu zu laden –, übergibt man callback= direkt an predict:

result = model.predict([img], callback=MyOtherPostprocessor())

Die Signatur des Callable ist in beiden Fällen identisch.

7.15.2. Was das Callable empfängt

  • model – die Model-Instanz, nützlich für die Quantisierungsparameter (output_scale, output_zero_point, output_dtype) und die Eingabedimensionen (input_shape).

  • inputs – die Liste der Eingaben, die die Anwendung an predict() übergeben hat. Das erste Element ist gewöhnlich die gebundene Normalization-Instanz; ihr roi-Attribut ist das, was NMS zum Zurückabbilden der Rahmen in das ursprüngliche Bild erwartet.

  • outputs – die rohen Ausgabetensoren als Liste von ndarray-Objekten in ihrem nativen dtype. Float-Ausgaben kommen unverändert an; Integer-Ausgaben kommen quantisiert an.

7.15.3. Quantisierte Arithmetik

Die mitgelieferten Dekoder greifen alle auf dieselben Hilfsfunktionen in ml.utils zurück, und ein benutzerdefinierter möchte gewöhnlich dasselbe Muster: quantize() hebt einen Float-Schwellenwert in den quantisierten Raum des Modells, threshold() filtert, ohne den gesamten Tensor zu dequantisieren, und dequantize() läuft einmal über die Überlebenden. sigmoid() und logit() stehen für Netze zur Verfügung, deren Ausgabekanäle Pre-Sigmoid-Logits sind (die MediaPipe-Detektoren sind der kanonische Fall).

Für Modelle mit Float-Ausgaben – Regressionsköpfe, Modelle mit einer eingebackenen finalen Dequantize-Schicht – werden die Quantisierungshilfsfunktionen unverändert durchgereicht, sodass derselbe Post-Prozessor-Code gegen beide dtypes ohne Sonderbehandlung funktioniert.

7.15.4. Rückgabewert

Was auch immer das Callable zurückgibt, ist das, was predict() zurückgibt. Für rahmenausgebende Dekoder besteht die Konvention darin, Kandidaten durch ein NMS zu schieben und seine Listen pro Klasse zurückzugeben – die Aufrufform, die Non-Maximum-Suppression dokumentiert und die YOLOv8-Durchführung im Kontext aufbaut. Für alles andere gibt man zurück, was die Anwendung praktisch findet: ein einzelnes ndarray, eine Label-Zeichenkette, ein Tupel aus (class, score, embedding), ein Dictionary.