Ga naar de inhoud

Handleidingen

Tutorial: afbeeldingen, met controles en een judge die ze ziet

Evalueer een functie die afbeeldingen maakt, hier een thumbnailmaker voor producten die ook een bijschrift schrijft. Controleer de thumbnails met metingen die geen model nodig hebben, beoordeel de bijschriften met een model dat elke thumbnail ziet, meet die judge tegen een mens en lever daarna een echte wijziging op. Elke stap draait op deze machine zonder model en zonder netwerk, en een optionele stap zet er een echt visiemodel voor in de plaats.

Wat je gaat bouwen

Een catalogusdienst ontvangt een productfoto, slaat een thumbnail op en schrijft een bijschrift van één regel dat de kleur van het product noemt. Drie vragen beslissen of een versie mag worden uitgebracht:

  • Past elke thumbnail in de catalogus: een webformaat, hoogstens 256 bij 256 pixels, hoogstens 50 kB? Dat is een meting, gedaan door media_check aan de bytes van de thumbnail.
  • Is elke thumbnail nog de foto van het product, en geen lege afbeelding, een uitsnede of een ander product? image_matches_reference vergelijkt hem met de foto op perceptuele hash.
  • Noemt elk bijschrift de kleur die een klant ziet? Daar zijn ogen voor nodig, dus een rubric-judge krijgt de thumbnail te zien.

De foto's en thumbnails blijven in de projectmap. Het systeem legt ze bij referentie vast (media/v1): waar elk bestand staat, zijn type, grootte en sha256. Termen als case, run, judge en gate worden gedefinieerd in Kernbegrippen; alles over media staat in Afbeeldingen, audio en video.

Vereisten

  • Python 3.11 of nieuwer, en Oloproof met zijn media-extra, dat Pillow toevoegt om afbeeldingen te vergelijken:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • Het voorbeeldproject, dat met het pakket meekomt. Kopieer het naar een nieuwe map en werk daar:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • Poort 8798 vrij voor de vervangende judge (pas hem anders op beide plaatsen aan).

Elke stap tot "Optioneel: een echt visiemodel" is offline en deterministisch: geen API-sleutel, geen provideraccount, geen kosten.

De bestanden

product-thumbnails/
  app.py                              the thumbnailer under test (baseline)
  app_v2.py                           the candidate change
  judge_server.py                     a stand-in vision judge speaking the OpenAI API on 127.0.0.1
  rubrics/caption_names_colour.md     the judge's rubric
  oloproof.yaml                       the suite
  release.yaml                        rules for a run
  compare.yaml                        a rule for a comparison
  data/products.jsonl                 20 cases
  photos/                             the 20 product photos the cases name
  make_photos.py                      how the photos were drawn
  labels/reviewer_verdicts.csv        one person's verdicts on the baseline's captions
  fill_labels.py                      copies those verdicts into a labelling sheet

Voer elk commando uit vanuit product-thumbnails/.

De applicatie legt haar afbeeldingen vast

