初始化项目,由ModelHub XC社区提供模型
Model: Nextorage/Llama-3.1-Swallow-8B-OpenMath-FT Source: Original Platform
This commit is contained in:
272
eval_utils.py
Normal file
272
eval_utils.py
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
評価ユーティリティ: boxed answer抽出、数学的等価判定、コード実行、フォーマットチェック
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Boxed Answer Extraction
|
||||
# =============================================================================
|
||||
|
||||
def extract_boxed_answer(text: str) -> Optional[str]:
|
||||
"""
|
||||
\\boxed{...} から最終回答を抽出する。
|
||||
ネスト対応: \\boxed{\\frac{1}{2}} のような場合も正しく抽出。
|
||||
複数ある場合は最後のものを返す (最終回答は通常末尾)。
|
||||
全角括弧()や日本語「箱に入れ」表記にも対応。
|
||||
"""
|
||||
# 表記ゆれに対応: \boxed, \\boxed, \Boxed, \\Boxed + 全角括弧
|
||||
pattern = r'\\?\\?[Bb]oxed\s*[\{(]'
|
||||
matches = list(re.finditer(pattern, text))
|
||||
|
||||
# フォールバック: 日本語「箱に入れ」パターン
|
||||
if not matches:
|
||||
jp_pattern = r'[\{(]([^})]+)[})]\s*(?:を箱に入れ|を.*?箱)'
|
||||
jp_match = re.search(jp_pattern, text)
|
||||
if jp_match:
|
||||
return jp_match.group(1).strip()
|
||||
return None
|
||||
|
||||
# 最後のマッチから抽出 (最終回答)
|
||||
last_match = matches[-1]
|
||||
start = last_match.end() # '{' または '(' の直後
|
||||
|
||||
# ネスト対応のブレースマッチング(全角括弧対応)
|
||||
depth = 1
|
||||
i = start
|
||||
while i < len(text) and depth > 0:
|
||||
if text[i] in ('{', '('):
|
||||
depth += 1
|
||||
elif text[i] in ('}', ')'):
|
||||
depth -= 1
|
||||
i += 1
|
||||
|
||||
if depth == 0:
|
||||
return text[start:i - 1].strip()
|
||||
return None
|
||||
|
||||
|
||||
def normalize_answer(answer: str) -> str:
|
||||
"""回答文字列を正規化する (比較用)"""
|
||||
if answer is None:
|
||||
return ""
|
||||
s = answer.strip()
|
||||
# $記号を除去
|
||||
s = s.replace("$", "")
|
||||
# LaTeXコマンドの正規化
|
||||
s = s.replace("\\%", "%")
|
||||
s = s.replace("\\$", "$")
|
||||
# LaTeXコマンド周辺のスペースを正規化
|
||||
# "\\ frac {a} {b}" → "\frac{a}{b}"
|
||||
s = re.sub(r'\\+\s*frac\s*', r'\\frac', s)
|
||||
s = re.sub(r'(\\frac)\s*\{', r'\1{', s)
|
||||
s = re.sub(r'\}\s*\{', '}{', s)
|
||||
# 余分な空白を除去
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
return s
|
||||
|
||||
|
||||
def math_equivalent(pred: str, gold: str) -> bool:
|
||||
"""
|
||||
2つの数学的表現が等価かどうかを判定する。
|
||||
sympyを使った数式パースを試み、失敗した場合は文字列比較にフォールバック。
|
||||
"""
|
||||
pred_norm = normalize_answer(pred)
|
||||
gold_norm = normalize_answer(gold)
|
||||
|
||||
if not pred_norm or not gold_norm:
|
||||
return False
|
||||
|
||||
# 1. 完全一致
|
||||
if pred_norm == gold_norm:
|
||||
return True
|
||||
|
||||
# 2. 数値比較 (小数・整数)
|
||||
try:
|
||||
pred_val = float(eval_simple_expr(pred_norm))
|
||||
gold_val = float(eval_simple_expr(gold_norm))
|
||||
if math.isclose(pred_val, gold_val, rel_tol=1e-6, abs_tol=1e-9):
|
||||
return True
|
||||
except (ValueError, TypeError, SyntaxError, ZeroDivisionError):
|
||||
pass
|
||||
|
||||
# 3. sympy による数式等価判定
|
||||
try:
|
||||
import sympy
|
||||
from sympy.parsing.latex import parse_latex
|
||||
|
||||
# LaTeX表記をsympyで解析
|
||||
try:
|
||||
pred_expr = parse_latex(pred_norm)
|
||||
gold_expr = parse_latex(gold_norm)
|
||||
except Exception:
|
||||
# LaTeXパースが失敗した場合、sympy.sympifyを試す
|
||||
pred_expr = sympy.sympify(pred_norm)
|
||||
gold_expr = sympy.sympify(gold_norm)
|
||||
|
||||
# 数値的に等しいか
|
||||
diff = sympy.simplify(pred_expr - gold_expr)
|
||||
if diff == 0:
|
||||
return True
|
||||
|
||||
# 数値に変換して比較
|
||||
pred_float = float(pred_expr.evalf())
|
||||
gold_float = float(gold_expr.evalf())
|
||||
if math.isclose(pred_float, gold_float, rel_tol=1e-6, abs_tol=1e-9):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 4. 文字列の緩い比較 (空白、カンマ区切り等を無視)
|
||||
pred_clean = re.sub(r'[,\s\\{}]', '', pred_norm.lower())
|
||||
gold_clean = re.sub(r'[,\s\\{}]', '', gold_norm.lower())
|
||||
if pred_clean == gold_clean:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def eval_simple_expr(s: str) -> float:
|
||||
"""簡単な数式文字列を評価する (分数、パーセント等に対応)"""
|
||||
s = s.strip()
|
||||
# パーセント
|
||||
if s.endswith('%'):
|
||||
return float(s[:-1])
|
||||
# LaTeX分数 \frac{a}{b}
|
||||
frac_match = re.match(r'\\frac\s*\{([^}]+)\}\s*\{([^}]+)\}', s)
|
||||
if frac_match:
|
||||
num = float(frac_match.group(1))
|
||||
den = float(frac_match.group(2))
|
||||
return num / den
|
||||
# 通常の分数 a/b
|
||||
if '/' in s and not any(c.isalpha() for c in s):
|
||||
parts = s.split('/')
|
||||
if len(parts) == 2:
|
||||
return float(parts[0]) / float(parts[1])
|
||||
return float(s)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Code Execution
|
||||
# =============================================================================
|
||||
|
||||
def extract_code_blocks(text: str) -> list[str]:
|
||||
"""<llm-code>...</llm-code> タグからPythonコードブロックを抽出"""
|
||||
pattern = r'<llm-code>(.*?)</llm-code>'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
return [m.strip() for m in matches if m.strip()]
|
||||
|
||||
|
||||
def _auto_print_last_expr(code: str) -> str:
|
||||
"""
|
||||
コードの最終行が print() を含まない式の場合、自動で print() を付与する。
|
||||
Jupyter ノートブック形式のコード(最終行が式評価のみ)に対応。
|
||||
"""
|
||||
lines = code.rstrip().split("\n")
|
||||
if not lines:
|
||||
return code
|
||||
|
||||
last_line = lines[-1].strip()
|
||||
|
||||
# 空行、コメント、代入文、制御文、print文はスキップ
|
||||
if (not last_line
|
||||
or last_line.startswith("#")
|
||||
or "=" in last_line and not last_line.startswith("=") and "==" not in last_line
|
||||
or last_line.startswith(("if ", "for ", "while ", "def ", "class ", "import ",
|
||||
"from ", "return ", "try:", "except", "with ", "raise "))
|
||||
or "print(" in last_line):
|
||||
return code
|
||||
|
||||
# 最終行が式→ print() で囲む
|
||||
lines[-1] = f"print({last_line})"
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def execute_code_safely(code: str, timeout: int = 10) -> dict:
|
||||
"""
|
||||
Pythonコードをサブプロセスで安全に実行する。
|
||||
最終行が式の場合は自動で print() を付与する。
|
||||
Returns: {"success": bool, "stdout": str, "stderr": str}
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
||||
# sympy は頻出なのでimportを追加
|
||||
code_with_print = _auto_print_last_expr(code)
|
||||
wrapped = "import sympy\nfrom sympy import *\n" + code_with_print
|
||||
f.write(wrapped)
|
||||
f.flush()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python3', f.name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"stdout": result.stdout.strip(),
|
||||
"stderr": result.stderr.strip(),
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": f"Timeout after {timeout}s",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": str(e),
|
||||
}
|
||||
|
||||
|
||||
def check_code_execution(text: str) -> dict:
|
||||
"""
|
||||
テキスト中のコードブロックを実行し、結果をまとめる。
|
||||
Returns: {"has_code": bool, "num_blocks": int, "all_success": bool, "results": list}
|
||||
"""
|
||||
blocks = extract_code_blocks(text)
|
||||
if not blocks:
|
||||
return {"has_code": False, "num_blocks": 0, "all_success": True, "results": []}
|
||||
|
||||
results = []
|
||||
for code in blocks:
|
||||
result = execute_code_safely(code)
|
||||
results.append(result)
|
||||
|
||||
return {
|
||||
"has_code": True,
|
||||
"num_blocks": len(blocks),
|
||||
"all_success": all(r["success"] for r in results),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Format Compliance
|
||||
# =============================================================================
|
||||
|
||||
def check_format_compliance(text: str) -> dict:
|
||||
"""回答のフォーマット遵守率をチェック"""
|
||||
has_boxed = bool(re.search(r'\\?\\?[Bb]oxed\s*\{', text))
|
||||
|
||||
# コードタグの整合性
|
||||
code_opens = len(re.findall(r'<llm-code>', text))
|
||||
code_closes = len(re.findall(r'</llm-code>', text))
|
||||
code_tags_balanced = code_opens == code_closes
|
||||
|
||||
# 不正なトークンが含まれていないか
|
||||
has_bad_tokens = bool(re.search(r'<\|im_end\|>|<\|im_start\|>|<\|endoftext\|>', text))
|
||||
|
||||
return {
|
||||
"has_boxed_answer": has_boxed,
|
||||
"code_tags_balanced": code_tags_balanced,
|
||||
"no_bad_tokens": not has_bad_tokens,
|
||||
"format_ok": has_boxed and code_tags_balanced and not has_bad_tokens,
|
||||
}
|
||||
Reference in New Issue
Block a user