aciang commited on
Commit
208dcde
·
verified ·
1 Parent(s): a94c11b

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +2 -6
  2. app.py +126 -166
  3. requirements.txt +2 -8
README.md CHANGED
@@ -9,9 +9,5 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- 一個**小型 LLM(Phi-2 / Phi-3-mini)+ SymPy** 的數學混合推理 Space:
13
- - LLM:把**文字題**解析為可計算的數學式或步驟
14
- - SymPy:遇到**可符號計算**的部分(方程、微積分、因式分解…)直接算出精準解
15
- - 自動偵測:若輸入就是算式/方程 → 直接 SymPy;否則走 LLM→SymPy 流程
16
-
17
- > 預設模型:`microsoft/phi-2`(可在 app.py 換成你喜歡的小型模型)
 
9
  pinned: false
10
  ---
11
 
12
+ 混合路線:**先用 SymPy 嘗試直接解/化簡**(極快);必要時再用 **Phi-2** 做文字→步驟→答案補齊。
13
+ 若延遲偏高,可在介面取消勾選「啟用 LLM」,就只走 SymPy(即時回覆)。
 
 
 
 
app.py CHANGED
@@ -1,181 +1,141 @@
1
- import os, sys, re, json
2
  import gradio as gr
3
  import sympy as sp
4
-
5
- from transformers import (
6
- AutoTokenizer, AutoModelForCausalLM, pipeline
7
- )
8
-
9
- TITLE = "LanguageBridge — Math Hybrid (Phi + SymPy)"
10
- MODEL_ID = "microsoft/phi-2"
11
- MAX_NEW_TOKENS = 512
12
-
13
- def try_sympy_direct(q: str):
14
- """若使用者輸入就是算式/方程,走 SymPy 精準計算。支援多行 / 分號分隔 / 聯立。"""
15
- q = (q or "").strip()
16
- if not q:
17
- return None
18
-
19
- # 粗略偵測:若含 = 明顯算式符號
20
- if any(s in q for s in ["=", "+", "-", "*", "/", "^", "sin", "cos", "tan", "log", "sqrt", "∫", "d/dx"]):
 
 
 
 
21
  try:
22
- # 支援多式/聯立:以分號或換行切
23
- parts = [s.strip() for seg in q.split(";") for s in seg.split("\n")]
24
- eqs, syms = [], set()
25
- for s in parts:
26
- if not s:
27
- continue
28
- if "=" in s:
29
- left, right = s.split("=", 1)
30
- eq = sp.Eq(sp.sympify(left), sp.sympify(right))
31
- eqs.append(eq)
32
- syms |= eq.free_symbols
33
- syms |= eq.rhs.free_symbols if hasattr(eq, "rhs") else set()
34
- else:
35
- # 非方程,當作一般表達式,做一輪常見操作
36
- expr = sp.sympify(s)
37
- tips = []
38
- try:
39
- tips.append(f"簡化:{sp.simplify(expr)}")
40
- except Exception:
41
- pass
42
- try:
43
- x = list(expr.free_symbols)[0] if expr.free_symbols else sp.symbols("x")
44
- tips.append(f"對 {x} 微分:{sp.diff(expr, x)}")
45
- tips.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
46
- except Exception:
47
- pass
48
- if tips:
49
- return "\n".join(tips) # 只要有一行就回傳
50
- else:
51
- return f"結果:{expr}"
52
-
53
- if eqs:
54
- if not syms:
55
- x = sp.symbols("x")
56
- syms = {x}
57
- sol = sp.solve(eqs, list(syms), dict=True)
58
- if not sol:
59
- return "SymPy:無解或需要更多條件。"
60
- lines = []
61
- for i, sdict in enumerate(sol, 1):
62
- lines.append("解 {}: ".format(i) + ", ".join([f"{k} = {sp.simplify(v)}" for k, v in sdict.items()]))
63
- return "\n".join(lines)
64
-
65
- except Exception as e:
66
- # 交給 LLM 流程
67
- return None
68
-
69
- return None
70
-
71
-
72
- def build_llm():
73
- """嘗試以 4-bit 啟動(有 CUDA 時),否則退回 CPU。"""
74
- import torch
75
- has_cuda = torch.cuda.is_available()
76
- load_kwargs = {"device_map":"auto"}
77
-
78
- if has_cuda:
79
- try:
80
- import bitsandbytes as bnb # 檢查有無 bnb
81
- load_kwargs.update({"load_in_4bit": True})
82
  except Exception:
