JuliaKreutzerCohere commited on
Commit
bc86635
·
verified ·
1 Parent(s): 2615371

Upload script.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +287 -0
script.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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."
89
+ "Finally, write a line that says exactly `FINAL ANSWERS:` "
90
+ "and, below it, write the answers to the items requested in QUERY (not those in CONTEXT),"
91
+ "one answer per line (separated by \n) in the order the items are asked for in the QUERY -- the "
92
+ "bare answer only, no numbering, no quotes, no extra text, according to the given TASK TYPE."
93
+ )
94
+
95
+ SYSTEM_POST_EXPLAIN = (
96
+ "You are a helpful assistant that explains the reasoning behind the answers to the International Linguistics Olympiad problems.
97
+ "You are given the following information:\n"
98
+ "- The context of the problem\n"
99
+ "- The task type\n"
100
+ "- The query\n"
101
+ "- The answer\n"
102
+ "- The reasoning\n"
103
+ "You need to explain the reasoning behind the answer in a way that is easy to understand and concisely focused on the key insights and rules deduced and applied.\n"
104
+ "Do not include any other text, do not includethe answer in the explanation, "
105
+ " and do not invent any new information."
106
+ )
107
+ tok = load_tokenizer(MODEL_ID)
108
+ model = AutoModelForCausalLM.from_pretrained(
109
+ MODEL_ID, torch_dtype=torch.float16, device_map="auto"
110
+ ).eval()
111
+
112
+ with open("/tmp/data/test.csv", encoding="utf-8", newline="") as f:
113
+ test_rows = list(csv.DictReader(f))
114
+
115
+ outputs_queries_types = []
116
+ for r in test_rows:
117
+
118
+ # Create the prompt.
119
+ messages = [
120
+ {"role": "system", "content": SYSTEM},
121
+ {"role": "user", "content":
122
+ f"CONTEXT:{r['context'].strip()}\nTASK TYPE:`{r['task_type']}`\n\nQUERY:{r['query'].strip()}"},
123
+ ]
124
+ ids = tok.apply_chat_template(
125
+ messages, add_generation_prompt=True, return_tensors="pt",
126
+ ).to(model.device)
127
+
128
+ # Generate the answer.
129
+ done = False
130
+ attempts = 0
131
+ while not done and attempts < MAX_ATTEMPTS:
132
+ with torch.no_grad():
133
+ out = model.generate(
134
+ ids,
135
+ max_new_tokens=MAX_NEW_TOKENS,
136
+ do_sample=True,
137
+ temperature=TEMPERATURE,
138
+ top_p=TOP_P,)
139
+ text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
140
+ # IF NO FINAL ANSWER keyword is used, try again.
141
+ attempts += 1
142
+ if "final answer" not in text.lower() or text.lower().split('final answer')[1].split('\n')==0:
143
+ print(f'TRYING AGAIN...attempts #{attempts+1}/{MAX_ATTEMPTS}')
144
+ else:
145
+ done = True
146
+
147
+ # Generate the explanation.
148
+ messages_post_explain = [
149
+ {"role": "system", "content": SYSTEM_POST_EXPLAIN},
150
+ {"role": "user", "content":
151
+ f"CONTEXT:{r['context'].strip()}\nTASK TYPE:`{r['task_type']}`\n\nQUERY:{r['query'].strip()}\n\nANSWER:{text.strip()}"},
152
+ ]
153
+ ids_post_explain = tok.apply_chat_template(
154
+ messages_post_explain, add_generation_prompt=True, return_tensors="pt",
155
+ ).to(model.device)
156
+ with torch.no_grad():
157
+ out_post_explain = model.generate(
158
+ ids_post_explain,
159
+ max_new_tokens=MAX_NEW_TOKENS,
160
+ do_sample=False)
161
+ text_post_explain = tok.decode(out_post_explain[0][ids_post_explain.shape[-1]:], skip_special_tokens=True).strip()
162
+
163
+ outputs_queries_types.append((text, text_post_explain, r['id'], r['query'], r['task_type']))
164
+ print(f"{len(outputs_queries_types)}/{len(test_rows)} done", flush=True)
165
+
166
+ # Postprocess and store the answers.
167
+ def expected_answer_count(query: str, task_type: str) -> int:
168
+ if task_type == "match_letters":
169
+ numbered = re.findall(r"^\s*\d+\.", query, re.MULTILINE)
170
+ return len(numbered) or 1
171
+
172
+ if "blanks" in query.lower():
173
+ range_match = re.search(r"\((\d+)-(\d+)\)", query)
174
+ if range_match:
175
+ return int(range_match.group(2)) - int(range_match.group(1)) + 1
176
+ return len(re.findall(r"\(\d+\)", query)) or 1
177
+
178
+ numbered = re.findall(r"^\s*\d+[.)]", query, re.MULTILINE)
179
+ return len(numbered) or 1
180
+
181
+
182
+ def split_single_line_answer(text: str, expected: int, task_type: str) -> list[str]:
183
+ text = text.strip()
184
+ if expected <= 1:
185
+ return [text]
186
+
187
+ def try_split(pattern: str) -> list[str] | None:
188
+ parts = [part.strip() for part in re.split(pattern, text) if part.strip()]
189
+ return parts if len(parts) == expected else None
190
+
191
+ if task_type == "match_letters":
192
+ for pattern in (r"\s+", r",\s*", r";\s*"):
193
+ if result := try_split(pattern):
194
+ return result
195
+ letters = re.findall(r"[A-Za-z]", text)
196
+ if len(letters) == expected:
197
+ return [letter.upper() for letter in letters]
198
+ return [text]
199
+
200
+ if task_type in ("text_to_num", "num_to_text"):
201
+ for pattern in (r",\s*", r";\s*", r"\s+"):
202
+ if result := try_split(pattern):
203
+ return result
204
+ return [text]
205
+
206
+ for pattern in (r";\s*", r",\s*"):
207
+ if result := try_split(pattern):
208
+ return result
209
+ return [text]
210
+
211
+
212
+ def postprocess_answer(text, query, task_type):
213
+ """Keep only the lines after the last 'FINAL ANSWERS:' marker, one answer per line,
214
+ stopping at the first empty line. If eval_type is multiple and only one line as answer, split at whitespace."""
215
+ # Updated regex to be more flexible with surrounding characters
216
+ marker_match = list(re.finditer(r"(?im)^[^\w\n]*final answers?[^\w\n]*:?\s*$", text))
217
+ if marker_match:
218
+ text_after_marker = text[marker_match[-1].end():]
219
+ #print('FOUND FINAL ANSWER', text_after_marker)
220
+ else:
221
+ #print("No 'FINAL ANSWERS:' marker found")
222
+ return []
223
+
224
+ answers = []
225
+ answer_lines = text_after_marker.splitlines()
226
+ for i, line in enumerate(answer_lines):
227
+ stripped_line = line.strip('`').strip()
228
+
229
+ # Stop processing if an empty line is encountered (not as first line)
230
+ if stripped_line=='':
231
+ continue
232
+
233
+ # Use a more precise regex to only remove numbering if it's a prefix to other text
234
+ # This ensures that lines which are just numbers (e.g., '1') are not stripped.
235
+ match_numbered_prefix = re.match(r"^\s*\d+[.)]\s+(.*)", stripped_line)
236
+ if match_numbered_prefix:
237
+ cleaned_line = match_numbered_prefix.group(1).strip()
238
+ else:
239
+ cleaned_line = stripped_line
240
+
241
+ # Remove any bold markdown '**'
242
+ cleaned_line = re.sub(r"\*\*", "", cleaned_line).strip()
243
+
244
+ # Specific handling for 'match_letters' task type to strip extra words
245
+ if task_type == 'match_letters':
246
+ parts = [
247
+ part.strip("().[]")
248
+ for part in re.split(r"[\s,;]+", cleaned_line)
249
+ if part.strip()
250
+ ]
251
+ if not (len(parts) > 1 and all(re.fullmatch(r"[A-Za-z]", part) for part in parts)):
252
+ match_letter_word = re.match(
253
+ r"^\s*(?:\(([A-Za-z])\)|\[([A-Za-z])\]|([A-Za-z]))\.?:?\s*(.*)$",
254
+ cleaned_line,
255
+ )
256
+ if match_letter_word:
257
+ letter = (
258
+ match_letter_word.group(1)
259
+ or match_letter_word.group(2)
260
+ or match_letter_word.group(3)
261
+ )
262
+ cleaned_line = letter.upper()
263
+
264
+ # Append the cleaned, non-empty line
265
+ if cleaned_line:
266
+ answers.append(cleaned_line)
267
+
268
+ #print('PARSED ANSWERS', answers)
269
+
270
+ # Compare against QUERY length: sometimes model forgets newlines
271
+ #print('QUERY', query)
272
+ expected = expected_answer_count(query, task_type)
273
+ query_len = len(query.splitlines()) - 2
274
+ #print(query_len)
275
+ if len(answers) == 1 and expected > 1:
276
+ answers = split_single_line_answer(answers[0], expected, task_type)
277
+ return answers
278
+
279
+ rows = []
280
+ for answer, explanation, row_id, query, task_type in outputs_queries_types:
281
+ answers = postprocess_answer(answer, query, task_type)
282
+ rows.append({"id": row_id, "pred": json.dumps(answers, ensure_ascii=False), "explanation": explanation})
283
+ with open("submission.csv", "w", encoding="utf-8", newline="") as f:
284
+ writer = csv.DictWriter(f, fieldnames=["id", "pred", "explanation"])
285
+ writer.writeheader()
286
+ writer.writerows(rows)
287
+ print("wrote submission.csv", flush=True)