メインコンテンツへスキップ
これはインタラクティブなノートブックです。ローカルで実行することも、以下のリンクを使用することもできます。
このチュートリアルでは、手書きの患者情報の画像に対して固有表現認識 (NER) を行うコンピュータビジョンパイプラインを構築、トレース、評価する方法を説明します。最後には、ビジョン言語モデル (VLM) を利用し、画像から構造化フィールドをどれだけ正確に抽出できるかを測定する W&B Weave 評価 を備えた、実際に動作する光学文字認識 (OCR) パイプラインが完成します。このガイドは、プロンプトを反復的に改善し、マルチモーダル抽出パイプラインの品質を体系的に測定するために Weave を使用したい開発者を対象としています。 以下のセクションでは、5 つの段階を順に説明します。プロンプトの作成と改善、データセットの取得、NER パイプラインの構築、Scorer の定義、そして評価の実行です。

事前準備

始める前に、必要なライブラリをインストールしてインポートし、W&B APIキーを取得して、Weave プロジェクトを初期化してください。この手順を完了すると、環境から W&B に認証できるようになり、トレースを Weave プロジェクトにログできるようになります。
# 必要な依存関係をインストールする
!pip install openai weave -q
python
import json
import os

from google.colab import userdata
from openai import OpenAI

import weave
python
# APIキーを取得する
os.environ["OPENAI_API_KEY"] = userdata.get(
    "OPENAI_API_KEY"
)  # 左側のメニューからColabの環境シークレットとしてキーを設定してください
os.environ["WANDB_API_KEY"] = userdata.get("WANDB_API_KEY")

# プロジェクト名を設定する
# PROJECT の値を実際のプロジェクト名に置き換えてください
PROJECT = "vlm-handwritten-ner"

# Weave プロジェクトを初期化する
weave.init(PROJECT)

Weave でプロンプトを作成して改善する

適切なプロンプトエンジニアリングは、モデルがエンティティを正しく抽出できるようにするうえで重要です。このセクションでは、最初のプロンプトを作成し、それを Weave に公開して経時的な変更をトラッキングできるようにしたうえで、より厳格な検証ルールを使って改善します。 まず、画像データから何を抽出し、どのような形式で出力するかをモデルに指示する基本的なプロンプトを作成します。次に、そのプロンプトを Weave に保存し、トラッキングと反復的な改善を行います。
# Weave で prompt オブジェクトを作成する
prompt = """
Extract all readable text from this image. Format the extracted entities as a valid JSON.
Do not return any extra text, just the JSON. Do not include ```json```
Use the following format:
{"Patient Name": "James James","Date": "4/22/2025","Patient ID": "ZZZZZZZ123","Group Number": "3452542525"}
"""
system_prompt = weave.StringPrompt(prompt)
# Weave に prompt を公開する
weave.publish(system_prompt, name="NER-prompt")
次に、出力に含まれるエラーを減らせるよう、指示と検証ルールをさらに追加してプロンプトを改善します。同じ名で改訂版を公開すると、Weave がそのプロンプトを新しいバージョンとしてトラッキングするため、反復ごとの結果を比較できます。
better_prompt = """
You are a precision OCR assistant. Given an image of patient information, extract exactly these fields into a single JSON object (and nothing else):

- Patient Name
- Date (MM/DD/YYYY)
- Patient ID
- Group Number

Validation rules:
1. Date must match MM/DD/YY; if not, set Date to "".
2. Patient ID must be alphanumeric; if unreadable, set to "".
3. Always zero-pad months and days (e.g. "04/07/25").
4. Omit any markup, commentary, or code fences.
5. Return strictly valid JSON with only those four keys.

Do not return any extra text, just the JSON. Do not include ```json```
Example output:
{"Patient Name":"James James","Date":"04/22/25","Patient ID":"ZZZZZZZ123","Group Number":"3452542525"}
"""
# promptを編集する
system_prompt = weave.StringPrompt(better_prompt)
# 編集したpromptをWeaveに公開する
weave.publish(system_prompt, name="NER-prompt")

データセットを取得する

プロンプトを用意したら、次はパイプラインに渡す入力データが必要です。OCR パイプラインの入力として使用する、手書きのメモのデータセットを取得します。 データセット内の画像はすでに base64 エンコードされているため、LLM は前処理なしでこのデータを使用できます。
# 以下の Weave プロジェクトからデータセットを取得する
dataset = weave.ref(
    "weave://wandb-smle/vlm-handwritten-ner/object/NER-eval-dataset:G8MEkqWBtvIxPYAY23sXLvqp8JKZ37Cj0PgcG19dGjw"
).get()

