EvalHarness/evalharness/eval/math_grader.py

146 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Optional sympy-based math grader (adapted from OpenAI PRM800K, MIT license).
Imported lazily by the math_equal scorer; only needed when normalized string
equality is not enough (unreduced fractions, symbolic forms, units).
Requires: pip install sympy pylatexenc
"""
import logging
from typing import Optional
logger = logging.getLogger(__name__)
try:
import sympy
from pylatexenc import latex2text
from sympy.parsing import sympy_parser
_OK = True
except ImportError: # pragma: no cover - optional dependency
_OK = False
BAD_SUBSTRINGS = ('^{', '^(')
TUPLE_CHARS = '()[]'
def _sympy_parse(expr: str):
return sympy_parser.parse_expr(
expr.replace('^', '**'),
transformations=(
sympy_parser.standard_transformations
+ (sympy_parser.implicit_multiplication_application,)
),
)
def _parse_latex(expr: str) -> str:
expr = expr.replace('\\tfrac', '\\frac').replace('\\dfrac', '\\frac')
expr = latex2text.LatexNodes2Text().latex_to_text(expr)
return (expr.replace('', 'sqrt').replace('π', 'pi').replace('·', '*')
.replace('×', '*').strip())
def _strip_commas(expr: str) -> str:
import re
return re.sub(r'(\d),(\d\d\d)(?=\D|$)', r'\1\2', expr)
def _is_float(x) -> bool:
try:
float(x)
return True
except (TypeError, ValueError):
return False
def _str_is_int(x: str) -> bool:
try:
return abs(float(x) - int(round(float(x)))) <= 1e-7
except (TypeError, ValueError):
return False
def _is_frac(expr: str) -> bool:
import re
return bool(re.fullmatch(r'-?[0-9]+.?/0*[1-9][0-9]*.?', expr or ''))
def _normalize(expr: Optional[str]) -> Optional[str]:
import re
if expr is None:
return None
m = re.fullmatch(r'\\text\{(.+?)\}', expr)
if m:
expr = m.group(1)
expr = (expr.replace('\\%', '%').replace('\\$', '$').replace('$', '').replace('%', '')
.replace(' or ', ' , ').replace(' and ', ' , ')
.replace('million', '*10^6').replace('billion', '*10^9'))
for unit in ('degree', 'cm', 'meter', 'mile', 'second', 'minute', 'hour',
'day', 'week', 'month', 'year', 'foot', 'feet', 'inch', 'yard'):
expr = re.sub(unit + r'(es)?(s)? *(\^[0-9]+)?', '', expr)
expr = re.sub(r'\^ *\\circ', '', expr)
if len(expr) > 1 and expr[0] == '{' and expr[-1] == '}':
expr = expr[1:-1]
expr = _strip_commas(expr)
if _is_float(expr):
try:
f = float(expr)
if abs(f - round(f)) <= 1e-7:
expr = str(int(round(f)))
except (TypeError, ValueError):
pass
if '\\' in expr:
try:
expr = _parse_latex(expr)
except Exception:
pass
expr = re.sub(r'- *', '-', expr.replace(' ', ''))
expr = expr.replace('{', '').replace('}', '').lower()
if _str_is_int(expr):
expr = str(int(round(float(expr))))
return expr
def _split_tuple(expr: str):
expr = _strip_commas(expr)
if (len(expr) > 2 and expr[0] in TUPLE_CHARS and expr[-1] in TUPLE_CHARS
and all(c not in expr[1:-1] for c in TUPLE_CHARS)):
return [e.strip() for e in expr[1:-1].split(',')]
return [expr]
def _sympy_equal(a: str, b: str) -> bool:
try:
diff = _sympy_parse(f'({a})-({b})')
return sympy.simplify(diff) == 0
except Exception:
return False
def grade_answer(given_answer: str, ground_truth: str) -> bool:
"""True iff equal under normalization or sympy simplification."""
if not _OK:
raise ImportError('pip install sympy pylatexenc for symbolic math grading')
if given_answer is None:
return False
a, b = _normalize(given_answer), _normalize(ground_truth)
if a == b:
return True
if not a or not b:
return False
a_elems, b_elems = _split_tuple(a), _split_tuple(b)
if len(a_elems) != len(b_elems):
return False
for x, y in zip(a_elems, b_elems):
if _is_frac(x) and _is_frac(y):
if x != y:
return False
elif _str_is_int(x) != _str_is_int(y):
return False
elif not _sympy_equal(x, y):
return False
return True