sageofai commited on
Commit
dacaca3
·
verified ·
1 Parent(s): d144419

fix: fuzzy question matching + drop bad adapter (use SimulaMet)

Browse files
Files changed (1) hide show
  1. submission_task1.py +138 -231
submission_task1.py CHANGED
@@ -1,32 +1,7 @@
1
  #!/usr/bin/env python
2
- """
3
- ImageCLEFmed-MEDVQA-GI-2026 Task 1 Submission (aggressive constraint pipeline)
4
- ==============================================================================
5
- CSMorgan-MEDVQA / Morgan State University
6
- Peter Ojonugwa Ejiga <ojeji1@morgan.edu>
7
-
8
- Strategy
9
- --------
10
- - Base: Qwen/Qwen2.5-VL-7B-Instruct in 4-bit nf4
11
- - Adapter: SimulaMet/Qwen2.5-VL-KvasirVQA-x1-ft (organizer baseline) OR the
12
- USER_ADAPTER_REPO if it has been pushed.
13
- - Decoding: greedy (T=0, top_k=1, max_tokens=20)
14
- - Per-question constraint: for each test question, look up the top-K training
15
- answers for that exact question text, fuzzy-match model output against those,
16
- snap to the best candidate above threshold. Falls back to model output if
17
- no good match.
18
- - No synonym collapse. Multi-word references like "sigmoid colon", "pink;red",
19
- "5-10mm" are preserved through the pipeline.
20
- """
21
-
22
- import json
23
- import os
24
- import re
25
- import sys
26
- import tempfile
27
- import time
28
- import subprocess
29
- import platform
30
  from collections import Counter
31
  from difflib import SequenceMatcher
32
 
@@ -36,7 +11,6 @@ from datasets import load_dataset
36
  from evaluate import load
37
  from transformers import BitsAndBytesConfig
38
 
39
- # Memory hints for shared HF-Space GPU
40
  os.environ.setdefault("MAX_PIXELS", "640000")
41
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
42
 
@@ -51,45 +25,38 @@ gpu_name = torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu"
51
  device = "cuda" if torch.cuda.is_available() else "cpu"
52
 
53
  def get_mem():
54
- return torch.cuda.memory_allocated(device) / (1024 ** 2) \
55
- if torch.cuda.is_available() else 0
56
-
57
  initial_mem = get_mem()
58
 
59
  SUBMISSION_INFO = {
60
  "Participant_Names": "Peter Ojonugwa Ejiga",
61
- "Affiliations": "Morgan State University, Computer Vision & AI Lab",
62
  "Contact_emails": ["ojeji1@morgan.edu"],
63
  "Team_Name": "CSMorgan-MEDVQA",
64
  "Country": "USA",
65
  "Notes_to_organizers": (
66
- "Per-question constrained decoding on top of SimulaMet's "
67
- "Kvasir-VQA-x1 adapter. Model generates greedily (T=0, max 20 tokens). "
68
- "Output is fuzzy-matched against top-K training answers for the exact "
69
- "question text using SequenceMatcher; best match above threshold 0.5 "
70
- "is returned; otherwise raw model output is used."
71
  ),
72
  }
73
 
74
- HF_REPO_ID = "sageofai/Qwen25VL-MEDVQA-GI-S1-subtask1-v4"
75
  FALLBACK_ADAPTER = "SimulaMet/Qwen2.5-VL-KvasirVQA-x1-ft"
76
 
77
- # ===========================================================================
78
- # COMPANION FILE FETCHER
79
- # medvqa container only pulls submission_task1.py + requirements.txt.
80
- # Pull the answer bank from the same repo at runtime.
81
- # ===========================================================================
82
  _HERE = os.path.dirname(os.path.abspath(__file__))
83
- if _HERE not in sys.path: sys.path.insert(0, _HERE)
 
84
 
85
- QUESTION_BANK = None
 
86
  _bank_path = os.path.join(_HERE, "qbank.json")
