math-solver / eval /metrics.py
Cuong2004
Deploy API from GitHub Actions
0772b5a
Raw
History Blame Contribute Delete
5.67 kB
"""
Evaluation Metrics for MathSolver Pipeline.
Defines metric calculators across all pipeline stages:
- OCR: Character Error Rate (CER), Word Error Rate (WER), LaTeX Exact Match, Confidence Calibration
- Parser: JSON Validity, DSL Validity, Geometry Solvability, Validation Pass Rate
- Solver: Final Answer Accuracy, SymPy Verification Rate
- End-to-End: E2E Accuracy, Latency, Token / LLM Usage, OCR Correction Rate, Geometry Degradation Rate
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
def _levenshtein_distance(s1: str, s2: str) -> int:
"""Computes standard Levenshtein edit distance between two strings."""
if len(s1) < len(s2):
return _levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
def compute_cer(reference: str, hypothesis: str) -> float:
"""Character Error Rate (CER)."""
if not reference and not hypothesis:
return 0.0
if not reference:
return 1.0
dist = _levenshtein_distance(reference, hypothesis)
return float(dist / max(len(reference), 1))
def compute_wer(reference: str, hypothesis: str) -> float:
"""Word Error Rate (WER)."""
ref_words = reference.strip().split()
hyp_words = hypothesis.strip().split()
if not ref_words and not hyp_words:
return 0.0
if not ref_words:
return 1.0
# Word-level edit distance
n, m = len(ref_words), len(hyp_words)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = i
for j in range(m + 1):
dp[0][j] = j
for i in range(1, n + 1):
for j in range(1, m + 1):
if ref_words[i - 1] == hyp_words[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)
return float(dp[n][m] / max(n, 1))
def normalize_latex(formula: str) -> str:
"""Normalizes LaTeX whitespace and common variations for comparison."""
if not formula:
return ""
f = formula.strip()
f = re.sub(r"\s+", "", f)
f = f.replace("\\cdot", "*").replace("\\times", "*")
f = re.sub(r"\\left|\\right", "", f)
return f
def latex_match(ref: str, hyp: str) -> bool:
"""Checks whether two LaTeX expressions match after normalization."""
return normalize_latex(ref) == normalize_latex(hyp)
@dataclass
class OCRMetrics:
"""Aggregated OCR metrics across evaluated samples."""
total_samples: int = 0
avg_cer: float = 0.0
avg_wer: float = 0.0
latex_exact_match_rate: float = 0.0
vlm_trigger_rate: float = 0.0
vlm_correction_rate: float = 0.0
confidence_calibration_bins: Dict[str, Dict[str, float]] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"total_samples": self.total_samples,
"avg_cer": round(self.avg_cer, 4),
"avg_wer": round(self.avg_wer, 4),
"latex_exact_match_rate": round(self.latex_exact_match_rate, 4),
"vlm_trigger_rate": round(self.vlm_trigger_rate, 4),
"vlm_correction_rate": round(self.vlm_correction_rate, 4),
"confidence_calibration": self.confidence_calibration_bins,
}
@dataclass
class ParserMetrics:
"""Aggregated Parser metrics."""
total_samples: int = 0
json_valid_rate: float = 0.0
dsl_valid_rate: float = 0.0
solvability_rate: float = 0.0
validation_pass_rate: float = 0.0
degradation_rate: float = 0.0
def to_dict(self) -> Dict[str, Any]:
return {
"total_samples": self.total_samples,
"json_valid_rate": round(self.json_valid_rate, 4),
"dsl_valid_rate": round(self.dsl_valid_rate, 4),
"solvability_rate": round(self.solvability_rate, 4),
"validation_pass_rate": round(self.validation_pass_rate, 4),
"degradation_rate": round(self.degradation_rate, 4),
}
@dataclass
class SolverMetrics:
"""Aggregated Solver metrics."""
total_samples: int = 0
answer_exact_match_rate: float = 0.0
sympy_verification_rate: float = 0.0
def to_dict(self) -> Dict[str, Any]:
return {
"total_samples": self.total_samples,
"answer_exact_match_rate": round(self.answer_exact_match_rate, 4),
"sympy_verification_rate": round(self.sympy_verification_rate, 4),
}
@dataclass
class PipelineEvalSummary:
"""Complete End-to-End Evaluation Report."""
ocr: OCRMetrics = field(default_factory=OCRMetrics)
parser: ParserMetrics = field(default_factory=ParserMetrics)
solver: SolverMetrics = field(default_factory=SolverMetrics)
e2e_success_rate: float = 0.0
avg_latency_ms: float = 0.0
total_samples: int = 0
def to_dict(self) -> Dict[str, Any]:
return {
"total_samples": self.total_samples,
"e2e_success_rate": round(self.e2e_success_rate, 4),
"avg_latency_ms": round(self.avg_latency_ms, 2),
"ocr_metrics": self.ocr.to_dict(),
"parser_metrics": self.parser.to_dict(),
"solver_metrics": self.solver.to_dict(),
}