83
- # 沒有 bnb 就用 fp16
84
- load_kwargs.update({"torch_dtype": torch.float16})
85
- else:
86
- # CPU:讓 transformers 自行決定 dtype
87
- pass
88
-
89
- tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
90
  if tok.pad_token_id is None and tok.eos_token_id is not None:
91
  tok.pad_token = tok.eos_token
 
 
 
 
 
 
92
 
93
- mdl = AutoModelForCausalLM.from_pretrained(
94
- MODEL_ID,
95
- trust_remote_code=True,
96
- **load_kwargs
97
- )
98
-
99
- pipe = pipeline(
100
- "text-generation",
101
- model=mdl,
102
- tokenizer=tok,
103
- do_sample=False,
104
- max_new_tokens=MAX_NEW_TOKENS,
105
- temperature=0.2,
106
- top_p=0.9
107
- )
108
- return pipe
109
-
110
- LLM = None
111
-
112
- SYSTEM_PROMPT = (
113
- "You are a math teacher. When the user asks a word problem,\n"
114
- "1) parse it into a clean mathematical expression or a system of equations;\n"
115
- "2) if it is solvable by SymPy, output a single line starting with 'SymPy:' followed by a Python/SymPy expression;\n"
116
- "3) then give a concise final answer on the next line starting with 'Answer:'."
117
- )
118
-
119
- def llm_to_sympy_and_answer(pipe, q: str):
120
- prompt = (
121
- f"<s>System:\n{SYSTEM_PROMPT}\n</s>\n"
122
- f"User: {q}\n"
123
- f"Assistant:"
124
- )
125
- out = pipe(prompt, pad_token_id=pipe.tokenizer.eos_token_id)[0]["generated_text"]
126
- # 嘗試抓 SymPy: 行
127
- sym_line = None
128
- ans_line = None
129
- for line in out.splitlines():
130
- if line.strip().startswith("SymPy:"):
131
- sym_line = line.split("SymPy:",1)[-1].strip()
132
- if line.strip().startswith("Answer:"):
133
- ans_line = line.split("Answer:",1)[-1].strip()
134
-
135
- checked = ""
136
- if sym_line:
137
- try:
138
- val = eval(sym_line, {"sp": sp, "sympy": sp})
139
- # 若是可列印的結果(非方程),試著數值化或簡化
140
- if isinstance(val, (int, float, sp.Basic)):
141
- checked = f"SymPy 檢算:{sp.simplify(val)}"
142
- except Exception as e:
143
- checked = f"SymPy 檢算失敗:{e}"
144
-
145
- merge = []
146
- if sym_line: merge.append(f"SymPy: {sym_line}")
147
- if ans_line: merge.append(f"Answer: {ans_line}")
148
- if checked: merge.append(checked)
149
- return "\n".join(merge) if merge else out
150
-
151
 
152
- def solve(q: str):
153
- global LLM
154
  q = (q or "").strip()
155
  if not q:
