Download probe.py from EugeneBoondock/sponge-compsearch: direct link, hf CLI and curl.
- Browser
- Download file 9.61 kB
-
https://huggingface.co/EugeneBoondock/sponge-compsearch/resolve/main/probe.py
- Command line
-
hf download hf://EugeneBoondock/sponge-compsearch/probe.py
-
curl -L -o probe.py https://huggingface.co/EugeneBoondock/sponge-compsearch/resolve/main/probe.py
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()) | |