# データセット内の特定のサンプルにアクセスする
example_image = dataset.rows[3]["image_base64"]

# example_image を表示する
from IPython.display import HTML, display

html = f'<img src="{example_image}" style="max-width: 100%; height: auto;">'
display(HTML(html))

NER パイプラインを構築する

プロンプトとデータセットの準備ができたので、それらを VLM に接続する NER パイプラインを構築します。パイプラインは 2 つの関数で構成されます。
  • データセット内の PIL 画像を受け取り、VLM に渡せる画像の base64 エンコード済み string 表現を返す encode_image 関数。
  • 画像とシステムプロンプトを受け取り、システムプロンプトの記述に従って、その画像から抽出した固有表現を返す extract_named_entities_from_image 関数。
# GPT-4-Visionを使用したトレース可能な関数
def extract_named_entities_from_image(image_base64) -> dict:
    # LLMクライアントの初期化
    client = OpenAI()

    # 指示プロンプトの設定
    # Weaveに保存されたプロンプトをweave.ref("weave://wandb-smle/vlm-handwritten-ner/object/NER-prompt:FmCv4xS3RFU21wmNHsIYUFal3cxjtAkegz2ylM25iB8").get().content.strip()で使用することも可能です
    prompt = better_prompt

    response = client.responses.create(
        model="gpt-4.1",
        input=[
            {
                "role": "user",
                "content": [
                    {"type": "input_text", "text": prompt},
                    {
                        "type": "input_image",
                        "image_url": image_base64,
                    },
                ],
            }
        ],
    )

    return response.output_text
次に、named_entity_recognation という関数を作成します。この関数は次の処理を行います。
  • 画像データを NER パイプラインに渡します。
  • 結果を正しい形式の JSON で返します。
関数の実行を W&B UI で自動的にトラッキングしてトレースするには、@weave.op() デコレータ を使用します。 named_entity_recognation が実行されるたびに、完全なトレース結果が Weave UI に表示されます。トレースを表示するには、Weave プロジェクトの Traces タブにアクセスします。
# 評価用NER関数
@weave.op()
def named_entity_recognation(image_base64, id):
    result = {}
    try:
        # 1) vision opを呼び出し、JSON文字列を取得する
        output_text = extract_named_entities_from_image(image_base64)

        # 2) JSONを一度だけパースする
        result = json.loads(output_text)

        print(f"Processed: {str(id)}")
    except Exception as e:
        print(f"Failed to process {str(id)}: {e}")
    return result
最後に、データセットに対してパイプラインを実行し、結果を確認します。このステップでは、次のセクションで評価するモデルの出力が生成されます。 次のコードはデータセットを反復処理し、結果をローカル file processing_results.json に保存します。結果は Weave UI でも確認できます。
# 結果を出力
results = []

# データセット内のすべての画像をループ処理
for row in dataset.rows:
    result = named_entity_recognation(row["image_base64"], str(row["id"]))
    result["image_id"] = str(row["id"])
    results.append(result)

# すべての結果をJSONファイルに保存
output_file = "processing_results.json"
with open(output_file, "w") as f:
    json.dump(results, f, indent=2)

print(f"Results saved to: {output_file}")
Weave UI の Traces 表には、次のように表示されます。
NER パイプラインの実行結果が表示された Weave Traces 表。

Weave を使ってパイプラインを評価する

VLM を使用して NER を実行するパイプラインを作成したので、次は Weave を使ってこれを体系的に評価し、どの程度うまく機能するかを確認できます。パイプラインを評価すると、スポットチェックに頼るのではなく、データセット全体にわたる抽出品質を測定できます。Weave の評価について詳しくは、Evaluations Overview を参照してください。 Weave の評価を構成する基本要素が Scorer です。Scorers は AI の出力を評価し、評価メトリクスを返します。AI の出力を受け取り、それを分析して、結果を dict として返します。Scorers は必要に応じて入力データを参照として使用できるほか、評価に関する説明や推論などの追加情報を出力することもできます。 このセクションでは、パイプラインを評価するために 2 つの scorers を作成します。
  • プログラムによる scorer。
  • LLM-as-a-judge scorer。

プログラムによる Scorer

