JuliaKreutzerCohere commited on
Commit
af2989e
·
verified ·
1 Parent(s): 57bd043

Upload script.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +304 -0
script.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 shutil
32
+ import tempfile
33
+ import torch
34
+ from transformers import AutoTokenizer, AutoModelForCausalLM
35
+
36
+ # The repo is the working directory at run time, and there is no network.
37
+ os.environ["HF_HUB_OFFLINE"] = "1"
38
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
39
+ MODEL_ID = "."
40
+ MAX_NEW_TOKENS = 2000
41
+ TEMPERATURE = 0.8
42
+ TOP_P = 0.95
43
+ MAX_ATTEMPTS = 5
44
+
45
+
46
+ def load_tokenizer(model_id: str = "."):
47
+ """Load tokenizer, converting tokenizer.json for older tokenizers if needed."""
48
+ tokenizer_path = os.path.join(model_id, "tokenizer.json")
49
+ with open(tokenizer_path, encoding="utf-8") as handle:
50
+ data = json.load(handle)
51
+
52
+ merges = data.get("model", {}).get("merges", [])
53
+ if not merges or not isinstance(merges[0], list):
54
+ return AutoTokenizer.from_pretrained(model_id)
55
+
56
+ # Older tokenizers expect merge pairs as "a b" strings, not ["a", "b"] lists.
57
+ data["model"]["merges"] = [" ".join(piece) for piece in merges]
58
+ tmpdir = tempfile.mkdtemp()
59
+ for name in ("tokenizer_config.json", "special_tokens_map.json"):
60
+ src = os.path.join(model_id, name)
61
+ if os.path.isfile(src):
62
+ shutil.copy(src, tmpdir)
63
+ with open(os.path.join(tmpdir, "tokenizer.json"), "w", encoding="utf-8") as handle:
64
+ json.dump(data, handle)
65
+ return AutoTokenizer.from_pretrained(tmpdir)
66
+
67
+
68
+ SYSTEM = (
69
+ "You solve International Linguistics Olympiad problems by reasoning from the "
70
+ "data in CONTEXT you are given to solve the problems in QUERY. \n"
71
+ "There are common TASK TYPES that we specify below, but "
72
+ "you may meet a TASK TYPE you have never seen: read the "
73
+ "instruction and the examples, and answer the QUERY in the same form they use.\n\n"
74
+ "Common TASK TYPES and what to return: \n"
75
+ "`translation`: return the translated form only, in the language the task asks for; \n"
76
+ "`fill_blanks`: return only the missing form for each indicated blank "
77
+ "(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"
78
+ "`match_letters`: return only the option letter (for example A, B, C); \n"
79
+ "`text_to_num`: return the number in digits; \n"
80
+ "`num_to_text`: return the number written out in words, in the language asked; \n"
81
+ "any other type: return exactly what the instruction asks for, nothing else. \n\n"
82
+ "As the first part of your answer, reason step by step about (1) the linguistic "
83
+ "rules that can be deduced from the given examples in CONTEXT, and (2) "
84
+ "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"
85
+ "Then write a draft of the final answer. "
86
+ "Subsequently, compare it with the format requirements again, "
87
+ "and verify it's compliant with the deduced rules, and it is complete, i.e. has an answer for each element in QUERY. "
88
+ "If necessary, correct and refine.\n"
89
+ "Structure your response in exactly two sections with these headers:\n\n"
90
+ "**Reasoning:**\n"
91
+ "(your step-by-step analysis of linguistic rules, how to apply them, and answer format)\n\n"
92
+ "**Final Answers:**\n"
93
+ "(one answer per line, in query order -- bare answers only, no numbering, no quotes, "
94
+ "no extra text, according to the TASK TYPE)"
95
+ )
96
+
97
+ tok = load_tokenizer(MODEL_ID)
98
+ model = AutoModelForCausalLM.from_pretrained(
99
+ MODEL_ID, torch_dtype=torch.float16, device_map="auto"
100
+ ).eval()
101
+
102
+ with open("/tmp/data/test.csv", encoding="utf-8", newline="") as f:
103
+ test_rows = list(csv.DictReader(f))
104
+
105
+ outputs_queries_types = []
106
+ for r in test_rows:
107
+
108
+ # Create the prompt.
109
+ messages = [
110
+ {"role": "system", "content": SYSTEM},
111
+ {"role": "user", "content":
112
+ f"CONTEXT:{r['context'].strip()}\nTASK TYPE:`{r['task_type']}`\n\nQUERY:{r['query'].strip()}"},
113
+ ]
114
+ ids = tok.apply_chat_template(
115
+ messages, add_generation_prompt=True, return_tensors="pt",
116
+ ).to(model.device)
117
+
118
+ # Generate the answer.
119
+ done = False
120
+ attempts = 0
121
+ while not done and attempts < MAX_ATTEMPTS:
122
+ with torch.no_grad():
123
+ out = model.generate(
124
+ ids,
125
+ max_new_tokens=MAX_NEW_TOKENS,
126
+ do_sample=True,
127
+ temperature=TEMPERATURE,
128
+ top_p=TOP_P,)
129
+ text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
130
+ # IF NO FINAL ANSWER keyword is used, try again.
131
+ attempts += 1
132
+ if "final answer" not in text.lower() or text.lower().split('final answer')[1].split('\n')==0:
133
+ print(f'TRYING AGAIN...attempts #{attempts+1}/{MAX_ATTEMPTS}')
134
+ else:
135
+ done = True
136
+
137
+ outputs_queries_types.append((text, r['id'], r['query'], r['task_type']))
138
+ print(f"{len(outputs_queries_types)}/{len(test_rows)} done", flush=True)
139
+
140
+ # Postprocess and store the answers.
141
+ def _normalize_header(line: str) -> str:
142
+ header = line.strip().lower()
143
+ header = re.sub(r"^#{1,3}\s*", "", header)
144
+ header = re.sub(r"\*+", "", header)
145
+ return header.rstrip(":").strip()
146
+
147
+
148
+ def _is_reasoning_header(line: str) -> bool:
149
+ header = _normalize_header(line)
150
+ return header == "reasoning" or header.endswith(" reasoning")
151
+
152
+
153
+ def _is_final_answers_header(line: str) -> bool:
154
+ header = _normalize_header(line)
155
+ return header in ("final answer", "final answers")
156
+
157
+
158
+ def split_response(text: str) -> tuple[str, str]:
159
+ """Return (explanation, raw final-answers section text)."""
160
+ lines = text.splitlines(keepends=True)
161
+ offset = 0
162
+ reasoning_content_start = None
163
+ explanation_end = None
164
+ last_final_content_start = None
165
+
166
+ reasoning_header_seen = False
167
+ final_header_indices: list[int] = []
168
+
169
+ for line in lines:
170
+ if _is_reasoning_header(line) and not reasoning_header_seen:
171
+ reasoning_header_seen = True
172
+ reasoning_content_start = offset + len(line)
173
+ elif _is_final_answers_header(line):
174
+ final_header_indices.append(offset)
175
+ if reasoning_content_start is not None and explanation_end is None:
176
+ explanation_end = offset
177
+ last_final_content_start = offset + len(line)
178
+ offset += len(line)
179
+
180
+ raw_final = text[last_final_content_start:].strip() if last_final_content_start is not None else ""
181
+
182
+ explanation = ""
183
+ if reasoning_content_start is not None and explanation_end is not None:
184
+ explanation = text[reasoning_content_start:explanation_end].strip()
185
+ if not explanation:
186
+ explanation = raw_final
187
+ return explanation, raw_final
188
+
189
+
190
+ def expected_answer_count(query: str, task_type: str) -> int:
191
+ if task_type == "match_letters":
192
+ numbered = re.findall(r"^\s*\d+\.", query, re.MULTILINE)
193
+ return len(numbered) or 1
194
+
195
+ if "blanks" in query.lower():
196
+ range_match = re.search(r"\((\d+)-(\d+)\)", query)
197
+ if range_match:
198
+ return int(range_match.group(2)) - int(range_match.group(1)) + 1
199
+ return len(re.findall(r"\(\d+\)", query)) or 1
200
+
201
+ numbered = re.findall(r"^\s*\d+[.)]", query, re.MULTILINE)
202
+ return len(numbered) or 1
203
+
204
+
205
+ def split_single_line_answer(text: str, expected: int, task_type: str) -> list[str]:
206
+ text = text.strip()
207
+ if expected <= 1:
208
+ return [text]
209
+
210
+ def try_split(pattern: str) -> list[str] | None:
211
+ parts = [part.strip() for part in re.split(pattern, text) if part.strip()]
212
+ return parts if len(parts) == expected else None
213
+
214
+ if task_type == "match_letters":
215
+ for pattern in (r"\s+", r",\s*", r";\s*"):
216
+ if result := try_split(pattern):
217
+ return result
218
+ letters = re.findall(r"[A-Za-z]", text)
219
+ if len(letters) == expected:
220
+ return [letter.upper() for letter in letters]
221
+ return [text]
222
+
223
+ if task_type in ("text_to_num", "num_to_text"):
224
+ for pattern in (r",\s*", r";\s*", r"\s+"):
225
+ if result := try_split(pattern):
226
+ return result
227
+ return [text]
228
+
229
+ for pattern in (r";\s*", r",\s*"):
230
+ if result := try_split(pattern):
231
+ return result
232
+ return [text]
233
+
234
+
235
+ def parse_answer_lines(text_after_marker: str, query: str, task_type: str) -> list[str]:
236
+ """Parse cleaned answer lines from the raw final-answers section."""
237
+ answers = []
238
+ for line in text_after_marker.splitlines():
239
+ stripped_line = line.strip("`").strip()
240
+
241
+ if stripped_line == "":
242
+ continue
243
+
244
+ match_numbered_prefix = re.match(r"^\s*\d+[.)]\s+(.*)", stripped_line)
245
+ if match_numbered_prefix:
246
+ cleaned_line = match_numbered_prefix.group(1).strip()
247
+ else:
248
+ cleaned_line = stripped_line
249
+
250
+ cleaned_line = re.sub(r"\*\*", "", cleaned_line).strip()
251
+
252
+ if task_type == "match_letters":
253
+ parts = [
254
+ part.strip("().[]")
255
+ for part in re.split(r"[\s,;]+", cleaned_line)
256
+ if part.strip()
257
+ ]
258
+ if not (
259
+ len(parts) > 1
260
+ and all(re.fullmatch(r"[A-Za-z]", part) for part in parts)
261
+ ):
262
+ match_letter_word = re.match(
263
+ r"^\s*(?:\(([A-Za-z])\)|\[([A-Za-z])\]|([A-Za-z]))\.?:?\s*(.*)$",
264
+ cleaned_line,
265
+ )
266
+ if match_letter_word:
267
+ letter = (
268
+ match_letter_word.group(1)
269
+ or match_letter_word.group(2)
270
+ or match_letter_word.group(3)
271
+ )
272
+ cleaned_line = letter.upper()
273
+
274
+ if cleaned_line:
275
+ answers.append(cleaned_line)
276
+
277
+ expected = expected_answer_count(query, task_type)
278
+ if len(answers) == 1 and expected > 1:
279
+ answers = split_single_line_answer(answers[0], expected, task_type)
280
+ return answers
281
+
282
+
283
+ def postprocess_answer(text, query, task_type):
284
+ """Extract explanation and parsed final answers from model output."""
285
+ explanation, raw_final = split_response(text)
286
+ if not raw_final:
287
+ return [], explanation
288
+ return parse_answer_lines(raw_final, query, task_type), explanation
289
+
290
+ rows = []
291
+ for answer, row_id, query, task_type in outputs_queries_types:
292
+ answers, explanation = postprocess_answer(answer, query, task_type)
293
+ rows.append(
294
+ {
295
+ "id": row_id,
296
+ "pred": json.dumps(answers, ensure_ascii=False),
297
+ "explanation": explanation,
298
+ }
299
+ )
300
+ with open("submission.csv", "w", encoding="utf-8", newline="") as f:
301
+ writer = csv.DictWriter(f, fieldnames=["id", "pred", "explanation"])
302
+ writer.writeheader()
303
+ writer.writerows(rows)
304
+ print("wrote submission.csv", flush=True)