| """SIEVE dataset builders. Fixed seeds; generated data shipped to data/ as json. |
| |
| - load_corpus(n, seed): plain sentences from the on-box v2/SENT corpus |
| (HF repo nickypro/sonar-sae, data_v2/sent_texts_*.json). Falls back to a |
| shipped json snapshot when HF / network is unavailable. |
| - entity_presence_pairs(seed, n): cross-paraphrase entity-presence pairs. |
| - active_passive_set(seed, n): agent/patient active+passive constructions. |
| |
| Every builder writes its output to data/<name>.json and reads it back on |
| subsequent calls so runs are reproducible without the corpus. |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import os |
| import random |
|
|
| DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") |
| HF_REPO = "nickypro/sonar-sae" |
|
|
|
|
| def _data_path(name: str) -> str: |
| os.makedirs(DATA_DIR, exist_ok=True) |
| return os.path.join(DATA_DIR, name) |
|
|
|
|
| |
| |
| |
| |
| BUNDLED_CORPUS = "corpus_bundled.json" |
|
|
|
|
| |
| |
| |
| |
| |
| |
| def load_corpus(n: int = 2000, min_words: int = 5, max_words: int = 60, |
| seed: int = 0, max_shards: int = 6, cache_name: str = None): |
| """Return list[str] of sentences. Caches to data/corpus_<n>_<seed>.json.""" |
| cache_name = cache_name or f"corpus_{n}_{seed}.json" |
| fp = _data_path(cache_name) |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
|
|
| |
| try: |
| texts = _load_corpus_from_hf(n, min_words, max_words, seed, max_shards) |
| except Exception: |
| texts = _load_corpus_from_bundle(n, min_words, max_words, seed) |
|
|
| with open(fp, "w") as f: |
| json.dump(texts, f) |
| return texts |
|
|
|
|
| def _load_corpus_from_bundle(n, min_words, max_words, seed): |
| """Offline fallback: subsample the shipped bundled snapshot, honoring the |
| word-length band so each task gets the distribution it expects. Deterministic |
| in (n, seed). If fewer than n sentences pass the band filter we return all of |
| them (so a run never crashes for lack of corpus).""" |
| fp = _data_path(BUNDLED_CORPUS) |
| if not os.path.exists(fp): |
| raise RuntimeError( |
| "Corpus unavailable: no HF access, no cached corpus_<n>_<seed>.json, " |
| f"and the bundled snapshot {BUNDLED_CORPUS} is missing. Re-clone the " |
| "repo (the snapshot ships in data/) or run on a box with HF access.") |
| with open(fp) as f: |
| pool = json.load(f) |
| seen, cands = set(), [] |
| for s in pool: |
| s = " ".join(str(s).split()) |
| if not (min_words <= len(s.split()) <= max_words): |
| continue |
| if s in seen: |
| continue |
| seen.add(s) |
| cands.append(s) |
| if len(cands) > n: |
| cands = random.Random(seed).sample(cands, n) |
| return cands |
|
|
|
|
| def _load_corpus_from_hf(n, min_words, max_words, seed, max_shards): |
| from huggingface_hub import hf_hub_download |
| cands, seen = [], set() |
| for shard in range(max_shards): |
| try: |
| p = hf_hub_download(HF_REPO, f"data_v2/sent_texts_{shard:03d}.json", |
| repo_type="model") |
| except Exception: |
| break |
| rows = json.load(open(p)) |
| for s in rows: |
| s = " ".join(str(s).split()) |
| if not (min_words <= len(s.split()) <= max_words): |
| continue |
| if s in seen: |
| continue |
| seen.add(s) |
| cands.append(s) |
| if len(cands) >= 4 * n: |
| break |
| if not cands: |
| raise RuntimeError( |
| "Corpus unavailable (no HF access and no shipped snapshot). " |
| "Run on the GPU box or ship data/corpus_*.json.") |
| if len(cands) > n: |
| cands = random.Random(seed).sample(cands, n) |
| return cands |
|
|
|
|
| |
| |
| |
| |
| |
| _ENTITIES = [ |
| "Amazon", "Google", "Tesla", "NASA", "Berlin", "Tokyo", "Brazil", |
| "Microsoft", "Paris", "Canada", "Sony", "Egypt", "Apple", "Norway", |
| "Reuters", "Oxford", "Boeing", "Kenya", "Toyota", "Sweden", |
| ] |
|
|
| _TEMPLATES = [ |
| "The report mentioned that {E} announced a new initiative on Tuesday.", |
| "According to officials, {E} will expand operations next year.", |
| "Analysts said {E} had outperformed expectations this quarter.", |
| "It was reported that {E} signed a major agreement abroad.", |
| "Sources confirmed {E} plans to invest heavily in research.", |
| "{E} was at the center of the discussion during the summit.", |
| ] |
|
|
| |
| _DISTRACTORS = ["Spain", "Intel", "Chile", "Ford", "London", "India"] |
|
|
|
|
| def entity_presence_pairs(seed: int = 0, n: int = 300): |
| """Build entity-presence items for ONE fixed target entity, tested |
| cross-paraphrase. label=1 -> the target entity is present (in some |
| template surface form); label=0 -> a different entity fills that slot. |
| |
| Key design: the SAME target entity appears across many templates, and the |
| cross-paraphrase split holds out TEMPLATES (surface forms), not entities. |
| So a probe must read "target entity present" robustly to wording. The |
| `pid` field is the template id (the paraphrase axis to hold out). |
| Returns list of dicts: {text, entity(target), label, pid(template id)}. |
| """ |
| fp = _data_path(f"entity_presence_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
|
|
| rng = random.Random(seed) |
| target = _ENTITIES[0] |
| negatives = [e for e in _ENTITIES[1:] + _DISTRACTORS if e != target] |
| items = [] |
| |
| reps = max(1, n // (2 * len(_TEMPLATES)) + 1) |
| for pid, tmpl in enumerate(_TEMPLATES): |
| for _ in range(reps): |
| items.append({"text": tmpl.format(E=target), "entity": target, |
| "label": 1, "pid": pid}) |
| neg = rng.choice(negatives) |
| items.append({"text": tmpl.format(E=neg), "entity": target, |
| "label": 0, "pid": pid}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| |
| _PEOPLE = [ |
| "Maria", "John", "Aisha", "Carlos", "Yuki", "Omar", "Elena", "David", |
| "Priya", "Tom", "Sofia", "Hassan", "Anna", "Wei", "Lucas", "Fatima", |
| ] |
| _VERBS = [ |
| ("chased", "was chased by"), |
| ("helped", "was helped by"), |
| ("called", "was called by"), |
| ("praised", "was praised by"), |
| ("blamed", "was blamed by"), |
| ("followed", "was followed by"), |
| ("warned", "was warned by"), |
| ("rescued", "was rescued by"), |
| ] |
|
|
|
|
| def active_passive_set(seed: int = 0, n: int = 300): |
| """Build agent/patient pairs in both active and passive voice. |
| |
| For each (agent A, patient P, verb v): |
| active: "A v_active P." agent=A patient=P |
| passive: "P v_passive A." agent=A patient=P (same semantics) |
| We emit, per sentence, two query rows: query=agent (is_agent=1) and |
| query=patient (is_agent=0). The benchmark trains on active, tests on |
| passive: a surface-position probe nails active but fails passive. |
| """ |
| fp = _data_path(f"active_passive_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
|
|
| items = _build_active_passive(seed, n) |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| NATURAL_ROLE_FILE = "natural_relational_role.json" |
|
|
|
|
| def natural_role_set(seed: int = 0, n: int = None): |
| """Return list of QUERY-row dicts for the natural relational role task. |
| |
| Each underlying sentence yields TWO rows (one per entity), so a probe that |
| is handed (z_sentence, entity_pointer) must decide whether the POINTED-TO |
| entity is the agent. Rows: |
| {text, construction, agent, patient, query_entity, is_agent(1/0), sid} |
| Constructions: active / passive / relcl / ditrans / fronted (natural syntax). |
| """ |
| fp = _data_path(NATURAL_ROLE_FILE) |
| if not os.path.exists(fp): |
| raise RuntimeError( |
| f"natural role dataset {NATURAL_ROLE_FILE} missing; regenerate on a " |
| "box with spaCy + HF corpus (see gen_natural_relational).") |
| with open(fp) as f: |
| blob = json.load(f) |
| sents = blob["items"] if isinstance(blob, dict) else blob |
| items = [] |
| for sid, s in enumerate(sents): |
| ag, pa = s["agent"], s["patient"] |
| constr = s["constr"] |
| for q, lab in [(ag, 1), (pa, 0)]: |
| items.append({"text": s["sent"], "construction": constr, |
| "agent": ag, "patient": pa, "query_entity": q, |
| "is_agent": lab, "verb": s.get("verb", ""), "sid": sid}) |
| if n is not None and len(items) > n: |
| |
| keep_sids = set(range(n // 2)) |
| items = [it for it in items if it["sid"] in keep_sids] |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| _NUM_TEMPLATES = [ |
| "The company reported {V} million dollars in revenue this quarter.", |
| "Officials confirmed that {V} people attended the event downtown.", |
| "The bridge spans {V} meters across the river valley.", |
| "Researchers measured a temperature of {V} degrees during the test.", |
| "The team scored {V} points in the final match of the season.", |
| "About {V} students enrolled in the program this year.", |
| "The shipment weighed {V} kilograms when it arrived at the port.", |
| "Investors poured {V} thousand into the new startup last month.", |
| ] |
|
|
|
|
| def number_exact_set(seed: int = 0, n: int = 300): |
| """Build sentences with a single explicit integer value, varied across |
| templates. label=the integer. Cross-template held-out so the probe reads |
| the NUMBER not a template-specific artifact. Returns list of dicts: |
| {text, value, pid}.""" |
| fp = _data_path(f"number_exact_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| |
| |
| while len(items) < n: |
| pid = rng.randrange(len(_NUM_TEMPLATES)) |
| v = rng.randint(1, 999) |
| items.append({"text": _NUM_TEMPLATES[pid].format(V=v), |
| "value": int(v), "pid": pid}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| |
| _NEG_SUBJECTS = ["the manager", "the doctor", "the pilot", "the teacher", |
| "the engineer", "the lawyer", "the nurse", "the chef"] |
| _NEG_PREDS = [("approved the plan", "did not approve the plan"), |
| ("signed the contract", "did not sign the contract"), |
| ("attended the meeting", "did not attend the meeting"), |
| ("finished the report", "did not finish the report"), |
| ("accepted the offer", "did not accept the offer"), |
| ("passed the exam", "did not pass the exam")] |
| _NEG_DISTRACT = ["while the office stayed open", "as the day went on", |
| "before the deadline arrived", "after the call ended", |
| "during the long afternoon", "once the rain stopped"] |
|
|
|
|
| def negation_scope_set(seed: int = 0, n: int = 300): |
| """Build clauses that are affirmed/negated. Positive label = the TARGET |
| clause is negated. To make the surface 'not'-detector NON-trivial as a |
| *scope* test, half the negatives put 'not/no' into the distractor clause |
| (negation present but NOT scoping the target) so a pure not-detector is |
| fooled. Returns list of dicts: {text, neg_present, neg_scopes_target, |
| label, pid}.""" |
| fp = _data_path(f"negation_scope_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| distract_neg = {"while the office stayed open": "while the office was not open", |
| "as the day went on": "as nothing went on", |
| "before the deadline arrived": "before no deadline arrived", |
| "after the call ended": "after the call had not ended", |
| "during the long afternoon": "during no long afternoon", |
| "once the rain stopped": "once the rain had not stopped"} |
| while len(items) < n: |
| subj = rng.choice(_NEG_SUBJECTS) |
| pid = rng.randrange(len(_NEG_PREDS)) |
| aff, neg = _NEG_PREDS[pid] |
| dist = rng.choice(_NEG_DISTRACT) |
| kind = rng.randrange(3) |
| if kind == 0: |
| clause = neg |
| d = dist |
| label, np_, scope = 1, 1, 1 |
| elif kind == 1: |
| clause = aff |
| d = dist |
| label, np_, scope = 0, 0, 0 |
| else: |
| clause = aff |
| d = distract_neg[dist] |
| label, np_, scope = 0, 1, 0 |
| text = f"{subj.capitalize()} {clause}, {d}." |
| items.append({"text": text, "neg_present": np_, |
| "neg_scopes_target": scope, "label": label, "pid": pid}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| _COREF_MALE = ["John", "David", "Carlos", "Omar", "Lucas", "Tom", "Hassan", "Wei"] |
| _COREF_FEMALE = ["Maria", "Aisha", "Elena", "Priya", "Sofia", "Anna", "Fatima", "Yuki"] |
| _COREF_ACTIONS = [ |
| ("went to the office early", "was very tired"), |
| ("finished the project", "felt proud"), |
| ("called the client", "needed more time"), |
| ("read the long report", "took a short break"), |
| ("led the meeting", "answered every question"), |
| ("drove to the airport", "missed the flight"), |
| ] |
|
|
|
|
| def coreference_set(seed: int = 0, n: int = 300): |
| """Build two-clause passages: 'A and B did X; PRONOUN did Y.' One of A,B is |
| male and the other female; the pronoun (he/she) disambiguates the referent |
| by gender. The query asks 'does the pronoun refer to <entity>'. The label |
| flips with the pronoun, NOT with surface position (we randomize which of |
| A,B is male and which slot they occupy). So a surface-position / bag reader |
| cannot solve it; it needs to bind pronoun->name. Returns list of dicts: |
| {text, query_entity, label, pid, pronoun}.""" |
| fp = _data_path(f"coreference_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| while len(items) < n: |
| m = rng.choice(_COREF_MALE) |
| f_ = rng.choice(_COREF_FEMALE) |
| pid = rng.randrange(len(_COREF_ACTIONS)) |
| x, y = _COREF_ACTIONS[pid] |
| |
| if rng.random() < 0.5: |
| first, second = m, f_ |
| else: |
| first, second = f_, m |
| |
| if rng.random() < 0.5: |
| referent, pron = m, "He" |
| else: |
| referent, pron = f_, "She" |
| text = f"{first} and {second} {x}; {pron} {y}." |
| |
| for cand in (first, second): |
| items.append({"text": text, "query_entity": cand, |
| "label": 1 if cand == referent else 0, |
| "pid": pid, "pronoun": pron}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| _LEN_TARGET = "Tesla" |
| _LEN_OTHERS = ["Amazon", "Google", "NASA", "Sony", "Boeing", "Toyota", |
| "Microsoft", "Apple", "Reuters", "Oxford"] |
| _LEN_FILLERS = [ |
| "after a long and detailed discussion among the senior staff", |
| "despite repeated concerns raised by several independent analysts", |
| "while markets remained volatile across most major global regions", |
| "as the quarterly figures were finally released to the public", |
| "following months of careful negotiation behind closed doors", |
| "amid growing speculation from reporters covering the industry", |
| ] |
|
|
|
|
| def length_generalization_set(seed: int = 0, n: int = 300): |
| """Entity-presence items where SHORT sentences are ~6-9 words and LONG ones |
| ~18-24 words (extra clauses padded). The target entity sits at a RANDOM |
| content slot in both bins. Probe trains short, tests long. label=target |
| present. Returns list of dicts: {text, label, length_bin, n_words}.""" |
| fp = _data_path(f"length_gen_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| short_core = ["{E} announced new results today.", |
| "Reports said {E} expanded abroad.", |
| "Officials praised {E} this week.", |
| "{E} opened a new facility."] |
| while len(items) < n: |
| bin_ = "short" if (len(items) % 2 == 0) else "long" |
| present = rng.random() < 0.5 |
| ent = _LEN_TARGET if present else rng.choice(_LEN_OTHERS) |
| core = rng.choice(short_core).format(E=ent) |
| if bin_ == "short": |
| text = core |
| else: |
| f1 = rng.choice(_LEN_FILLERS) |
| f2 = rng.choice([x for x in _LEN_FILLERS if x != f1]) |
| text = f"{core[:-1]}, {f1}, {f2}." |
| items.append({"text": text, "label": 1 if present else 0, |
| "length_bin": bin_, "n_words": len(text.split())}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| import re as _re |
|
|
| _R9_POS = set("good great excellent wonderful happy joy love loved beautiful best better positive success win won amazing fantastic perfect nice enjoy glad pleased proud hope bright warm kind peace safe healthy strong rich free fun fine clear smart wise true honest".split()) |
| _R9_NEG = set("bad terrible awful horrible miserable disgusting sad angry hate hated fear afraid worst worse negative fail failed failure loss lost pain hurt cruel ugly poor weak sick ill dead death kill killed war violence danger dangerous wrong false dark cold dirty broken difficult hard struggle suffer crisis problem disaster threat guilty corrupt".split()) |
| _R9_NEGATORS = set("not no never none nobody nothing nowhere neither nor cannot can't won't don't doesn't didn't isn't aren't wasn't weren't hasn't haven't without".split()) |
| _R9_P1 = set("i me my mine we us our ours myself ourselves".split()) |
| _R9_P2 = set("you your yours yourself yourselves".split()) |
| _R9_P3 = set("he him his she her hers it its they them their theirs himself herself itself themselves".split()) |
| _R9_FUT = set("will shall going gonna".split()) |
| _R9_PRES = set("is are am do does has have being".split()) |
| _R9_PAST = set("was were did had been".split()) |
| _R9_NUMW = set("one two three four five six seven eight nine ten eleven twelve hundred thousand million billion".split()) |
| _R9_STARTERS = set("the a an this that these those it he she they we you i but and or so if when while".split()) |
| _R9_tokre = _re.compile(r"[A-Za-z']+|[0-9]+|[.,!?;:]") |
| _R9_wordre = _re.compile(r"[A-Za-z']+") |
|
|
|
|
| def readable_props(text): |
| """Scalar readable-property features (sentiment/tense/number/negation/ |
| coref/NE/discourse/length). Ported from tae_r9_meaning_coverage.""" |
| toks = _R9_tokre.findall(text) |
| ws = [t.lower() for t in toks if _R9_wordre.fullmatch(t)] |
| n = max(len(ws), 1) |
| f = {} |
| pos = sum(w in _R9_POS for w in ws); neg = sum(w in _R9_NEG for w in ws) |
| f["sent_pos"] = pos / n; f["sent_neg"] = neg / n |
| f["sent_signed"] = (pos - neg) / n; f["sent_mag"] = (pos + neg) / n |
| f["tense_ed"] = sum(bool(_re.fullmatch(r"[a-z]+ed", w)) for w in ws) / n |
| f["tense_ing"] = sum(bool(_re.fullmatch(r"[a-z]+ing", w)) for w in ws) / n |
| f["tense_aux_past"] = sum(w in _R9_PAST for w in ws) / n |
| f["tense_aux_pres"] = sum(w in _R9_PRES for w in ws) / n |
| f["tense_fut"] = sum(w in _R9_FUT for w in ws) / n |
| f["num_plural"] = sum(bool(_re.fullmatch(r"[a-z]{3,}s", w)) and not w.endswith("ss") for w in ws) / n |
| f["num_words"] = sum(w in _R9_NUMW for w in ws) / n |
| f["num_digits"] = sum(bool(_re.fullmatch(r"[0-9]+", t)) for t in toks) / n |
| neg_idx = [i for i, w in enumerate(ws) if w in _R9_NEGATORS] |
| f["neg_present"] = 1.0 if neg_idx else 0.0 |
| f["neg_count"] = len(neg_idx) / n |
| f["neg_first_pos"] = (neg_idx[0] / n) if neg_idx else -1.0 |
| f["pron_1"] = sum(w in _R9_P1 for w in ws) / n |
| f["pron_2"] = sum(w in _R9_P2 for w in ws) / n |
| f["pron_3"] = sum(w in _R9_P3 for w in ws) / n |
| f["pron_any"] = 1.0 if any(w in _R9_P1 | _R9_P2 | _R9_P3 for w in ws) else 0.0 |
| caps_mid = sum(1 for i, t in enumerate(toks) |
| if _R9_wordre.fullmatch(t) and t[0].isupper() and i > 0 |
| and t.lower() not in _R9_STARTERS) |
| f["ne_caps"] = caps_mid / n |
| f["len_tok"] = len(ws) / 30.0 |
| f["punc_comma"] = sum(t == "," for t in toks) / n |
| f["punc_q"] = 1.0 if "?" in toks else 0.0 |
| f["punc_excl"] = 1.0 if "!" in toks else 0.0 |
| return f |
|
|
|
|
| def _build_active_passive(seed, n): |
| """Role-balanced generator: for every (agent=A,patient=P,verb) we ALSO emit |
| the swapped (agent=P,patient=A) so each name is agent exactly as often as |
| patient. This removes any name-frequency confound from the query-entity |
| feature -> the query one-hot carries ZERO marginal signal, so only true |
| thematic binding (absent in a bag) could lift the score. Without this the |
| A6 'bag~chance' guarantee leaks ~0.67 via name priors.""" |
| rng = random.Random(seed) |
| items = [] |
| sid = 0 |
| while len(items) < n: |
| a, p = rng.sample(_PEOPLE, 2) |
| v_act, v_pas = rng.choice(_VERBS) |
| for ag, pa in [(a, p), (p, a)]: |
| active = f"{ag} {v_act} {pa}." |
| passive = f"{pa} {v_pas} {ag}." |
| for text, constr in [(active, "active"), (passive, "passive")]: |
| items.append({"text": text, "construction": constr, |
| "agent": ag, "patient": pa, |
| "query_entity": ag, "is_agent": 1, "sid": sid}) |
| items.append({"text": text, "construction": constr, |
| "agent": ag, "patient": pa, |
| "query_entity": pa, "is_agent": 0, "sid": sid}) |
| sid += 1 |
| rng.shuffle(items) |
| return items[:n] |
|
|
|
|
| |
| |
| |
| |
|
|
| |
| |
| _STOP = { |
| "the", "a", "an", "of", "to", "in", "on", "at", "and", "or", "but", "for", |
| "with", "as", "by", "that", "this", "it", "its", "is", "was", "were", "are", |
| "be", "been", "had", "has", "have", "will", "would", "can", "could", "from", |
| "their", "his", "her", "they", "he", "she", "we", "you", "i", "into", "out", |
| "up", "down", "over", "than", "then", "so", "if", "not", "no", "do", "did", |
| "during", "next", "new", "said", "according", "reported", "confirmed", |
| } |
|
|
|
|
| def content_words(text: str): |
| """Lowercased content tokens (stopwords + pure punctuation removed).""" |
| out = [] |
| for w in text.split(): |
| c = w.strip(".,!?;:'\"()").lower() |
| if c and c not in _STOP and not c.isdigit(): |
| out.append(c) |
| return out |
|
|
|
|
| def sentence_from_words_items(seed: int = 0, n: int = 300, smoke: bool = False): |
| """C1: take real corpus sentences, expose their SHUFFLED content words. |
| item: {gold(sentence), words(shuffled content words)}.""" |
| fp = _data_path(f"words_from_sentence_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| sents = load_corpus(n=max(n * 3, 600), seed=seed, min_words=6, max_words=22) |
| rng = random.Random(seed) |
| items = [] |
| for s in sents: |
| w = content_words(s) |
| if not (3 <= len(w) <= 12): |
| continue |
| ws = w[:] |
| rng.shuffle(ws) |
| items.append({"gold": s, "words": ws}) |
| if len(items) >= n: |
| break |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| _VOCAB_BANDS = { |
| "high": ["time", "people", "world", "year", "government", "country", "city", |
| "company", "water", "money", "school", "family", "music", "story", |
| "market", "health", "system", "power", "energy", "history"], |
| "mid": ["glacier", "senator", "vaccine", "harvest", "orchestra", "comet", |
| "monastery", "turbine", "sediment", "ballot", "pension", "refinery", |
| "antibody", "tournament", "estuary", "manuscript", "satellite", |
| "diplomat", "fossil", "canyon"], |
| "low": ["quokka", "obsidian", "samovar", "zephyr", "philately", "tessellation", |
| "quasar", "marzipan", "cassowary", "gabbro", "petrichor", "syzygy", |
| "thylacine", "cuneiform", "axolotl", "mycelium", "narwhal", |
| "basalt", "lemur", "lichen"], |
| } |
|
|
|
|
| def vocab_set(seed: int = 0, n: int = None, smoke: bool = False): |
| """C2: frequency-stratified single words. item: {word, band}.""" |
| fp = _data_path("vocab_bands.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| items = json.load(f) |
| else: |
| items = [] |
| for band, words in _VOCAB_BANDS.items(): |
| for w in words: |
| items.append({"word": w, "band": band}) |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| if n: |
| items = items[:n] |
| return items |
|
|
|
|
| |
| |
| CONCEPT_LEX = { |
| "money": ["money", "dollars", "price", "cost", "bank", "profit", "loan", |
| "tax", "salary", "budget", "investment", "cash", "payment"], |
| "location": ["city", "country", "mountain", "river", "ocean", "village", |
| "street", "border", "island", "desert", "forest", "harbor"], |
| "time": ["yesterday", "tomorrow", "century", "morning", "evening", "decade", |
| "hour", "minute", "week", "month", "summer", "winter", "midnight"], |
| "weather": ["rain", "storm", "snow", "sunny", "wind", "cloud", "fog", |
| "thunder", "drought", "humid", "frost", "hail"], |
| } |
|
|
|
|
| def _concept_label(text, concept): |
| toks = set(content_words(text)) |
| return int(any(w in toks for w in CONCEPT_LEX[concept])) |
|
|
|
|
| def concept_corpus(seed: int = 0, n: int = 600, concept: str = "money", |
| smoke: bool = False): |
| """D4/D5: corpus sentences labeled present/absent for `concept`. |
| item: {text, label}. Balanced positive/negative where possible.""" |
| fp = _data_path(f"concept_{concept}_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| sents = load_corpus(n=max(n * 5, 2000), seed=seed, min_words=6, max_words=30) |
| pos = [s for s in sents if _concept_label(s, concept)] |
| neg = [s for s in sents if not _concept_label(s, concept)] |
| rng = random.Random(seed) |
| rng.shuffle(pos) |
| rng.shuffle(neg) |
| half = n // 2 |
| pos, neg = pos[:half], neg[:half] |
| items = [{"text": s, "label": 1} for s in pos] + \ |
| [{"text": s, "label": 0} for s in neg] |
| rng.shuffle(items) |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| _SUBJECTS = ["The scientist", "A teacher", "The pilot", "A farmer", "The nurse", |
| "A lawyer", "The chef", "An artist", "The engineer", "A sailor"] |
| _PREDICATES = [ |
| "studied the ancient ruins", "repaired the old engine", |
| "planted rows of wheat", "painted a bright mural", |
| "measured the rising tide", "wrote a long letter", |
| "fixed the broken radio", "watered the green garden", |
| "counted the silver coins", "climbed the steep hill", |
| "read the thick report", "cooked a warm meal", |
| ] |
|
|
|
|
| def _clause(rng): |
| return f"{rng.choice(_SUBJECTS)} {rng.choice(_PREDICATES)}" |
|
|
|
|
| def three_sentence_items(seed: int = 0, n: int = 200, smoke: bool = False): |
| """D2/D3: 3-sentence docs with a clean swap/edit target. |
| item: {sents:[s1,s2,s3], text, s2_alt(replacement clause for s2)}.""" |
| fp = _data_path(f"three_sentence_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| for _ in range(n): |
| s = [_clause(rng) + "." for _ in range(3)] |
| |
| alt = _clause(rng) + "." |
| while alt == s[1]: |
| alt = _clause(rng) + "." |
| items.append({"sents": s, "text": " ".join(s), "s2_alt": alt}) |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| _LG_MONEY_SHORT = ["dollars", "salary", "budget", "loan", "profit", "tax", |
| "wages", "savings"] |
| _LG_MONEY_LONG = ["revenue", "investment", "dividend", "mortgage", "earnings", |
| "funding", "payroll", "subsidy", "capital", "pension"] |
| |
| |
| |
| _LG_OTHER_SHORT = ["garden", "mountain", "river", "forest", "harbor", "meadow", |
| "orchard", "valley"] |
| _LG_OTHER_LONG = ["glacier", "canyon", "tundra", "lagoon", "savanna", |
| "plateau", "estuary", "prairie", "fjord", "marsh"] |
|
|
| |
| |
| _LG_CARRIERS = [ |
| "the {W} was mentioned in the notes", |
| "they finally discussed the {W} today", |
| "a report described the {W} in brief", |
| "everyone kept asking about the {W}", |
| "the {W} came up during the review", |
| "she wrote a short line about the {W}", |
| ] |
|
|
|
|
| def length_generalization_set(seed: int = 0, n: int = 2000, smoke: bool = False): |
| """v0.3: money-concept presence embedded in REAL corpus context, binned |
| SHORT vs LONG, with DISJOINT short/long trigger vocab. Probe trains short, |
| tests long; the score measures whether the concept readout GENERALIZES across |
| length (binding-beyond-bag), normalized into the within-long ceiling.""" |
| fp = _data_path(f"length_gen_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| |
| try: |
| pool = load_corpus(n=max(n, 1500), seed=seed, min_words=8, max_words=22) |
| except Exception: |
| pool = [] |
| if len(pool) < 50: |
| |
| pool = [c.format(W=w) for c in _LG_CARRIERS |
| for w in ("meeting", "weather", "schedule", "building", "journey", |
| "festival", "machine", "lecture", "harvest", "voyage")] |
| rng.shuffle(pool) |
|
|
| def _make(label, length_bin): |
| money = (label == 1) |
| if length_bin == "short": |
| words = _LG_MONEY_SHORT if money else _LG_OTHER_SHORT |
| else: |
| words = _LG_MONEY_LONG if money else _LG_OTHER_LONG |
| w = rng.choice(words) |
| clause = rng.choice(_LG_CARRIERS).format(W=w) |
| if length_bin == "short": |
| text = clause[0].upper() + clause[1:] + "." |
| else: |
| |
| k = rng.randint(2, 3) |
| distractors = [pool[rng.randrange(len(pool))] for _ in range(k)] |
| parts = distractors[:] |
| insert = rng.randint(0, len(parts)) |
| parts.insert(insert, clause[0].upper() + clause[1:]) |
| text = ". ".join(p.rstrip(".") for p in parts) + "." |
| return {"text": text, "label": label, "length_bin": length_bin, |
| "trigger": w, "n_words": len(text.split())} |
|
|
| items = [] |
| while len(items) < n: |
| for length_bin in ("short", "long"): |
| items.append(_make(1, length_bin)) |
| items.append(_make(0, length_bin)) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| _MALE = ["John", "Carlos", "Omar", "David", "Tom", "Hassan", "Lucas", "Wei"] |
| _FEMALE = ["Maria", "Aisha", "Yuki", "Elena", "Priya", "Sofia", "Anna", "Fatima"] |
| _COREF_TMPL = [ |
| "{n1} met {n2} at the station, and {pro} carried the heavy suitcase.", |
| "{n1} sat near {n2} during lunch, then {pro} left the room quietly.", |
| "{n1} called {n2} last night because {pro} needed urgent help.", |
| "{n1} thanked {n2} warmly after {pro} finished the difficult task.", |
| ] |
|
|
|
|
| def coreference_set(seed: int = 0, n: int = 2000, smoke: bool = False): |
| fp = _data_path(f"coreference_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| items = [] |
| while len(items) < n: |
| male = rng.choice(_MALE) |
| female = rng.choice(_FEMALE) |
| |
| referent, pro = rng.choice([(male, "he"), (female, "she")]) |
| |
| n1, n2 = (male, female) if rng.random() < 0.5 else (female, male) |
| tmpl = rng.choice(_COREF_TMPL) |
| text = tmpl.format(n1=n1, n2=n2, pro=pro) |
| |
| other = female if referent == male else male |
| items.append({"text": text, "query_entity": referent, "label": 1}) |
| items.append({"text": text, "query_entity": other, "label": 0}) |
| rng.shuffle(items) |
| items = items[:n] |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
|
|
|
|
| |
| def svo_swap_items(seed: int = 0, n: int = 200, smoke: bool = False): |
| """D6: single-sentence SVO docs with a role-swapped counterfactual. |
| item: {text(A V B), swapped(B V A), agent, patient, verb}.""" |
| fp = _data_path(f"svo_swap_{n}_{seed}.json") |
| if os.path.exists(fp): |
| with open(fp) as f: |
| return json.load(f) |
| rng = random.Random(seed) |
| verbs = ["helped", "chased", "called", "praised", "blamed", "followed", |
| "warned", "rescued", "questioned", "interviewed"] |
| items = [] |
| for _ in range(n): |
| a, p = rng.sample(_PEOPLE, 2) |
| v = rng.choice(verbs) |
| items.append({"text": f"{a} {v} {p}.", "swapped": f"{p} {v} {a}.", |
| "agent": a, "patient": p, "verb": v}) |
| with open(fp, "w") as f: |
| json.dump(items, f) |
| return items |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _WORD_EDIT_PAIRS = [ |
| |
| {"x": "river", "y": "mountain", "pos": "noun", "axis": "concrete"}, |
| {"x": "doctor", "y": "teacher", "pos": "noun", "axis": "concrete"}, |
| {"x": "engine", "y": "garden", "pos": "noun", "axis": "concrete"}, |
| {"x": "castle", "y": "harbor", "pos": "noun", "axis": "concrete"}, |
| {"x": "winter", "y": "summer", "pos": "noun", "axis": "concrete"}, |
| |
| {"x": "freedom", "y": "justice", "pos": "noun", "axis": "abstract"}, |
| {"x": "sorrow", "y": "courage", "pos": "noun", "axis": "abstract"}, |
| {"x": "wisdom", "y": "silence", "pos": "noun", "axis": "abstract"}, |
| |
| {"x": "glacier", "y": "savanna", "pos": "noun", "axis": "rare"}, |
| {"x": "obsidian", "y": "marble", "pos": "noun", "axis": "rare"}, |
| {"x": "monastery", "y": "laboratory", "pos": "noun", "axis": "rare"}, |
| {"x": "diplomat", "y": "merchant", "pos": "noun", "axis": "rare"}, |
| {"x": "turbine", "y": "lantern", "pos": "noun", "axis": "rare"}, |
| |
| {"x": "increased", "y": "decreased", "pos": "verb", "axis": "antonym"}, |
| {"x": "praised", "y": "criticized", "pos": "verb", "axis": "antonym"}, |
| {"x": "arrived", "y": "departed", "pos": "verb", "axis": "antonym"}, |
| |
| {"x": "ancient", "y": "modern", "pos": "adj", "axis": "antonym"}, |
| {"x": "bright", "y": "gloomy", "pos": "adj", "axis": "antonym"}, |
| {"x": "fragile", "y": "sturdy", "pos": "adj", "axis": "antonym"}, |
| ] |
|
|
| |
| _WORD_EDIT_CARRIERS = { |
| "noun": [ |
| "The {W} was clearly visible from the road.", |
| "Everyone in the town talked about the {W} that morning.", |
| "She wrote a long essay describing the {W} in detail.", |
| "According to the guide, the {W} drew many visitors.", |
| "They spent the afternoon thinking about the {W}.", |
| "A short article mentioned the {W} near the end.", |
| ], |
| "verb": [ |
| "The visitors {W} just before noon on Tuesday.", |
| "Reports say the workers {W} during the long meeting.", |
| "Last spring the travellers {W} without much warning.", |
| "Officials noted that the numbers {W} that year.", |
| "Quietly, the guests {W} as the evening went on.", |
| "By midday the crowd {W} along the river.", |
| ], |
| "adj": [ |
| "The building looked remarkably {W} from the hill.", |
| "Visitors found the old hall surprisingly {W} inside.", |
| "Critics described the new design as rather {W}.", |
| "The whole valley seemed {W} in the morning light.", |
| "Reporters called the proposal unexpectedly {W}.", |
| "Everyone agreed the room felt {W} that day.", |
| ], |
| } |
|
|
|
|
| def word_edit_pairs(seed: int = 0, n: int = None, smoke: bool = False): |
| """D1: diverse word-swap pairs (noun/verb/adj, concrete/abstract/rare) in |
| POS-appropriate carriers. Returns dict with `pairs` (the X->Y definitions) |
| and `items` (per-pair, per-carrier instantiations). item: |
| {x_text, y_text, x, y, pos, axis, cid}.""" |
| pairs = _WORD_EDIT_PAIRS |
| items = [] |
| for p in pairs: |
| for cid, carrier in enumerate(_WORD_EDIT_CARRIERS[p["pos"]]): |
| items.append({ |
| "x_text": carrier.format(W=p["x"]), |
| "y_text": carrier.format(W=p["y"]), |
| "x": p["x"], "y": p["y"], "pos": p["pos"], "axis": p["axis"], |
| "cid": cid, |
| }) |
| return {"pairs": pairs, "items": items} |
|
|