最初の Scorer は、LLM を使用せずに実行される決定的なチェックです。プログラムによる Scorer である check_for_missing_fields_programatically は、モデルの出力 (named_entity_recognition 関数の出力) を受け取り、結果内で欠落している、または空の keys を特定します。 このチェックは、モデルがいずれのフィールドも抽出できなかったサンプルを特定するのに役立ちます。
# Scorerの実行をトラッキングするためにweave.op()を追加する
@weave.op()
def check_for_missing_fields_programatically(model_output):
    # すべてのエントリに必須のキー
    required_fields = {"Patient Name", "Date", "Patient ID", "Group Number"}

    for key in required_fields:
        if (
            key not in model_output
            or model_output[key] is None
            or str(model_output[key]).strip() == ""
        ):
            return False  # このエントリにはフィールドの欠落または空欄がある

    return True  # すべての必須フィールドが存在し、空欄でない

LLM-as-a-judge scorer

プログラムによる scorer では、欠落しているフィールドや空のフィールドしか検出できないため、抽出された値が画像に表示されている内容と一致しているかどうかを確認するために、2 つ目の scorer が必要です。この評価ステップでは、評価結果が実際の NER のパフォーマンスを反映するように、画像データとモデルの output の両方を提供します。参照されるのはモデルの出力だけでなく、画像の内容そのものです。 このステップで使用する scorer check_for_missing_fields_with_llm は、LLM (具体的には OpenAI の gpt-4o) を使用してスコアリングを行います。eval_prompt の内容で指定されているとおり、check_for_missing_fields_with_llmBoolean 値を出力します。すべてのフィールドが画像内の情報と一致し、形式も正しい場合、scorer は true を返します。いずれかのフィールドが欠落している、空である、不正確である、または一致していない場合、結果は false となり、scorer は問題を説明するメッセージも返します。
# LLM-as-a-judge のシステムプロンプト

eval_prompt = """
You are an OCR validation system. Your role is to assess whether the structured text extracted from an image accurately reflects the information in that image.
Only validate the structured text and use the image as your source of truth.

Expected input text format:
{"Patient Name": "First Last", "Date": "04/23/25", "Patient ID": "131313JJH", "Group Number": "35453453"}

Evaluation criteria:
- All four fields must be present.
- No field should be empty or contain placeholder/malformed values.
- The "Date" should be in MM/DD/YY format (e.g., "04/07/25") (zero padding the date is allowed)

Scoring:
- Return: {"Correct": true, "Reason": ""} if **all fields** match the information in the image and formatting is correct.
- Return: {"Correct": false, "Reason": "EXPLANATION"} if **any** field is missing, empty, incorrect, or mismatched.

Output requirements:
- Respond with a valid JSON object only.
- "Correct" must be a JSON boolean: true or false (not a string or number).
- "Reason" must be a short, specific string indicating all the problem — e.g., "Patient Name mismatch", "Date not zero-padded", or "Missing Group Number".
- Do not return any additional explanation or formatting.

Your response must be exactly one of the following:
{"Correct": true, "Reason": null}
OR
{"Correct": false, "Reason": "EXPLANATION_HERE"}
"""

# weave.op() を追加して Scorer の実行をトラッキングする
@weave.op()
def check_for_missing_fields_with_llm(model_output, image_base64):
    client = OpenAI()
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "developer", "content": [{"text": eval_prompt, "type": "text"}]},
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_base64,
                        },
                    },
                    {"type": "text", "text": str(model_output)},
                ],
            },
        ],
        response_format={"type": "json_object"},
    )
    response = json.loads(response.choices[0].message.content)
    return response

評価を実行する

両方の Scorers を定義したので、評価を実行できるようになりました。渡された dataset を自動的に反復処理し、結果をまとめて Weave UI にログする評価 call を定義します。 次のコードは評価を開始し、NER パイプラインの各出力に 2 つの Scorers を適用します。結果は Weave UI の Evals タブで確認できます。
evaluation = weave.Evaluation(
    dataset=dataset,
    scorers=[
        check_for_missing_fields_with_llm,
        check_for_missing_fields_programatically,
    ],
    name="Evaluate_4.1_NER",
)

print(await evaluation.evaluate(named_entity_recognation))
前のコードを実行すると、Weave UI の評価表へのリンクが生成されます。リンクを開いて結果を確認し、任意のモデル、プロンプト、データセット間でパイプラインの異なる反復を比較してください。Weave UI は、以下のような可視化をチーム用に自動的に作成します。
データセット全体で Scorer の出力を比較した Weave 評価結果。