Özgür Işık Damar
8 Min Lesezeit

CLIP vs SigLIP für einen türkischen Produktkatalog: eine Marken-Affinitäts-Ablation

Im zweiten Monat hatte unsere CTO aufgehört zu fragen 'ist es CLIP-Qualität?' und begann zu fragen 'ist es SigLIP-fair?'

hybrid-searchembeddingscomputer-vision

Wir hatten 2,4 Millionen Produktbilder, meist von Verkäuferinnen mit dem Handy aufgenommen, meist mit türkischsprachigen Bildunterschriften, die unser Text-Encoder nicht treffen konnte. Die erste Bildsuche ging mit CLIP ViT-B/32 live. Nach einer Woche waren die Marken-Affinitätswerte in eine sehr spezifische Richtung falsch: Jeder dunkle Schuh sah für das Modell wie Nike aus.

Dieser Satz ist die ganze Geschichte, warum wir CLIP verlassen haben. Der Rest sind die Belege.

Der Katalog, den wir embedded haben

Der Bild-Tower sitzt innerhalb der Hybrid-Search-Pipeline. Text-, BM25- und Bild-Embeddings werden parallel abgerufen, dann fusioniert. Der Bild-Tower trägt etwa ein Drittel der Last für visuelle Anfragen — ein Foto-Upload, ein Screenshot, ein vages "sowas in der Art" — und einen kleineren, aber nicht-trivialen Anteil der Textanfragen über einen Text-zu-Bild-Projection-Head.

Die Katalog-Skalierung war der leicht zu beschreibende Teil. 2,4M Bilder bei einem Bild pro SKU, durchschnittliche Breite um 1200 px, JPEG-Qualität überall verteilt, weil Verkäufer-Handys nicht konsistent sind. Der schwer zu beschreibende Teil war die Verteilung. Etwa 38 Prozent des Katalogs war Bekleidung. Innerhalb der Bekleidung war der lange Schwanz türkische und mazedonische Lokalmarken — Namen, die ein CLIP-Trainingsset nie gesehen hätte.

Mit CLIP live gehen, die offensichtliche Wahl

CLIP war der offensichtliche Startpunkt. Das war das Modell, für das das Team bereits Infrastruktur hatte. Die Vektordimension passt sauber in Qdrants Named-Vector-Slot. Es gab gute türkische Blogposts darüber, wie man es verkabelt. Die Throughput-Zahlen waren bekannt.

# Was wir zuerst ausgeliefert haben. Einmal embedden, einmal upserten, ewig suchen.
# Das war die Version, die dunkle Schuhe zu Nike machte.
import torch
from transformers import CLIPModel, CLIPProcessor
 
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").eval().to("cuda")
proc  = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
 
def embed_image(pil_img) -> list[float]:
    inputs = proc(images=pil_img, return_tensors="pt").to("cuda")
    with torch.no_grad():
        feats = model.get_image_features(**inputs)
        feats = feats / feats.norm(dim=-1, keepdim=True)
    return feats.cpu().numpy()[0].tolist()

Im Offline-Benchmark war CLIP in Ordnung. Recall@10 gegen ein gelabeltes Set von 8.000 reinen Bildanfragen lag bei 0,71. Latenz pro Embed bei 38 ms auf einer A10 — deutlich unter unserem 80-ms-Budget für den Bildschritt.

Wir rollten aus, und die Suchen sahen plausibel aus. Sahen plausibel aus.

Der Tag, an dem wir es bemerkten

Marken-Affinität war eine Metrik, die das Merchant-Ops-Team für einen anderen Zweck gebaut hatte. Sie bewertete die Produkte jedes Händlers daran, wie sauber sich ihre Bilder um die beabsichtigte Markenidentität des Händlers gruppieren. Eine Boutique, die minimalistische skandinavische Kleidung verkauft, sollte hohe Kohärenz über den eigenen Katalog haben. Ein Multi-Brand-Shop sollte naturgemäß geringere Kohärenz haben, aber die Marken darin sollten weiterhin erkennbar sein.

Am Mittwoch nach dem Rollout schickte mir der Merchant-Ops-Lead ein Chart. Der Marken-Affinitätswert für jeden dunklen Schuh im Katalog — über mindestens sechzig türkische und mazedonische Boutique-Marken hinweg — war Richtung "Nike-förmig" gedriftet. Der Bild-Embedder projizierte sie alle in dieselbe Nachbarschaft. Ein Paar lokal gefertigte Yıldız-Lederstiefel und ein Paar Nike Cortez waren im Bildraum Nahnachbarn, obwohl das Einzige, was sie teilten, dunkel und schuhförmig war.