156
- return "請輸入題目或算式。"
157
-
158
- # 1) 先嘗試 SymPy 直接處理
159
- direct = try_sympy_direct(q)
160
- if direct:
161
- return direct
162
-
163
- # 2) LLM → SymPy
164
- if LLM is None:
165
- LLM = build_llm()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  try:
167
- return llm_to_sympy_and_answer(LLM, q)
168
- except Exception as e:
169
- return f"[LLM流程失敗] {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- with gr.Blocks(title=TITLE) as demo:
172
- gr.Markdown(f"## {TITLE}\n貼上文字題或算式:LLM 解析 → SymPy 精算(可聯立)")
173
- with gr.Row():
174
- q = gr.Textbox(label="題目 / 算式", lines=8, placeholder="例如:一個數加上 5 等於 11,求這個數。\n或:2*x + 5 = 11;或:sin(x)**2 + cos(x)**2")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  with gr.Row():
176
- out = gr.Textbox(label="輸出", lines=12)
 
 
177
  btn = gr.Button("送出 🚀")
178
- btn.click(fn=solve, inputs=q, outputs=out)
 
179
 
180
- if __name__ == "__main__":
181
- demo.launch()
 
1
+ import os, re, torch
2
  import gradio as gr
3
  import sympy as sp
4
+ from functools import lru_cache
5
+
6
+ # 允許用環境變數覆蓋
7
+ MODEL_ID = os.getenv("MODEL_ID", "microsoft/phi-2")
8
+
9
+ USE_CUDA = torch.cuda.is_available()
10
+ DTYPE = torch.float16 if USE_CUDA else torch.float32
11
+
12
+ model = None
13
+ tok = None
14
+
15
+ def _load_model_once():
16
+ global model, tok
17
+ if model is not None:
18
+ return
19
+ from transformers import AutoTokenizer, AutoModelForCausalLM
20
+ kwargs = dict(torch_dtype=DTYPE, low_cpu_mem_usage=True, trust_remote_code=False)
21
+ if USE_CUDA:
22
+ kwargs["device_map"] = "auto"
23
+ kwargs["attn_implementation"] = "sdpa"
24
+ # 優先嘗試 4bit(若後端不支援會自動回退)
25
  try:
