Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions evalscope/benchmarks/pumed_qa/pubmed_qa_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
@register_benchmark(
BenchmarkMeta(
name='pubmedqa',
evaluation_version='v1.1',
pretty_name='PubMedQA',
tags=[Tags.KNOWLEDGE, Tags.YES_NO],
description=DESCRIPTION.strip(),
Expand Down Expand Up @@ -104,9 +105,12 @@ def compute_metrics(scores: List[SampleScore]):
maybe_count = 0
total_count = len(scores)
correct_count = 0
actual_counts = dict.fromkeys(confusion_matrix, 0)

for ss in scores:
gt = ss.sample_metadata['answer'].strip().upper()
if gt in actual_counts:
actual_counts[gt] += 1

if ss.score.main_value == 1:
correct_count += 1
Expand Down Expand Up @@ -148,8 +152,8 @@ def compute_metrics(scores: List[SampleScore]):
# Calculate predicted positives (column sum)
pred_pos = sum(confusion_matrix[true_cls][cls] for true_cls in classes)

# Calculate actual positives (row sum)
act_pos = sum(confusion_matrix[cls][pred_cls] for pred_cls in classes)
# Unrecognized answers are false negatives, even without a predicted class.
act_pos = actual_counts[cls]

# Calculate precision and recall for this class
cls_precision = tp / pred_pos if pred_pos > 0 else 0.0
Expand Down
76 changes: 76 additions & 0 deletions tests/benchmark/test_pubmedqa_aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""PubMedQA must count unrecognized answers as false negatives in class recall."""

from typing import Dict, List, Tuple

import pytest

from evalscope.api.evaluator import TaskState
from evalscope.api.model import ModelOutput
from evalscope.api.registry import get_benchmark
from evalscope.benchmarks.pumed_qa.pubmed_qa_adapter import PubMedQAAdapter
from evalscope.config import TaskConfig


@pytest.fixture
def adapter() -> PubMedQAAdapter:
return get_benchmark('pubmedqa', TaskConfig(datasets=['pubmedqa']))


def aggregate(adapter: PubMedQAAdapter, answers: List[Tuple[str, str]]) -> Dict[str, float]:
"""Exercise record conversion and the normal scoring path with canned completions."""
scores = []
for index, (label, answer) in enumerate(answers):
sample = adapter.record_to_sample(
{
'context': 'Study abstract.',
'question': 'Does the study support the hypothesis?',
'answer': label.lower(),
'reasoning': '',
}
)
sample.id = index
state = TaskState(
model='scripted', sample=sample, output=ModelOutput.from_content('scripted', answer), completed=True
)
scores.append(adapter.calculate_metrics(state))
return {score.metric_name: score.score for score in adapter.aggregate_scores(scores)}


@pytest.mark.parametrize('unrecognized', ['', 'Insufficient evidence.', 'I am unable to answer.'])
def test_unrecognized_answers_remain_in_recall_denominator(adapter: PubMedQAAdapter, unrecognized: str) -> None:
answers = [(label, answer) for label in ['YES', 'NO', 'MAYBE'] for answer in [label, unrecognized]]
metrics = aggregate(adapter, answers)
assert metrics['accuracy'] == pytest.approx(0.5)
assert metrics['precision'] == pytest.approx(1.0)
assert metrics['recall'] == pytest.approx(0.5)
assert metrics['f1'] == pytest.approx(2 / 3)


def test_abstentions_do_not_reverse_f1_ranking(adapter: PubMedQAAdapter) -> None:
labels = ['YES', 'NO', 'MAYBE']
a = aggregate(
adapter, [(label, label if i < 8 else 'Insufficient evidence.') for label in labels for i in range(10)]
)
b = aggregate(
adapter, [(label, label if i < 9 else labels[(j + 1) % 3]) for j, label in enumerate(labels) for i in range(10)]
)
assert a['f1'] == pytest.approx(8 / 9)
assert b['f1'] == pytest.approx(0.9)
assert a['f1'] < b['f1']


def test_unrecognized_answers_count_against_their_own_class(adapter: PubMedQAAdapter) -> None:
answers = [('YES', 'YES'), ('YES', ''), ('YES', ''), ('NO', 'NO'), ('MAYBE', 'MAYBE')]
metrics = aggregate(adapter, answers)
assert metrics['recall'] == pytest.approx((1 / 3 + 1 + 1) / 3)
assert metrics['f1'] == pytest.approx((0.5 + 1 + 1) / 3)


@pytest.mark.parametrize('mode', ['correct', 'wrong', 'abstain', 'empty'])
def test_existing_boundary_cases(adapter: PubMedQAAdapter, mode: str) -> None:
labels = ['YES', 'NO', 'MAYBE']
predictions = {'correct': labels, 'wrong': ['NO', 'MAYBE', 'YES'], 'abstain': ['', '', ''], 'empty': []}
metrics = aggregate(adapter, list(zip(labels, predictions[mode])))
expected = 1.0 if mode == 'correct' else 0.0
for metric in ['accuracy', 'precision', 'recall', 'f1']:
assert metrics[metric] == pytest.approx(expected)
Loading