87
  if not os.path.exists(_bank_path):
88
  try:
89
  from huggingface_hub import hf_hub_download
90
- _bank_path = hf_hub_download(
91
- repo_id=HF_REPO_ID, filename="qbank.json", repo_type="model",
92
- )
93
  print(f"[fetch] qbank.json from HF: {_bank_path}")
94
  except Exception as e:
95
  print(f"[fetch] qbank.json unavailable: {e}")
@@ -98,79 +65,36 @@ if not os.path.exists(_bank_path):
98
  if _bank_path and os.path.exists(_bank_path):
99
  with open(_bank_path) as f:
100
  QUESTION_BANK = json.load(f)
101
- print(f"Loaded question bank: {len(QUESTION_BANK)} unique training questions")
102
  else:
103
- print("WARNING: no question bank. Falling back to raw model output.")
104
- QUESTION_BANK = {}
105
-
106
- # Build flat list of ALL training answers for fallback when test question
107
- # isn't in the question bank.
108
- ALL_TRAINING_ANSWERS = []
109
- for q, answers in QUESTION_BANK.items():
110
- for ans_obj in answers:
111
- ALL_TRAINING_ANSWERS.append(ans_obj["answer"])
112
- ALL_TRAINING_ANSWERS = list(set(ALL_TRAINING_ANSWERS))
113
- print(f" flat training answers: {len(ALL_TRAINING_ANSWERS)}")
114
-
115
- # ===========================================================================
116
- # ADAPTER SELECTION
117
- # Prefer user-pushed adapter; fall back to SimulaMet's baseline if absent.
118
- # ===========================================================================
119
  def _adapter_exists(repo_id):
120
  if not repo_id: return False
121
  try:
122
  from huggingface_hub import HfApi
123
  info = HfApi().repo_info(repo_id=repo_id, repo_type="model")
124
  files = {s.rfilename for s in info.siblings}
125
- return "adapter_config.json" in files and any(
126
- f.startswith("adapter_model.") for f in files
127
- )
128
  except Exception:
129
  return False
130
 
131
  if _adapter_exists(HF_REPO_ID):
132
  ADAPTER = HF_REPO_ID
133
- print(f"[adapter] Using user-fine-tuned: {ADAPTER}")
134
  else:
135
  ADAPTER = FALLBACK_ADAPTER
136
- print(f"[adapter] User adapter not found. Falling back: {ADAPTER}")
137
 
138
- # ===========================================================================
139
- # MODEL LOAD (PtEngine)
140
- # ===========================================================================
141
- from swift.llm import PtEngine, RequestConfig, InferRequest
142
-
143
- print(f"Loading base + adapter...")
144
- model_hf = PtEngine(
145
- model_id_or_path="Qwen/Qwen2.5-VL-7B-Instruct",
146
- adapters=[ADAPTER],
147
- quantization_config=BitsAndBytesConfig(
148
- load_in_4bit=True,
149
- bnb_4bit_quant_type="nf4",
150
- bnb_4bit_use_double_quant=True,
151
- bnb_4bit_compute_dtype=torch.float16,
152
- ),
153
- attn_impl="sdpa",
154
- use_hf=True,
155
- max_length=2048,
156
- )
157
- req_cfg = RequestConfig(
158
- max_tokens=20,
159
- temperature=0.0,
160
- top_k=1,
161
- top_p=1.0,
162
- repetition_penalty=1.0,
163
- )
164
- post_load_mem = get_mem()
165
- print("Model loaded.")
166
-
167
- # ===========================================================================
168
- # PER-QUESTION CONSTRAINED DECODING
169
- # ===========================================================================
170
 
171
  def _normalize_for_match(text):
172
- """Light normalization for fuzzy comparison. Preserves hyphens and semicolons
173
- so multi-token refs like '5-10mm', 'pink;red' survive."""
174
  if not isinstance(text, str): text = str(text)
175
  t = text.lower().strip()
176
  if "\n" in t:
