divaspoudel commited on
Commit
a0caa2f
·
verified ·
1 Parent(s): 002913d

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +229 -1184
script.py CHANGED
@@ -1,11 +1,13 @@
1
-
2
-
3
  import os
4
  os.environ["HF_HUB_OFFLINE"] = "1"
5
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
6
 
7
- # --- make sure quantization lib is present (PyPI is reachable at run time) ---
8
- import subprocess, sys
 
 
 
 
9
 
10
  def _run_pkg_cmd(args):
11
  for cmd in (["uv", "pip", *args], [sys.executable, "-m", "pip", *args], ["pip", *args]):
@@ -16,13 +18,14 @@ def _run_pkg_cmd(args):
16
  continue
17
  return False
18
 
 
19
  try:
20
- import bitsandbytes # noqa: F401
21
  except ImportError:
22
  _run_pkg_cmd(["install", "-q", "bitsandbytes"])
23
 
24
  try:
25
- import torchvision # noqa: F401
26
  _run_pkg_cmd(["uninstall", "-y", "-q", "torchvision"])
27
  except ImportError:
28
  pass
@@ -30,418 +33,72 @@ except ImportError:
30
  import re
31
  import json
32
  import time
33
- import math
34
- import os
35
- import sys
36
  import pandas as pd
37
  import torch
38
- from collections import Counter
39
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
40
 
41
- # Safety configuration
42
- DISABLE_RL = os.environ.get("IOL_SAFE_MODE", "").lower() == "true"
43
- if DISABLE_RL:
44
- print("SAFE MODE: RL and HP tuning disabled", flush=True)
45
 
 
 
 
 
46
  MODEL_ID = "."
47
- TIME_LIMIT = 30 * 60 # hard competition limit, seconds
48
- SAFETY_BUFFER = 90 # stop issuing new generations this many seconds before the limit
49
- MAX_NEW_TOKENS = 1800 # increased for scratchpad + rules
50
- NUM_PATHS = 3 # N=3 for test-time scaling
51
- TEMPERATURE = 0.3 # T=0.3 for diverse but coherent paths
 
52
 
53
  START = time.time()
54
 
55
  def time_left():
56
  return TIME_LIMIT - (time.time() - START)
57
 
58
- print("Loading tokenizer/model...", flush=True)
59
- tok = AutoTokenizer.from_pretrained(MODEL_ID)
60
-
61
- bnb_config = BitsAndBytesConfig(
62
- load_in_4bit=True,
63
- bnb_4bit_quant_type="nf4",
64
- bnb_4bit_compute_dtype=torch.float16,
65
- bnb_4bit_use_double_quant=True,
66
- )
67
-
68
- # Load model with OOM protection
69
- try:
70
- model = AutoModelForCausalLM.from_pretrained(
71
- MODEL_ID,
72
- quantization_config=bnb_config,
73
- device_map="auto",
74
- dtype=torch.float16,
75
- ).eval()
76
- print("Model loaded successfully", flush=True)
77
- except torch.cuda.OutOfMemoryError as e:
78
- print(f"FATAL: GPU OOM during model load: {e}", flush=True)
79
- # Try CPU fallback
80
  try:
81
- print("Attempting CPU loading...", flush=True)
82
  model = AutoModelForCausalLM.from_pretrained(
83
  MODEL_ID,
84
- device_map="cpu",
85
- torch_dtype=torch.float32,
 
 
86
  ).eval()
87
- except Exception as e2:
88
- print(f"FATAL: Could not load model even on CPU: {e2}", flush=True)
89
- # Create error submission
90
- with open("submission.csv", "w") as f:
91
- f.write("id,prediction,explanation\n")
92
- sys.exit(1)
93
- print(f"Model loaded in {time.time() - START:.1f}s", flush=True)
94
-
95
- # Robust data loading with error handling
96
- try:
97
- if not os.path.exists("/tmp/data/test.csv"):
98
- print("ERROR: test.csv not found at /tmp/data/test.csv", flush=True)
99
- # Create empty submission as fallback
100
- with open("submission.csv", "w") as f:
101
- f.write("id,prediction,explanation\n")
102
- sys.exit(1)
103
-
104
- df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
105
-
106
- if len(df) == 0:
107
- print("WARNING: test.csv is empty", flush=True)
108
- # Still create valid submission
109
- pd.DataFrame(columns=["id", "prediction", "explanation"]).to_csv("submission.csv", index=False)
110
- sys.exit(0)
111
-
112
- print(f"Loaded {len(df)} test items", flush=True)
113
-
114
- except Exception as e:
115
- print(f"FATAL: Could not load test.csv: {e}", flush=True)
116
- # Create error submission
117
- with open("submission.csv", "w") as f:
118
- f.write("id,prediction,explanation\n")
119
- sys.exit(1)
120
-
121
- ITEM_RE = re.compile(r"(?m)^\s*(\d+)\.\s")
122
-
123
- # =============================================================================
124
- # 1. DYNAMIC TASK ROUTER
125
- # Analyzes: task_type, eval_type, item count (K)
126
- # =============================================================================
127
-
128
- TASK_PROFILES = {
129
- "translation": {
130
- "eval_metric": "chrF",
131
- "format_hint": "Translate to the target language preserving diacritics and orthography.",
132
- "grammar_focus": "morphophonology, tense, case, agreement",
133
- },
134
- "match_letters": {
135
- "eval_metric": "exact",
136
- "format_hint": "Answer with only the option letter (e.g. A, B, C) for each item.",
137
- "grammar_focus": "phonological rules, allophony, orthographic patterns",
138
- },
139
- "fill_blanks": {
140
- "eval_metric": "chrF",
141
- "format_hint": "Fill the blank with the correct inflected/corrected form.",
142
- "grammar_focus": "inflection, derivation, agreement, sandhi",
143
- },
144
- "text_to_num": {
145
- "eval_metric": "exact",
146
- "format_hint": "Answer with the value written in digits (e.g. 285).",
147
- "grammar_focus": "numeral systems, base systems, place value",
148
- },
149
- "num_to_text": {
150
- "eval_metric": "chrF",
151
- "format_hint": "Answer with the number written out in words in the task language.",
152
- "grammar_focus": "numeral morphology, ordinals, cardinals",
153
- },
154
- }
155
 
156
  def count_items(query: str) -> int:
157
  """Extract K = number of items in the query."""
158
- nums = ITEM_RE.findall(query)
159
  if nums:
160
  return len(set(nums))
161
- # fallback: count non-empty lines
162
  return max(1, len([l for l in query.splitlines() if l.strip()]))
163
 
