Download compsearch/classify.py from EugeneBoondock/sponge-compsearch: direct link, hf CLI and curl.
- Browser
- Download file 4.25 kB
-
https://huggingface.co/EugeneBoondock/sponge-compsearch/resolve/main/compsearch/classify.py
- Command line
-
hf download hf://EugeneBoondock/sponge-compsearch/compsearch/classify.py
-
curl -L -o classify.py https://huggingface.co/EugeneBoondock/sponge-compsearch/resolve/main/compsearch/classify.py
4.25 kB
| """Decision mode: turn a next-byte learner into a classifier that outputs probabilities. | |
| One learner per label is fed that label's examples. A new input is judged by how many | |
| bits each label's learner needs to encode it: fewer bits means "this looks like mine". | |
| Learning an example is just more counting, so there is no retraining step. | |
| The learner code runs inside this process, so only use learner files you trust | |
| (the shipped seeds, or a searched candidate you have read). | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import math | |
| from pathlib import Path | |
| from typing import Callable | |
| from compsearch.runner import byte_cost_bits | |
| from compsearch.safety import check_source | |
| SEPARATOR = b"\n\x00\n" # marks the boundary between examples | |
| TEMPERATURES = (1, 2, 3, 5, 8, 12, 20, 30, 50, 80) | |
| def load_learner_factory(path: str | Path) -> Callable[[], object]: | |
| """Return a zero-argument function that builds a fresh Learner from a file.""" | |
| source = Path(path).read_text(encoding="utf-8") | |
| problem = check_source(source) | |
| if problem: | |
| raise ValueError(f"{path}: {problem}") | |
| namespace: dict = {"__name__": "learner"} | |
| exec(compile(source, str(path), "exec"), namespace) # noqa: S102 - checked, and trusted by the caller | |
| return namespace["Learner"] | |
| def encode_cost_bits(learner, text: bytes) -> float: | |
| """Bits the learner needs for `text`. Advances the learner's state.""" | |
| bits = 0.0 | |
| for byte in text: | |
| bits += byte_cost_bits(learner.predict(), byte) | |
| learner.update(byte) | |
| return bits | |
| class Classifier: | |
| def __init__(self, learner_factory: Callable[[], object], temperature: float = 1.0): | |
| if temperature <= 0: | |
| raise ValueError("temperature must be positive") | |
| self.factory = learner_factory | |
| self.temperature = temperature | |
| self.learners: dict[str, object] = {} | |
| self.counts: dict[str, int] = {} | |
| def learn(self, label: str, text: bytes) -> None: | |
| """Absorb one labelled example. Can be called at any time, in any order.""" | |
| if not text: | |
| raise ValueError("cannot learn from empty text") | |
| learner = self.learners.setdefault(label, self.factory()) | |
| for byte in text + SEPARATOR: | |
| learner.predict() | |
| learner.update(byte) | |
| self.counts[label] = self.counts.get(label, 0) + 1 | |
| def costs(self, text: bytes) -> dict[str, float]: | |
| """Bits each label's learner needs for the text. Leaves the learners unchanged.""" | |
| if not self.learners: | |
| raise ValueError("no examples learned yet") | |
| if not text: | |
| raise ValueError("cannot judge empty text") | |
| return {label: encode_cost_bits(copy.deepcopy(learner), text) | |
| for label, learner in self.learners.items()} | |
| def judge(self, text: bytes) -> dict[str, float]: | |
| """Probability of each label for the text.""" | |
| return probabilities(self.costs(text), self.temperature) | |
| def surprise(self, text: bytes) -> float: | |
| """Bits per byte under the best-fitting label; high means 'unlike anything seen'.""" | |
| return min(self.costs(text).values()) / len(text) | |
| def calibrate(self, examples: list[tuple[str, bytes]]) -> float: | |
| """Pick the temperature that makes probabilities honest on held-out examples.""" | |
| if not examples: | |
| raise ValueError("need at least one example to calibrate") | |
| scored = [(label, self.costs(text)) for label, text in examples] | |
| self.temperature = min(TEMPERATURES, key=lambda t: log_loss(scored, t)) | |
| return self.temperature | |
| def probabilities(costs: dict[str, float], temperature: float) -> dict[str, float]: | |
| best = min(costs.values()) | |
| weights = {label: 2.0 ** (-(cost - best) / temperature) for label, cost in costs.items()} | |
| total = sum(weights.values()) | |
| return {label: weight / total for label, weight in weights.items()} | |
| def log_loss(scored: list[tuple[str, dict[str, float]]], temperature: float) -> float: | |
| """Average bits of surprise at the true label; lower means better-calibrated.""" | |
| return sum(-math.log2(max(probabilities(costs, temperature)[label], 1e-12)) | |
| for label, costs in scored) / len(scored) | |