Der Merchant-Ops-Lead fragte höflich, ob das so beabsichtigt sei.

War es nicht. Es waren die Trainingsdaten.

Warum CLIP das tat

CLIP wurde auf etwa 400 Millionen aus dem offenen Web gescrapten Bild-Text-Paaren trainiert. In diesem Korpus übersteigt die Anzahl der Foto-Text-Paare für Nike, Adidas, Puma die für jede türkische oder mazedonische Lokalmarke bei Weitem. Die dichten Regionen von CLIPs Bildraum rund um "Schuh + dunkle Farbe" sind dicht besiedelt von den populären Marken, und die Long-Tail-Marken sind dünn besiedelte Außenposten, die durch einfache Nearest-Neighbour-Geometrie in die dichte Nachbarschaft gezogen werden.

Das ist kein Bug in CLIP. CLIP tut genau das, wofür es trainiert wurde. Es ist ein Bug darin, dass unser Katalog CLIP auf halbem Weg trifft. Der Katalog ist schwer in Long-Tail-Marken. CLIP ist schwer in globalen Marken. Die beiden Verteilungen überlappten sich dort nicht, wo es zählte.

Ich verbrachte zwei Tage damit, einen Head auf CLIP mit unseren eigenen Markenlabels zu fine-tunen. Es half ein wenig. Recall@10 stieg vielleicht um 2 Punkte, Markenfairness bewegte sich kaum. Die Information, die nötig war, um den Bias zu fixen, lag nicht im Head, sondern im Encoder.

Also tauschten wir den Encoder.

Der SigLIP-Swap

Was dir niemand über die Migration von CLIP zu SigLIP sagt, ist, wie klein der Diff ist. Die Architekturen sind ähnlich genug, dass sich der Embedding-Code kaum ändert:

# Die gesamten Migrationskosten. Eine Import-Zeile, eine Modell-ID.
# Die Pipeline downstream änderte sich nicht.
from transformers import AutoModel, AutoProcessor
 
model = AutoModel.from_pretrained("google/siglip-base-patch16-256").eval().to("cuda")
proc  = AutoProcessor.from_pretrained("google/siglip-base-patch16-256")
 
def embed_image(pil_img) -> list[float]:
    inputs = proc(images=pil_img, return_tensors="pt").to("cuda")
    with torch.no_grad():
        feats = model.get_image_features(**inputs)
        feats = feats / feats.norm(dim=-1, keepdim=True)
    return feats.cpu().numpy()[0].tolist()

Das war's. Gleicher Input, gleiche Output-Form, gleicher Qdrant-Upsert-Pfad. Der Katalog musste natürlich neu vektorisiert werden — jedes Bild wird einmal mehr auf der GPU-Farm embedded — aber im Code war der Diff eine Import-Zeile.

Was nicht klein ist, ist das, was sich darunter geändert hat. SigLIPs Trainings-Loss ist ein Sigmoid-Loss statt CLIPs kontrastivem Softmax. Die praktische Konsequenz: SigLIPs Bildraum ist um die markenreichen Regionen herum weniger gekrümmt, weil der Loss nicht jedes Paar gegen jedes andere Paar im Batch diskriminieren muss. Das Objective ist pro Paar statt pro Batch. Die Geometrie ist dort flacher, wo die Daten dünn sind.

Die Zahlen, gegen dieselben 8k Anfragen

Auf dem Offline-Set:

  • Recall@10: CLIP 0,71, SigLIP 0,83. +12 Prozentpunkte.
  • Markenfairness-Index: CLIP 0,42, SigLIP 0,70. +28 Punkte.
  • P95-Embed-Latenz: CLIP 38 ms, SigLIP 53 ms. +15 ms.

Die Latenzkosten waren real, und wir haben sie bezahlt. SigLIP-Base bei Patch-16-256 ist um einen brauchbaren Betrag schwerer als CLIP ViT-B/32. Wir erwogen, auf das kleinere siglip-small-patch16-224 zu wechseln, um die Latenz zurückzuholen, ließen das Eval laufen, sahen die Markenfairness-Zahl absacken und behielten das größere.

Der Markenfairness-Anstieg war die Schlagzeile. Long-Tail-Marken wurden nicht mehr in populäre Markennachbarschaften absorbiert. Die Yıldız-Stiefel und die Nike Cortez trennten sich im Vektorraum. Sie waren nicht mehr Nahnachbarn. Sie waren das, was sie tatsächlich waren — zwei verschiedene Schuhe von zwei verschiedenen Marken, die zufällig beide dunkel waren.

