File size: 16,765 Bytes
0598ff3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
09_big_suite.py - the large four-domain task set behind `07_domain_eval.py run --suite big --hard-n N` (N tasks per domain, up to the pool size).
Public benchmarks, English only, every answer checked by code; the tasks of a smaller N are the FIRST N of a bigger one (nested), so a quick run
(--hard-n 60) is a prefix of the full run (--hard-n 300) and the two can be pooled:
  code    HumanEval+ (164) and MBPP sanitized test (257), 2:3 mix; the model writes the function, the unit tests run on it
  math    MATH-500 integer answers of level 2-5 (278) and AIME (90, AI-MO/aimo-validation-aime), 3:1 mix; final line "#### <integer>"
  reason  MMLU-Pro (240, 14 subjects round-robin, 10 options) and BIG-Bench Hard (120, 8 tasks), 2:1 mix; final line "Answer: <x>"
  en      RACE-high reading comprehension (120), CoLA grammar acceptability (120, half acceptable), and 120 writing prompts with machine-checked
          constraints (word counts, paragraphs, bullets, case, keywords, start/end text, quotes, title, commas, P.S.), 1:1:1 mix
Needs `pip install datasets` and network access; the model-written code is EXECUTED (see 07).  Selftest (same needs):
  python 09_big_suite.py selftest          reference answers pass, garbage fails, every constraint checker passes its own good/bad example
  python 09_big_suite.py validate          runs EVERY reference solution (421 code tasks) and prints the ids to put in BAD_CODE
