JuliaKreutzerCohere commited on
Commit
c5c2aca
·
verified ·
1 Parent(s): da338f2

Upload script.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +402 -0
script.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+
6
+ def _install_bundled_deps() -> None:
7
+ """Install transformers from bundled wheels (eval sandbox has no PyPI access)."""
8
+ wheels_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wheels")
9
+ if not os.path.isdir(wheels_dir):
10
+ return
11
+ subprocess.run(
12
+ [
13
+ sys.executable,
14
+ "-m",
15
+ "pip",
16
+ "install",
17
+ "-q",
18
+ "--no-index",
19
+ f"--find-links={wheels_dir}",
20
+ "transformers==4.56.2",
21
+ ],
22
+ check=True,
23
+ )
24
+
25
+
26
+ _install_bundled_deps()
27
+
28
+ import re
29
+ import csv
30
+ import json
31
+ import random
32
+ import shutil
33
+ import tempfile
34
+ import unicodedata
35
+ from collections import Counter
36
+
37
+ import torch
38
+ from transformers import AutoTokenizer, AutoModelForCausalLM
39
+
40
+ # The repo is the working directory at run time, and there is no network.
41
+ os.environ["HF_HUB_OFFLINE"] = "1"
42
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
43
+ MODEL_ID = "."
44
+ MAX_NEW_TOKENS = 2000
45
+ TEMPERATURE = 0.8
46
+ TOP_P = 0.95
47
+ MAX_ATTEMPTS = 2 # per language; ensemble provides redundancy
48
+ MIN_LANGS = 3
49
+ MAX_LANGS = 5
50
+
51
+ # Languages tiny-aya-global handles well for chain-of-thought.
52
+ REASONING_LANGUAGES = [
53
+ "English",
54
+ "Spanish",
55
+ "French",
56
+ "German",
57
+ "Portuguese",
58
+ "Italian",
59
+ "Dutch",
60
+ "Russian",
61
+ "Arabic",
62
+ "Simplified Chinese",
63
+ "Japanese",
64
+ "Korean",
65
+ "Turkish",
66
+ "Hindi",
67
+ "Indonesian",
68
+ "Vietnamese",
69
+ "Polish",
70
+ "Swedish",
71
+ "Greek",
72
+ "Hebrew",
73
+ # "Swahili",
74
+ "Ukrainian",
75
+ "Romanian",
76
+ "Czech",
77
+ "Hungarian",
78
+ ]
79
+
80
+
81
+ def load_tokenizer(model_id: str = "."):
82
+ """Load tokenizer, converting tokenizer.json for older tokenizers if needed."""
83
+ tokenizer_path = os.path.join(model_id, "tokenizer.json")
84
+ with open(tokenizer_path, encoding="utf-8") as handle:
85
+ data = json.load(handle)
86
+
87
+ merges = data.get("model", {}).get("merges", [])
88
+ if not merges or not isinstance(merges[0], list):
89
+ return AutoTokenizer.from_pretrained(model_id)
90
+
91
+ # Older tokenizers expect merge pairs as "a b" strings, not ["a", "b"] lists.
92
+ data["model"]["merges"] = [" ".join(piece) for piece in merges]
93
+ tmpdir = tempfile.mkdtemp()
94
+ for name in ("tokenizer_config.json", "special_tokens_map.json"):
95
+ src = os.path.join(model_id, name)
96
+ if os.path.isfile(src):
97
+ shutil.copy(src, tmpdir)
98
+ with open(os.path.join(tmpdir, "tokenizer.json"), "w", encoding="utf-8") as handle:
99
+ json.dump(data, handle)
100
+ return AutoTokenizer.from_pretrained(tmpdir)
101
+
102
+
103
+ SYSTEM_TEMPLATE = (
104
+ "You solve International Linguistics Olympiad problems by reasoning from the "
105
+ "data in CONTEXT you are given to solve the problems in QUERY. \n"
106
+ "There are common TASK TYPES that we specify below, but "
107
+ "you may meet a TASK TYPE you have never seen: read the "
108
+ "instruction and the examples, and answer the QUERY in the same form they use.\n\n"
109
+ "Common TASK TYPES and what to return: \n"
110
+ "`translation`: return the translated form only, in the language the task asks for; \n"
111
+ "`fill_blanks`: return only the missing form for each indicated blank "
112
+ "(beware: this could be many different things: a word, a part of a word or a phonetic transcription---pay close attention to what part of the CONTEXT is missing in QUERY); \n"
113
+ "`match_letters`: return only the option letter (for example A, B, C); \n"
114
+ "`text_to_num`: return the number in digits; \n"
115
+ "`num_to_text`: return the number written out in words, in the language asked; \n"
116
+ "any other type: return exactly what the instruction asks for, nothing else. \n\n"
117
+ "IMPORTANT: Write ALL of your step-by-step reasoning in {language}. "
118
+ "Do not mix languages in the reasoning. "
119
+ "The FINAL ANSWERS section must still use the English marker `FINAL ANSWERS:` "
120
+ "and the answer values themselves must follow the TASK TYPE / QUERY requirements "
121
+ "(do not translate those answers into {language} unless the query asks for that).\n\n"
122
+ "As the first part of your answer, reason step by step in {language} about (1) the linguistic "
123
+ "rules that can be deduced from the given examples in CONTEXT, and (2) "
124
+ "how to apply them to the given problems in QUERY, and (3) in what format answers need to be returned (words, numbers, phonetic transcriptions, ...). \n"
125
+ "Then write a draft of the final answer. "
126
+ "Subsequently, compare it with the format requirements again, "
127
+ "and verify it's compliant with the deduced rules, and it is complete, i.e. has an answer for each element in QUERY. "
128
+ "If necessary, correct and refine."
129
+ "Finally, write a line that says exactly `FINAL ANSWERS:` "
130
+ "and, below it, write the answers to the items requested in QUERY (not those in CONTEXT),"
131
+ "one answer per line (separated by \\n) in the order the items are asked for in the QUERY -- the "
132
+ "bare answer only, no numbering, no quotes, no extra text, according to the given TASK TYPE."
133
+ )
134
+
135
+
136
+ # Prefer a dedicated header line; also allow same-line answers after the colon.
137
+ FINAL_ANSWERS_LINE_RE = re.compile(
138
+ r"(?im)^[^\w\n]*final answers?[^\w\n]*:?[ \t]*(?=\n|$)|"
139
+ r"(?im)^[^\w\n]*final answers?\s*:\s*"
140
+ )
141
+ FINAL_ANSWERS_INLINE_RE = re.compile(
142
+ r"(?is)\bfinal answers?\s*:\s*"
143
+ )
144
+
145
+
146
+ def extract_raw_final(text: str) -> str:
147
+ """Return text after the last final-answers marker, or '' if none found."""
148
+ line_matches = list(FINAL_ANSWERS_LINE_RE.finditer(text))
149
+ if line_matches:
150
+ return text[line_matches[-1].end() :]
151
+
152
+ inline_matches = list(FINAL_ANSWERS_INLINE_RE.finditer(text))
153
+ if inline_matches:
154
+ return text[inline_matches[-1].end() :]
155
+
156
+ return ""
157
+
158
+
159
+ def expected_answer_count(query: str, task_type: str) -> int:
160
+ if task_type == "match_letters":
161
+ numbered = re.findall(r"^\s*\d+\.", query, re.MULTILINE)
162
+ return len(numbered) or 1
163
+
164
+ if "blanks" in query.lower():
165
+ range_match = re.search(r"\((\d+)-(\d+)\)", query)
166
+ if range_match:
167
+ return int(range_match.group(2)) - int(range_match.group(1)) + 1
168
+ return len(re.findall(r"\(\d+\)", query)) or 1
169
+
170
+ numbered = re.findall(r"^\s*\d+[.)]", query, re.MULTILINE)
171
+ return len(numbered) or 1
172
+
173
+
174
+ def split_single_line_answer(text: str, expected: int, task_type: str) -> list[str]:
175
+ text = text.strip()
176
+ if expected <= 1:
177
+ return [text]
178
+
179
+ def try_split(pattern: str) -> list[str] | None:
180
+ parts = [part.strip() for part in re.split(pattern, text) if part.strip()]
181
+ return parts if len(parts) == expected else None
182
+
183
+ if task_type == "match_letters":
184
+ for pattern in (r"\s+", r",\s*", r";\s*"):
185
+ if result := try_split(pattern):
186
+ return result
187
+ letters = re.findall(r"[A-Za-z]", text)
188
+ if len(letters) == expected:
189
+ return [letter.upper() for letter in letters]
190
+ return [text]
191
+
192
+ if task_type in ("text_to_num", "num_to_text"):
193
+ for pattern in (r",\s*", r";\s*", r"\s+"):
194
+ if result := try_split(pattern):
195
+ return result
196
+ return [text]
197
+
198
+ for pattern in (r";\s*", r",\s*"):
199
+ if result := try_split(pattern):
200
+ return result
201
+ return [text]
202
+
203
+
204
+ def parse_answer_lines(text_after_marker: str, query: str, task_type: str) -> list[str]:
205
+ """Parse cleaned answer lines from the raw final-answers section."""
206
+ answers = []
207
+ for line in text_after_marker.splitlines():
208
+ stripped_line = line.strip("`").strip()
209
+ if stripped_line == "":
210
+ continue
211
+
212
+ match_numbered_prefix = re.match(r"^\s*\d+[.)]\s+(.*)", stripped_line)
213
+ if match_numbered_prefix:
214
+ cleaned_line = match_numbered_prefix.group(1).strip()
215
+ else:
216
+ cleaned_line = stripped_line
217
+
218
+ cleaned_line = re.sub(r"\*\*", "", cleaned_line).strip()
219
+
220
+ if task_type == "match_letters":
221
+ parts = [
222
+ part.strip("().[]")
223
+ for part in re.split(r"[\s,;]+", cleaned_line)
224
+ if part.strip()
225
+ ]
226
+ if not (
227
+ len(parts) > 1
228
+ and all(re.fullmatch(r"[A-Za-z]", part) for part in parts)
229
+ ):
230
+ match_letter_word = re.match(
231
+ r"^\s*(?:\(([A-Za-z])\)|\[([A-Za-z])\]|([A-Za-z]))\.?:?\s*(.*)$",
232
+ cleaned_line,
233
+ )
234
+ if match_letter_word:
235
+ letter = (
236
+ match_letter_word.group(1)
237
+ or match_letter_word.group(2)
238
+ or match_letter_word.group(3)
239
+ )
240
+ cleaned_line = letter.upper()
241
+
242
+ if cleaned_line:
243
+ answers.append(cleaned_line)
244
+
245
+ expected = expected_answer_count(query, task_type)
246
+ if len(answers) == 1 and expected > 1:
247
+ answers = split_single_line_answer(answers[0], expected, task_type)
248
+ return answers
249
+
250
+
251
+ def postprocess_answer(text, query, task_type):
252
+ """Keep only the content after the last 'FINAL ANSWERS' marker."""
253
+ text_after_marker = extract_raw_final(text)
254
+ if not text_after_marker.strip():
255
+ return []
256
+ return parse_answer_lines(text_after_marker, query, task_type)
257
+
258
+
259
+ def normalize_for_vote(text: str, task_type: str) -> str:
260
+ text = unicodedata.normalize("NFC", text.strip())
261
+ if task_type == "match_letters":
262
+ return text.upper()
263
+ return " ".join(text.split())
264
+
265
+
266
+ def majority_vote(
267
+ lang_rollouts: list[tuple[str, list[str]]],
268
+ expected: int,
269
+ task_type: str,
270
+ ) -> list[str]:
271
+ """Per-item majority vote; ties break toward the English rollout."""
272
+ if expected <= 0:
273
+ return []
274
+
275
+ # Prefer rollouts whose length matches the expected answer count.
276
+ eligible = [
277
+ (lang, rollout)
278
+ for lang, rollout in lang_rollouts
279
+ if len(rollout) == expected and any(a.strip() for a in rollout)
280
+ ]
281
+ if not eligible:
282
+ eligible = [
283
+ (lang, rollout)
284
+ for lang, rollout in lang_rollouts
285
+ if any(a.strip() for a in rollout)
286
+ ]
287
+ if not eligible:
288
+ return []
289
+
290
+ final: list[str] = []
291
+ for i in range(expected):
292
+ tagged = [
293
+ (lang, rollout[i])
294
+ for lang, rollout in eligible
295
+ if i < len(rollout) and rollout[i].strip()
296
+ ]
297
+ if not tagged:
298
+ final.append("")
299
+ continue
300
+
301
+ pairs = [
302
+ (lang, normalize_for_vote(ans, task_type), ans)
303
+ for lang, ans in tagged
304
+ ]
305
+ counter = Counter(norm for _, norm, _ in pairs)
306
+ top_count = max(counter.values())
307
+ tied_norms = {norm for norm, count in counter.items() if count == top_count}
308
+
309
+ english_pair = next(
310
+ ((norm, ans) for lang, norm, ans in pairs if lang == "English"),
311
+ None,
312
+ )
313
+ if english_pair is not None and english_pair[0] in tied_norms:
314
+ winner_norm = english_pair[0]
315
+ # Prefer English's surface form when it matches the winning norm.
316
+ final.append(english_pair[1])
317
+ continue
318
+
319
+ winner_norm = sorted(tied_norms)[0]
320
+ originals = [ans for _, norm, ans in pairs if norm == winner_norm]
321
+ final.append(Counter(originals).most_common(1)[0][0])
322
+ return final
323
+
324
+
325
+ def sample_reasoning_languages() -> list[str]:
326
+ """Always include English; sample the rest from the non-English pool."""
327
+ k = random.randint(MIN_LANGS, MAX_LANGS)
328
+ others = [lang for lang in REASONING_LANGUAGES if lang != "English"]
329
+ return ["English"] + random.sample(others, k - 1)
330
+
331
+
332
+ def generate_for_language(tok, model, language: str, context: str, task_type: str, query: str) -> str:
333
+ system = SYSTEM_TEMPLATE.format(language=language)
334
+ messages = [
335
+ {"role": "system", "content": system},
336
+ {
337
+ "role": "user",
338
+ "content": (
339
+ f"CONTEXT:{context.strip()}\n"
340
+ f"TASK TYPE:`{task_type}`\n\n"
341
+ f"QUERY:{query.strip()}\n\n"
342
+ f"Remember: reason entirely in {language}."
343
+ ),
344
+ },
345
+ ]
346
+ ids = tok.apply_chat_template(
347
+ messages, add_generation_prompt=True, return_tensors="pt",
348
+ ).to(model.device)
349
+
350
+ text = ""
351
+ for attempt in range(1, MAX_ATTEMPTS + 1):
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=TOP_P,
359
+ )
360
+ text = tok.decode(out[0][ids.shape[-1] :], skip_special_tokens=True).strip()
361
+ if extract_raw_final(text).strip():
362
+ return text
363
+ print(
364
+ f" [{language}] retry {attempt}/{MAX_ATTEMPTS}: no FINAL ANSWERS",
365
+ flush=True,
366
+ )
367
+ return text
368
+
369
+
370
+ tok = load_tokenizer(MODEL_ID)
371
+ model = AutoModelForCausalLM.from_pretrained(
372
+ MODEL_ID, torch_dtype=torch.float16, device_map="auto"
373
+ ).eval()
374
+
375
+ with open("/tmp/data/test.csv", encoding="utf-8", newline="") as f:
376
+ test_rows = list(csv.DictReader(f))
377
+
378
+ rows = []
379
+ for idx, r in enumerate(test_rows, start=1):
380
+ languages = sample_reasoning_languages()
381
+ print(f"{idx}/{len(test_rows)} langs={languages}", flush=True)
382
+
383
+ lang_rollouts: list[tuple[str, list[str]]] = []
384
+ for language in languages:
385
+ text = generate_for_language(
386
+ tok, model, language, r["context"], r["task_type"], r["query"],
387
+ )
388
+ answers = postprocess_answer(text, r["query"], r["task_type"])
389
+ lang_rollouts.append((language, answers))
390
+ print(f" [{language}] parsed={answers!r}", flush=True)
391
+
392
+ expected = expected_answer_count(r["query"], r["task_type"])
393
+ voted = majority_vote(lang_rollouts, expected, r["task_type"])
394
+ print(f" vote -> {voted!r}", flush=True)
395
+
396
+ rows.append({"id": r["id"], "pred": json.dumps(voted, ensure_ascii=False)})
397
+
398
+ with open("submission.csv", "w", encoding="utf-8", newline="") as f:
399
+ writer = csv.DictWriter(f, fieldnames=["id", "pred"])
400
+ writer.writeheader()
401
+ writer.writerows(rows)
402
+ print("wrote submission.csv", flush=True)