@@ -183,87 +107,91 @@ def _normalize_for_match(text):
183
  t = re.sub(r"\s+", " ", t).strip()
184
  return t
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  def _constrain(pred, question, threshold=0.5):
188
- """For a model prediction + question, snap to the closest training answer
189
- seen for that exact question text. Falls back to fuzzy match across all
190
- training answers if the question is novel. Returns pred unchanged if no
191
- candidate exceeds threshold."""
192
- if pred is None:
193
- pred = ""
194
- qkey = (question or "").strip().lower()
195
  pnorm = _normalize_for_match(pred)
196
 
197
- # 1. Direct lookup: exact normalized match against this question's candidates
198
- candidates = QUESTION_BANK.get(qkey, [])
199
- if candidates:
200
- # Try exact normalized match first (strongest signal)
201
- for cand in candidates[:30]:
202
- if _normalize_for_match(cand["answer"]) == pnorm:
203
- return cand["answer"]
204
 
205
- # 2. Empty prediction: fall back to top training answer for this question
206
  if not pnorm:
207
- if candidates:
208
- return candidates[0]["answer"]
209
- return pred
210
-
211
- # 3. Fuzzy match against top-K candidates for this question
212
- pool = candidates[:30] if candidates else None
213
- if pool:
214
- best_score, best_ans = 0.0, pred
215
- max_count = max((c["count"] for c in pool), default=1)
216
- for cand in pool:
217
- cn = _normalize_for_match(cand["answer"])
218
- if not cn: continue
219
- s = SequenceMatcher(None, pnorm, cn).ratio()
220
- # Bias slightly toward more common answers for this question
221
- prior = 0.05 * (cand["count"] / max_count)
222
- score = s + prior
223
- if score > best_score:
224
- best_score, best_ans = score, cand["answer"]
225
- if best_score >= threshold:
226
- return best_ans
227
-
228
- # 4. Question novel or no good match: light fuzzy across all training answers
229
- # Bucket by first word for speed.
230
- if pnorm:
231
- first = pnorm.split()[0]
232
- candidates_fb = [a for a in ALL_TRAINING_ANSWERS
233
- if _normalize_for_match(a).startswith(first[:3])]
234
- if not candidates_fb:
235
- candidates_fb = ALL_TRAINING_ANSWERS[:5000] # cap
236
- best_score, best_ans = 0.0, pred
237
- for ans in candidates_fb:
238
- an = _normalize_for_match(ans)
239
- if not an: continue
240
- s = SequenceMatcher(None, pnorm, an).ratio()
241
- if s > best_score:
242
- best_score, best_ans = s, ans
243
- if best_score >= threshold + 0.1: # tighter threshold for unrestricted
244
- return best_ans
245
-
246
- return pred
247
-
248
-
249
- # ===========================================================================
250
- # INFERENCE LOOP
251
- # ===========================================================================
252
  print("Starting inference...")
253
  _t0 = time.time()
254
- _raw_samples = []
255
 
256
  for idx, ex in enumerate(tqdm(val_dataset, desc="Validating")):
257
  question = ex["question"]
258
- image = ex["image"]
259
  if hasattr(image, "convert") and image.mode != "RGB":
260
  image = image.convert("RGB")
261
-
262
  tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
263
- tmp_path = tmp.name
264
- tmp.close()
265
  image.save(tmp_path)
266
-
267
  try:
268
  req = InferRequest(messages=[{
269
  "role": "user",
@@ -273,79 +201,58 @@ for idx, ex in enumerate(tqdm(val_dataset, desc="Validating")):
273
  ],
274
  }])
275
  resp = model_hf.infer([req], req_cfg)
276
- raw_answer = resp[0].choices[0].message.content
277
- if raw_answer is None: raw_answer = ""
278
-
279
- constrained = _constrain(raw_answer, question, threshold=0.5)
280
  finally:
281
  try: os.unlink(tmp_path)
282
  except OSError: pass
 
 
 
 
283
 
