File size: 6,261 Bytes
ef3dde7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """AI2D evaluation for Vision-Language Models.
Evaluates VQA accuracy on AI2 Diagrams — multiple-choice visual questions
about scientific diagrams (food webs, life cycles, etc.). Uses lmms-lab/ai2d
from HuggingFace Hub.
This is the VLM equivalent of GSM8K: clear right/wrong answers, exercises
genuine visual reasoning, and is sensitive to quantization quality degradation.
"""
import re
from dataclasses import dataclass, field
import numpy as np
from tqdm import tqdm
@dataclass
class AI2DResult:
n_correct: int
n_total: int
accuracy: float
per_question: list[dict] = field(default_factory=list)
def _build_prompt(question: str, options: list[str]) -> str:
"""Build a multiple-choice VQA prompt."""
labels = ["A", "B", "C", "D", "E", "F", "G", "H"]
choices = "\n".join(f"{labels[i]}. {opt}" for i, opt in enumerate(options))
return (
f"{question}\n{choices}\n"
f"Answer with the letter of the correct option."
)
def _extract_answer_letter(text: str, n_options: int) -> str | None:
"""Extract the answer letter from model output."""
text = text.strip()
valid = set("ABCDEFGH"[:n_options])
# Check if response starts with a valid letter
if text and text[0].upper() in valid:
return text[0].upper()
# Look for patterns like "A.", "A)", "Answer: A", "The answer is A"
patterns = [
r"(?:answer|option)\s*(?:is|:)\s*([A-H])",
r"\b([A-H])\s*[\.\):]",
r"\b([A-H])\b",
]
for pattern in patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
letter = match.group(1).upper()
if letter in valid:
return letter
return None
def evaluate_ai2d_mlx(
model_path: str,
n_samples: int = 100,
max_new_tokens: int = 32,
max_image_size: int = 512,
seed: int = 42,
) -> AI2DResult:
"""Evaluate a quantized MLX VLM on AI2D diagram understanding.
Args:
model_path: Path to MLX model directory (converted via mlx-vlm).
n_samples: Number of test questions to evaluate.
max_new_tokens: Max tokens to generate per question.
max_image_size: Max image dimension (limits visual token count).
seed: Random seed for sample selection.
Returns:
AI2DResult with accuracy and per-question details.
"""
import os
from datasets import load_dataset
from PIL import Image
from mlx_vlm import load, generate
from mlx_vlm.prompt_utils import apply_chat_template
from mlx_vlm.utils import load_config
# Resolve to absolute path
if os.path.isdir(model_path):
model_path = os.path.abspath(model_path)
print(f" Loading MLX VLM from {model_path}...")
model, processor = load(model_path)
config = load_config(model_path)
print(" Loading AI2D test set from HuggingFace...")
ds = load_dataset("lmms-lab/ai2d", split="test")
rng = np.random.RandomState(seed)
indices = rng.choice(len(ds), size=min(n_samples, len(ds)), replace=False)
indices.sort()
n_correct = 0
per_question = []
print(f" Evaluating {len(indices)} AI2D questions...")
for idx in tqdm(indices, desc=" AI2D eval"):
item = ds[int(idx)]
img = item["image"]
if not isinstance(img, Image.Image):
img = Image.open(img)
img = img.convert("RGB")
w, h = img.size
if max(w, h) > max_image_size:
scale = max_image_size / max(w, h)
img = img.resize((int(w * scale), int(h * scale)), Image.BILINEAR)
question = item["question"]
options = item["options"]
gt_idx = int(item["answer"])
labels = ["A", "B", "C", "D", "E", "F", "G", "H"]
gt_letter = labels[gt_idx]
prompt_text = _build_prompt(question, options)
try:
# Save image to temp file for mlx-vlm generate()
import tempfile
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
img.save(f, format="PNG")
temp_path = f.name
# Build prompt via mlx-vlm chat template
formatted = apply_chat_template(processor, config, prompt_text, num_images=1)
result = generate(
model, processor,
prompt=formatted,
image=[temp_path],
max_tokens=max_new_tokens,
verbose=False,
temp=0.0,
)
output_text = result.text if hasattr(result, 'text') else str(result)
os.unlink(temp_path)
predicted = _extract_answer_letter(output_text, len(options))
correct = predicted == gt_letter
except Exception as e:
output_text = f"ERROR: {e}"
predicted = None
correct = False
if correct:
n_correct += 1
per_question.append({
"idx": int(idx),
"question": question[:100] + ("..." if len(question) > 100 else ""),
"ground_truth": gt_letter,
"predicted": predicted,
"output": str(output_text)[:200],
"correct": correct,
})
accuracy = n_correct / len(indices) if len(indices) > 0 else 0.0
return AI2DResult(
n_correct=n_correct,
n_total=len(indices),
accuracy=accuracy,
per_question=per_question,
)
def print_ai2d_report(result: AI2DResult):
"""Print AI2D evaluation results."""
print(f"\n AI2D Results")
print(f" {'=' * 50}")
print(f" Accuracy: {result.n_correct}/{result.n_total} "
f"({result.accuracy:.1%})")
right = [q for q in result.per_question if q["correct"]]
wrong = [q for q in result.per_question if not q["correct"]]
if right:
print(f"\n Correct examples:")
for q in right[:3]:
print(f" Q: {q['question']}")
print(f" GT: {q['ground_truth']}, Pred: {q['predicted']}")
if wrong:
print(f"\n Incorrect examples:")
for q in wrong[:3]:
print(f" Q: {q['question']}")
print(f" GT: {q['ground_truth']}, Pred: {q['predicted']}")
print(f" Output: {q['output'][:100]}")
|