File size: 11,348 Bytes
546739e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c03a970
 
 
 
 
 
546739e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c03a970
546739e
 
 
 
 
 
 
 
 
 
 
 
 
c03a970
 
546739e
c03a970
546739e
 
 
 
 
 
 
 
 
 
 
 
 
c03a970
 
546739e
 
 
 
 
c03a970
 
 
 
 
 
 
 
 
 
546739e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c03a970
546739e
 
c03a970
546739e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7512879
546739e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7512879
 
 
 
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
Smart MCQ Solver β€” Gradio Web Application
Loads ELECTRA-base + LoRA fine-tuned model weights from Hugging Face Hub (SpreadSheets600/electra-lora-mcq)
Scored by MAP@3 on Kaggle Science MCQ (0.8849 OOF MAP@3)
"""

import time
import threading
import numpy as np
import torch
import torch.nn as nn
import gradio as gr
from transformers import AutoTokenizer, AutoModel
from peft import PeftModel

try:
    import spaces
    HAS_SPACES = True
except ImportError:
    HAS_SPACES = False

# ── Model Configuration & Hub Repositories ────────────────────────────────
ADAPTER_REPO = "SpreadSheets600/electra-lora-mcq"
BACKBONE     = "google/electra-base-discriminator"
LABELS       = ["A", "B", "C", "D", "E"]
MAX_LENGTH   = 256

# ── Neural Architecture (mirrors notebook) ─────────────────────────────────
class EncoderMCQModel(nn.Module):
    def __init__(self, backbone):
        super().__init__()
        self.encoder    = backbone
        h               = backbone.config.hidden_size
        self.dropouts   = nn.ModuleList([nn.Dropout(0.2) for _ in range(5)])
        self.classifier = nn.Linear(h, 1)

    def mean_pool(self, hidden, mask):
        m = mask.unsqueeze(-1).float()
        return (hidden * m).sum(1) / m.sum(1).clamp(min=1e-9)

    def forward(self, input_ids, attention_mask):
        b, n, L = input_ids.shape
        out  = self.encoder(
            input_ids=input_ids.view(b*n, L),
            attention_mask=attention_mask.view(b*n, L),
        )
        pool  = self.mean_pool(out.last_hidden_state, attention_mask.view(b*n, L))
        logit = torch.mean(
            torch.stack([self.classifier(dp(pool)) for dp in self.dropouts]), 0
        )
        return logit.view(b, n)

# ── Lazy Thread-Safe Model Loader ──────────────────────────────────────────
_STATE = {"tokenizer": None, "model": None, "ready": False, "error": None}
_LOCK  = threading.Lock()

def _load_model():
    with _LOCK:
        if _STATE["ready"] or _STATE["error"]:
            return
        try:
            tok  = AutoTokenizer.from_pretrained(BACKBONE)
            base = AutoModel.from_pretrained(BACKBONE)
            wrap = EncoderMCQModel(base)
            model = PeftModel.from_pretrained(wrap, ADAPTER_REPO, is_trainable=False)
            model = model.eval()
            _STATE["tokenizer"] = tok
            _STATE["model"]     = model
            _STATE["ready"]     = True
        except Exception as e:
            _STATE["error"] = str(e)

def _ensure_model_ready():
    if not _STATE["ready"] and not _STATE["error"]:
        _load_model()
    if _STATE["error"]:
        raise RuntimeError(f"Model weight load error: {_STATE['error']}")

# ── Neural Inference Routine ───────────────────────────────────────────────
def run_electra_core(prompt, options):
    device = "cuda" if torch.cuda.is_available() else "cpu"
    tok   = _STATE["tokenizer"]
    model = _STATE["model"].to(device)

    ids_list, mask_list = [], []
    for opt in options:
        enc = tok(
            f"{prompt} [OPTION] {opt}",
            max_length=MAX_LENGTH,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )
        ids_list.append(enc["input_ids"].squeeze(0))
        mask_list.append(enc["attention_mask"].squeeze(0))

    ids   = torch.stack(ids_list).unsqueeze(0).to(device)
    masks = torch.stack(mask_list).unsqueeze(0).to(device)

    logits = model(ids, masks).squeeze(0).cpu().float().numpy()
    probs  = torch.softmax(torch.tensor(logits), dim=0).numpy()
    return logits, probs

if HAS_SPACES:
    @spaces.GPU
    @torch.inference_mode()
    def run_electra_inference(prompt, options):
        return run_electra_core(prompt, options)
else:
    @torch.inference_mode()
    def run_electra_inference(prompt, options):
        return run_electra_core(prompt, options)

def calculate_top3(logits):
    return [LABELS[i] for i in np.argsort(-logits)[:3]]

def calculate_map3(correct_label, ranked_labels):
    for k, label in enumerate(ranked_labels[:3], 1):
        if label == correct_label:
            return 1.0 / k
    return 0.0

# ── Gradio Prediction Callback ─────────────────────────────────────────────
def predict_mcq(prompt, opt_a, opt_b, opt_c, opt_d, opt_e, correct_key):
    if not prompt or not prompt.strip():
        raise gr.Error("Please enter a valid question prompt.")

    options = [opt_a, opt_b, opt_c, opt_d, opt_e]
    if any(not o or not o.strip() for o in options):
        raise gr.Error("Please fill in all 5 option choices (A–E).")

    _ensure_model_ready()

    t0 = time.perf_counter()
    logits, probs = run_electra_inference(prompt.strip(), [o.strip() for o in options])
    elapsed_ms = (time.perf_counter() - t0) * 1000

    ranked = calculate_top3(logits)
    medals = ["πŸ₯‡ 1st", "πŸ₯ˆ 2nd", "πŸ₯‰ 3rd"]

    top3_markdown = "   |   ".join([f"{medals[r]}: **{ranked[r]}**" for r in range(3)])

    conf_dict = {f"Option {LABELS[i]}": float(probs[i]) for i in range(5)}

    map3_markdown = ""
    if correct_key and correct_key.strip().upper() in LABELS:
        cl = correct_key.strip().upper()
        map3_val = calculate_map3(cl, ranked)
        pos = next((r + 1 for r, l in enumerate(ranked) if l == cl), None)
        pos_str = f"rank **{pos}**" if pos else "outside top-3"
        map3_markdown = f"πŸ“Š MAP@3 Metric = **{map3_val:.4f}** (Correct Answer: **{cl}** found at {pos_str})"

    order = np.argsort(-logits)
    table_rows = []
    for rank_idx, idx in enumerate(order):
        lbl = LABELS[idx]
        tick = " βœ“" if correct_key and lbl == correct_key.strip().upper() else ""
        table_rows.append([
            f"#{rank_idx + 1}",
            f"{lbl}{tick}",
            options[idx][:70] + ("…" if len(options[idx]) > 70 else ""),
            f"{probs[idx] * 100:.1f}%",
            f"{logits[idx]:.3f}"
        ])

    device_used = "ZeroGPU" if torch.cuda.is_available() else "CPU"
    info_markdown = (
        f"⏱ **{elapsed_ms:.0f} ms** response time  ·  "
        f"Device: **{device_used}**  Β·  "
        f"Model: **ELECTRA-base + LoRA ($r=16$)**  Β·  "
        f"5-Fold OOF MAP@3: **0.8849**"
    )

    return top3_markdown, conf_dict, table_rows, map3_markdown, info_markdown

# ── Pre-loaded Test Cases ──────────────────────────────────────────────────
EXAMPLES = [
    ["Which of the following is a prime number?", "15", "21", "23", "25", "27", "C"],
    ["What is the chemical symbol for gold?", "Ag", "Au", "Fe", "Pb", "Hg", "B"],
    ["The process by which plants make food using sunlight is called:", "Respiration", "Fermentation", "Photosynthesis", "Transpiration", "Osmosis", "C"],
    ["Which planet in our solar system has the most moons?", "Jupiter", "Saturn", "Uranus", "Neptune", "Mars", "B"],
    ["DNA replication occurs during which phase of the cell cycle?", "G1 phase", "S phase", "G2 phase", "M phase", "G0 phase", "B"],
    ["What is the powerhouse of the cell?", "Nucleus", "Ribosome", "Mitochondria", "Golgi apparatus", "Endoplasmic reticulum", "C"]
]

# ── Gradio Blocks Interface ────────────────────────────────────────────────
with gr.Blocks(title="Smart MCQ Solver") as demo:

    gr.HTML("""
    <div style="text-align:center; padding:20px 0 10px">
      <h1 style="margin:0; font-size:2.3rem; font-weight:800; color:#4f46e5">
        🎯 Smart MCQ Solver
      </h1>
      <p style="color:#475569; font-size:1.05rem; margin:6px 0 0">
        ELECTRA-base + LoRA Neural Option Scorer &nbsp;Β·&nbsp;
        <strong style="color:#059669">MAP@3 = 0.8849</strong> on 5-Fold CV
      </p>
    </div>
    """)

    with gr.Row():
        with gr.Column(scale=5):
            gr.Markdown("### πŸ“ Question Input")
            prompt_input = gr.Textbox(
                label="Question Prompt",
                placeholder="Type your prompt here...",
                lines=3
            )
            with gr.Row():
                opt_a = gr.Textbox(label="Option A")
                opt_b = gr.Textbox(label="Option B")
            with gr.Row():
                opt_c = gr.Textbox(label="Option C")
                opt_d = gr.Textbox(label="Option D")
            opt_e = gr.Textbox(label="Option E")
            correct_input = gr.Textbox(
                label="βœ… Correct Answer Key (Optional - A/B/C/D/E for MAP@3 Evaluation)",
                placeholder="Leave blank if unknown",
                max_lines=1
            )
            with gr.Row():
                predict_button = gr.Button("πŸ” Predict Top-3 & Confidence Scores", variant="primary", size="lg")
                clear_button = gr.ClearButton(
                    [prompt_input, opt_a, opt_b, opt_c, opt_d, opt_e, correct_input],
                    value="πŸ—‘ Clear",
                    size="lg"
                )

        with gr.Column(scale=5):
            gr.Markdown("### πŸ† Model Predictions & Analytics")
            top3_output = gr.Markdown(value="*Submit a question to see predictions.*")
            map3_output = gr.Markdown()

            gr.Markdown("#### Option Confidence Distribution")
            conf_output = gr.Label(label="Softmax Probability", num_top_classes=5)

            gr.Markdown("#### Full Option Ranking Details")
            table_output = gr.Dataframe(
                headers=["Rank", "Option", "Text", "Confidence %", "Logit"],
                interactive=False,
                wrap=True
            )
            info_output = gr.Markdown()

    gr.Markdown("---")
    gr.Examples(
        examples=EXAMPLES,
        inputs=[prompt_input, opt_a, opt_b, opt_c, opt_d, opt_e, correct_input],
        outputs=[top3_output, conf_output, table_output, map3_output, info_output],
        fn=predict_mcq,
        cache_examples=False,
        label="πŸ“š Load Example Test Case",
        examples_per_page=6
    )

    with gr.Accordion("ℹ️ Model Architecture & Evaluation Specs", open=False):
        gr.Markdown("""
| Component | Specification |
|---|---|
| **Backbone Model** | `google/electra-base-discriminator` |
| **LoRA Adapter** | `SpreadSheets600/electra-lora-mcq` ($r=16, \\alpha=32$) |
| **Trainable Parameters** | ~1.18M (~1% of total weights) |
| **Scoring Head** | Joint 5-way scoring via 5Γ— Dropout(0.2) + Linear layer |
| **5-Fold CV MAP@3** | **0.8849** |
| **5-Fold CV Accuracy** | **81.1%** |
| **Course & Student** | BITS Pilani WILP (DL & GenAI) Β· Student 24F2008474 |
        """)

    predict_button.click(
        fn=predict_mcq,
        inputs=[prompt_input, opt_a, opt_b, opt_c, opt_d, opt_e, correct_input],
        outputs=[top3_output, conf_output, table_output, map3_output, info_output]
    )

try:
    demo.launch(ssr_mode=False)
except TypeError:
    demo.launch()