Skip to content

Commit a0e8805

Browse files
jiafatomCopilot
andauthored
Add vision evaluation metrics (exact_match, relaxed_accuracy, word_sort_ratio) (microsoft#2476)
## Summary Extends Olive's evaluator framework with three vision-oriented accuracy sub-metrics for VQA, ChartQA, and OCR evaluation, following the existing pattern used for speech metrics (PR microsoft#2444). ## New Metrics | Metric | Task Type | Suitable Benchmarks | |--------|-----------|-------------------| | `exact_match` | `vision-vqa` | AI2D, ScienceQA, TextVQA, MathVista, MMMU, InterGPS | | `relaxed_accuracy` | `vision-chart-qa` | ChartQA (±5% numeric tolerance for numbers) | | `word_sort_ratio` | `vision-ocr` | OCR benchmarks (word-level overlap) | ## Public HuggingFace Datasets These metrics are designed to work with publicly available datasets: | Metric | Recommended Dataset | HuggingFace ID | |--------|-------------------|----------------| | `exact_match` | TextVQA | `facebook/textvqa` | | `relaxed_accuracy` | ChartQA | `HuggingFaceM4/ChartQA` | | `word_sort_ratio` | DocumentVQA | `HuggingFaceM4/DocumentVQA` | Example configuration snippets are provided in `docs/source/how-to/configure-workflows/metrics-configuration.md`. ## Changes - **`olive/evaluator/metric.py`**: Adds `EXACT_MATCH`, `RELAXED_ACCURACY`, `WORD_SORT_RATIO` to `AccuracySubType` enum - **`olive/evaluator/accuracy.py`**: Implements the three metric classes with multi-answer support - **`olive/evaluator/olive_evaluator.py`**: Adds vision inference path and task-metric validation - **`olive/data/component/pre_process_data.py`**: Adds `vision_vqa_pre_process` component - **`olive/data/component/dataloader.py`**: Adds `vision_vqa_dataloader` with custom collate_fn for PIL images - **`olive/data/container/huggingface_container.py`**: Registers `vision-vqa`, `vision-chart-qa`, `vision-ocr` task types with appropriate dataloader - **`olive/olive_config.json`**: Adds `vision` extras (pillow) - **`docs/source/how-to/configure-workflows/metrics-configuration.md`**: Adds vision metrics documentation with public dataset examples - **`test/evaluator/test_accuracy.py`**: Unit tests covering all new metrics ## Design - Vision metrics are text-based (compare predicted answer string to ground truth), task-dependent - Multiple valid answers supported via `|` separator (metrics match against any valid answer) - Task-metric validation ensures incompatible combinations raise `ValueError` - Custom `vision_vqa_dataloader` handles PIL images with a collate_fn that avoids PyTorch default collation issues - PyTorch path: model processor handles images natively - ONNX path: single forward pass assumed (classification-style VQA); for autoregressive models use PyTorch evaluator with generation loop --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cd47ebb commit a0e8805

10 files changed

Lines changed: 902 additions & 4 deletions

File tree

docs/source/how-to/configure-workflows/metrics-configuration.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,131 @@ WER can be used as an accuracy sub-type when your data pipeline returns text pre
153153
- `wer` (Word Error Rate): Measures transcription errors. Lower is better (defaults to `higher_is_better: false`).
154154
- `rtfx` (Real-Time Factor): Ratio of audio duration to inference time. Higher means faster (defaults to `higher_is_better: true`).
155155
```
156+
157+
## Vision Evaluation Metrics
158+
159+
Olive provides three built-in accuracy sub-types for evaluating vision/multimodal models:
160+
161+
| Metric | Task Type | Description | Suitable Benchmarks |
162+
|--------|-----------|-------------|---------------------|
163+
| `exact_match` | `vision-vqa` | Case-insensitive string equality | AI2D, ScienceQA, TextVQA, MMMU |
164+
| `relaxed_accuracy` | `vision-chart-qa` | ±5% numeric tolerance for numbers | ChartQA |
165+
| `word_sort_ratio` | `vision-ocr` | Word-level overlap ratio | OCR benchmarks |
166+
167+
### Example: VQA with TextVQA (exact_match)
168+
169+
```json
170+
{
171+
"data_configs": [
172+
{
173+
"name": "textvqa_data",
174+
"type": "HuggingfaceContainer",
175+
"load_dataset_config": {
176+
"data_name": "facebook/textvqa",
177+
"split": "validation"
178+
},
179+
"pre_process_data_config": {
180+
"type": "vision_vqa_pre_process",
181+
"image_col": "image",
182+
"question_col": "question",
183+
"answer_col": "answers",
184+
"limit": 100
185+
},
186+
"dataloader_config": {
187+
"batch_size": 1
188+
}
189+
}
190+
],
191+
"metrics": [
192+
{
193+
"name": "vqa_accuracy",
194+
"type": "accuracy",
195+
"data_config": "textvqa_data",
196+
"sub_types": [
197+
{"name": "exact_match", "priority": 1, "goal": {"type": "max-degradation", "value": 0.05}}
198+
]
199+
}
200+
]
201+
}
202+
```
203+
204+
### Example: ChartQA with relaxed_accuracy
205+
206+
```json
207+
{
208+
"data_configs": [
209+
{
210+
"name": "chartqa_data",
211+
"type": "HuggingfaceContainer",
212+
"load_dataset_config": {
213+
"data_name": "HuggingFaceM4/ChartQA",
214+
"split": "test"
215+
},
216+
"pre_process_data_config": {
217+
"type": "vision_vqa_pre_process",
218+
"image_col": "image",
219+
"question_col": "question",
220+
"answer_col": "answer",
221+
"limit": 100
222+
},
223+
"dataloader_config": {
224+
"batch_size": 1
225+
}
226+
}
227+
],
228+
"metrics": [
229+
{
230+
"name": "chart_accuracy",
231+
"type": "accuracy",
232+
"data_config": "chartqa_data",
233+
"sub_types": [
234+
{"name": "relaxed_accuracy", "priority": 1, "goal": {"type": "max-degradation", "value": 0.05}}
235+
]
236+
}
237+
]
238+
}
239+
```
240+
241+
### Example: OCR with DocumentVQA (word_sort_ratio)
242+
243+
```json
244+
{
245+
"data_configs": [
246+
{
247+
"name": "docvqa_data",
248+
"type": "HuggingfaceContainer",
249+
"load_dataset_config": {
250+
"data_name": "HuggingFaceM4/DocumentVQA",
251+
"split": "validation"
252+
},
253+
"pre_process_data_config": {
254+
"type": "vision_vqa_pre_process",
255+
"image_col": "image",
256+
"question_col": "question",
257+
"answer_col": "answers",
258+
"limit": 100
259+
},
260+
"dataloader_config": {
261+
"batch_size": 1
262+
}
263+
}
264+
],
265+
"metrics": [
266+
{
267+
"name": "ocr_accuracy",
268+
"type": "accuracy",
269+
"data_config": "docvqa_data",
270+
"sub_types": [
271+
{"name": "word_sort_ratio", "priority": 1, "goal": {"type": "max-degradation", "value": 0.05}}
272+
]
273+
}
274+
]
275+
}
276+
```
277+
278+
```{Note}
279+
- Vision metrics compare predicted answer strings to ground truth. The model's `post_func` must decode model output into text.
280+
- Use `batch_size: 1` since images have variable sizes.
281+
- Multiple valid answers (lists) are joined with `|` and the metric matches against any valid answer.
282+
- For ONNX models, provide a custom pre-process that applies the processor/tokenizer to produce numeric tensors.
283+
```

olive/data/component/dataloader.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ def no_auto_batch_dataloader(dataset, **kwargs):
5555
return DataLoader(dataset, batch_size=None, **kwargs)
5656

5757

58+
@Registry.register_dataloader()
59+
def vision_vqa_dataloader(dataset, batch_size=1, **kwargs):
60+
from torch.utils.data import DataLoader
61+
62+
collate_fn = getattr(dataset, "collate_fn", None)
63+
return DataLoader(dataset, batch_size=batch_size, collate_fn=collate_fn, **kwargs)
64+
65+
5866
@Registry.register_dataloader()
5967
def default_calibration_dataloader(
6068
dataloader,

olive/data/component/pre_process_data.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,97 @@ def collate_fn(batch):
379379
return (audios, texts)
380380

381381
return SpeechTranscriptionDataset(dataset, audio_col, text_col)
382+
383+
384+
@Registry.register_pre_process()
385+
def vision_vqa_pre_process(
386+
dataset,
387+
image_col: str = "image",
388+
question_col: str = "question",
389+
answer_col: str = "answer",
390+
max_samples: Optional[int] = None,
391+
limit: Optional[float] = None,
392+
seed: int = 42,
393+
**kwargs,
394+
):
395+
"""Pre-process data for vision VQA evaluation.
396+
397+
Loads image, question, and ground truth answer from a HuggingFace dataset.
398+
Returns a dataset of ({"image": image, "question": question}, answer) pairs.
399+
400+
Note: This returns raw PIL images and question strings. For the PyTorch evaluator,
401+
the model's own processor/tokenizer should be applied within the model's forward
402+
method (or via a custom pre-process component). For the ONNX evaluator, provide a
403+
custom pre-process component that applies the appropriate processor/tokenizer to
404+
produce numeric tensors matching the model's io_config.
405+
406+
Args:
407+
dataset: HuggingFace dataset with image, question, and answer columns.
408+
image_col: Name of the image column. Defaults to "image".
409+
question_col: Name of the question column. Defaults to "question".
410+
answer_col: Name of the answer column. Defaults to "answer".
411+
max_samples: Maximum number of samples (deprecated, use limit). Defaults to None.
412+
limit: Sampling limit following Olive convention:
413+
If >= 1: use first N samples.
414+
If 0 < limit < 1: randomly sample that percentage.
415+
If 0 or None: use all samples.
416+
seed: Random seed for percentage-based sampling. Defaults to 42.
417+
**kwargs: Additional arguments.
418+
419+
"""
420+
# Apply sampling: prefer limit over max_samples
421+
effective_limit = limit if limit is not None else (max_samples if max_samples else 0)
422+
if effective_limit and effective_limit != 0:
423+
from random import Random
424+
425+
total = len(dataset)
426+
if 0 < effective_limit < 1:
427+
n = max(1, int(total * effective_limit))
428+
rng = Random(seed)
429+
indices = sorted(rng.sample(range(total), min(n, total)))
430+
dataset = dataset.select(indices)
431+
elif effective_limit >= 1:
432+
n = min(int(effective_limit), total)
433+
dataset = dataset.select(range(n))
434+
435+
class VisionVQADataset:
436+
"""Dataset that returns (input_dict, answer_text) pairs for VQA evaluation.
437+
438+
Note: Use batch_size=1 in dataloader config as images have variable sizes.
439+
"""
440+
441+
def __init__(self, hf_dataset, image_column, question_column, answer_column):
442+
self.dataset = hf_dataset
443+
self.image_column = image_column
444+
self.question_column = question_column
445+
self.answer_column = answer_column
446+
447+
def __len__(self):
448+
return len(self.dataset)
449+
450+
def __getitem__(self, idx):
451+
item = self.dataset[idx]
452+
image = item[self.image_column]
453+
question = item[self.question_column]
454+
answer = item[self.answer_column]
455+
# Handle list/tuple answers (some datasets have multiple valid answers)
456+
# Join with | separator so metrics can match against any valid answer
457+
if isinstance(answer, (list, tuple)):
458+
answer = "|".join(str(a) for a in answer) if answer else ""
459+
return {"image": image, "question": question}, str(answer)
460+
461+
@staticmethod
462+
def collate_fn(batch):
463+
"""Collate VQA batches. Use with batch_size=1 for variable-size images.
464+
465+
Note: answers are always strings at this point (list/tuple answers are
466+
joined with "|" in __getitem__), so no list-of-lists issue arises.
467+
"""
468+
if len(batch) == 1:
469+
input_dict, answer = batch[0]
470+
return (input_dict, [answer])
471+
inputs = [item[0] for item in batch]
472+
answers = [item[1] for item in batch]
473+
return (inputs, answers)
474+
475+
return VisionVQADataset(dataset, image_col, question_col, answer_col)

olive/data/container/huggingface_container.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,16 @@ class HuggingfaceContainer(DataContainer):
4141
"speech-transcription": {
4242
DataComponentType.PRE_PROCESS_DATA.value: "speech_transcription_pre_process",
4343
},
44+
"vision-vqa": {
45+
DataComponentType.PRE_PROCESS_DATA.value: "vision_vqa_pre_process",
46+
DataComponentType.DATALOADER.value: "vision_vqa_dataloader",
47+
},
48+
"vision-chart-qa": {
49+
DataComponentType.PRE_PROCESS_DATA.value: "vision_vqa_pre_process",
50+
DataComponentType.DATALOADER.value: "vision_vqa_dataloader",
51+
},
52+
"vision-ocr": {
53+
DataComponentType.PRE_PROCESS_DATA.value: "vision_vqa_pre_process",
54+
DataComponentType.DATALOADER.value: "vision_vqa_dataloader",
55+
},
4456
}

0 commit comments

Comments
 (0)