File size: 7,848 Bytes
31edbfc | 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 | import spaces
import os
import torch
import numpy as np
import gradio as gr
from scipy.stats import rankdata
from scipy.special import softmax
from transformers import (
AutoModelForCausalLM,
AutoModelForMultipleChoice,
AutoModelForSequenceClassification,
AutoTokenizer,
BitsAndBytesConfig
)
from peft import PeftModel
HF_TOKEN = os.environ.get("HF_TOKEN")
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
FOLDS = [1]
BASE_MODEL_NAME_QWEN = "Qwen/Qwen3-4B-Instruct-2507"
MODEL1_RAG_QWEN3_REPO = "a-01a/QSolver_Decoder_V16"
MODEL2_DEBERTA_REPO = "a-01a/QSolver_Encoder_V2"
MODEL3_SCRATCH_REPO = "a-01a/QSolver_Scratch"
OPTION_LETTERS = ["A", "B", "C", "D", "E"]
IDX_TO_LABEL = {0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E'}
models_loaded = False
tokenizer_m1 = None
model1 = None
tokenizer_m2 = None
model2 = None
tokenizer_m3 = None
model3 = None
qwen_option_token_ids = None
def sharpen_probs_single(probs, power=1.5):
p_pow = np.power(probs, power)
return p_pow / np.sum(p_pow)
def to_percentile_ranks_single(probs):
return rankdata(probs) / len(probs)
def format_qwen3_prompt(context, question, opts):
ctx_line = f"Context: {context.strip()}\n" if context.strip() else ""
return (
f"<|im_start|>system\n"
f"You are a scientific expert. Base your answer STRICTLY on the provided Context. "
f"Output ONLY the single letter corresponding to the correct option (A, B, C, D, or E).<|im_end|>\n"
f"<|im_start|>user\n"
f"{ctx_line}Question: {question}\n"
f"A) {opts['A']}\nB) {opts['B']}\nC) {opts['C']}\nD) {opts['D']}\nE) {opts['E']}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
def get_answer_token_id(tokenizer, letter):
probe = "<|im_start|>assistant\n"
return tokenizer(probe + letter, add_special_tokens=False)["input_ids"][len(tokenizer(probe, add_special_tokens=False)["input_ids"]):][0]
def load_all_models():
global models_loaded, tokenizer_m1, model1, tokenizer_m2, model2, tokenizer_m3, model3, qwen_option_token_ids
if models_loaded:
return
device = "cuda"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16
)
tokenizer_m1 = AutoTokenizer.from_pretrained(BASE_MODEL_NAME_QWEN, token=HF_TOKEN, use_fast=True)
tokenizer_m1.truncation_side = "left"
tokenizer_m1.padding_side = "left"
if tokenizer_m1.pad_token is None:
tokenizer_m1.pad_token = tokenizer_m1.eos_token
qwen_base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_NAME_QWEN,
quantization_config=bnb_config,
device_map="auto",
token=HF_TOKEN
)
model1 = PeftModel.from_pretrained(qwen_base, MODEL1_RAG_QWEN3_REPO, subfolder=f"fold_{FOLDS[0]}", token=HF_TOKEN)
model1.eval()
qwen_option_token_ids = [get_answer_token_id(tokenizer_m1, l) for l in OPTION_LETTERS]
tokenizer_m2 = AutoTokenizer.from_pretrained(MODEL2_DEBERTA_REPO, subfolder=f"fold_{FOLDS[0]}", token=HF_TOKEN, use_fast=True)
model2 = AutoModelForMultipleChoice.from_pretrained(
MODEL2_DEBERTA_REPO,
subfolder=f"fold_{FOLDS[0]}",
token=HF_TOKEN,
torch_dtype=torch.float16
).to(device)
model2.eval()
tokenizer_m3 = AutoTokenizer.from_pretrained(MODEL3_SCRATCH_REPO, subfolder=f"fold_{FOLDS[0]}", token=HF_TOKEN, use_fast=True)
model3 = AutoModelForSequenceClassification.from_pretrained(
MODEL3_SCRATCH_REPO,
subfolder=f"fold_{FOLDS[0]}",
token=HF_TOKEN,
torch_dtype=torch.float16,
num_labels=1
).to(device)
model3.eval()
models_loaded = True
@spaces.GPU
@torch.inference_mode()
def run_inference(context, question, opt_a, opt_b, opt_c, opt_d, opt_e):
load_all_models()
device = "cuda"
opts = {"A": opt_a, "B": opt_b, "C": opt_c, "D": opt_d, "E": opt_e}
qwen_prompt = format_qwen3_prompt(context, question, opts)
inputs_m1 = tokenizer_m1([qwen_prompt], return_tensors="pt", padding=True, truncation=True, max_length=1280).to(device)
out_m1 = model1(**inputs_m1)
logits_m1 = out_m1.logits[:, -1, qwen_option_token_ids].float().cpu().numpy()[0]
m1_probs = softmax(logits_m1)
q_repeats = [question] * 5
opt_list = [opts["A"], opts["B"], opts["C"], opts["D"], opts["E"]]
inputs_m2 = tokenizer_m2(q_repeats, opt_list, padding=True, truncation=True, max_length=512, return_tensors="pt")
inputs_m2 = {k: v.unsqueeze(0).to(device) for k, v in inputs_m2.items()}
out_m2 = model2(**inputs_m2)
m2_probs = softmax(out_m2.logits.cpu().numpy()[0])
scratch_texts = [f"Question: {question}\nOption: {opt}" for opt in opt_list]
inputs_m3 = tokenizer_m3(scratch_texts, padding=True, truncation=True, max_length=256, return_tensors="pt").to(device)
out_m3 = model3(**inputs_m3)
logits_m3 = out_m3.logits.squeeze(-1).cpu().numpy()
m3_probs = softmax(logits_m3)
m1_sharp = sharpen_probs_single(m1_probs, power=1.5)
m2_sharp = sharpen_probs_single(m2_probs, power=1.8)
m3_sharp = sharpen_probs_single(m3_probs, power=1.5)
m1_ranks = to_percentile_ranks_single(m1_probs)
m2_ranks = to_percentile_ranks_single(m2_probs)
m3_ranks = to_percentile_ranks_single(m3_probs)
deb_sorted = np.sort(m2_probs)[::-1]
margin = deb_sorted[0] - deb_sorted[1]
if margin >= 0.35:
w_deb, w_scratch, w_qwen = 0.82, 0.11, 0.07
elif margin >= 0.15:
w_deb, w_scratch, w_qwen = 0.62, 0.23, 0.15
else:
w_deb, w_scratch, w_qwen = 0.42, 0.34, 0.24
p_blend = (w_deb * m2_sharp) + (w_scratch * m3_sharp) + (w_qwen * m1_sharp)
r_blend = (w_deb * m2_ranks) + (w_scratch * m3_ranks) + (w_qwen * m1_ranks)
final_blend_scores = (0.85 * p_blend) + (0.15 * r_blend)
top3_idx = np.argsort(-final_blend_scores)[:3]
top3_labels = " ".join([IDX_TO_LABEL[i] for i in top3_idx])
def dict_format(probs):
return {IDX_TO_LABEL[i]: float(probs[i]) for i in range(5)}
return dict_format(m1_probs), dict_format(m2_probs), dict_format(m3_probs), top3_labels
with gr.Blocks(title="TriModel Optimized Ensemble", theme=gr.themes.Soft()) as demo:
gr.Markdown("TriModel Optimized MCQ Solver Ensemble")
gr.Markdown("Input a question and 5 options. Optionally, provide context for Qwen3's RAG component. The application dynamically weights the 3 models based on DeBERTa's prediction confidence margin.")
with gr.Row():
with gr.Column(scale=2):
context_in = gr.Textbox(label="Context (Optional RAG input)", lines=3, placeholder="Paste background text here...")
question_in = gr.Textbox(label="Question", lines=2, placeholder="What is the process of...")
opt_a_in = gr.Textbox(label="Option A")
opt_b_in = gr.Textbox(label="Option B")
opt_c_in = gr.Textbox(label="Option C")
opt_d_in = gr.Textbox(label="Option D")
opt_e_in = gr.Textbox(label="Option E")
submit_btn = gr.Button("Run Inference", variant="primary")
with gr.Column(scale=3):
gr.Markdown("Ensemble Final Prediction (Map@3)")
ensemble_out = gr.Textbox(label="Top 3 Predictions", text_align="center", scale=2)
gr.Markdown("Individual Model Probabilities")
with gr.Row():
m1_out = gr.Label(label="Model 1: Qwen3 RAG (4B)")
m2_out = gr.Label(label="Model 2: DeBERTa-v3 Encoder")
m3_out = gr.Label(label="Model 3: Scratch Classifier")
submit_btn.click(
fn=run_inference,
inputs=[context_in, question_in, opt_a_in, opt_b_in, opt_c_in, opt_d_in, opt_e_in],
outputs=[m1_out, m2_out, m3_out, ensemble_out]
)
if __name__ == "__main__":
demo.launch() |