Spaces:
Running
Running
| """ | |
| 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) | |
| 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, | |
| } | |
| 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), | |
| } | |
| 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), | |
| } | |
| 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(), | |
| } | |