164
- def get_task_profile(task_type: str) -> dict:
165
- """Route to task-specific configuration."""
166
- return TASK_PROFILES.get(task_type, TASK_PROFILES["translation"])
167
-
168
- # =============================================================================
169
- # 2. FEW-SHOT EXAMPLES: Filled-out Grammar Scratchpads
170
- # Shows the model HOW to reason, not just WHAT to do
171
- # =============================================================================
172
-
173
- FEW_SHOT_EXAMPLES = """
174
-
175
- === EXAMPLE 1: Simple Translation ===
176
-
177
- DATA:
178
- 1. mo ka | I go
179
- 2. ti ka | you go
180
- 3. mo tuʔ | I sleep
181
- 4. ti tuʔ | you sleep
182
-
183
- QUERY: Translate into English:
184
- 1. ti tuʔ
185
- 2. mo ka
186
-
187
- 【GRAMMAR SCRATCHPAD】
188
- 1. DATA INVENTORY:
189
- - mo: "I" (appears in sentences 1, 3)
190
- - ti: "you" (appears in sentences 2, 4)
191
- - ka: "go" (appears in sentences 1, 2)
192
- - tuʔ: "sleep" (appears in sentences 3, 4)
193
-
194
- 2. MORPHEME SEGMENTATION:
195
- - All words are monomorphemic (no affixes detected)
196
- - Structure: [PRONOUN] + [VERB]
197
-
198
- 3. PARADIGM MAPPING:
199
- - Pronouns: mo = 1st person singular, ti = 2nd person singular
200
- - Verbs: ka = "go", tuʔ = "sleep" (no inflection seen)
201
-
202
- 4. CONSTRAINT IDENTIFICATION:
203
- - No agreement marking on verbs
204
- - Word order: PRONOUN + VERB
205
-
206
- 5. VERIFICATION:
207
- - Sentence 1: mo ka = "I go" - matches data
208
- - Sentence 2: ti ka = "you go" - matches data
209
- - Pattern is consistent
210
-
211
- 6. APPLICATION:
212
- - ti tuʔ = "you + sleep" = "you sleep"
213
- - mo ka = "I + go" = "I go"
214
-
215
- RULES:
216
- - RULE 1: mo -> "I" / _ (1st person singular pronoun)
217
- - RULE 2: ti -> "you" / _ (2nd person singular pronoun)
218
- - RULE 3: ka -> "go" / _ (uninflected verb)
219
- - RULE 4: tuʔ -> "sleep" / _ (uninflected verb)
220
- - RULE 5: [PRONOUN] + [VERB] -> [PRONOUN] + [VERB]
221
-
222
- EXPLANATION: The language uses independent pronouns and invariant verbs. Word order is PRONOUN-VERB.
223
-
224
- FINAL: ["you sleep", "I go"]
225
-
226
- === EXAMPLE 2: Morphological Rule Detection ===
227
-
228
- DATA:
229
- 1. book | one book
230
- 2. books | two books
231
- 3. box | one box
232
- 4. boxes | two boxes
233
-
234
- QUERY: Complete the translation:
235
- 1. cat | one cat
236
- 2. cats | ___
237
-
238
- 【GRAMMAR SCRATCHPAD】
239
- 1. DATA INVENTORY:
240
- - "book" appears as "book" (one) and "books" (two)
241
- - "box" appears as "box" (one) and "boxes" (two)
242
- - "one" marks singular, "two" marks plural
243
-
244
- 2. MORPHEME SEGMENTATION:
245
- - books = book + -s
246
- - boxes = box + -es
247
- - Singular forms lack suffix
248
-
249
- 3. PARADIGM MAPPING:
250
- - book ~ books: add -s
251
- - box ~ boxes: add -es
252
- - Pattern: plural = stem + suffix
253
-
254
- 4. CONSTRAINT IDENTIFICATION:
255
- - ALTERNATION: -s vs -es depends on stem-final sound
256
- - "book" ends in /k/ (non-sibilant) -> -s
257
- - "box" ends in /ks/ (sibilant) -> -es
258
- - RULE: Use -es after sibilants (/s/, /z/, /ʃ/, /tʃ/, /ks/, etc.)
259
-
260
- 5. VERIFICATION:
261
- - "book" + "s" = "books" - matches data
262
- - "box" + "es" = "boxes" - matches data
263
- - Rule holds
264
-
265
- 6. APPLICATION:
266
- - "cat" ends in /t/ (non-sibilant) -> plural = "cats"
267
- - "two" + "cats" = "two cats"
268
-
269
- RULES:
270
- - RULE 1: NOUN_sg -> NOUN_pl / [two] (context: plural number)
271
- - RULE 2: X -> X-s / _# (default plural: add -s)
272
- - RULE 3: X[sibilant] -> X-es / _# (sibilant plural: add -es)
273
- - RULE 4: Sibilant set: {s, z, ʃ, ʒ, tʃ, dʒ, ks, gz}
274
- - RULE 5: [two] + [NOUN_pl] -> "two" + [NOUN_pl]
275
-
276
- EXPLANATION: Plural formation uses -s by default. After sibilant sounds (s, z, sh, ch, x, etc.), use -es instead. "cat" ends in /t/, so regular -s plural applies.
277
-
278
- FINAL: ["one cat", "two cats"]
279
-
280
- """
281
-
282
- GRAMMAR_SCRATCHPAD_TEMPLATE = """Before answering, you MUST work through this deduction process:
283
-
284
- 【GRAMMAR SCRATCHPAD】
285
- 1. DATA INVENTORY: List forms and glosses explicitly shown
286
- 2. MORPHEME SEGMENTATION: Break forms into morphemes, mark boundaries with -
287
- 3. PARADIGM MAPPING: Sketch inflectional categories (person, number, tense, case)
288
- 4. CONSTRAINT IDENTIFICATION: Phonological rules (sandhi, harmony, mutation), morphological rules (affixation, stem change)
289
- 5. VERIFICATION: Test hypothesis against exceptions
290
- 6. APPLICATION: Apply rules to query items step by step
291
-
292
- Then provide:
293
- RULES: List explicit rules in format "RULE N: [input] -> [output] / [condition]"
294
- EXPLANATION: Summary of grammatical analysis
295
- FINAL: JSON array with answers
296
- """
297
-
298
- BASE_SYSTEM_PROMPT = (
299
- "You are an expert linguist competing in the International Linguistics Olympiad. "
300
- "Analyze linguistic data, deduce grammar rules, and solve problems.\n\n"
301
- + FEW_SHOT_EXAMPLES + "\n\n"
302
- + GRAMMAR_SCRATCHPAD_TEMPLATE + "\n\n"
303
- "LINGUISTIC PHENOMENA TO CONSIDER:\n"
304
- "- Phonology: assimilation, dissimilation, deletion, epenthesis, metathesis, lenition, fortition\n"
305
- "- Morphology: prefixation, suffixation, infixation, circumfixation, reduplication, ablaut, suppletion\n"
306
- "- Phonotactics: consonant clusters, vowel constraints, syllable structure\n"
307
- "- Sandhi: external (word-boundary), internal (morpheme-boundary)\n"
308
- "- Syntax: word order (SOV, SVO, VSO), agreement patterns, case marking\n\n"
309
- "Respond with these sections:\n"
310
- "1. 【GRAMMAR SCRATCHPAD】 (complete all 6 steps with specific analysis)\n"
311
- "2. RULES: Explicit rewrite rules like:\n"
312
- " - RULE 1: [morpheme A] + [morpheme B] -> [result] / [environment]\n"
313
- " - RULE 2: X -> Y / _(condition) (phonological rule)\n"
314
- " - RULE 3: [CATEGORY] -> [translation/meaning]\n"
315
- "3. EXPLANATION: Clear summary of reasoning and rules\n"
316
- "4. FINAL: JSON array [\"answer1\", \"answer2\", ...] with exactly one string per query item"
317
- )
318
-
319
- def build_router_aware_prompt(context: str, query: str, task_type: str, eval_type: str, k: int) -> str:
320
- """Build a prompt enriched with task routing information."""
321
- profile = get_task_profile(task_type)
322
-
323
- header = f"""【TASK PROFILE】
324
- Task Type: {task_type}
325
- Evaluation: {eval_type} (using {profile['eval_metric']})
326
- Number of Items: {k}
327
-
328
- Focus: {profile['grammar_focus']}
329
- """
330
-
331
- hint = profile["format_hint"]
332
- return f"{header}\n---\nDATA:\n{context.strip()}\n\nQUERY ({k} items):\n{query.strip()}\n\nFormat: {hint}"
333
-
334
- # =============================================================================
335
- # 3. TEST-TIME SCALING (Self-Consistency Engine)
336
- # =============================================================================
337
-
338
- def generate_n_paths(prompt: str, n: int = NUM_PATHS, temperature: float = TEMPERATURE) -> list:
339
- """Generate N diverse reasoning paths using sampling."""
340
- messages = [
341
- {"role": "system", "content": BASE_SYSTEM_PROMPT},
342
- {"role": "user", "content": prompt},
343
- ]
344
- model_inputs = tok.apply_chat_template(
345
- messages, add_generation_prompt=True, return_tensors="pt"
346
- )
347
- ids = model_inputs['input_ids'].to(model.device)
348
-
349
- paths = []
350
- for path_idx in range(n):
351
- try:
352
- with torch.no_grad():
353
- out = model.generate(
354
- ids,
355
- max_new_tokens=MAX_NEW_TOKENS,
356
- do_sample=True,
357
- temperature=temperature,
358
- top_p=0.9,
359
- pad_token_id=tok.eos_token_id,
360
- )
361
- text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
362
- paths.append(text)
363
- print(f" Path {path_idx+1}/{n} generated", flush=True)
364
- except Exception as e:
365
- print(f" Path {path_idx+1} failed: {e}", flush=True)
366
- paths.append("")
367
- return paths
368
-
369
- def chrF_score(candidate: str, reference: str) -> float:
370
- """Compute character n-gram F-score (simplified chrF)."""
371
- if not candidate or not reference:
372
- return 0.0
373
-
374
- def get_ngrams(s, n):
375
- s = s.lower()
376
- return [s[i:i+n] for i in range(len(s) - n + 1)]
377
-
378
- total_f = 0.0
379
- for n in range(2, 7):
380
- cand_ngrams = Counter(get_ngrams(candidate, n))
381
- ref_ngrams = Counter(get_ngrams(reference, n))
382
- overlapping = sum((cand_ngrams & ref_ngrams).values())
383
- precision = overlapping / max(sum(cand_ngrams.values()), 1)
384
- recall = overlapping / max(sum(ref_ngrams.values()), 1)
385
- if precision + recall > 0:
386
- f = 2 * precision * recall / (precision + recall)
387
- total_f += f
388
- return total_f / 6.0
389
-
390
- def consensus_vote(path_answers: list, eval_metric: str) -> tuple:
391
- """
392
- Aggregate N paths using consensus voting.
393
- Returns (consensus_answer, confidence, explanation)
394
- """
395
- if not path_answers or len(path_answers) == 0:
396
- return [], 0.0, "No paths generated"
397
-
398
- path_answers = [ans for ans in path_answers if ans]
399
- if not path_answers:
400
- return [], 0.0, "All paths failed"
401
-
402
- k = len(path_answers[0])
403
- consensus = []
404
- confidences = []
405
-
406
- for item_idx in range(k):
407
- item_answers = []
408
- for path in path_answers:
409
- if item_idx < len(path):
410
- item_answers.append(path[item_idx])
411
-
412
- if not item_answers:
413
- consensus.append("")
414
- confidences.append(0.0)
415
- continue
416
-
417
- if eval_metric == "exact":
418
- answer_counts = Counter(item_answers)
419
- best_answer, count = answer_counts.most_common(1)[0]
420
- confidence = count / len(item_answers)
421
- consensus.append(best_answer)
422
- confidences.append(confidence)
423
- else:
424
- best_answer = item_answers[0]
425
- best_score = 0.0
426
- for candidate in item_answers:
427
- score = sum(chrF_score(candidate, other) for other in item_answers) / len(item_answers)
428
- if score > best_score:
429
- best_score = score
430
- best_answer = candidate
431
- agreeing = sum(1 for ans in item_answers if chrF_score(best_answer, ans) > 0.8)
432
- confidence = agreeing / len(item_answers)
433
- consensus.append(best_answer)
434
- confidences.append(confidence)
435
-
436
- avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
437
- explanation_parts = [
438
- f"Generated {len(path_answers)} paths with T={TEMPERATURE}",
439
- f"Consensus ({eval_metric}) confidence: {avg_confidence:.0%}",
440
- ]
441
- return consensus, avg_confidence, " | ".join(explanation_parts)
442
 
