Moonshot PerceptionBench、多モーダル視覚モデル評価ワークフローを公開
本文の状態
日本語全文を表示中
詳細モードで約12分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
MarkTechPost
Moonshot AI が公開した Vision モデル評価ベンチマーク「PerceptionBench」を、Colab 環境で動作するエンドツーエンドのワークフローと自動化された判定システムを用いて実装・分析する方法が示されている。
AI深層分析を開く2026年8月4日 08:13
AI深層分析
キーポイント
多機能な視覚知能評価ベンチマーク
OCR、数え上げ、位置特定、文脈推論、比較、深さ理解、ハルシネーション検出など、微細な視覚知能能力を測定する「PerceptionBench」が紹介される。
堅牢なデータ読み込み戦略
バランスの取れたサブセットを読み込むために、マルチステージストリーミングとダウンロード戦略を採用し、ベース64エンコードされた画像のデコードや正規化を行う手法が詳述される。
柔軟な評価ハーンの構築
盲検事前基準(blind-prior baseline)、OpenAI 互換 API、および Hugging Face のローカルビジョン言語モデルに対応した統一された評価ハーンが実装される。
自動化された判定と分析
ルールベースまたは LLM 支援による自動判定を実行し、ブートストラップ信頼区間の計算や難易度別パフォーマンスの比較が行われる。
設定可能な評価パラメータの定義
データセットのスプリットやカテゴリごとのサンプル数、画像処理の解像度と品質、そしてローカルモデルまたはAPIバックエンドの切り替えなど、多様な評価設定を辞書形式で管理する。
重要な引用
This multimodal benchmark measures fine-grained visual perception capabilities across tasks such as OCR, counting, localization, contextual reasoning, comparison, depth understanding, and hallucination detection.
We begin by configuring a Colab-compatible environment, installing the required libraries, and loading a balanced subset of the dataset through a robust multi-stage streaming and download strategy.
CFG = dict( REPO = "moonshotai/PerceptionBench", SPLIT = "train", N_PER_CATEGORY = 12, MAX_SCAN = 1200, SEED = 0, LOAD_MODE = "stream", BACKEND = "blind" ... )
We configure the PerceptionBench environment, define the dataset, backend, image-processing, judging, and output settings, and initialize reproducible random behavior.
編集コメントを表示
編集コメント
Vision モデルの評価において、単なる精度の数値だけでなく、ハルシネーションや深さ理解といった多様な能力を網羅的に測定する手法は極めて貴重である。提供される Colab 環境での実装例は、開発者が即座に自社のモデル評価パイプラインを組み立てるための強力な足がかりとなる。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
本チュートリアルでは、PerceptionBench のためのエンドツーエンド評価ワークフローを設計します。このマルチモーダルベンチマークは、OCR、カウント、位置特定、文脈推論、比較、深さ理解、ハルシネーション検出といったタスクにわたる微細な視覚知覚能力を測定するものです。
まず、Colab 互換環境を設定し、必要なライブラリをインストールします。その後、堅牢なマルチステージのストリーミングおよびダウンロード戦略を通じて、バランスの取れたデータセットサブセットを読み込みます。次に、Base64 でエンコードされた画像のデコード、インターリーブされた画像プレースホルダーのパース、各例を一貫したレコード形式への正規化を行います。さらに、データセットの能力分布、画像要件、回答タイプ、ソースベンチマークについても分析します。
これらを基に、盲検事前ベースライン、OpenAI 互換マルチモーダル API、およびローカルの Hugging Face ビジョン・ランゲージモデルをサポートする統合評価ハーンネスを構築します。また、ルールベースおよびオプションの LLM 支援による判定機能を実装し、ブートストラップ信頼区間を計算します。さらに、難易度スライスごとの性能を検証し、同梱されたリーダーボードとの能力プロファイルを比較し、再現可能な予測結果と報告アーティファクトのエクスポートを行います。
Copy CodeCopiedUse a different Browser
import os, sys, io, re, json, time, math, base64, random, hashlib, subprocess, warnings
from collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
warnings.filterwarnings("ignore")
CFG = dict(
REPO = "moonshotai/PerceptionBench",
SPLIT = "train",
N_PER_CATEGORY = 12,
MAX_SCAN = 1200,
SEED = 0,
LOAD_MODE = "stream",
BACKEND = "blind",
API_BASE = os.environ.get("PB_API_BASE", "https://api.openai.com/v1"),
API_KEY = os.environ.get("PB_API_KEY", ""),
API_MODEL = os.environ.get("PB_API_MODEL", "gpt-4o-mini"),
API_WORKERS = 4,
API_MAX_TOKENS = 512,
LOCAL_MODEL = "HuggingFaceTB/SmolVLM2-2.2B-Instruct",
LOCAL_MAX_NEW = 128,
MAX_IMAGE_SIDE = 1024,
JPEG_QUALITY = 90,
JUDGE = "rule",
NUM_REL_TOL = 0.0,
OUT_DIR = "/content/perceptionbench_out" if os.path.isdir("/content") else "./perceptionbench_out",
INSTALL_DEPS = True,
SHOW_PLOTS = True,
)
random.seed(CFG["SEED"])
os.makedirs(CFG["OUT_DIR"], exist_ok=True)
def _sh(pkgs):
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *pkgs],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if CFG["INSTALL_DEPS"]:
print("[setup] installing dependencies (quiet, ~30s on a cold Colab)…")
_sh(["datasets>=3.0.0", "huggingface_hub>=0.25.0", "pillow", "pandas",
「numpy」「matplotlib」「requests」「pyarrow」をインストールします。
CFG["BACKEND"] が "local" の場合、以下のライブラリも追加でインストールします:
- transformers>=4.51.0
- accelerate
- torch
- num2words
import numpy as np
import pandas as pd
import requests
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image
matplotlib.rcParams.update({"figure.dpi": 110, "font.size": 9, "axes.grid": True,
"grid.alpha": .25, "axes.spines.top": False,
"axes.spines.right": False})
print("[setup] ready\n")
PerceptionBench の環境を設定し、データセットやバックエンド、画像処理、判定ロジック、出力先などのパラメータを定義します。また、再現性を確保するために乱数の初期化も行います。
必要となるライブラリとして、データ読み込み、数値解析、可視化、HTTP 通信、画像処理用のパッケージをインストールしています。さらに Matplotlib の設定を調整し、出力ディレクトリの準備も完了させています。これにより、Google Colab でもローカル環境でも評価ワークフローが安定して実行できるようになります。
Copy CodeCopiedUse a different Browser
def _iter_rows(repo, split, mode, max_scan):
"""逐次、負荷の重い戦略を試しながら辞書形式の行を生成する。"""
from datasets import load_dataset
if mode == "full":
print("[load] full download (~1.63 GB) …")
ds = load_dataset(repo, split=split)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
try:
from huggingface_hub import HfApi, hf_hub_url
api = HfApi()
files = api.list_repo_files(repo, repo_type="dataset", revision="refs/convert/parquet")
pq = sorted(f for f in files if f.endswith(".parquet") and f"/{split}/" in f)
if pq:
urls = [hf_hub_url(repo, f, repo_type="dataset", revision="refs/convert/parquet") for f in pq]
print(f"[load] streaming {len(urls)} parquet shard(s) from refs/convert/parquet")
ds = load_dataset("parquet", data_files=urls, split="train", streaming=True)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
except Exception as e:
print(f"[load] parquet stream unavailable ({type(e).__name__}: {e}); falling back")
try:
print("[load] streaming original data files")
ds = load_dataset(repo, split=split, streaming=True)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
except Exception as e:
print(f"[load] json stream failed ({type(e).__name__}); doing a full download")
ds = load_dataset(repo, split=split)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
def stratified_subset(repo, split, n_per_cat, max_scan, mode):
"""error_category(10 の原子的能力)間でバランスの取れたサンプリングを行う。
バランスが重要である:ベンチマークは「能力プロファイル」を報告するものであり、
不均衡なサンプルでは、シャード内で最初に出現した能力に依存した加重平均しか
得られないためだ。
"""
buckets, scanned, t0 = defaultdict(list), 0, time.time()
for row in _iter_rows(repo, split, mode, max_scan):
scanned += 1
cat = row.get("error_category") or "unknown"
if len(buckets[cat]) < n_per_cat:
buckets[cat].append(row)
filled = sum(1 for v in buckets.values() if len(v) >= n_per_cat)
print(f" scanned={scanned:5d} categories={len(buckets):2d} "
f"filled={filled:2d} {time.time()-t0:5.1f}s", end="\r")
if scanned >= 250 and len(buckets) >= 10 and all(len(v) >= n_per_cat for v in buckets.values()):
break
rows = [r for v in buckets.values() for r in v]
random.Random(CFG["SEED"]).shuffle(rows)
print(f"\n[load] scanned {scanned} rows -> kept {len(rows)} across "
f"{len(buckets)} capabilities ({time.time()-t0:.1f}s)")
return rows, scanned
ROWS, N_SCANNED = stratified_subset(
CFG["REPO"], CFG["SPLIT"], CFG["N_PER_CATEGORY"], CFG["MAX_SCAN"], CFG["LOAD_MODE"])
私たちは、まず変換された Parquet のストリーミングを試行し、失敗した場合は元のファイルのストリーミングにフォールバックし、必要に応じて完全なダウンロードを実行する耐性のあるデータセットローダーを実装しました。データセットのスキャン時には処理行数を制限しつつ、error_category フィールドを用いて例を能力別のバケットに整理します。その後、各視覚的機能が同等数の評価質問に寄与できるよう、バランスの取れたシャッフル済みサブセットを作成します。
Copy CodeCopiedUse a different Browser
DATA_URI_RE = re.compile(r"^data:(image/[A-Za-z0-9.+-]+);base64,(.*)$", re.S)
PLACEHOLDER_RE = re.compile(r"")
def decode_image(entry):
"""data-URI string | raw b64 | bytes | HF Image dict -> PIL.Image (RGB)."""
if isinstance(entry, Image.Image):
return entry.convert("RGB")
if isinstance(entry, dict):
if entry.get("bytes"):
return Image.open(io.BytesIO(entry["bytes"])).convert("RGB")
if entry.get("path"):
return Image.open(entry["path"]).convert("RGB")
if isinstance(entry, (bytes, bytearray)):
return Image.open(io.BytesIO(entry)).convert("RGB")
s = str(entry).strip()
m = DATA_URI_RE.match(s)
b64 = m.group(2) if m else s
b64 = re.sub(r"\s+", "", b64)
b64 += "=" * (-len(b64) % 4)
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
def load_images(row):
imgs = row.get("image") or []
if isinstance(imgs, (str, bytes, dict)):
imgs = [imgs]
out = []
for e in imgs:
try:
out.append(decode_image(e))
except Exception as err:
print(f" [warn] undecodable image on idx={row.get('index')}: {err}")
return out
def shrink(img, max_side, quality):
"""Downscale + re-encode. Returns (PIL, data_uri). Controls the token bill:
a 3000px screenshot can cost >2k vision tokens per image, and these
questions carry up to 8 images each."""
w, h = img.size
if max(w, h) > max_side:
s = max_side / max(w, h)
画像リサイズとエンコード処理
画像を指定したサイズにリサイズし、JPEG 形式で圧縮して Base64 エンコードする関数です。まず、画像の幅 (w) と高さ (h) にスケール係数 s を掛け合わせ、最小値が 1 になるように計算します。その後、メモリ上のバッファに JPEG として保存し、Base64 でエンコードして URI を生成します。
img = img.resize((max(1, int(w * s)), max(1, int(h * s))), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality)
uri = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
return img, uriプレースホルダーに基づく問題文の分割
問題文中の画像プレースホルダー(例:text?)を解析し、テキストと画像の順序をリストとして抽出する関数です。この処理により、プレースホルダーで参照されていない画像は末尾に自動的に追加され、視覚的証拠が失われることがなくなります。
def split_on_placeholders(problem, n_images):
"""`text?` -> [('image',0),('text','…'),('image',1)…]
Any image never referenced by a placeholder is appended at the end, so we
never silently drop visual evidence."""
parts, last = [], 0
for m in PLACEHOLDER_RE.finditer(problem):
chunk = problem[last:m.start()].strip()
if chunk:
parts.append(("text", chunk))
i = int(m.group(1)) - 1
if 0 <= i < n_images:
parts.append(("image", i))
last = m.end()
remaining_text = problem[last:].strip()
if remaining_text:
parts.append(("text", remaining_text))
# Append any unreferenced images at the end
referenced_indices = {idx for _, idx in parts if _ == "image"}
for i in range(n_images):
if i not in referenced_indices:
parts.append(("image", i))
return partsデータセットの統計情報出力
データセットの主要な統計情報をコンソールに出力する処理です。画像の最大辺長の中央値と最大値、および出典ベンチマークの上位 8 つのカウントを表示します。
print(f"-- median longest image edge: {df.max_side.median():.0f}px "
f"(max {df.max_side.max():.0f}px)")
print("\n-- provenance (top source benchmarks) --")
print(df["source_bmk"].value_counts().head(8).to_string())
print(f"\n-- newly-authored (source_bmk == 'NA'): {(df.source_bmk=='NA').mean():.1%} "
f"(card: 40% authored / 60% decomposed)\n")Hugging Face データセット統計の取得
指定された Hugging Face リポジトリのデータセット統計情報を取得する関数です。API を呼び出してエラーカテゴリなどの詳細を取得し、処理します。
def hf_full_stats(repo, split="train", config="default"):
try:
r = requests.get("https://datasets-server.huggingface.co/statistics",
params={"dataset": repo, "config": config, "split": split},
timeout=30)
r.raise_for_status()
for col in r.json().get("statistics", []):
if col["column_name"] == "error_category":
# Process error category statistics here
pass
except Exception as e:
print(f"Failed to fetch stats for {repo}: {e}")freq = col["column_statistics"].get("frequencies", {})
if freq:
tot = sum(freq.values())
print("-- FULL-CORPUS capability distribution (3,000 rows, via datasets-server) --")
for k, v in sorted(freq.items(), key=lambda x: -x[1]):
print(f" {cat_code(k):6s} {k:34s} {v:5d} {v/tot:6.1%}")
print()
return freq
except Exception as e:
print(f"[stats] datasets-server unavailable ({type(e).__name__}); "
f"using subset statistics only\n")
return None
FULL_FREQ = hf_full_stats(CFG["REPO"], CFG["SPLIT"])
if CFG["SHOW_PLOTS"]:
fig, ax = plt.subplots(1, 3, figsize=(13, 3.4))
order = [c for c in CODE_ORDER if c in set(df.code)] + \
[c for c in sorted(set(df.code)) if c not in CODE_ORDER]
df.code.value_counts().reindex(order).plot.bar(ax=ax[0], color="#4C72B0")
ax[0].set_title("Questions per atomic capability"); ax[0].set_xlabel("")
df.n_images.value_counts().sort_index().plot.bar(ax=ax[1], color="#DD8452")
ax[1].set_title("Images per question"); ax[1].set_xlabel("# images")
df.ans_type.value_counts().plot.barh(ax=ax[2], color="#55A868")
ax[2].set_title("Answer surface form")
plt.tight_layout(); plt.show()
データ URI、生の Base64 文字列、バイト配列、PIL オブジェクト、Hugging Face の画像辞書から読み込んだ画像を、一貫した RGB フォーマットに変換します。各データセットの行は、質問文、回答、画像、能力ラベル、解像度、プレースホルダー数、ソース情報を含む構造化レコードとして正規化されます。その後、データの能力カバレッジ、回答形式、画像数、解像度特性、ソースベンチマークを分析し、データセットのプロファイルを可視化します。
Copy CodeCopiedUse a different Browser
def show_record(rec, max_imgs=4):
imgs = rec["images"][:max_imgs]
n = len(imgs)
fig, axes = plt.subplots(1, n, figsize=(4.2 * n, 4.2))
axes = np.atleast_1d(axes)
for a, im in zip(axes, imgs):
a.imshow(im); a.axis("off")
q = re.sub(r"\s+", " ", rec["problem"])
q = (q[:150] + "…") if len(q) > 150 else q
fig.suptitle(f"[{rec['code']} · {rec['category']}] {q}\n"
f"gold = {rec['answer']!r} | src = {rec['source_bmk']}",
fontsize=9, y=1.06)
plt.tight_layout(); plt.show()
if CFG["SHOW_PLOTS"]:
print("=" * 78); print("§5 ONE EXEMPLAR PER CAPABILITY"); print("=" * 78)
seen = set()
for rec in RECORDS:
if rec["code"] not in seen:
seen.add(rec["code"]); show_record(rec)
if len(seen) >= 4:
break
SYSTEM_PROMPT = (
"You are a careful visual perception assistant. Examine the image(s) closely "
"before answering. Every question has a short, uniquely determined answer.\n"
"Reason briefly if needed, then end your reply with exactly one line:\n"
"Answer: \n"
"Give only the value (a number, word, or short phrase) after 'Answer:' — "
"no units, no explanation, no full sentence."
)
def build_payload(rec, max_side, quality):
"""Returns (interleaved_parts, resized_pils, data_uris)."""
resized, uris = [], []
for im in rec["images"]:
pil, uri = shrink(im, max_side, quality)
resized.append(pil); uris.append(uri)
問題文のプレースホルダーに基づいて parts を分割し、結果画像のサイズに合わせて処理します。
parts = split_on_placeholders(rec["problem"], len(resized))
if rec["hint"]:
parts.append(("text", f"Hint: {rec['hint']}"))
return parts, resized, urisヒントが存在する場合は、テキスト形式で追加します。その後、各要素の種別(テキストまたは画像)に応じて OpenAI 形式のメッセージ構造を構築します。
def parts_to_openai(parts, uris):
content = []
for kind, val in parts:
if kind == "text":
content.append({"type": "text", "text": val})
else:
content.append({"type": "image_url", "image_url": {"url": uris[val]}})
return [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": content}]ベンチマークの代表的な例については、関連する画像を読みやすいグリッド状に配置し、各質問に対してその能力、正解(参照回答)、出典を併記して提示します。評価対象モデルには、すべての画像を確認した上で、一貫した形式で簡潔な最終回答を返すよう指示する厳格なマルチモーダルシステムプロンプトを定義しています。
また、画像のサイズ調整を行い、質問内のプレースホルダーに対する相対的な配置を保ちながら、最終的に OpenAI 互換性のマルチモーダルメッセージに変換します。
class Backend:
name = "base"
def predict(self, rec): raise NotImplementedError
def predict_batch(self, recs):
return [self.predict(r) for r in recs]
class BlindPriorBackend(Backend):
"""テキストのみによる下限モデル。質問の表面形式から推測される「回答の事前分布」に基づいて回答するもので、画像ピクセルは一切読み込まない。
これは精度の数値に意味を持たせるための対照条件だ。「ヒンジはいくつあるか?」といった問いには答えやすい事前知識(小整数が支配的)が存在するため、視覚モデルがこの基準をわずかに上回る程度なら、それは知覚しているのではなく単なる推測に過ぎない。"""
name = "blind-prior"
def __init__(self, records, seed=0):
self.rng = random.Random(seed)
self.by_type = defaultdict(list)
for r in records:
self.by_type[answer_type(r["answer"])].append(r["answer"])
self.all = [r["answer"] for r in records]
def predict(self, rec):
q = rec["problem"].lower()
if re.search(r"how many|number of|count", q):
pool = self.by_type.get("integer") or self.all
elif re.search(r"\bis\b.*\?|does |are there", q):
pool = self.by_type.get("boolean") or self.all
else:
pool = self.all
return f"Answer: {self.rng.choice(pool)}"
class OpenAICompatBackend(Backend):
"""OpenAI、Moonshot/Kimi、OpenRouter、Together、vLLM、LM Studio など、画像 URL を含む POST {base}/chat/completions エンドポイントを公開するあらゆるサービスと動作します。"""
def __init__(self, base, key, model, max_tokens, workers, max_side, quality):
self.base, self.key, self.model = base.rstrip("/"), key, model
self.max_tokens, self.workers = max_tokens, workers
self.max_side, self.quality = max_side, quality
self.name = f"api:{model}"
def _one(self, rec, retries=4):
parts, _, uris = build_payload(rec, self.max_side, self.quality)
body = {"model": self.model, "messages": parts_to_openai(parts, uris),
"max_tokens": self.max_to
原文を表示
In this tutorial, we design an end-to-end evaluation workflow for PerceptionBench. This multimodal benchmark measures fine-grained visual perception capabilities across tasks such as OCR, counting, localization, contextual reasoning, comparison, depth understanding, and hallucination detection. We begin by configuring a Colab-compatible environment, installing the required libraries, and loading a balanced subset of the dataset through a robust multi-stage streaming and download strategy. We then decode base64-encoded images, parse interleaved image placeholders, normalize each example into a consistent record format, and analyze the dataset’s capability distribution, image requirements, answer types, and source benchmarks. From there, we construct a unified evaluation harness that supports a blind-prior baseline, OpenAI-compatible multimodal APIs, and local Hugging Face vision-language models. We also implement rule-based and optional LLM-assisted judging, calculate bootstrap confidence intervals, examine performance across difficulty slices, compare capability profiles with the included leaderboard, and export reproducible prediction and reporting artifacts.
Copy CodeCopiedUse a different Browser
import os, sys, io, re, json, time, math, base64, random, hashlib, subprocess, warnings
from collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
warnings.filterwarnings("ignore")
CFG = dict(
REPO = "moonshotai/PerceptionBench",
SPLIT = "train",
N_PER_CATEGORY = 12,
MAX_SCAN = 1200,
SEED = 0,
LOAD_MODE = "stream",
BACKEND = "blind",
API_BASE = os.environ.get("PB_API_BASE", "https://api.openai.com/v1"),
API_KEY = os.environ.get("PB_API_KEY", ""),
API_MODEL = os.environ.get("PB_API_MODEL", "gpt-4o-mini"),
API_WORKERS = 4,
API_MAX_TOKENS = 512,
LOCAL_MODEL = "HuggingFaceTB/SmolVLM2-2.2B-Instruct",
LOCAL_MAX_NEW = 128,
MAX_IMAGE_SIDE = 1024,
JPEG_QUALITY = 90,
JUDGE = "rule",
NUM_REL_TOL = 0.0,
OUT_DIR = "/content/perceptionbench_out" if os.path.isdir("/content") else "./perceptionbench_out",
INSTALL_DEPS = True,
SHOW_PLOTS = True,
)
random.seed(CFG["SEED"])
os.makedirs(CFG["OUT_DIR"], exist_ok=True)
def _sh(pkgs):
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *pkgs],
check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if CFG["INSTALL_DEPS"]:
print("[setup] installing dependencies (quiet, ~30s on a cold Colab)…")
_sh(["datasets>=3.0.0", "huggingface_hub>=0.25.0", "pillow", "pandas",
"numpy", "matplotlib", "requests", "pyarrow"])
if CFG["BACKEND"] == "local":
_sh(["transformers>=4.51.0", "accelerate", "torch", "num2words"])
import numpy as np
import pandas as pd
import requests
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image
matplotlib.rcParams.update({"figure.dpi": 110, "font.size": 9, "axes.grid": True,
"grid.alpha": .25, "axes.spines.top": False,
"axes.spines.right": False})
print("[setup] ready\n")
We configure the PerceptionBench environment, define the dataset, backend, image-processing, judging, and output settings, and initialize reproducible random behavior. We install the required libraries for dataset loading, numerical analysis, visualization, HTTP communication, and image processing. We also configure Matplotlib and prepare the output directory so the remaining evaluation workflow runs consistently in Google Colab or a local environment.
Copy CodeCopiedUse a different Browser
def _iter_rows(repo, split, mode, max_scan):
"""Yield dict rows, trying progressively heavier strategies."""
from datasets import load_dataset
if mode == "full":
print("[load] full download (~1.63 GB) …")
ds = load_dataset(repo, split=split)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
try:
from huggingface_hub import HfApi, hf_hub_url
api = HfApi()
files = api.list_repo_files(repo, repo_type="dataset", revision="refs/convert/parquet")
pq = sorted(f for f in files if f.endswith(".parquet") and f"/{split}/" in f)
if pq:
urls = [hf_hub_url(repo, f, repo_type="dataset", revision="refs/convert/parquet") for f in pq]
print(f"[load] streaming {len(urls)} parquet shard(s) from refs/convert/parquet")
ds = load_dataset("parquet", data_files=urls, split="train", streaming=True)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
except Exception as e:
print(f"[load] parquet stream unavailable ({type(e).__name__}: {e}); falling back")
try:
print("[load] streaming original data files")
ds = load_dataset(repo, split=split, streaming=True)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
return
except Exception as e:
print(f"[load] json stream failed ({type(e).__name__}); doing a full download")
ds = load_dataset(repo, split=split)
for i, r in enumerate(ds):
if i >= max_scan:
return
yield r
def stratified_subset(repo, split, n_per_cat, max_scan, mode):
"""Balanced sample across error_category — the ten atomic capabilities.
Balancing matters: the benchmark reports a *capability profile*, and an
unbalanced sample makes the overall number a weighted average of whichever
capabilities happened to appear first in the shard.
"""
buckets, scanned, t0 = defaultdict(list), 0, time.time()
for row in _iter_rows(repo, split, mode, max_scan):
scanned += 1
cat = row.get("error_category") or "unknown"
if len(buckets[cat]) < n_per_cat:
buckets[cat].append(row)
if scanned % 100 == 0:
filled = sum(len(v) >= n_per_cat for v in buckets.values())
print(f" scanned={scanned:5d} categories={len(buckets):2d} "
f"filled={filled:2d} {time.time()-t0:5.1f}s", end="\r")
if scanned >= 250 and len(buckets) >= 10 and all(len(v) >= n_per_cat for v in buckets.values()):
break
rows = [r for v in buckets.values() for r in v]
random.Random(CFG["SEED"]).shuffle(rows)
print(f"\n[load] scanned {scanned} rows -> kept {len(rows)} across "
f"{len(buckets)} capabilities ({time.time()-t0:.1f}s)")
return rows, scanned
ROWS, N_SCANNED = stratified_subset(
CFG["REPO"], CFG["SPLIT"], CFG["N_PER_CATEGORY"], CFG["MAX_SCAN"], CFG["LOAD_MODE"])
We implement a resilient dataset loader that first attempts converted Parquet streaming, then falls back to streaming the original files, and finally performs a full download when necessary. We scan the dataset while limiting the number of processed rows and organize examples into capability-specific buckets using the error_category field. We then create a balanced, shuffled subset so each visual capability contributes a comparable number of evaluation questions.
Copy CodeCopiedUse a different Browser
DATA_URI_RE = re.compile(r"^data:(image/[A-Za-z0-9.+-]+);base64,(.*)$", re.S)
PLACEHOLDER_RE = re.compile(r"<\|image _\|>")
def decode_image(entry):
"""data-URI string | raw b64 | bytes | HF Image dict -> PIL.Image (RGB)."""
if isinstance(entry, Image.Image):
return entry.convert("RGB")
if isinstance(entry, dict):
if entry.get("bytes"):
return Image.open(io.BytesIO(entry["bytes"])).convert("RGB")
if entry.get("path"):
return Image.open(entry["path"]).convert("RGB")
if isinstance(entry, (bytes, bytearray)):
return Image.open(io.BytesIO(entry)).convert("RGB")
s = str(entry).strip()
m = DATA_URI_RE.match(s)
b64 = m.group(2) if m else s
b64 = re.sub(r"\s+", "", b64)
b64 += "=" * (-len(b64) % 4)
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
def load_images(row):
imgs = row.get("image") or []
if isinstance(imgs, (str, bytes, dict)):
imgs = [imgs]
out = []
for e in imgs:
try:
out.append(decode_image(e))
except Exception as err:
print(f" [warn] undecodable image on idx={row.get('index')}: {err}")
return out
def shrink(img, max_side, quality):
"""Downscale + re-encode. Returns (PIL, data_uri). Controls the token bill:
a 3000px screenshot can cost >2k vision tokens per image, and these
questions carry up to 8 images each."""
w, h = img.size
if max(w, h) > max_side:
s = max_side / max(w, h)
img = img.resize((max(1, int(w * s)), max(1, int(h * s))), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality)
uri = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
return img, uri
def split_on_placeholders(problem, n_images):
"""<|image_1|>text<|image_2|>? -> [('image',0),('text','…'),('image',1)…]
Any image never referenced by a placeholder is appended at the end, so we
never silently drop visual evidence."""
parts, last = [], 0
for m in PLACEHOLDER_RE.finditer(problem):
chunk = problem[last:m.start()].strip()
if chunk:
parts.append(("text", chunk))
i = int(m.group(1)) - 1
if 0 <= i < n_images:
parts.append(("image", i))
last = m.end()
tail = problem[last:].strip()
if tail:
parts.append(("text", tail))
used = {p[1] for p in parts if p[0] == "image"}
for i in range(n_images):
if i not in used:
parts.append(("image", i))
return parts
def to_record(row):
imgs = load_images(row)
problem = (row.get("problem") or "").strip()
return dict(
index = row.get("index"),
problem = problem,
answer = str(row.get("answer", "")).strip(),
hint = (row.get("hint") or "").strip(),
category = row.get("error_category") or "unknown",
source_bmk = row.get("source_bmk") or "NA",
source_idx = row.get("source_idx"),
images = imgs,
n_images = len(imgs),
n_placeholders= len(PLACEHOLDER_RE.findall(problem)),
q_chars = len(problem),
px_total = sum(w * h for w, h in (im.size for im in imgs)),
max_side = max([max(im.size) for im in imgs], default=0),
)
print("[decode] decoding images…")
RECORDS = [to_record(r) for r in ROWS]
RECORDS = [r for r in RECORDS if r["images"] and r["answer"]]
print(f"[decode] {len(RECORDS)} usable records\n")
def cat_code(cat):
c = (cat or "").lower()
for key, code in [("hallucin", "Hallu"), ("ocr", "OCR"), ("context", "Ctx"),
("fine_grain", "FGR"), ("fine-grain", "FGR"),
("compar", "Comp"), ("local", "Loc"), ("position", "Loc"),
("depth", "Depth"), ("3d", "Depth"),
("attribut", "Attr"), ("count", "Count"),
("relation", "VRel")]:
if key in c:
return code
return cat[:6].title()
CODE_ORDER = ["VRel", "Count", "Attr", "Depth", "Loc", "Comp", "FGR", "Ctx", "OCR", "Hallu"]
CODE_FULL = {"VRel": "visual relation", "Count": "counting", "Attr": "attribute",
"Depth": "depth & 3D", "Loc": "localization", "Comp": "comparison",
"FGR": "fine-grained recog.", "Ctx": "contextual integration",
"OCR": "OCR", "Hallu": "perception hallucination"}
for r in RECORDS:
r["code"] = cat_code(r["category"])
df = pd.DataFrame([{k: v for k, v in r.items() if k != "images"} for r in RECORDS])
def answer_type(a):
a = a.strip()
if re.fullmatch(r"-?\d+", a): return "integer"
if re.fullmatch(r"-?\d*\.\d+", a): return "decimal"
if re.fullmatch(r"(?i)(yes|no|true|false)", a): return "boolean"
if re.fullmatch(r"(?i)[A-H]", a): return "letter"
if len(a.split()) == 1: return "single-word"
return "phrase"
df["ans_type"] = df["answer"].map(answer_type)
print("=" * 78)
print("§4 DATASET PROFILE (stratified subset — the card reports 3,000 total)")
print("=" * 78)
print("\n-- atomic capabilities present --")
print(df.groupby("code").agg(n=("index", "size"),
mean_imgs=("n_images", "mean"),
mean_qlen=("q_chars", "mean")).round(2).to_string())
print("\n-- answer surface forms --")
print(df["ans_type"].value_counts().to_string())
print("\n-- images per question --")
print(df["n_images"].value_counts().sort_index().to_string())
print(f"\n-- multi-image questions: {(df.n_images > 1).mean():.1%} of the subset")
print(f"-- median longest image edge: {df.max_side.median():.0f}px "
f"(max {df.max_side.max():.0f}px)")
print("\n-- provenance (top source benchmarks) --")
print(df["source_bmk"].value_counts().head(8).to_string())
print(f"\n-- newly-authored (source_bmk == 'NA'): {(df.source_bmk=='NA').mean():.1%} "
f"(card: 40% authored / 60% decomposed)\n")
def hf_full_stats(repo, split="train", config="default"):
try:
r = requests.get("https://datasets-server.huggingface.co/statistics",
params={"dataset": repo, "config": config, "split": split},
timeout=30)
r.raise_for_status()
for col in r.json().get("statistics", []):
if col["column_name"] == "error_category":
freq = col["column_statistics"].get("frequencies", {})
if freq:
tot = sum(freq.values())
print("-- FULL-CORPUS capability distribution (3,000 rows, via datasets-server) --")
for k, v in sorted(freq.items(), key=lambda x: -x[1]):
print(f" {cat_code(k):6s} {k:34s} {v:5d} {v/tot:6.1%}")
print()
return freq
except Exception as e:
print(f"[stats] datasets-server unavailable ({type(e).__name__}); "
f"using subset statistics only\n")
return None
FULL_FREQ = hf_full_stats(CFG["REPO"], CFG["SPLIT"])
if CFG["SHOW_PLOTS"]:
fig, ax = plt.subplots(1, 3, figsize=(13, 3.4))
order = [c for c in CODE_ORDER if c in set(df.code)] + \
[c for c in sorted(set(df.code)) if c not in CODE_ORDER]
df.code.value_counts().reindex(order).plot.bar(ax=ax[0], color="#4C72B0")
ax[0].set_title("Questions per atomic capability"); ax[0].set_xlabel("")
df.n_images.value_counts().sort_index().plot.bar(ax=ax[1], color="#DD8452")
ax[1].set_title("Images per question"); ax[1].set_xlabel("# images")
df.ans_type.value_counts().plot.barh(ax=ax[2], color="#55A868")
ax[2].set_title("Answer surface form")
plt.tight_layout(); plt.show()
We decode images from data URIs, raw base64 strings, byte arrays, PIL objects, and Hugging Face image dictionaries into a consistent RGB format. We normalize every dataset row into a structured record containing question text, answers, images, capability labels, dimensions, placeholder counts, and source information. We then analyze capability coverage, answer formats, image counts, resolution characteristics, and source benchmarks while visualizing the resulting dataset profile.
Copy CodeCopiedUse a different Browser
def show_record(rec, max_imgs=4):
imgs = rec["images"][:max_imgs]
n = len(imgs)
fig, axes = plt.subplots(1, n, figsize=(4.2 * n, 4.2))
axes = np.atleast_1d(axes)
for a, im in zip(axes, imgs):
a.imshow(im); a.axis("off")
q = re.sub(r"\s+", " ", rec["problem"])
q = (q[:150] + "…") if len(q) > 150 else q
fig.suptitle(f"[{rec['code']} · {rec['category']}] {q}\n"
f"gold = {rec['answer']!r} | src = {rec['source_bmk']}",
fontsize=9, y=1.06)
plt.tight_layout(); plt.show()
if CFG["SHOW_PLOTS"]:
print("=" * 78); print("§5 ONE EXEMPLAR PER CAPABILITY"); print("=" * 78)
seen = set()
for rec in RECORDS:
if rec["code"] not in seen:
seen.add(rec["code"]); show_record(rec)
if len(seen) >= 4:
break
SYSTEM_PROMPT = (
"You are a careful visual perception assistant. Examine the image(s) closely "
"before answering. Every question has a short, uniquely determined answer.\n"
"Reason briefly if needed, then end your reply with exactly one line:\n"
"Answer: <your final short answer>\n"
"Give only the value (a number, word, or short phrase) after 'Answer:' — "
"no units, no explanation, no full sentence."
)
def build_payload(rec, max_side, quality):
"""Returns (interleaved_parts, resized_pils, data_uris)."""
resized, uris = [], []
for im in rec["images"]:
pil, uri = shrink(im, max_side, quality)
resized.append(pil); uris.append(uri)
parts = split_on_placeholders(rec["problem"], len(resized))
if rec["hint"]:
parts.append(("text", f"Hint: {rec['hint']}"))
return parts, resized, uris
def parts_to_openai(parts, uris):
content = []
for kind, val in parts:
if kind == "text":
content.append({"type": "text", "text": val})
else:
content.append({"type": "image_url", "image_url": {"url": uris[val]}})
return [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": content}]
We display representative benchmark examples by arranging the associated images into readable grids and presenting each question with its capability, reference answer, and source. We define a strict multimodal system prompt that instructs the evaluated model to inspect all images and return a concise final answer in a consistent format. We also resize images, preserve their placement relative to question placeholders, and convert the resulting content into OpenAI-compatible multimodal messages.
Copy CodeCopiedUse a different Browser
class Backend:
name = "base"
def predict(self, rec): raise NotImplementedError
def predict_batch(self, recs):
return [self.predict(r) for r in recs]
class BlindPriorBackend(Backend):
"""Text-only floor. Answers using the *answer prior* conditioned on the
surface form the question implies — no pixels are ever read.
This is the control condition that makes an accuracy number meaningful:
'How many hinges?' has a guessable prior (small integers dominate). If a
vision model barely beats this, it isn't perceiving, it's guessing."""
name = "blind-prior"
def __init__(self, records, seed=0):
self.rng = random.Random(seed)
self.by_type = defaultdict(list)
for r in records:
self.by_type[answer_type(r["answer"])].append(r["answer"])
self.all = [r["answer"] for r in records]
def predict(self, rec):
q = rec["problem"].lower()
if re.search(r"how many|number of|count", q):
pool = self.by_type.get("integer") or self.all
elif re.search(r"\bis\b.*\?|does |are there", q):
pool = self.by_type.get("boolean") or self.all
else:
pool = self.all
return f"Answer: {self.rng.choice(pool)}"
class OpenAICompatBackend(Backend):
"""Works with OpenAI, Moonshot/Kimi, OpenRouter, Together, vLLM, LM Studio…
anything exposing POST {base}/chat/completions with image_url content."""
def __init__(self, base, key, model, max_tokens, workers, max_side, quality):
self.base, self.key, self.model = base.rstrip("/"), key, model
self.max_tokens, self.workers = max_tokens, workers
self.max_side, self.quality = max_side, quality
self.name = f"api:{model}"
def _one(self, rec, retries=4):
parts, _, uris = build_payload(rec, self.max_side, self.quality)
body = {"model": self.model, "messages": parts_to_openai(parts, uris),
"max_tokens": self.max_to
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み