alami-vision-api / docs /PHASE_A_SCENE_GATE.md
alami-ci
Deploy from alami-eco/alami-trash-ai@aee69796b70947e95efdb9c7483fa52f8d3b4520
76838d6
|
Raw
History Blame Contribute Delete
28.9 kB

Phase A — „Ist das Müll?"-Szenen-Gate (Open-Vocabulary)

Stand: 2026-07-25. Umsetzung von VISION_ROADMAP.md §7.5.2. Diese Datei ist die Spezifikation + der Contract-Vorschau für Dev/Web/Admin — sie legt fest, was Phase A ist, wie die API sich additiv erweitert und wie wir es messen, bevor irgendetwas Live-Auszahlungen berührt. Kein Live-Contract-Change ohne die Messung aus §5 und Founder-Sign-off.


1. Warum (in einem Satz)

Das Modell hat nie gelernt, wie Nicht-Müll aussieht — deshalb „sieht" es zu 86 % „paper" auf einer grauen Fläche (dokumentiert in VISION_ROADMAP.md §7.1). Jeder solche False Positive zahlt unverdiente TrashCoin aus und belastet die Review-Queue. Phase A gibt dem Modell den fehlenden Weltkontext: „das ist eine Pflanze / ein Stuhl / eine Katze — kein Müll" — und liefert nebenbei das verkaufbare Szenen-Inventar „3 Flaschen + 1 Katze".

Zwei Nutzen aus einem Modell:

  1. FP-Gate (Geld-Leck stopfen) — der wichtigste kurzfristige Effekt.
  2. Szenen-Inventar (Nicht-Müll benennen + zählen) — der sichtbare „ChatGPT-kann-das-auch"-Konter und die Basis für den Verkauf an Industrien.

2. Architektur — Zwei-Schichten-Strategie (aus Roadmap §7.2, konkretisiert)

        Bild
         │
   ┌─────▼───────────────────────────────────────────────┐
   │  Fundament (die Augen): Open-Vocabulary-Detektor     │  ← von der Stange
   │  benennt WAS im Bild ist: bottle, cat, plant, chair, │    (YOLOE / YOLO-World)
   │  can, cup, bag, person, tree, road …                 │
   └─────┬───────────────────────────────────────────────┘
         │ Objekt-Inventar mit Boxen + Labels
   ┌─────▼───────────────────────────────────────────────┐
   │  Alami-Fach-Kopf (das Gehirn): unsere Domänen-Logik  │  ← nur Alami hat das
   │  • ist das Objekt Müll? (Waste-Vokabular-Mapping)    │    (aus Flywheel +
   │  • welches Material-Bucket? welche Entsorgung?       │     Fach-Wissen)
   │  • Nicht-Müll (Pflanze/Tier/Möbel) → explizit raus   │
   └─────┬───────────────────────────────────────────────┘
         │
   ┌─────▼───────────────────────────────────────────────┐
   │  Fusion mit dem bestehenden YOLOv8-Seg-Detektor #13  │
   │  (Material-Feinklassifikation bleibt unser Modell)   │
   └──────────────────────────────────────────────────────┘