443
- def extract_answers_from_text(text: str, expected_k: int) -> list:
444
  """Extract the JSON answer array from model output."""
 
445
  # Try FINAL: block first
446
  m = re.search(r"FINAL:\s*(\[.*?\])", text, re.DOTALL | re.IGNORECASE)
447
  if m:
@@ -449,632 +106,26 @@ def extract_answers_from_text(text: str, expected_k: int) -> list:
449
  parsed = json.loads(m.group(1))
450
  if isinstance(parsed, list):
451
  return [str(x) for x in parsed]
452
- except Exception:
453
  pass
454
-
455
  # Look for any JSON array
456
  for m2 in re.finditer(r"\[.*?\]", text, re.DOTALL):
457
  try:
458
  parsed = json.loads(m2.group(0))
459
- if isinstance(parsed, list) and parsed:
460
  return [str(x) for x in parsed]
461
- except Exception:
462
  continue
463
-
464
  # Fallback: extract lines after FINAL:
465
- tail = text.split("FINAL:")[-1]
466
- lines = [re.sub(r"^\s*\d+[\.\)\]]\s*", "", l).strip(' \t\"\'')
467
- for l in tail.splitlines() if l.strip()]
468
- lines = [l for l in lines if l]
469
- return lines
470
-
471
- def extract_rules(text: str) -> list:
472
- """Extract explicit rules from RULES: section."""
473
- rules = []
474
- # Look for RULES: section
475
- rules_match = re.search(r"RULES:(.+?)(?=EXPLANATION:|FINAL:|$)", text, re.DOTALL | re.IGNORECASE)
476
- if rules_match:
477
- rules_text = rules_match.group(1)
478
- # Extract individual rule lines starting with - or RULE
479
- for line in rules_text.split('\n'):
480
- line = line.strip()
481
- if line and (line.startswith('-') or line.startswith('RULE') or
482
- (len(line) > 10 and '->' in line)):
483
- # Clean up the rule
484
- rule = re.sub(r'^[-•*]\s*', '', line).strip()
485
- if rule and len(rule) > 5:
486
- rules.append(rule)
487
- return rules
488
-
489
- def extract_explanation(text: str) -> str:
490
- """Extract the explanation portion from output."""
491
- m = re.search(r"EXPLANATION:\s*(.+?)(?:RULES:|FINAL:|$)", text, re.DOTALL | re.IGNORECASE)
492
- if m:
493
- return m.group(1).strip()[:800]
494
- return ""
495
-
496
- # =============================================================================
497
- # =============================================================================
498
- # HYPERPARAMETER TUNING ENGINE
499
- # Optimizes: NUM_PATHS, TEMPERATURE, MAX_NEW_TOKENS, TOP_P
500
- # Uses small validation set to find best configuration
501
- # =============================================================================
502
-
503
- HYPERPARAM_SPACE = {
504
- "num_paths": [2, 3, 4, 5], # N: number of reasoning paths
505
- "temperature": [0.1, 0.2, 0.3, 0.4, 0.5], # T: sampling diversity
506
- "top_p": [0.85, 0.9, 0.95, 0.99], # Nucleus sampling
507
- "max_tokens_factor": [1.0, 1.2, 1.5], # Multiplier for base tokens
508
- }
509
-
510
- # Validation set for hyperparameter tuning (subset of typical IOL problems)
511
- VALIDATION_EXAMPLES = [
512
- {
513
- "id": "val-001",
514
- "task_type": "translation",
515
- "context": "1. áaka | I see\n2. tíika | you see\n3. áatʃi | I walk\n4. tíitʃi | you walk",
516
- "query": "Translate:\n1. áatʃi\n2. tíika",
517
- "expected": ["I walk", "you see"],
518
- "eval_metric": "chrF",
519
- },
520
- {
521
- "id": "val-002",
522
- "task_type": "match_letters",
523
- "context": "1. atu A. water\n2. keno B. fire\n3. suna C. sun\n4. mizu D. east\n5. umi E. sea",
524
- "query": "Match:\n1. atu\n2. keno\n3. suna",
525
- "expected": ["A", "B", "C"],
526
- "eval_metric": "exact",
527
- },
528
- ]
529
-
530
- def evaluate_hyperparams(config: dict, val_examples: list, max_evals: int = 2) -> dict:
531
- """
532
- Evaluate a hyperparameter configuration on validation examples.
533
- Returns: {"score": float, "avg_time": float, "consistency": float}
534
- """
535
- n_paths = config["num_paths"]
536
- temp = config["temperature"]
537
- top_p = config["top_p"]
538
- max_tokens = int(MAX_NEW_TOKENS * config["max_tokens_factor"])
539
-
540
- total_score = 0.0
541
- total_time = 0.0
542
- consistency_scores = []
543
-
544
- # Only evaluate on first max_evals examples for speed
545
- for example in val_examples[:max_evals]:
546
- start_t = time.time()
547
-
548
- # Generate N paths
549
- messages = [
550
- {"role": "system", "content": BASE_SYSTEM_PROMPT},
551
- {"role": "user", "content": example["context"] + "\n\n" + example["query"]},
552
- ]
553
- model_inputs = tok.apply_chat_template(
554
- messages, add_generation_prompt=True, return_tensors="pt"
555
- )
556
- ids = model_inputs['input_ids'].to(model.device)
557
-
558
- paths = []
559
- for _ in range(n_paths):
560
- try:
561
- with torch.no_grad():
562
- out = model.generate(
563
- ids,
564
- max_new_tokens=max_tokens,
565
- do_sample=True,
566
- temperature=temp,
567
- top_p=top_p,
568
- pad_token_id=tok.eos_token_id,
569
- )
570
- text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
571
- paths.append(text)
572
- except:
573
- paths.append("")
574
-
575
- gen_time = time.time() - start_t
576
- total_time += gen_time
577
-
578
- # Extract answers
579
- path_answers = []
580
- for path in paths:
581
- if path:
582
- answers = extract_answers_from_text(path, len(example["expected"]))
583
- answers = enforce_k_length(answers, len(example["expected"]))
584
- path_answers.append(answers)
585
-
586
- if not path_answers:
587
- continue
588
-
589
- # Calculate consensus and score
590
- if example["eval_metric"] == "exact":
591
- # For exact match: check if consensus matches expected
592
- answers_list = [tuple(a) for a in path_answers if a]
593
- if answers_list:
594
- most_common = Counter(answers_list).most_common(1)[0][0]
595
- consensus = list(most_common)
596
- score = sum(1 for i, exp in enumerate(example["expected"])
597
- if i < len(consensus) and consensus[i] == exp) / len(example["expected"])
598
- # Consistency = agreement among paths
599
- consistency = Counter(answers_list).most_common(1)[0][1] / len(answers_list)
600
- else:
601
- score = 0.0
602
- consistency = 0.0
603
- else:
604
- # For chrF: use fuzzy matching
605
- consensus, _, _ = consensus_vote(path_answers, "chrF")
606
- score = sum(chrF_score(consensus[i], example["expected"][i])
607
- for i in range(min(len(consensus), len(example["expected"])))) / len(example["expected"])
608
- # Consistency based on path agreement
609
- consistency = sum(
610
- sum(chrF_score(a1, a2) for a2 in path_answers) / len(path_answers)
611
- for a1 in path_answers
612
- ) / len(path_answers) if path_answers else 0.0
613
-
614
- total_score += score
615
- consistency_scores.append(consistency)
616
-
617
- n_evaluated = min(max_evals, len(val_examples))
618
- avg_score = total_score / n_evaluated if n_evaluated > 0 else 0.0
619
- avg_time = total_time / n_evaluated if n_evaluated > 0 else 0.0
620
- avg_consistency = sum(consistency_scores) / len(consistency_scores) if consistency_scores else 0.0
621
-
622
- return {
623
- "score": avg_score,
624
- "avg_time": avg_time,
625
- "consistency": avg_consistency,
626
- }
627
-
628
- def grid_search_hyperparams(val_examples: list, max_configs: int = 8) -> dict:
629
- """
630
- Grid search over hyperparameter space.
631
- Tests promising configurations and returns the best one.
632
- """
633
- print("\n=== HYPERPARAMETER TUNING ===", flush=True)
634
- print(f"Testing configurations on {len(val_examples)} validation examples...", flush=True)
635
-
636
- # Priority order based on typical IOL performance
637
- test_configs = [
638
- {"num_paths": 3, "temperature": 0.2, "top_p": 0.9, "max_tokens_factor": 1.0},
639
- {"num_paths": 3, "temperature": 0.3, "top_p": 0.9, "max_tokens_factor": 1.0},
640
- {"num_paths": 4, "temperature": 0.2, "top_p": 0.95, "max_tokens_factor": 1.2},
641
- {"num_paths": 2, "temperature": 0.1, "top_p": 0.85, "max_tokens_factor": 1.0},
642
- {"num_paths": 5, "temperature": 0.3, "top_p": 0.95, "max_tokens_factor": 1.2},
643
- {"num_paths": 3, "temperature": 0.4, "top_p": 0.9, "max_tokens_factor": 1.0},
644
- {"num_paths": 4, "temperature": 0.2, "top_p": 0.9, "max_tokens_factor": 1.5},
645
- {"num_paths": 3, "temperature": 0.2, "top_p": 0.99, "max_tokens_factor": 1.2},
646
- ][:max_configs]
647
-
648
- results = []
649
- for i, config in enumerate(test_configs):
650
- print(f"\nConfig {i+1}/{len(test_configs)}: N={config['num_paths']}, T={config['temperature']}, top_p={config['top_p']}", flush=True)
651
- metrics = evaluate_hyperparams(config, val_examples)
652
- results.append((config, metrics))
653
- print(f" Score: {metrics['score']:.3f}, Consistency: {metrics['consistency']:.3f}, Time: {metrics['avg_time']:.2f}s", flush=True)
654
-
655
- # Select best configuration based on combined score
656
- # Weight: accuracy 60%, consistency 30%, speed 10%
657
- def combined_score(config, metrics):
658
- # Normalize time (lower is better, assume max 30s per example)
659
- time_score = max(0, 1 - metrics['avg_time'] / 30.0)
660
- return (0.6 * metrics['score'] +
661
- 0.3 * metrics['consistency'] +
662
- 0.1 * time_score)
663
-
664
- best_config, best_metrics = max(results, key=lambda x: combined_score(x[0], x[1]))
665
-
666
- print(f"\n=== BEST CONFIGURATION ===", flush=True)
667
- print(f"NUM_PATHS: {best_config['num_paths']}", flush=True)
668
- print(f"TEMPERATURE: {best_config['temperature']}", flush=True)
669
- print(f"TOP_P: {best_config['top_p']}", flush=True)
670
- print(f"MAX_TOKENS_FACTOR: {best_config['max_tokens_factor']}", flush=True)
671
- print(f"Expected Score: {best_metrics['score']:.3f}", flush=True)
672
-
673
- return best_config
674
-
675
- # Global hyperparameters (will be set by tuning or use defaults)
676
- TUNED_NUM_PATHS = None
677
- TUNED_TEMPERATURE = None
678
- TUNED_TOP_P = None
679
- TUNED_MAX_TOKENS_FACTOR = None
680
-
681
- def apply_tuned_hyperparams(config: dict = None):
682
- """Apply tuned hyperparameters to global variables."""
683
- global NUM_PATHS, TEMPERATURE, TOP_P, MAX_NEW_TOKENS
684
- global TUNED_NUM_PATHS, TUNED_TEMPERATURE, TUNED_TOP_P, TUNED_MAX_TOKENS_FACTOR
685
-
686
- if config is None:
687
- # Use default/baseline configuration
688
- config = {
689
- "num_paths": NUM_PATHS,
690
- "temperature": TEMPERATURE,
691
- "top_p": 0.9,
692
- "max_tokens_factor": 1.0,
693
- }
694
-
695
- TUNED_NUM_PATHS = config["num_paths"]
696
- TUNED_TEMPERATURE = config["temperature"]
697
- TUNED_TOP_P = config["top_p"]
698
- TUNED_MAX_TOKENS_FACTOR = config["max_tokens_factor"]
699
-
700
- # Update the globals used by other functions
701
- NUM_PATHS = TUNED_NUM_PATHS
702
- TEMPERATURE = TUNED_TEMPERATURE
703
-
704
- print(f"\nApplied tuned hyperparameters:", flush=True)
705
- print(f" NUM_PATHS: {NUM_PATHS}", flush=True)
706
- print(f" TEMPERATURE: {TEMPERATURE}", flush=True)
707
- print(f" TOP_P: {TUNED_TOP_P}", flush=True)
708
- print(f" MAX_NEW_TOKENS: {int(MAX_NEW_TOKENS * TUNED_MAX_TOKENS_FACTOR)}", flush=True)
709
-
710
- \n# =============================================================================
711
- # REINFORCEMENT LEARNING COMPONENT
712
- # Online learning from test samples using self-consistency as reward
713
- # =============================================================================
714
-
715
- class ReinforcementLearner:
716
- """
717
- Online RL that samples from test data and reinforces successful patterns.
718
- Uses self-consistency and confidence as reward signals.
719
- """
720
-
721
- def __init__(self):
722
- self.learned_patterns = {} # pattern -> success_score
723
- self.morpheme_inventory = {} # morpheme -> {translations, contexts}
724
- self.rule_confidence = {} # rule -> confidence_score
725
- self.paradigm_library = {} # pattern_type -> paradigms
726
- self.iteration_rewards = [] # Track reward over iterations
727
-
728
- def sample_test_data(self, df: pd.DataFrame, n_samples: int = 3) -> pd.DataFrame:
729
- """
730
- Sample representative items from test set for RL training.
731
- Strategy: diverse task types + medium complexity (not too short/not too long)
732
- """
733
- if len(df) <= n_samples:
734
- return df.copy()
735
-
736
- # Get distribution of task types
737
- task_types = df['task_type'].value_counts()
738
- samples = []
739
-
740
- # Sample from each task type proportionally
741
- for task_type in task_types.index:
742
- type_df = df[df['task_type'] == task_type]
743
- n_from_type = max(1, int(n_samples * len(type_df) / len(df)))
744
-
745
- # Sort by context length (prefer medium-length contexts)
746
- type_df = type_df.copy()
747
- type_df['context_len'] = type_df['context'].str.len()
748
- type_df['complexity_score'] = abs(type_df['context_len'] - type_df['context_len'].median())
749
- type_df = type_df.sort_values('complexity_score')
750
-
751
- samples.append(type_df.head(n_from_type))
752
-
753
- sampled = pd.concat(samples).head(n_samples)
754
- return sampled
755
-
756
- def compute_reward(self, paths: list, consensus: list, confidence: float,
757
- expected_k: int, generation_time: float) -> dict:
758
- """
759
- Compute multi-faceted reward for RL training.
760
-
761
- Reward components:
762
- - consistency_reward: Agreement between paths (self-consistency)
763
- - confidence_reward: Model confidence in its answer
764
- - format_reward: Proper JSON formatting and K-length
765
- - efficiency_reward: Reasonable generation time
766
- """
767
- # Extract answers from all paths
768
- path_answers = []
769
- for path in paths:
770
- if path:
771
- answers = extract_answers_from_text(path, expected_k)
772
- if answers:
773
- path_answers.append(tuple(answers))
774
-
775
- # Consistency reward: pairwise agreement
776
- if len(path_answers) >= 2:
777
- agreements = 0
778
- total_pairs = 0
779
- for i in range(len(path_answers)):
780
- for j in range(i+1, len(path_answers)):
781
- # Compare answer similarity
782
- if len(path_answers[i]) == len(path_answers[j]):
783
- matches = sum(1 for a, b in zip(path_answers[i], path_answers[j])
784
- if chrF_score(a, b) > 0.8)
785
- agreements += matches / len(path_answers[i])
786
- total_pairs += 1
787
- consistency_reward = agreements / total_pairs if total_pairs > 0 else 0.0
788
- else:
789
- consistency_reward = 0.0
790
-
791
- # Confidence reward (already computed)
792
- confidence_reward = confidence
793
-
794
- # Format reward: Did we get valid K-length outputs?
795
- format_reward = 1.0 if len(consensus) == expected_k and all(consensus) else 0.5
796
-
797
- # Efficiency reward: Prefer faster generations (normalize to 30s target)
798
- efficiency_reward = max(0, 1 - generation_time / 30.0)
799
-
800
- # Combined reward (weighted)
801
- total_reward = (
802
- 0.35 * consistency_reward +
803
- 0.35 * confidence_reward +
804
- 0.20 * format_reward +
805
- 0.10 * efficiency_reward
806
- )
807
-
808
- return {
809
- "total": total_reward,
810
- "consistency": consistency_reward,
811
- "confidence": confidence_reward,
812
- "format": format_reward,
813
- "efficiency": efficiency_reward,
814
- }
815
-
816
- def extract_patterns(self, paths: list, consensus: list, task_type: str) -> list:
817
- """
818
- Extract successful/reusable patterns from generated paths.
819
- """
820
- patterns = []
821
-
822
- for path in paths:
823
- if not path:
824
- continue
825
-
826
- # Extract morphemes from scratchpad
827
- morpheme_section = re.search(
828
- r'2\.\s*MORPHEME SEGMENTATION:(.+?)(?=3\.|PARADIGM|RULES|EXPLANATION|$)',
829
- path, re.DOTALL | re.IGNORECASE
830
- )
831
- if morpheme_section:
832
- # Look for morpheme mappings like "word = morpheme1 + morpheme2"
833
- mappings = re.findall(
834
- r'([\w\-ʔʼʰʱʲʷː̥̩̪̯̃͡ɑ-ʯḀ-ẞ]+)\s*=\s*([\w\-ʔʼʰʱʲʷː̥̩̪̯̃͡ɑ-ʯḀ-ẞ]+)\s*\+\s*([\w\-ʔʼʰʱʲʷː̥̩̪̯̃͡ɑ-ʯḀ-ẞ]+)',
835
- morpheme_section.group(1)
836
- )
837
- for mapping in mappings:
838
- pattern_key = f"SEG:{mapping[0]}->{mapping[1]}+{mapping[2]}"
839
- patterns.append(("morpheme_segmentation", pattern_key, mapping))
840
-
841
- # Extract rules from RULES section
842
- rules = extract_rules(path)
843
- for rule in rules:
844
- rule_type = self._classify_rule(rule, task_type)
845
- patterns.append(("rule", rule_type, rule))
846
-
847
- # Extract paradigm patterns
848
- paradigm_section = re.search(
849
- r'3\.\s*PARADIGM MAPPING:(.+?)(?=4\.|CONSTRAINT|RULES|EXPLANATION|$)',
850
- path, re.DOTALL | re.IGNORECASE
851
- )
852
- if paradigm_section:
853
- # Look for category mappings
854
- categories = re.findall(
855
- r'(1st|2nd|3rd|singular|plural|present|past|nominative|accusative)\s*[=:]\s*([\w\-]+)',
856
- paradigm_section.group(1), re.IGNORECASE
857
- )
858
- if categories:
859
- patterns.append(("paradigm", task_type, categories))
860
-
861
- return patterns
862
-
863
- def _classify_rule(self, rule: str, task_type: str) -> str:
864
- """Classify rule type for organizing the library."""
865
- rule_lower = rule.lower()
866
- if any(x in rule_lower for x in ['phonol', 'consonant', 'vowel', 'sound', 'harmony', 'sandhi']):
867
- return f"{task_type}:phonological"
868
- elif any(x in rule_lower for x in ['prefix', 'suffix', 'infix', 'affix', 'morpheme']):
869
- return f"{task_type}:morphological"
870
- elif any(x in rule_lower for x in ['word order', 'synta', 'sov', 'svo', 'agreement']):
871
- return f"{task_type}:syntactic"
872
- else:
873
- return f"{task_type}:general"
874
-
875
- def update_learned_patterns(self, patterns: list, reward: float):
876
- """
877
- Update pattern library with exponential moving average.
878
- """
879
- alpha = 0.3 # Learning rate
880
-
881
- for pattern_type, pattern_key, pattern_value in patterns:
882
- key = f"{pattern_type}:{pattern_key}"
883
-
884
- if key not in self.learned_patterns:
885
- self.learned_patterns[key] = {
886
- "type": pattern_type,
887
- "key": pattern_key,
888
- "value": pattern_value,
889
- "success_score": reward,
890
- "count": 1,
891
- }
892
- else:
893
- # Exponential moving average update
894
- old_score = self.learned_patterns[key]["success_score"]
895
- self.learned_patterns[key]["success_score"] = (
896
- (1 - alpha) * old_score + alpha * reward
897
- )
898
- self.learned_patterns[key]["count"] += 1
899
-
900
- def build_rl_enhanced_prompt(self, base_prompt: str, task_type: str) -> str:
901
- """
902
- Enhance prompt with learned patterns relevant to this task type.
903
- """
904
- enhancements = []
905
-
906
- # Add high-confidence patterns for this task type
907
- relevant_patterns = [
908
- p for k, p in self.learned_patterns.items()
909
- if task_type in str(p.get("key", "")) and p["success_score"] > 0.5
910
- ]
911
-
912
- # Sort by success score
913
- relevant_patterns.sort(key=lambda x: x["success_score"], reverse=True)
914
-
915
- if relevant_patterns:
916
- enhancements.append("\n【LEARNED PATTERNS FROM TEST DATA】")
917
- enhancements.append("Based on analysis of similar problems:")
918
-
919
- for i, pattern in enumerate(relevant_patterns[:3]): # Top 3
920
- if pattern["type"] == "rule":
921
- enhancements.append(f" {i+1}. {pattern['key']}: {pattern['value']}")
922
- elif pattern["type"] == "morpheme_segmentation":
923
- val = pattern["value"]
924
- enhancements.append(f" {i+1}. Morpheme pattern: {val[0]} = {val[1]} + {val[2]}")
925
-
926
- enhancements.append("Consider these patterns in your analysis.\n")
927
-
928
- return base_prompt + "\n" + "\n".join(enhancements) if enhancements else base_prompt
929
-
930
- def rl_training_loop(self, df: pd.DataFrame, n_iterations: int = 2, samples_per_iter: int = 3) -> dict:
931
- """
932
- Main RL training loop: sample, evaluate, reinforce, repeat.
933
-
934
- Returns optimized hyperparameters and learned patterns.
935
- """
936
- print(f"\n=== REINFORCEMENT LEARNING ({n_iterations} iterations) ===", flush=True)
937
-
938
- best_config = None
939
- best_avg_reward = 0.0
940
-
941
- # Configurations to try (will be refined based on rewards)
942
- configs_to_try = [
943
- {"num_paths": 3, "temperature": 0.2, "top_p": 0.9},
944
- {"num_paths": 4, "temperature": 0.3, "top_p": 0.95},
945
- {"num_paths": 5, "temperature": 0.25, "top_p": 0.92},
946
- ]
947
-
948
- for iteration in range(n_iterations):
949
- print(f"\n--- RL Iteration {iteration + 1}/{n_iterations} ---", flush=True)
950
-
951
- # Sample test data
952
- sample_df = self.sample_test_data(df, samples_per_iter)
953
- print(f"Sampled {len(sample_df)} items for training", flush=True)
954
-
955
- iteration_rewards = []
956
-
957
- for _, row in sample_df.iterrows():
958
- task_type = row.get("task_type", "translation")
959
- eval_type = row.get("eval_type", "chr_f1")
960
- eval_metric = "exact" if eval_type.startswith("exact") else "chrF"
961
- k = count_items(row["query"])
962
-
963
- # Build base prompt
964
- base_prompt = build_router_aware_prompt(
965
- row["context"], row["query"], task_type, eval_type, k
966
- )
967
-
968
- # Enhance with learned patterns
969
- enhanced_prompt = self.build_rl_enhanced_prompt(base_prompt, task_type)
970
-
971
- # Try different configurations
972
- for config in configs_to_try[:2]: # Try first 2 configs
973
- start_t = time.time()
974
-
975
- # Generate paths
976
- messages = [
977
- {"role": "system", "content": BASE_SYSTEM_PROMPT},
978
- {"role": "user", "content": enhanced_prompt},
979
- ]
980
- model_inputs = tok.apply_chat_template(
981
- messages, add_generation_prompt=True, return_tensors="pt"
982
- )
983
- ids = model_inputs['input_ids'].to(model.device)
984
-
985
- # Check time again before generation
986
- if time_left() < TIME_LIMIT * 0.4:
987
- print(f" RL sample aborted: critical time ({time_left():.0f}s)", flush=True)
988
- break
989
-
990
- paths = []
991
- for _ in range(config["num_paths"]):
992
- try:
993
- with torch.no_grad():
994
- out = model.generate(
995
- ids,
996
- max_new_tokens=MAX_NEW_TOKENS,
997
- do_sample=True,
998
- temperature=config["temperature"],
999
- top_p=config["top_p"],
1000
- pad_token_id=tok.eos_token_id,
1001
- )
1002
- text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
1003
- paths.append(text)
1004
- except:
1005
- paths.append("")
1006
-
1007
- gen_time = time.time() - start_t
1008
-
1009
- # Extract answers and compute consensus
1010
- path_answers = []
1011
- for path in paths:
1012
- if path:
1013
- ans = extract_answers_from_text(path, k)
1014
- path_answers.append(enforce_k_length(ans, k))
1015
-
1016
- if path_answers:
1017
- consensus, confidence, _ = consensus_vote(path_answers, eval_metric)
1018
- consensus = enforce_k_length(consensus, k)
1019
-
1020
- # Compute reward
1021
- reward = self.compute_reward(
1022
- paths, consensus, confidence, k, gen_time
1023
- )
1024
- iteration_rewards.append(reward["total"])
1025
-
1026
- # Extract and reinforce patterns if reward is good
1027
- if reward["total"] > 0.5:
1028
- patterns = self.extract_patterns(paths, consensus, task_type)
1029
- self.update_learned_patterns(patterns, reward["total"])
1030
-
1031
- # Track best config
1032
- if reward["total"] > best_avg_reward:
1033
- best_avg_reward = reward["total"]
1034
- best_config = config
1035
-
1036
- avg_reward = sum(iteration_rewards) / len(iteration_rewards) if iteration_rewards else 0.0
1037
- self.iteration_rewards.append(avg_reward)
1038
- print(f" Average reward: {avg_reward:.3f}", flush=True)
1039
- print(f" Learned patterns: {len(self.learned_patterns)}", flush=True)
1040
-
1041
- # Determine final config
1042
- if best_config is None:
1043
- best_config = configs_to_try[0] # Default
1044
-
1045
- print(f"\n=== RL TRAINING COMPLETE ===", flush=True)
1046
- print(f"Best config: N={best_config['num_paths']}, T={best_config['temperature']}", flush=True)
1047
- print(f"Best reward: {best_avg_reward:.3f}", flush=True)
1048
- print(f"Total learned patterns: {len(self.learned_patterns)}", flush=True)
1049
-
1050
- return {
1051
- "best_config": best_config,
1052
- "learned_patterns": self.learned_patterns,
1053
- "reward_history": self.iteration_rewards,
1054
- }
1055
-
1056
- def get_learned_pattern_summary(self) -> str:
1057
- """Return summary of learned patterns for explanation."""
1058
- if not self.learned_patterns:
1059
- return "No patterns learned yet"
1060
-
1061
- top_patterns = sorted(
1062
- self.learned_patterns.values(),
1063
- key=lambda x: x["success_score"],
1064
- reverse=True
1065
- )[:5]
1066
-
1067
- summary = []
1068
- for p in top_patterns:
1069
- summary.append(f" - {p['key']} (score: {p['success_score']:.2f}, n={p['count']})")
1070
-
1071
- return "\n".join(summary)
1072
-
1073
- # Global RL learner instance
1074
- rl_learner = ReinforcementLearner()
1075
-
1076
- \n# 4. DETERMINISTIC ALIGNMENT & FALLBACK GUARDRAIL
1077
- # =============================================================================
1078
 