284
- predictions.append({
285
- "index": idx, "img_id": ex["img_id"],
286
- "question": question, "answer": constrained,
287
- })
288
- if len(_raw_samples) < 30:
289
- _raw_samples.append((question, raw_answer, constrained))
290
-
291
- # ===========================================================================
292
- # DIAGNOSTIC
293
- # ===========================================================================
294
  if predictions:
295
  answers = [p["answer"] for p in predictions]
296
- word_counts = [len(a.split()) for a in answers]
297
  freq = Counter(answers)
298
  print()
299
- print("=========== TASK 1 DIAGNOSTIC ===========")
300
- print(f"predictions : {len(predictions)}")
301
- print(f"empty answers : {sum(1 for a in answers if not a.strip())}")
302
- print(f"avg answer length (w) : {sum(word_counts)/len(word_counts):.2f}")
303
- print(f"answers with >1 word : {sum(1 for n in word_counts if n > 1)} ({100*sum(1 for n in word_counts if n > 1)/len(predictions):.1f}%)")
304
- print(f"answers with >=2 word : {sum(1 for n in word_counts if n >= 2)} ({100*sum(1 for n in word_counts if n >= 2)/len(predictions):.1f}%)")
305
- print()
306
- print("Top-30 most frequent predictions:")
307
- for ans, c in freq.most_common(30):
308
- print(f" {c:5d} {ans!r}")
309
- print()
310
- print("Sample of 20 (question | raw | constrained):")
311
- for q, r, c in _raw_samples[:20]:
312
- print(f" Q: {q[:50]!r:55s} raw: {r!r:50s} -> {c!r}")
313
- print("===========================================")
314
-
315
- # ===========================================================================
316
- # FINAL SCORING
317
- # ===========================================================================
318
- preds_texts = [p["answer"] for p in predictions]
319
- refs_texts = [ex["answer"] for ex in val_dataset]
320
- def _flat(r):
321
- return "; ".join(str(x) for x in r) if isinstance(r, list) else str(r)
322
- refs_flat = [_flat(r) for r in refs_texts]
323
-
324
- # Use BLEU-1 (max_order=1) as the primary BLEU. HuggingFace's default bleu
325
- # is BLEU-4, which is a geometric mean over n=1..4 and collapses to 0 for
326
- # 1-word answers. Per organizer's note (May 21), BLEU-1 is the meaningful
327
- # score for this short-answer task and won't be used as a final ranking
328
- # metric anyway. We still compute BLEU-4 separately for completeness.
329
- bleu_s = bleu.compute(predictions=preds_texts, references=[[r] for r in refs_flat], max_order=1)
330
- bleu4_s = bleu.compute(predictions=preds_texts, references=[[r] for r in refs_flat])
331
- rouge_s = rouge.compute(predictions=preds_texts, references=refs_flat)
332
- meteor_s = meteor.compute(predictions=preds_texts, references=refs_flat)
333
-
334
  scores = {
335
- "bleu": round(bleu_s["bleu"], 4), # BLEU-1 (unigram) - meaningful for short answers
336
- "bleu4": round(bleu4_s["bleu"], 4), # BLEU-4 (HF default) - usually 0 for this task
337
- "rouge1": round(rouge_s["rouge1"], 4),
338
- "rouge2": round(rouge_s["rouge2"], 4),
339
- "rougeL": round(rouge_s["rougeL"], 4),
340
- "meteor": round(meteor_s["meteor"], 4),
341
  }
342
- print(f"\u2728Public scores: {scores}")
343
 
344
  with open("predictions_1.json", "w") as f:
345
  json.dump(predictions, f, indent=2)
346
 
347
  elapsed = time.time() - _t0
348
- print(f"Time: {elapsed:.1f}s | Mem: {get_mem():.2f}MB | "
349
- f"Model Load Mem: {post_load_mem:.2f}MB | GPU: {gpu_name}")
350
- print("Generation complete. Results saved to 'predictions_1.json'.")
351
  print(f"Run:: medvqa validate_and_submit --competition=gi-2026 --task=1 --repo_id={HF_REPO_ID}")
 