26
+ kwargs.update(dict(
27
+ load_in_4bit=True,
28
+ bnb_4bit_compute_dtype=torch.float16,
29
+ bnb_4bit_quant_type="nf4",
30
+ bnb_4bit_use_double_quant=True,
31
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  except Exception:
33
+ pass
34
+ tok = AutoTokenizer.from_pretrained(MODEL_ID)
 
 
 
 
 
35
  if tok.pad_token_id is None and tok.eos_token_id is not None:
36
  tok.pad_token = tok.eos_token
37
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, **kwargs)
38
+ model.eval()
39
+ try:
40
+ _ = infer_llm("Solve: 2x+5=11 → x = ?", max_new_tokens=8)
41
+ except Exception:
42
+ pass
43
 
44
+ @lru_cache(maxsize=64)
45
+ def _looks_like_math(s: str) -> bool:
46
+ return bool(re.search(r"[=+\-*/^()]|sin|cos|tan|sqrt|\^|\d", s or ""))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
+ def _try_sympy_first(q: str):
 
49
  q = (q or "").strip()
50
  if not q:
51
+ return None
52
+ # 先處理「聯立/多行」:分號或換行分割
53
+ parts = [p.strip() for seg in q.split(";") for p in seg.split("\n")]
54
+ eqs, syms = [], set()
55
+ for s in parts:
56
+ if not s:
57
+ continue
58
+ if "=" in s:
59
+ L, R = s.split("=", 1)
60
+ eq = sp.Eq(sp.sympify(L), sp.sympify(R))
61
+ eqs.append(eq)
62
+ syms |= eq.free_symbols
63
+ if hasattr(eq, "rhs"):
64
+ syms |= eq.rhs.free_symbols
65
+ if eqs:
66
+ if not syms:
67
+ syms = {sp.symbols("x")}
68
+ sol = sp.solve(eqs, list(syms), dict=True)
69
+ if sol:
70
+ lines = []
71
+ for i, s in enumerate(sol, 1):
72
+ lines.append("解 {}: ".format(i) + ", ".join([f"{k} = {sp.simplify(v)}" for k, v in s.items()]))
73
+ return "\n".join(lines)
74
+ return "無解或需要更多條件。"
75
+
76
+ # 非方程:嘗試化簡 / 微分 / 積分建議
77
  try:
78
+ expr = sp.sympify(q)
79
+ tips = []
80
+ try:
81
+ tips.append(f"簡化:{sp.simplify(expr)}")
82
+ except Exception:
83
+ pass
84
+ try:
85
+ x = list(expr.free_symbols)[0] if expr.free_symbols else sp.symbols("x")
86
+ tips.append(f"對 {x} 微分:{sp.diff(expr, x)}")
87
+ tips.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
88
+ except Exception:
89
+ pass
90
+ if tips:
91
+ return "\n".join(tips)
92
+ except Exception:
93
+ pass
94
+ return None
95
 
96
+ SYS = "You are a concise math parser. Return minimal steps and a final boxed answer."
97
+ def build_prompt(q: str):
98
+ return f"{SYS}\nQuestion: {q}\nAnswer:"
99
+
100
+ def infer_llm(prompt: str, max_new_tokens=64):
101
+ _load_model_once()
102
+ inputs = tok(prompt, return_tensors="pt").to(model.device)
103
+ with torch.inference_mode():
104
+ out = model.generate(
105
+ **inputs,
106
+ max_new_tokens=max_new_tokens,
107
+ do_sample=False,
108
+ temperature=0.2,
109
+ top_p=0.9,
110
+ repetition_penalty=1.05,
111
+ use_cache=True,
112
+ eos_token_id=tok.eos_token_id,
113
+ pad_token_id=tok.eos_token_id,
114
+ )
115
+ return tok.decode(out[0], skip_special_tokens=True)
116
+
117
+ def hybrid_solve(q, use_llm=True, max_new_tokens=64):
118
+ # 1) 先試 SymPy(極快)
119
+ ans = _try_sympy_first(q)
120
+ if ans is not None:
121
+ return ans
122
+ # 2) 再用 LLM(需要算力)
123
+ if not use_llm:
124
+ return "(已關閉 LLM)請提供可由 SymPy 直接處理的算式/方程。"
125
+ if not _looks_like_math(q):
126
+ return "請貼數學���或方程;一般文字可能造成延遲。"
127
+ return infer_llm(build_prompt(q), max_new_tokens=max_new_tokens).strip()
128
+
129
+ with gr.Blocks(title="LanguageBridge — Math Hybrid (Phi + SymPy)") as demo:
130
+ gr.Markdown("貼上文字或算式:LLM 解析 → SymPy 寫算(可聯立)")
131
+ q = gr.Textbox(lines=6, label="題目 / 算式(可含聯立)")
132
  with gr.Row():
133
+ use_llm = gr.Checkbox(value=True, label="啟用 LLM(慢時可關,只走 SymPy)")
134
+ mx_tok = gr.Slider(16, 128, value=64, step=8, label="max_new_tokens")
135
+ out = gr.Textbox(lines=12, label="輸出")
136
  btn = gr.Button("送出 🚀")
137
+ btn.click(hybrid_solve, inputs=[q, use_llm, mx_tok], outputs=out)
138
+ gr.Markdown("**小秘訣**:短提示、明確格式、能用等號就用等號(SymPy 快很多)。")
139
 
140
+ # queue 可同時處理 2 個請求;Spaces 後端較慢時可調小
141
+ demo.queue(concurrency_count=2).launch()
requirements.txt CHANGED
@@ -1,10 +1,4 @@
1
  gradio==4.44.1
2
- transformers==4.44.2
3
- accelerate>=0.31.0
4
- bitsandbytes==0.43.3
5
- sentencepiece
6
  sympy>=1.12
7
- huggingface_hub>=0.24.0
8
- safetensors
9
- einops
10
- numpy<2
 
1
  gradio==4.44.1
 
 
 
 
2
  sympy>=1.12
3
+ huggingface_hub==0.24.0
4
+ transformers==4.44.2