Spaces:
Running
Running
Download semantic_evaluation.py from Baiyinyou/PDF2TEX_Formula: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/Baiyinyou/PDF2TEX_Formula/resolve/main/semantic_evaluation.py
- Command line
-
hf download hf://spaces/Baiyinyou/PDF2TEX_Formula/semantic_evaluation.py
-
curl -L -o semantic_evaluation.py https://huggingface.co/spaces/Baiyinyou/PDF2TEX_Formula/resolve/main/semantic_evaluation.py
11.3 kB
| # ========================================================= | |
| # Semantic Evaluation Pipeline for LaTeX Mathematical Expressions | |
| # ========================================================= | |
| import re | |
| import json | |
| import os | |
| from typing import Optional, Tuple, Dict, List | |
| import sympy as sp | |
| from sympy.parsing.latex import parse_latex | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from peft import PeftModel | |
| # ========================================================= | |
| # 1. Heuristic Analyzer | |
| # ========================================================= | |
| class HeuristicAnalyzer: | |
| OCR_GARBAGE_PATTERNS = [ | |
| r"\\underline", r"\\rlap", r"\\mit", r"\\bf" | |
| ] | |
| INVALID_LATEX_PATTERNS = [ | |
| r"\\backslash\s+[A-Za-z]", | |
| ] | |
| def check(self, formula: str) -> Dict: | |
| for p in self.OCR_GARBAGE_PATTERNS: | |
| if re.search(p, formula): | |
| return { | |
| "syntactically_correct": False, | |
| "mathematically_correct": False, | |
| "semantic_verdict": "garbled", | |
| "error_type": "garbled_ocr", | |
| "confidence": 0.95, | |
| "source": "heuristic", | |
| "explanation": "The formula contains OCR or encoding artifacts.", | |
| "suggested_fix": "Clean or retype the LaTeX." | |
| } | |
| for p in self.INVALID_LATEX_PATTERNS: | |
| if re.search(p, formula): | |
| return { | |
| "syntactically_correct": False, | |
| "mathematically_correct": False, | |
| "semantic_verdict": "invalid_notation", | |
| "error_type": "notation_error", | |
| "confidence": 0.95, | |
| "source": "heuristic", | |
| "explanation": "Invalid LaTeX command usage.", | |
| "suggested_fix": "Replace with valid LaTeX." | |
| } | |
| return {"pass": True} | |
| # ========================================================= | |
| # 2. Symbolic Verifier | |
| # ========================================================= | |
| class SymbolicVerifier: | |
| def normalize_latex(latex: str) -> str: | |
| s = latex.strip() | |
| s = re.sub(r"(\d)([a-zA-Z])", r"\1*\2", s) | |
| s = re.sub(r"([a-zA-Z])([a-zA-Z])", r"\1*\2", s) | |
| s = s.replace("^", "**") | |
| return s | |
| def split_equation(latex: str) -> Optional[Tuple[str, str]]: | |
| if latex.count("=") != 1: | |
| return None | |
| lhs, rhs = latex.split("=") | |
| return lhs.strip(), rhs.strip() | |
| def equivalent(self, lhs_latex: str, rhs_latex: str) -> Optional[bool]: | |
| try: | |
| lhs = parse_latex(lhs_latex) | |
| rhs = parse_latex(rhs_latex) | |
| return sp.simplify(lhs - rhs) == 0 | |
| except Exception: | |
| pass | |
| try: | |
| lhs = sp.sympify(self.normalize_latex(lhs_latex)) | |
| rhs = sp.sympify(self.normalize_latex(rhs_latex)) | |
| return sp.simplify(lhs - rhs) == 0 | |
| except Exception: | |
| return None | |
| # ========================================================= | |
| # 3. Explanation-Only LLM | |
| # ========================================================= | |
| class ExplanationLLM: | |
| def __init__( | |
| self, | |
| base_model: str = "Qwen/Qwen2.5-Math-1.5B-Instruct", | |
| lora_dir: Optional[str] = None, | |
| device: str = "cuda" | |
| ): | |
| self.device = device | |
| self.tokenizer = AutoTokenizer.from_pretrained(base_model) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| base_model, | |
| device_map="auto", | |
| torch_dtype=torch.float16 | |
| ) | |
| if lora_dir: | |
| model = PeftModel.from_pretrained(model, lora_dir, device_map="auto") | |
| self.model = model | |
| def explain(self, formula: str, issue: str) -> str: | |
| prompt = f"""You are a mathematical LaTeX reviewer. | |
| Formula: | |
| {formula} | |
| Issue: | |
| {issue} | |
| Explain: | |
| - What is happening mathematically | |
| - Why it cannot be verified or is incorrect | |
| - What should be checked or fixed | |
| Do not output True/False. | |
| """ | |
| inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device) | |
| with torch.no_grad(): | |
| output = self.model.generate( | |
| **inputs, | |
| max_new_tokens=800, | |
| do_sample=False, | |
| eos_token_id=self.tokenizer.eos_token_id | |
| ) | |
| decoded = self.tokenizer.decode(output[0], skip_special_tokens=True) | |
| return decoded.replace(prompt, "").strip() | |
| # ========================================================= | |
| # 4. Formula Evaluator | |
| # ========================================================= | |
| class FormulaEvaluator: | |
| def __init__(self, explainer: ExplanationLLM): | |
| self.heuristic = HeuristicAnalyzer() | |
| self.symbolic = SymbolicVerifier() | |
| self.explainer = explainer | |
| def evaluate(self, formula: str) -> Dict: | |
| h = self.heuristic.check(formula) | |
| if "pass" not in h: | |
| return h | |
| eq = self.symbolic.split_equation(formula) | |
| if eq: | |
| result = self.symbolic.equivalent(eq[0], eq[1]) | |
| if result is True: | |
| return { | |
| "syntactically_correct": True, | |
| "mathematically_correct": True, | |
| "semantic_verdict": "equivalent", | |
| "error_type": None, | |
| "confidence": 0.99, | |
| "source": "sympy", | |
| "explanation": "Both sides are mathematically equivalent.", | |
| "suggested_fix": None | |
| } | |
| if result is False: | |
| return { | |
| "syntactically_correct": True, | |
| "mathematically_correct": False, | |
| "semantic_verdict": "contradiction", | |
| "error_type": "math_error", | |
| "confidence": 0.99, | |
| "source": "sympy+llm", | |
| "explanation": self.explainer.explain( | |
| formula, "Equality does not hold in general" | |
| ), | |
| "suggested_fix": "Correct the mathematical identity." | |
| } | |
| return { | |
| "syntactically_correct": True, | |
| "mathematically_correct": None, | |
| "semantic_verdict": "non_verifiable", | |
| "error_type": "uncertain", | |
| "confidence": 0.5, | |
| "source": "llm", | |
| "explanation": self.explainer.explain( | |
| formula, "Expression is not a verifiable equality" | |
| ), | |
| "suggested_fix": "Manual inspection required." | |
| } | |
| # ========================================================= | |
| # 5. LaTeX Processor | |
| # ========================================================= | |
| class LatexProcessor: | |
| MATH_ENV_PATTERNS = [ | |
| r"\$(.*?)\$", | |
| r"\\\((.*?)\\\)", | |
| r"\\\[(.*?)\\\]", | |
| r"\\begin\{equation\}(.*?)\\end\{equation\}", | |
| ] | |
| def extract(self, tex: str) -> List[str]: | |
| formulas = [] | |
| for p in self.MATH_ENV_PATTERNS: | |
| formulas.extend(re.findall(p, tex, re.DOTALL)) | |
| return [f.strip() for f in formulas] | |
| def clean(self, formula: str) -> str: | |
| formula = re.sub(r"\\underline\{\{.*?\}\}", "", formula) | |
| formula = re.sub(r"\\mit|\\bf", "", formula) | |
| formula = re.sub(r"\\l\\ell|\\L", "", formula) | |
| formula = formula.replace(r"\rlap/", "") | |
| formula = re.sub(r"[\^\_]{2,}", "", formula) | |
| formula = re.sub(r"\{\s*\}", "", formula) | |
| return formula.strip() | |
| def fix(self, formula: str) -> str: | |
| formula = re.sub(r"\\backslash\s*([A-Za-z])", r"\\setminus \1", formula) | |
| formula = formula.replace(r"\implies", r"\Rightarrow") | |
| return formula | |
| # ========================================================= | |
| # 6. High-Level Pipeline | |
| # ========================================================= | |
| class SemanticEvaluationPipeline: | |
| def __init__( | |
| self, | |
| base_model: str = "Qwen/Qwen2.5-Math-1.5B-Instruct", | |
| lora_dir: Optional[str] = None, | |
| device: str = "cuda" | |
| ): | |
| self.latex = LatexProcessor() | |
| self.explainer = ExplanationLLM(base_model, lora_dir, device) | |
| self.evaluator = FormulaEvaluator(self.explainer) | |
| def evaluate_formula(self, formula: str) -> Dict: | |
| cleaned = self.latex.clean(formula) | |
| fixed = self.latex.fix(cleaned) | |
| return { | |
| "original_formula": formula, | |
| "fixed_formula": fixed, | |
| **self.evaluator.evaluate(fixed) | |
| } | |
| def evaluate_tex_file(self, tex_path: str) -> List[Dict]: | |
| with open(tex_path, "r", encoding="utf-8") as f: | |
| tex = f.read() | |
| formulas = self.latex.extract(tex) | |
| return [self.evaluate_formula(f) for f in formulas] | |
| def save_results(self, results: List[Dict], out_dir: str): | |
| os.makedirs(out_dir, exist_ok=True) | |
| # JSON | |
| json_path = os.path.join(out_dir, "results_test_1.json") | |
| with open(json_path, "w", encoding="utf-8") as f: | |
| json.dump(results, f, indent=2, ensure_ascii=False) | |
| # TXT | |
| txt_path = os.path.join(out_dir, "results_test_1.txt") | |
| with open(txt_path, "w", encoding="utf-8") as f: | |
| for i, r in enumerate(results, 1): | |
| f.write(f"Formula #{i}\n") | |
| f.write("-" * 80 + "\n") | |
| f.write(f"Original Formula:\n{r['original_formula']}\n\n") | |
| f.write(f"Fixed Formula:\n{r['fixed_formula']}\n\n") | |
| f.write(f"Syntactically Correct: {r['syntactically_correct']}\n") | |
| f.write(f"Mathematically Correct: {r['mathematically_correct']}\n") | |
| f.write(f"Semantic Verdict: {r['semantic_verdict']}\n") | |
| f.write(f"Error Type: {r['error_type']}\n") | |
| f.write(f"Confidence: {r['confidence']}\n") | |
| f.write(f"Source: {r['source']}\n\n") | |
| f.write("Explanation:\n") | |
| f.write(r["explanation"] + "\n") | |
| f.write("=" * 80 + "\n\n") | |
| return json_path, txt_path | |
| # ========================================================= | |
| # 7. CLI / Main | |
| # ========================================================= | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| description="Semantic evaluation of LaTeX mathematical expressions" | |
| ) | |
| parser.add_argument("tex_file", help="Path to LaTeX (.tex) file") | |
| parser.add_argument("--out", default="output", help="Output directory") | |
| parser.add_argument("--base_model", default="Qwen/Qwen2.5-Math-1.5B-Instruct") | |
| parser.add_argument("--lora_dir", default=None) | |
| parser.add_argument("--device", default="cuda") | |
| args = parser.parse_args() | |
| pipeline = SemanticEvaluationPipeline( | |
| base_model=args.base_model, | |
| lora_dir=args.lora_dir, | |
| device=args.device | |
| ) | |
| results = pipeline.evaluate_tex_file(args.tex_file) | |
| json_path, txt_path = pipeline.save_results(results, args.out) | |
| print(f"✓ Evaluated {len(results)} formulas") | |
| print(f"✓ JSON saved to: {json_path}") | |
| print(f"✓ TXT saved to: {txt_path}") | |
| if __name__ == "__main__": | |
| main() | |