1
  #!/usr/bin/env python
2
+ """CSMorgan Task 1 v4 — SimulaMet adapter + fuzzy-question constrained decoding."""
3
+
4
+ import json, os, re, sys, tempfile, time, subprocess, platform
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from collections import Counter
6
  from difflib import SequenceMatcher
7
 
 
11
  from evaluate import load
12
  from transformers import BitsAndBytesConfig
13
 
 
14
  os.environ.setdefault("MAX_PIXELS", "640000")
15
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
16
 
 
25
  device = "cuda" if torch.cuda.is_available() else "cpu"
26
 
27
  def get_mem():
28
+ return torch.cuda.memory_allocated(device) / (1024**2) if torch.cuda.is_available() else 0
 
 
29
  initial_mem = get_mem()
30
 
31
  SUBMISSION_INFO = {
32
  "Participant_Names": "Peter Ojonugwa Ejiga",
33
+ "Affiliations": "Morgan State University",
34
  "Contact_emails": ["ojeji1@morgan.edu"],
35
  "Team_Name": "CSMorgan-MEDVQA",
36
  "Country": "USA",
37
  "Notes_to_organizers": (
38
+ "SimulaMet/Qwen2.5-VL-KvasirVQA-x1-ft adapter (no extra fine-tuning). "
39
+ "Greedy decoding, max 20 tokens. Per-question constrained decoding: "
40
+ "test question is fuzzy-matched against training questions (substring + "
41
+ "SequenceMatcher) to find the right answer-set, then model output is "
42
+ "fuzzy-matched against the top-30 training answers for that question."
43
  ),
44
  }
45
 
46
+ HF_REPO_ID = "sageofai/Qwen25VL-MEDVQA-GI-S1-subtask1-v4"
47
  FALLBACK_ADAPTER = "SimulaMet/Qwen2.5-VL-KvasirVQA-x1-ft"
48
 
 
 
 
 
 
49
  _HERE = os.path.dirname(os.path.abspath(__file__))
50
+ if _HERE not in sys.path:
51
+ sys.path.insert(0, _HERE)
52
 
53
+ # -------- fetch qbank.json from the repo at runtime --------
54
+ QUESTION_BANK = {}
55
  _bank_path = os.path.join(_HERE, "qbank.json")
56
  if not os.path.exists(_bank_path):
57
  try:
58
  from huggingface_hub import hf_hub_download
59
+ _bank_path = hf_hub_download(repo_id=HF_REPO_ID, filename="qbank.json", repo_type="model")
 
 
60
  print(f"[fetch] qbank.json from HF: {_bank_path}")
61
  except Exception as e:
62
  print(f"[fetch] qbank.json unavailable: {e}")
 
65
  if _bank_path and os.path.exists(_bank_path):
66
  with open(_bank_path) as f:
67
  QUESTION_BANK = json.load(f)
68
+ print(f"Loaded question bank: {len(QUESTION_BANK)} training questions")
69
  else:
70
+ print("WARNING: no qbank.json constraint disabled, raw model output only")
71
+
72
+ # -------- adapter selection (use SimulaMet's directly) --------
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _adapter_exists(repo_id):
74
  if not repo_id: return False
75
  try:
76
  from huggingface_hub import HfApi
77
  info = HfApi().repo_info(repo_id=repo_id, repo_type="model")
78
  files = {s.rfilename for s in info.siblings}
79
+ return "adapter_config.json" in files and any(f.startswith("adapter_model.") for f in files)
 
 
80
  except Exception:
81
  return False
82
 
83
  if _adapter_exists(HF_REPO_ID):
84
  ADAPTER = HF_REPO_ID
85
+ print(f"[adapter] using user-fine-tuned: {ADAPTER}")
86
  else:
87
  ADAPTER = FALLBACK_ADAPTER
