본문으로 건너뛰기

가이드

튜토리얼: 이미지, 검사와 이미지를 보는 심사 모델로 평가하기

이미지를 만드는 함수를 평가합니다. 여기서는 캡션도 작성하는 제품 썸네일 생성기입니다. 모델이 필요 없는 측정으로 썸네일을 검사하고, 각 썸네일을 보는 모델로 캡션을 심사하고, 그 심사 모델을 사람과 대조해 측정한 다음, 실제 변경을 출시합니다. 모든 단계는 모델도 네트워크도 없이 이 머신에서 실행되며, 선택 단계에서 실제 비전 모델로 바꿀 수 있습니다.

만들 것

카탈로그 서비스는 제품 사진을 받아 썸네일을 저장하고, 제품의 색을 말하는 한 줄 캡션을 씁니다. 버전을 출시해도 되는지는 세 가지 질문이 결정합니다.

  • 각 썸네일이 카탈로그에 맞는가: 웹 형식이고, 최대 256 × 256픽셀, 최대 50 kB인가? 이것은 측정이며, media_check가 썸네일의 바이트로 수행합니다.
  • 각 썸네일이 여전히 제품의 사진이며, 빈 이미지나 잘린 이미지나 다른 제품이 아닌가? image_matches_reference가 지각 해시로 사진과 비교합니다.
  • 각 캡션이 쇼핑객이 보는 색을 말하는가? 이것은 눈이 필요하므로, 루브릭 심사 모델에게 썸네일을 보여 줍니다.

사진과 썸네일은 프로젝트 폴더에 머뭅니다. 시스템은 그것들을 참조(media/v1)로 기록합니다: 각 파일의 위치, 유형, 크기, sha256입니다. 케이스, 실행, 심사 모델, 게이트 같은 용어는 핵심 개념에 정의되어 있고, 미디어에 관한 모든 것은 이미지, 오디오, 비디오에 있습니다.

사전 준비

  • Python 3.11 이상, 그리고 미디어 추가 기능을 포함한 Oloproof. 이 추가 기능은 이미지 비교를 위한 Pillow를 더합니다:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • 패키지와 함께 제공되는 예제 프로젝트. 새 디렉터리로 복사해 그곳에서 작업합니다:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • 대역 심사 모델을 위한 비어 있는 포트 8798(비어 있지 않다면 두 곳 모두에서 바꾸십시오).

"선택 사항: 실제 비전 모델" 이전의 모든 단계는 오프라인이고 결정적입니다. API 키도, 제공자 계정도, 비용도 없습니다.

파일

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

모든 명령은 product-thumbnails/에서 실행하십시오.

애플리케이션이 이미지를 기록합니다

# 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)는 파일을 기술합니다: 그 file:// 위치, MIME 유형, 크기, sha256이며, 이름은 파일 이름을 따릅니다. role="input"은 시스템이 받은 사진을 표시하고, 썸네일은 기본값인 출력입니다. current_case().media(...)는 둘을 media/v1 아티팩트 하나로 기록하며, oloproof.yaml의 records: [media/v1]는 시스템이 그것을 기록한다고 선언합니다. 아무것도 복사되지 않습니다. 검사나 심사 모델은 나중에 각 파일을 다시 읽고, 바이트가 더 이상 레코드와 일치하지 않는 파일은 거부합니다.

이 기준선에는 결함이 두 가지 있습니다. 썸네일의 너비가 320픽셀이고, 캡션이 사진 가운데 픽셀의 색을 말하는데, 제품이 한쪽에 치우쳐 있으면 그 픽셀은 배경입니다. 각 버전은 썸네일을 thumbs/<version>/ 아래에 쓰므로, 나중 실행이 이전 실행이 기록한 파일을 덮어쓰는 일은 없습니다.

데이터셋

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

input은 시스템이 받는 것입니다. expected.photo는 썸네일과 비교하는 참조입니다. colour와 shape는 케이스를 읽는 사람을 위한 것이며, 어떤 평가기도 읽지 않습니다.

평가기 고르기

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는 각 출력 이미지를 측정합니다. 형식은 첫 바이트에서, 크기는 헤더에서 읽으며, 시스템이 선언한 값에서는 결코 읽지 않습니다. image_matches_reference는 썸네일과 expected.photo가 지정하는 사진을 해시하고, 두 해시가 64비트 중 최대 10비트만 다를 때 통과합니다. 크기 변경이나 재압축은 해시를 몇 비트 움직이고, 다른 그림은 많은 비트를 움직입니다. 둘 다 모델을 호출하지 않으므로 둘 다 첫 실행부터 결정합니다.

media: output은 케이스의 출력 이미지를 심사 모델의 요청에 첨부하며, 각각은 프롬프트에서 이름으로 지칭됩니다. 심사 모델은 사람의 레이블이 심사 모델과의 일치를 보여 주기 전까지 아무것도 결정하지 않으므로, 세 번째 규칙은 그것을 기다립니다.

대역 심사 모델

루브릭 심사 모델은 프롬프트와 첨부한 이미지를 모델에 보내고 {"pass": true|false, "rationale": "..."}를 읽어 옵니다. 여기서 모델은 judge_server.py이며, 127.0.0.1에서 같은 API를 말하지만 모델은 아닙니다. 첨부된 썸네일을 디코딩해 배경의 흰색과 회색을 제외하고 가장 흔한 색을 찾고, 그 색을 말하는 캡션을 통과시킵니다. 두 번째 터미널에서 시작하고 계속 실행해 두십시오:

