sponge-compsearch / probe.py
EugeneBoondock's picture
Sponge online learners and the compression-scored search that found them
b999890 verified
Raw History Blame Contribute Delete
9.61 kB
"""Probe a learner for behaviours nobody programmed into it directly.
Each probe is a small experiment with a number that could have come out either way.
Runs the learner inside this process, so only use learner files you trust.
python probe.py # the seed
python probe.py --learner runs/first/cands/00004/learner.py --out runs/first/probe.json
"""
from __future__ import annotations
import argparse
import copy
import json
import math
import random
import sysconfig
import time
from pathlib import Path
import numpy as np
from compsearch.classify import load_learner_factory
KB = 1024
PROBE_BYTES = 20 * KB
def read_streams() -> tuple[bytes, bytes]:
"""Python source (the held-out stream) and English prose (Python's changelog)."""
python = Path("data/holdout.bin").read_bytes()
english_path = Path("data/english.bin")
if not english_path.is_file():
news = Path(sysconfig.get_paths()["stdlib"]).parent / "NEWS.txt"
english_path.write_bytes(news.read_bytes()[:1024 * KB])
return python, english_path.read_bytes()
def feed(learner, text: bytes) -> list[tuple[float, float, bool]]:
"""Run text through the learner (it learns). Per byte: (bits, top probability, top was right)."""
trace = []
for byte in text:
probs = np.asarray(learner.predict(), dtype=np.float64)
probs = probs / probs.sum()
top = int(probs.argmax())
trace.append((-math.log2(max(float(probs[byte]), 1e-12)), float(probs[top]), top == byte))
learner.update(byte)
return trace
def bpb(trace: list[tuple[float, float, bool]]) -> float:
return sum(item[0] for item in trace) / len(trace)
def frozen(learner, text: bytes) -> list[tuple[float, float, bool]]:
"""Measure on a copy, so the learner itself does not learn the text."""
return feed(copy.deepcopy(learner), text)
def probe_learning_curve(factory, python: bytes) -> tuple[dict, object]:
learner, windows = factory(), []
for start in range(0, 400 * KB, 100 * KB):
windows.append(round(bpb(feed(learner, python[start:start + 100 * KB])), 3))
return {"bits_per_byte_by_100KB_window": windows,
"improvement_first_to_last": round(windows[0] - windows[-1], 3)}, learner
def probe_forgetting(python_learner, python_probe: bytes, english: bytes) -> dict:
before = bpb(frozen(python_learner, python_probe))
feed(python_learner, english[:400 * KB])
after = bpb(frozen(python_learner, python_probe))
return {"python_cost_before_english": round(before, 3),
"python_cost_after_400KB_english": round(after, 3),
"change_percent": round(100 * (after - before) / before, 2)}
def probe_transfer(factory, python_learner, english: bytes) -> tuple[dict, object]:
sample = english[:100 * KB]
fresh = factory()
from_scratch = bpb(feed(fresh, sample))
pretrained = bpb(frozen(python_learner, sample)) # copy keeps learning as it reads
return {"english_cost_from_scratch": round(from_scratch, 3),
"english_cost_after_python_first": round(pretrained, 3),
"gain_percent": round(100 * (from_scratch - pretrained) / from_scratch, 2)}, fresh
def probe_order(factory, python: bytes, english: bytes, python_probe: bytes, english_probe: bytes) -> dict:
first, second = python[:150 * KB], english[:150 * KB]
results = []
for order in ((first, second), (second, first)):
learner = factory()
for chunk in order:
feed(learner, chunk)
results.append((bpb(frozen(learner, python_probe)), bpb(frozen(learner, english_probe))))
(py_a, en_a), (py_b, en_b) = results
return {"python_probe_python_first_vs_english_first": [round(py_a, 3), round(py_b, 3)],
"english_probe_python_first_vs_english_first": [round(en_a, 3), round(en_b, 3)],
"largest_difference_percent": round(100 * max(abs(py_a - py_b) / py_a, abs(en_a - en_b) / en_a), 2)}
def probe_one_shot(learner, filler: bytes, rng: random.Random) -> dict:
learner = copy.deepcopy(learner)
novel = bytes(rng.choice(b"abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(48))
first = bpb(feed(learner, novel))
feed(learner, filler[:2 * KB])
soon = bpb(frozen(learner, novel))
feed(learner, filler[2 * KB:202 * KB])
later = bpb(frozen(learner, novel))
return {"random_string_first_sight": round(first, 2),
"seen_once_then_2KB_later": round(soon, 2),
"seen_once_then_200KB_later": round(later, 2)}
def probe_word_boundaries(trace: list, text: bytes) -> dict:
starts, insides = [], []
for index in range(1, len(text)):
if chr(text[index]).isalpha():
(insides if chr(text[index - 1]).isalpha() else starts).append(trace[index][0])
start_bits, inside_bits = sum(starts) / len(starts), sum(insides) / len(insides)
return {"surprise_at_first_letter_of_word": round(start_bits, 2),
"surprise_inside_word": round(inside_bits, 2),
"ratio": round(start_bits / inside_bits, 1)}
def probe_typos(learner, text: bytes, rng: random.Random) -> dict:
letters = [i for i in range(1, len(text) - 1) if chr(text[i]).islower()]
typos = sorted(rng.sample(letters, 40))
corrupted = bytearray(text)
for index in typos:
corrupted[index] = rng.choice([c for c in b"abcdefghijklmnopqrstuvwxyz" if c != text[index]])
trace = frozen(learner, bytes(corrupted))
flagged_count = len(text) // 50 # flag the 2% most surprising bytes
flagged = set(sorted(range(len(text)), key=lambda i: -trace[i][0])[:flagged_count])
caught = sum(any(i in flagged for i in (index, index + 1)) for index in typos)
return {"typos_planted": len(typos), "caught_in_top_2_percent": caught,
"caught_percent": round(100 * caught / len(typos), 1), "chance_percent": 4.0}
def probe_calibration(trace: list) -> dict:
edges = [(0.0, 0.5), (0.5, 0.8), (0.8, 0.95), (0.95, 1.01)]
rows, error = [], 0.0
for low, high in edges:
bucket = [item for item in trace if low <= item[1] < high]
if not bucket:
continue
claimed = sum(item[1] for item in bucket) / len(bucket)
right = sum(item[2] for item in bucket) / len(bucket)
error += abs(claimed - right) * len(bucket) / len(trace)
rows.append({"confidence": f"{low:.2f}-{min(high, 1.0):.2f}", "share_of_bytes": round(len(bucket) / len(trace), 3),
"claimed": round(claimed, 3), "actually_right": round(right, 3)})
return {"buckets": rows, "average_gap": round(error, 4)}
def probe_generation(learner, prompt: bytes, rng: np.random.Generator) -> dict:
learner = copy.deepcopy(learner)
feed(learner, prompt)
produced = bytearray()
for _ in range(240):
probs = np.asarray(learner.predict(), dtype=np.float64) ** 1.3 # slightly sharpened
byte = int(rng.choice(256, p=probs / probs.sum()))
produced.append(byte)
learner.update(byte)
text = produced.decode("latin-1")
words = [w.strip(".,:;()'\"`").lower() for w in text.split()]
return {"prompt": prompt.decode("latin-1"), "continuation": text, "word_count": len(words)}
def memory_rows(learner) -> int:
"""Rough size of what the learner holds: entries across its dicts, lists and arrays."""
total = 0
for value in vars(learner).values():
items = value if isinstance(value, (list, tuple)) else [value]
for item in items:
if isinstance(item, (dict, bytearray, list)):
total += len(item)
elif isinstance(item, np.ndarray):
total += item.size
return total
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--learner", default="seeds/sponge.py")
parser.add_argument("--out", help="also write the results as JSON here")
args = parser.parse_args()
factory = load_learner_factory(args.learner)
python, english = read_streams()
python_probe = python[600 * KB:600 * KB + PROBE_BYTES]
english_probe = english[600 * KB:600 * KB + PROBE_BYTES]
started = time.monotonic()
report = {"learner": args.learner}
report["keeps_improving"], python_learner = probe_learning_curve(factory, python)
report["memory_rows_after_400KB"] = memory_rows(python_learner)
report["transfer_python_to_english"], english_learner = probe_transfer(factory, python_learner, english)
report["forgetting"] = probe_forgetting(python_learner, python_probe, english)
feed(english_learner, english[100 * KB:300 * KB])
clean_trace = frozen(english_learner, english_probe)
report["word_boundaries"] = probe_word_boundaries(clean_trace, english_probe)
report["calibration"] = probe_calibration(clean_trace)
report["typo_spotting"] = probe_typos(english_learner, english_probe, random.Random(1))
report["one_shot_memory"] = probe_one_shot(english_learner, english[300 * KB:], random.Random(2))
report["generation"] = probe_generation(english_learner, b"The interpreter now ", np.random.default_rng(3))
report["order_of_learning"] = probe_order(factory, python, english, python_probe, english_probe)
report["minutes"] = round((time.monotonic() - started) / 60, 1)
print(json.dumps(report, indent=1))
if args.out:
Path(args.out).write_text(json.dumps(report, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())