本文へスキップ

ガイド

チュートリアル: 画像を、チェックとそれを見るジャッジで評価する

画像を作る関数を評価します。ここでは、キャプションも書く製品サムネイル生成器です。モデルを必要としない測定でサムネイルをチェックし、各サムネイルを見るモデルでキャプションを判定し、そのジャッジを人に対して測定し、そのうえで実際の変更をリリースします。すべてのステップはこのマシン上で、モデルもネットワークも使わずに実行でき、任意のステップで実際の視覚モデルに差し替えます。

作るもの

カタログサービスは製品写真を受け取り、サムネイルを保存し、製品の色を示す 1 行のキャプションを書きます。バージョンをリリースしてよいかは、3 つの問いで決まります。

  • 各サムネイルはカタログに収まるか。Web 用の形式で、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(...) は両方を 1 つの media/v1 アーティファクトとして記録し、oloproof.yaml の records: [media/v1] は、システムがそれを記録することを宣言します。何もコピーされません。チェックやジャッジは後で各ファイルを読み直し、バイトがもう記録と一致しないファイルは拒否します。

このベースラインには 2 つの欠陥があります。サムネイルの幅が 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 は、ケースの出力画像をジャッジのリクエストに添付し、プロンプトの中でそれぞれに名前を付けます。ジャッジは、人のラベルがジャッジとの一致を示すまで何も判断しないので、3 つ目のルールはそれを待ちます。

代役のジャッジ

ルーブリックジャッジは、プロンプトと添付した画像をモデルに送り、{"pass": true|false, "rationale": "..."} を読み取ります。ここでのモデルは judge_server.py です。これは 127.0.0.1 上で同じ API を話しますが、モデルではありません。添付されたサムネイルをデコードし、背景の白と灰色以外で最も多い色を見つけ、その色を示すキャプションを合格にします。2 つ目のターミナルで起動し、動かしたままにしてください:

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 │

収まるサムネイルは 1 つもなく、すべてのサムネイルはその写真であり、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)で、キーも費用も不要です。クラウドでは、provider: anthropic または openai と api_key_env を使い、各単語だけでなく各画像についても課金されます。

# 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 個のラベルでは一致度の区間は広くなります。頼りにするジャッジには、無作為かつ盲検で、より多くのラベルを付けてください。
  • 音声と動画も、別の設定で同じように動作します。画像、音声、動画を参照してください。