Layer 1.5: architecture-general neural existence proof
Re-scopes Layer 2 into a cheaper, architecture-general neural collapse proof
before the LLM rung. Realises the same Wright–Fisher abstractions in real trained
generative models on a fully-synthetic sandbox with an exact oracle, reusing
knowledge.metrics/truth/seeding and the output contract so neural curves overlay
the Layer-1 analytic curves.
- src/neural/: synthetic token-grammar sandbox (lossless identity + stochastic
style), ExactOracle, HistogramModel bridge, generation loop, experiment runner
- HARD GATE passed: histogram lineage reproduces Layer 1 exactly (neutral decay,
exact H_eq, tracks run_lineage) — tests/test_neural_validation.py
- torch models: autoregressive RNN + MLP (VAE implemented, not yet fidelity-
passing); determinism seeding derived from the SeedSequence stream
- N0 bridge (neural g*=0.047 ≈ Layer-1 0.048), N1 collapse-in-weights, N2 phase
boundary, N5 architecture-generality (collapse + grounding-rescue in histogram
+ RNN + MLP). Manifests/configs committed; parquet gitignored, hashes tracked
- additive backward-compatible save_artifacts extension; Makefile neural targets
Finding: neural smoothing partially resists H-collapse, so forward-KL and tail
survival are the sharp neural collapse metrics (H is smooth, per Layer 1).
92 tests green. Remaining (tasks/todo.md): N4 merge, N2 refine, N3/N6, VAE
fidelity, MNIST tier, figures. LLM/LoRA rung and C3 deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1721d047fa
commit
840b6b00b3
35 changed files with 3679 additions and 23 deletions
109
src/neural/torch_mlp.py
Normal file
109
src/neural/torch_mlp.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Autoregressive MLP generative model (Stage C, for the N5 architecture-generality axis).
|
||||
|
||||
A causal feed-forward next-token model: token ``i`` is predicted from the concatenated
|
||||
(causally-masked) embeddings of all earlier tokens. Deliberately a *different* inductive
|
||||
bias from the GRU — if collapse appears here too, it is a property of the transmission
|
||||
operator, not of any one architecture.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .config import ModelCfg, SyntheticCfg
|
||||
from .oracle import Oracle
|
||||
from .torch_models import _BaseTorchGenerator
|
||||
from .train import device_generator, seed_everything
|
||||
|
||||
|
||||
def _make_mlp_net(V: int, L: int, embed: int, hidden: int):
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
class MLPNet(nn.Module):
|
||||
"""Predict every position from a causally-masked flatten of prior embeddings."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.V, self.L, self.E = V, L, embed
|
||||
self.bos = V
|
||||
self.embed = nn.Embedding(V + 1, embed)
|
||||
self.net = nn.Sequential(
|
||||
nn.Linear(L * embed, hidden), nn.ReLU(),
|
||||
nn.Linear(hidden, hidden), nn.ReLU(),
|
||||
nn.Linear(hidden, V),
|
||||
)
|
||||
# lower-triangular INCLUSIVE mask over input positions: position i sees inputs
|
||||
# 0..i (the input is already shifted by one, so this is strictly causal on x).
|
||||
mask = torch.tril(torch.ones(L, L))
|
||||
self.register_buffer("mask", mask)
|
||||
|
||||
def _context(self, inp): # inp: (B, L) input tokens
|
||||
B = inp.shape[0]
|
||||
emb = self.embed(inp) # (B, L, E)
|
||||
m = self.mask.to(emb.dtype) # (L, L)
|
||||
# ctx[b, i] = concat_j ( emb[b, j] * mask[i, j] ) -> (B, L, L*E)
|
||||
ctx = emb.unsqueeze(1) * m.unsqueeze(0).unsqueeze(-1) # (B, L, L, E)
|
||||
return ctx.reshape(B, self.L, self.L * self.E)
|
||||
|
||||
def forward(self, x): # x: (B, L) targets
|
||||
B = x.shape[0]
|
||||
bos = torch.full((B, 1), self.bos, dtype=torch.long, device=x.device)
|
||||
inp = torch.cat([bos, x[:, :-1]], dim=1)
|
||||
ctx = self._context(inp)
|
||||
return self.net(ctx) # (B, L, V)
|
||||
|
||||
def step_logits(self, prefix): # prefix: (B, pos) tokens so far
|
||||
"""Logits for the next token given the tokens generated so far."""
|
||||
B, pos = prefix.shape
|
||||
bos = torch.full((B, 1), self.bos, dtype=torch.long, device=prefix.device)
|
||||
inp = torch.cat([bos, prefix], dim=1)[:, : self.L] # (B, <=L)
|
||||
if inp.shape[1] < self.L:
|
||||
pad = torch.zeros((B, self.L - inp.shape[1]), dtype=torch.long,
|
||||
device=prefix.device)
|
||||
inp = torch.cat([inp, pad], dim=1)
|
||||
ctx = self._context(inp) # (B, L, L*E)
|
||||
return self.net(ctx[:, pos, :]) # logits at position `pos`
|
||||
|
||||
return MLPNet()
|
||||
|
||||
|
||||
class MLPGenerator(_BaseTorchGenerator):
|
||||
"""Autoregressive feed-forward generative model over token sequences."""
|
||||
|
||||
def fit(self, X: np.ndarray, rng: np.random.Generator) -> None:
|
||||
import torch
|
||||
|
||||
g = seed_everything(int(rng.integers(2 ** 31)))
|
||||
net = _make_mlp_net(self.V, self.L, self.mcfg.embed, self.mcfg.hidden).to(self.device)
|
||||
net.train()
|
||||
opt = torch.optim.Adam(net.parameters(), lr=self.mcfg.lr)
|
||||
loss_fn = torch.nn.CrossEntropyLoss()
|
||||
data = torch.as_tensor(np.asarray(X), dtype=torch.long, device=self.device)
|
||||
n, bs = data.shape[0], self.mcfg.batch_size
|
||||
for _ in range(self.mcfg.epochs):
|
||||
perm = torch.randperm(n, generator=g).to(self.device)
|
||||
for i in range(0, n, bs):
|
||||
batch = data[perm[i:i + bs]]
|
||||
logits = net(batch)
|
||||
loss = loss_fn(logits.reshape(-1, self.V), batch.reshape(-1))
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
net.eval()
|
||||
self.net = net
|
||||
|
||||
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
||||
import torch
|
||||
|
||||
if self.net is None:
|
||||
raise RuntimeError("MLPGenerator.sample called before fit")
|
||||
g = device_generator(int(rng.integers(2 ** 31)), self.device)
|
||||
prefix = torch.empty((n, 0), dtype=torch.long, device=self.device)
|
||||
with torch.no_grad():
|
||||
for pos in range(self.L):
|
||||
logits = self.net.step_logits(prefix)
|
||||
probs = torch.softmax(logits, dim=-1)
|
||||
tok = torch.multinomial(probs, 1, generator=g)
|
||||
prefix = torch.cat([prefix, tok], dim=1)
|
||||
return prefix.cpu().numpy()
|
||||
Loading…
Add table
Add a link
Reference in a new issue