# app.py (excerpt)
@system(name="thumbnailer", version=VERSION, records=("media/v1",))
def describe(case: dict[str, Any]) -> dict[str, str]:
    photo_path = HERE / case["photo"]
    photo = Image.open(photo_path).convert("RGB")
    thumb = photo.copy()
    thumb.thumbnail((320, 320))
    out = HERE / "thumbs" / VERSION / photo_path.name
    out.parent.mkdir(parents=True, exist_ok=True)
    thumb.save(out)
    given = media_file(photo_path, role="input")
    made = media_file(out, width=thumb.width, height=thumb.height)
    current_case().media([given, made])
    centre = photo.getpixel((photo.width // 2, photo.height // 2))
    return {"caption": f"A {colour_name(centre)} product."}

media_file(path) beschrijft een bestand: zijn file://-locatie, MIME-type, grootte en sha256, genoemd naar het bestand. role="input" markeert de foto die het systeem kreeg; de thumbnail is een output, de standaard. current_case().media(...) legt beide vast als één media/v1-artefact, en records: [media/v1] in oloproof.yaml declareert dat het systeem dat vastlegt. Er wordt niets gekopieerd: een controle of een judge leest elk bestand later terug en weigert er een waarvan de bytes niet meer met het record overeenkomen.

Deze baseline heeft twee fouten. Zijn thumbnails zijn 320 pixels breed, en zijn bijschrift noemt de kleur van de middelste pixel van de foto, en dat is de achtergrond zodra het product aan een kant staat. Elke versie schrijft haar thumbnails onder thumbs/<version>/, dus een latere run overschrijft nooit de bestanden die een eerdere run vastlegde.

De dataset

{"id": "p03", "input": {"photo": "photos/p03.png"}, "expected": {"photo": "photos/p03.png", "colour": "yellow", "shape": "triangle"}}

input is wat het systeem ontvangt. expected.photo is de referentie waarmee de thumbnail wordt vergeleken; colour en shape staan er voor een mens die de case leest, en geen evaluator leest ze.

De evaluators kiezen

evaluators:
  # The catalogue's limits, measured from the thumbnail's bytes: a web format, at most 256 by
  # 256 pixels, at most 50 kB.
  - type: media_check
    criterion: thumbnail_fits
    kind: image
    formats: [png, jpeg, webp]
    max_width: 256
    max_height: 256
    max_bytes: 50000
  # The thumbnail is still the product's photo: a perceptual hash within 10 of 64 bits.
  - type: image_matches_reference
    criterion: same_photo
    reference_field: photo
    max_distance: 10
  # The caption, judged by a model that sees the thumbnail.
  - type: rubric_judge
    criterion: caption_names_colour
    provider: openai_compatible
    model: stand-in-vision
    base_url: http://127.0.0.1:8798/v1
    rubric_file: rubrics/caption_names_colour.md
    media: output

media_check meet elke uitvoerafbeelding: het formaat uit de eerste bytes, de afmetingen uit de header, nooit uit wat het systeem declareerde. image_matches_reference hasht de thumbnail en de foto die expected.photo noemt, en slaagt wanneer de hashes in hoogstens 10 van hun 64 bits verschillen; verkleinen of opnieuw comprimeren verschuift een hash een paar bits, een ander beeld verschuift hem er veel. Geen van beide roept een model aan, dus beide beslissen vanaf de eerste run.

media: output voegt de uitvoerafbeeldingen van de case bij het verzoek aan de judge, elk bij naam genoemd in zijn prompt. Een judge beslist niets totdat labels van mensen laten zien dat hij het met hen eens is, dus de derde regel wacht daarop.

De vervangende judge

Een rubric-judge stuurt zijn prompt en de bijgevoegde afbeeldingen naar een model en leest {"pass": true|false, "rationale": "..."} terug. Hier is het model judge_server.py, dat dezelfde API spreekt op 127.0.0.1 en geen model is: hij decodeert de bijgevoegde thumbnail, zoekt de meest voorkomende kleur buiten het wit en grijs van de achtergrond, en laat een bijschrift slagen dat die kleur noemt. Start hem in een tweede terminal en laat hem draaien:

python judge_server.py --port 8798

De policy

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: thumbnails-fit
    metric: thumbnail_fits
    kind: observed_count
    max_failures: 0
  - id: same-photo
    metric: same_photo
    kind: observed_count
    max_failures: 0
  - id: caption-floor
    metric: caption_names_colour
    min: 0.80

Elke thumbnail moet passen en moet zijn foto zijn. Minstens 80% van de bijschriften moet de juiste kleur noemen, alleen beslist wanneer de ondergrens van het interval boven 80% uitkomt.

Draaien

oloproof run
Gate: BLOCK (exit 1)
│ thumbnails-fit │ thumbnail_fits       │ FAIL                  │ observed_failures_exceed_limit   │
│ same-photo     │ same_photo           │ PASS                  │ observed_failures_within_limit   │
│ caption-floor  │ caption_names_colour │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated          │
│ thumbnail_fits       │ 0.0%     │ [0.0%, 16.9%]   │ 0 / 20 observed · 0 missing · 0 excluded  │
│ same_photo           │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ caption_names_colour │ 60.0%    │ [36.0%, 80.9%]  │ 12 / 20 observed · 0 missing · 0 excluded │

Geen enkele thumbnail past, elke thumbnail is zijn foto, en 12 van de 20 bijschriften noemen de juiste kleur. De regel van de judge is onbeslist omdat niemand de judge al heeft gemeten.

De mislukkingen bekijken

oloproof inspect RUN_ID --failures
p03
  output: {"caption": "A white product."}
  thumbnail_fits: failed
    measured: p03.png: width 320 px is above 256 px
  caption_names_colour: failed
    judge text, not verified: the product is yellow; the caption says 'a white product.'

De reden van een controle is een meting, gemarkeerd als measured; die van een judge zijn zijn eigen woorden, die niemand heeft gecontroleerd. De foto's en thumbnails verschijnen ook naast elk oordeel op de pagina van de case in de workbench, na oloproof export RUN_ID, gelezen uit deze map; een gehoste werkruimte laat je browser ze ophalen bij oloproof collect en bewaart ze nooit (Afbeeldingen, audio en video).

De judge tegen een mens meten

Trek een willekeurige steekproef van de cases van de run in een sheet, zonder de oordelen van de judge, laat een mens elke thumbnail en elk bijschrift bekijken, en importeer wat die zei. Hier speelt fill_labels.py die persoon en kopieert zijn oordelen uit labels/reviewer_verdicts.csv:

oloproof labels export RUN_ID --criterion caption_names_colour --sample 20 --local --out sample.csv
python fill_labels.py sample.csv
oloproof labels import sample.csv

Meet daarna de judge tegen die labels, en leg vast wie dat deed:

oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alice
caption_names_colour: sha256:53605dd6... is now VALIDATED
  agreement 100.0% [83.1%, 100.0%] · 20 of 20 labelled cases agreed · 0 labelled but not judged · kappa 1.000

Een judge die afbeeldingen ziet, wordt precies zo gemeten als een judge die tekst leest. In een gehoste werkruimte toont de reviewwachtrij de afbeeldingen van elke case aan de reviewer, opgehaald door diens browser bij je collector (Judges).

oloproof gate RUN_ID --policy release.yaml
thumbnails-fit: FAIL (observed_failures_exceed_limit)
same-photo: PASS (observed_failures_within_limit)
caption-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)

De judge mag nu beslissen, en 60% is niet genoeg.

Een echte wijziging maken

app_v2.py maakt thumbnails van 256 pixels en noemt de meest voorkomende kleur van de foto buiten de achtergrond, waar het product ook staat. Kopieer het over app.py, zet version: main-colour onder system in oloproof.yaml, laat de judge draaien, en:

oloproof run
Gate: ALLOW (exit 0)
│ thumbnails-fit │ thumbnail_fits       │ PASS  │ observed_failures_within_limit │
│ same-photo     │ same_photo           │ PASS  │ observed_failures_within_limit │
│ caption-floor  │ caption_names_colour │ PASS  │ lower_bound_meets_minimum      │

De judge is dezelfde evaluatorversie die gevalideerd werd, dus zijn oordelen tellen.

De kandidaat met de baseline vergelijken

oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
thumbnail_fits: +100.0 points [+52.9, +100.0] · 20 paired · 0 missing · 0 excluded
caption_names_colour: +40.0 points [+1.8, +72.7] · 20 paired · 0 missing · 0 excluded
  captions-better  caption_names_colour  superiority  PASS  difference_above_zero

Optioneel: een echt visiemodel

Deze stap verlaat het offline pad. Hij heeft een model nodig dat afbeeldingen leest: lokaal Ollama met een visiemodel (ollama pull qwen2.5vl), geen sleutel en geen kosten; in de cloud provider: anthropic of openai met api_key_env, gefactureerd per afbeelding en per woord.

# live_judge.yaml
type: rubric_judge
criterion: caption_names_colour
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
rubric_file: rubrics/caption_names_colour.md
media: output
timeout_s: 300
oloproof evaluators try live_judge.yaml

Dat beoordeelt de cases die je reviewer labelde met het echte model en meldt de overeenstemming, zonder het over te nemen. Om het over te nemen, zet je het in oloproof.yaml in plaats van de vervanger: een nieuw model is een nieuwe evaluatorversie, dus valideer hem tegen de labels, zoals hierboven.

Problemen oplossen

SymptoomOorzaak en oplossing
caption_names_colour helemaal ontbrekendDe judgeserver draait niet of niet op base_url; oloproof inspect RUN_ID --failures toont waarom.
image_matches_reference stopt de run en noemt PillowInstalleer de media-extra: pip install 'oloproof[media]'.
Een case is ontbrekend met media_unavailableEen vastgelegd bestand is na de run verplaatst of verwijderd. Bewaar de bestanden die een run vastlegde zolang je de run bewaart.
Een run stopt met is not the recorded contentEen vastgelegd bestand is na de run gewijzigd. Een controle of judge leest nooit andere bytes als de vastgelegde; schrijf de bestanden van elke versie naar een eigen plek.
evaluator_not_validated nadat je valideerdeJe wijzigde de judge (model, endpoint, poort, rubric, media) en maakte een nieuwe versie. Valideer die.

Beperkingen

  • De vervangende judge telt pixels. Hij demonstreert de werkwijze, niet de kwaliteit van beoordelen.
  • De perceptuele hash leest compositie en licht, geen kleur: hij onderscheidt een thumbnail van de foto van een lege afbeelding of een andere scène, niet het ene product van het andere uit dezelfde studio. Vlakke graphics met grote lege vlakken verschuiven hem meer dan foto's.
  • Twintig labels geven een breed overeenstemmingsinterval. Label er meer, willekeurig en blind, voor een judge waarop je vertrouwt.
  • Audio en video werken op dezelfde manier met andere instellingen: zie Afbeeldingen, audio en video.