Der Bonus, den wir nicht erwartet haben

Unsere Text-zu-Bild-Projektion — für Textanfragen, die den Bildindex treffen wollen — wurde mit SigLIP spürbar besser. Türkische Textanfragen besonders. SigLIPs Training enthielt eine viel größere multilinguale Teilmenge als das ursprüngliche CLIP, und die Ausrichtung zwischen türkischen Bildunterschriften und Bildregionen war enger. Eine Anfrage wie kırmızı kemerli yazlık elbise (rot gegürteltes Sommerkleid) lieferte kohärente Kleiderbilder in einer Rate, die wir auf CLIP ohne zusätzliches Fine-Tune nicht gesehen hatten.

Wir taten dafür nichts. Es war ein Nebeneffekt des Swaps. Dass dasselbe Modell gleichzeitig multilingualer und fairer zu Long-Tail-Marken wurde, ist der Teil, der den Trade leicht machte.

Worüber sich die Modellgrößen-Leute irren

Es gibt einen reflexartigen Instinkt im Team, "ist das neue Modell größer?" zu fragen, wenn sich etwas verbessert. SigLIP-Base ist nicht besonders größer als CLIP ViT-B/32 — sie liegen in derselben Größenordnung. Der Gewinn war nicht Größe. Der Gewinn war der Trainings-Loss und der Trainingsdatenmix.

Hätten wir auf CLIP-Large mit doppelten Parametern gesprungen, wären unsere Markenfairness-Zahlen leicht schlechter geworden, weil dieselben verzerrten Daten, aggressiver modelliert, dieselben Verzerrungen intensivieren. Wir haben es getestet. CLIP-Larges Markenfairness lag bei 0,39 gegenüber CLIP-Bases 0,42. Größer half nicht. Anders schon.

Auf den ersten Blick kontraintuitiv. Im Nachhinein offensichtlich. Der Encoder ist eine Linse. Die Trainingsdaten sind das Licht. Eine größere Linse, fokussiert auf verzerrtes Licht, produziert ein schärferes verzerrtes Bild.

Ein kleines Markenfairness-Eval

Der Markenfairness-Index läuft nächtlich auf einer 12.000-Item-Stichprobe. Die Mathematik ist nicht exotisch — sie ist ein Vergleich, wie eng deine Top-N-Nachbarn auf dem Markenlabel clustern, normalisiert gegen die Verteilung der Marke im Gesamtkatalog.

def brand_fairness(query_results: list[Result],
                   catalogue_brand_counts: dict[str, int]) -> float:
    # Höher ist fairer. Eine perfekt proportionale Stichprobe erreicht 1,0.
    # Eine Stichprobe, die populäre Marken überrepräsentiert, liegt unter 1.
    total = sum(catalogue_brand_counts.values())
    expected = {b: c / total for b, c in catalogue_brand_counts.items()}
    observed = Counter(r.brand for r in query_results)
    n = len(query_results)
    observed = {b: c / n for b, c in observed.items()}
    # KL-artige Divergenz, invertiert — höher = fairer.
    return 1.0 / (1.0 + sum(
        o * math.log(o / expected.get(b, 1e-9))
        for b, o in observed.items() if o > 0
    ))

Wir verfolgen das pro Händler und pro Kategorie. Es ist die Metrik, die "die Suche kennt insgeheim nur die Top-Marken" abfängt, bevor Händler es tun.

Sechs Monate später

Wir sind immer noch auf SigLIP-Base-Patch16-256. Wir haben den Markenfairness-Head einmal nachtrainiert, als eine neue Welle mazedonischer Boutiquen onboarden und die Verteilung erneut verschoben hat. Wir sind für keinen Produktionstraffic zu CLIP zurückgekehrt.

Im zweiten Monat hatte unsere CTO aufgehört zu fragen "ist es CLIP-Qualität?" und begann zu fragen "ist es SigLIP-fair?" Das ist die Art kleiner sprachlicher Verschiebung, die dir sagt, dass das Team internalisiert hat, worum es eigentlich ging. Das Problem war nie visuelle Qualität auf Stockfotografie. Das Problem war, ob die Linse, die wir an unseren Katalog hielten, jemals auf etwas trainiert wurde, das wie unser Katalog aussah.

War sie nicht. Wir tauschten die Linse. Der Katalog sah endlich nach sich selbst aus.

// wenn du schon hier bist