gridlm v0: screen + generator + odin operator, trained on colony-one
Browse files- LICENSE +9 -0
- README.md +122 -0
- extract.py +51 -0
- generator.pt +3 -0
- generator.safetensors +3 -0
- gridlm_operator.py +115 -0
- results.json +157 -0
- samples.jsonl +0 -0
- screen.pt +3 -0
- screen.safetensors +3 -0
- train.log +26 -0
- train.py +452 -0
- vocab.json +1 -0
LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Nick Flach and Kannaka
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
| 6 |
+
|
| 7 |
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
| 8 |
+
|
| 9 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
tags:
|
| 5 |
+
- artificial-life
|
| 6 |
+
- evolution
|
| 7 |
+
- thegrid
|
| 8 |
+
- genome
|
| 9 |
+
- mutation-operator
|
| 10 |
+
datasets:
|
| 11 |
+
- flaukowski/thegrid-colony-one-transitions
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# gridlm: a mutation screen and generator learned from a thegrid fossil record
|
| 15 |
+
|
| 16 |
+
[thegrid](https://github.com/EckmanTechLLC/thegrid) (Matt Eckman, MIT) is an artificial-life lab.
|
| 17 |
+
Its organisms are small programs on an evolvable instruction set, and they compete for real CPU,
|
| 18 |
+
memory and heat on the host. Every birth goes into a fossil record.
|
| 19 |
+
|
| 20 |
+
thegrid's own findings (`tools/analyse/LESSONS.md`) include this one: **model-authored mutation
|
| 21 |
+
founded nothing.** 4,554 genomes written by a general LLM from a prompt produced 4,554 births and
|
| 22 |
+
no lineage. Over the same period, blind segment transfer compounded at 1.3x.
|
| 23 |
+
|
| 24 |
+
gridlm asks whether a model trained on **what actually reproduced** does better than one prompted
|
| 25 |
+
with the instruction set. The idea is borrowed from DNA language models such as Hugging Face's
|
| 26 |
+
Carbon-X: give each unit of the sequence its own token and learn from outcomes. Here the unit is
|
| 27 |
+
an opcode, and the outcomes come from the fossil record.
|
| 28 |
+
|
| 29 |
+
Two models, about 0.8M parameters each:
|
| 30 |
+
|
| 31 |
+
- **screen** (`screen.pt`): an encoder over `[CLS] parent [SEP] child`. It outputs P(the child
|
| 32 |
+
genome is born at least twice in its epoch), meaning it copied itself and founded something.
|
| 33 |
+
- **generator** (`generator.pt`): a causal LM over `[BOS] parent [SEP] child [EOS]`, trained only
|
| 34 |
+
on children that reproduced. It writes a child for a given parent.
|
| 35 |
+
|
| 36 |
+
There is also **`gridlm_operator.py`**, a drop-in operator for thegrid's `odin` mutator. It reads
|
| 37 |
+
the same `request.json` / `proposal.json` queue that thegrid's `tools/operator` answers. For each
|
| 38 |
+
request it samples 8 children, the screen picks the best, and it proposes that one. It runs on
|
| 39 |
+
CPU in milliseconds. The colony validates every proposal with its own parser, so a proposal it
|
| 40 |
+
cannot use costs one birth, never the colony.
|
| 41 |
+
|
| 42 |
+
## Data
|
| 43 |
+
|
| 44 |
+
The fossil record of **colony-one**, the thegrid colony that kannaka-grid runs on its own host,
|
| 45 |
+
read 2026-10-27 (dataset: `flaukowski/thegrid-colony-one-transitions`):
|
| 46 |
+
|
| 47 |
+
- 1,573,008 rows: one per (epoch, child genome), with its earliest parent and exact self-copies excluded.
|
| 48 |
+
- Label: the child was born at least twice in its epoch. The positive rate is 24.8%.
|
| 49 |
+
- 614 epochs. **This is an older thegrid version and a starvation economy** (613 of 614 epochs ended
|
| 50 |
+
in extinction). It is not the eviction or class worlds Matt runs today.
|
| 51 |
+
|
| 52 |
+
Deliberately excluded from the inputs:
|
| 53 |
+
- `mutation_type`: thegrid records a mutation origin only when its child was born, so its presence
|
| 54 |
+
leaks the label.
|
| 55 |
+
- The parent's birth count for the epoch: it includes births after the child appeared.
|
| 56 |
+
|
| 57 |
+
The inputs are the two instruction sequences and nothing else.
|
| 58 |
+
|
| 59 |
+
## Results (held out in time)
|
| 60 |
+
|
| 61 |
+
Split by epoch: train epochs 1–565 (1.27M rows), validation 566–593, test 594–614 (152k rows).
|
| 62 |
+
The "novel" columns use only test children whose genome never appears in training (145k).
|
| 63 |
+
|
| 64 |
+
| model | AUROC | avg precision | precision in top 10% | AUROC, novel | top 10%, novel |
|
| 65 |
+
|---|---|---|---|---|---|
|
| 66 |
+
| base rate | | 0.248 | 0.248 | | 0.240 |
|
| 67 |
+
| rule: has alloc+copy+fork | 0.526 | 0.267 | 0.283 | 0.528 | 0.265 |
|
| 68 |
+
| logistic regression, 15 hand features | 0.609 | 0.327 | 0.384 | 0.605 | 0.362 |
|
| 69 |
+
| **gridlm screen** | **0.667** | **0.385** | **0.452** | **0.659** | **0.418** |
|
| 70 |
+
| gridlm generator, log p(child \| parent) | 0.582 | 0.296 | 0.321 | 0.586 | 0.320 |
|
| 71 |
+
|
| 72 |
+
**What this says:** the outcome of a single mutation is mostly noise, which the weak baselines
|
| 73 |
+
show. But the screen ranks children well above chance and well above hand features. The 10% it
|
| 74 |
+
likes best reproduce at 1.8x the base rate (1.7x on genomes it has never seen).
|
| 75 |
+
|
| 76 |
+
**What it does not say:** whether the generator's children found lineages in a live colony. That
|
| 77 |
+
needs a colony.
|
| 78 |
+
|
| 79 |
+
The generator's 1,000 samples on held-out parents:
|
| 80 |
+
- 100% are valid length;
|
| 81 |
+
- 83.2% contain alloc+copy+fork;
|
| 82 |
+
- 2.3% are identical to the parent.
|
| 83 |
+
|
| 84 |
+
The screen rates them higher than the real test mutants: mean P 0.525 against 0.469. That is
|
| 85 |
+
one model judging another and is no evidence of fitness.
|
| 86 |
+
|
| 87 |
+
The samples look like what LESSONS says evolution does there. They include tandem duplications
|
| 88 |
+
of an earning motif and duplicated `fork copy alloc` blocks. Some are bad, for example
|
| 89 |
+
substitutions that remove `fork`.
|
| 90 |
+
|
| 91 |
+
## Limits
|
| 92 |
+
|
| 93 |
+
- **One colony, one ISA version, one economy.** Instruction names colony-one never used are
|
| 94 |
+
unknown tokens, and the model's taste is a starvation world's taste. It should be retrained on
|
| 95 |
+
the target fleet's own record. `extract.py` and `train.py` do that end to end from any thegrid
|
| 96 |
+
`history.sqlite3`.
|
| 97 |
+
- **No replicate colonies.** thegrid's own rule is "never run one of anything you intend to
|
| 98 |
+
learn from". The time split is held out, but it is one colony. The real test is several
|
| 99 |
+
replicate colonies with this operator against the same colonies on blind variation, compared on
|
| 100 |
+
lineages founded per proposal.
|
| 101 |
+
- **LESSONS 11–12 say the economy decides where a world ends up**, not the mutation operator.
|
| 102 |
+
A better mutator may only reach stasis faster. That is worth measuring, not assuming away.
|
| 103 |
+
|
| 104 |
+
## Use
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
pip install torch numpy
|
| 108 |
+
# one proposal
|
| 109 |
+
python gridlm_operator.py --model-dir . --once-genome "harvest harvest alloc copy ifnotdone jmpb fork scan move"
|
| 110 |
+
# serve one or more colonies' odin queues
|
| 111 |
+
python gridlm_operator.py --model-dir . --queue /path/to/colony/operator
|
| 112 |
+
# retrain on your own fossil record
|
| 113 |
+
python extract.py /path/to/history.sqlite3 transitions.jsonl.gz
|
| 114 |
+
python train.py transitions.jsonl.gz out --device cuda
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Trained on one qBraid RTX 4090 in about 7 minutes (12.8 credits, about $0.13).
|
| 118 |
+
|
| 119 |
+
## Credits
|
| 120 |
+
|
| 121 |
+
Built by Kannaka (an AI) with Nick Flach, on top of Matt Eckman's thegrid. thegrid's instruction
|
| 122 |
+
set, fossil record format and operator protocol are his. The model, code and card are MIT.
|
extract.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extract a parent->child training set from a thegrid fossil record (read-only).
|
| 2 |
+
|
| 3 |
+
One row per (epoch, child genome): the child's earliest recorded parent in that epoch,
|
| 4 |
+
exact self-copies (parent == child) excluded. Label: the child genome was born at least
|
| 5 |
+
twice in that epoch (it copied itself, so it "founded" something), from genome_stats.births.
|
| 6 |
+
|
| 7 |
+
mutation_origins is deliberately NOT used: it only records origins whose child was born,
|
| 8 |
+
so its presence would leak the label.
|
| 9 |
+
|
| 10 |
+
usage: python3 extract.py <history.sqlite3> <out.jsonl.gz>
|
| 11 |
+
"""
|
| 12 |
+
import gzip
|
| 13 |
+
import json
|
| 14 |
+
import sqlite3
|
| 15 |
+
import sys
|
| 16 |
+
|
| 17 |
+
db, out = sys.argv[1], sys.argv[2]
|
| 18 |
+
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
| 19 |
+
|
| 20 |
+
src = {gid: s for gid, s in c.execute("SELECT genome_id, source FROM genomes")}
|
| 21 |
+
print("genomes", len(src), file=sys.stderr)
|
| 22 |
+
births = {(e, g): b for e, g, b in c.execute("SELECT epoch, genome_id, births FROM genome_stats")}
|
| 23 |
+
print("stats", len(births), file=sys.stderr)
|
| 24 |
+
|
| 25 |
+
rows = c.execute(
|
| 26 |
+
"SELECT epoch, child_genome_id, parent_genome_id, first_tick FROM transitions "
|
| 27 |
+
"WHERE parent_genome_id != child_genome_id ORDER BY epoch, child_genome_id, first_tick")
|
| 28 |
+
n = kept = skipped = 0
|
| 29 |
+
last = None
|
| 30 |
+
with gzip.open(out, "wt", encoding="utf-8") as f:
|
| 31 |
+
for epoch, child, parent, tick in rows:
|
| 32 |
+
n += 1
|
| 33 |
+
if (epoch, child) == last:
|
| 34 |
+
continue # a later parent of the same child in the same epoch
|
| 35 |
+
last = (epoch, child)
|
| 36 |
+
if child not in src or parent not in src:
|
| 37 |
+
skipped += 1
|
| 38 |
+
continue
|
| 39 |
+
cb = births.get((epoch, child), 0)
|
| 40 |
+
f.write(json.dumps({
|
| 41 |
+
"epoch": epoch,
|
| 42 |
+
"child_id": child,
|
| 43 |
+
"parent": src[parent].split(" · "),
|
| 44 |
+
"child": src[child].split(" · "),
|
| 45 |
+
"child_births": cb,
|
| 46 |
+
"parent_births": births.get((epoch, parent), 0),
|
| 47 |
+
"first_tick": tick,
|
| 48 |
+
"label": int(cb >= 2),
|
| 49 |
+
}, separators=(",", ":")) + "\n")
|
| 50 |
+
kept += 1
|
| 51 |
+
print(json.dumps({"transitions_scanned": n, "rows": kept, "skipped_missing_source": skipped}), file=sys.stderr)
|
generator.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2a98428b5cb61075f33a728efe052dd368c6e343d5d459aa28b11998f1a3e031
|
| 3 |
+
size 3315176
|
generator.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a393b36d0be7b60bc5d295d2eec14bd04ffe282390d9f5d2f9cdd50eff3caea0
|
| 3 |
+
size 3303840
|
gridlm_operator.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""A thegrid `odin` operator backed by gridlm instead of a general LLM.
|
| 3 |
+
|
| 4 |
+
Same queue protocol as thegrid's tools/operator/grid_operator.py: the colony writes
|
| 5 |
+
<queue>/request.json (with "genome", a list of instruction names, and "request_id"); this
|
| 6 |
+
writes <queue>/proposal.json ({"request_id", "authored_at", "genome"}) atomically. The colony
|
| 7 |
+
validates every proposal with its own parser, so a proposal it can't use costs one birth (it
|
| 8 |
+
falls back to blind variation) and never the colony.
|
| 9 |
+
|
| 10 |
+
How a proposal is made: the generator samples K children for the parent; the screen scores
|
| 11 |
+
each; the best-scoring child that differs from the parent is proposed. Runs on CPU in
|
| 12 |
+
milliseconds, so there is no GPU queue to wait for.
|
| 13 |
+
|
| 14 |
+
Honest scope: trained on ONE colony (kannaka-grid's colony-one, an older thegrid ISA and a
|
| 15 |
+
starvation economy). Instruction names that colony never used are unknown to the model, and
|
| 16 |
+
anything it proposes that a destination colony can't run is rejected there by design.
|
| 17 |
+
|
| 18 |
+
usage:
|
| 19 |
+
python gridlm_operator.py --model-dir . --queue /path/to/colony/operator [--queue ...]
|
| 20 |
+
python gridlm_operator.py --model-dir . --once-genome "harvest harvest alloc copy ifnotdone jmpb fork scan move"
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
|
| 31 |
+
import torch
|
| 32 |
+
|
| 33 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 34 |
+
import train as g # noqa: E402 (model classes, tokens, sampling)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load(model_dir: Path):
|
| 38 |
+
vocab = json.loads((model_dir / "vocab.json").read_text())
|
| 39 |
+
screen = g.Screen(len(vocab))
|
| 40 |
+
gen = g.Generator(len(vocab))
|
| 41 |
+
for model, name in ((screen, "screen"), (gen, "generator")):
|
| 42 |
+
st = model_dir / f"{name}.safetensors"
|
| 43 |
+
if st.exists(): # preferred: no pickle
|
| 44 |
+
from safetensors.torch import load_file
|
| 45 |
+
model.load_state_dict(load_file(str(st)))
|
| 46 |
+
else:
|
| 47 |
+
model.load_state_dict(torch.load(model_dir / f"{name}.pt", map_location="cpu", weights_only=True))
|
| 48 |
+
screen.eval()
|
| 49 |
+
gen.eval()
|
| 50 |
+
return vocab, {t: i for i, t in enumerate(vocab)}, screen, gen
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def propose(names, vocab, idx, screen, gen, k=8, temperature=0.9, seed=None):
|
| 54 |
+
parent = [idx.get(n, g.UNK) for n in names][: g.MAXG]
|
| 55 |
+
kids = g.sample_children(gen, [parent] * k, vocab, k, temperature=temperature,
|
| 56 |
+
rng_seed=seed if seed is not None else int(time.time() * 1000) % (2 ** 31))
|
| 57 |
+
cands = [c for c in kids if 1 <= len(c) <= g.MAXG and c != list(names)]
|
| 58 |
+
if not cands:
|
| 59 |
+
return None, None
|
| 60 |
+
rows = [{"parent": list(names)[: g.MAXG], "child": c, "label": 0, "child_id": ""} for c in cands]
|
| 61 |
+
X, _ = g.screen_tensor(rows, idx)
|
| 62 |
+
p = g.screen_scores(screen, X)
|
| 63 |
+
best = int(p.argmax())
|
| 64 |
+
return cands[best], float(p[best])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def serve(queue: Path, model, stats) -> bool:
|
| 68 |
+
req_p, prop_p = queue / "request.json", queue / "proposal.json"
|
| 69 |
+
if prop_p.exists() or not req_p.exists():
|
| 70 |
+
return False
|
| 71 |
+
try:
|
| 72 |
+
req = json.loads(req_p.read_text())
|
| 73 |
+
except (OSError, ValueError):
|
| 74 |
+
return False
|
| 75 |
+
child, score = propose(req.get("genome", []), *model)
|
| 76 |
+
if child is None:
|
| 77 |
+
stats["rejected"] += 1
|
| 78 |
+
return True
|
| 79 |
+
tmp = prop_p.with_suffix(".tmp")
|
| 80 |
+
tmp.write_text(json.dumps({"request_id": req.get("request_id"), "authored_at": time.time(),
|
| 81 |
+
"genome": child, "gridlm_screen_p": score}))
|
| 82 |
+
tmp.replace(prop_p)
|
| 83 |
+
stats["served"] += 1
|
| 84 |
+
print(f"{time.strftime('%H:%M:%S')} {queue}: {len(req.get('genome', []))} -> {len(child)} "
|
| 85 |
+
f"(screen p={score:.3f}; served {stats['served']} rejected {stats['rejected']})", flush=True)
|
| 86 |
+
return True
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def main():
|
| 90 |
+
ap = argparse.ArgumentParser()
|
| 91 |
+
ap.add_argument("--model-dir", default=".")
|
| 92 |
+
ap.add_argument("--queue", action="append", default=[])
|
| 93 |
+
ap.add_argument("--once-genome", help="print one proposal for this genome (space-separated names) and exit")
|
| 94 |
+
ap.add_argument("--poll", type=float, default=1.0)
|
| 95 |
+
a = ap.parse_args()
|
| 96 |
+
torch.set_num_threads(2)
|
| 97 |
+
model = load(Path(a.model_dir))
|
| 98 |
+
if a.once_genome:
|
| 99 |
+
child, score = propose(a.once_genome.split(), *model)
|
| 100 |
+
print(json.dumps({"parent": a.once_genome.split(), "child": child, "screen_p": score}))
|
| 101 |
+
return
|
| 102 |
+
stats = {"served": 0, "rejected": 0}
|
| 103 |
+
queues = [Path(q) for q in a.queue]
|
| 104 |
+
print("gridlm operator; queues:", ", ".join(map(str, queues)), flush=True)
|
| 105 |
+
while True:
|
| 106 |
+
worked = False
|
| 107 |
+
for q in queues:
|
| 108 |
+
if q.exists():
|
| 109 |
+
worked = serve(q, model, stats) or worked
|
| 110 |
+
if not worked:
|
| 111 |
+
time.sleep(a.poll)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|
results.json
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"split": {
|
| 3 |
+
"epoch_cuts": [
|
| 4 |
+
565,
|
| 5 |
+
593
|
| 6 |
+
],
|
| 7 |
+
"train": 1266165,
|
| 8 |
+
"val": 154346,
|
| 9 |
+
"test": 152497,
|
| 10 |
+
"novel_test": 145441
|
| 11 |
+
},
|
| 12 |
+
"test": [
|
| 13 |
+
{
|
| 14 |
+
"model": "baseline: has alloc+copy+fork",
|
| 15 |
+
"n": 152497,
|
| 16 |
+
"base_rate": 0.24748027830055674,
|
| 17 |
+
"auroc": 0.5259577223102965,
|
| 18 |
+
"avg_precision": 0.2668144186982295,
|
| 19 |
+
"precision_at_top10pct": 0.2829693750409863
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"model": "baseline: logistic on 15 hand features",
|
| 23 |
+
"n": 152497,
|
| 24 |
+
"base_rate": 0.24748027830055674,
|
| 25 |
+
"auroc": 0.6091323573663239,
|
| 26 |
+
"avg_precision": 0.3265924671833763,
|
| 27 |
+
"precision_at_top10pct": 0.3836317135549872
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"model": "gridlm screen",
|
| 31 |
+
"n": 152497,
|
| 32 |
+
"base_rate": 0.24748027830055674,
|
| 33 |
+
"auroc": 0.6667371511948852,
|
| 34 |
+
"avg_precision": 0.38529948709031914,
|
| 35 |
+
"precision_at_top10pct": 0.451898485146567
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"model": "gridlm generator log p(child|parent)",
|
| 39 |
+
"n": 152497,
|
| 40 |
+
"base_rate": 0.24748027830055674,
|
| 41 |
+
"auroc": 0.5821718263469734,
|
| 42 |
+
"avg_precision": 0.2962117960990998,
|
| 43 |
+
"precision_at_top10pct": 0.3207423437602466
|
| 44 |
+
}
|
| 45 |
+
],
|
| 46 |
+
"test_novel": [
|
| 47 |
+
{
|
| 48 |
+
"model": "baseline: has alloc+copy+fork",
|
| 49 |
+
"n": 145441,
|
| 50 |
+
"base_rate": 0.23971919885039294,
|
| 51 |
+
"auroc": 0.5277137815178677,
|
| 52 |
+
"avg_precision": 0.2554675640963218,
|
| 53 |
+
"precision_at_top10pct": 0.2647827282728273
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"model": "baseline: logistic on 15 hand features",
|
| 57 |
+
"n": 145441,
|
| 58 |
+
"base_rate": 0.23971919885039294,
|
| 59 |
+
"auroc": 0.6053365913437163,
|
| 60 |
+
"avg_precision": 0.31109046086779596,
|
| 61 |
+
"precision_at_top10pct": 0.36234873487348734
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"model": "gridlm screen",
|
| 65 |
+
"n": 145441,
|
| 66 |
+
"base_rate": 0.23971919885039294,
|
| 67 |
+
"auroc": 0.6589837791458187,
|
| 68 |
+
"avg_precision": 0.3585924010318861,
|
| 69 |
+
"precision_at_top10pct": 0.4183168316831683
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"model": "gridlm generator log p(child|parent)",
|
| 73 |
+
"n": 145441,
|
| 74 |
+
"base_rate": 0.23971919885039294,
|
| 75 |
+
"auroc": 0.5858987532745887,
|
| 76 |
+
"avg_precision": 0.2914317479740002,
|
| 77 |
+
"precision_at_top10pct": 0.3201320132013201
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"samples": {
|
| 81 |
+
"n": 1000,
|
| 82 |
+
"valid_length": 1.0,
|
| 83 |
+
"has_alloc_copy_fork": 0.832,
|
| 84 |
+
"identical_to_parent": 0.023,
|
| 85 |
+
"screen_mean_p_generated": 0.5254685282707214,
|
| 86 |
+
"screen_mean_p_real_test_children": 0.4690079689025879,
|
| 87 |
+
"note": "screen scores on generated children are one model judging another; only a live colony is ground truth"
|
| 88 |
+
},
|
| 89 |
+
"vocab": [
|
| 90 |
+
"[PAD]",
|
| 91 |
+
"[CLS]",
|
| 92 |
+
"[SEP]",
|
| 93 |
+
"[BOS]",
|
| 94 |
+
"[EOS]",
|
| 95 |
+
"[UNK]",
|
| 96 |
+
"add",
|
| 97 |
+
"alloc",
|
| 98 |
+
"build",
|
| 99 |
+
"burn",
|
| 100 |
+
"call",
|
| 101 |
+
"copy",
|
| 102 |
+
"copyn",
|
| 103 |
+
"corrupt",
|
| 104 |
+
"dec",
|
| 105 |
+
"define",
|
| 106 |
+
"fetch",
|
| 107 |
+
"fork",
|
| 108 |
+
"free",
|
| 109 |
+
"harvest",
|
| 110 |
+
"ifnotdone",
|
| 111 |
+
"ifzero",
|
| 112 |
+
"inc",
|
| 113 |
+
"input",
|
| 114 |
+
"jmpb",
|
| 115 |
+
"jmpr",
|
| 116 |
+
"link",
|
| 117 |
+
"listen",
|
| 118 |
+
"load",
|
| 119 |
+
"locate",
|
| 120 |
+
"macro0",
|
| 121 |
+
"macro1",
|
| 122 |
+
"macro2",
|
| 123 |
+
"macro3",
|
| 124 |
+
"macro4",
|
| 125 |
+
"macro5",
|
| 126 |
+
"macro6",
|
| 127 |
+
"macro7",
|
| 128 |
+
"move",
|
| 129 |
+
"nand",
|
| 130 |
+
"nop",
|
| 131 |
+
"offer",
|
| 132 |
+
"output",
|
| 133 |
+
"peek",
|
| 134 |
+
"post",
|
| 135 |
+
"publish",
|
| 136 |
+
"push",
|
| 137 |
+
"salvage",
|
| 138 |
+
"scan",
|
| 139 |
+
"signal",
|
| 140 |
+
"steal",
|
| 141 |
+
"store",
|
| 142 |
+
"sub",
|
| 143 |
+
"swap",
|
| 144 |
+
"write",
|
| 145 |
+
"xor"
|
| 146 |
+
],
|
| 147 |
+
"config": {
|
| 148 |
+
"d_model": 128,
|
| 149 |
+
"layers": 4,
|
| 150 |
+
"heads": 4,
|
| 151 |
+
"ff": 512,
|
| 152 |
+
"max_genome": 64,
|
| 153 |
+
"label": "child genome born >= 2 times in its epoch",
|
| 154 |
+
"screen_params": 817409,
|
| 155 |
+
"generator_params": 824632
|
| 156 |
+
}
|
| 157 |
+
}
|
samples.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
screen.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2be4bc1ab9dddf5413877f23a92600f31a67ebe97ef6a0f810f862571d71734
|
| 3 |
+
size 3282746
|
screen.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1eb1fb50c43b02f72f34bf2a17b6d8fc4936f09c5e5aea521fec880a202d2ea4
|
| 3 |
+
size 3274948
|
train.log
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
06:26:30 rows 1573008 train 1266165 val 154346 test 152497 (novel test 145441); epoch cuts (565, 593); vocab 56; train base rate 0.248
|
| 2 |
+
06:26:56 baseline: has alloc+copy+fork: test auroc 0.5260
|
| 3 |
+
06:26:56 baseline: logistic on 15 hand features: test auroc 0.6091
|
| 4 |
+
06:27:41 screen epoch 1/8 loss 1.0093 val_auroc 0.6292 (32s)
|
| 5 |
+
06:28:13 screen epoch 2/8 loss 0.9852 val_auroc 0.6459 (31s)
|
| 6 |
+
06:28:44 screen epoch 3/8 loss 0.9709 val_auroc 0.6557 (31s)
|
| 7 |
+
06:29:14 screen epoch 4/8 loss 0.9630 val_auroc 0.6573 (30s)
|
| 8 |
+
06:29:44 screen epoch 5/8 loss 0.9575 val_auroc 0.6627 (30s)
|
| 9 |
+
06:30:16 screen epoch 6/8 loss 0.9534 val_auroc 0.6626 (31s)
|
| 10 |
+
06:30:46 screen epoch 7/8 loss 0.9494 val_auroc 0.6642 (31s)
|
| 11 |
+
06:31:17 screen epoch 8/8 loss 0.9460 val_auroc 0.6666 (31s)
|
| 12 |
+
06:31:19 screen: test auroc 0.6667 (novel 0.6590)
|
| 13 |
+
06:31:32 generator epoch 1/12 nll 1.4800 val_logprob/token -0.8854 (12s)
|
| 14 |
+
06:31:44 generator epoch 2/12 nll 0.8963 val_logprob/token -0.7759 (12s)
|
| 15 |
+
06:31:56 generator epoch 3/12 nll 0.8297 val_logprob/token -0.7381 (12s)
|
| 16 |
+
06:32:08 generator epoch 4/12 nll 0.7913 val_logprob/token -0.7091 (12s)
|
| 17 |
+
06:32:20 generator epoch 5/12 nll 0.7655 val_logprob/token -0.6833 (12s)
|
| 18 |
+
06:32:33 generator epoch 6/12 nll 0.7458 val_logprob/token -0.6740 (13s)
|
| 19 |
+
06:32:45 generator epoch 7/12 nll 0.7329 val_logprob/token -0.6664 (12s)
|
| 20 |
+
06:32:57 generator epoch 8/12 nll 0.7240 val_logprob/token -0.6586 (12s)
|
| 21 |
+
06:33:09 generator epoch 9/12 nll 0.7147 val_logprob/token -0.6504 (12s)
|
| 22 |
+
06:33:21 generator epoch 10/12 nll 0.7092 val_logprob/token -0.6461 (12s)
|
| 23 |
+
06:33:34 generator epoch 11/12 nll 0.7024 val_logprob/token -0.6477 (13s)
|
| 24 |
+
06:33:45 generator epoch 12/12 nll 0.6972 val_logprob/token -0.6433 (12s)
|
| 25 |
+
06:33:48 generator ranking: test auroc 0.5822
|
| 26 |
+
06:34:16 done: {"n": 1000, "valid_length": 1.0, "has_alloc_copy_fork": 0.832, "identical_to_parent": 0.023, "screen_mean_p_generated": 0.5254685282707214, "screen_mean_p_real_test_children": 0.4690079689025879, "note": "screen scores on generated children are one model judging another; only a live colony is ground truth"}
|
train.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""gridlm: two small models trained on a thegrid fossil record.
|
| 2 |
+
|
| 3 |
+
screen encoder: [CLS] parent [SEP] child -> P(child is born >= 2 times in its epoch)
|
| 4 |
+
generator causal LM: [BOS] parent [SEP] child [EOS], trained on successful children only
|
| 5 |
+
|
| 6 |
+
Held-out design: rows are split by EPOCH (train = earliest ~80% of rows' epochs, val = next
|
| 7 |
+
~10%, test = last ~10%), so the test set is later, unseen runs of the colony. Test metrics are
|
| 8 |
+
also reported on "novel" children whose genome never appears in the training split.
|
| 9 |
+
|
| 10 |
+
Inputs are the two instruction sequences only. Excluded on purpose: mutation_type (recorded
|
| 11 |
+
only for successful origins, so it leaks the label) and the parent's epoch birth count
|
| 12 |
+
(includes births after the child appeared).
|
| 13 |
+
|
| 14 |
+
usage: python train.py data.jsonl.gz outdir [--threads N] [--epochs-screen E] [--epochs-gen E]
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import gzip
|
| 20 |
+
import json
|
| 21 |
+
import math
|
| 22 |
+
import os
|
| 23 |
+
import random
|
| 24 |
+
import time
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
|
| 31 |
+
MAXG = 64
|
| 32 |
+
SPECIALS = ["[PAD]", "[CLS]", "[SEP]", "[BOS]", "[EOS]", "[UNK]"]
|
| 33 |
+
PAD, CLS, SEP, BOS, EOS, UNK = range(6)
|
| 34 |
+
DEV = torch.device("cpu")
|
| 35 |
+
KEY_OPS = ["alloc", "copy", "fork", "harvest", "scan", "move", "ifnotdone", "jmpb", "salvage", "steal"]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------- data
|
| 39 |
+
def load(path):
|
| 40 |
+
rows = []
|
| 41 |
+
with gzip.open(path, "rt", encoding="utf-8") as f:
|
| 42 |
+
for line in f:
|
| 43 |
+
r = json.loads(line)
|
| 44 |
+
if 1 <= len(r["child"]) <= MAXG and 1 <= len(r["parent"]) <= MAXG:
|
| 45 |
+
rows.append(r)
|
| 46 |
+
return rows
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def split_by_epoch(rows):
|
| 50 |
+
epochs = sorted({r["epoch"] for r in rows})
|
| 51 |
+
per = {e: 0 for e in epochs}
|
| 52 |
+
for r in rows:
|
| 53 |
+
per[r["epoch"]] += 1
|
| 54 |
+
total, acc, cut1, cut2 = len(rows), 0, None, None
|
| 55 |
+
for e in epochs:
|
| 56 |
+
acc += per[e]
|
| 57 |
+
if cut1 is None and acc >= 0.8 * total:
|
| 58 |
+
cut1 = e
|
| 59 |
+
if cut2 is None and acc >= 0.9 * total:
|
| 60 |
+
cut2 = e
|
| 61 |
+
tr = [r for r in rows if r["epoch"] <= cut1]
|
| 62 |
+
va = [r for r in rows if cut1 < r["epoch"] <= cut2]
|
| 63 |
+
te = [r for r in rows if r["epoch"] > cut2]
|
| 64 |
+
return tr, va, te, (cut1, cut2)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def build_vocab(rows):
|
| 68 |
+
ops = sorted({t for r in rows for t in r["parent"] + r["child"]})
|
| 69 |
+
return SPECIALS + ops
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def enc_pair(r, idx):
|
| 73 |
+
p = [idx.get(t, UNK) for t in r["parent"]]
|
| 74 |
+
c = [idx.get(t, UNK) for t in r["child"]]
|
| 75 |
+
return p, c
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def screen_tensor(rows, idx):
|
| 79 |
+
L = 2 * MAXG + 2
|
| 80 |
+
X = np.full((len(rows), L), PAD, dtype=np.int64)
|
| 81 |
+
y = np.zeros(len(rows), dtype=np.float32)
|
| 82 |
+
for i, r in enumerate(rows):
|
| 83 |
+
p, c = enc_pair(r, idx)
|
| 84 |
+
seq = [CLS] + p + [SEP] + c
|
| 85 |
+
X[i, :len(seq)] = seq
|
| 86 |
+
y[i] = r["label"]
|
| 87 |
+
return torch.from_numpy(X), torch.from_numpy(y)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def lm_tensor(rows, idx):
|
| 91 |
+
L = 2 * MAXG + 3
|
| 92 |
+
X = np.full((len(rows), L), PAD, dtype=np.int64)
|
| 93 |
+
M = np.zeros((len(rows), L), dtype=np.float32) # 1 where the NEXT token is a child token or EOS
|
| 94 |
+
for i, r in enumerate(rows):
|
| 95 |
+
p, c = enc_pair(r, idx)
|
| 96 |
+
seq = [BOS] + p + [SEP] + c + [EOS]
|
| 97 |
+
X[i, :len(seq)] = seq
|
| 98 |
+
start = 1 + len(p) # position of SEP; predicts child[0]
|
| 99 |
+
M[i, start:start + len(c) + 1] = 1 # child tokens and EOS are the targets
|
| 100 |
+
return torch.from_numpy(X), torch.from_numpy(M)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ---------------------------------------------------------------- models
|
| 104 |
+
class Encoder(nn.Module):
|
| 105 |
+
def __init__(self, V, d=128, layers=4, heads=4, ff=512, L=2 * MAXG + 2, causal=False):
|
| 106 |
+
super().__init__()
|
| 107 |
+
self.tok = nn.Embedding(V, d, padding_idx=PAD)
|
| 108 |
+
self.pos = nn.Embedding(L + 1, d)
|
| 109 |
+
layer = nn.TransformerEncoderLayer(d, heads, ff, dropout=0.1, batch_first=True, norm_first=True)
|
| 110 |
+
self.body = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
|
| 111 |
+
self.norm = nn.LayerNorm(d)
|
| 112 |
+
self.causal = causal
|
| 113 |
+
|
| 114 |
+
def forward(self, x):
|
| 115 |
+
T = x.shape[1]
|
| 116 |
+
h = self.tok(x) + self.pos(torch.arange(T, device=x.device))[None]
|
| 117 |
+
mask = nn.Transformer.generate_square_subsequent_mask(T, device=x.device) if self.causal else None
|
| 118 |
+
h = self.body(h, mask=mask, src_key_padding_mask=(x == PAD), is_causal=self.causal)
|
| 119 |
+
return self.norm(h)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Screen(nn.Module):
|
| 123 |
+
def __init__(self, V, **kw):
|
| 124 |
+
super().__init__()
|
| 125 |
+
self.enc = Encoder(V, **kw)
|
| 126 |
+
self.head = nn.Linear(self.enc.tok.embedding_dim, 1)
|
| 127 |
+
|
| 128 |
+
def forward(self, x):
|
| 129 |
+
return self.head(self.enc(x)[:, 0]).squeeze(-1)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class Generator(nn.Module):
|
| 133 |
+
def __init__(self, V, **kw):
|
| 134 |
+
super().__init__()
|
| 135 |
+
self.enc = Encoder(V, L=2 * MAXG + 3, causal=True, **kw)
|
| 136 |
+
self.head = nn.Linear(self.enc.tok.embedding_dim, V)
|
| 137 |
+
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
return self.head(self.enc(x))
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ---------------------------------------------------------------- metrics
|
| 143 |
+
def auroc(scores, labels):
|
| 144 |
+
s = np.asarray(scores, dtype=np.float64)
|
| 145 |
+
y = np.asarray(labels, dtype=np.int64)
|
| 146 |
+
pos, neg = y.sum(), len(y) - y.sum()
|
| 147 |
+
if pos == 0 or neg == 0:
|
| 148 |
+
return float("nan")
|
| 149 |
+
order = np.argsort(s, kind="mergesort")
|
| 150 |
+
ranks = np.empty(len(s))
|
| 151 |
+
sorted_s = s[order]
|
| 152 |
+
i = 0
|
| 153 |
+
while i < len(s): # average ranks for ties
|
| 154 |
+
j = i
|
| 155 |
+
while j + 1 < len(s) and sorted_s[j + 1] == sorted_s[i]:
|
| 156 |
+
j += 1
|
| 157 |
+
ranks[order[i:j + 1]] = (i + j) / 2 + 1
|
| 158 |
+
i = j + 1
|
| 159 |
+
return float((ranks[y == 1].sum() - pos * (pos + 1) / 2) / (pos * neg))
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def avg_precision(scores, labels):
|
| 163 |
+
order = np.argsort(-np.asarray(scores))
|
| 164 |
+
y = np.asarray(labels)[order]
|
| 165 |
+
tp = np.cumsum(y)
|
| 166 |
+
prec = tp / np.arange(1, len(y) + 1)
|
| 167 |
+
return float((prec * y).sum() / max(1, y.sum()))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def precision_at(scores, labels, frac):
|
| 171 |
+
k = max(1, int(len(scores) * frac))
|
| 172 |
+
order = np.argsort(-np.asarray(scores))[:k]
|
| 173 |
+
return float(np.asarray(labels)[order].mean())
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def report(name, scores, labels):
|
| 177 |
+
return {"model": name, "n": len(labels), "base_rate": float(np.mean(labels)),
|
| 178 |
+
"auroc": auroc(scores, labels), "avg_precision": avg_precision(scores, labels),
|
| 179 |
+
"precision_at_top10pct": precision_at(scores, labels, 0.10)}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# ---------------------------------------------------------------- baselines
|
| 183 |
+
def edit_distance(a, b):
|
| 184 |
+
prev = list(range(len(b) + 1))
|
| 185 |
+
for i, x in enumerate(a, 1):
|
| 186 |
+
cur = [i]
|
| 187 |
+
for j, yv in enumerate(b, 1):
|
| 188 |
+
cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (x != yv)))
|
| 189 |
+
prev = cur
|
| 190 |
+
return prev[-1]
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def features(r):
|
| 194 |
+
c, p = r["child"], r["parent"]
|
| 195 |
+
f = [len(c), len(p), len(c) - len(p), edit_distance(p, c)]
|
| 196 |
+
f += [c.count(op) for op in KEY_OPS]
|
| 197 |
+
f.append(float(all(op in c for op in ("alloc", "copy", "fork"))))
|
| 198 |
+
return f
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def logistic(Xtr, ytr, Xte, iters=300):
|
| 202 |
+
mu, sd = Xtr.mean(0), Xtr.std(0) + 1e-6
|
| 203 |
+
A = torch.tensor((Xtr - mu) / sd, dtype=torch.float32)
|
| 204 |
+
B = torch.tensor((Xte - mu) / sd, dtype=torch.float32)
|
| 205 |
+
y = torch.tensor(ytr, dtype=torch.float32)
|
| 206 |
+
w = torch.zeros(A.shape[1], requires_grad=True)
|
| 207 |
+
b = torch.zeros(1, requires_grad=True)
|
| 208 |
+
opt = torch.optim.LBFGS([w, b], max_iter=iters)
|
| 209 |
+
|
| 210 |
+
def closure():
|
| 211 |
+
opt.zero_grad()
|
| 212 |
+
loss = F.binary_cross_entropy_with_logits(A @ w + b, y) + 1e-4 * (w * w).sum()
|
| 213 |
+
loss.backward()
|
| 214 |
+
return loss
|
| 215 |
+
opt.step(closure)
|
| 216 |
+
with torch.no_grad():
|
| 217 |
+
return (B @ w + b).numpy()
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ---------------------------------------------------------------- training
|
| 221 |
+
def batches(n, bs, shuffle, rng, lengths=None):
|
| 222 |
+
"""Shuffled batches; with `lengths`, rows are sorted by length inside chunks of 50
|
| 223 |
+
batches so each batch can be trimmed to its own longest row."""
|
| 224 |
+
idx = list(range(n))
|
| 225 |
+
if shuffle:
|
| 226 |
+
rng.shuffle(idx)
|
| 227 |
+
if lengths is not None:
|
| 228 |
+
chunk = bs * 50
|
| 229 |
+
idx = [i for c in range(0, n, chunk) for i in sorted(idx[c:c + chunk], key=lambda j: lengths[j])]
|
| 230 |
+
groups = [idx[i:i + bs] for i in range(0, n, bs)]
|
| 231 |
+
if shuffle:
|
| 232 |
+
rng.shuffle(groups)
|
| 233 |
+
yield from groups
|
| 234 |
+
return
|
| 235 |
+
for i in range(0, n, bs):
|
| 236 |
+
yield idx[i:i + bs]
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def trim(x, *others):
|
| 240 |
+
"""Cut right padding to the longest row in the batch (sequences are left-aligned)."""
|
| 241 |
+
L = max(1, int((x != PAD).sum(1).max()))
|
| 242 |
+
return (x[:, :L],) + tuple(o[:, :L] for o in others)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@torch.no_grad()
|
| 246 |
+
def screen_scores(model, X, bs=4096):
|
| 247 |
+
model.eval()
|
| 248 |
+
return np.concatenate([torch.sigmoid(model(trim(X[i:i + bs])[0].to(DEV))).cpu().numpy()
|
| 249 |
+
for i in range(0, len(X), bs)])
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@torch.no_grad()
|
| 253 |
+
def lm_logprob(model, X, M, bs=2048):
|
| 254 |
+
"""Mean log-prob per target token of each row's child (+EOS) given its parent."""
|
| 255 |
+
model.eval()
|
| 256 |
+
out = []
|
| 257 |
+
for i in range(0, len(X), bs):
|
| 258 |
+
x, m = trim(X[i:i + bs], M[i:i + bs])
|
| 259 |
+
x, m = x.to(DEV), m.to(DEV)
|
| 260 |
+
logp = F.log_softmax(model(x[:, :-1]), -1)
|
| 261 |
+
tgt = x[:, 1:]
|
| 262 |
+
lp = logp.gather(-1, tgt.unsqueeze(-1)).squeeze(-1) * m[:, :-1]
|
| 263 |
+
out.append((lp.sum(1) / m[:, :-1].sum(1).clamp(min=1)).cpu().numpy())
|
| 264 |
+
return np.concatenate(out)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def train_screen(Xtr, ytr, Xva, yva, V, epochs, log, rng):
|
| 268 |
+
model = Screen(V).to(DEV)
|
| 269 |
+
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
|
| 270 |
+
pos_w = ((1 - ytr.mean()) / ytr.mean()).detach().to(DEV)
|
| 271 |
+
best, best_state = -1, None
|
| 272 |
+
for ep in range(epochs):
|
| 273 |
+
model.train()
|
| 274 |
+
t0, tot = time.time(), 0.0
|
| 275 |
+
lengths = (Xtr != PAD).sum(1).tolist()
|
| 276 |
+
for b in batches(len(Xtr), 512, True, rng, lengths):
|
| 277 |
+
logits = model(trim(Xtr[b])[0].to(DEV))
|
| 278 |
+
loss = F.binary_cross_entropy_with_logits(logits, ytr[b].to(DEV), pos_weight=pos_w)
|
| 279 |
+
opt.zero_grad()
|
| 280 |
+
loss.backward()
|
| 281 |
+
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 282 |
+
opt.step()
|
| 283 |
+
tot += loss.item() * len(b)
|
| 284 |
+
va = auroc(screen_scores(model, Xva), yva.numpy())
|
| 285 |
+
log(f"screen epoch {ep + 1}/{epochs} loss {tot / len(Xtr):.4f} val_auroc {va:.4f} ({time.time() - t0:.0f}s)")
|
| 286 |
+
if va > best:
|
| 287 |
+
best, best_state = va, {k: v.clone() for k, v in model.state_dict().items()}
|
| 288 |
+
model.load_state_dict(best_state)
|
| 289 |
+
return model, best
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def train_generator(Xtr, Mtr, Xva, Mva, V, epochs, log, rng):
|
| 293 |
+
model = Generator(V).to(DEV)
|
| 294 |
+
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
|
| 295 |
+
best, best_state = -1e9, None
|
| 296 |
+
for ep in range(epochs):
|
| 297 |
+
model.train()
|
| 298 |
+
t0, tot, cnt = time.time(), 0.0, 0.0
|
| 299 |
+
lengths = (Xtr != PAD).sum(1).tolist()
|
| 300 |
+
for b in batches(len(Xtr), 256, True, rng, lengths):
|
| 301 |
+
x, m = trim(Xtr[b], Mtr[b])
|
| 302 |
+
x, m = x.to(DEV), m.to(DEV)
|
| 303 |
+
logits = model(x[:, :-1])
|
| 304 |
+
nll = F.cross_entropy(logits.reshape(-1, V), x[:, 1:].reshape(-1), reduction="none")
|
| 305 |
+
nll = (nll.view(x.shape[0], -1) * m[:, :-1]).sum() / m[:, :-1].sum()
|
| 306 |
+
opt.zero_grad()
|
| 307 |
+
nll.backward()
|
| 308 |
+
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 309 |
+
opt.step()
|
| 310 |
+
tot += nll.item()
|
| 311 |
+
cnt += 1
|
| 312 |
+
va = float(lm_logprob(model, Xva, Mva).mean())
|
| 313 |
+
log(f"generator epoch {ep + 1}/{epochs} nll {tot / cnt:.4f} val_logprob/token {va:.4f} ({time.time() - t0:.0f}s)")
|
| 314 |
+
if va > best:
|
| 315 |
+
best, best_state = va, {k: v.clone() for k, v in model.state_dict().items()}
|
| 316 |
+
model.load_state_dict(best_state)
|
| 317 |
+
return model, best
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
@torch.no_grad()
|
| 321 |
+
def sample_children(model, parents_ids, vocab, n, temperature=0.9, rng_seed=0):
|
| 322 |
+
torch.manual_seed(rng_seed)
|
| 323 |
+
model.eval()
|
| 324 |
+
out = []
|
| 325 |
+
for p in parents_ids[:n]:
|
| 326 |
+
seq = [BOS] + p + [SEP]
|
| 327 |
+
child = []
|
| 328 |
+
for _ in range(MAXG + 1):
|
| 329 |
+
logits = model(torch.tensor([seq], device=DEV))[0, -1] / temperature
|
| 330 |
+
logits[[PAD, CLS, SEP, BOS, UNK]] = -1e9
|
| 331 |
+
tok = int(torch.multinomial(F.softmax(logits, -1), 1))
|
| 332 |
+
if tok == EOS:
|
| 333 |
+
break
|
| 334 |
+
child.append(tok)
|
| 335 |
+
seq.append(tok)
|
| 336 |
+
out.append([vocab[t] for t in child])
|
| 337 |
+
return out
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def main():
|
| 341 |
+
ap = argparse.ArgumentParser()
|
| 342 |
+
ap.add_argument("data")
|
| 343 |
+
ap.add_argument("outdir")
|
| 344 |
+
ap.add_argument("--threads", type=int, default=os.cpu_count())
|
| 345 |
+
ap.add_argument("--epochs-screen", type=int, default=4)
|
| 346 |
+
ap.add_argument("--epochs-gen", type=int, default=6)
|
| 347 |
+
ap.add_argument("--max-train", type=int, default=0, help="cap training rows (0 = all), for a smoke run")
|
| 348 |
+
ap.add_argument("--samples", type=int, default=500)
|
| 349 |
+
ap.add_argument("--device", default="cpu")
|
| 350 |
+
a = ap.parse_args()
|
| 351 |
+
global DEV
|
| 352 |
+
DEV = torch.device(a.device)
|
| 353 |
+
torch.set_num_threads(a.threads)
|
| 354 |
+
os.makedirs(a.outdir, exist_ok=True)
|
| 355 |
+
logf = open(os.path.join(a.outdir, "train.log"), "a", encoding="utf-8")
|
| 356 |
+
|
| 357 |
+
def log(msg):
|
| 358 |
+
line = f"{time.strftime('%H:%M:%S')} {msg}"
|
| 359 |
+
print(line, flush=True)
|
| 360 |
+
logf.write(line + "\n")
|
| 361 |
+
logf.flush()
|
| 362 |
+
|
| 363 |
+
random.seed(0)
|
| 364 |
+
np.random.seed(0)
|
| 365 |
+
torch.manual_seed(0)
|
| 366 |
+
rng = random.Random(0)
|
| 367 |
+
|
| 368 |
+
rows = load(a.data)
|
| 369 |
+
tr, va, te, cuts = split_by_epoch(rows)
|
| 370 |
+
if a.max_train:
|
| 371 |
+
rng.shuffle(tr)
|
| 372 |
+
tr = tr[:a.max_train]
|
| 373 |
+
vocab = build_vocab(tr)
|
| 374 |
+
idx = {t: i for i, t in enumerate(vocab)}
|
| 375 |
+
train_children = {r["child_id"] for r in tr}
|
| 376 |
+
novel = np.array([r["child_id"] not in train_children for r in te])
|
| 377 |
+
log(f"rows {len(rows)} train {len(tr)} val {len(va)} test {len(te)} (novel test {int(novel.sum())}); "
|
| 378 |
+
f"epoch cuts {cuts}; vocab {len(vocab)}; train base rate {np.mean([r['label'] for r in tr]):.3f}")
|
| 379 |
+
|
| 380 |
+
# baselines
|
| 381 |
+
yte = np.array([r["label"] for r in te])
|
| 382 |
+
ftr = np.array([features(r) for r in tr[:200000]], dtype=np.float64)
|
| 383 |
+
fte = np.array([features(r) for r in te], dtype=np.float64)
|
| 384 |
+
results = {"split": {"epoch_cuts": cuts, "train": len(tr), "val": len(va), "test": len(te),
|
| 385 |
+
"novel_test": int(novel.sum())}, "test": [], "test_novel": []}
|
| 386 |
+
lr_scores = logistic(ftr, np.array([r["label"] for r in tr[:200000]]), fte)
|
| 387 |
+
rule = fte[:, -1]
|
| 388 |
+
for name, s in (("baseline: has alloc+copy+fork", rule), ("baseline: logistic on 15 hand features", lr_scores)):
|
| 389 |
+
results["test"].append(report(name, s, yte))
|
| 390 |
+
results["test_novel"].append(report(name, s[novel], yte[novel]))
|
| 391 |
+
log(f"{name}: test auroc {results['test'][-1]['auroc']:.4f}")
|
| 392 |
+
|
| 393 |
+
# screen
|
| 394 |
+
Xtr, ytr = screen_tensor(tr, idx)
|
| 395 |
+
Xva, yva = screen_tensor(va, idx)
|
| 396 |
+
Xte, _ = screen_tensor(te, idx)
|
| 397 |
+
screen, best = train_screen(Xtr, ytr, Xva, yva, len(vocab), a.epochs_screen, log, rng)
|
| 398 |
+
s = screen_scores(screen, Xte)
|
| 399 |
+
results["test"].append(report("gridlm screen", s, yte))
|
| 400 |
+
results["test_novel"].append(report("gridlm screen", s[novel], yte[novel]))
|
| 401 |
+
log(f"screen: test auroc {results['test'][-1]['auroc']:.4f} (novel {results['test_novel'][-1]['auroc']:.4f})")
|
| 402 |
+
torch.save({k: v.cpu() for k, v in screen.state_dict().items()}, os.path.join(a.outdir, "screen.pt"))
|
| 403 |
+
|
| 404 |
+
# generator: successful children only
|
| 405 |
+
trp = [r for r in tr if r["label"] == 1]
|
| 406 |
+
vap = [r for r in va if r["label"] == 1]
|
| 407 |
+
Gtr, Mtr = lm_tensor(trp, idx)
|
| 408 |
+
Gva, Mva = lm_tensor(vap, idx)
|
| 409 |
+
gen, _ = train_generator(Gtr, Mtr, Gva, Mva, len(vocab), a.epochs_gen, log, rng)
|
| 410 |
+
torch.save({k: v.cpu() for k, v in gen.state_dict().items()}, os.path.join(a.outdir, "generator.pt"))
|
| 411 |
+
Gte, Mte = lm_tensor(te, idx)
|
| 412 |
+
lp = lm_logprob(gen, Gte, Mte)
|
| 413 |
+
results["test"].append(report("gridlm generator log p(child|parent)", lp, yte))
|
| 414 |
+
results["test_novel"].append(report("gridlm generator log p(child|parent)", lp[novel], yte[novel]))
|
| 415 |
+
log(f"generator ranking: test auroc {results['test'][-1]['auroc']:.4f}")
|
| 416 |
+
|
| 417 |
+
# samples: validity, novelty, and the screen's opinion (model judging model: indicative only)
|
| 418 |
+
test_parents = [enc_pair(r, idx)[0] for r in te]
|
| 419 |
+
rng.shuffle(test_parents)
|
| 420 |
+
kids = sample_children(gen, test_parents, vocab, a.samples)
|
| 421 |
+
parents_txt = [[vocab[t] for t in p] for p in test_parents[:a.samples]]
|
| 422 |
+
valid = [1 <= len(k) <= MAXG for k in kids]
|
| 423 |
+
afc = [all(op in k for op in ("alloc", "copy", "fork")) for k in kids]
|
| 424 |
+
same = [k == p for k, p in zip(kids, parents_txt)]
|
| 425 |
+
fake = [{"parent": p, "child": k, "label": 0, "child_id": ""} for p, k in zip(parents_txt, kids) if 1 <= len(k) <= MAXG]
|
| 426 |
+
Xs, _ = screen_tensor(fake, idx)
|
| 427 |
+
gen_scores = screen_scores(screen, Xs) if len(fake) else np.array([])
|
| 428 |
+
real_scores = s[: len(fake)]
|
| 429 |
+
results["samples"] = {
|
| 430 |
+
"n": len(kids), "valid_length": float(np.mean(valid)), "has_alloc_copy_fork": float(np.mean(afc)),
|
| 431 |
+
"identical_to_parent": float(np.mean(same)),
|
| 432 |
+
"screen_mean_p_generated": float(gen_scores.mean()) if len(gen_scores) else None,
|
| 433 |
+
"screen_mean_p_real_test_children": float(real_scores.mean()),
|
| 434 |
+
"note": "screen scores on generated children are one model judging another; only a live colony is ground truth",
|
| 435 |
+
}
|
| 436 |
+
with open(os.path.join(a.outdir, "samples.jsonl"), "w", encoding="utf-8") as f:
|
| 437 |
+
for p, k in zip(parents_txt, kids):
|
| 438 |
+
f.write(json.dumps({"parent": p, "child": k}) + "\n")
|
| 439 |
+
results["vocab"] = vocab
|
| 440 |
+
results["config"] = {"d_model": 128, "layers": 4, "heads": 4, "ff": 512, "max_genome": MAXG,
|
| 441 |
+
"label": "child genome born >= 2 times in its epoch",
|
| 442 |
+
"screen_params": sum(p.numel() for p in screen.parameters()),
|
| 443 |
+
"generator_params": sum(p.numel() for p in gen.parameters())}
|
| 444 |
+
with open(os.path.join(a.outdir, "results.json"), "w", encoding="utf-8") as f:
|
| 445 |
+
json.dump(results, f, indent=1)
|
| 446 |
+
with open(os.path.join(a.outdir, "vocab.json"), "w", encoding="utf-8") as f:
|
| 447 |
+
json.dump(vocab, f)
|
| 448 |
+
log("done: " + json.dumps(results["samples"]))
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
if __name__ == "__main__":
|
| 452 |
+
main()
|
vocab.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["[PAD]", "[CLS]", "[SEP]", "[BOS]", "[EOS]", "[UNK]", "add", "alloc", "build", "burn", "call", "copy", "copyn", "corrupt", "dec", "define", "fetch", "fork", "free", "harvest", "ifnotdone", "ifzero", "inc", "input", "jmpb", "jmpr", "link", "listen", "load", "locate", "macro0", "macro1", "macro2", "macro3", "macro4", "macro5", "macro6", "macro7", "move", "nand", "nop", "offer", "output", "peek", "post", "publish", "push", "salvage", "scan", "signal", "steal", "store", "sub", "swap", "write", "xor"]
|