File size: 6,150 Bytes
f12132c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""Smart MCQ Solver - Gradio demo.

Loads the fine-tuned DeBERTa-v3-large multiple-choice model from the Hub and
ranks five candidate answers for a question.
"""
import os
import numpy as np
import torch
import gradio as gr
from transformers import AutoTokenizer, AutoModelForMultipleChoice

# Available only on ZeroGPU hardware. On CPU-basic the import fails and we stay on CPU,
# so this one file runs unchanged on either.
try:
    import spaces
    HAS_ZEROGPU = True
except ImportError:
    HAS_ZEROGPU = False

MODEL_ID = os.environ.get("MODEL_ID", "SriragData/smart-mcq-deberta-v3-large")
MAX_LEN = 256
LAB = list("ABCDE")

torch.set_num_threads(max(1, (os.cpu_count() or 2) // 2))

print(f"loading {MODEL_ID} ...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

# fp32 is forced explicitly on purpose: transformers honours the dtype recorded in
# config.json, and an fp16 DeBERTa-v3 collapses to a constant output with no error
# raised. The kwarg was renamed (torch_dtype -> dtype) between versions, so try both
# rather than pinning ourselves to one.
try:
    model = AutoModelForMultipleChoice.from_pretrained(MODEL_ID, dtype=torch.float32)
except TypeError:
    model = AutoModelForMultipleChoice.from_pretrained(MODEL_ID, torch_dtype=torch.float32)
model = model.eval()
assert next(model.parameters()).dtype == torch.float32, "model did not load in fp32"
print("loaded. params:", sum(p.numel() for p in model.parameters()))


TEMPLATES = [
    "Pick the best possible answer:", "Select the most accurate option:",
    "Identify the correct statement:", "Determine the correct option:",
    "Choose the correct answer:", "Which of the following is correct?",
]


def clean(prompt: str) -> str:
    """Strip the boilerplate wrappers the training data was generated with."""
    for t in TEMPLATES:
        prompt = prompt.replace(t, "")
    return prompt.strip()


@torch.no_grad()
def solve(question, a, b, c, d, e):
    options = [a, b, c, d, e]
    if not question or not question.strip():
        return "Enter a question.", None
    if any(not str(o).strip() for o in options):
        return "All five options are required.", None

    # On ZeroGPU, CUDA only exists inside the decorated call, so resolve the device here
    # rather than at import time.
    device = "cuda" if torch.cuda.is_available() else "cpu"
    m = model.to(device)

    q = clean(question)
    enc = tokenizer([q] * 5, [str(o) for o in options],
                    truncation=True, max_length=MAX_LEN,
                    padding=True, return_tensors="pt")
    logits = m(input_ids=enc["input_ids"].unsqueeze(0).to(device),
               attention_mask=enc["attention_mask"].unsqueeze(0).to(device)).logits[0]
    probs = torch.softmax(logits.float().cpu(), dim=-1).numpy()

    order = np.argsort(-probs)
    top3 = " ".join(LAB[i] for i in order[:3])
    verdict = (f"### {LAB[order[0]]}{options[order[0]]}\n\n"
               f"**MAP@3 submission format:** `{top3}`")

    rows = [[LAB[i], str(options[i])[:200], round(float(probs[i]), 4),
             int(np.where(order == i)[0][0]) + 1] for i in range(5)]
    rows.sort(key=lambda r: r[3])
    return verdict, rows


if HAS_ZEROGPU:
    # Requests a GPU slice for the duration of the call. Applied programmatically so the
    # same file still imports on CPU-basic hardware, where `spaces` does not exist.
    solve = spaces.GPU(duration=60)(solve)


EXAMPLES = [
    ["Which philosopher argued that existence precedes essence?",
     "Immanuel Kant", "Jean-Paul Sartre", "David Hume", "Rene Descartes", "Baruch Spinoza"],
    ["What is the primary function of mitochondria in a eukaryotic cell?",
     "Protein synthesis", "Storage of genetic material",
     "Production of ATP through oxidative phosphorylation",
     "Breakdown of cellular waste", "Regulation of cell division"],
    ["According to the second law of thermodynamics, what happens to the entropy "
     "of an isolated system over time?",
     "It decreases to zero", "It remains exactly constant",
     "It never decreases", "It oscillates periodically", "It becomes negative"],
]

CSS = """
.gradio-container {max-width: 1000px !important}
footer {visibility: hidden}
"""

with gr.Blocks(title="Smart MCQ Solver", css=CSS, theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        "# Smart MCQ Solver\n"
        "Fine-tuned **DeBERTa-v3-large** ranking five candidate answers. "
        "Scored by MAP@3: credit 1, 1/2, 1/3 if the correct option is ranked 1st, 2nd or 3rd.\n\n"
        "*3-fold grouped CV MAP@3 **0.7567** — random baseline is 0.3667. "
        "Closed-book: it answers from its weights, with no retrieval and no abstention.*"
    )
    with gr.Row():
        with gr.Column(scale=3):
            question = gr.Textbox(label="Question", lines=2,
                                  placeholder="Ask a science or philosophy question...")
            with gr.Row():
                a = gr.Textbox(label="A")
                b = gr.Textbox(label="B")
            with gr.Row():
                c = gr.Textbox(label="C")
                d = gr.Textbox(label="D")
            e = gr.Textbox(label="E")
            btn = gr.Button("Rank the options", variant="primary")
        with gr.Column(scale=2):
            verdict = gr.Markdown(label="Answer")
            table = gr.Dataframe(headers=["Option", "Text", "Probability", "Rank"],
                                 datatype=["str", "str", "number", "number"],
                                 label="All five, ranked", wrap=True)

    btn.click(solve, [question, a, b, c, d, e], [verdict, table])
    gr.Examples(EXAMPLES, inputs=[question, a, b, c, d, e])

    gr.Markdown(
        "---\n"
        "**Limitations.** Trained on 2,000 rows covering 252 unique questions, so coverage "
        "is narrow. It has no way to say *I don't know* — it will rank five options "
        "confidently even when it knows nothing about the topic. Reported scores reflect a "
        "dataset whose test split overlaps its training split heavily; the leakage-free "
        "estimate for this project is 0.6817."
    )

if __name__ == "__main__":
    demo.launch()