python judge_server.py --port 8798

정책

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

모든 썸네일은 맞아야 하고 자기 사진이어야 합니다. 캡션의 최소 80%가 올바른 색을 말해야 하며, 이는 구간의 하한이 80%를 넘을 때에만 결정됩니다.

실행하기

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 │

맞는 썸네일은 하나도 없고, 모든 썸네일은 자기 사진이며, 캡션 20개 중 12개가 올바른 색을 말합니다. 심사 모델의 규칙이 결정되지 않은 것은 아직 아무도 심사 모델을 측정하지 않았기 때문입니다.

실패 살펴보기

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.'

검사의 사유는 측정이며 measured로 표시됩니다. 심사 모델의 사유는 그 자신의 말이며, 아무도 확인하지 않았습니다. 사진과 썸네일은 oloproof export RUN_ID 후 워크벤치의 케이스 페이지에서도 각 판정 옆에 나타나며, 이 폴더에서 읽어 옵니다. 호스팅된 워크스페이스에서는 여러분의 브라우저가 oloproof collect에서 그것들을 가져오며, 워크스페이스는 결코 그것들을 보관하지 않습니다(이미지, 오디오, 비디오).

사람과 대조해 심사 모델 측정하기

실행의 케이스에서 무작위 표본을 심사 모델의 판정 없이 시트로 뽑고, 사람이 각 썸네일과 캡션을 보게 한 다음, 그 사람이 말한 것을 가져옵니다. 여기서는 fill_labels.py가 그 사람을 대신해 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

그런 다음 그 레이블과 대조해 심사 모델을 측정하고, 누가 했는지 기록하십시오:

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

이미지를 보는 심사 모델은 텍스트를 읽는 심사 모델과 똑같이 측정됩니다. 호스팅된 워크스페이스에서는 리뷰 대기열이 각 케이스의 이미지를 리뷰어에게 보여 주며, 이미지는 리뷰어의 브라우저가 여러분의 수집기에서 가져옵니다(심사 모델).

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)

이제 심사 모델이 결정할 수 있으며, 60%로는 충분하지 않습니다.

실제로 변경하기

app_v2.py는 256픽셀 썸네일을 만들고, 제품이 어디에 있든 배경을 제외하고 사진에서 가장 흔한 색을 말합니다. 이를 app.py 위에 복사하고, oloproof.yaml의 system 아래에 version: main-colour를 설정하고, 심사 모델을 계속 실행한 채로:

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      │

심사 모델은 검증된 것과 같은 평가기 버전이므로 그 판정이 효력을 갖습니다.

후보를 기준선과 비교하기

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

선택 사항: 실제 비전 모델

이 단계는 오프라인 경로를 벗어납니다. 이미지를 읽는 모델이 필요합니다. 로컬에서는 비전 모델을 받은 Ollama(ollama pull qwen2.5vl)로, 키와 비용이 없습니다. 클라우드에서는 api_key_env와 함께 provider: anthropic 또는 openai를 쓰며, 단어뿐 아니라 이미지마다 요금이 청구됩니다.

# 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

이 명령은 리뷰어가 레이블을 단 케이스를 실제 모델로 심사하고, 채택하지 않은 채 그 일치도를 보고합니다. 채택하려면 대역 대신 oloproof.yaml에 넣으십시오. 새 모델은 새 평가기 버전이므로, 위와 같이 레이블과 대조해 검증하십시오.

문제 해결

증상원인과 해결
caption_names_colour가 모두 누락심사 서버가 실행 중이 아니거나 base_url에 없습니다. oloproof inspect RUN_ID --failures가 이유를 보여 줍니다.
image_matches_reference가 Pillow를 지목하며 실행을 멈춤미디어 추가 기능을 설치하십시오: pip install 'oloproof[media]'.
케이스가 media_unavailable로 누락됨기록된 파일이 실행 후에 옮겨지거나 삭제되었습니다. 실행을 보관하는 동안에는 그 실행이 기록한 파일을 보관하십시오.
실행이 is not the recorded content로 멈춤기록된 파일이 실행 후에 바뀌었습니다. 검사나 심사 모델은 다른 바이트를 기록된 바이트로 읽는 일이 결코 없습니다. 각 버전의 파일은 각자의 위치에 쓰십시오.
검증한 뒤에도 evaluator_not_validated심사 모델(모델, 엔드포인트, 포트, 루브릭, media)을 바꿔 새 버전을 만들었습니다. 그 버전을 검증하십시오.

한계

  • 대역 심사 모델은 픽셀을 셉니다. 심사 품질이 아니라 작업 흐름을 보여 줍니다.
  • 지각 해시는 색이 아니라 구도와 밝기를 읽습니다. 사진의 썸네일을 빈 이미지나 다른 장면과 구별할 수는 있지만, 같은 스튜디오에서 찍은 다른 제품과는 구별하지 못합니다. 큰 빈 영역이 있는 평면 그래픽은 사진보다 해시를 더 많이 움직입니다.
  • 레이블 20개로는 일치 구간이 넓습니다. 의존하는 심사 모델이라면 무작위로, 블라인드로 더 많이 레이블을 다십시오.
  • 오디오와 비디오도 다른 설정으로 같은 방식으로 작동합니다: 이미지, 오디오, 비디오를 보십시오.