Baiyinyou commited on
Commit
32e7f3a
·
verified ·
1 Parent(s): 3a3e98d

Upload semantic_evaluation.py

Browse files
Files changed (1) hide show
  1. semantic_evaluation.py +339 -0
semantic_evaluation.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =========================================================
2
+ # Semantic Evaluation Pipeline for LaTeX Mathematical Expressions
3
+ # =========================================================
4
+
5
+ import re
6
+ import json
7
+ import os
8
+ from typing import Optional, Tuple, Dict, List
9
+
10
+ import sympy as sp
11
+ from sympy.parsing.latex import parse_latex
12
+
13
+ import torch
14
+ from transformers import AutoTokenizer, AutoModelForCausalLM
15
+ from peft import PeftModel
16
+
17
+
18
+ # =========================================================
19
+ # 1. Heuristic Analyzer
20
+ # =========================================================
21
+
22
+ class HeuristicAnalyzer:
23
+ OCR_GARBAGE_PATTERNS = [
24
+ r"\\underline", r"\\rlap", r"\\mit", r"\\bf"
25
+ ]
26
+
27
+ INVALID_LATEX_PATTERNS = [
28
+ r"\\backslash\s+[A-Za-z]",
29
+ ]
30
+
31
+ def check(self, formula: str) -> Dict:
32
+ for p in self.OCR_GARBAGE_PATTERNS:
33
+ if re.search(p, formula):
34
+ return {
35
+ "syntactically_correct": False,
36
+ "mathematically_correct": False,
37
+ "semantic_verdict": "garbled",
38
+ "error_type": "garbled_ocr",
39
+ "confidence": 0.95,
40
+ "source": "heuristic",
41
+ "explanation": "The formula contains OCR or encoding artifacts.",
42
+ "suggested_fix": "Clean or retype the LaTeX."
43
+ }
44
+
45
+ for p in self.INVALID_LATEX_PATTERNS:
46
+ if re.search(p, formula):
47
+ return {
48
+ "syntactically_correct": False,
49
+ "mathematically_correct": False,
50
+ "semantic_verdict": "invalid_notation",
51
+ "error_type": "notation_error",
52
+ "confidence": 0.95,
53
+ "source": "heuristic",
54
+ "explanation": "Invalid LaTeX command usage.",
55
+ "suggested_fix": "Replace with valid LaTeX."
56
+ }
57
+
58
+ return {"pass": True}
59
+
60
+
61
+ # =========================================================
62
+ # 2. Symbolic Verifier
63
+ # =========================================================
64
+
65
+ class SymbolicVerifier:
66
+
67
+ @staticmethod
68
+ def normalize_latex(latex: str) -> str:
69
+ s = latex.strip()
70
+ s = re.sub(r"(\d)([a-zA-Z])", r"\1*\2", s)
71
+ s = re.sub(r"([a-zA-Z])([a-zA-Z])", r"\1*\2", s)
72
+ s = s.replace("^", "**")
73
+ return s
74
+
75
+ @staticmethod
76
+ def split_equation(latex: str) -> Optional[Tuple[str, str]]:
77
+ if latex.count("=") != 1:
78
+ return None
79
+ lhs, rhs = latex.split("=")
80
+ return lhs.strip(), rhs.strip()
81
+
82
+ def equivalent(self, lhs_latex: str, rhs_latex: str) -> Optional[bool]:
83
+ try:
84
+ lhs = parse_latex(lhs_latex)
85
+ rhs = parse_latex(rhs_latex)
86
+ return sp.simplify(lhs - rhs) == 0
87
+ except Exception:
88
+ pass
89
+
90
+ try:
91
+ lhs = sp.sympify(self.normalize_latex(lhs_latex))
92
+ rhs = sp.sympify(self.normalize_latex(rhs_latex))
93
+ return sp.simplify(lhs - rhs) == 0
94
+ except Exception:
95
+ return None
96
+
97
+
98
+ # =========================================================
99
+ # 3. Explanation-Only LLM
100
+ # =========================================================
101
+
102
+ class ExplanationLLM:
103
+ def __init__(
104
+ self,
105
+ base_model: str = "Qwen/Qwen2.5-Math-1.5B-Instruct",
106
+ lora_dir: Optional[str] = None,
107
+ device: str = "cuda"
108
+ ):
109
+ self.device = device
110
+ self.tokenizer = AutoTokenizer.from_pretrained(base_model)
111
+
112
+ model = AutoModelForCausalLM.from_pretrained(
113
+ base_model,
114
+ device_map="auto",
115
+ torch_dtype=torch.float16
116
+ )
117
+
118
+ if lora_dir:
119
+ model = PeftModel.from_pretrained(model, lora_dir, device_map="auto")
120
+
121
+ self.model = model
122
+
123
+ def explain(self, formula: str, issue: str) -> str:
124
+ prompt = f"""You are a mathematical LaTeX reviewer.
125
+
126
+ Formula:
127
+ {formula}
128
+
129
+ Issue:
130
+ {issue}
131
+
132
+ Explain:
133
+ - What is happening mathematically
134
+ - Why it cannot be verified or is incorrect
135
+ - What should be checked or fixed
136
+
137
+ Do not output True/False.
138
+ """
139
+
140
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
141
+
142
+ with torch.no_grad():
143
+ output = self.model.generate(
144
+ **inputs,
145
+ max_new_tokens=800,
146
+ do_sample=False,
147
+ eos_token_id=self.tokenizer.eos_token_id
148
+ )
149
+
150
+ decoded = self.tokenizer.decode(output[0], skip_special_tokens=True)
151
+ return decoded.replace(prompt, "").strip()
152
+
153
+
154
+ # =========================================================
155
+ # 4. Formula Evaluator
156
+ # =========================================================
157
+
158
+ class FormulaEvaluator:
159
+ def __init__(self, explainer: ExplanationLLM):
160
+ self.heuristic = HeuristicAnalyzer()
161
+ self.symbolic = SymbolicVerifier()
162
+ self.explainer = explainer
163
+
164
+ def evaluate(self, formula: str) -> Dict:
165
+ h = self.heuristic.check(formula)
166
+ if "pass" not in h:
167
+ return h
168
+
169
+ eq = self.symbolic.split_equation(formula)
170
+ if eq:
171
+ result = self.symbolic.equivalent(eq[0], eq[1])
172
+
173
+ if result is True:
174
+ return {
175
+ "syntactically_correct": True,
176
+ "mathematically_correct": True,
177
+ "semantic_verdict": "equivalent",
178
+ "error_type": None,
179
+ "confidence": 0.99,
180
+ "source": "sympy",
181
+ "explanation": "Both sides are mathematically equivalent.",
182
+ "suggested_fix": None
183
+ }
184
+
185
+ if result is False:
186
+ return {
187
+ "syntactically_correct": True,
188
+ "mathematically_correct": False,
189
+ "semantic_verdict": "contradiction",
190
+ "error_type": "math_error",
191
+ "confidence": 0.99,
192
+ "source": "sympy+llm",
193
+ "explanation": self.explainer.explain(
194
+ formula, "Equality does not hold in general"
195
+ ),
196
+ "suggested_fix": "Correct the mathematical identity."
197
+ }
198
+
199
+ return {
200
+ "syntactically_correct": True,
201
+ "mathematically_correct": None,
202
+ "semantic_verdict": "non_verifiable",
203
+ "error_type": "uncertain",
204
+ "confidence": 0.5,
205
+ "source": "llm",
206
+ "explanation": self.explainer.explain(
207
+ formula, "Expression is not a verifiable equality"
208
+ ),
209
+ "suggested_fix": "Manual inspection required."
210
+ }
211
+
212
+
213
+ # =========================================================
214
+ # 5. LaTeX Processor
215
+ # =========================================================
216
+
217
+ class LatexProcessor:
218
+ MATH_ENV_PATTERNS = [
219
+ r"\$(.*?)\$",
220
+ r"\\\((.*?)\\\)",
221
+ r"\\\[(.*?)\\\]",
222
+ r"\\begin\{equation\}(.*?)\\end\{equation\}",
223
+ ]
224
+
225
+ def extract(self, tex: str) -> List[str]:
226
+ formulas = []
227
+ for p in self.MATH_ENV_PATTERNS:
228
+ formulas.extend(re.findall(p, tex, re.DOTALL))
229
+ return [f.strip() for f in formulas]
230
+
231
+ def clean(self, formula: str) -> str:
232
+ formula = re.sub(r"\\underline\{\{.*?\}\}", "", formula)
233
+ formula = re.sub(r"\\mit|\\bf", "", formula)
234
+ formula = re.sub(r"\\l\\ell|\\L", "", formula)
235
+ formula = formula.replace(r"\rlap/", "")
236
+ formula = re.sub(r"[\^\_]{2,}", "", formula)
237
+ formula = re.sub(r"\{\s*\}", "", formula)
238
+ return formula.strip()
239
+
240
+ def fix(self, formula: str) -> str:
241
+ formula = re.sub(r"\\backslash\s*([A-Za-z])", r"\\setminus \1", formula)
242
+ formula = formula.replace(r"\implies", r"\Rightarrow")
243
+ return formula
244
+
245
+
246
+ # =========================================================
247
+ # 6. High-Level Pipeline
248
+ # =========================================================
249
+
250
+ class SemanticEvaluationPipeline:
251
+ def __init__(
252
+ self,
253
+ base_model: str = "Qwen/Qwen2.5-Math-1.5B-Instruct",
254
+ lora_dir: Optional[str] = None,
255
+ device: str = "cuda"
256
+ ):
257
+ self.latex = LatexProcessor()
258
+ self.explainer = ExplanationLLM(base_model, lora_dir, device)
259
+ self.evaluator = FormulaEvaluator(self.explainer)
260
+
261
+ def evaluate_formula(self, formula: str) -> Dict:
262
+ cleaned = self.latex.clean(formula)
263
+ fixed = self.latex.fix(cleaned)
264
+ return {
265
+ "original_formula": formula,
266
+ "fixed_formula": fixed,
267
+ **self.evaluator.evaluate(fixed)
268
+ }
269
+
270
+ def evaluate_tex_file(self, tex_path: str) -> List[Dict]:
271
+ with open(tex_path, "r", encoding="utf-8") as f:
272
+ tex = f.read()
273
+
274
+ formulas = self.latex.extract(tex)
275
+ return [self.evaluate_formula(f) for f in formulas]
276
+
277
+ def save_results(self, results: List[Dict], out_dir: str):
278
+ os.makedirs(out_dir, exist_ok=True)
279
+
280
+ # JSON
281
+ json_path = os.path.join(out_dir, "results_test_1.json")
282
+ with open(json_path, "w", encoding="utf-8") as f:
283
+ json.dump(results, f, indent=2, ensure_ascii=False)
284
+
285
+ # TXT
286
+ txt_path = os.path.join(out_dir, "results_test_1.txt")
287
+ with open(txt_path, "w", encoding="utf-8") as f:
288
+ for i, r in enumerate(results, 1):
289
+ f.write(f"Formula #{i}\n")
290
+ f.write("-" * 80 + "\n")
291
+ f.write(f"Original Formula:\n{r['original_formula']}\n\n")
292
+ f.write(f"Fixed Formula:\n{r['fixed_formula']}\n\n")
293
+ f.write(f"Syntactically Correct: {r['syntactically_correct']}\n")
294
+ f.write(f"Mathematically Correct: {r['mathematically_correct']}\n")
295
+ f.write(f"Semantic Verdict: {r['semantic_verdict']}\n")
296
+ f.write(f"Error Type: {r['error_type']}\n")
297
+ f.write(f"Confidence: {r['confidence']}\n")
298
+ f.write(f"Source: {r['source']}\n\n")
299
+ f.write("Explanation:\n")
300
+ f.write(r["explanation"] + "\n")
301
+ f.write("=" * 80 + "\n\n")
302
+
303
+ return json_path, txt_path
304
+
305
+
306
+ # =========================================================
307
+ # 7. CLI / Main
308
+ # =========================================================
309
+
310
+ def main():
311
+ import argparse
312
+
313
+ parser = argparse.ArgumentParser(
314
+ description="Semantic evaluation of LaTeX mathematical expressions"
315
+ )
316
+ parser.add_argument("tex_file", help="Path to LaTeX (.tex) file")
317
+ parser.add_argument("--out", default="output", help="Output directory")
318
+ parser.add_argument("--base_model", default="Qwen/Qwen2.5-Math-1.5B-Instruct")
319
+ parser.add_argument("--lora_dir", default=None)
320
+ parser.add_argument("--device", default="cuda")
321
+
322
+ args = parser.parse_args()
323
+
324
+ pipeline = SemanticEvaluationPipeline(
325
+ base_model=args.base_model,
326
+ lora_dir=args.lora_dir,
327
+ device=args.device
328
+ )
329
+
330
+ results = pipeline.evaluate_tex_file(args.tex_file)
331
+ json_path, txt_path = pipeline.save_results(results, args.out)
332
+
333
+ print(f"✓ Evaluated {len(results)} formulas")
334
+ print(f"✓ JSON saved to: {json_path}")
335
+ print(f"✓ TXT saved to: {txt_path}")
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()