88
+ print(f"[adapter] no user adapter falling back to: {ADAPTER}")
89
 
90
+ # -------- normalization helpers --------
91
+ def _normalize_question(q):
92
+ q = (q or "").lower().strip()
93
+ q = re.sub(r"[^\w\s]", " ", q)
94
+ q = re.sub(r"\s+", " ", q).strip()
95
+ return q
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  def _normalize_for_match(text):
 
 
98
  if not isinstance(text, str): text = str(text)
99
  t = text.lower().strip()
100
  if "\n" in t:
 
107
  t = re.sub(r"\s+", " ", t).strip()
108
  return t
109
 
110
+ # Pre-build normalized index of training questions for fast fuzzy lookup
111
+ QBANK_NORM_KEYS = {_normalize_question(k): k for k in QUESTION_BANK.keys()}
112
+ print(f" pre-indexed {len(QBANK_NORM_KEYS)} normalized question keys")
113
+
114
+ def _find_question_in_bank(question, threshold=0.7):
115
+ """Locate the training question whose answer pool we should constrain to."""
116
+ qn = _normalize_question(question)
117
+ if not qn:
118
+ return None
119
+ # 1. Direct normalized hit
120
+ if qn in QBANK_NORM_KEYS:
121
+ return QBANK_NORM_KEYS[qn]
122
+ # 2. Substring containment (test is in training, or training is in test)
123
+ for nk, orig in QBANK_NORM_KEYS.items():
124
+ if qn in nk or nk in qn:
125
+ return orig
126
+ # 3. Fuzzy match
127
+ best_s, best_k = 0.0, None
128
+ for nk, orig in QBANK_NORM_KEYS.items():
129
+ s = SequenceMatcher(None, qn, nk).ratio()
130
+ if s > best_s:
131
+ best_s, best_k = s, orig
132
+ return best_k if best_s >= threshold else None
133
 
134
  def _constrain(pred, question, threshold=0.5):
135
+ if pred is None: pred = ""
 
 
 
 
 
 
136
  pnorm = _normalize_for_match(pred)
137
 
138
+ matched_q = _find_question_in_bank(question)
139
+ candidates = QUESTION_BANK.get(matched_q, []) if matched_q else []
140
+
141
+ if not candidates:
142
+ return pred # no matching training question — give up
 
 
143
 
 
144
  if not pnorm:
145
+ return candidates[0]["answer"]
146
+
147
+ # Direct normalized hit in candidates
148
+ for c in candidates[:30]:
149
+ if _normalize_for_match(c["answer"]) == pnorm:
150
+ return c["answer"]
151
+
152
+ # Fuzzy match weighted by frequency
153
+ best_s, best_a = 0.0, pred
154
+ max_cnt = max((c["count"] for c in candidates[:30]), default=1)
155
+ for c in candidates[:30]:
156
+ cn = _normalize_for_match(c["answer"])
157
+ if not cn: continue
158
+ s = SequenceMatcher(None, pnorm, cn).ratio() + 0.05 * (c["count"]/max_cnt)
159
+ if s > best_s:
160
+ best_s, best_a = s, c["answer"]
161
+
162
+ # If no candidate is even moderately close, default to most-common
163
+ return best_a if best_s >= threshold else candidates[0]["answer"]
164
+
165
+ # -------- model load --------
166
+ from swift.llm import PtEngine, RequestConfig, InferRequest
167
+
168
+ print(f"Loading model + adapter...")
169
+ model_hf = PtEngine(
170
+ model_id_or_path="Qwen/Qwen2.5-VL-7B-Instruct",
171
+ adapters=[ADAPTER],
172
+ quantization_config=BitsAndBytesConfig(
173
+ load_in_4bit=True, bnb_4bit_quant_type="nf4",
174
+ bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.float16,
175
+ ),
176
+ attn_impl="sdpa", use_hf=True, max_length=2048,
177
+ )
178
+ req_cfg = RequestConfig(max_tokens=20, temperature=0.0, top_k=1, top_p=1.0, repetition_penalty=1.0)
179
+ post_load_mem = get_mem()
180
+ print("Model loaded.")
181
+
182
+ # -------- inference loop --------
 
 
 
 
 
 
 