1079
  def enforce_k_length(answers: list, k: int) -> list:
1080
  """Ensure output is exactly K items."""
@@ -1083,188 +134,182 @@ def enforce_k_length(answers: list, k: int) -> list:
1083
  answers.append("")
1084
  return answers
1085
 
1086
- def chrF_fallback_recovery(paths: list, expected_k: int, target_item_idx: int) -> str:
1087
- """When consensus fails, try chrF-based selection."""
1088
- candidates = []
1089
- for path in paths:
1090
- answers = extract_answers_from_text(path, expected_k)
1091
- if target_item_idx < len(answers) and answers[target_item_idx]:
1092
- candidates.append(answers[target_item_idx])
1093
- if not candidates:
1094
- return ""
1095
- counts = Counter(candidates)
1096
- return counts.most_common(1)[0][0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1097
 
1098
- def process_row_with_architecture(row: pd.Series, use_rl_prompt: bool = True) -> dict:
1099
- """Main processing function with optional RL enhancement."""
1100
  task_type = row.get("task_type", "translation")
1101
- eval_type = row.get("eval_type", "chr_f1")
1102
  context = row["context"]
1103
  query = row["query"]
1104
  k = count_items(query)
1105
-
1106
- profile = get_task_profile(task_type)
1107
- eval_metric = "exact" if eval_type.startswith("exact") else "chrF"
1108
-
1109
- print(f"\n[{row['id']}] Task={task_type}, Eval={eval_metric}, K={k}", flush=True)
1110
-
1111
- base_prompt = build_router_aware_prompt(context, query, task_type, eval_type, k)
1112
-
1113
- # Enhance with learned patterns if RL is active
1114
- if use_rl_prompt:
1115
- prompt = rl_learner.build_rl_enhanced_prompt(base_prompt, task_type)
1116
- else:
1117
- prompt = base_prompt
1118
-
1119
- print(f" Generating N={NUM_PATHS} paths with Few-Shot Rules...", flush=True)
1120
- paths = generate_n_paths(prompt, n=NUM_PATHS, temperature=TEMPERATURE)
1121
-
1122
- path_answers = []
1123
- all_rules = []
1124
- for i, path in enumerate(paths):
1125
- if path:
1126
- answers = extract_answers_from_text(path, k)
1127
- answers = enforce_k_length(answers, k)
1128
- path_answers.append(answers)
1129
- rules = extract_rules(path)
1130
- all_rules.extend(rules)
1131
- print(f" Path {i+1}: {answers}", flush=True)
1132
-
1133
- if path_answers:
1134
- consensus, confidence, voting_explanation = consensus_vote(path_answers, eval_metric)
1135
- consensus = enforce_k_length(consensus, k)
1136
- print(f" Consensus: {consensus} (confidence={confidence:.2f})", flush=True)
1137
- else:
1138
- consensus = [""] * k
1139
- confidence = 0.0
1140
- voting_explanation = "No valid paths"
1141
-
1142
- final_answers = enforce_k_length(consensus, k)
1143
-
1144
- if confidence < 0.5 and path_answers:
1145
- for i in range(k):
1146
- if not final_answers[i] or not final_answers[i].strip():
1147
- recovered = chrF_fallback_recovery(paths, k, i)
1148
- if recovered:
1149
- final_answers[i] = recovered
1150
- print(f" chrF recovery for item {i+1}: {recovered}", flush=True)
1151
-
1152
- final_answers = enforce_k_length(final_answers, k)
1153
-
1154
- best_explanation = ""
1155
- for path in paths:
1156
- if path:
1157
- best_explanation = extract_explanation(path)
1158
- if best_explanation:
1159
- break
1160
-
1161
- unique_rules = list(dict.fromkeys([r for r in all_rules if r]))
1162
- rules_summary = " | ".join(unique_rules[:5]) if unique_rules else "No rules"
1163
-
1164
- full_explanation = f"{voting_explanation} | {best_explanation[:350]} | Rules: {rules_summary[:250]}"
1165
-
1166
  return {
1167
  "id": row["id"],
1168
- "prediction": json.dumps(final_answers, ensure_ascii=False),
1169
  "explanation": full_explanation[:1200],
1170
  }
1171
 
1172
- # =============================================================================
1173
- # SETUP: Reinforcement Learning + Hyperparameter Tuning
1174
- # =============================================================================
1175
-
1176
- rl_results = None
1177
-
1178
- # Phase 1: Reinforcement Learning from test samples (if time permits and not disabled)
1179
- if not DISABLE_RL and time_left() > TIME_LIMIT * 0.75: # If we have >75% time
1180
  try:
1181
- print("\n" + "="*60, flush=True)
1182
- print("PHASE 1: REINFORCEMENT LEARNING FROM TEST DATA", flush=True)
1183
- print("="*60, flush=True)
1184
- rl_results = rl_learner.rl_training_loop(
1185
- df,
1186
- n_iterations=2,
1187
- samples_per_iter=3
1188
- )
1189
-
1190
- # Apply RL-discovered config
1191
- if rl_results and rl_results.get("best_config"):
1192
- cfg = rl_results["best_config"]
1193
- global NUM_PATHS, TEMPERATURE, TUNED_TOP_P
1194
- NUM_PATHS = cfg["num_paths"]
1195
- TEMPERATURE = cfg["temperature"]
1196
- TUNED_TOP_P = cfg.get("top_p", 0.9)
1197
- print(f"\n>>> Applied RL config: N={NUM_PATHS}, T={TEMPERATURE}, top_p={TUNED_TOP_P}", flush=True)
1198
-
1199
- print(f"\n>>> Learned patterns:")
1200
- print(rl_learner.get_learned_pattern_summary(), flush=True)
1201
-
1202
- except Exception as e:
1203
- print(f"RL training error: {e}", flush=True)
1204
-
1205
- # Phase 2: Traditional hyperparameter tuning (if RL didn't run or failed)
1206
- if not rl_results and time_left() > TIME_LIMIT * 0.5:
1207
- try:
1208
- print("\n" + "="*60, flush=True)
1209
- print("PHASE 2: HYPERPARAMETER TUNING", flush=True)
1210
- print("="*60, flush=True)
1211
- best_config = grid_search_hyperparams(VALIDATION_EXAMPLES, max_configs=4)
1212
- apply_tuned_hyperparams(best_config)
1213
- except Exception as e:
1214
- print(f"HP tuning error: {e}", flush=True)
1215
- apply_tuned_hyperparams(None)
1216
- else:
1217
- if not rl_results:
1218
- print("Skipping tuning - time constrained", flush=True)
1219
- apply_tuned_hyperparams(None)
1220
-
1221
- # =============================================================================
1222
- # MAIN PROCESSING LOOP (with RL-enhanced prompts)
1223
- # =============================================================================
1224
- # =============================================================================
1225
-
1226
- rows_out = []
1227
- n_rows = len(df)
1228
-
1229
- # Main processing with time awareness and graceful degradation
1230
- for i, r in df.iterrows():
1231
- expected_k = count_items(r["query"])
1232
- remaining_rows = n_rows - i
1233
- per_row_budget = (time_left() - SAFETY_BUFFER) / max(1, remaining_rows)
1234
-
1235
- # Safety: absolute time cutoff
1236
- if time_left() < SAFETY_BUFFER:
1237
- print(f"TIMEOUT: Only {time_left():.0f}s left, using fast fallback", flush=True)
1238
- rows_out.append({
1239
- "id": r["id"],
1240
- "prediction": json.dumps([""] * expected_k, ensure_ascii=False),
1241
- "explanation": "Timeout: processing stopped",
1242
- })
1243
- continue
1244
-
1245
- if time_left() < SAFETY_BUFFER or per_row_budget < 10:
1246
- rows_out.append({
1247
- "id": r["id"],
1248
- "prediction": json.dumps([""] * expected_k, ensure_ascii=False),
1249
- "explanation": "Timeout fallback",
1250
- })
1251
- continue
1252
-
1253
- try:
1254
- result = process_row_with_architecture(r)
1255
- rows_out.append(result)
1256
  except Exception as e:
1257
- print(f"Error processing {r['id']}: {e}", flush=True)
1258
- rows_out.append({
1259
- "id": r["id"],
1260
- "prediction": json.dumps([""] * expected_k, ensure_ascii=False),
1261
- "explanation": f"Error: {str(e)[:150]}",
1262
- })
1263
-
1264
- pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
1265
- print(f"Progress: {len(rows_out)}/{n_rows} rows, {time_left():.0f}s left", flush=True)
1266
-
1267
- pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
1268
- print("\n=== submission.csv written ===", flush=True)
1269
- print(f"Total time: {time.time() - START:.1f}s", flush=True)
1270
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  os.environ["HF_HUB_OFFLINE"] = "1"
3
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
4
 
5
+ # Ensure output directory exists IMMEDIATELY
6
+ os.makedirs("/tmp/model", exist_ok=True)
7
+ os.makedirs("/tmp/data", exist_ok=True)
8
+
9
+ import subprocess
10
+ import sys
11
 
12
  def _run_pkg_cmd(args):
13
  for cmd in (["uv", "pip", *args], [sys.executable, "-m", "pip", *args], ["pip", *args]):
 
18
  continue
19
  return False
20
 
21
+ # Install required packages
22
  try:
23
+ import bitsandbytes
24
  except ImportError:
25
  _run_pkg_cmd(["install", "-q", "bitsandbytes"])
26
 
27
  try:
28
+ import torchvision
29
  _run_pkg_cmd(["uninstall", "-y", "-q", "torchvision"])
30
  except ImportError:
31
  pass
 
33
  import re
34
  import json
35
  import time
 
 
 
36
  import pandas as pd
37
  import torch
 
38
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
39
 
40
+ # Submission path - evaluation expects it here
41
+ SUBMISSION_PATH = "/tmp/model/submission.csv"
 
 
42
 
43
+ # Ensure output directory exists
44
+ os.makedirs(os.path.dirname(SUBMISSION_PATH), exist_ok=True)
45
+
46
+ # Configuration
47
  MODEL_ID = "."
48
+ TIME_LIMIT = 30 * 60
49
+ SAFETY_BUFFER = 90
50
+ MAX_NEW_TOKENS = 1800
51
+ NUM_PATHS = 3
52
+ TEMPERATURE = 0.3
53
+ TOP_P = 0.9
54
 
55
  START = time.time()
56
 
57
  def time_left():
58
  return TIME_LIMIT - (time.time() - START)
59
 
60
+ # Global variables for model
61
+ tok = None
62
+ model = None
63
+
64
+ def load_model():
65
+ global tok, model
66
+ print("Loading tokenizer/model...", flush=True)
67
+ tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
68
+
69
+ bnb_config = BitsAndBytesConfig(
70
+ load_in_4bit=True,
71
+ bnb_4bit_quant_type="nf4",
72
+ bnb_4bit_compute_dtype=torch.float16,
73
+ bnb_4bit_use_double_quant=True,
74
+ )
75
+
 
 
 
 
 
 
76
  try:
 
77
  model = AutoModelForCausalLM.from_pretrained(
78
  MODEL_ID,
79
+ trust_remote_code=True,
80
+ quantization_config=bnb_config,
81
+ device_map="auto",
82
+ torch_dtype=torch.float16,
83
  ).eval()
84
+ print("Model loaded successfully", flush=True)
85
+ except Exception as e:
86
+ print(f"Error loading model: {e}", flush=True)
87
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  def count_items(query: str) -> int:
90
  """Extract K = number of items in the query."""
91
+ nums = re.findall(r"(?m)^\s*(\d+)\.\s", query)
92
  if nums:
93
  return len(set(nums))
 
94
  return max(1, len([l for l in query.splitlines() if l.strip()]))
95
 
96
+ def build_prompt(context: str, query: str, task_type: str, k: int) -> str:
97
+ return f"Task: {task_type}\n\nData:\n{context.strip()}\n\nQuery ({k} items):\n{query.strip()}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ def extract_answers(text: str, expected_k: int) -> list:
100
  """Extract the JSON answer array from model output."""
101
+ text = text.strip()
102
  # Try FINAL: block first
103
  m = re.search(r"FINAL:\s*(\[.*?\])", text, re.DOTALL | re.IGNORECASE)
104
  if m:
 
106
  parsed = json.loads(m.group(1))
107
  if isinstance(parsed, list):
108
  return [str(x) for x in parsed]
109
+ except:
110
  pass
111
+
112
  # Look for any JSON array
113
  for m2 in re.finditer(r"\[.*?\]", text, re.DOTALL):
114
  try:
115
  parsed = json.loads(m2.group(0))
116
+ if isinstance(parsed, list):
117
  return [str(x) for x in parsed]
118
+ except:
119
  continue
120
+
121
  # Fallback: extract lines after FINAL:
122
+ if "FINAL:" in text:
123
+ tail = text.split("FINAL:")[-1]
124
+ lines = [re.sub(r"^\s*\d+[\.\)\]]\s*", "", l).strip(' \t\"\'')
125
+ for l in tail.splitlines() if l.strip()]
126
+ lines = [l for l in lines if l]
127
+ return lines
128
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  def enforce_k_length(answers: list, k: int) -> list:
131
  """Ensure output is exactly K items."""
 
134
  answers.append("")
135
  return answers
136
 
137
+ def generate_answer(prompt: str, k: int) -> tuple:
138
+ """Generate answer for a single prompt."""
139
+ messages = [
140
+ {"role": "system", "content": "You are an expert linguist solving linguistic puzzles."},
141
+ {"role": "user", "content": prompt},
142
+ ]
143
+
144
+ chat_text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
145
+ inputs = tok(chat_text, return_tensors="pt").to(model.device)
146
+
147
+ try:
148
+ with torch.no_grad():
149
+ outputs = model.generate(
150
+ **inputs,
151
+ max_new_tokens=MAX_NEW_TOKENS,
152
+ temperature=TEMPERATURE,
153
+ top_p=TOP_P,
154
+ do_sample=True,
155
+ pad_token_id=tok.eos_token_id,
156
+ )
157
+
158
+ generated = tok.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
159
+
160
+ # Extract answers
161
+ answers = extract_answers(generated, k)
162
+ answers = enforce_k_length(answers, k)
163
+
164
+ # Extract explanation
165
+ explanation = ""
166
+ if "EXPLANATION:" in generated:
167
+ m = re.search(r"EXPLANATION:(.+?)(?:FINAL:|$)", generated, re.DOTALL | re.IGNORECASE)
168
+ if m:
169
+ explanation = m.group(1).strip()[:800]
170
+
171
+ return answers, explanation, generated
172
+ except Exception as e:
173
+ print(f"Generation error: {e}", flush=True)
174
+ return [""] * k, f"Error: {e}", ""
175
 
176
+ def process_row(row: pd.Series) -> dict:
177
+ """Process a single row."""
178
  task_type = row.get("task_type", "translation")
 
179
  context = row["context"]
180
  query = row["query"]
181
  k = count_items(query)
182
+
183
+ print(f"[{row['id']}] Task={task_type}, K={k}", flush=True)
184
+
185
+ prompt = build_prompt(context, query, task_type, k)
186
+
187
+ # Generate multiple paths and select best
188
+ answers_list = []
189
+ explanations = []
190
+ all_outputs = []
191
+
192
+ for i in range(NUM_PATHS):
193
+ if time_left() < SAFETY_BUFFER:
194
+ print(f" Time running low ({time_left():.0f}s), using partial results", flush=True)
195
+ break
196
+
197
+ answers, explanation, full_output = generate_answer(prompt, k)
198
+ answers_list.append(answers)
199
+ if explanation:
200
+ explanations.append(explanation)
201
+ all_outputs.append(full_output)
202
+ print(f" Path {i+1}: {answers}", flush=True)
203
+
204
+ # Simple consensus: take the most common answer for each position
205
+ consensus = []
206
+ for i in range(k):
207
+ position_answers = [ans[i] for ans in answers_list if i < len(ans) and ans[i]]
208
+ if position_answers:
209
+ from collections import Counter
210
+ most_common = Counter(position_answers).most_common(1)[0][0]
211
+ consensus.append(most_common)
212
+ else:
213
+ consensus.append("")
214
+
215
+ consensus = enforce_k_length(consensus, k)
216
+ print(f" Consensus: {consensus}", flush=True)
217
+
218
+ # Build explanation
219
+ best_explanation = explanations[0] if explanations else "No explanation generated"
220
+ full_explanation = f"Consensus from {len(answers_list)} paths | {best_explanation}"
221
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  return {
223
  "id": row["id"],
224
+ "prediction": json.dumps(consensus, ensure_ascii=False),
225
  "explanation": full_explanation[:1200],
226
  }
227
 
228
+ def main():
229
+ """Main entry point."""
230
+ DATA_PATH = "/tmp/data/test.csv"
231
+
 
 
 
 
232
  try:
233
+ # Load model
234
+ load_model()
235
+
236
+ # Load test data
237
+ if not os.path.exists(DATA_PATH):
238
+ print(f"ERROR: test.csv not found at {DATA_PATH}", flush=True)
239
+ # Write empty submission
240
+ pd.DataFrame(columns=["id", "prediction", "explanation"]).to_csv(SUBMISSION_PATH, index=False)
241
+ return
242
+
243
+ df = pd.read_csv(DATA_PATH, dtype=str).fillna("")
244
+ print(f"Loaded {len(df)} test items", flush=True)
245
+
246
+ if len(df) == 0:
247
+ print("WARNING: test.csv is empty", flush=True)
248
+ pd.DataFrame(columns=["id", "prediction", "explanation"]).to_csv(SUBMISSION_PATH, index=False)
249
+ return
250
+
251
+ # Process rows
252
+ rows_out = []
253
+ for idx, row in df.iterrows():
254
+ if time_left() < SAFETY_BUFFER:
255
+ print(f"TIMEOUT: Only {time_left():.0f}s left, stopping", flush=True)
256
+ # Fill remaining with empty
257
+ for j in range(idx, len(df)):
258
+ k = count_items(df.iloc[j]["query"])
259
+ rows_out.append({
260
+ "id": df.iloc[j]["id"],
261
+ "prediction": json.dumps([""] * k, ensure_ascii=False),
262
+ "explanation": "Timeout: processing stopped",
263
+ })
264
+ break
265
+
266
+ try:
267
+ result = process_row(row)
268
+ rows_out.append(result)
269
+ except Exception as e:
270
+ print(f"Error processing {row['id']}: {e}", flush=True)
271
+ k = count_items(row["query"])
272
+ rows_out.append({
273
+ "id": row["id"],
274
+ "prediction": json.dumps([""] * k, ensure_ascii=False),
275
+ "explanation": f"Error: {str(e)[:150]}",
276
+ })
277
+
278
+ # Save progress
279
+ pd.DataFrame(rows_out).to_csv(SUBMISSION_PATH, index=False)
280
+ print(f"Progress: {len(rows_out)}/{len(df)} rows, {time_left():.0f}s left", flush=True)
281
+
282
+ # Final save
283
+ pd.DataFrame(rows_out).to_csv(SUBMISSION_PATH, index=False)
284
+ print(f"Wrote: {SUBMISSION_PATH}", flush=True)
285
+ print(f"Total time: {time.time() - START:.1f}s", flush=True)
286
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  except Exception as e:
288
+ print(f"FATAL ERROR: {e}", flush=True)
289
+ import traceback
290
+ traceback.print_exc()
291
+ # Always write submission file even on crash
292
+ try:
293
+ if os.path.exists(DATA_PATH):
294
+ df = pd.read_csv(DATA_PATH, dtype=str).fillna("")
295
+ rows_out = []
296
+ for _, row in df.iterrows():
297
+ k = count_items(row["query"])
298
+ rows_out.append({
299
+ "id": row["id"],
300
+ "prediction": json.dumps([""] * k, ensure_ascii=False),
301
+ "explanation": f"Fatal error: {str(e)[:100]}",
302
+ })
303
+ pd.DataFrame(rows_out).to_csv(SUBMISSION_PATH, index=False)
304
+ else:
305
+ # No test data, write empty
306
+ pd.DataFrame(columns=["id", "prediction", "explanation"]).to_csv(SUBMISSION_PATH, index=False)
307
+ print(f"Wrote emergency submission to {SUBMISSION_PATH}", flush=True)
308
+ except Exception as e2:
309
+ print(f"Could not write emergency submission: {e2}", flush=True)
310
+ # Last resort: create minimal file
311
+ with open(SUBMISSION_PATH, "w") as f:
312
+ f.write("id,prediction,explanation\n")
313
+
314
+ if __name__ == "__main__":
315
+ main()