Wichtig: Der bestehende Material-Detektor (#13, live) wird nicht ersetzt. Das Open-Vocab-Fundament liegt daneben und liefert (a) das FP-Veto und (b) das Nicht-Müll-Inventar. Die Material-Feinklassen (plastic/glass/…) kommen weiter aus unserem trainierten Modell — dort ist unser Datengraben.


3. Backbone-Entscheidung (ML-Engineering-Call, begründet)

Kandidat Open-Vocab Lokalisiert CPU-tauglich (HF Space) Stack-Fit Verdikt
YOLOE / YOLO-World v2 ✅ (Boxen) ✅ (S/M, ONNX) ultralytics 8.4 + ONNX — schon im Serving gewählt
CLIP (Zero-Shot, ganzes Bild) ❌ (kein Box) ✅ (billig) teils nur als Ultra-Cheap-Preview-Gate
Grounding-DINO / OWLv2 ✅✅ (stärker) ❌ (GPU/$$-Space nötig) neuer Stack zu schwer für jetzt

Gewählt: YOLOE/YOLO-World v2 → ONNX. Gründe:

  • Kein neuer Stack: Serving läuft bereits auf ultralytics + onnxruntime (siehe requirements.txt, ml/serving/model_loader.py). Das Fundament nutzt denselben Export- und Lade-Pfad wie unser Material-Modell.
  • CPU-tauglich: Die S/M-Größe läuft auf dem kostenlosen HF-Space (heutige Betriebsbasis), ohne auf einen GPU-Space ($$) zu wechseln.
  • Liefert beides: Boxen + offene Labels → FP-Gate und Inventar aus einem Modell.
  • Training/Export schwer, Inferenz leicht — genau wie beim Material-Modell: schwere Schritte laufen auf Kaggle-GPU (bestehende ml/kaggle/-Straße), das Serving lädt nur das exportierte ONNX.

4. API-Erweiterung — strikt additiv (Hard Contract §5.1)

Der Mobile-Client hängt an der exakten Shape von /predict und /v1/analyze (schemas.py, Kommentar Zeile 6–9). Nichts Bestehendes ändert sich. Neu kommt ausschließlich additiv hinzu:

4.1 AnalyzeResponse.summary — zwei neue Felder (advisory)

"summary": {
  "item_count": 3,
  "trash_detected": true,          // UNVERÄNDERT in Semantik (siehe §6!)
  "materials": { "plastic": 3 },
  "total_weight_estimate_g": 180.0,
  "weight_source": "material_prior_v0",
  // --- NEU, additiv, advisory ---
  "waste_likelihood": 0.94,        // 0..1: Wahrscheinlichkeit, dass die Szene echt Müll enthält
  "scene_gate_version": "yoloe-s-20260801"
}

4.2 Neuer optionaler scene-Block auf AnalyzeResponse (default weglassbar)

"scene": {
  "non_waste_objects": [
    { "label": "cat",   "label_de": "Katze",   "confidence": 0.88, "bbox_norm": [x,y,w,h] },
    { "label": "plant", "label_de": "Pflanze", "confidence": 0.79, "bbox_norm": [x,y,w,h] }
  ],
  "inventory_text": "3 Plastikflaschen · 1 Katze · 1 Pflanze",   // fertige Anzeige
  "counts": { "waste": 3, "non_waste": 2 }
}

4.3 domain-Achse (aus Roadmap §2, additiv)

  • Heute: domain="trash". Der Scene-Gate läuft innerhalb von trash mit — bestehende Clients bekommen das FP-Veto automatisch, ohne Code-Change.
  • Zusätzlich neu: domain="scene" = reine Objekt-/Szenen-Inventarisierung für externe API-Kunden (Industrie), ohne Material-Auszahlungs-Logik.

Für Dev/Web heißt das: keine Breaking Changes. Ihr könnt die neuen Felder ignorieren, bis ihr sie nutzt. Die Feedback-v2-Payload bleibt unverändert.


5. Mess-Protokoll — erst messen, dann verdrahten (wie bei Lauf B)

Kein Live-Deploy, bevor wir auf einem Eval-Set gemessen haben. Zwei Zahlen entscheiden:

  1. FP-Kill-Rate — Anteil der bekannten False Positives (Müll auf leerem Boden / Natur / Innenraum), die das Gate korrekt unterdrückt. Hoch = gut.
  2. TP-Retention — Anteil echter Müll-Detektionen, die das Gate fälschlich unterdrückt. Muss ~0 bleiben — ein False Negative heißt: ehrlicher Nutzer bekommt 0 TC (Hard Contract §2). Das ist die Schmerzgrenze.

Eval-Set (zusammenzustellen):

  • Positive: echte Müll-Bilder aus unserem bestehenden Val-Set (#13).
  • Negative: leerer Boden/Natur (Auto-Negatives Runde 3, harvest_negatives.py), plus Innenraum/Tier/Möbel/Pflanze aus lizenzsauberen offenen Sets.
  • Inventar-Stichprobe: ~200 handgelabelte Bilder für die Zähl-Genauigkeit („3 Flaschen + 1 Katze" — stimmt die Zählung?).

Metriken kommen als summary_scene_gate.json neben summary_eval.json (gleiche Konvention wie evaluate.py) → fließen als Snapshot in die Chronik.

Gate-Kriterium für Live-Schaltung (Vorschlag): FP-Kill ≥ 60 % bei TP-Retention ≥ 99 %. Wird die Schmerzgrenze verletzt, bleibt das Gate advisory.


6. Auszahlungs-Sicherheit — der sensible Punkt (Founder + Dev)

trash_detected speist in der App ai_status → beeinflusst TC. Deshalb:

  • Stufe advisory (Start): Das Gate liefert nur waste_likelihood + scene-Inventar. trash_detected behält exakt die heutige Semantik (len(objects) > 0). Die App zahlt wie bisher. Null Risiko für ehrliche Nutzer.
  • Stufe aktiv (später, nur mit Messung §5 + Founder- und Dev-Sign-off): Das Gate darf trash_detected bei sehr niedriger waste_likelihood überstimmen (FP-Veto). Erst wenn TP-Retention nachweislich ~100 % ist.

Hard Contracts bleiben unangetastet: KI setzt nie TC-Beträge (§5.1), Submit-Pfad bleibt non-throwing (§5.2), ai_status-Vokabular fix (§5.3).


7. Serving-Integration (Latenz-Budget)

  • Snapped Photo (log=true): voller Scene-Gate läuft mit. Latenz-Aufschlag akzeptabel (einmal pro Einreichung).
  • Live-Preview (log=false, ~1/s): hier zählt jede ms. Optionen: (a) Gate im Preview auslassen und nur auf dem Schnappschuss laufen, oder (b) nur der ultra-billige Ganzbild-Confidence-Check im Preview. Default: (a) — Preview bleibt so schnell wie heute.
  • Modell-Load wie model_loader.py: ONNX-Session einmal beim Boot, Bundle-Dir über Env — das Scene-Gate-ONNX kommt als zweites Bundle neben deploy/latest.

8. Rollout-Stufen

Stufe Inhalt Berührt Live-Auszahlung?
A0 Prototyp + Eval-Harness offline, Backbone auf Eval-Set messen nein
A1 Scene-Gate-ONNX exportiert (Kaggle), ins Serving als 2. Bundle nein (advisory)
A2 scene-Block + waste_likelihood additiv in /v1/analyze live nein (advisory)
A3 FP-Veto auf trash_detectednur nach §5-Messung + Sign-off ja

8b. A0 — Erste Messung (2026-07-25, Floor-Zahl)

Erster Lauf des Mess-Harness auf 127 echten Bildern (lizenzsauber via harvest_negatives.py / openverse CC0): 47 Litter-Positive, 80 Negative (32 leere Szenen + 48 Nicht-Müll-Objekte: Katze/Hund/Pflanze/Stuhl/Auto/Bank). Backbone: YOLO-World v2 S, zero-shot, unkalibriert (bewusst die einfachste Stufe).

τ (waste_likelihood) TP-Retention (echten Müll behalten) FP-Kill (Nicht-Müll ablehnen)
0.10 0,79 0,59
0.20 0,49 0,74
0.40 0,28 0,94

Gate-Kriterium (FP-Kill ≥ 0,60 bei TP-Retention ≥ 0,99): NICHT erfüllt. Der einzige Punkt mit ~100 % TP-Retention ist τ=0, der 0 FPs killt.

Ehrliche Einordnung — das ist ein FLOOR, nicht die echte Zahl:

  • Daten-Mismatch: openverse-Web-Bilder („litter on street" liefert auch Luftaufnahmen/Kunstfotos) sind viel härter als unser kuratiertes, nah aufgenommenes Val-Set (#13) — das reale Einsatz-Regime. Die maßgebliche Messung läuft auf Kaggle mit unserem Val-Set.
  • Roheste mögliche Definition: waste_likelihood = max Müll-Confidence, ein globaler Threshold, S-Modell, keine Kalibrierung, kein Prompt-Tuning.
  • Signal ist da: bei τ=0,10 behalten wir 79 % des echten Mülls und killen 59 % der False Positives. Nur noch nicht an der Schmerzgrenze.

Ungenutzte Hebel (Reihenfolge für A1):

  1. Per-Box-Veto statt Ganzbild-Score — der eigentliche Design-Punkt (§2): das Gate vetoiert einzelne Boxen unseres Detektors („diese Box ist eine Katze/ leerer Boden"), nicht das ganze Bild. Inhärent präziser als der hier gemessene Ganzbild-Score.
  2. Stärkeres Backbone (YOLO-World-L/X oder YOLOE-seg) — deutlich besserer Zero-Shot.
  3. Bessere waste_likelihood — Objektzahl + Fläche + Penalty bei dominantem Nicht-Müll-Objekt, statt nur max-Confidence.
  4. Per-Prompt/Material-Thresholds + Prompt-Engineering.

Schlussfolgerung: Genau deshalb advisory-first + messen-vor-schalten. Die naive Version fasst Auszahlungen nicht an — bestätigt. Das Messgerüst steht, die Hebel sind klar. Ergebnis-JSON-Konvention: summary_scene_gate.json.

8c. A1 — Per-Box-Veto (2026-07-25, lokales Experiment)

Der eigentliche Design-Hebel (§2) gebaut und lokal gemessen: unser Detektor #13 (ONNX, lokal) liefert Material-Boxen, das Open-Vocab-Modell liefert Kontext, der Per-Box-Veto entfernt eine Box nur, wenn ein diskretes Nicht-Müll-Objekt sie sicher überlagert (TP-sichere Grundregel, ml/scene_gate/veto.py). Gleiches 127-Bild-Set wie A0.

Verfahren TP-Retention (gefeuert) FP-Kill (gefeuert)
Ganzbild-Score (A0-Floor) 0,28 @ FP-Kill 0,80
Per-Box-Veto v1 (Flächen vetoieren) 0,80 0,81
Per-Box-Veto v2 (Flächen vetoieren NICHT) 0,93 0,68

Zwei bestätigte Erkenntnisse:

  1. Der Per-Box-Veto schlägt den Ganzbild-Score deutlich (Pareto): bei ~80 % FP-Kill steigt die TP-Retention von 28 % auf 80 %.
  2. Hintergrund-Flächen dürfen nicht vetoieren. „grass/road/floor/sky" aus der Veto-Liste zu nehmen (Müll liegt AUF ihnen) hob die TP-Retention 80 %→93 %. Preis: einige Leerboden-FPs überleben — die brauchen ein eigenes Signal (Confidence-Downgrade bei „nur Fläche, kein Objekt, keine Müll-Bestätigung"), keinen Flächen-Veto (der würde echten Müll auf Flächen bestrafen).

Randbefund zum FP-Problem: Unser Detektor #13 feuerte auf 39 % zufälliger Nicht-Müll-Bilder einen False Positive — der Per-Box-Veto (v2) entfernt zwei Drittel davon, ohne echten Müll anzufassen.

Grenzen dieses Laufs (ehrlich): openverse-Web-Bilder, S-Backbone, Detektor-Recall auf den harten Positiven nur 32 % (Detektor-, nicht Veto-Limit). Die 99 %-Schmerzgrenze ist noch nicht erreicht. Deshalb der nächste Schritt auf UNSEREM Val-Set (Kaggle) mit stärkerem Backbone + Inspektion der Falsch-Vetos.

8d. Maßgebliche Messung auf Kaggle — verdrahtet (ready to dispatch)

Die autoritative Messung läuft dort, wo die Daten liegen (Founder-Entscheid 2026-07-25):

  • Kernel: ml/kaggle/scene_gate_kernel.py — baut einen echten Val-Split (TACO-Positive + openverse-Hard-Negatives), lädt den deployten Detektor #13 (deploy/latest) + ein stärkeres Open-Vocab-Backbone und misst über evaluate_valset() beide Betriebsarten (whole_image vs per_box_veto) mit τ-Sweep. Ground Truth = Label-Datei nicht-leer.
  • Workflow: .github/workflows/scene-gate-eval.ymlworkflow_dispatch, Backbone wählbar (S/M/L/X, Default L), pollt bis fertig, lädt summary_scene_gate.json als Artefakt. Gleiche Kaggle-Secrets wie der Trainings-Workflow.
  • Kern-Logik lokal getestet (test_scene_gate.py, Mock-Detektor/-Weltmodell): Ground-Truth-aus-Labels + Sweep + Veto-Pfad. Der Kernel ist dünner Glue.

Auslösen: GitHub → Actions → „Scene-Gate Eval on Kaggle" → Run. Ergebnis-JSON fließt als Snapshot in die Chronik; erst danach A2-Entscheidung (additive API).

8e. Maßgebliche Messung — ERGEBNIS (2026-07-25, Kaggle GPU, Lauf #3)

Backbone yolov8l-worldv2, deployter Detektor #13 (deploy/latest), Val-Set 620 Bilder (339 mit Müll, 281 Hard-Negatives). Kriterium: FP-Kill ≥ 0,60 und TP-Retention ≥ 0,99. Artefakt: scene-gate-eval-3 (Actions-Run 30178326681).

τ whole_image TP / FP-Kill per_box_veto TP / FP-Kill
0,02 0,761 / 0,569 0,569 / 0,801
0,20 0,596 / 0,747 0,569 / 0,801
0,30 0,466 / 0,819 0,507 / 0,826
0,40 0,348 / 0,883 0,378 / 0,883
0,60 0,201 / 0,950 0,168 / 0,950
0,80 0,027 / 0,989 0,083 / 0,979

Verdikt: gate_passed = false in BEIDEN Betriebsarten. Der empfohlene Betriebspunkt fällt auf τ=0 zurück (= gar kein Gating) — es existiert kein Schwellwert, der gleichzeitig ≥99 % echten Müll behält und ≥60 % Nicht-Müll killt.

Drei belastbare Erkenntnisse:

  1. Recall-Klippe am ersten Schwellwert. Von τ=0 auf τ=0,02 bricht die TP-Retention sofort ein: whole_image 100 %→76 %, per_box 100 %→57 %. D.h. das Open-Vocab-Modell gibt ~24 % (whole_image) bzw. ~43 % (per_box) echter Müllbilder ein waste_likelihood von ~0 — es „sieht" den Müll dort gar nicht. Das ist ein Coverage-/Recall-Problem, kein Schwellwert-Tuning-Problem.
  2. Umkehrung gegenüber dem lokalen 127-Bild-Vorexperiment (§8c). Dort schlug per_box_veto das Ganzbild klar; auf dem autoritativen Set ist es umgekehrt — whole_image behält mehr echten Müll (0,76 vs 0,57 bei τ=0,02). Ursache: der Per-Box-Pfad erbt den Recall-Deckel des Detektors #13 (findet der Detektor die Box nicht, hat der Veto nichts zu bewerten → Likelihood 0). Der Ganzbild-Pfad umgeht den Detektor und ist daher recall-robuster.
  3. Als reiner FP-Killer (Recall-Verlust in Kauf genommen) liefert per_box bei τ≈0,02 einen realen Punkt: 80 % der Hintergrund-FPs weg, 57 % echter Müll bleibt. Für ein hartes Auszahlungs-Gate zu verlustig, als weiches Confidence-Downgrade-Signal aber verwertbar.

Konsequenz (ML-Call):

  • Kein hartes Gate ausliefern. §6 bleibt: strikt advisory, keine Auszahlungs-Kopplung.
  • Nächster billiger Diagnoseschritt: waste_likelihood-Histogramm über die Positiven, die 0-Signal-Fälle inspizieren. Zwei Hebel — (a) Open-Vocab-Prompts verbreitern (Vokabular deckt viele Müllarten nicht), (b) Detektor-Recall heben.
  • Ein einzelner X-Backbone-Lauf zur Absicherung, dass es keine reine Kapazitätsgrenze ist (Erwartung: hilft wenig gegen eine Coverage-Klippe).
  • Falls 99 %-Recall Produktziel bleibt: das Gate sollte eher ein gelernter Kopf / die Detektor-Confidence sein als Zero-Shot-Open-Vocab.

8f. 0-Signal-Diagnose — verdrahtet (2026-07-26)

Der Diagnoseschritt aus §8e ist jetzt im selben Kaggle-Durchlauf gebaut (evaluate_valset(collect_diagnosis=True) + bucket_misses(), lokal getestet). Jeder Lauf schreibt zusätzlich diagnosis_scene_gate.json + Kontaktbögen der übersehenen Bilder. Die entscheidende Frage, die er beantwortet — warum bekommt echter Müll likelihood ~0:

  • model_saw_nothing (n_scene_dets = 0): Das Open-Vocab-Modell fand gar nichts → reine Detektions-/Recall-Lücke (Vokabular oder Backbone zu schwach).
  • saw_but_not_waste (Objekte erkannt, aber keins zählt als Müll) → Vokabular-/Label-Mapping-Lücke (das Ding ist da, aber nicht in unserer Müll-Sprache: Kippe, Textil, Kleinteile).
  • Überlagernd: too_dark (mittlere Helligkeit), tiny_image.
  • Für den Per-Box-Pfad zusätzlich no_detector_box (Detektor #13 fand nichts → belegt den in §8e vermuteten Recall-Deckel direkt an den Zahlen).

Das Verhältnis model_saw_nothing : saw_but_not_waste entscheidet den Hebel: überwiegt Ersteres → stärkeres/anderes Backbone; überwiegt Letzteres → Prompts/ Vokabular erweitern. Feldsignal dazu: added_items aus dem Feedback-v2 (§ api.md) liefert laufend reale Miss-Beispiele aus der Produktverteilung.

8g. 0-Signal-Diagnose — ERGEBNIS (2026-07-26, L, T4)

339 Positive, Backbone yolov8l-worldv2, Detektor #13. (Ausgeführt direkt via Kaggle-Kernel v4, nicht Actions — Kontingent erschöpft; Wichtig fürs Repro: der Kernel MUSS mit --accelerator NvidiaTeslaT4 laufen. Ein Lauf ohne das Flag landet auf einer P100/sm_60, die das aktuelle PyTorch nicht unterstützt → alle Inferenzen crashen → degeneriertes „100 % übersehen". Das Workflow-YAML setzt das Flag; beim Direkt-Push nicht vergessen.)

Pfad übersehen (≤0,02) Aufschlüsselung
whole_image (A0) 81/339 (24 %) saw_but_not_waste 76 · model_saw_nothing 5 · too_dark 0 · tiny_image 0
per_box_veto (A1) 146/339 (43 %) no_detector_box 129 · boxes_present_but_zero 17

Klartext — die Recall-Klippe ist KEINE Blindheit und KEIN Dunkelheits-/Größenproblem:

  1. Ganzbild: 94 % der Misses (76/81) sind saw_but_not_waste. Das Open-Vocab- Modell sieht die Szene, aber das Müllstück wird nicht als Müll gewertet. Der Blick auf die Kontaktbögen zeigt warum: kleiner/verstreuter Müll in unruhigen Naturszenen (Sand, Schlick, Gras, flaches Wasser, Laub/Muscheln) — niedrige Salienz, nicht fehlende Fähigkeit. Nur 5/339 waren echte Blindheit (Mikro-Müll auf Textur). too_dark/tiny_image = 0 → kein Faktor. → Hebel: Vokabular/Prompts + Salienz, NICHT größeres Backbone. Konkret: Klein-Müll-Prompts ergänzen (litter, wrapper, cigarette butt, bottle cap, plastic scrap), Kachel-/Zoom-Inferenz für kleine Objekte, conf-Schwelle senken. Ein X-Backbone würde die Salienz-Klippe kaum bewegen (nur 5 echte Blindfälle).
  2. Per-Box: 88 % der Misses (129/146) sind no_detector_box. Der Engpass ist der Recall-Deckel des Detektors #13 selbst (kein Box → nichts zu vetoen). → Hebel: mehr/härtere Detektor-Trainingsdaten — genau das, was der Flywheel
    • added_items (Feedback-v2) jetzt laufend einsammeln.

Fazit: Das „Gate fällt bei 99 % Recall durch" ist erklärt — realer Müll ist oft klein/niedrig-salient. §6 bleibt bis dahin advisory. Artefakte: diagnosis_scene_gate.json, misses_saw_not_waste.jpg, misses_saw_nothing.jpg (Kaggle-Kernel alami-vision-scene-gate v4).

8h. Hebel getestet: Kachel-Inferenz + Klein-Müll-Prompts — KEIN Pareto-Gewinn (2026-07-26)

Hypothese aus §8g (kleiner/niedrig-salienter Müll → Auflösung ist der Hebel) sauber getestet: 2×2-Kachel-Inferenz (SAHI, ml/scene_gate/tiling.py) + 4 spezifische Klein-Müll-Prompts, Lauf v5 (L, T4) vs Baseline v4.

Roh sah es gut aus (Ganzbild-Miss 24 %→11 %, model_saw_nothing 5→0). Aber bei gleichem FP-Kill verschwindet der Gewinn — es war nur ein verschobener Betriebspunkt auf derselben Kurve:

FP-Kill ≥ Baseline TP Kacheln TP Δ
0,60 0,714 0,752 +0,04
0,80 0,510 0,510 ±0,00
0,90 0,339 0,357 +0,02
0,95 0,227 0,159 −0,07

(per_box_veto: flach ±0,003 — erwartet, der Pfad ist detektor-limitiert.)

Grund: Kacheln heben die Müll-Confidence auf Positiven UND Negativen (ein Sand-/Gras-Ausschnitt triggert öfter ein spurioses „Plastikfetzen"). Die Trennschärfe bleibt gleich. Der Limiter ist die Score-Aggregation („max Müll-Confidence im Bild"), nicht Auflösung oder Vokabular.

Konsequenz: Kachel-Code bleibt dormant (default tile_grid=0, getestet, wiederverwendbar), wird nicht ausgeliefert. Nächster Hebel adressiert die Trennschärfe direkt: ein gelernter Szenen-Score (kleine Klassifikation über Features des Open-Vocab-/Detektor-Outputs statt roher Max-Confidence), CV-validiert auf dem Val-Set — bewegt er die Pareto-Front, lohnt Serving; wenn nicht, ist Zero-Shot nachweislich gedeckelt und der Weg führt über Feld-Daten (added_items) + einen echten trainierten Kopf.

8i. Gelernter Szenen-Score — DURCHBRUCH (2026-07-26)

Der Hebel aus §8h getestet: statt waste_likelihood = max(Müll-Confidence) ein gelerntes lineares Logit über 11 Output-Features (ml/scene_gate/features.py: max/sum/mean/count/area der Müll-Confidences, Nicht-Müll-Max/-Count, Müll-Anteil, Detektor-Box-Statistik). Trainer ml/scripts/train_scene_score.py (numpy, kein sklearn), fair out-of-fold (5-Fold-CV) auf dem Val-Set (620 Bilder).

Max TP-Retention bei gleichem FP-Kill (OOF):

FP-Kill ≥ Baseline (max-conf) Gelernt Δ
0,60 0,743 0,971 +0,23
0,80 0,501 0,879 +0,38
0,90 0,327 0,699 +0,37
0,95 0,206 0,581 +0,38

AUC 0,73 → 0,91. Anders als Kacheln bewegt der gelernte Score die Pareto-Front massiv — bei FP-Kill 0,80 steigt die TP-Retention von 50 % auf 88 %. Damit ist bestätigt: der Limiter war die Score-Aggregation, nicht das Modell/die Auflösung.

Feature-Wichtigkeit (interpretierbar): stärkstes Signal ist ov_nonwaste_max negativ (−1,50) — ein sicher erkanntes Nicht-Müll-Objekt spricht gegen eine Auszahlung (die Veto-Intuition, gelernt), dazu ov_waste_frac/det_max_conf positiv. Das Gate sagt also: „viel Müll-Anteil + Detektor-Confidence UND kein dominantes Nicht-Müll-Objekt".

Status: Modell-Artefakt ml/scene_gate/scene_score.json + reiner Scorer features.learned_score() (kein sklearn/torch → serving-fähig). Ehrliche Grenze: trainiert auf 620 TACO-Bildern = Obergrenze-Indikation, nicht die App-Verteilung; 99 %-Recall-Hardgate ist noch nicht erreicht (88 % @ FP-Kill 0,80). Aber als advisory FP-Killer jetzt stark genug, um Wert zu liefern — und über added_items-Feld-Daten neu trainierbar.

Nächster Schritt (Task 3): den gelernten Score als waste_likelihood in build_scene verdrahten (Fallback max-conf, wenn Artefakt fehlt), dann Serving- Integration (Open-Vocab im Space) — jetzt datenbelegt lohnend.

8j. Serving-Machbarkeit — GEMESSEN, nicht vermutet (2026-07-26)

Die offene Frage war: der Space fährt onnxruntime auf cpu-basic, kein torch. Läuft der Gate dort überhaupt? Antwort: ja.

1. ONNX-Export funktioniert. YOLO-Worldv2 backt bei set_classes() die Text-Embeddings ein. Weil unser Vokabular fest ist, braucht die Laufzeit danach kein CLIP und kein torch — der Output ist ein normales Detektions- Modell (1, 72, 8400) = 4 Box-Koordinaten + 68 Klassen (= unsere Prompts). Script: ml/scripts/export_scene_gate_onnx.py → Bundle im gleichen Format wie deploy/latest, damit ModelBundle es ohne neuen Inferenz-Code lädt (§7).

2. Latenz auf 2 Threads (cpu-basic-Näherung), gemessen:

Modell Latenz (median)
Detektor #13 (heute live) 13 MB 106 ms
Szenen-Gate S (neu) 50 MB 224 ms
Summe pro Schnappschuss ~330 ms

Verdikt: tragbar — aber nur auf dem Schnappschuss-Pfad (log=true), wie §7 ohnehin vorsieht. Der Live-Preview bleibt unangetastet. L ist für CPU-Serving zu schwer → Serving-Kandidat ist S; die §8i-Messung lief auf L, der Score muss für S neu trainiert/geprüft werden (offen, siehe unten).

3. End-to-End verifiziert (Detektor #13 + Gate + gelernter Score auf echten Fotos) — und der §8i-Gewinn wird dabei sichtbar: Auf einem der saw_but_not_waste- Miss-Bilder findet das Open-Vocab-Modell null Müll-Objekte (counts.waste=0), die alte max-conf-Baseline hätte also 0.0 gesagt. Der gelernte Score zieht aus Detektor-Boxen + Kontext 0,61; eine leere Fläche bekommt 0,22.

Nebenbefund + Fix: ModelBundle.predict() stürzte bei reinen Detektions- Bundles ab (mask_coef ist dann None, wurde aber indiziert). Fiel nie auf, weil Detektor #13 einen Seg-Kopf hat. Behoben — betrifft jedes maskenlose Bundle.

Offen vor dem Ausrollen:

  • Score für S validierenerledigt, siehe §8k.
  • scene-Block-Kontrakt: Rückmeldung von PO/Monorepo (docs/SCENE_BLOCK_CONTRACT.md).
  • Bundle-Auslieferung in den Space (Größe/Boot-Zeit) + Env-Schalter.

8k. Hält der Score mit dem servebaren Backbone? — JA (2026-07-26)

§8i wurde auf L gemessen, Serving läuft aber auf S (CPU-Budget, §8j). Deshalb S-Feature-Dump + Retrain, gleiche Methode. Fairer Vergleich: die 281 Negative des L-Laufs sind eine Teilmenge der 480 des S-Laufs, Positive identisch → beide auf exakt denselben 620 Bildern ausgewertet.

Konfiguration AUC TP @ FP-Kill 0,80
L + max-conf (Ausgangslage) 0,73 0,50
L + gelernt (§8i) 0,91 0,88
S + max-conf 0,66 0,39
S + gelernt (ausgeliefert) 0,85 0,77

Verdikt: S trägt. Der entscheidende Vergleich ist nicht S-gelernt vs L-gelernt (−0,11), sondern S + gelernt (0,77) vs L + max-conf (0,50): das billige, servebare Backbone mit gutem Score schlägt das teure mit naivem Score deutlich. Serving mit S ist damit verantwortbar — Gate-Kosten 224 ms statt eines nicht-servebaren L.

Wichtig (Fallstrick): Die Features sind backbone-spezifisch — ein auf L trainierter Score passt nicht auf S-Verteilungen. Das ausgelieferte Artefakt ml/scene_gate/scene_score.json ist deshalb auf S-Features trainiert und trägt das Feld backbone; train_scene_score.py --backbone schreibt es mit. Wer das Serving-Backbone wechselt, muss den Score neu trainieren.

Ehrliche Einordnung: 0,77 TP-Retention bei 80 % FP-Kill heißt: als advisory FP-Killer stark (4 von 5 Fehl-Auszahlungen abfangbar, 77 % echter Müll unbehelligt, Rest in Human-Review). Für ein hartes Auto-Credit-Gate (≥99 % Recall) reicht es weiterhin nicht — dafür braucht es Feld-Daten (added_items) und einen trainierten Kopf statt Zero-Shot.


9. Offene Entscheidungen (Founder)

  1. Erst-Fokus: FP-Killer zuerst (Geld-Leck, messbar, risikoarm) — oder das sichtbare Voll-Inventar zuerst (Demo-Wow, „ChatGPT-Konter")? Empfehlung: ein Modell baut beides; wir validieren + schalten den FP-Nutzen zuerst, das Inventar läuft additiv mit.
  2. Auszahlungs-Stufe: Bleibt es bis zur Messung strikt advisory (§6)? Empfehlung: ja.