sdanda99 commited on
Commit
9c0c45d
Β·
verified Β·
1 Parent(s): ab87b82

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +9 -7
  2. app.py +247 -0
  3. requirements.txt +5 -0
README.md CHANGED
@@ -1,13 +1,15 @@
1
  ---
2
- title: Dann Lora Comparison
3
- emoji: πŸ†
4
- colorFrom: pink
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.15.2
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Reading Level Classifier Comparison
3
+ emoji: πŸ“š
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.0.0
8
+ python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
+ # Reading Level Classifier Comparison
14
+
15
+ Side-by-side comparison of ELECTRA+DANN vs Phi-3.5-mini+LoRA for text difficulty classification.
app.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Any, List, Tuple, Dict
3
+
4
+ import os
5
+ import time
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from torch import Tensor
10
+ from torch.nn import Parameter, ParameterList
11
+ from huggingface_hub import hf_hub_download
12
+ from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer
13
+ from peft import PeftModel
14
+ import gradio as gr
15
+
16
+ # ── Device ────────────────────────────────────────────────────────────────────
17
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+ dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
19
+ print(f"Loading on {device} …")
20
+
21
+ # ══════════════════════════════════════════════════════════════════════════════
22
+ # MODEL 1 β€” DANN (ELECTRA-large + ScalarMix)
23
+ # ══════════════════════════════════════════════════════════════════════════════
24
+ DANN_MODEL_NAME = "google/electra-large-discriminator"
25
+ MAX_LEN = 512
26
+
27
+ DANN_CKPT_PATH = hf_hub_download(
28
+ repo_id="sdanda99/demo_models",
29
+ filename="best_model.pt",
30
+ token=os.environ.get("HF_TOKEN"),
31
+ )
32
+
33
+ label2id = {"elementary": 0, "middle": 1, "high": 2}
34
+ id2label = {v: k for k, v in label2id.items()}
35
+
36
+
37
+ class ScalarMix(nn.Module):
38
+ def __init__(self, mixture_size: int, trainable: bool = True) -> None:
39
+ super().__init__()
40
+ self.scalar_parameters = ParameterList(
41
+ [Parameter(torch.zeros(1), requires_grad=trainable) for _ in range(mixture_size)]
42
+ )
43
+ self.gamma = Parameter(torch.ones(1), requires_grad=trainable)
44
+
45
+ def forward(self, tensors: List[Tensor]) -> Tensor:
46
+ w = F.softmax(torch.cat(list(self.scalar_parameters)), dim=0)
47
+ w = torch.split(w, 1)
48
+ return self.gamma * sum(weight * t for weight, t in zip(w, tensors))
49
+
50
+
51
+ class GradientReversalFunction(torch.autograd.Function):
52
+ @staticmethod
53
+ def forward(ctx: Any, x: Tensor, lambda_: float) -> Tensor:
54
+ ctx.lambda_ = float(lambda_)
55
+ return x.view_as(x)
56
+
57
+ @staticmethod
58
+ def backward(ctx: Any, grad_output: Tensor) -> Tuple[Tensor, None]:
59
+ return -ctx.lambda_ * grad_output, None
60
+
61
+
62
+ class DifficultyClassifierHead(nn.Module):
63
+ def __init__(self, in_dim: int, num_classes: int = 3, dropout: float = 0.1) -> None:
64
+ super().__init__()
65
+ self.net = nn.Sequential(
66
+ nn.Linear(in_dim, 256), nn.ReLU(), nn.Dropout(dropout),
67
+ nn.Linear(256, num_classes),
68
+ )
69
+
70
+ def forward(self, x: Tensor) -> Tensor:
71
+ return self.net(x)
72
+
73
+
74
+ class DomainClassifierHead(nn.Module):
75
+ def __init__(self, in_dim: int, num_domains: int, dropout: float = 0.1) -> None:
76
+ super().__init__()
77
+ self.net = nn.Sequential(
78
+ nn.Linear(in_dim, 256), nn.ReLU(), nn.Dropout(dropout),
79
+ nn.Linear(256, num_domains),
80
+ )
81
+
82
+ def forward(self, x: Tensor) -> Tensor:
83
+ return self.net(x)
84
+
85
+
86
+ class ElectraScalarMixDANN(nn.Module):
87
+ def __init__(self, model_name, num_classes, num_domains, head_in_dim=None, dropout=0.2):
88
+ super().__init__()
89
+ self.encoder = AutoModel.from_pretrained(model_name)
90
+ hidden = int(self.encoder.config.hidden_size)
91
+ n_layers = int(self.encoder.config.num_hidden_layers) + 1
92
+ self.scalar_mix = ScalarMix(n_layers)
93
+ self.dropout = nn.Dropout(dropout)
94
+ in_dim = head_in_dim if head_in_dim is not None else hidden
95
+ self.difficulty_head = DifficultyClassifierHead(in_dim, num_classes, dropout)
96
+ self.domain_head = DomainClassifierHead(in_dim, num_domains, dropout)
97
+
98
+ def encode_pooled(self, input_ids, attention_mask):
99
+ out = self.encoder(input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True)
100
+ mixed = self.dropout(self.scalar_mix(list(out.hidden_states)))
101
+ mask = attention_mask.unsqueeze(-1).float()
102
+ pooled = (mixed * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9)
103
+ expected = self.difficulty_head.net[0].in_features
104
+ if pooled.shape[-1] < expected:
105
+ pad = torch.zeros(pooled.shape[0], expected - pooled.shape[-1], device=pooled.device)
106
+ pooled = torch.cat([pooled, pad], dim=-1)
107
+ return pooled
108
+
109
+ def difficulty_logits_only(self, input_ids, attention_mask):
110
+ return self.difficulty_head(self.encode_pooled(input_ids, attention_mask))
111
+
112
+
113
+ ckpt = torch.load(DANN_CKPT_PATH, map_location=device)
114
+ domain2id = ckpt.get("domain2id", {"default": 0})
115
+ head_in_dim = ckpt["state_dict"]["difficulty_head.net.0.weight"].shape[1]
116
+
117
+ dann_model = ElectraScalarMixDANN(
118
+ DANN_MODEL_NAME, num_classes=3, num_domains=len(domain2id), head_in_dim=head_in_dim
119
+ ).to(device)
120
+ dann_model.load_state_dict(ckpt["state_dict"])
121
+ dann_model.eval()
122
+
123
+ dann_tokenizer = AutoTokenizer.from_pretrained(DANN_MODEL_NAME)
124
+ print("DANN model ready.")
125
+
126
+
127
+ def predict_dann(text: str) -> Dict[str, float]:
128
+ text = str(text).strip()
129
+ if not text:
130
+ return {"elementary": 0.0, "middle": 0.0, "high": 0.0}
131
+ enc = dann_tokenizer(text, truncation=True, max_length=MAX_LEN, padding=True, return_tensors="pt")
132
+ enc = {k: v.to(device) for k, v in enc.items()}
133
+ with torch.no_grad():
134
+ logits = dann_model.difficulty_logits_only(enc["input_ids"], enc["attention_mask"])
135
+ probs = F.softmax(logits, dim=-1).squeeze(0).cpu().numpy()
136
+ return {id2label[i]: float(probs[i]) for i in range(len(probs))}
137
+
138
+
139
+ # ══════════════════════════════════════════════════════════════════════════════
140
+ # MODEL 2 β€” Pillar B+ (Phi-3.5-mini + LoRA)
141
+ # ══════════════════════════════════════════════════════════════════════════════
142
+ LORA_BASE_MODEL = "microsoft/Phi-3.5-mini-instruct"
143
+ LORA_ADAPTER_REPO = os.environ.get("ADAPTER_REPO", "sdanda99/pillar-b-plus-lora")
144
+
145
+ lora_tok = AutoTokenizer.from_pretrained(LORA_BASE_MODEL)
146
+ if lora_tok.pad_token_id is None:
147
+ lora_tok.pad_token = lora_tok.eos_token
148
+ lora_tok.padding_side = "left"
149
+
150
+ lora_base = AutoModelForCausalLM.from_pretrained(LORA_BASE_MODEL, torch_dtype=dtype, attn_implementation="sdpa").to(device)
151
+ lora_model = PeftModel.from_pretrained(lora_base, LORA_ADAPTER_REPO).to(device).eval()
152
+ print("LoRA model ready.")
153
+
154
+ INSTRUCTION = (
155
+ "Read the following text and classify it by the curriculum grade level "
156
+ "required to understand its CONCEPTS (not just its reading complexity). "
157
+ "Answer with only one letter: E for elementary school (US grades 1-5), "
158
+ "M for middle school (US grades 6-8), H for high school (US grades 9-12)."
159
+ )
160
+ LEVELS = ["elementary", "middle", "high"]
161
+ GRADE_BAND = {"elementary": "US grades 1-5", "middle": "US grades 6-8", "high": "US grades 9-12"}
162
+
163
+
164
+ def _letter_ids(tokenizer):
165
+ out = {}
166
+ for letter in "EMH":
167
+ for candidate in [f" {letter}", letter]:
168
+ ids = tokenizer(candidate, add_special_tokens=False)["input_ids"]
169
+ if len(ids) == 1:
170
+ out[letter] = ids[0]
171
+ break
172
+ else:
173
+ out[letter] = tokenizer(f" {letter}", add_special_tokens=False)["input_ids"][0]
174
+ return out
175
+
176
+ letter_ids = _letter_ids(lora_tok)
177
+
178
+
179
+ @torch.no_grad()
180
+ def predict_lora(text: str) -> Dict[str, float]:
181
+ text = (text or "").strip()
182
+ if not text:
183
+ return {"elementary": 0.0, "middle": 0.0, "high": 0.0}
184
+ prompt = f"{INSTRUCTION}\n\nText: {text}\n\nAnswer:"
185
+ enc = lora_tok(prompt, return_tensors="pt", truncation=True, max_length=1280).to(device)
186
+ out = lora_model(**enc, use_cache=False)
187
+ last = out.logits[0, -1, :]
188
+ logits = torch.stack([last[letter_ids["E"]], last[letter_ids["M"]], last[letter_ids["H"]]]).float()
189
+ probs = torch.softmax(logits, dim=-1).cpu().numpy()
190
+ return {LEVELS[i]: float(probs[i]) for i in range(3)}
191
+
192
+
193
+ # ══════════════════════════════════════════════════════════════════════════════
194
+ # Combined inference
195
+ # ══════════════════════════════════════════════════════════════════════════════
196
+ def classify_both(text: str):
197
+ if not (text or "").strip():
198
+ empty = {"elementary": 0.0, "middle": 0.0, "high": 0.0}
199
+ return empty, empty
200
+ return predict_dann(text), predict_lora(text)
201
+
202
+
203
+ # ══════════════════════════════════════════════════════════════════════════════
204
+ # Gradio UI
205
+ # ══════════════════════════════════════════════════════════════════════════════
206
+ EXAMPLES = [
207
+ ["The cat sat on the mat. It was warm and cozy."],
208
+ ["Plants use sunlight to make food through a process called photosynthesis."],
209
+ ["Climate change affects ecosystems, agriculture, and public health across the globe."],
210
+ ["The mitochondria produces ATP via cellular respiration, using glucose and oxygen."],
211
+ ["The legislature ratified the constitutional amendment after prolonged bipartisan negotiations."],
212
+ ["Quantum entanglement describes a phenomenon where particles remain correlated regardless of distance."],
213
+ ]
214
+
215
+ with gr.Blocks(title="Reading Level Classifier Comparison", theme=gr.themes.Soft()) as demo:
216
+ gr.Markdown(
217
+ """# πŸ“š Reading Level Classifier β€” Model Comparison
218
+ Compare two approaches to text difficulty classification side by side."""
219
+ )
220
+
221
+ with gr.Row():
222
+ with gr.Column(scale=2):
223
+ text_input = gr.Textbox(
224
+ lines=8,
225
+ placeholder="Paste a sentence, paragraph, or passage here...",
226
+ label="Input text",
227
+ )
228
+ with gr.Row():
229
+ submit_btn = gr.Button("Classify", variant="primary")
230
+ clear_btn = gr.ClearButton(text_input, value="Clear")
231
+
232
+ with gr.Column(scale=1):
233
+ gr.Markdown("### ELECTRA + ScalarMix + DANN")
234
+ gr.Markdown("*Trained on CNN/DailyMail Β· OneStop Β· RACE*")
235
+ dann_out = gr.Label(num_top_classes=3, label="Predicted level")
236
+
237
+ with gr.Column(scale=1):
238
+ gr.Markdown("### Phi-3.5-mini + LoRA (Pillar B+)")
239
+ gr.Markdown("*Concept-level curriculum classifier*")
240
+ lora_out = gr.Label(num_top_classes=3, label="Predicted level")
241
+
242
+ gr.Examples(examples=EXAMPLES, inputs=text_input, label="Try an example")
243
+
244
+ submit_btn.click(classify_both, inputs=text_input, outputs=[dann_out, lora_out])
245
+ text_input.submit(classify_both, inputs=text_input, outputs=[dann_out, lora_out])
246
+
247
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ transformers>=4.43.0,<5.0.0
2
+ accelerate>=0.30.0
3
+ peft>=0.19.0
4
+ safetensors>=0.4.0
5
+ torch>=2.5.0