Files
confident-ai--deepeval/deepeval/benchmarks/math_qa/math_qa.py
T
2026-07-13 13:32:05 +08:00

292 lines
10 KiB
Python

from typing import List, Optional, Dict, Union
from tqdm import tqdm
from deepeval.dataset import Golden
from deepeval.benchmarks.base_benchmark import (
DeepEvalBaseBenchmark,
DeepEvalBaseBenchmarkResult,
)
from deepeval.models import DeepEvalBaseLLM
from deepeval.benchmarks.math_qa.task import MathQATask
from deepeval.benchmarks.math_qa.template import MathQATemplate
from deepeval.benchmarks.utils import should_use_batch
from deepeval.benchmarks.schema import MultipleChoiceSchemaLower
from deepeval.telemetry import capture_benchmark_run
class MathQA(DeepEvalBaseBenchmark):
def __init__(
self,
tasks: List[MathQATask] = None,
n_shots: int = 5,
n_problems_per_task: Optional[int] = None,
verbose_mode: bool = False,
confinement_instructions: Optional[str] = None,
**kwargs,
):
from deepeval.scorer import Scorer
import pandas as pd
assert n_shots <= 5, "MathQA only supports n_shots <= 5"
super().__init__(**kwargs)
self.tasks: List[MathQATask] = (
list(MathQATask) if tasks is None else tasks
)
self.n_problems_per_task: Optional[int] = n_problems_per_task
self.scorer = Scorer()
self.n_shots: int = n_shots
self.predictions: Optional[pd.DataFrame] = None
self.task_scores: Optional[pd.DataFrame] = None
self.overall_score: Optional[float] = None
self.verbose_mode = verbose_mode
if not confinement_instructions:
self.confinement_instructions = (
"Output 'a', 'b', 'c', or 'd'. Full answer not needed."
)
else:
self.confinement_instructions = confinement_instructions
def evaluate(
self,
model: DeepEvalBaseLLM,
*args,
batch_size: Union[int, None] = None,
**kwargs,
) -> DeepEvalBaseBenchmarkResult:
import pandas as pd
with capture_benchmark_run("MathQA", len(self.tasks)):
overall_correct_predictions = 0
overall_total_predictions = 0
predictions_row = []
scores_row = []
use_batch = should_use_batch(model, batch_size)
for task in self.tasks:
goldens = self.load_benchmark_dataset(task)
if (
self.n_problems_per_task is not None
and self.n_problems_per_task < len(goldens)
):
goldens = goldens[: self.n_problems_per_task]
task_correct_predictions = 0
task_total_predictions = len(goldens)
overall_total_predictions += len(goldens)
# Calculate task accuracy
if use_batch:
for i in tqdm(
range(0, len(goldens), batch_size),
desc=f"Batch Processing {task.value} (batch_size={batch_size})",
):
goldens_batch = goldens[i : i + batch_size]
batch_predictions = self.batch_predict(
model, goldens_batch
)
for golden, prediction_dict in zip(
goldens_batch, batch_predictions
):
prediction = prediction_dict["prediction"]
score = prediction_dict["score"]
if score:
task_correct_predictions += 1
overall_correct_predictions += 1
predictions_row.append(
(
task.value,
golden.input,
prediction,
golden.expected_output,
score,
)
)
else:
for idx, golden in enumerate(
tqdm(goldens, desc=f"Processing {task.value}")
):
prediction, score = self.predict(model, golden).values()
if score:
task_correct_predictions += 1
overall_correct_predictions += 1
predictions_row.append(
(
task.value,
golden.input,
prediction,
golden.expected_output,
score,
)
)
if self.verbose_mode:
self.print_verbose_logs(
idx,
task.value,
golden.input,
golden.expected_output,
prediction,
score,
)
task_accuracy = (
task_correct_predictions / task_total_predictions
)
print(
f"MathQA Task Accuracy (task={task.value}): {task_accuracy}"
)
scores_row.append((task.value, task_accuracy))
# Calculate overall accuracy
overall_accuracy = (
overall_correct_predictions / overall_total_predictions
)
print(f"Overall MathQA Accuracy: {overall_accuracy}")
# Create a DataFrame from task_results_data
# Columns: 'Task', 'Input', 'Prediction', 'Score'
self.predictions = pd.DataFrame(
predictions_row,
columns=[
"Task",
"Input",
"Prediction",
"Expected Output",
"Correct",
],
)
self.task_scores = pd.DataFrame(
scores_row, columns=["Task", "Score"]
)
self.overall_score = overall_accuracy
return DeepEvalBaseBenchmarkResult(
overall_accuracy=overall_accuracy
)
def predict(self, model: DeepEvalBaseLLM, golden: Golden) -> Dict:
# Define prompt template
prompt: dict = MathQATemplate.generate_output(
input=golden.input,
n_shots=self.n_shots,
)
# Enforced model generation
try:
res: MultipleChoiceSchemaLower = model.generate(
prompt=prompt, schema=MultipleChoiceSchemaLower
)
if isinstance(res, (tuple, list)):
prediction = res[0].answer
else:
prediction = res.answer
except TypeError:
prompt += f"\n\n{self.confinement_instructions}"
prediction = model.generate(prompt)
# For native models, shouldn't happen but just in case
if isinstance(prediction, tuple):
prediction = prediction[0]
prediction = str(prediction)
# Define Metric
score = self.scorer.exact_match_score(
golden.expected_output, prediction
)
return {"prediction": prediction, "score": score}
def batch_predict(
self, model: DeepEvalBaseLLM, goldens: List[Golden]
) -> List[Dict]:
# Define prompt template
prompts = []
for golden in goldens:
prompt: dict = MathQATemplate.generate_output(
input=golden.input,
n_shots=self.n_shots,
)
prompts.append(prompt)
# Enforced model generation
try:
responses: List[MultipleChoiceSchemaLower] = model.batch_generate(
prompts=prompts,
schemas=[MultipleChoiceSchemaLower for _ in prompts],
)
predictions = [res.answer for res in responses]
except TypeError:
prompts = [
prompt
+ "\n\nOutput 'a', 'b', 'c', or 'd'. Full answer not needed."
for prompt in prompts
]
predictions = model.batch_generate(prompts)
if len(predictions) is not len(goldens):
raise ValueError(
"Custom `batch_generate` method did not return the same number of generations as the number of prompts."
)
res = []
for i in range(len(predictions)):
prediction = predictions[i]
golden = goldens[i]
# Define Metric
score = self.scorer.exact_match_score(
golden.expected_output, prediction
)
res.append({"prediction": prediction, "score": score})
return res
def load_benchmark_dataset(self, task: MathQATask) -> List[Golden]:
from datasets import load_dataset
if self.dataset:
dataset = self.dataset
else:
dataset = load_dataset("regisss/math_qa")
self.dataset = dataset
# Construct test set
test_set = dataset["test"].filter(
lambda data: data["category"] == task.value
)
goldens: List[Golden] = []
for data in test_set:
input = MathQATemplate.format_question(data, include_answer=False)
expected_output = MathQATemplate.format_output(data)
golden = Golden(input=input, expected_output=expected_output)
goldens.append(golden)
return goldens
def print_verbose_logs(
self,
idx: int,
task_value: str,
input: str,
expected_output: str,
prediction: str,
score: int,
) -> str:
steps = [
f"Input:\n{input}",
f"Score: {score}\nPrediction: {prediction}\nExpected Output: {expected_output}",
]
verbose_logs = ""
for i in range(len(steps) - 1):
verbose_logs += steps[i]
# don't add new line for penultimate step
if i < len(steps) - 2:
verbose_logs += " \n \n"
if self.verbose_mode:
print("*" * 50)
print(f"Problem {idx + 1} (Task = {task_value})")
print("*" * 50)
print("")
print(verbose_logs + f"\n \n{steps[-1]}")
print("")
print("=" * 70)
return verbose_logs