"""
import random
import re
import sys
from fractions import Fraction as Fr

HOW = "\n\nThink step by step, then give the final answer on the last line as: #### <integer>"
LETTERS = "ABCDEFGHIJ"
BAD_CODE = {"code_he32"}      # code task ids whose own reference solution fails its tests (`validate`: HumanEval/32 find_zero is float-sensitive)


def mix(pools, pattern):
    """The pools interleaved by the digits of `pattern` ("00101" = two from pool 0, one from pool 1, ...) until every pool is used up."""
    pos, out, i = [0] * len(pools), [], 0
    while any(pos[k] < len(pools[k]) for k in range(len(pools))) and i < 10 ** 6:
        k = int(pattern[i % len(pattern)])
        i += 1
        if pos[k] < len(pools[k]):
            out.append(pools[k][pos[k]])
            pos[k] += 1
    return out


def letter(L, k):
    """check: the last 'Answer: X' of the response is the letter L (X among the first k letters)."""
    pat = rf"Answer:[\s*(]*([{LETTERS[:k]}])\b"
    return lambda resp: (re.findall(pat, resp) or [None])[-1] == L


def token(target):
    """check: the last 'Answer: X' of the response has the single word/number X equal to the target (case-insensitive, '(A)' = 'a')."""
    want = re.sub(r"[^a-z0-9\-]", "", target.casefold())
    return lambda resp: bool((got := re.findall(r"Answer:[\s*(\[]*([A-Za-z0-9\-]+)", resp))) and got[-1].casefold() == want


# ---------------------------------------------------------------------------------------------------------------------------------- code
def pool_code(rnd, run_code, code_of):
    from datasets import load_dataset
    he, mb = [], []
    for r in load_dataset("evalplus/humanevalplus", split="test"):
        tests = r["test"] + f"\n\ncheck({r['entry_point']})"
        he.append((f"code_he{r['task_id'].split('/')[-1]}", "code",
                   "Complete this Python function. Reply with one Python code block containing the whole function (and any imports it needs).\n\n"
                   f"```python\n{r['prompt']}```",
                   (lambda t: lambda resp: run_code(code_of(resp), t, 20))(tests), f"```python\n{r['prompt']}{r['canonical_solution']}\n```"))
    for r in load_dataset("google-research-datasets/mbpp", "sanitized", split="test"):
        tests = "\n".join(r["test_imports"]) + "\n" + "\n".join(r["test_list"])
        mb.append((f"code_mbpp{r['task_id']}", "code",
                   "Write a Python function for the task below. Reply with one Python code block containing the whole function (and any imports it "
                   f"needs).\n\nTask: {r['prompt']}\n\nYour function must pass these tests (they fix its name and signature):\n" + "\n".join(r["test_list"]),
                   (lambda t: lambda resp: run_code(code_of(resp), t, 20))(tests), f"```python\n{r['code']}\n```"))
    he, mb = [t for t in he if t[0] not in BAD_CODE], [t for t in mb if t[0] not in BAD_CODE]
    rnd.shuffle(he)
    rnd.shuffle(mb)
    return mix([he, mb], "00111")       # 2 HumanEval+ : 3 MBPP per cycle of five


# ---------------------------------------------------------------------------------------------------------------------------------- math
def pool_math(rnd, num):
    from datasets import load_dataset
    ma, ai = [], []
    for r in load_dataset("HuggingFaceH4/MATH-500", split="test"):
        if re.fullmatch(r"-?\d+", str(r["answer"]).strip()) and int(r["level"]) >= 2:
            ans = Fr(str(r["answer"]).strip())
            ma.append((f"math_{r['unique_id'].replace('/', '_').removesuffix('.json')}", "math", r["problem"] + HOW, num(ans), f"#### {ans}"))
    for r in load_dataset("AI-MO/aimo-validation-aime", split="train"):
        if re.fullmatch(r"-?\d+", str(r["answer"]).strip()):
            ans = Fr(str(r["answer"]).strip())
            ai.append((f"math_aime{r['id']}", "math", r["problem"] + HOW, num(ans), f"#### {ans}"))
    rnd.shuffle(ma)
    rnd.shuffle(ai)
    return mix([ma, ai], "0001")                  # 3 MATH-500 : 1 AIME


# ---------------------------------------------------------------------------------------------------------------------------------- reasoning
BBH = {       # task -> the closing instruction that names the answer format
    "logical_deduction_five_objects": "Answer: <option letter>", "tracking_shuffled_objects_five_objects": "Answer: <option letter>",
    "date_understanding": "Answer: <option letter>", "web_of_lies": "Answer: Yes or Answer: No", "boolean_expressions": "Answer: True or Answer: False",
    "navigate": "Answer: Yes or Answer: No", "formal_fallacies": "Answer: valid or Answer: invalid", "object_counting": "Answer: <integer>"}


def pool_reason(rnd):
    from datasets import load_dataset
    by = {}
    for r in load_dataset("TIGER-Lab/MMLU-Pro", split="test"):
        by.setdefault(r["category"], []).append(r)
    cats = sorted(by)
    for c in cats:
        rnd.shuffle(by[c])
    mm = []
    for i in range(18):                                       # round-robin over the 14 subjects, the subject order shuffled each round
        order = cats[:]
        rnd.shuffle(order)
        for c in order:
            r = by[c][i]
            opts = "\n".join(f"{LETTERS[j]}. {o}" for j, o in enumerate(r["options"]))
            mm.append((f"reason_mmlu{r['question_id']}", "reason", f"{r['question']}\n\n{opts}\n\nThink step by step, then end with exactly: Answer: <letter>",
                       letter(r["answer"], 10), f"Answer: {r['answer']}"))
    mm = mm[:240]
    bb = []
    for task, fmt in BBH.items():
        rows = list(load_dataset("lukaemon/bbh", task, split="test"))
        rnd.shuffle(rows)
        bb.append([(f"reason_bbh_{task}_{k}", "reason", f"{r['input']}\n\nThink step by step, then end with exactly: {fmt}", token(r["target"]),
                    f"Answer: {r['target'].strip('()')}") for k, r in enumerate(rows[:15])])
    return mix([mm, mix(bb, "01234567")], "001")             # 2 MMLU-Pro : 1 BBH


# ---------------------------------------------------------------------------------------------------------------------------------- English
def _words(r):
    return len(re.findall(r"\w+", r))


C = {}      # constraint name -> (tag, make); make(rnd) -> (rule sentence, check, a response that passes, a response that fails)
CLASH = [("lowercase", "end_phrase"), ("lowercase", "start_word"), ("lowercase", "postscript"), ("uppercase", "end_phrase"),
         ("uppercase", "start_word"), ("wrap_quotes", "end_phrase"), ("wrap_quotes", "start_word"), ("wrap_quotes", "postscript"),
         ("end_phrase", "postscript"), ("title", "start_word")]
KEYS = ("river", "lantern", "courage", "market", "silver", "window", "winter", "bridge")
BANNED = ("very", "really", "good", "nice", "thing")
ENDS = ("Is there anything else I can help with?", "That is all for now.", "Thank you for reading.")
STARTS = ("Today", "Imagine", "Remember", "Consider")


def cons(tag):
    def deco(f):
        C[f.__name__] = (tag, f)
        return f
    return deco


@cons("len")
def words_min(rnd):
    n = rnd.choice((80, 120, 150, 200))
    return f"Your response must contain at least {n} words.", lambda r: _words(r) >= n, "word " * n, "word " * (n - 1)


@cons("len")
def words_max(rnd):
    n = rnd.choice((60, 80, 100, 120))
    return f"Your response must contain fewer than {n} words.", lambda r: _words(r) < n, "word " * (n - 1), "word " * n


@cons("struct")
def paragraphs(rnd):
    n = rnd.choice((2, 3, 4))
    return (f"Write exactly {n} paragraphs, separated from each other by a line containing only ***.",
            lambda r: len(p := re.split(r"(?m)^[ \t]*\*\*\*[ \t]*$", r.strip())) == n and all(x.strip() for x in p),
            "\n***\n".join(["para"] * n), "\n***\n".join(["para"] * (n + 1)))


@cons("struct")
def bullets(rnd):
    n = rnd.choice((3, 4, 5))
    return (f"Use exactly {n} bullet points in markdown format, each line starting with \"* \", and no other bullets.",
            lambda r: sum(1 for l in r.splitlines() if re.match(r"\s*\* ", l)) == n, "\n".join(["* item"] * n), "\n".join(["* item"] * (n + 1)))


@cons("kw")
def keywords(rnd):
    a, b = rnd.sample(KEYS, 2)
    return (f"Include the words \"{a}\" and \"{b}\".", lambda r: all(re.search(rf"\b{k}\b", r, re.I) for k in (a, b)), f"{a} and {b}", f"only {a}")


@cons("forbid")
def forbid(rnd):
    w = rnd.choice(BANNED)
    return f"Do not use the word \"{w}\" anywhere.", lambda r: not re.search(rf"\b{w}\b", r, re.I), "fine text", f"a {w} text"


@cons("case")
def lowercase(rnd):
    return "Your entire response must be in lowercase letters, with no capital letters at all.", lambda r: r == r.lower(), "all lower", "Not lower"


@cons("case")
def uppercase(rnd):
    return "Your entire response must be in ALL CAPITAL LETTERS.", lambda r: r == r.upper(), "ALL UPPER", "Not upper"


@cons("end")
def end_phrase(rnd):
    p = rnd.choice(ENDS)
    return f"Finish your response with the exact phrase \"{p}\" and add nothing after it.", lambda r: r.strip().endswith(p), f"text {p}", f"{p} text"


@cons("start")
def start_word(rnd):
    w = rnd.choice(STARTS)
    return f"Begin your response with the word \"{w}\".", lambda r: bool(re.match(rf"\W*{w}\b", r.strip())), f"{w} is a day", f"Then {w}"


@cons("wrap")
def wrap_quotes(rnd):
    return ("Wrap your entire response in double quotation marks.", lambda r: len(r.strip()) > 1 and r.strip()[0] == '"' and r.strip()[-1] == '"',
            '"text"', "text")


@cons("title")
def title(rnd):
    return "Include a title wrapped in double angular brackets, like <<title>>.", lambda r: bool(re.search(r"<<[^<>\n]+>>", r)), "<<A title>> body", "A title body"


@cons("punct")
def no_commas(rnd):
    return "Do not use any commas in your response.", lambda r: "," not in r, "no comma", "a, b"


@cons("ps")
def postscript(rnd):
    return "At the end, add a postscript that starts with P.S.", lambda r: bool(re.search(r"(?m)^\s*P\.S\.", r)), "text\nP.S. more", "text"


@cons("letter")
def letter_z(rnd):
    n = rnd.choice((3, 5))
    return f"The letter \"z\" must appear at least {n} times.", lambda r: r.lower().count("z") >= n, "z" * n, "z" * (n - 1)


TOPICS = ("a rainy morning in a small town", "why sleep matters", "the history of tea", "a lost umbrella", "learning to swim as an adult", "a city without cars",
          "the first day at a new job", "a village market", "the smell of fresh bread", "an old lighthouse", "a long train journey", "keeping a diary",
          "a robot that learns to paint", "the quiet of a library", "planting a vegetable garden", "a broken clock", "moving to another country",
          "the last bus home", "a mountain hike", "a neighbour who plays the violin", "saving money as a student", "a power cut at night",
          "the invention of the bicycle", "a street musician", "cooking for a big family", "an unexpected letter", "the sound of the sea",
          "a school sports day", "a very small apartment", "the first snow of winter")
FORMS = ("short story", "product description", "email to a colleague", "blog post", "poem", "short speech", "review", "letter to a friend",
         "news summary", "explanation for a ten-year-old")


def pool_en(rnd):
    from datasets import load_dataset
    race = list(load_dataset("ehovy/race", "high", split="test"))
    rnd.shuffle(race)
    ra = []
    for k, r in enumerate(race[:120]):          # several questions share one passage id, so the position is part of the task id
        opts = "\n".join(f"{LETTERS[j]}. {o}" for j, o in enumerate(r["options"]))
        ra.append((f"en_race_{k:03d}_{r['example_id'].replace('/', '_').replace('.txt', '')}", "en",
                   f"Read the passage, then answer the question.\n\nPassage:\n{r['article']}\n\nQuestion: {r['question']}\n\n{opts}\n\n"
                   "Think briefly, then end with exactly: Answer: <letter>", letter(r["answer"], 4), f"Answer: {r['answer']}"))
    cola = list(load_dataset("nyu-mll/glue", "cola", split="validation"))
    yes, no = [r for r in cola if r["label"] == 1], [r for r in cola if r["label"] == 0]
    rnd.shuffle(yes)
    rnd.shuffle(no)
    co = []
    for r in mix([yes[:60], no[:60]], "01"):
        co.append((f"en_cola_{r['idx']}", "en", "Is this sentence grammatically acceptable English, as a careful native speaker would judge it?\n\n"
                   f"Sentence: {r['sentence']}\n\nGive at most two sentences of reasoning, then end with exactly: Answer: Yes or Answer: No",
                   token("Yes" if r["label"] == 1 else "No"), f"Answer: {'Yes' if r['label'] == 1 else 'No'}"))
    wr, names = [], sorted(C)
    for i in range(120):
        for _ in range(200):
            pick = rnd.sample(names, 2 if rnd.random() < 0.7 else 3)
            if len({C[n][0] for n in pick}) == len(pick) and not any(a in pick and b in pick for a, b in CLASH):
                break
        parts = [C[n][1](rnd) for n in pick]
        wr.append((f"en_if{i:03d}", "en", f"Write a {rnd.choice(FORMS)} about {rnd.choice(TOPICS)}. Follow every rule below.\n" + "\n".join(f"- {p[0]}" for p in parts),
                   (lambda cs: lambda resp: all(c(resp) for c in cs))([p[1] for p in parts]), None))
    return mix([ra, co, wr], "012")


# ---------------------------------------------------------------------------------------------------------------------------------- the suite
def build_big(seed, n, run_code, code_of, num):
    """[(id, category, prompt, check(response) -> bool, reference response or None)]: the first n tasks of each pool (nested for any n)."""
    T = []
    for name, pool in (("code", lambda r: pool_code(r, run_code, code_of)), ("math", lambda r: pool_math(r, num)), ("reason", pool_reason), ("en", pool_en)):
        T += pool(random.Random(f"{seed}big{name}"))[:n]
    assert len({t[0] for t in T}) == len(T), "duplicate task ids"
    return T


def constraint_unit_tests():
    bad = []
    for name, (tag, make) in C.items():
        for s in range(8):
            _, chk, good, worse = make(random.Random(s))
            if not chk(good) or chk(worse):
                bad.append((name, s))
    return bad


if __name__ == "__main__" and sys.argv[1:] in (["selftest"], ["validate"]):
    import importlib
    sys.path.insert(0, __import__("pathlib").Path(__file__).resolve().parent.as_posix())
    m = importlib.import_module("07_domain_eval")
    if sys.argv[1] == "selftest":
        assert not constraint_unit_tests(), constraint_unit_tests()
        T = build_big(1, 12, m.run_code, m.code_of, m.num)
        ref = [t for t in T if t[4] is not None]
        bad = [t[0] for t in ref if not t[3](t[4])]
        junk = [t[0] for t in T if t[4] is not None and t[3]("zzz 987654321")]      # the constraint tasks have no reference and garbage may satisfy a rule
        per = {c: sum(t[1] == c for t in T) for c in ("code", "math", "reason", "en")}
        assert not bad and not junk and per == {c: 12 for c in per} and len({t[0] for t in T}) == len(T), (bad, junk, per)
        print(f"selftest OK: {len(T)} tasks {per}, {len(ref)} references pass, garbage fails, {len(C)} constraint checkers pass their own examples")
    else:
        bad = [t[0] for t in pool_code(random.Random("v"), m.run_code, m.code_of) if not t[3](t[4])]
        print(f"{len(bad)} reference solutions fail: BAD_CODE = {set(bad)!r}")