|
|
|
|
|
|
|
|
|
|
|
import os |
|
import json |
|
import logging |
|
from typing import Tuple |
|
|
|
import gradio as gr |
|
import spaces |
|
|
|
|
|
|
|
|
|
ADAPTER_PATH = os.getenv("ADAPTER_PATH", "./lora_adapter") |
|
FALLBACK_MODEL = "distilbert-base-uncased" |
|
LABELS = {0: "โ
Correct", |
|
1: "๐ค Conceptual Error", |
|
2: "๐ข Computational Error"} |
|
|
|
logging.basicConfig(level=logging.INFO) |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
model = None |
|
tokenizer = None |
|
model_ty = None |
|
|
|
|
|
|
|
|
|
def _load_model_gpu(): |
|
""" |
|
Runs **inside the GPU worker**. |
|
Tries LoRA classification adapter โ LoRA causal-LM adapter โ plain baseline. |
|
""" |
|
global model, tokenizer, model_ty |
|
import torch |
|
from transformers import ( |
|
AutoTokenizer, |
|
AutoModelForSequenceClassification, |
|
) |
|
from peft.auto import ( |
|
AutoPeftModelForSequenceClassification, |
|
AutoPeftModelForCausalLM, |
|
) |
|
|
|
dtype = torch.float16 |
|
if os.path.isdir(ADAPTER_PATH): |
|
logger.info(f"[GPU] Loading adapter from {ADAPTER_PATH}") |
|
|
|
try: |
|
model = AutoPeftModelForSequenceClassification.from_pretrained( |
|
ADAPTER_PATH, torch_dtype=dtype, device_map="auto" |
|
) |
|
model_ty = "classification" |
|
except ValueError: |
|
logger.info("[GPU] Not a classifier, trying causal-LM") |
|
model = AutoPeftModelForCausalLM.from_pretrained( |
|
ADAPTER_PATH, torch_dtype=dtype, device_map="auto" |
|
) |
|
model_ty = "causal_lm" |
|
|
|
tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH) |
|
|
|
else: |
|
logger.warning("[GPU] No adapter found โ using baseline DistilBERT") |
|
tokenizer = AutoTokenizer.from_pretrained(FALLBACK_MODEL) |
|
model = AutoModelForSequenceClassification.from_pretrained( |
|
FALLBACK_MODEL, num_labels=3, ignore_mismatched_sizes=True |
|
) |
|
model_ty = "baseline" |
|
|
|
if tokenizer.pad_token is None: |
|
tokenizer.pad_token = tokenizer.eos_token or tokenizer.sep_token |
|
|
|
model.eval() |
|
logger.info(f"[GPU] Model ready ({model_ty})") |
|
|
|
|
|
def _classify_logits(question: str, solution: str) -> Tuple[str, str, str]: |
|
import torch |
|
text = f"Question: {question}\n\nSolution:\n{solution}" |
|
inputs = tokenizer( |
|
text, return_tensors="pt", padding=True, truncation=True, max_length=512 |
|
).to("cuda") |
|
|
|
with torch.no_grad(): |
|
logits = model(**inputs).logits |
|
probs = torch.softmax(logits, dim=-1)[0] |
|
pred = int(torch.argmax(probs)) |
|
conf = f"{probs[pred].item():.3f}" |
|
|
|
return LABELS[pred], conf, "โ" |
|
|
|
|
|
def _classify_generate(question: str, solution: str) -> Tuple[str, str, str]: |
|
import torch |
|
prompt = ( |
|
"You are a mathematics tutor.\n" |
|
"You are given a math word problem and a student's solution. Decide whether the solution is correct.\n\n" |
|
"- Correct = all reasoning and calculations are correct.\n" |
|
"- Conceptual Error = reasoning is wrong.\n" |
|
"- Computational Error= reasoning okay but arithmetic off.\n\n" |
|
"Reply with ONLY one of these JSON lines:\n" |
|
'{"verdict": "correct"}\n' |
|
'{"verdict": "conceptual"}\n' |
|
'{"verdict": "computational"}\n\n' |
|
f"Question: {question}\n\nSolution:\n{solution}\n\nAnswer:" |
|
) |
|
|
|
inputs = tokenizer(prompt, return_tensors="pt").to("cuda") |
|
with torch.no_grad(): |
|
out_ids = model.generate( |
|
**inputs, |
|
max_new_tokens=32, |
|
pad_token_id=tokenizer.eos_token_id, |
|
) |
|
generated = tokenizer.decode( |
|
out_ids[0][inputs["input_ids"].shape[1]:], |
|
skip_special_tokens=True, |
|
).strip() |
|
|
|
verdict = "Unparsed" |
|
try: |
|
data = json.loads(generated.splitlines()[-1]) |
|
v = data.get("verdict", "").lower() |
|
if v.startswith("corr"): |
|
verdict = LABELS[0] |
|
elif v.startswith("conc"): |
|
verdict = LABELS[1] |
|
elif v.startswith("comp"): |
|
verdict = LABELS[2] |
|
except Exception: |
|
pass |
|
|
|
return verdict, "", generated |
|
|
|
|
|
@spaces.GPU |
|
def gpu_classify(question: str, solution: str): |
|
""" |
|
Proxy target for Gradio. Executed in the GPU worker so CUDA is allowed. |
|
Returns (verdict, confidence, raw_output) |
|
""" |
|
if model is None: |
|
_load_model_gpu() |
|
|
|
if not question.strip() or not solution.strip(): |
|
return "Please fill both fields.", "", "" |
|
|
|
if model_ty in ("classification", "baseline"): |
|
return _classify_logits(question, solution) |
|
else: |
|
return _classify_generate(question, solution) |
|
|
|
|
|
|
|
|
|
def classify_proxy(q, s): |
|
"""Simple wrapper so Gradio can call the GPU function.""" |
|
return gpu_classify(q, s) |
|
|
|
with gr.Blocks(title="Math Solution Classifier") as demo: |
|
gr.Markdown("# ๐งฎ Math Solution Classifier") |
|
gr.Markdown( |
|
"Classify a student's math solution as **correct**, **conceptually flawed**, " |
|
"or **computationally flawed**." |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
q_in = gr.Textbox(label="Math Question", lines=3) |
|
s_in = gr.Textbox(label="Proposed Solution", lines=6) |
|
btn = gr.Button("Classify", variant="primary") |
|
with gr.Column(): |
|
verdict = gr.Textbox(label="Verdict", interactive=False) |
|
conf = gr.Textbox(label="Confidence", interactive=False) |
|
raw = gr.Textbox(label="Model Output", interactive=False) |
|
|
|
btn.click(classify_proxy, [q_in, s_in], [verdict, conf, raw]) |
|
|
|
gr.Examples( |
|
[ |
|
["Solve for x: 2x + 5 = 13", "2x + 5 = 13\n2x = 8\nx = 4"], |
|
["Find the derivative of f(x)=xยฒ", "f'(x)=2x+1"], |
|
["What is 15% of 200?", "0.15 ร 200 = 30"], |
|
], |
|
inputs=[q_in, s_in], |
|
) |
|
app = demo |
|
|
|
|
|
|
|
|