183
  print("Starting inference...")
184
  _t0 = time.time()
185
+ _samples = []
186
 
187
  for idx, ex in enumerate(tqdm(val_dataset, desc="Validating")):
188
  question = ex["question"]
189
+ image = ex["image"]
190
  if hasattr(image, "convert") and image.mode != "RGB":
191
  image = image.convert("RGB")
 
192
  tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
193
+ tmp_path = tmp.name; tmp.close()
 
194
  image.save(tmp_path)
 
195
  try:
196
  req = InferRequest(messages=[{
197
  "role": "user",
 
201
  ],
202
  }])
203
  resp = model_hf.infer([req], req_cfg)
204
+ raw = resp[0].choices[0].message.content or ""
205
+ final = _constrain(raw, question, threshold=0.5)
 
 
206
  finally:
207
  try: os.unlink(tmp_path)
208
  except OSError: pass
209
+ predictions.append({"index": idx, "img_id": ex["img_id"],
210
+ "question": question, "answer": final})
211
+ if len(_samples) < 25:
212
+ _samples.append((question, raw, final))
213
 
214
+ # -------- diagnostic --------
 
 
 
 
 
 
 
 
 
215
  if predictions:
216
  answers = [p["answer"] for p in predictions]
217
+ wc = [len(a.split()) for a in answers]
218
  freq = Counter(answers)
219
  print()
220
+ print("=========== DIAGNOSTIC ===========")
221
+ print(f"predictions : {len(predictions)}")
222
+ print(f"empty : {sum(1 for a in answers if not a.strip())}")
223
+ print(f"avg length : {sum(wc)/len(wc):.2f}")
224
+ print(f"top-20:")
225
+ for a, c in freq.most_common(20):
226
+ print(f" {c:5d} {a!r}")
227
+ print("Sample (Q | raw | constrained):")
228
+ for q, r, c in _samples[:15]:
229
+ print(f" Q: {q[:50]!r:55s} raw={r!r:30s} -> {c!r}")
230
+ print("==================================")
231
+
232
+ # -------- scoring --------
233
+ preds_t = [p["answer"] for p in predictions]
234
+ refs_t = [ex["answer"] for ex in val_dataset]
235
+ def _flat(r): return "; ".join(str(x) for x in r) if isinstance(r, list) else str(r)
236
+ refs_f = [_flat(r) for r in refs_t]
237
+
238
+ bleu1 = bleu.compute(predictions=preds_t, references=[[r] for r in refs_f], max_order=1)
239
+ bleu4 = bleu.compute(predictions=preds_t, references=[[r] for r in refs_f])
240
+ rg = rouge.compute(predictions=preds_t, references=refs_f)
241
+ mt = meteor.compute(predictions=preds_t, references=refs_f)
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  scores = {
243
+ "bleu": round(bleu1["bleu"], 4),
244
+ "bleu4": round(bleu4["bleu"], 4),
245
+ "rouge1": round(rg["rouge1"], 4),
246
+ "rouge2": round(rg["rouge2"], 4),
247
+ "rougeL": round(rg["rougeL"], 4),
248
+ "meteor": round(mt["meteor"], 4),
249
  }
250
+ print(f"\u2728Public scores: {scores}")
251
 
252
  with open("predictions_1.json", "w") as f:
253
  json.dump(predictions, f, indent=2)
254
 
255
  elapsed = time.time() - _t0
256
+ print(f"Time: {elapsed:.1f}s | Mem: {get_mem():.2f}MB | GPU: {gpu_name}")
257
+ print("Done. Results saved to 'predictions_1.json'.")
 
258
  print(f"Run:: medvqa validate_and_submit --competition=gi-2026 --task=1 --repo_id={HF_REPO_ID}")