File size: 10,785 Bytes
837909a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""proofread — a code-generated supervised task: a short phrase with one common
writing error -> the corrected phrase.

The cleanest kind of must-beat-a-script story: the errors are CONTEXT-DEPENDENT
homophone confusions a lookup table cannot resolve. `their`, `there`, and `they're`
are spelled differently and sound the same; which one is right depends entirely on the
surrounding words. A context-free script can only guess the most common one. The model
reads the phrase and uses the grammatical context to pick.

Scope (stated plainly): a FIXED set of high-frequency confusions plus doubled words,
not general grammar correction:

    their / there / they're      your / you're        its / it's
    then / than                  to / too             could have / could of

Each example is one line::

    their going to win => they're going to win
    its raining again => it's raining again
    the the cat sat => the cat sat

Generation is correct by construction: phrases are built from ~65 distinct grammatical
FRAMES with rich, agreement-checked slot fillers and optional prefix/suffix wrapping,
so the surface form varies enormously and the model must learn the local grammatical
CUE for each word rather than a template. We then inject one error (swap the confusion
word, or double a word). ~15% identity. `proofread_pairs` is shared by train and eval;
`naive_fix` is the best context-free script baseline; `OOD_CASES` is a hand-written
natural-phrase set (NOT drawn from the frames) for measuring true generalisation.
numpy-only, no torch.
"""

from __future__ import annotations

import numpy as np

_NOUN = ("house car dog cat book phone plan idea project dinner movie story song city "
         "street dream bag coffee game party trip photo report meeting budget window "
         "door garden room school office team chair table laptop ticket letter").split()
_NOUN_PL = ("friends parents kids keys shoes neighbours children students teachers dogs "
            "cats books phones plans ideas cars rooms photos tickets letters").split()
_ADJ = ("happy tired ready late early busy angry quiet loud bright dark cold warm funny "
        "serious tough easy strange lovely simple clever lucky honest calm noisy fancy "
        "gentle brave proud nervous curious cheerful sad hungry sleepy").split()
_VING = ("running coming leaving working playing waiting joking winning losing laughing "
         "talking cooking driving reading writing studying dancing moving growing "
         "building helping trying singing sleeping").split()
_VERB = ("win lose leave stay help start finish call visit move build cook read write "
         "study dance play sing watch learn travel rest eat go").split()
_PVERB = ("ran left came stayed helped started finished called visited moved built "
          "cooked wrote studied danced played sang watched learned travelled ate").split()
_PP = ("won lost left helped started finished called moved built cooked played watched "
       "learned tried gone done seen made said known taken called").split()
_ADJ_ER = ("bigger smaller faster slower taller shorter older younger richer stronger "
           "warmer colder louder quieter cheaper nicer braver kinder").split()
_NAME = "anna ben sara tom maria john lucy sam emma dave mike kate".split()
_PLACE = "park store office gym beach city market station library cafe shop airport".split()
_SUBJ_PL = "we they people students teachers kids friends parents neighbours children".split()
_SUBJ_SG = "i you he she".split()
_SUBJ_ANY = _SUBJ_PL + _SUBJ_SG + _NAME

_SLOTS = {
    "n": _NOUN, "np": _NOUN_PL, "a": _ADJ, "vg": _VING, "v": _VERB, "pv": _PVERB,
    "pp": _PP, "ae": _ADJ_ER, "nm": _NAME, "pl": _PLACE,
    "sp": _SUBJ_PL, "sa": _SUBJ_ANY,
}

_CONFUSIONS = {
    "their": ["there", "they're"], "there": ["their", "they're"],
    "they're": ["their", "there"],
    "your": ["you're"], "you're": ["your"],
    "its": ["it's"], "it's": ["its"],
    "then": ["than"], "than": ["then"],
    "to": ["too"], "too": ["to"],
    "could have": ["could of"],
}

# canonical (most-common) member the context-free script falls back to per family
_CANON = {
    "their": "there", "there": "there", "they're": "there",
    "your": "your", "you're": "your",
    "its": "its", "it's": "its",
    "then": "then", "than": "then",
    "to": "to", "too": "to",
}

_FRAMES = [
    ("their {n}", "their"), ("their {a} {n}", "their"), ("{sp} love their {np}", "their"),
    ("{nm} forgot their {n}", "their"), ("their {np} are {a}", "their"),
    ("i saw their {n}", "their"),
    ("there is a {n}", "there"), ("there are {np}", "there"), ("over there", "there"),
    ("{sa} went there", "there"), ("{nm} sat there", "there"), ("put it there", "there"),
    ("there it is", "there"),
    ("they're {vg}", "they're"), ("they're {a}", "they're"),
    ("they're going to {v}", "they're"), ("they're not {a}", "they're"),
    ("i think they're {a}", "they're"), ("they're here", "they're"),
    ("your {n}", "your"), ("your {a} {n}", "your"), ("i like your {n}", "your"),
    ("your {np} are {a}", "your"), ("is your {n} ready", "your"),
    ("you're {vg}", "you're"), ("you're {a}", "you're"),
    ("you're going to {v}", "you're"), ("you're the best", "you're"),
    ("i think you're {a}", "you're"), ("you're right", "you're"),
    ("its {n}", "its"), ("the {n} lost its {n}", "its"), ("every {n} has its {n}", "its"),
    ("its {n} is {a}", "its"), ("the dog wagged its tail", "its"),
    ("it's {a}", "it's"), ("it's {vg}", "it's"), ("it's a {n}", "it's"),
    ("it's going to {v}", "it's"), ("it's late", "it's"), ("i think it's {a}", "it's"),
    ("and then {sa} {pv}", "then"), ("back then", "then"), ("{sa} {pv} then left", "then"),
    ("see you then", "then"), ("first this then that", "then"),
    ("we {pv} then went home", "then"),
    ("{ae} than before", "than"), ("more {a} than {nm}", "than"),
    ("better than ever", "than"), ("{nm} is {ae} than {nm}", "than"),
    ("rather than wait", "than"), ("it is {ae} than that", "than"),
    ("go to the {pl}", "to"), ("time to {v}", "to"), ("{sp} want to {v}", "to"),
    ("talk to {nm}", "to"), ("back to work", "to"), ("i need to {v}", "to"),
    ("too {a}", "too"), ("{a} too", "too"), ("me too", "too"), ("way too {a}", "too"),
    ("it is too {a} to {v}", "too"), ("{nm} came too", "too"),
    ("{sa} could have {pp}", "could have"), ("{nm} could have {pp}", "could have"),
    ("we could have won", "could have"), ("you could have {pp}", "could have"),
    ("it could have {pp}", "could have"),
]

_PREFIX = "honestly well look ok hey so".split()
_SUFFIX = ["today", "again", "right now", "this time", "for sure", "i guess", "as well"]


def _pick(rng, seq):
    return seq[int(rng.integers(len(seq)))]


def _fill(rng, template):
    out = template
    while "{" in out:
        a = out.index("{")
        b = out.index("}")
        key = out[a + 1:b]
        out = out[:a] + _pick(rng, _SLOTS[key]) + out[b + 1:]
    return out


def _correct_phrase(rng):
    template, target = _FRAMES[int(rng.integers(len(_FRAMES)))]
    phrase = _fill(rng, template)
    if rng.random() < 0.25:
        pre = _pick(rng, _PREFIX)
        if pre != phrase.split()[0]:
            phrase = pre + " " + phrase
    if rng.random() < 0.25:
        suf = _pick(rng, _SUFFIX)
        if suf.split()[0] != phrase.split()[-1]:
            phrase = phrase + " " + suf
    return phrase, target


def _double_a_word(rng, phrase):
    words = phrase.split()
    i = int(rng.integers(len(words)))
    return " ".join(words[:i + 1] + [words[i]] + words[i + 1:])


def _swap_target(phrase, target, wrong):
    toks = phrase.split()
    tgt = target.split()
    for i in range(len(toks) - len(tgt) + 1):
        if toks[i:i + len(tgt)] == tgt:
            return " ".join(toks[:i] + wrong.split() + toks[i + len(tgt):])
    return phrase


def _corrupt(rng, phrase, target):
    if rng.random() < 0.75 and target in _CONFUSIONS:
        wrong = _pick(rng, _CONFUSIONS[target])
        return _swap_target(phrase, target, wrong)
    return _double_a_word(rng, phrase)


def proofread_pairs(seed, n):
    """`n` deterministic (prompt, corrected-phrase) pairs from `seed`. ~15% identity."""
    rng = np.random.default_rng(seed)
    out = []
    while len(out) < n:
        correct, target = _correct_phrase(rng)
        if rng.random() < 0.15:
            inp = correct
        else:
            inp = _corrupt(rng, correct, target)
            if inp == correct:
                inp = _double_a_word(rng, correct)
        if len(inp) + 4 + len(correct) + 1 > 64:
            continue
        out.append((f"{inp} => ", correct))
    return out


def naive_fix(phrase):
    """The best context-free script: drop adjacent doubled words + normalise every
    homophone to the most common member of its family. Fixes doubled words and
    could-of perfectly; cannot resolve context-dependent homophones."""
    toks = phrase.split()
    dedup = []
    for t in toks:
        if not dedup or dedup[-1] != t:
            dedup.append(t)
    out, i = [], 0
    while i < len(dedup):
        two = " ".join(dedup[i:i + 2])
        if two in ("could of", "could have"):
            out += ["could", "have"]; i += 2
        elif dedup[i] in _CANON:
            out.append(_CANON[dedup[i]]); i += 1
        else:
            out.append(dedup[i]); i += 1
    return " ".join(out)


# Hand-written natural phrases NOT drawn from the frames — the honest generalisation
# test. (input, gold-correction). Mix of every confusion + identity (leave-alone) cases.
OOD_CASES = [
    ("their going to win", "they're going to win"),
    ("your the best", "you're the best"),
    ("its raining again", "it's raining again"),
    ("the the cat sat", "the cat sat"),
    ("i could of helped", "i could have helped"),
    ("this is bigger then that", "this is bigger than that"),
    ("it is to late", "it is too late"),
    ("they're house is big", "their house is big"),
    ("we went too the park", "we went to the park"),
    ("your going to love it", "you're going to love it"),
    ("its a great day", "it's a great day"),
    ("there going home", "they're going home"),
    ("i like there car", "i like their car"),
    ("i want too help", "i want to help"),
    ("me to", "me too"),
    ("she is faster then me", "she is faster than me"),
    ("you could of won", "you could have won"),
    ("its been a long day", "it's been a long day"),
    ("their not ready", "they're not ready"),
    ("put it over their", "put it over there"),
    ("she is happy today", "she is happy today"),
    ("the dog lost its bone", "the dog lost its bone"),
    ("your dog is cute", "your dog is cute"),
    ("they're going to win", "they're going to win"),
    ("it is too late", "it is too late"),
]