Prizma / src /data.py
AylinMaylinn's picture
Publish Prizma — mirror of github.com/nazmiefearmutcu/Prizma (PRISM-Seq §4 bar + continual-learning Prizma)
5066d15 verified
Raw
History Blame Contribute Delete
7.55 kB
"""
Deterministic synthetic continual-learning benchmarks for Prizma.
Everything is offline and seeded. Two task constructions are provided:
* permuted_tasks -- analog of Permuted-MNIST. A single base classification
problem; task t applies a fixed random *feature permutation*
pi_t to the inputs. Output classes are shared. This is the
canonical *strong* catastrophic-forgetting setup: the same
input coordinates mean something different in each task, so
sequential training overwrites prior tasks.
* split_tasks -- analog of Split-MNIST. The base classes are partitioned into
disjoint groups; task t asks the network to classify only the
classes in group t (re-indexed to 0..g-1). Different tasks live
in different input regions, so a *router* can tell them apart
from input statistics alone -- the regime Prizma's gate exploits.
The base dataset is labelled by a fixed random "teacher" MLP, which guarantees a
non-linear, learnable structure (a linear model cannot solve it) without any download.
"""
from __future__ import annotations
import numpy as np
# --------------------------------------------------------------------------- #
# Base dataset: teacher-MLP-labelled synthetic classification
# --------------------------------------------------------------------------- #
def _teacher_logits(X, params):
"""Forward pass of a fixed random 2-hidden-layer tanh teacher network."""
(W1, b1, W2, b2, W3, b3) = params
h1 = np.tanh(X @ W1 + b1)
h2 = np.tanh(h1 @ W2 + b2)
return h2 @ W3 + b3
def make_base_dataset(n_samples=6000, d=20, n_classes=10, hidden=64, seed=0):
"""Return (X, y) for a non-linearly separable synthetic classification task.
X ~ N(0, I) in R^d, labelled by a fixed random teacher MLP. Returns float32
inputs in [-1, 1] (squashed) and int labels. Deterministic in `seed`.
"""
rng = np.random.default_rng(seed)
W1 = rng.normal(0, 1.0 / np.sqrt(d), (d, hidden))
b1 = rng.normal(0, 0.1, hidden)
W2 = rng.normal(0, 1.0 / np.sqrt(hidden), (hidden, hidden))
b2 = rng.normal(0, 0.1, hidden)
W3 = rng.normal(0, 1.0 / np.sqrt(hidden), (hidden, n_classes))
b3 = rng.normal(0, 0.1, n_classes)
params = (W1, b1, W2, b2, W3, b3)
X = rng.normal(0, 1.0, (n_samples, d)).astype(np.float32)
logits = _teacher_logits(X, params)
y = np.argmax(logits, axis=1).astype(np.int64)
# squash inputs to a bounded analog-friendly range
X = np.tanh(X).astype(np.float32)
return X, y
def _split_train_test(X, y, test_frac=0.2, seed=0):
rng = np.random.default_rng(seed + 999)
n = len(X)
idx = rng.permutation(n)
n_test = int(n * test_frac)
te, tr = idx[:n_test], idx[n_test:]
return (X[tr], y[tr]), (X[te], y[te])
# --------------------------------------------------------------------------- #
# Continual-learning task sequences
# --------------------------------------------------------------------------- #
class Task:
"""A single continual-learning task: train/test splits + metadata."""
def __init__(self, name, Xtr, ytr, Xte, yte, n_classes):
self.name = name
self.Xtr, self.ytr = Xtr, ytr
self.Xte, self.yte = Xte, yte
self.n_classes = n_classes
def __repr__(self):
return (f"Task({self.name}, train={len(self.Xtr)}, test={len(self.Xte)}, "
f"classes={self.n_classes})")
def permuted_tasks(n_tasks=5, n_samples=6000, d=20, n_classes=10, seed=0):
"""Permuted-MNIST analog. Each task applies a fixed feature permutation."""
X, y = make_base_dataset(n_samples=n_samples, d=d, n_classes=n_classes, seed=seed)
(Xtr, ytr), (Xte, yte) = _split_train_test(X, y, seed=seed)
rng = np.random.default_rng(seed + 1)
tasks = []
for t in range(n_tasks):
perm = np.arange(d) if t == 0 else rng.permutation(d)
tasks.append(Task(
name=f"perm{t}",
Xtr=Xtr[:, perm].copy(), ytr=ytr.copy(),
Xte=Xte[:, perm].copy(), yte=yte.copy(),
n_classes=n_classes,
))
return tasks
def structured_permuted_tasks(n_tasks=5, n_samples=6000, d=24, k_latent=8,
n_classes=8, seed=0, noise_std=0.0):
"""Domain-incremental stream that is genuinely INPUT-DISTINGUISHABLE.
Base features are a fixed linear mixing of latent factors: v = latent @ A^T,
latent ~ N(0, I_k), so cov(v) = A A^T != I (correlated features). Labels come from a
teacher applied to the LATENTS (shared across tasks). Task t permutes the observed
features by pi_t, so cov(x_t) = P_t (A A^T) P_t^T differs per task -> an autoencoder /
recognizer CAN tell domains apart from the input alone (unlike permuted iid-Gaussian,
where permutation leaves the distribution invariant). Naive sequential training still
forgets: the input->label map differs per permutation and overwrites shared weights.
"""
rng = np.random.default_rng(seed)
A = rng.normal(0, 1.0, (d, k_latent)).astype(np.float32) # fixed mixing
latent = rng.normal(0, 1.0, (n_samples, k_latent)).astype(np.float32)
v = latent @ A.T # correlated features
v = (v / (v.std(0, keepdims=True) + 1e-6)).astype(np.float32)
if noise_std > 0:
# iid (permutation-invariant) noise dilutes the structured signal -> shrinks the
# recognition margin. A separability knob: noise_std=0 -> cleanly distinguishable;
# large noise_std -> domains indistinguishable (Prizma should degrade to naive).
v = (v + noise_std * rng.normal(0, 1, v.shape)).astype(np.float32)
# teacher labels on the LATENTS (shared rule, domain-invariant)
Wt1 = rng.normal(0, 1.0 / np.sqrt(k_latent), (k_latent, 32))
Wt2 = rng.normal(0, 1.0 / np.sqrt(32), (32, n_classes))
y = np.argmax(np.tanh(latent @ Wt1) @ Wt2, axis=1).astype(np.int64)
(vtr, ytr), (vte, yte) = _split_train_test(v, y, seed=seed)
rng2 = np.random.default_rng(seed + 1)
tasks = []
for t in range(n_tasks):
perm = np.arange(d) if t == 0 else rng2.permutation(d)
tasks.append(Task(
name=f"sperm{t}",
Xtr=vtr[:, perm].copy(), ytr=ytr.copy(),
Xte=vte[:, perm].copy(), yte=yte.copy(),
n_classes=n_classes,
))
return tasks
def split_tasks(n_tasks=5, classes_per_task=2, n_samples=12000, d=20, seed=0):
"""Split-MNIST analog. Disjoint class groups; labels re-indexed per task."""
n_classes = n_tasks * classes_per_task
X, y = make_base_dataset(n_samples=n_samples, d=d, n_classes=n_classes, seed=seed)
(Xtr, ytr), (Xte, yte) = _split_train_test(X, y, seed=seed)
tasks = []
for t in range(n_tasks):
lo = t * classes_per_task
hi = lo + classes_per_task
cls = list(range(lo, hi))
mtr = np.isin(ytr, cls)
mte = np.isin(yte, cls)
tasks.append(Task(
name=f"split{t}:{cls}",
Xtr=Xtr[mtr].copy(), ytr=(ytr[mtr] - lo).copy(),
Xte=Xte[mte].copy(), yte=(yte[mte] - lo).copy(),
n_classes=classes_per_task,
))
return tasks
if __name__ == "__main__":
for tasks, label in [(permuted_tasks(), "PERMUTED"), (split_tasks(), "SPLIT")]:
print(f"\n=== {label} ===")
for t in tasks:
print(" ", t)