Keep K3 suite selection and report-schema scoring in bash, merge K3/vision dataset_args into dpv4 yamls, and pin EvalScope at 735d920ee911 with local patches. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
6.8 KiB
Python
178 lines
6.8 KiB
Python
# Copyright (c) Alibaba, Inc. and its affiliates.
|
|
|
|
import json
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
import pytest
|
|
from PIL import Image as PILImage
|
|
|
|
from evalscope.api.benchmark import BenchmarkMeta
|
|
from evalscope.api.dataset import Sample
|
|
from evalscope.api.evaluator import TaskState
|
|
from evalscope.api.messages import ChatMessageUser, ContentImage, ContentText
|
|
from evalscope.api.metric.semantics import MetricSelector
|
|
from evalscope.api.registry import BENCHMARK_REGISTRY
|
|
from evalscope.benchmarks.general_qa_vqa_metrics import METRIC_SCORE_KEYS
|
|
from evalscope.benchmarks.general_vqa.general_vqa_adapter import GeneralVQAAdapter
|
|
from evalscope.config import TaskConfig
|
|
|
|
ROUGE_KEYS = METRIC_SCORE_KEYS['Rouge']
|
|
BLEU_KEYS = METRIC_SCORE_KEYS['BLEU']
|
|
|
|
|
|
@pytest.fixture
|
|
def adapter() -> GeneralVQAAdapter:
|
|
return GeneralVQAAdapter(
|
|
benchmark_meta=BenchmarkMeta(
|
|
name='general_vqa',
|
|
dataset_id='dummy',
|
|
eval_split='test',
|
|
metric_list=['Rouge', 'BLEU'],
|
|
primary_metric=MetricSelector(
|
|
name='rouge', aggregation='mean', dimensions={'variant': 'l', 'statistic': 'recall'}
|
|
),
|
|
),
|
|
task_config=TaskConfig(datasets=['general_vqa']),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def png_bytes() -> bytes:
|
|
image = PILImage.new(mode='RGB', size=(10, 10), color=(255, 0, 0))
|
|
buffer = BytesIO()
|
|
image.save(buffer, format='PNG')
|
|
return buffer.getvalue()
|
|
|
|
|
|
def _task_state() -> TaskState:
|
|
return TaskState(model='mock-model', sample=Sample(input='question', target='answer'))
|
|
|
|
|
|
def _rouge_values(value: float) -> Dict[str, float]:
|
|
return dict.fromkeys(ROUGE_KEYS, value)
|
|
|
|
|
|
def _raise_metric_error(*args: Any, **kwargs: Any) -> Dict[str, float]:
|
|
raise LookupError('metric exploded')
|
|
|
|
|
|
class TestGeneralVQAAdapterMatchScore:
|
|
|
|
def test_rouge_error_returns_real_zero_score_schema(
|
|
self, adapter: GeneralVQAAdapter, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
'evalscope.metrics.utils.rouge.compute_rouge_score_one_sample_zh', _raise_metric_error
|
|
)
|
|
|
|
score = adapter.match_score('pred', 'pred', 'answer', _task_state())
|
|
|
|
assert score is not None
|
|
assert score.value['Rouge-L-R'] == 0.0
|
|
assert {key: score.value[key] for key in ROUGE_KEYS} == dict.fromkeys(ROUGE_KEYS, 0.0)
|
|
assert score.metadata['metric_errors']['Rouge'] == 'LookupError: metric exploded'
|
|
|
|
def test_bleu_error_keeps_successful_rouge_values(
|
|
self, adapter: GeneralVQAAdapter, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
'evalscope.metrics.utils.rouge.compute_rouge_score_one_sample_zh',
|
|
lambda *args, **kwargs: _rouge_values(1.0),
|
|
)
|
|
monkeypatch.setattr('evalscope.metrics.bleu_ngram_one_sample', _raise_metric_error)
|
|
|
|
score = adapter.match_score('answer', 'answer', 'answer', _task_state())
|
|
|
|
assert score.value['Rouge-L-R'] == 1.0
|
|
assert {key: score.value[key] for key in BLEU_KEYS} == dict.fromkeys(BLEU_KEYS, 0.0)
|
|
assert score.metadata['metric_errors']['BLEU'] == 'LookupError: metric exploded'
|
|
|
|
def test_evaluation_version(self) -> None:
|
|
assert BENCHMARK_REGISTRY['general_vqa'].evaluation_version == 'v1.1'
|
|
|
|
|
|
class TestGeneralVQAAdapterRecordToSample:
|
|
|
|
def test_tsv_json_string_messages(self, adapter: GeneralVQAAdapter, tmp_path: Path):
|
|
"""Messages as a JSON string (TSV format) with placeholders."""
|
|
image_path = tmp_path / 'car.jpg'
|
|
PILImage.new(mode='RGB', size=(10, 10), color=(128, 128, 0)).save(image_path)
|
|
|
|
record = {
|
|
'messages': json.dumps([
|
|
{'role': 'user', 'content': '<image 1> What brand is this car?'}
|
|
]),
|
|
'image_1': str(image_path),
|
|
'answer': 'Tesla',
|
|
}
|
|
sample = adapter.record_to_sample(record)
|
|
content_list = sample.input[0].content
|
|
assert isinstance(content_list, list)
|
|
assert isinstance(content_list[0], ContentImage)
|
|
assert content_list[0].image == str(image_path)
|
|
assert isinstance(content_list[1], ContentText)
|
|
assert 'What brand is this car?' in content_list[1].text
|
|
|
|
def test_no_messages_key(self, adapter: GeneralVQAAdapter):
|
|
"""Record without 'messages' key returns empty input."""
|
|
record = {'answer': 'Nothing'}
|
|
sample = adapter.record_to_sample(record)
|
|
assert sample.input == []
|
|
assert sample.target == 'Nothing'
|
|
|
|
def test_plain_text_skips_unreferenced_media(self, adapter: GeneralVQAAdapter):
|
|
"""Malformed media cells do not affect records without placeholders."""
|
|
sample = adapter.record_to_sample({
|
|
'messages': [{'role': 'user', 'content': 'Describe the scene.'}],
|
|
'image_1': {},
|
|
'answer': 'A scene.',
|
|
})
|
|
|
|
assert sample.input[0].content == 'Describe the scene.'
|
|
|
|
def test_only_referenced_media_is_parsed(self, adapter: GeneralVQAAdapter):
|
|
"""Unreferenced indexed and list media columns do not fail placeholder resolution."""
|
|
sample = adapter.record_to_sample({
|
|
'messages': [{'role': 'user', 'content': '<image 1> Describe the scene.'}],
|
|
'image_1': 'https://example.com/scene.jpg',
|
|
'audio_1': {},
|
|
'videos': 'not-a-list',
|
|
'answer': 'A scene.',
|
|
})
|
|
|
|
assert isinstance(sample.input[0].content[0], ContentImage)
|
|
|
|
def test_indexed_media_columns_take_precedence_over_list(self, adapter: GeneralVQAAdapter):
|
|
"""Indexed media columns take precedence over the equivalent list column."""
|
|
sample = adapter.record_to_sample({
|
|
'messages': [{'role': 'user', 'content': '<image 1> then <image 2>'}],
|
|
'image_1': 'https://example.com/indexed.jpg',
|
|
'images': ['https://example.com/list-1.jpg', 'https://example.com/list-2.jpg'],
|
|
'answer': 'A scene.',
|
|
})
|
|
|
|
images = [content.image for content in sample.input[0].content if isinstance(content, ContentImage)]
|
|
assert images == ['https://example.com/indexed.jpg']
|
|
|
|
def test_unresolved_only_placeholder_preserves_valid_content(self, adapter: GeneralVQAAdapter):
|
|
"""A missing media reference never emits an empty user message."""
|
|
sample = adapter.record_to_sample({
|
|
'messages': [{'role': 'user', 'content': '<image 1>'}],
|
|
'answer': 'Unknown',
|
|
})
|
|
|
|
assert sample.input[0].content == '<image 1>'
|
|
|
|
def test_empty_answer(self, adapter: GeneralVQAAdapter):
|
|
"""Record with empty answer string."""
|
|
record = {
|
|
'messages': [
|
|
{'role': 'user', 'content': 'Say nothing.'}
|
|
],
|
|
'answer': '',
|
|
}
|
|
sample = adapter.record_to_sample(record)
|
|
assert sample.target == ''
|