feat: modernize PSO and add convergence research

Migrate the package and examples to the tensor-native PyTorch implementation, add benchmark evidence, and add the guarded post-training convergence protocol with TensorBoard progress monitoring and hash-verified recovery.

Constraint: Preserve one-shot official-test sealing and auditable research artifacts

Rejected: Commit local .omc runs and downloaded datasets | multi-gigabyte runtime state is machine-local

Confidence: high

Scope-risk: broad

Not-tested: Production CUDA run on pieroot-server
This commit is contained in:
2026-09-07 22:03:25 +09:00
parent 9745bb7ad4
commit 813433000a
120 changed files with 198148 additions and 4745 deletions
+119 -53
View File
@@ -1,72 +1,138 @@
import os
"""Dry Bean dataset gradient baseline (PyTorch standard backprop optimizer, non-PSO)."""
from keras.layers import Dense
from keras.models import Sequential
from keras.utils import to_categorical
import copy
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
from tensorflow import keras
from sklearn.preprocessing import LabelEncoder
from ucimlrepo import fetch_ucirepo
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
class BeanModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(16, 12),
nn.ReLU(),
nn.Linear(12, 8),
nn.ReLU(),
nn.Linear(8, 7),
)
def forward(self, x):
return self.net(x)
def make_model():
model = Sequential()
model.add(Dense(12, input_dim=16, activation="relu"))
model.add(Dense(8, activation="relu"))
model.add(Dense(7, activation="softmax"))
return model
def get_data():
# fetch dataset
def get_data(seed: int = 42):
dry_bean_dataset = fetch_ucirepo(id=602)
# data (as pandas dataframes)
X = dry_bean_dataset.data.features
y = dry_bean_dataset.data.targets
x = X.to_numpy()
# object to categorical
x = x.astype("float32")
y_class = to_categorical(y)
# metadata
# print(dry_bean_dataset.metadata)
# variable information
# print(dry_bean_dataset.variables)
# print(X.head())
# print(y.head())
# y_class = to_categorical(y)
x = X.to_numpy().astype("float32")
encoder = LabelEncoder()
y_encoded = encoder.fit_transform(y.values.ravel()).astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
x, y_class, test_size=0.2, random_state=42, shuffle=True
x, y_encoded, test_size=0.2, random_state=seed, shuffle=True
)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(y_test, dtype=torch.int64),
)
return x_train, x_test, y_train, y_test
x_train, x_test, y_train, y_test = get_data()
model = make_model()
early_stopping = keras.callbacks.EarlyStopping(
patience=10, min_delta=0.001, restore_best_weights=True
)
def get_device() -> torch.device:
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_built()
and torch.backends.mps.is_available()
):
return torch.device("mps")
elif torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
model.compile(
loss="sparse_categorical_crossentropy",
optimizer="adam",
metrics=["accuracy", "mse"],
)
def main():
torch.manual_seed(42)
np.random.seed(42)
model.summary()
device = get_device()
print(f"Selected device: {device}")
history = model.fit(
x_train, y_train, epochs=150, batch_size=10, callbacks=[early_stopping]
)
score = model.evaluate(x_test, y_test, verbose=2)
x_train, x_test, y_train, y_test = get_data(seed=42)
train_dataset = TensorDataset(x_train, y_train)
val_dataset = TensorDataset(x_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=10, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=10, shuffle=False)
model = BeanModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
best_val_loss = float("inf")
best_state = None
patience = 10
min_delta = 0.001
patience_counter = 0
for epoch in range(150):
model.train()
for bx, by in train_loader:
bx, by = bx.to(device), by.to(device)
optimizer.zero_grad()
out = model(bx)
loss = criterion(out, by)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0.0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
val_loss += loss.item() * bx.size(0)
total += bx.size(0)
val_loss /= total
if val_loss < best_val_loss - min_delta:
best_val_loss = val_loss
best_state = copy.deepcopy(model.state_dict())
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
break
if best_state is not None:
model.load_state_dict(best_state)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
test_loss += loss.item() * bx.size(0)
preds = out.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
test_loss /= total
test_acc = correct / total
print(f"Final test loss: {test_loss:.4f}, accuracy: {test_acc:.4f}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+490
View File
@@ -0,0 +1,490 @@
"""
Command-Line Interface Helpers for PSO Experiments.
Provides standard argparse argument groups and helper functions for PSO stage selectors
(method, initialization, evaluation, convergence, refinement) and common execution parameters.
"""
import argparse
from typing import Any
# Supported stage plugin selector options
STAGE_SELECTORS: dict[str, list[str]] = {
"method": [
"original",
"inertia",
"constriction",
"fips",
"clpso",
"bare_bones",
"adaptive_moment",
],
"initialization": ["model_noise", "uniform"],
"evaluation": ["full", "fixed_subset"],
"convergence": ["none", "particle_reset", "early_stopping"],
"refinement": ["none", "adam"],
}
def add_stage_selector_args(
parser: argparse.ArgumentParser, defaults: dict[str, Any] | None = None
) -> argparse.ArgumentParser:
"""Adds the 5 explicit stage selector arguments to an argparse parser."""
defaults = defaults or {}
parser.add_argument(
"--method",
type=str,
choices=STAGE_SELECTORS["method"],
default=defaults.get("method", "original"),
help="PSO movement stage method (default: %(default)s)",
)
parser.add_argument(
"--initialization",
type=str,
choices=STAGE_SELECTORS["initialization"],
default=defaults.get("initialization", "model_noise"),
help="PSO particle initialization stage (default: %(default)s)",
)
parser.add_argument(
"--evaluation",
type=str,
choices=STAGE_SELECTORS["evaluation"],
default=defaults.get("evaluation", "full"),
help="PSO objective evaluation stage (default: %(default)s)",
)
parser.add_argument(
"--convergence",
type=str,
choices=STAGE_SELECTORS["convergence"],
default=defaults.get("convergence", "none"),
help="PSO convergence behavior stage (default: %(default)s)",
)
parser.add_argument(
"--refinement",
type=str,
choices=STAGE_SELECTORS["refinement"],
default=defaults.get("refinement", "none"),
help="PSO post-search refinement stage (default: %(default)s)",
)
return parser
def add_pso_args(
parser: argparse.ArgumentParser, defaults: dict[str, Any] | None = None
) -> argparse.ArgumentParser:
"""Adds stage selectors and common PSO hyperparameter arguments to an argparse parser."""
defaults = defaults or {}
# Add 5 stage selectors
add_stage_selector_args(parser, defaults)
# Core execution and hyperparameter options
parser.add_argument(
"--seed",
type=int,
default=defaults.get("seed", 42),
help="Random seed for reproducibility (default: %(default)s)",
)
parser.add_argument(
"--device",
type=str,
default=defaults.get("device", None),
help="Execution target device (cpu, cuda, mps) (default: auto)",
)
parser.add_argument(
"--n-particles",
"--particles",
dest="n_particles",
type=int,
default=defaults.get("n_particles", 30),
help="Number of swarm particles (default: %(default)s)",
)
parser.add_argument(
"--epochs",
type=int,
default=defaults.get("epochs", 80),
help="Number of PSO optimization epochs (default: %(default)s)",
)
parser.add_argument(
"--batch-size",
"--batch",
dest="batch_size",
type=int,
default=defaults.get("batch_size", None),
help="Batch size for objective evaluation (default: %(default)s)",
)
parser.add_argument(
"--fitness-size",
type=int,
default=defaults.get("fitness_size", None),
help="Fixed subset sample count (required when evaluation='fixed_subset')",
)
parser.add_argument(
"--refinement-epochs",
type=int,
default=defaults.get("refinement_epochs", 0),
help="Refinement epoch count (required when refinement='adam')",
)
parser.add_argument(
"--refinement-lr",
type=float,
default=defaults.get("refinement_lr", 0.001),
help="Refinement learning rate for Adam optimizer (default: %(default)s)",
)
parser.add_argument(
"--renewal",
type=str,
choices=["acc", "loss", "mse"],
default=defaults.get("renewal", "loss"),
help="Primary metric for global best selection (default: %(default)s)",
)
parser.add_argument(
"--output-dir",
type=str,
default=defaults.get("output_dir", None),
help="Directory to save model checkpoints and logs",
)
# Optional coefficient overrides
parser.add_argument(
"--c0",
type=float,
default=defaults.get("c0", None),
help="Cognitive acceleration coefficient override",
)
parser.add_argument(
"--c1",
type=float,
default=defaults.get("c1", None),
help="Social acceleration coefficient override",
)
parser.add_argument(
"--w-min",
dest="w_min",
type=float,
default=defaults.get("w_min", None),
help="Minimum inertia weight override",
)
parser.add_argument(
"--w-max",
dest="w_max",
type=float,
default=defaults.get("w_max", None),
help="Maximum inertia weight override",
)
parser.add_argument(
"--negative-swarm",
type=float,
default=defaults.get("negative_swarm", 0.0),
help="Negative swarm velocity coefficient (default: %(default)s)",
)
parser.add_argument(
"--mutation-swarm",
type=float,
default=defaults.get("mutation_swarm", 0.0),
help="Swarm mutation probability (default: %(default)s)",
)
parser.add_argument(
"--particle-min",
type=float,
default=defaults.get("particle_min", None),
help="Lower bound for particle position clamping",
)
parser.add_argument(
"--particle-max",
type=float,
default=defaults.get("particle_max", None),
help="Upper bound for particle position clamping",
)
parser.add_argument(
"--velocity-limit-ratio",
type=float,
default=defaults.get("velocity_limit_ratio", None),
help="Maximum velocity limit ratio relative to search domain",
)
parser.add_argument(
"--boundary-strategy",
type=str,
choices=["clip", "reflect"],
default=defaults.get("boundary_strategy", "clip"),
help="Position boundary handling strategy (default: %(default)s)",
)
parser.add_argument(
"--initial-position-noise",
type=float,
default=defaults.get("initial_position_noise", 0.05),
help="Initial position noise scale (default: %(default)s)",
)
# Convergence stage options
parser.add_argument(
"--convergence-patience",
dest="convergence_patience",
type=int,
default=defaults.get("convergence_patience", 10),
help="Convergence reset/early stopping patience epochs (default: %(default)s)",
)
parser.add_argument(
"--convergence-min-delta",
dest="convergence_min_delta",
type=float,
default=defaults.get("convergence_min_delta", 0.0001),
help="Minimum improvement delta for convergence (default: %(default)s)",
)
parser.add_argument(
"--convergence-monitor",
dest="convergence_monitor",
type=str,
choices=["loss", "acc", "accuracy", "mse"],
default=defaults.get("convergence_monitor", "loss"),
help="Metric monitored for convergence (default: %(default)s)",
)
# Adaptive moment options
parser.add_argument(
"--moment-blend",
dest="moment_blend",
type=float,
default=defaults.get("moment_blend", None),
help="Adaptive moment blend factor (default: 0.25 when method='adaptive_moment', else 0.0)",
)
parser.add_argument(
"--moment-beta1",
dest="moment_beta1",
type=float,
default=defaults.get("moment_beta1", None),
help="Adaptive moment beta1 parameter (default: 0.9 when method='adaptive_moment')",
)
parser.add_argument(
"--moment-beta2",
dest="moment_beta2",
type=float,
default=defaults.get("moment_beta2", None),
help="Adaptive moment beta2 parameter (default: 0.999 when method='adaptive_moment')",
)
parser.add_argument(
"--moment-step-size",
dest="moment_step_size",
type=float,
default=defaults.get("moment_step_size", None),
help="Adaptive moment step size (default: 1.0 when method='adaptive_moment')",
)
parser.add_argument(
"--moment-epsilon",
dest="moment_epsilon",
type=float,
default=defaults.get("moment_epsilon", None),
help="Adaptive moment epsilon parameter (default: 1e-8 when method='adaptive_moment')",
)
# Repeatable method options
parser.add_argument(
"--method-option",
dest="method_options",
action="append",
metavar="KEY=VALUE",
help="Additional key=value option for movement method (repeatable)",
)
return parser
def parse_method_options(options: list[str] | None) -> dict[str, Any]:
"""Parses a list of 'KEY=VALUE' strings into a dictionary with typed values."""
res: dict[str, Any] = {}
if not options:
return res
for opt in options:
if "=" not in opt:
raise ValueError(f"Invalid --method-option format '{opt}', expected 'KEY=VALUE'")
key, val = opt.split("=", 1)
key = key.strip()
val = val.strip()
val_lower = val.lower()
if val_lower == "true":
parsed_val: Any = True
elif val_lower == "false":
parsed_val = False
else:
try:
parsed_val = int(val)
except ValueError:
try:
parsed_val = float(val)
except ValueError:
parsed_val = val
res[key] = parsed_val
return res
def build_optimizer_kwargs(
args: argparse.Namespace,
*,
model: Any = None,
loss: Any = None,
task: str | None = None,
inertia_profile: dict[str, float] | None = None,
**extra_kwargs: Any,
) -> dict[str, Any]:
"""Builds Optimizer constructor keyword arguments from parsed CLI arguments.
Applies method-specific parameter compatibility rules, workload inertia profiles
(only when method='inertia'), repeatable method options (--method-option), and
adaptive moment flags (only when method='adaptive_moment').
"""
method = getattr(args, "method", "original")
parsed_method_opts = parse_method_options(getattr(args, "method_options", None))
if method == "inertia":
profile = inertia_profile or {}
c0 = (
parsed_method_opts["c0"]
if "c0" in parsed_method_opts
else (args.c0 if getattr(args, "c0", None) is not None else profile.get("c0"))
)
c1 = (
parsed_method_opts["c1"]
if "c1" in parsed_method_opts
else (args.c1 if getattr(args, "c1", None) is not None else profile.get("c1"))
)
w_min = (
parsed_method_opts["w_min"]
if "w_min" in parsed_method_opts
else (args.w_min if getattr(args, "w_min", None) is not None else profile.get("w_min"))
)
w_max = (
parsed_method_opts["w_max"]
if "w_max" in parsed_method_opts
else (args.w_max if getattr(args, "w_max", None) is not None else profile.get("w_max"))
)
elif method in ("original", "constriction", "fips"):
c0 = (
parsed_method_opts["c0"]
if "c0" in parsed_method_opts
else getattr(args, "c0", None)
)
c1 = (
parsed_method_opts["c1"]
if "c1" in parsed_method_opts
else getattr(args, "c1", None)
)
w_min = None
w_max = None
elif method == "bare_bones":
c0 = None
c1 = None
w_min = None
w_max = None
else:
c0 = (
parsed_method_opts["c0"]
if "c0" in parsed_method_opts
else getattr(args, "c0", None)
)
c1 = (
parsed_method_opts["c1"]
if "c1" in parsed_method_opts
else getattr(args, "c1", None)
)
w_min = (
parsed_method_opts["w_min"]
if "w_min" in parsed_method_opts
else getattr(args, "w_min", None)
)
w_max = (
parsed_method_opts["w_max"]
if "w_max" in parsed_method_opts
else getattr(args, "w_max", None)
)
if method in ("fips", "clpso", "bare_bones"):
neg_swarm = 0.0
else:
neg_swarm = (
float(parsed_method_opts["negative_swarm"])
if "negative_swarm" in parsed_method_opts
else float(getattr(args, "negative_swarm", 0.0))
)
if method == "bare_bones":
mut_swarm = 0.0
else:
mut_swarm = (
float(parsed_method_opts["mutation_swarm"])
if "mutation_swarm" in parsed_method_opts
else float(getattr(args, "mutation_swarm", 0.0))
)
if method == "bare_bones":
vel_ratio = None
else:
vel_ratio = (
parsed_method_opts["velocity_limit_ratio"]
if "velocity_limit_ratio" in parsed_method_opts
else getattr(args, "velocity_limit_ratio", None)
)
fitness_size = (
getattr(args, "fitness_size", None)
if getattr(args, "evaluation", None) == "fixed_subset"
else None
)
refinement_epochs = (
getattr(args, "refinement_epochs", 0)
if getattr(args, "refinement", None) == "adam"
else 0
)
kwargs: dict[str, Any] = {
"model": model,
"loss": loss,
"task": task,
"method": method,
"initialization": getattr(args, "initialization", "model_noise"),
"evaluation": getattr(args, "evaluation", "full"),
"convergence": getattr(args, "convergence", "none"),
"refinement": getattr(args, "refinement", "none"),
"method_options": parsed_method_opts,
"n_particles": getattr(args, "n_particles", 30),
"c0": c0,
"c1": c1,
"w_min": w_min,
"w_max": w_max,
"negative_swarm": neg_swarm,
"mutation_swarm": mut_swarm,
"particle_min": getattr(args, "particle_min", None),
"particle_max": getattr(args, "particle_max", None),
"velocity_limit_ratio": vel_ratio,
"boundary_strategy": getattr(args, "boundary_strategy", "clip"),
"initial_position_noise": getattr(args, "initial_position_noise", 0.05),
"seed": getattr(args, "seed", None),
"device": getattr(args, "device", None),
"fitness_size": fitness_size,
"convergence_patience": getattr(args, "convergence_patience", 10),
"convergence_min_delta": getattr(args, "convergence_min_delta", 0.0001),
"convergence_monitor": getattr(args, "convergence_monitor", "loss"),
"refinement_epochs": refinement_epochs,
"refinement_lr": getattr(args, "refinement_lr", 0.001),
}
if method == "adaptive_moment":
if "moment_blend" in parsed_method_opts:
m_blend = float(parsed_method_opts["moment_blend"])
elif getattr(args, "moment_blend", None) is not None:
m_blend = float(getattr(args, "moment_blend"))
else:
m_blend = 0.25
kwargs["moment_blend"] = m_blend
for param_name in (
"moment_beta1",
"moment_beta2",
"moment_step_size",
"moment_epsilon",
):
if param_name in parsed_method_opts:
kwargs[param_name] = float(parsed_method_opts[param_name])
elif getattr(args, param_name, None) is not None:
kwargs[param_name] = float(getattr(args, param_name))
kwargs.update(extra_kwargs)
return kwargs
+427
View File
@@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""
PSO Stage Plugin Method Comparison Tool.
Compares PSO movement methods (original, inertia, constriction, fips, clpso, bare_bones,
adaptive_moment, local_best, quantum)
across benchmark datasets (xor, iris, mnist) with reproducible model initialization, dataset splits,
and clean console output & JSON result reporting.
"""
import argparse
import json
import sys
import time
from typing import Any
import torch
import torch.nn as nn
from cli import add_pso_args, parse_method_options
from pso import Optimizer
from pso.plugins import available_plugins
def print_available_methods():
"""Prints available movement method plugins and metadata provenance."""
movement_plugins = available_plugins("movement")
print("Available PSO Movement Method Plugins:")
print("=" * 80)
for name, meta in movement_plugins.items():
print(f" Stage Key : {name}")
print(f" Title : {meta.title}")
print(f" Source (DOI) : {meta.source or 'N/A'}")
print(f" Fidelity : {meta.fidelity}")
print(f" Needs Grad : {meta.gradient_required}")
print("-" * 80)
def get_xor_workload(seed: int):
"""Builds identical XOR dataset and PyTorch model state for a given seed."""
torch.manual_seed(seed)
x_train = torch.tensor(
[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], dtype=torch.float32
)
y_train = torch.tensor([[0.0], [1.0], [1.0], [0.0]], dtype=torch.float32)
model = nn.Sequential(
nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
)
loss_fn = nn.BCEWithLogitsLoss()
return x_train, y_train, None, model, loss_fn, "binary"
def get_iris_workload(seed: int):
"""Builds identical Iris dataset and PyTorch model state for a given seed."""
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
torch.manual_seed(seed)
iris = load_iris()
X = iris.data.astype("float32")
y = iris.target.astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=True, stratify=y, random_state=seed
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.transform(x_test)
x_tr = torch.tensor(x_train, dtype=torch.float32)
y_tr = torch.tensor(y_train, dtype=torch.int64)
x_te = torch.tensor(x_test, dtype=torch.float32)
y_te = torch.tensor(y_test, dtype=torch.int64)
model = nn.Sequential(
nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 3),
)
loss_fn = nn.CrossEntropyLoss()
return x_tr, y_tr, (x_te, y_te), model, loss_fn, "multiclass"
def get_mnist_workload(seed: int):
"""Builds identical PCA32 MNIST dataset and PyTorch model state for a given seed."""
from sklearn.decomposition import PCA
from torchvision.datasets import MNIST
torch.manual_seed(seed)
train_ds = MNIST(root="./data", train=True, download=True)
test_ds = MNIST(root="./data", train=False, download=True)
x_tr_raw = (train_ds.data[:3000].float() / 255.0).reshape(3000, -1).numpy()
y_tr = train_ds.targets[:3000].long()
x_te_raw = (test_ds.data[:1000].float() / 255.0).reshape(1000, -1).numpy()
y_te = test_ds.targets[:1000].long()
pca = PCA(n_components=32, whiten=True, random_state=seed)
x_tr_pca = pca.fit_transform(x_tr_raw)
x_te_pca = pca.transform(x_te_raw)
x_tr = torch.tensor(x_tr_pca, dtype=torch.float32)
x_te = torch.tensor(x_te_pca, dtype=torch.float32)
model = nn.Linear(32, 10)
loss_fn = nn.CrossEntropyLoss()
return x_tr, y_tr, (x_te, y_te), model, loss_fn, "multiclass"
DATASET_LOADERS = {
"xor": get_xor_workload,
"iris": get_iris_workload,
"mnist": get_mnist_workload,
}
def main():
parser = argparse.ArgumentParser(
description="PSO Stage Plugin Method Comparison Surface"
)
parser.add_argument(
"--list-methods",
action="store_true",
help="List available movement methods and exit",
)
parser.add_argument(
"--dataset",
choices=["xor", "iris", "mnist"],
default="xor",
help="Target dataset workload (default: %(default)s)",
)
parser.add_argument(
"--methods",
nargs="+",
default=["all"],
help="Movement method keys to evaluate or 'all' (default: all)",
)
parser.add_argument(
"--seeds",
type=int,
nargs="+",
default=[42],
help="Random seed list (default: 42)",
)
parser.add_argument(
"--json-path",
"--output-json",
"--json",
dest="json_path",
type=str,
default=None,
help="Optional JSON file path to save detailed evaluation metrics",
)
# Add standard stage selector and hyperparameter options
add_pso_args(parser, defaults={"n_particles": 20, "epochs": 30})
args = parser.parse_args()
if args.list_methods:
print_available_methods()
sys.exit(0)
# Determine movement methods to test
available_m_plugins = available_plugins("movement")
if "all" in args.methods or "ALL" in args.methods:
methods_to_test = list(available_m_plugins.keys())
else:
methods_to_test = []
for m in args.methods:
if m not in available_m_plugins:
raise ValueError(
f"Unknown movement method '{m}'. Available: {list(available_m_plugins.keys())}"
)
methods_to_test.append(m)
loader = DATASET_LOADERS[args.dataset]
eval_stage = args.evaluation
fitness_size = args.fitness_size if eval_stage == "fixed_subset" else None
if eval_stage == "fixed_subset" and fitness_size is None:
if args.dataset == "xor":
fitness_size = 4
elif args.dataset == "iris":
fitness_size = 100
elif args.dataset == "mnist":
fitness_size = 2000
refine_stage = args.refinement
refinement_epochs = args.refinement_epochs if refine_stage == "adam" else 0
results: list[dict[str, Any]] = []
parsed_method_opts = parse_method_options(args.method_options)
has_val = args.dataset != "xor"
print("=" * 80)
print(f"PSO Movement Method Comparison on '{args.dataset}' Dataset")
print(
f"Stages: initialization='{args.initialization}', evaluation='{eval_stage}', "
f"convergence='{args.convergence}', refinement='{refine_stage}'"
)
print(
f"Parameters: particles={args.n_particles}, epochs={args.epochs}, seeds={args.seeds}"
)
print("=" * 80)
if has_val:
print(
f"{'Method':<18} {'Seed':<6} {'Val Loss':<12} {'Val Accuracy':<12} {'Val MSE':<12} {'Time (s)':<10}"
)
else:
print(
f"{'Method':<18} {'Seed':<6} {'Loss':<12} {'Accuracy':<12} {'MSE':<12} {'Time (s)':<10}"
)
print("-" * 80)
for method_key in methods_to_test:
for seed in args.seeds:
x_tr, y_tr, val_data, model, loss_fn, task = loader(seed)
eff_fitness_size = (
min(fitness_size, x_tr.shape[0])
if fitness_size is not None
else None
)
c0 = args.c0
c1 = args.c1
w_min = args.w_min
w_max = args.w_max
neg_swarm = (
args.negative_swarm
if method_key not in ("fips", "clpso", "bare_bones")
else 0.0
)
mut_swarm = args.mutation_swarm if method_key != "bare_bones" else 0.0
vel_ratio = (
args.velocity_limit_ratio if method_key != "bare_bones" else None
)
kwargs: dict[str, Any] = {
"model": model,
"loss": loss_fn,
"task": task,
"method": method_key,
"initialization": args.initialization,
"evaluation": eval_stage,
"convergence": args.convergence,
"refinement": refine_stage,
"method_options": parsed_method_opts,
"n_particles": args.n_particles,
"c0": c0,
"c1": c1,
"w_min": w_min,
"w_max": w_max,
"negative_swarm": neg_swarm,
"mutation_swarm": mut_swarm,
"particle_min": args.particle_min,
"particle_max": args.particle_max,
"velocity_limit_ratio": vel_ratio,
"boundary_strategy": args.boundary_strategy,
"initial_position_noise": args.initial_position_noise,
"seed": seed,
"device": args.device,
"fitness_size": eff_fitness_size,
"convergence_patience": args.convergence_patience,
"convergence_min_delta": args.convergence_min_delta,
"convergence_monitor": args.convergence_monitor,
"refinement_epochs": refinement_epochs,
"refinement_lr": args.refinement_lr,
}
if method_key == "adaptive_moment":
if args.moment_blend is not None:
kwargs["moment_blend"] = args.moment_blend
elif "moment_blend" in parsed_method_opts:
kwargs["moment_blend"] = float(parsed_method_opts["moment_blend"])
else:
kwargs["moment_blend"] = 0.25
if args.moment_beta1 is not None:
kwargs["moment_beta1"] = args.moment_beta1
if args.moment_beta2 is not None:
kwargs["moment_beta2"] = args.moment_beta2
if args.moment_step_size is not None:
kwargs["moment_step_size"] = args.moment_step_size
if args.moment_epsilon is not None:
kwargs["moment_epsilon"] = args.moment_epsilon
opt = Optimizer(**kwargs)
start_t = time.perf_counter()
best_score = opt.fit(
x_tr,
y_tr,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=eff_fitness_size,
renewal=args.renewal,
validation_data=val_data,
output_dir=None,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
elapsed_t = time.perf_counter() - start_t
tr_loss, tr_acc, tr_mse = best_score
if val_data is not None:
val_x, val_y = val_data
val_score = opt.evaluate(val_x, val_y)
eval_loss, eval_acc, eval_mse = val_score
score_src = "validation"
else:
eval_loss, eval_acc, eval_mse = tr_loss, tr_acc, tr_mse
score_src = "training"
results.append(
{
"method": method_key,
"seed": seed,
"score_source": score_src,
"train_loss": tr_loss,
"train_accuracy": tr_acc,
"train_mse": tr_mse,
"eval_loss": eval_loss,
"eval_accuracy": eval_acc,
"eval_mse": eval_mse,
"loss": eval_loss,
"accuracy": eval_acc,
"mse": eval_mse,
"elapsed_time": elapsed_t,
}
)
print(
f"{method_key:<18} {seed:<6} {eval_loss:<12.6f} {eval_acc:<12.6f} {eval_mse:<12.6f} {elapsed_t:<10.4f}"
)
print("-" * 80)
print("\nAggregate Summary (Mean across seeds):")
print("=" * 80)
if has_val:
print(
f"{'Method':<18} {'Mean Val Loss':<14} {'Mean Val Acc':<14} {'Mean Val MSE':<14} {'Mean Time (s)':<12}"
)
else:
print(
f"{'Method':<18} {'Mean Loss':<12} {'Mean Acc':<12} {'Mean MSE':<12} {'Mean Time (s)':<12}"
)
print("-" * 80)
summary_list: list[dict[str, Any]] = []
for method_key in methods_to_test:
method_runs = [r for r in results if r["method"] == method_key]
if not method_runs:
continue
n_runs = len(method_runs)
mean_tr_loss = sum(r["train_loss"] for r in method_runs) / n_runs
mean_tr_acc = sum(r["train_accuracy"] for r in method_runs) / n_runs
mean_tr_mse = sum(r["train_mse"] for r in method_runs) / n_runs
mean_eval_loss = sum(r["eval_loss"] for r in method_runs) / n_runs
mean_eval_acc = sum(r["eval_accuracy"] for r in method_runs) / n_runs
mean_eval_mse = sum(r["eval_mse"] for r in method_runs) / n_runs
mean_time = sum(r["elapsed_time"] for r in method_runs) / n_runs
score_src = method_runs[0]["score_source"]
summary_entry = {
"method": method_key,
"title": available_m_plugins[method_key].title,
"source": available_m_plugins[method_key].source,
"score_source": score_src,
"mean_train_loss": mean_tr_loss,
"mean_train_accuracy": mean_tr_acc,
"mean_train_mse": mean_tr_mse,
"mean_eval_loss": mean_eval_loss,
"mean_eval_accuracy": mean_eval_acc,
"mean_eval_mse": mean_eval_mse,
"mean_loss": mean_eval_loss,
"mean_accuracy": mean_eval_acc,
"mean_mse": mean_eval_mse,
"mean_elapsed_time": mean_time,
"runs": n_runs,
}
summary_list.append(summary_entry)
if has_val:
print(
f"{method_key:<18} {mean_eval_loss:<14.6f} {mean_eval_acc:<14.6f} {mean_eval_mse:<14.6f} {mean_time:<12.4f}"
)
else:
print(
f"{method_key:<18} {mean_eval_loss:<12.6f} {mean_eval_acc:<12.6f} {mean_eval_mse:<12.6f} {mean_time:<12.4f}"
)
print("=" * 80)
if args.json_path:
payload = {
"dataset": args.dataset,
"score_source": "validation" if has_val else "training",
"selectors": {
"initialization": args.initialization,
"evaluation": eval_stage,
"convergence": args.convergence,
"refinement": refine_stage,
},
"parameters": {
"n_particles": args.n_particles,
"epochs": args.epochs,
"batch_size": args.batch_size,
"fitness_size": fitness_size,
"refinement_epochs": refinement_epochs,
"refinement_lr": args.refinement_lr,
"seeds": args.seeds,
},
"results": results,
"summary": summary_list,
}
with open(args.json_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
print(f"\nSaved comparison results JSON to: {args.json_path}")
if __name__ == "__main__":
main()
+932
View File
@@ -0,0 +1,932 @@
"""
MNIST Deep Accuracy Study: Architecture vs Optimizer Profiles
Evaluates official MNIST (60,000 train / 10,000 test) across:
1. Architecture Lane: Raw Linear, Raw MLP, Compact CNN under standard full-data Adam.
2. Optimizer Lane: Adam-Only, PSO-Only (adaptive_moment on 2k subset), and Hybrid (PSO warm start + Adam fine-tuning) on Compact CNN.
Contract:
- Official 60k train / 10k test split with train-only statistics normalization (no PCA).
- Avoid BatchNorm/Dropout so PSO and eval semantics match.
- Seed model construction identically per seed to preserve explicit initial state fingerprint across lanes.
- Fixed no-scheduler contract for Adam with CrossEntropyLoss and lr=1e-3.
"""
import argparse
import csv
import datetime
import json
import math
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
# Ensure test/ directory is in sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from benchmark_suite import (
calc_stats,
compute_data_fingerprint,
compute_model_fingerprint,
extract_plugin_metadata,
get_hardware_provenance,
resolve_execution_device,
save_json_atomic,
sync_device,
)
from pso import Optimizer, __version__ as pso_version
DEEP_ACCURACY_PROTOCOL_VERSION = "1.0.0"
# ==========================================
# Data Preparation (No PCA, Raw 1x28x28)
# ==========================================
def prepare_deep_accuracy_mnist_data() -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
str,
Dict[str, Any],
]:
"""
Loads official torchvision MNIST dataset (60,000 train / 10,000 test).
Normalizes images using train-only mean and std (no PCA).
Validates sample counts and label range [0, 9].
Returns (x_train, y_train, x_test, y_test, data_fingerprint, provenance_dict).
"""
from torchvision.datasets import MNIST
cache_dir = Path("result/cache")
cache_dir.mkdir(parents=True, exist_ok=True)
train_dataset = MNIST(root=str(cache_dir), train=True, download=True)
test_dataset = MNIST(root=str(cache_dir), train=False, download=True)
n_train = len(train_dataset.data)
n_test = len(test_dataset.data)
if n_train != 60000:
raise ValueError(f"Expected 60,000 training samples; got {n_train}")
if n_test != 10000:
raise ValueError(f"Expected 10,000 test samples; got {n_test}")
y_train = train_dataset.targets.long()
y_test = test_dataset.targets.long()
min_tr, max_tr = int(y_train.min()), int(y_train.max())
min_te, max_te = int(y_test.min()), int(y_test.max())
if min_tr != 0 or max_tr != 9:
raise ValueError(f"Train label range must be [0, 9]; got [{min_tr}, {max_tr}]")
if min_te != 0 or max_te != 9:
raise ValueError(f"Test label range must be [0, 9]; got [{min_te}, {max_te}]")
x_train_raw = train_dataset.data.float() / 255.0 # (60000, 28, 28)
x_test_raw = test_dataset.data.float() / 255.0 # (10000, 28, 28)
# Compute normalization statistics from TRAIN split only
mean_val = float(x_train_raw.mean())
std_val = float(x_train_raw.std())
x_train_norm = ((x_train_raw - mean_val) / std_val).unsqueeze(1) # (60000, 1, 28, 28)
x_test_norm = ((x_test_raw - mean_val) / std_val).unsqueeze(1) # (10000, 1, 28, 28)
data_fp = compute_data_fingerprint(x_train_norm, x_test_norm, y_train, y_test)
normalization_provenance = {
"input_shape": [1, 28, 28],
"pca": False,
"raw_inputs": True,
"normalization_scope": "official_train_split_60000_only",
"train_mean": round(mean_val, 6),
"train_std": round(std_val, 6),
"train_samples": 60000,
"test_samples": 10000,
}
return x_train_norm, y_train, x_test_norm, y_test, data_fp, normalization_provenance
# ==========================================
# Architectures (No BatchNorm / No Dropout)
# ==========================================
def count_parameters(model: nn.Module) -> int:
return sum(p.numel() for p in model.parameters() if p.requires_grad)
def make_raw_linear(seed: int = 41) -> nn.Module:
torch.manual_seed(seed)
return nn.Sequential(
nn.Flatten(),
nn.Linear(784, 10),
)
def make_raw_mlp(seed: int = 41) -> nn.Module:
torch.manual_seed(seed)
return nn.Sequential(
nn.Flatten(),
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 10),
)
class CompactCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, kernel_size=3, padding=1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(8, 16, kernel_size=3, padding=1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(2, 2)
self.flatten = nn.Flatten()
self.fc = nn.Linear(784, 10)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if x.dim() == 2 and x.shape[1] == 784:
x = x.view(-1, 1, 28, 28)
out = self.pool1(self.relu1(self.conv1(x)))
out = self.pool2(self.relu2(self.conv2(out)))
out = self.flatten(out)
return self.fc(out)
def make_compact_cnn(seed: int = 41) -> nn.Module:
torch.manual_seed(seed)
return CompactCNN()
# ==========================================
# Evaluation & Training Routines
# ==========================================
def evaluate_model_on_test(
model: nn.Module,
x_test: torch.Tensor,
y_test: torch.Tensor,
device: torch.device,
batch_size: int = 512,
) -> Tuple[float, float]:
"""
Evaluates model on test data without gradients.
Returns (test_loss, test_accuracy).
"""
model.to(device)
model.eval()
criterion = nn.CrossEntropyLoss()
total_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
n_test = x_test.shape[0]
for i in range(0, n_test, batch_size):
bx = x_test[i : i + batch_size].to(device)
by = y_test[i : i + batch_size].to(device)
outputs = model(bx)
loss = criterion(outputs, by)
total_loss += loss.item() * bx.size(0)
preds = outputs.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
avg_loss = total_loss / total if total > 0 else 0.0
accuracy = correct / total if total > 0 else 0.0
return float(avg_loss), float(accuracy)
def train_adam_routine(
model: nn.Module,
x_train: torch.Tensor,
y_train: torch.Tensor,
x_test: torch.Tensor,
y_test: torch.Tensor,
epochs: int,
batch_size: int,
lr: float,
seed: int,
device: torch.device,
) -> Tuple[List[Dict[str, Any]], float, float, float]:
"""
Standard full-data Adam training routine with CrossEntropyLoss, Adam lr, fixed no-scheduler contract.
Returns (history, final_test_loss, final_test_acc, elapsed_sec).
"""
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
generator = torch.Generator().manual_seed(seed)
n_train = x_train.shape[0]
history = []
init_loss, init_acc = evaluate_model_on_test(model, x_test, y_test, device)
history.append({"epoch": 0, "test_loss": round(init_loss, 6), "test_acc": round(init_acc, 6)})
sync_device(device)
t0 = time.time()
for epoch in range(1, epochs + 1):
model.train()
perm = torch.randperm(n_train, generator=generator)
for i in range(0, n_train, batch_size):
indices = perm[i : i + batch_size]
bx = x_train[indices].to(device)
by = y_train[indices].to(device)
optimizer.zero_grad()
outputs = model(bx)
loss = criterion(outputs, by)
loss.backward()
optimizer.step()
test_loss, test_acc = evaluate_model_on_test(model, x_test, y_test, device)
history.append({
"epoch": epoch,
"test_loss": round(test_loss, 6),
"test_acc": round(test_acc, 6),
})
sync_device(device)
elapsed = time.time() - t0
final_loss = history[-1]["test_loss"]
final_acc = history[-1]["test_acc"]
return history, final_loss, final_acc, elapsed
def run_pso_routine(
model: nn.Module,
x_train: torch.Tensor,
y_train: torch.Tensor,
x_test: torch.Tensor,
y_test: torch.Tensor,
pso_epochs: int,
n_particles: int,
fitness_size: int,
seed: int,
device: torch.device,
) -> Tuple[nn.Module, Dict[str, float], float, float, Dict[str, Any]]:
"""
PSO-only adaptive_moment on fixed train subset without gradient refinement.
Returns (best_model, fitness_score_dict, test_loss, test_acc, pso_metadata).
"""
model.to(device)
loss_fn = nn.CrossEntropyLoss()
opt = Optimizer(
model=model,
loss=loss_fn,
task="multiclass",
method="adaptive_moment",
evaluation="fixed_subset",
fitness_size=fitness_size,
n_particles=n_particles,
c0=1.49618,
c1=1.49618,
w_min=0.7298,
w_max=0.7298,
particle_min=-3.0,
particle_max=3.0,
boundary_strategy="reflect",
velocity_limit_ratio=0.025,
mutation_swarm=0.02,
initialization="model_noise",
initial_position_noise=0.05,
moment_blend=0.06,
moment_step_size=0.5,
moment_beta1=0.9,
seed=seed,
device=device,
)
t0 = time.time()
opt.fit(x_train, y_train, epochs=pso_epochs)
sync_device(device)
elapsed = time.time() - t0
best_model = opt.get_best_model()
best_score_tuple = opt.get_best_score()
fitness_score = {
"subset_loss": round(float(best_score_tuple[0]), 6),
"subset_acc": round(float(best_score_tuple[1]), 6),
"subset_mse": round(float(best_score_tuple[2]), 6),
}
test_loss, test_acc = evaluate_model_on_test(best_model, x_test, y_test, device)
plugin_meta = extract_plugin_metadata(opt)
pso_meta = {
"elapsed_sec": round(elapsed, 4),
"particles": n_particles,
"pso_epochs": pso_epochs,
"fitness_size": fitness_size,
"plugins": plugin_meta,
}
return best_model, fitness_score, float(test_loss), float(test_acc), pso_meta
# ==========================================
# Output Generation (CSV, Plot)
# ==========================================
def save_csv_records(records: List[Dict[str, Any]], csv_path: Path):
csv_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"lane",
"profile_or_arch",
"seed",
"model_name",
"param_count",
"initial_test_acc",
"final_test_acc",
"final_test_loss",
"subset_fitness_acc",
"subset_fitness_loss",
"pso_epochs",
"adam_epochs",
"elapsed_sec",
"model_fingerprint",
"data_fingerprint",
]
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for r in records:
writer.writerow({
"lane": r.get("lane"),
"profile_or_arch": r.get("profile_or_arch"),
"seed": r.get("seed"),
"model_name": r.get("model_name"),
"param_count": r.get("param_count"),
"initial_test_acc": r.get("initial_test_acc"),
"final_test_acc": r.get("final_test_acc"),
"final_test_loss": r.get("final_test_loss"),
"subset_fitness_acc": r.get("subset_fitness_acc"),
"subset_fitness_loss": r.get("subset_fitness_loss"),
"pso_epochs": r.get("pso_epochs"),
"adam_epochs": r.get("adam_epochs"),
"elapsed_sec": r.get("elapsed_sec"),
"model_fingerprint": r.get("model_fingerprint"),
"data_fingerprint": r.get("data_fingerprint"),
})
def render_plots(
arch_summary: Dict[str, Dict[str, float]],
opt_summary: Dict[str, Dict[str, float]],
figure_path: Path,
):
figure_path.parent.mkdir(parents=True, exist_ok=True)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
if arch_summary:
arch_labels = list(arch_summary.keys())
arch_means = [arch_summary[k]["mean"] * 100 for k in arch_labels]
arch_sds = [arch_summary[k]["std"] * 100 for k in arch_labels]
arch_display = {
"raw_linear": "Raw Linear",
"raw_mlp": "Raw MLP",
"compact_cnn": "Compact CNN",
}
x_arch = np.arange(len(arch_labels))
ax1.bar(x_arch, arch_means, yerr=arch_sds, capsize=5, color="#56B4E9", edgecolor="black", alpha=0.85)
ax1.axhline(98.0, color="red", linestyle="--", linewidth=1.5, label="98% Target")
ax1.set_xticks(x_arch)
ax1.set_xticklabels([arch_display[k] for k in arch_labels], rotation=15)
ax1.set_ylabel("Final Test Accuracy (%)")
ax1.set_title("Architecture Lane (Full-Data Adam)")
ax1.set_ylim(0, 110)
ax1.set_axisbelow(True)
ax1.grid(axis="y", linestyle=":", alpha=0.6)
ax1.legend(loc="lower left")
for i, (m, sd) in enumerate(zip(arch_means, arch_sds)):
ax1.text(
i,
105.0 if m >= 90.0 else min(m + sd + 1.5, 102.5),
f"{m:.2f}%",
ha="center",
va="bottom",
fontsize=9,
fontweight="bold",
bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9, "pad": 1.5},
)
if opt_summary:
opt_labels = list(opt_summary.keys())
opt_means = [opt_summary[k]["mean"] * 100 for k in opt_labels]
opt_sds = [opt_summary[k]["std"] * 100 for k in opt_labels]
opt_display = {
"adam_only": "Adam Only",
"pso_only": "PSO Only",
"hybrid": "PSO → Adam",
}
x_opt = np.arange(len(opt_labels))
colors = ["#009E73", "#E69F00", "#CC79A7"]
ax2.bar(x_opt, opt_means, yerr=opt_sds, capsize=5, color=colors[:len(opt_labels)], edgecolor="black", alpha=0.85)
ax2.axhline(98.0, color="red", linestyle="--", linewidth=1.5, label="98% Target")
ax2.set_xticks(x_opt)
ax2.set_xticklabels([opt_display[k] for k in opt_labels], rotation=15)
ax2.set_ylabel("Final Test Accuracy (%)")
ax2.set_title("Optimizer Lane (Compact CNN)")
ax2.set_ylim(0, 110)
ax2.set_axisbelow(True)
ax2.grid(axis="y", linestyle=":", alpha=0.6)
ax2.legend(loc="lower left")
for i, (m, sd) in enumerate(zip(opt_means, opt_sds)):
ax2.text(
i,
105.0 if m >= 90.0 else min(m + sd + 1.5, 102.5),
f"{m:.2f}%",
ha="center",
va="bottom",
fontsize=9,
fontweight="bold",
bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9, "pad": 1.5},
)
plt.suptitle("MNIST Deep Accuracy Study: Architectures & Optimizer Profiles", fontsize=14, fontweight="bold")
plt.tight_layout()
fig.savefig(figure_path, dpi=300)
plt.close(fig)
# ==========================================
# Main CLI & Runner
# ==========================================
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="MNIST Deep Accuracy Study: Architecture vs Optimizer Profiles"
)
parser.add_argument(
"--seeds",
type=str,
default="101,102,103",
help="Comma-separated random seeds (default: 101,102,103)",
)
parser.add_argument(
"--adam-epochs",
type=int,
default=10,
help="Full-data Adam training epochs (default: 10)",
)
parser.add_argument(
"--pso-epochs",
type=int,
default=40,
help="PSO swarm optimization epochs (default: 40)",
)
parser.add_argument(
"--particles",
"--n-particles",
type=int,
default=30,
dest="particles",
help="Number of PSO particles (default: 30)",
)
parser.add_argument(
"--fitness-size",
type=int,
default=2000,
help="Fixed train subset fitness size for PSO (default: 2000)",
)
parser.add_argument(
"--batch-size",
type=int,
default=256,
help="Batch size for Adam DataLoader (default: 256)",
)
parser.add_argument(
"--lr",
"--learning-rate",
type=float,
default=1e-3,
dest="lr",
help="Learning rate for Adam optimizer (default: 1e-3)",
)
parser.add_argument(
"--device",
type=str,
default=None,
help="Target PyTorch device ('cpu', 'cuda', 'mps'; default: auto-detect)",
)
parser.add_argument(
"--json-path",
type=str,
default="benchmark_results/pso_v4_deep_accuracy.json",
help="JSON result output path (default: benchmark_results/pso_v4_deep_accuracy.json)",
)
parser.add_argument(
"--csv-path",
type=str,
default="benchmark_results/pso_v4_deep_accuracy.csv",
help="CSV result output path (default: benchmark_results/pso_v4_deep_accuracy.csv)",
)
parser.add_argument(
"--figure-path",
"--plot-path",
type=str,
default="history_plt/pso_v4_deep_accuracy.png",
dest="figure_path",
help="Figure output path (default: history_plt/pso_v4_deep_accuracy.png)",
)
parser.add_argument(
"--lanes",
"--profiles",
type=str,
choices=["all", "architectures", "optimizers"],
default="all",
help="Lanes/profiles to evaluate (choices: all, architectures, optimizers; default: all)",
)
return parser.parse_args()
def main():
args = parse_args()
seed_list = [int(s.strip()) for s in args.seeds.split(",") if s.strip()]
if not seed_list:
raise ValueError("--seeds must contain at least one integer")
if len(seed_list) != len(set(seed_list)):
raise ValueError("--seeds must not contain duplicates")
if any(seed < 0 for seed in seed_list):
raise ValueError("--seeds values must be non-negative")
for name, value in (
("--adam-epochs", args.adam_epochs),
("--pso-epochs", args.pso_epochs),
("--particles", args.particles),
("--fitness-size", args.fitness_size),
("--batch-size", args.batch_size),
):
if value <= 0:
raise ValueError(f"{name} must be positive")
if not math.isfinite(args.lr) or args.lr <= 0.0:
raise ValueError("--lr must be a positive finite number")
device = resolve_execution_device(args.device)
json_path = Path(args.json_path)
csv_path = Path(args.csv_path)
figure_path = Path(args.figure_path)
print(f"=== Starting MNIST Deep Accuracy Study (Protocol v{DEEP_ACCURACY_PROTOCOL_VERSION}) ===")
print(f"Device: {device} | Seeds: {seed_list} | Adam Epochs: {args.adam_epochs} | PSO Epochs: {args.pso_epochs}")
print(f"Particles: {args.particles} | Fitness Size: {args.fitness_size} | Batch Size: {args.batch_size} | LR: {args.lr}")
# Prepare data
x_train, y_train, x_test, y_test, data_fp, norm_provenance = prepare_deep_accuracy_mnist_data()
print(f"Data Loaded: Train {x_train.shape[0]} / Test {x_test.shape[0]} | Fingerprint: {data_fp[:12]}...")
hardware_prov = get_hardware_provenance(device)
all_csv_records: List[Dict[str, Any]] = []
architecture_lane_runs: List[Dict[str, Any]] = []
optimizer_lane_runs: List[Dict[str, Any]] = []
# Map architecture factories
arch_factories = {
"raw_linear": ("Raw Linear (784->10)", make_raw_linear),
"raw_mlp": ("Raw MLP (784->128->64->10)", make_raw_mlp),
"compact_cnn": ("Compact CNN (9,098 params)", make_compact_cnn),
}
try:
# ----------------------------------------------------
# 1. Architecture Lane
# ----------------------------------------------------
compact_cnn_adam_cache: Dict[int, Dict[str, Any]] = {}
if args.lanes in ("all", "architectures"):
print("\n--- Running Architecture Lane (Full-Data Adam) ---")
for arch_key, (arch_name, factory) in arch_factories.items():
for s in seed_list:
# Construct base model and record initial fingerprint
model = factory(seed=s)
p_count = count_parameters(model)
init_fp = compute_model_fingerprint(model)
init_loss, init_acc = evaluate_model_on_test(model, x_test, y_test, device)
print(f"[Arch: {arch_key} | Seed: {s}] Params: {p_count} | Init Acc: {init_acc*100:.2f}% | Training Adam...")
history, final_loss, final_acc, elapsed = train_adam_routine(
model=model,
x_train=x_train,
y_train=y_train,
x_test=x_test,
y_test=y_test,
epochs=args.adam_epochs,
batch_size=args.batch_size,
lr=args.lr,
seed=s,
device=device,
)
run_record = {
"lane": "architecture",
"profile_or_arch": arch_key,
"seed": s,
"model_name": arch_name,
"param_count": p_count,
"initial_test_acc": round(init_acc, 6),
"final_test_acc": round(final_acc, 6),
"final_test_loss": round(final_loss, 6),
"subset_fitness_acc": None,
"subset_fitness_loss": None,
"pso_epochs": 0,
"adam_epochs": args.adam_epochs,
"elapsed_sec": round(elapsed, 4),
"model_fingerprint": init_fp,
"data_fingerprint": data_fp,
"epoch_history": history,
}
architecture_lane_runs.append(run_record)
all_csv_records.append(run_record)
if arch_key == "compact_cnn":
compact_cnn_adam_cache[s] = run_record
print(f" -> Final Test Acc: {final_acc*100:.2f}% | Loss: {final_loss:.4f} | Time: {elapsed:.2f}s")
# ----------------------------------------------------
# 2. Optimizer Lane (Compact CNN)
# ----------------------------------------------------
if args.lanes in ("all", "optimizers"):
print("\n--- Running Optimizer Lane (Compact CNN) ---")
profiles = ["adam_only", "pso_only", "hybrid"]
for prof in profiles:
for s in seed_list:
# Construct Compact CNN with seed s to ensure same base initial state
model_base = make_compact_cnn(seed=s)
p_count = count_parameters(model_base)
init_fp = compute_model_fingerprint(model_base)
init_loss, init_acc = evaluate_model_on_test(model_base, x_test, y_test, device)
if prof == "adam_only":
if s in compact_cnn_adam_cache:
# Explicitly reuse record from Architecture Lane
cached = compact_cnn_adam_cache[s]
run_record = {
"lane": "optimizer",
"profile_or_arch": "adam_only",
"seed": s,
"model_name": "Compact CNN (Adam-Only)",
"param_count": p_count,
"initial_test_acc": cached["initial_test_acc"],
"final_test_acc": cached["final_test_acc"],
"final_test_loss": cached["final_test_loss"],
"subset_fitness_acc": None,
"subset_fitness_loss": None,
"pso_epochs": 0,
"adam_epochs": args.adam_epochs,
"elapsed_sec": cached["elapsed_sec"],
"model_fingerprint": init_fp,
"data_fingerprint": data_fp,
"reused_from_architecture_lane": True,
"epoch_history": cached["epoch_history"],
}
print(f"[Opt: adam_only | Seed: {s}] Reused from Architecture Lane | Final Acc: {cached['final_test_acc']*100:.2f}%")
else:
print(f"[Opt: adam_only | Seed: {s}] Training Adam...")
history, final_loss, final_acc, elapsed = train_adam_routine(
model=model_base,
x_train=x_train,
y_train=y_train,
x_test=x_test,
y_test=y_test,
epochs=args.adam_epochs,
batch_size=args.batch_size,
lr=args.lr,
seed=s,
device=device,
)
run_record = {
"lane": "optimizer",
"profile_or_arch": "adam_only",
"seed": s,
"model_name": "Compact CNN (Adam-Only)",
"param_count": p_count,
"initial_test_acc": round(init_acc, 6),
"final_test_acc": round(final_acc, 6),
"final_test_loss": round(final_loss, 6),
"subset_fitness_acc": None,
"subset_fitness_loss": None,
"pso_epochs": 0,
"adam_epochs": args.adam_epochs,
"elapsed_sec": round(elapsed, 4),
"model_fingerprint": init_fp,
"data_fingerprint": data_fp,
"reused_from_architecture_lane": False,
"epoch_history": history,
}
print(f" -> Final Test Acc: {final_acc*100:.2f}% | Loss: {final_loss:.4f} | Time: {elapsed:.2f}s")
optimizer_lane_runs.append(run_record)
all_csv_records.append(run_record)
elif prof == "pso_only":
print(
f"[Opt: pso_only | Seed: {s}] Running PSO Adaptive Moment "
f"on {args.fitness_size:,} fixed-subset samples..."
)
best_model, fitness_score, pso_test_loss, pso_test_acc, pso_meta = run_pso_routine(
model=model_base,
x_train=x_train,
y_train=y_train,
x_test=x_test,
y_test=y_test,
pso_epochs=args.pso_epochs,
n_particles=args.particles,
fitness_size=args.fitness_size,
seed=s,
device=device,
)
run_record = {
"lane": "optimizer",
"profile_or_arch": "pso_only",
"seed": s,
"model_name": "Compact CNN (PSO-Only)",
"param_count": p_count,
"initial_test_acc": round(init_acc, 6),
"final_test_acc": round(pso_test_acc, 6),
"final_test_loss": round(pso_test_loss, 6),
"subset_fitness_acc": fitness_score["subset_acc"],
"subset_fitness_loss": fitness_score["subset_loss"],
"pso_epochs": args.pso_epochs,
"adam_epochs": 0,
"elapsed_sec": pso_meta["elapsed_sec"],
"model_fingerprint": init_fp,
"data_fingerprint": data_fp,
"pso_metadata": pso_meta,
}
optimizer_lane_runs.append(run_record)
all_csv_records.append(run_record)
print(f" -> Fitness Subset Acc: {fitness_score['subset_acc']*100:.2f}% | Full Test Acc: {pso_test_acc*100:.2f}% | Time: {pso_meta['elapsed_sec']:.2f}s")
elif prof == "hybrid":
print(f"[Opt: hybrid | Seed: {s}] Running Hybrid (PSO Warm Start + Adam Fine-Tuning)...")
# 1. PSO Warm Start
t_hyb_start = time.time()
pso_best_model, fitness_score, pso_test_loss, pso_test_acc, pso_meta = run_pso_routine(
model=model_base,
x_train=x_train,
y_train=y_train,
x_test=x_test,
y_test=y_test,
pso_epochs=args.pso_epochs,
n_particles=args.particles,
fitness_size=args.fitness_size,
seed=s,
device=device,
)
# 2. Continue with full-data Adam
post_adam_history, final_test_loss, final_test_acc, adam_elapsed = train_adam_routine(
model=pso_best_model,
x_train=x_train,
y_train=y_train,
x_test=x_test,
y_test=y_test,
epochs=args.adam_epochs,
batch_size=args.batch_size,
lr=args.lr,
seed=s,
device=device,
)
hyb_total_elapsed = time.time() - t_hyb_start
run_record = {
"lane": "optimizer",
"profile_or_arch": "hybrid",
"seed": s,
"model_name": "Compact CNN (Hybrid)",
"param_count": p_count,
"initial_test_acc": round(init_acc, 6),
"final_test_acc": round(final_test_acc, 6),
"final_test_loss": round(final_test_loss, 6),
"subset_fitness_acc": fitness_score["subset_acc"],
"subset_fitness_loss": fitness_score["subset_loss"],
"pso_epochs": args.pso_epochs,
"adam_epochs": args.adam_epochs,
"elapsed_sec": round(hyb_total_elapsed, 4),
"model_fingerprint": init_fp,
"data_fingerprint": data_fp,
"post_pso_test_acc": round(pso_test_acc, 6),
"post_pso_test_loss": round(pso_test_loss, 6),
"post_adam_history": post_adam_history,
"efficiency_label": "HYBRID_GETS_EXTRA_WORK_UNFAIR_EFFICIENCY_COMPARISON",
}
optimizer_lane_runs.append(run_record)
all_csv_records.append(run_record)
print(f" -> Post-PSO Test Acc: {pso_test_acc*100:.2f}% | Final Hybrid Test Acc: {final_test_acc*100:.2f}% | Time: {hyb_total_elapsed:.2f}s")
# Compute summary statistics
arch_summary: Dict[str, Dict[str, float]] = {}
for arch_key in arch_factories.keys():
vals = [r["final_test_acc"] for r in architecture_lane_runs if r["profile_or_arch"] == arch_key]
if vals:
arch_summary[arch_key] = calc_stats(vals)
opt_summary: Dict[str, Dict[str, float]] = {}
for prof in ["adam_only", "pso_only", "hybrid"]:
vals = [r["final_test_acc"] for r in optimizer_lane_runs if r["profile_or_arch"] == prof]
if vals:
opt_summary[prof] = calc_stats(vals)
# Structure complete JSON output
result_payload = {
"protocol_version": DEEP_ACCURACY_PROTOCOL_VERSION,
"pso_version": pso_version,
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"completed": True,
"error": None,
"hardware_provenance": hardware_prov,
"data_provenance": norm_provenance,
"data_fingerprint": data_fp,
"configuration": {
"seeds": seed_list,
"adam_epochs": args.adam_epochs,
"pso_epochs": args.pso_epochs,
"particles": args.particles,
"fitness_size": args.fitness_size,
"batch_size": args.batch_size,
"lr": args.lr,
"device": str(device),
},
"summaries": {
"architecture_lane": arch_summary,
"optimizer_lane": opt_summary,
},
"architecture_lane_runs": architecture_lane_runs,
"optimizer_lane_runs": optimizer_lane_runs,
}
# Save JSON output atomically
save_json_atomic(result_payload, json_path)
print(f"\nSaved JSON results to {json_path}")
# Save CSV records and render plot only after all runs succeed
save_csv_records(all_csv_records, csv_path)
print(f"Saved CSV records to {csv_path}")
render_plots(arch_summary, opt_summary, figure_path)
print(f"Saved summary figure to {figure_path}")
# Print final summary table
print("\n========================================================")
print(" FINAL SUMMARY TABLE ")
print("========================================================")
if arch_summary:
print("Architecture Lane (Full-Data Adam):")
for arch_key, stats in arch_summary.items():
print(f" - {arch_key:15s}: Mean Acc = {stats['mean']*100:6.2f}% ± {stats['std']*100:5.2f}% (Median: {stats['median']*100:.2f}%)")
if opt_summary:
print("\nOptimizer Lane (Compact CNN):")
for prof, stats in opt_summary.items():
print(f" - {prof:15s}: Mean Acc = {stats['mean']*100:6.2f}% ± {stats['std']*100:5.2f}% (Median: {stats['median']*100:.2f}%)")
print("========================================================\n")
except Exception as e:
error_payload = {
"protocol_version": DEEP_ACCURACY_PROTOCOL_VERSION,
"pso_version": pso_version,
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"completed": False,
"error": str(e),
"hardware_provenance": hardware_prov,
"configuration": {
"seeds": seed_list,
"adam_epochs": args.adam_epochs,
"pso_epochs": args.pso_epochs,
"particles": args.particles,
"fitness_size": args.fitness_size,
"batch_size": args.batch_size,
"lr": args.lr,
"device": str(device),
},
"architecture_lane_runs": architecture_lane_runs,
"optimizer_lane_runs": optimizer_lane_runs,
}
save_json_atomic(error_payload, json_path)
print(f"\n[ERROR] Study failed: {e}")
print(f"Saved failure audit record to {json_path}")
sys.exit(1)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+1464
View File
File diff suppressed because it is too large Load Diff
+94 -54
View File
@@ -1,71 +1,111 @@
import os
import sys
from keras.layers import Dense
from keras.models import Sequential
from keras.utils import to_categorical
import argparse
import torch
import torch.nn as nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from pso import optimizer
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
def make_model():
model = Sequential()
model.add(Dense(12, input_dim=64, activation="relu"))
model.add(Dense(10, activation="relu"))
model.add(Dense(10, activation="softmax"))
return model
def make_model(seed: int = 42):
torch.manual_seed(seed)
return nn.Sequential(
nn.Linear(64, 12),
nn.ReLU(),
nn.Linear(12, 10),
nn.ReLU(),
nn.Linear(10, 10),
)
def get_data():
def get_data(seed: int = 42):
digits = load_digits()
X = digits.data
y = digits.target
x = X.astype("float32")
y_class = to_categorical(y)
x = digits.data.astype("float32")
y = digits.target.astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
x, y_class, test_size=0.2, random_state=42, shuffle=True
x, y, test_size=0.2, random_state=seed, shuffle=True
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.transform(x_test)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(y_test, dtype=torch.int64),
)
return x_train, x_test, y_train, y_test
x_train, x_test, y_train, y_test = get_data()
model = make_model()
def main():
parser = argparse.ArgumentParser(description="PSO Digits Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "original",
"initialization": "model_noise",
"evaluation": "fixed_subset",
"convergence": "particle_reset",
"refinement": "adam",
"n_particles": 30,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.0,
"mutation_swarm": 0.1,
"particle_min": -3.0,
"particle_max": 3.0,
"velocity_limit_ratio": 0.1,
"boundary_strategy": "reflect",
"seed": 42,
"epochs": 80,
"batch_size": 200,
"fitness_size": 1000,
"renewal": "loss",
"output_dir": "output/digits",
"refinement_epochs": 10,
"refinement_lr": 0.001,
},
)
args = parser.parse_args()
digits_pso = optimizer(
model,
loss="categorical_crossentropy",
n_particles=300,
c0=0.5,
c1=0.3,
w_min=0.2,
w_max=0.9,
negative_swarm=0,
mutation_swarm=0.1,
convergence_reset=True,
convergence_reset_patience=10,
convergence_reset_monitor="loss",
convergence_reset_min_delta=0.001,
)
x_train, x_test, y_train, y_test = get_data(seed=args.seed)
model = make_model(seed=args.seed)
digits_pso.fit(
x_train,
y_train,
epochs=500,
validate_data=(x_test, y_test),
log=2,
save_info=True,
renewal="loss",
log_name="digits",
)
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
print("Done!")
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.CrossEntropyLoss(),
task="multiclass",
inertia_profile={"c0": 0.5, "c1": 0.3, "w_min": 0.2, "w_max": 0.9},
)
digits_pso = Optimizer(**kwargs)
sys.exit(0)
print(f"Optimizer device: {digits_pso.device}")
best_score = digits_pso.fit(
x_train,
y_train,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
validation_data=(x_test, y_test),
output_dir=args.output_dir,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
if __name__ == "__main__":
main()
-81
View File
@@ -1,81 +0,0 @@
import os
import sys
import pandas as pd
import tensorflow as tf
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import to_categorical
gpus = tf.config.experimental.list_physical_devices("GPU")
if gpus:
try:
tf.config.experimental.set_memory_growth(gpus[0], True)
except RuntimeError as r:
print(r)
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
def make_model():
model = Sequential()
model.add(Dense(12, input_dim=64, activation="relu"))
model.add(Dense(12, activation="relu"))
model.add(Dense(10, activation="softmax"))
return model
def get_data():
digits = load_digits()
X = digits.data
y = digits.target
x = X.astype("float32")
y_class = to_categorical(y)
x_train, x_test, y_train, y_test = train_test_split(
x, y_class, test_size=0.2, random_state=42, shuffle=True
)
return x_train, x_test, y_train, y_test
if __name__ == "__main__":
model = make_model()
x_train, x_test, y_train, y_test = get_data()
callbacks = [
tf.keras.callbacks.EarlyStopping(
monitor="val_loss", patience=10, restore_best_weights=True
)
]
print(x_train.shape, y_train.shape)
model.compile(
optimizer="adam",
loss="categorical_crossentropy",
metrics=["accuracy", "mse"],
)
print(model.summary())
history = model.fit(
x_train,
y_train,
epochs=500,
batch_size=32,
verbose=1,
validation_data=(x_test, y_test),
callbacks=callbacks,
)
print("Done!")
sys.exit(0)
+134
View File
@@ -0,0 +1,134 @@
"""Digits dataset gradient baseline (PyTorch standard backprop optimizer, non-PSO)."""
import copy
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
class DigitsModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(64, 12),
nn.ReLU(),
nn.Linear(12, 12),
nn.ReLU(),
nn.Linear(12, 10),
)
def forward(self, x):
return self.net(x)
def get_data(seed: int = 42):
digits = load_digits()
X = digits.data.astype("float32")
y = digits.target.astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=seed, shuffle=True
)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(y_test, dtype=torch.int64),
)
def get_device() -> torch.device:
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_built()
and torch.backends.mps.is_available()
):
return torch.device("mps")
elif torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
def main():
torch.manual_seed(42)
np.random.seed(42)
device = get_device()
print(f"Selected device: {device}")
x_train, x_test, y_train, y_test = get_data(seed=42)
train_loader = DataLoader(
TensorDataset(x_train, y_train), batch_size=32, shuffle=True
)
val_loader = DataLoader(
TensorDataset(x_test, y_test), batch_size=32, shuffle=False
)
model = DigitsModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
best_val_loss = float("inf")
best_state = None
patience = 10
patience_counter = 0
for epoch in range(500):
model.train()
for bx, by in train_loader:
bx, by = bx.to(device), by.to(device)
optimizer.zero_grad()
out = model(bx)
loss = criterion(out, by)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0.0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
val_loss += loss.item() * bx.size(0)
total += bx.size(0)
val_loss /= total
if val_loss < best_val_loss:
best_val_loss = val_loss
best_state = copy.deepcopy(model.state_dict())
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
break
if best_state is not None:
model.load_state_dict(best_state)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
test_loss += loss.item() * bx.size(0)
preds = out.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
test_loss /= total
test_acc = correct / total
print(f"Final test loss: {test_loss:.4f}, accuracy: {test_acc:.4f}")
if __name__ == "__main__":
main()
+673
View File
@@ -0,0 +1,673 @@
"""
Adaptive Moment 120-Particle x 240-Epoch MNIST Convergence & Trajectory Analysis
Evaluates whether the published 120-particle x 80-epoch MNIST PCA32 Adaptive Moment run
benefits from continued optimization up to 240 epochs or enters a generalization plateau.
Predeclared Contract Criteria:
1. Exact Replay Verification at Epoch 80 (seeds 71-75):
Per-seed test accuracy absolute delta vs baseline <= 0.005 (0.5%p).
2. Primary Diagnostic Endpoints: Epochs 80, 120, 160, 200, 240.
3. Classifications:
- Training Still Improving: Mean global-best fitness loss falls >= 1% from epoch 80 to 240.
- Meaningful Held-Out Gain: Mean test accuracy at epoch 240 rises >= 1 percentage point vs epoch 80.
- Overfitting Signal: Training loss improves but epoch 240 test accuracy falls >= 1 point.
- Early Stagnation: Training loss improves < 1% and absolute test change remains < 1 point.
- Generalization Plateau: Training loss improves >= 1% while absolute test gain remains < 1 point.
- Late Plateau Diagnostic: 200->240 training-loss improvement < 1% AND absolute test-accuracy change < 0.5 points.
"""
import argparse
import csv
import datetime
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
# Ensure test/ directory is in sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from benchmark_suite import (
calc_stats,
compute_model_fingerprint,
get_hardware_provenance,
make_mnist_model,
resolve_execution_device,
save_json_atomic,
sync_device,
)
from pso import Optimizer, __version__ as pso_version
from reproduce_scaling import (
REPLAY_SEEDS,
REPLAY_TOLERANCE,
prepare_full_pca_data,
validate_and_load_baseline,
)
from tuning_suite import TUNING_PROTOCOL_VERSION
EPOCH_CONVERGENCE_PROTOCOL_VERSION = "1.0.0"
CHECKPOINT_EPOCHS = [20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240]
POST80_LOSS_REDUCTION_THRESHOLD = 0.01
TEST_ACCURACY_GAIN_THRESHOLD = 0.01
OVERFITTING_ACCURACY_DROP_THRESHOLD = -0.01
LATE_LOSS_REDUCTION_THRESHOLD = 0.01
LATE_ACCURACY_CHANGE_THRESHOLD = 0.005
def run_epoch_convergence_analysis(
baseline_path: Path,
output_json_path: Path,
output_csv_path: Path,
figure_path: Path,
device_str: str | None = None,
) -> bool:
device = resolve_execution_device(device_str)
hw_provenance = get_hardware_provenance(device)
# 1. Validate and load baseline
baseline_data, baseline_records, winner_cfg, expected_fp = validate_and_load_baseline(
baseline_path
)
if baseline_data.get("device") != device.type:
raise ValueError(
f"Exact trajectory extension requires baseline device "
f"{baseline_data.get('device')!r}; got {device.type!r}."
)
if baseline_data.get("pso_version") != pso_version:
raise ValueError(
f"Exact trajectory extension requires pso version "
f"{baseline_data.get('pso_version')!r}; got {pso_version!r}."
)
if baseline_data.get("torch_version") != torch.__version__:
raise ValueError(
f"Exact trajectory extension requires torch version "
f"{baseline_data.get('torch_version')!r}; got {torch.__version__!r}."
)
base_rec_by_seed = {r["seed"]: r for r in baseline_records}
# 2. Prepare full PCA dataset
x_full_tr, y_train_3000, x_full_test, y_test_1000, data_fp = prepare_full_pca_data()
if data_fp != expected_fp:
raise ValueError(
f"Data fingerprint mismatch: computed {data_fp}, baseline expected {expected_fp}"
)
opt_kwargs = winner_cfg.to_optimizer_kwargs(quick=False)
n_particles = 120
target_epochs = 240
batch_size = 1000
runs: List[Dict[str, Any]] = []
flat_csv_rows: List[Dict[str, Any]] = []
fidelity_passed = True
seed_fidelity_deltas: Dict[int, float] = {}
# 3. Process seeds 71-75
for seed in sorted(REPLAY_SEEDS):
base_rec = base_rec_by_seed[seed]
expected_model_fp = base_rec["model_fingerprint"]
baseline_test_acc = float(base_rec["test_acc"])
# --- Untimed 2-Epoch Warmup Phase ---
warmup_model = make_mnist_model(seed=seed)
warmup_loss = nn.CrossEntropyLoss()
warmup_opt = Optimizer(
model=warmup_model,
loss=warmup_loss,
task="multiclass",
n_particles=n_particles,
seed=seed,
device=device,
**opt_kwargs,
)
warmup_opt.fit(
x_full_tr,
y_train_3000,
epochs=2,
batch_size=batch_size,
renewal="loss",
)
sync_device(device)
del warmup_opt, warmup_model, warmup_loss
# --- Timed 240-Epoch Continuous Trajectory ---
model = make_mnist_model(seed=seed)
model_fp = compute_model_fingerprint(model)
fp_match = (model_fp == expected_model_fp)
if not fp_match:
print(
f"[WARNING] Seed {seed} model fingerprint mismatch: "
f"got {model_fp}, expected {expected_model_fp}"
)
fidelity_passed = False
loss_inst = nn.CrossEntropyLoss()
opt = Optimizer(
model=model,
loss=loss_inst,
task="multiclass",
n_particles=n_particles,
seed=seed,
device=device,
**opt_kwargs,
)
with tempfile.TemporaryDirectory() as temp_dir_str:
output_dir = Path(temp_dir_str)
sync_device(device)
t0 = time.perf_counter()
train_loss_final, train_acc_final, train_mse_final = opt.fit(
x_full_tr,
y_train_3000,
epochs=target_epochs,
batch_size=batch_size,
renewal="loss",
output_dir=output_dir,
log_format="csv",
checkpoint_interval=20,
)
sync_device(device)
t1 = time.perf_counter()
fit_time_sec = t1 - t0
# Read full epoch history from history.csv
history_csv_path = output_dir / "history.csv"
epoch_history = []
with open(history_csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
epoch_history.append({
"epoch": int(row["epoch"]),
"loss": float(row["loss"]),
"accuracy": float(row["accuracy"]),
"mse": float(row["mse"]),
})
# Track training global-best improvement epochs
prev_best_loss = float("inf")
improvement_count = 0
last_improvement_epoch = 1
for row in epoch_history:
ep_num = row["epoch"]
l_val = row["loss"]
if l_val < prev_best_loss:
improvement_count += 1
last_improvement_epoch = ep_num
prev_best_loss = l_val
# Read and evaluate checkpoints
checkpoints: List[Dict[str, Any]] = []
ckpt_dir = output_dir / "checkpoints"
epoch80_test_acc = None
for ep in CHECKPOINT_EPOCHS:
ckpt_path = ckpt_dir / f"epoch-{ep}.pt"
if not ckpt_path.exists():
raise FileNotFoundError(f"Missing checkpoint file: {ckpt_path}")
payload = torch.load(ckpt_path, map_location=device, weights_only=True)
ckpt_train_loss, ckpt_train_acc, ckpt_train_mse = payload["score"]
# Load checkpoint state_dict into opt.eval_model and evaluate on held-out test tensor
opt.eval_model.load_state_dict(payload["model_state_dict"])
opt._global_best_weights = opt.codec.encode(opt.eval_model)
test_loss, test_acc, test_mse = opt.evaluate(x_full_test, y_test_1000)
ckpt_record = {
"epoch": ep,
"train_loss": float(ckpt_train_loss),
"train_acc": float(ckpt_train_acc),
"train_mse": float(ckpt_train_mse),
"test_loss": float(test_loss),
"test_acc": float(test_acc),
"test_mse": float(test_mse),
}
checkpoints.append(ckpt_record)
if ep == 80:
epoch80_test_acc = float(test_acc)
flat_csv_rows.append({
"seed": seed,
"epoch": ep,
"train_loss": float(ckpt_train_loss),
"train_acc": float(ckpt_train_acc),
"train_mse": float(ckpt_train_mse),
"test_loss": float(test_loss),
"test_acc": float(test_acc),
"test_mse": float(test_mse),
"fit_time_sec": round(fit_time_sec, 4),
})
# Fidelity check at epoch 80 vs baseline
assert epoch80_test_acc is not None
delta_ep80 = abs(epoch80_test_acc - baseline_test_acc)
seed_fidelity_deltas[seed] = delta_ep80
if delta_ep80 > REPLAY_TOLERANCE:
print(
f"[WARNING] Seed {seed} epoch 80 test accuracy delta {delta_ep80:.6f} "
f"exceeds tolerance {REPLAY_TOLERANCE} (actual={epoch80_test_acc:.4f}, baseline={baseline_test_acc:.4f})"
)
fidelity_passed = False
runs.append({
"seed": seed,
"model_fingerprint": model_fp,
"expected_model_fingerprint": expected_model_fp,
"fingerprint_matched": fp_match,
"baseline_epoch80_test_acc": baseline_test_acc,
"epoch80_test_acc": epoch80_test_acc,
"epoch80_abs_delta": delta_ep80,
"fit_time_sec": round(fit_time_sec, 4),
"improvement_count": improvement_count,
"last_improvement_epoch": last_improvement_epoch,
"checkpoints": checkpoints,
})
max_ep80_delta = max(seed_fidelity_deltas.values()) if seed_fidelity_deltas else 0.0
# 4. Aggregations and Statistical Summaries
checkpoint_stats: Dict[int, Dict[str, Any]] = {}
for ep in CHECKPOINT_EPOCHS:
ep_train_losses = [next(c["train_loss"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_train_accs = [next(c["train_acc"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_train_mses = [next(c["train_mse"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_losses = [next(c["test_loss"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_accs = [next(c["test_acc"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_mses = [next(c["test_mse"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
checkpoint_stats[ep] = {
"train_loss": calc_stats(ep_train_losses),
"train_acc": calc_stats(ep_train_accs),
"train_mse": calc_stats(ep_train_mses),
"test_loss": calc_stats(ep_test_losses),
"test_acc": calc_stats(ep_test_accs),
"test_mse": calc_stats(ep_test_mses),
}
# Endpoint paired deltas (80->240 and 200->240)
deltas_80_to_240_train_rel = []
deltas_80_to_240_test_acc = []
deltas_200_to_240_train_rel = []
deltas_200_to_240_test_acc = []
deltas_200_to_240_test_acc_abs = []
for r in runs:
ckpt_map = {c["epoch"]: c for c in r["checkpoints"]}
# 80 to 240
tl80 = ckpt_map[80]["train_loss"]
tl240 = ckpt_map[240]["train_loss"]
rel_red_80_240 = (tl80 - tl240) / tl80 if tl80 > 0 else 0.0
deltas_80_to_240_train_rel.append(rel_red_80_240)
ta80 = ckpt_map[80]["test_acc"]
ta240 = ckpt_map[240]["test_acc"]
acc_delta_80_240 = ta240 - ta80
deltas_80_to_240_test_acc.append(acc_delta_80_240)
# 200 to 240
tl200 = ckpt_map[200]["train_loss"]
rel_red_200_240 = (tl200 - tl240) / tl200 if tl200 > 0 else 0.0
deltas_200_to_240_train_rel.append(rel_red_200_240)
ta200 = ckpt_map[200]["test_acc"]
acc_delta_200_240 = ta240 - ta200
acc_abs_change_200_240 = abs(ta240 - ta200)
deltas_200_to_240_test_acc.append(acc_delta_200_240)
deltas_200_to_240_test_acc_abs.append(acc_abs_change_200_240)
# Calculate overall mean metrics for classifications
mean_train_loss_80 = checkpoint_stats[80]["train_loss"]["mean"]
mean_train_loss_200 = checkpoint_stats[200]["train_loss"]["mean"]
mean_train_loss_240 = checkpoint_stats[240]["train_loss"]["mean"]
mean_test_acc_80 = checkpoint_stats[80]["test_acc"]["mean"]
mean_test_acc_200 = checkpoint_stats[200]["test_acc"]["mean"]
mean_test_acc_240 = checkpoint_stats[240]["test_acc"]["mean"]
rel_train_loss_reduction_80_240 = (
(mean_train_loss_80 - mean_train_loss_240) / mean_train_loss_80
)
test_acc_gain_80_240 = mean_test_acc_240 - mean_test_acc_80
rel_train_loss_reduction_200_240 = (
(mean_train_loss_200 - mean_train_loss_240) / mean_train_loss_200
)
abs_test_acc_change_200_240 = abs(mean_test_acc_240 - mean_test_acc_200)
# 5. Shared Contract Predeclared Classifications
post_80_training_converging = bool(
rel_train_loss_reduction_80_240 >= POST80_LOSS_REDUCTION_THRESHOLD
)
meaningful_held_out_gain = bool(
test_acc_gain_80_240 >= TEST_ACCURACY_GAIN_THRESHOLD
)
overfitting_signal = bool(
post_80_training_converging
and test_acc_gain_80_240 <= OVERFITTING_ACCURACY_DROP_THRESHOLD
)
early_stagnation = bool(
not post_80_training_converging
and abs(test_acc_gain_80_240) < TEST_ACCURACY_GAIN_THRESHOLD
)
generalization_plateau = bool(
post_80_training_converging
and not meaningful_held_out_gain
and not overfitting_signal
)
late_plateau_diagnostic = bool(
rel_train_loss_reduction_200_240 < LATE_LOSS_REDUCTION_THRESHOLD
and abs_test_acc_change_200_240 < LATE_ACCURACY_CHANGE_THRESHOLD
)
if meaningful_held_out_gain:
summary_verdict = (
f"Training beyond epoch 80 continues to improve held-out test accuracy by "
f"{test_acc_gain_80_240 * 100:.2f} percentage points "
f"(from {mean_test_acc_80 * 100:.2f}% to {mean_test_acc_240 * 100:.2f}%)."
)
elif overfitting_signal:
summary_verdict = (
f"Training beyond epoch 80 exhibits overfitting: training loss falls by "
f"{rel_train_loss_reduction_80_240 * 100:.2f}% while held-out test accuracy drops by "
f"{abs(test_acc_gain_80_240) * 100:.2f} percentage points."
)
elif early_stagnation:
summary_verdict = (
f"Optimization has effectively stagnated after epoch 80: training loss falls by only "
f"{rel_train_loss_reduction_80_240 * 100:.2f}% and held-out accuracy changes by "
f"{test_acc_gain_80_240 * 100:+.2f} percentage points through epoch 240."
)
elif generalization_plateau:
summary_verdict = (
f"Training loss continues to improve after epoch 80, but held-out accuracy plateaus: "
f"{test_acc_gain_80_240 * 100:+.2f} percentage points "
f"(from {mean_test_acc_80 * 100:.2f}% to {mean_test_acc_240 * 100:.2f}%)."
)
else:
summary_verdict = (
"The fixed endpoint criteria are inconclusive; inspect the paired checkpoint "
"trajectory before extending the epoch horizon."
)
last_improvement_epochs_dict = {r["seed"]: r["last_improvement_epoch"] for r in runs}
paired_endpoint_deltas = []
for r in runs:
ckpt_map = {c["epoch"]: c for c in r["checkpoints"]}
paired_endpoint_deltas.append(
{
"seed": r["seed"],
"train_loss_relative_reduction_80_to_240": (
ckpt_map[80]["train_loss"] - ckpt_map[240]["train_loss"]
)
/ ckpt_map[80]["train_loss"],
"test_accuracy_delta_80_to_240": (
ckpt_map[240]["test_acc"] - ckpt_map[80]["test_acc"]
),
"train_loss_relative_reduction_200_to_240": (
ckpt_map[200]["train_loss"] - ckpt_map[240]["train_loss"]
)
/ ckpt_map[200]["train_loss"],
"test_accuracy_delta_200_to_240": (
ckpt_map[240]["test_acc"] - ckpt_map[200]["test_acc"]
),
}
)
# Build final payload
payload = {
"epoch_convergence_protocol_version": EPOCH_CONVERGENCE_PROTOCOL_VERSION,
"tuning_protocol_version": TUNING_PROTOCOL_VERSION,
"pso_version": pso_version,
"torch_version": torch.__version__,
"hardware": hw_provenance,
"device": device.type,
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"baseline_path": str(baseline_path),
"data_fingerprint": data_fp,
"candidate_label": winner_cfg.candidate_label,
"config": {
**opt_kwargs,
"n_particles": n_particles,
"epochs": target_epochs,
"batch_size": batch_size,
"renewal": "loss",
"checkpoint_interval": 20,
},
"contract_criteria": {
"primary_endpoints": [80, 120, 160, 200, 240],
"post_80_convergence_threshold_loss_reduction": POST80_LOSS_REDUCTION_THRESHOLD,
"meaningful_gain_threshold_test_acc": TEST_ACCURACY_GAIN_THRESHOLD,
"overfitting_threshold_test_acc": OVERFITTING_ACCURACY_DROP_THRESHOLD,
"late_plateau_200_240_loss_threshold": LATE_LOSS_REDUCTION_THRESHOLD,
"late_plateau_200_240_acc_threshold": LATE_ACCURACY_CHANGE_THRESHOLD,
"replay_tolerance": REPLAY_TOLERANCE,
},
"fidelity_validation": {
"replay_seeds": sorted(REPLAY_SEEDS),
"max_epoch80_test_acc_delta": round(max_ep80_delta, 6),
"tolerance": REPLAY_TOLERANCE,
"passed": fidelity_passed,
},
"predeclared_classifications": {
"post_80_training_converging": post_80_training_converging,
"meaningful_held_out_gain": meaningful_held_out_gain,
"overfitting_signal": overfitting_signal,
"early_stagnation": early_stagnation,
"generalization_plateau": generalization_plateau,
"late_plateau_diagnostic": late_plateau_diagnostic,
"summary_verdict": summary_verdict,
},
"summary": {
"epochs": CHECKPOINT_EPOCHS,
"checkpoint_stats": {str(ep): stats for ep, stats in checkpoint_stats.items()},
"paired_deltas": {
"80_to_240": {
"train_loss_rel_reduction": calc_stats(deltas_80_to_240_train_rel),
"test_acc_delta": calc_stats(deltas_80_to_240_test_acc),
},
"200_to_240": {
"train_loss_rel_reduction": calc_stats(deltas_200_to_240_train_rel),
"test_acc_delta": calc_stats(deltas_200_to_240_test_acc),
"test_acc_abs_change": calc_stats(deltas_200_to_240_test_acc_abs),
},
},
"paired_endpoint_deltas_by_seed": paired_endpoint_deltas,
"last_training_best_improvement_epochs": last_improvement_epochs_dict,
},
"runs": runs,
"completed": True,
"valid": fidelity_passed,
"error": None,
}
# Write output JSON
save_json_atomic(payload, output_json_path)
print(f"Saved analysis JSON to {output_json_path}")
# Write output CSV
output_csv_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_csv_path, "w", newline="", encoding="utf-8") as f:
fieldnames = [
"seed",
"epoch",
"train_loss",
"train_acc",
"train_mse",
"test_loss",
"test_acc",
"test_mse",
"fit_time_sec",
]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(flat_csv_rows)
print(f"Saved flat checkpoint CSV to {output_csv_path}")
# 6. Render Figure
render_convergence_figure(CHECKPOINT_EPOCHS, runs, checkpoint_stats, figure_path)
print(f"Rendered plot to {figure_path}")
# Print summary block
print("\n" + "=" * 70)
print("EPOCH CONVERGENCE & TRAJECTORY ANALYSIS RESULTS")
print("=" * 70)
print(f"Device: {device.type} | Seeds: {sorted(REPLAY_SEEDS)}")
print(f"Fidelity Replay Check (Epoch 80 <= {REPLAY_TOLERANCE}): Max Delta = {max_ep80_delta:.6f} -> Passed: {fidelity_passed}")
print("-" * 70)
print("Predeclared Classifications (Epoch 80 -> 240):")
print(f" Post-80 Training Converging (Loss Drop >= 1%): {post_80_training_converging} ({rel_train_loss_reduction_80_240 * 100:.2f}%)")
print(f" Meaningful Held-Out Gain (Acc Rise >= 1%p): {meaningful_held_out_gain} ({test_acc_gain_80_240 * 100:+.2f}%p)")
print(f" Overfitting Signal: {overfitting_signal}")
print(f" Early Stagnation: {early_stagnation}")
print(f" Generalization Plateau: {generalization_plateau}")
print(f" Late Plateau Diagnostic (200 -> 240): {late_plateau_diagnostic}")
print("-" * 70)
print(f"Verdict: {summary_verdict}")
print("=" * 70 + "\n")
return fidelity_passed
def render_convergence_figure(
epochs: List[int],
runs: List[Dict[str, Any]],
checkpoint_stats: Dict[int, Dict[str, Any]],
figure_path: Path,
):
figure_path.parent.mkdir(parents=True, exist_ok=True)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5.5))
mean_train_loss = [checkpoint_stats[ep]["train_loss"]["mean"] for ep in epochs]
std_train_loss = [checkpoint_stats[ep]["train_loss"]["std"] for ep in epochs]
mean_test_acc = [checkpoint_stats[ep]["test_acc"]["mean"] for ep in epochs]
std_test_acc = [checkpoint_stats[ep]["test_acc"]["std"] for ep in epochs]
epochs_arr = np.array(epochs)
mean_tl_arr = np.array(mean_train_loss)
std_tl_arr = np.array(std_train_loss)
mean_ta_arr = np.array(mean_test_acc)
std_ta_arr = np.array(std_test_acc)
# Subplot 1: Training Loss
for r in runs:
r_epochs = [c["epoch"] for c in r["checkpoints"]]
r_losses = [c["train_loss"] for c in r["checkpoints"]]
ax1.plot(r_epochs, r_losses, color="#1f77b4", alpha=0.25, linestyle=":", linewidth=1.2)
ax1.plot(epochs_arr, mean_tl_arr, marker="o", color="#1f77b4", linewidth=2.2, label="Mean Training Loss")
ax1.fill_between(
epochs_arr,
mean_tl_arr - std_tl_arr,
mean_tl_arr + std_tl_arr,
color="#1f77b4",
alpha=0.15,
label="±1 Std Dev",
)
ax1.axvline(80, color="#d62728", linestyle="--", linewidth=1.5, label="Baseline Horizon (Epoch 80)")
ax1.set_xlabel("Epoch", fontsize=11)
ax1.set_ylabel("Global-Best Training Loss", fontsize=11)
ax1.set_title("Global-Best Training Loss Trajectory", fontsize=12, fontweight="bold")
ax1.grid(True, alpha=0.3)
ax1.legend(loc="upper right", frameon=True)
# Subplot 2: Held-Out Test Accuracy
for r in runs:
r_epochs = [c["epoch"] for c in r["checkpoints"]]
r_accs = [c["test_acc"] for c in r["checkpoints"]]
ax2.plot(r_epochs, r_accs, color="#2ca02c", alpha=0.25, linestyle=":", linewidth=1.2)
ax2.plot(epochs_arr, mean_ta_arr, marker="s", color="#2ca02c", linewidth=2.2, label="Mean Test Accuracy")
ax2.fill_between(
epochs_arr,
mean_ta_arr - std_ta_arr,
mean_ta_arr + std_ta_arr,
color="#2ca02c",
alpha=0.15,
label="±1 Std Dev",
)
ax2.axvline(80, color="#d62728", linestyle="--", linewidth=1.5, label="Baseline Horizon (Epoch 80)")
ax2.set_xlabel("Epoch", fontsize=11)
ax2.set_ylabel("Held-Out Test Accuracy", fontsize=11)
ax2.set_title("Held-Out Test Accuracy Trajectory", fontsize=12, fontweight="bold")
ax2.grid(True, alpha=0.3)
ax2.legend(loc="lower right", frameon=True)
fig.suptitle(
"Adaptive Moment 120-Particle MNIST 240-Epoch Convergence & Trajectory Analysis",
fontsize=13,
fontweight="bold",
)
plt.tight_layout(rect=(0.0, 0.0, 1.0, 0.95))
plt.savefig(figure_path, dpi=300, bbox_inches="tight")
plt.close(fig)
def main():
parser = argparse.ArgumentParser(
description="Adaptive Moment 120-Particle x 240-Epoch MNIST Convergence Analysis"
)
parser.add_argument(
"--baseline-json",
type=Path,
default=Path("benchmark_results/pso_v4_tuning.json"),
help="Path to baseline tuning JSON file",
)
parser.add_argument(
"--output-json",
type=Path,
default=Path("benchmark_results/pso_v4_epoch_convergence.json"),
help="Path to output analysis JSON file",
)
parser.add_argument(
"--output-csv",
type=Path,
default=Path("benchmark_results/pso_v4_epoch_convergence.csv"),
help="Path to output checkpoint CSV file",
)
parser.add_argument(
"--figure",
type=Path,
default=Path("history_plt/pso_v4_epoch_convergence.png"),
help="Path to output convergence PNG figure",
)
parser.add_argument(
"--device",
type=str,
default=None,
help="Execution device (mps, cuda, cpu; default: auto-detect)",
)
args = parser.parse_args()
passed = run_epoch_convergence_analysis(
baseline_path=args.baseline_json,
output_json_path=args.output_json,
output_csv_path=args.output_csv,
figure_path=args.figure,
device_str=args.device,
)
if not passed:
sys.exit(1)
if __name__ == "__main__":
main()
+539
View File
@@ -0,0 +1,539 @@
"""
Strict Pareto Evaluator for Heavy Task PSO Autoresearch.
Evaluates candidate equalized signed-hash subspace experiment artifacts against
baseline heavy task benchmark results (benchmark_results/pso_v6_heavy_tasks.json).
Baseline Policy:
- mnist_compact: G8
- mnist_wide: G5
- fashion_compact: G8
- fashion_wide: G5
Evaluates 7 Hard Gates:
1. Finite Metrics (all validation metrics finite across runs)
2. Test-Sealed (zero official test data loaded & 0 test evaluations)
3. Config Matched (12 particles, 80 epochs, 10k subset, seeds 101-103)
4. State Ratio Boundary (max state ratio <= 0.5 across all workloads)
5. Workload Accuracy Regression Boundary (each workload acc regression <= 1.0 pp)
6. Workload NLL Regression Boundary (each workload NLL regression <= 5.0%)
7. Worst-Workload (mnist_wide) Improvement (acc gain >= 2.0 pp OR NLL reduction >= 5.0%)
Numeric Score:
Score = mean_rel_nll_reduction_pct + mean_acc_gain_pp + 10 * log2(1 / max_state_ratio) - 100 * failed_gate_count
"""
from __future__ import annotations
import argparse
import json
import math
import os
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
# Ensure test directory and repo root are in Python path
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).resolve().parent.name != "PSO" else Path(__file__).resolve().parent
TEST_DIR = REPO_ROOT / "test"
if str(TEST_DIR) not in sys.path:
sys.path.insert(0, str(TEST_DIR))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from benchmark_suite import save_json_atomic
from heavy_pso_autoresearch import compute_latent_dim, compute_core_swarm_state_bytes
EVALUATOR_VERSION = "EVALUATE-HEAVY-AUTORESEARCH 1.0.0"
BASELINE_POLICY: Dict[str, str] = {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
}
EXPECTED_SEEDS = [101, 102, 103]
EXPECTED_PARTICLES = 12
EXPECTED_EPOCHS = 80
EXPECTED_SUBSET_SIZE = 10000
EXPECTED_WORKLOADS = ["mnist_compact", "mnist_wide", "fashion_compact", "fashion_wide"]
def evaluate_heavy_autoresearch(
baseline_path: Union[str, Path],
candidate_path: Union[str, Path],
output_path: Optional[Union[str, Path]] = None,
) -> Dict[str, Any]:
"""
Evaluates a candidate experiment artifact against baseline heavy task results.
Validates schemas, configurations, seeds, test-sealed constraints, finiteness,
calculates every hard gate and numeric score, selects the winning candidate,
and returns a structured evaluator payload.
"""
baseline_path = Path(baseline_path)
candidate_path = Path(candidate_path)
if not baseline_path.is_file():
raise FileNotFoundError(f"Baseline artifact not found at '{baseline_path}'")
if not candidate_path.is_file():
raise FileNotFoundError(f"Candidate artifact not found at '{candidate_path}'")
with open(baseline_path, "r", encoding="utf-8") as f:
baseline_data = json.load(f)
with open(candidate_path, "r", encoding="utf-8") as f:
candidate_data = json.load(f)
WORKLOAD_TOTAL_DIMS: Dict[str, int] = {
"mnist_compact": 9098,
"mnist_wide": 55338,
"fashion_compact": 9098,
"fashion_wide": 55338,
}
if (
baseline_data.get("official_test_data_loaded") is not False
or baseline_data.get("official_test_evaluations") != 0
):
raise ValueError("Baseline artifact must explicitly seal official test data.")
# Validate baseline JSON schema & confirmation results
if "confirmation_results" not in baseline_data:
raise KeyError("Baseline JSON missing top-level 'confirmation_results' key")
baseline_confirm = baseline_data["confirmation_results"]
baseline_metrics: Dict[str, Dict[str, float]] = {}
baseline_core_bytes: Dict[str, int] = {}
for wl in EXPECTED_WORKLOADS:
if wl not in baseline_confirm:
raise KeyError(f"Baseline confirmation results missing workload '{wl}'")
selected_method = BASELINE_POLICY[wl]
if selected_method not in baseline_confirm[wl]:
raise KeyError(
f"Baseline confirmation results for '{wl}' missing policy method '{selected_method}'"
)
entry = baseline_confirm[wl][selected_method]
stats = entry.get("stats", {}) if isinstance(entry, dict) else {}
val_nll_mean = float(stats.get("val_nll", {}).get("mean", float("nan")))
val_acc_mean = float(stats.get("val_acc", {}).get("mean", float("nan")))
if not (math.isfinite(val_nll_mean) and math.isfinite(val_acc_mean)):
raise ValueError(f"Baseline metrics for '{wl}' method '{selected_method}' contain NaN or non-finite value")
baseline_metrics[wl] = {
"val_nll": val_nll_mean,
"val_acc": val_acc_mean,
"val_brier": float(stats.get("val_brier", {}).get("mean", 0.0) or 0.0),
"val_ece": float(stats.get("val_ece", {}).get("mean", 0.0) or 0.0),
"method_id": selected_method,
}
bytes_list = []
if (
isinstance(entry, dict)
and isinstance(entry.get("per_seed_runs"), list)
):
bytes_list = [
int(run["core_swarm_state_bytes"])
for run in entry["per_seed_runs"]
if (
isinstance(run, dict)
and "core_swarm_state_bytes" in run
and math.isfinite(float(run["core_swarm_state_bytes"]))
)
]
if bytes_list and len(set(bytes_list)) != 1:
raise ValueError(f"Baseline core-state bytes vary across seeds for '{wl}'.")
if bytes_list:
b_bytes = bytes_list[0]
else:
states = 5 * EXPECTED_PARTICLES + (1 if selected_method == "G8" else 0)
b_bytes = states * WORKLOAD_TOTAL_DIMS[wl] * 4
baseline_core_bytes[wl] = b_bytes
# Validate candidate JSON schema
if "candidate_runs" not in candidate_data:
raise KeyError("Candidate JSON missing top-level 'candidate_runs' key")
candidate_runs = candidate_data["candidate_runs"]
if not isinstance(candidate_runs, dict) or len(candidate_runs) == 0:
raise ValueError("Candidate JSON 'candidate_runs' must be a non-empty dictionary")
# Global candidate test-sealed checks
top_test_loaded_present = "official_test_data_loaded" in candidate_data
top_test_loaded_val = candidate_data.get("official_test_data_loaded")
top_test_evals_present = "official_test_evaluations" in candidate_data
top_test_evals_val = candidate_data.get("official_test_evaluations")
top_test_sealed = (
top_test_loaded_present
and top_test_loaded_val is False
and top_test_evals_present
and top_test_evals_val == 0
)
candidate_evaluations: Dict[str, Dict[str, Any]] = {}
for cand_id, wl_map in candidate_runs.items():
gate_finite = True
gate_test_sealed = bool(top_test_sealed)
gate_config_matched = True
if not isinstance(wl_map, dict):
gate_config_matched = False
gate_finite = False
wl_map = {}
if set(wl_map) != set(EXPECTED_WORKLOADS):
gate_config_matched = False
for wl in EXPECTED_WORKLOADS:
if wl not in wl_map:
gate_config_matched = False
gate_finite = False
derived_ratios: Dict[str, float] = {}
per_workload_deltas: Dict[str, Dict[str, float]] = {}
acc_regressions_valid = True
nll_regressions_valid = True
for wl in EXPECTED_WORKLOADS:
if wl not in wl_map or not isinstance(wl_map[wl], dict):
acc_regressions_valid = False
nll_regressions_valid = False
continue
wl_entry = wl_map[wl]
p_val = wl_entry.get("particles")
e_val = wl_entry.get("epochs")
sub_val = wl_entry.get("subset_size")
seeds_val = wl_entry.get("seeds")
if (
p_val != EXPECTED_PARTICLES
or e_val != EXPECTED_EPOCHS
or sub_val != EXPECTED_SUBSET_SIZE
or seeds_val != EXPECTED_SEEDS
):
gate_config_matched = False
per_seed = wl_entry.get("per_seed_runs")
if not isinstance(per_seed, list) or len(per_seed) != len(EXPECTED_SEEDS):
gate_config_matched = False
gate_finite = False
actual_seeds = []
else:
actual_seeds = [
s_run.get("seed") for s_run in per_seed if isinstance(s_run, dict) and "seed" in s_run
]
if actual_seeds != EXPECTED_SEEDS:
gate_config_matched = False
expected_queries = EXPECTED_PARTICLES * EXPECTED_EPOCHS
expected_samples = expected_queries * EXPECTED_SUBSET_SIZE
for s_run in per_seed:
if (
not isinstance(s_run, dict)
or s_run.get("total_queries") != expected_queries
or s_run.get("total_sample_evaluations") != expected_samples
):
gate_config_matched = False
stats = wl_entry.get("stats") if isinstance(wl_entry.get("stats"), dict) else {}
val_nll_dict = stats.get("val_nll") if isinstance(stats.get("val_nll"), dict) else {}
val_acc_dict = stats.get("val_acc") if isinstance(stats.get("val_acc"), dict) else {}
c_nll_raw = val_nll_dict.get("mean")
c_acc_raw = val_acc_dict.get("mean")
if (
c_nll_raw is None
or c_acc_raw is None
or not isinstance(c_nll_raw, (int, float))
or not isinstance(c_acc_raw, (int, float))
or not (math.isfinite(float(c_nll_raw)) and math.isfinite(float(c_acc_raw)))
):
gate_finite = False
c_nll = float("nan")
c_acc = float("nan")
else:
c_nll = float(c_nll_raw)
c_acc = float(c_acc_raw)
if isinstance(per_seed, list):
for s_run in per_seed:
if not isinstance(s_run, dict):
gate_finite = False
gate_config_matched = False
continue
if "official_test_evaluations" not in s_run or s_run["official_test_evaluations"] != 0:
gate_test_sealed = False
if s_run.get("is_finite") is not True:
gate_finite = False
s_nll = s_run.get("val_selected_loss")
s_acc = s_run.get("val_selected_acc")
g_nll = s_run.get("gbest_loss")
g_acc = s_run.get("gbest_acc")
w_time = s_run.get("wall_time_sec")
val_m = s_run.get("val_metrics")
val_m_ok = (
isinstance(val_m, dict)
and len(val_m) > 0
and all(
isinstance(v, (int, float)) and math.isfinite(float(v))
for v in val_m.values()
)
)
scalars_for_s_run = [s_nll, s_acc, g_nll, g_acc, w_time]
s_scalars_ok = all(
v is not None and isinstance(v, (int, float)) and math.isfinite(float(v))
for v in scalars_for_s_run
)
if not (val_m_ok and s_scalars_ok):
gate_finite = False
expected_total_dim = WORKLOAD_TOTAL_DIMS[wl]
total_dim = wl_entry.get("total_dim")
raw_ratio = wl_entry.get("ratio")
if (
total_dim != expected_total_dim
or not isinstance(raw_ratio, (int, float))
or not (0.0 < float(raw_ratio) <= 1.0)
):
gate_config_matched = False
total_dim = expected_total_dim
raw_ratio = 1.0
latent_dim = compute_latent_dim(total_dim, float(raw_ratio))
if wl_entry.get("latent_dim") != latent_dim:
gate_config_matched = False
analytical_cand_bytes = compute_core_swarm_state_bytes(EXPECTED_PARTICLES, latent_dim)
b_bytes = baseline_core_bytes[wl]
derived_ratio = float(analytical_cand_bytes) / float(b_bytes)
derived_ratios[wl] = derived_ratio
if wl_entry.get("core_swarm_state_bytes") != analytical_cand_bytes:
gate_config_matched = False
if isinstance(per_seed, list):
for s_run in per_seed:
if isinstance(s_run, dict) and s_run.get("core_swarm_state_bytes") != analytical_cand_bytes:
gate_config_matched = False
reported_state_ratio = wl_entry.get("state_ratio")
if (
not isinstance(reported_state_ratio, (int, float))
or not math.isfinite(float(reported_state_ratio))
or not math.isclose(
float(reported_state_ratio),
derived_ratio,
rel_tol=1e-5,
abs_tol=1e-5,
)
):
gate_config_matched = False
b_metrics = baseline_metrics[wl]
b_nll = b_metrics["val_nll"]
b_acc = b_metrics["val_acc"]
if math.isfinite(c_nll) and math.isfinite(c_acc) and math.isfinite(b_nll) and math.isfinite(b_acc):
nll_delta = c_nll - b_nll
rel_nll_reduction_pct = ((b_nll - c_nll) / b_nll) * 100.0 if b_nll > 0 else 0.0
acc_gain_pp = c_acc - b_acc
per_workload_deltas[wl] = {
"state_ratio": derived_ratio,
"baseline_nll": b_nll,
"candidate_nll": c_nll,
"nll_delta": nll_delta,
"rel_nll_reduction_pct": rel_nll_reduction_pct,
"baseline_acc": b_acc,
"candidate_acc": c_acc,
"acc_gain_pp": acc_gain_pp,
}
if acc_gain_pp < -1.0:
acc_regressions_valid = False
if rel_nll_reduction_pct < -5.0:
nll_regressions_valid = False
else:
acc_regressions_valid = False
nll_regressions_valid = False
gate_finite = False
if derived_ratios and len(derived_ratios) == len(EXPECTED_WORKLOADS):
max_state_ratio = max(derived_ratios.values())
else:
max_state_ratio = 1.0
gate_state_ratio = bool(0.0 < max_state_ratio <= 0.5 and math.isfinite(max_state_ratio))
gate_acc_regression = bool(acc_regressions_valid)
gate_nll_regression = bool(nll_regressions_valid)
if "mnist_wide" in per_workload_deltas:
mw_delta = per_workload_deltas["mnist_wide"]
mw_acc_gain = mw_delta["acc_gain_pp"]
mw_nll_red = mw_delta["rel_nll_reduction_pct"]
gate_baseline_worst_improvement = bool((mw_acc_gain >= 2.0) or (mw_nll_red >= 5.0))
else:
gate_baseline_worst_improvement = False
gates = {
"gate_finite": bool(gate_finite),
"gate_test_sealed": bool(gate_test_sealed),
"gate_config_matched": bool(gate_config_matched),
"gate_state_ratio": bool(gate_state_ratio),
"gate_acc_regression": bool(gate_acc_regression),
"gate_nll_regression": bool(gate_nll_regression),
"gate_baseline_worst_improvement": bool(gate_baseline_worst_improvement),
}
failed_gates = [g_name for g_name, g_pass in gates.items() if not g_pass]
failed_gate_count = len(failed_gates)
cand_pass = bool(failed_gate_count == 0)
if len(per_workload_deltas) == len(EXPECTED_WORKLOADS):
mean_rel_nll_reduction_pct = float(
sum(d["rel_nll_reduction_pct"] for d in per_workload_deltas.values())
/ len(per_workload_deltas)
)
mean_acc_gain_pp = float(
sum(d["acc_gain_pp"] for d in per_workload_deltas.values())
/ len(per_workload_deltas)
)
else:
mean_rel_nll_reduction_pct = 0.0
mean_acc_gain_pp = 0.0
if 0.0 < max_state_ratio <= 1.0 and math.isfinite(max_state_ratio):
state_efficiency_bonus = 10.0 * math.log2(1.0 / max_state_ratio)
else:
state_efficiency_bonus = 0.0
score = (
mean_rel_nll_reduction_pct
+ mean_acc_gain_pp
+ state_efficiency_bonus
- (100.0 * failed_gate_count)
)
if not math.isfinite(score):
score = -100.0 * max(1, failed_gate_count)
candidate_evaluations[cand_id] = {
"candidate_id": cand_id,
"pass": cand_pass,
"score": float(score),
"failed_gates": failed_gates,
"failed_gate_count": failed_gate_count,
"gate_details": gates,
"per_workload": per_workload_deltas,
"state_ratios": derived_ratios,
"summary_metrics": {
"mean_rel_nll_reduction_pct": mean_rel_nll_reduction_pct,
"mean_acc_gain_pp": mean_acc_gain_pp,
"max_state_ratio": max_state_ratio,
"state_efficiency_bonus": state_efficiency_bonus,
},
}
passing_cand_ids = [
c_id for c_id, c_eval in candidate_evaluations.items() if c_eval["pass"]
]
if passing_cand_ids:
selected_candidate_id = max(
passing_cand_ids, key=lambda c_id: candidate_evaluations[c_id]["score"]
)
overall_pass = True
else:
selected_candidate_id = max(
candidate_evaluations.keys(),
key=lambda c_id: candidate_evaluations[c_id]["score"],
)
overall_pass = False
selected_eval = candidate_evaluations[selected_candidate_id]
evaluator_output = {
"pass": overall_pass,
"score": float(selected_eval["score"]),
"selected_candidate_id": selected_candidate_id,
"evaluator_version": EVALUATOR_VERSION,
"source_paths": {
"baseline_path": str(baseline_path),
"candidate_path": str(candidate_path),
},
"baseline_policy": BASELINE_POLICY,
"candidate_evaluations": candidate_evaluations,
"per_workload_deltas": selected_eval["per_workload"],
"state_ratios": selected_eval["state_ratios"],
}
if output_path is not None:
save_json_atomic(evaluator_output, Path(output_path))
return evaluator_output
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Strict Pareto Evaluator for Heavy Task PSO Autoresearch"
)
parser.add_argument(
"--baseline",
type=str,
default="benchmark_results/pso_v6_heavy_tasks.json",
help="Path to baseline heavy tasks JSON artifact",
)
parser.add_argument(
"--candidate",
type=str,
default=None,
help="Path to candidate heavy autoresearch JSON artifact",
)
parser.add_argument(
"candidate_pos",
nargs="?",
type=str,
default=None,
help="Positional candidate JSON path fallback",
)
parser.add_argument(
"--output",
type=str,
default=None,
help="Optional path to write evaluator result JSON",
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
cand_path = args.candidate or args.candidate_pos
if not cand_path:
parser.error("Candidate JSON path must be supplied via --candidate or positional argument.")
out_path = Path(args.output) if args.output else None
result = evaluate_heavy_autoresearch(
baseline_path=args.baseline,
candidate_path=cand_path,
output_path=out_path,
)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+741
View File
@@ -0,0 +1,741 @@
"""Strict evaluator for the Heavy PSO cross-split robustness mission.
Every candidate is compared with a baseline rerun on the same train/validation
partition and swarm seeds. Official test data must remain sealed. Development
may qualify a policy for one-shot confirmation, but mission ``pass`` is true
only when both phases satisfy the frozen evaluator contract.
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
TEST_DIR = REPO_ROOT / "test"
if str(TEST_DIR) not in sys.path:
sys.path.insert(0, str(TEST_DIR))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from benchmark_suite import save_json_atomic
from heavy_pso_autoresearch import compute_core_swarm_state_bytes, compute_latent_dim
EVALUATOR_VERSION = "HEAVY-PSO-CROSS-SPLIT-EVALUATOR 1.0.0"
EXPECTED_PARTICLES = 12
EXPECTED_EPOCHS = 80
EXPECTED_SUBSET_SIZE = 10000
EXPECTED_QUERIES = EXPECTED_PARTICLES * EXPECTED_EPOCHS
EXPECTED_SAMPLES = EXPECTED_QUERIES * EXPECTED_SUBSET_SIZE
EXPECTED_DEV_SPLIT_SEEDS = [20260905, 20260906]
EXPECTED_DEV_SWARM_SEEDS = [101, 102, 103]
EXPECTED_CONF_SPLIT_SEEDS = [20260907]
EXPECTED_CONF_SWARM_SEEDS = [111, 112, 113]
WORKLOADS = ["mnist_compact", "mnist_wide", "fashion_compact", "fashion_wide"]
BASELINE_METHODS = {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
}
TOTAL_DIMS = {
"mnist_compact": 9098,
"mnist_wide": 55338,
"fashion_compact": 9098,
"fashion_wide": 55338,
}
PHASE_SPECS = {
"development": (EXPECTED_DEV_SPLIT_SEEDS, EXPECTED_DEV_SWARM_SEEDS),
"confirmation": (EXPECTED_CONF_SPLIT_SEEDS, EXPECTED_CONF_SWARM_SEEDS),
}
def load_artifact(path: Path) -> Dict[str, Any]:
if not path.is_file():
raise FileNotFoundError(f"Artifact file not found: {path}")
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, dict):
raise ValueError(f"Artifact at {path} must be a JSON object")
return data
def _is_finite_number(value: Any) -> bool:
return (
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(float(value))
)
def _mean(values: Sequence[float]) -> float:
return float(math.fsum(values) / len(values))
def _append_issue(issues: Dict[str, List[str]], category: str, message: str) -> None:
issues[category].append(message)
def _validate_stats(
entry: Dict[str, Any],
per_seed_runs: List[Dict[str, Any]],
label: str,
issues: Dict[str, List[str]],
) -> Optional[Tuple[float, float]]:
stats = entry.get("stats")
if not isinstance(stats, dict):
_append_issue(issues, "schema", f"{label}: missing stats object")
return None
try:
acc_mean = stats["val_acc"]["mean"]
nll_mean = stats["val_nll"]["mean"]
except (KeyError, TypeError):
_append_issue(issues, "schema", f"{label}: missing val_acc/val_nll means")
return None
if not (_is_finite_number(acc_mean) and _is_finite_number(nll_mean)):
_append_issue(issues, "finite", f"{label}: non-finite aggregate metrics")
return None
if len(per_seed_runs) > 0:
run_accs: List[float] = []
run_nlls: List[float] = []
for run in per_seed_runs:
if not isinstance(run, dict):
continue
seed_label = f"{label}/seed={run.get('seed')}"
if "val_selected_acc" not in run or run.get("val_selected_acc") is None:
_append_issue(issues, "schema", f"{seed_label}: missing val_selected_acc")
else:
val_acc = run["val_selected_acc"]
if not isinstance(val_acc, (int, float)) or isinstance(val_acc, bool):
_append_issue(issues, "schema", f"{seed_label}: non-numeric val_selected_acc")
elif not math.isfinite(float(val_acc)):
_append_issue(issues, "finite", f"{seed_label}: non-finite val_selected_acc")
else:
run_accs.append(float(val_acc))
if "val_selected_loss" not in run or run.get("val_selected_loss") is None:
_append_issue(issues, "schema", f"{seed_label}: missing val_selected_loss")
else:
val_nll = run["val_selected_loss"]
if not isinstance(val_nll, (int, float)) or isinstance(val_nll, bool):
_append_issue(issues, "schema", f"{seed_label}: non-numeric val_selected_loss")
elif not math.isfinite(float(val_nll)):
_append_issue(issues, "finite", f"{seed_label}: non-finite val_selected_loss")
else:
run_nlls.append(float(val_nll))
if len(run_accs) == len(per_seed_runs):
if not math.isclose(float(acc_mean), _mean(run_accs), rel_tol=1e-6, abs_tol=1e-5):
_append_issue(issues, "schema", f"{label}: val_acc mean disagrees with per-seed runs")
if len(run_nlls) == len(per_seed_runs):
if not math.isclose(float(nll_mean), _mean(run_nlls), rel_tol=1e-6, abs_tol=1e-5):
_append_issue(issues, "schema", f"{label}: val_nll mean disagrees with per-seed runs")
return float(acc_mean), float(nll_mean)
def _validate_runs(
entry: Dict[str, Any],
expected_seeds: List[int],
expected_state_bytes: int,
label: str,
candidate: bool,
issues: Dict[str, List[str]],
) -> Tuple[List[Dict[str, Any]], int, int]:
runs = entry.get("per_seed_runs")
if not isinstance(runs, list) or len(runs) != len(expected_seeds):
_append_issue(issues, "schema", f"{label}: expected {len(expected_seeds)} per-seed runs")
return [], 0, 0
if [run.get("seed") if isinstance(run, dict) else None for run in runs] != expected_seeds:
_append_issue(issues, "config", f"{label}: per-seed run order/content does not match {expected_seeds}")
total_queries = 0
total_samples = 0
numeric_fields = (
"val_selected_loss",
"val_selected_acc",
"gbest_loss",
"gbest_acc",
"wall_time_sec",
"optimization_wall_time_sec",
"validation_wall_time_sec",
"throughput_samples_per_sec",
)
valid_runs: List[Dict[str, Any]] = []
for run in runs:
if not isinstance(run, dict):
_append_issue(issues, "schema", f"{label}: non-object seed record")
continue
seed_label = f"{label}/seed={run.get('seed')}"
for field in numeric_fields:
if not _is_finite_number(run.get(field)):
_append_issue(issues, "finite", f"{seed_label}: missing/non-finite {field}")
val_metrics = run.get("val_metrics")
if (
not isinstance(val_metrics, dict)
or not val_metrics
or any(not _is_finite_number(value) for value in val_metrics.values())
):
_append_issue(issues, "finite", f"{seed_label}: missing/non-finite val_metrics")
if run.get("official_test_evaluations") != 0:
_append_issue(issues, "test", f"{seed_label}: official_test_evaluations must be 0")
if candidate and run.get("is_finite") is not True:
_append_issue(issues, "finite", f"{seed_label}: candidate is_finite must be true")
if run.get("total_queries") != EXPECTED_QUERIES:
_append_issue(issues, "accounting", f"{seed_label}: total_queries must be {EXPECTED_QUERIES}")
if run.get("total_sample_evaluations") != EXPECTED_SAMPLES:
_append_issue(issues, "accounting", f"{seed_label}: total_sample_evaluations must be {EXPECTED_SAMPLES}")
if run.get("core_swarm_state_bytes") != expected_state_bytes:
_append_issue(issues, "state", f"{seed_label}: incorrect core_swarm_state_bytes")
if isinstance(run.get("total_queries"), int):
total_queries += run["total_queries"]
if isinstance(run.get("total_sample_evaluations"), int):
total_samples += run["total_sample_evaluations"]
valid_runs.append(run)
return valid_runs, total_queries, total_samples
def _validate_artifact(artifact: Dict[str, Any], phase: str) -> Dict[str, Any]:
expected_splits, expected_seeds = PHASE_SPECS[phase]
issues: Dict[str, List[str]] = {
"schema": [],
"test": [],
"finite": [],
"provenance": [],
"config": [],
"accounting": [],
"state": [],
}
cells: List[Dict[str, Any]] = []
state_ratios: Dict[str, float] = {}
expected_split_keys = {str(seed) for seed in expected_splits}
if not isinstance(artifact, dict):
_append_issue(issues, "schema", f"{phase}: artifact must be an object")
return {
"issues": issues,
"cells": cells,
"state_ratios": state_ratios,
"max_state_ratio": 1.0,
"policy_signature": None,
}
if artifact.get("phase") != phase:
_append_issue(issues, "schema", f"{phase}: phase field mismatch")
if artifact.get("split_seeds") != expected_splits:
_append_issue(issues, "config", f"{phase}: split_seeds must be {expected_splits}")
if artifact.get("swarm_seeds") != expected_seeds:
_append_issue(issues, "config", f"{phase}: swarm_seeds must be {expected_seeds}")
if artifact.get("official_test_data_loaded") is not False:
_append_issue(issues, "test", f"{phase}: official_test_data_loaded must be false")
if artifact.get("official_test_evaluations") != 0:
_append_issue(issues, "test", f"{phase}: official_test_evaluations must be 0")
candidate_config = artifact.get("candidate_config")
if not isinstance(candidate_config, dict):
_append_issue(issues, "schema", f"{phase}: missing candidate_config")
candidate_config = {}
for field, expected in (
("particles", EXPECTED_PARTICLES),
("epochs", EXPECTED_EPOCHS),
("subset_size", EXPECTED_SUBSET_SIZE),
):
if candidate_config.get(field) != expected:
_append_issue(issues, "config", f"{phase}: candidate_config.{field} must be {expected}")
workload_config = artifact.get("workloads")
if not isinstance(workload_config, dict) or set(workload_config) != set(WORKLOADS):
_append_issue(issues, "schema", f"{phase}: workloads metadata must contain exactly {WORKLOADS}")
workload_config = {}
splits = artifact.get("splits")
if not isinstance(splits, dict) or set(splits) != expected_split_keys:
_append_issue(issues, "schema", f"{phase}: splits must contain exactly {sorted(expected_split_keys)}")
splits = splits if isinstance(splits, dict) else {}
observed_runs = 0
observed_queries = 0
observed_samples = 0
for split_seed in expected_splits:
split_key = str(split_seed)
split_entry = splits.get(split_key)
if not isinstance(split_entry, dict):
_append_issue(issues, "schema", f"{phase}/{split_key}: missing split object")
continue
if split_entry.get("split_seed") != split_seed:
_append_issue(issues, "provenance", f"{phase}/{split_key}: split_seed mismatch")
baselines = split_entry.get("baselines")
candidates = split_entry.get("candidates")
if not isinstance(baselines, dict) or set(baselines) != set(WORKLOADS):
_append_issue(issues, "schema", f"{phase}/{split_key}: baseline workloads incomplete")
baselines = baselines if isinstance(baselines, dict) else {}
if not isinstance(candidates, dict) or set(candidates) != set(WORKLOADS):
_append_issue(issues, "schema", f"{phase}/{split_key}: candidate workloads incomplete")
candidates = candidates if isinstance(candidates, dict) else {}
for workload in WORKLOADS:
baseline = baselines.get(workload)
candidate_entry = candidates.get(workload)
label = f"{phase}/{split_key}/{workload}"
if not isinstance(baseline, dict) or not isinstance(candidate_entry, dict):
_append_issue(issues, "schema", f"{label}: missing baseline or candidate entry")
continue
if baseline.get("method_id") != BASELINE_METHODS[workload]:
_append_issue(issues, "config", f"{label}: wrong baseline method")
for mode, entry in (("baseline", baseline), ("candidate", candidate_entry)):
if entry.get("workload_id") != workload:
_append_issue(issues, "schema", f"{label}/{mode}: workload_id mismatch")
if entry.get("split_seed") != split_seed:
_append_issue(issues, "provenance", f"{label}/{mode}: split_seed mismatch")
if entry.get("particles") != EXPECTED_PARTICLES:
_append_issue(issues, "config", f"{label}/{mode}: particles mismatch")
if entry.get("epochs") != EXPECTED_EPOCHS:
_append_issue(issues, "config", f"{label}/{mode}: epochs mismatch")
if entry.get("subset_size") != EXPECTED_SUBSET_SIZE:
_append_issue(issues, "config", f"{label}/{mode}: subset_size mismatch")
if entry.get("seeds") != expected_seeds:
_append_issue(issues, "config", f"{label}/{mode}: seeds mismatch")
fingerprints = (
baseline.get("split_fingerprint"),
candidate_entry.get("split_fingerprint"),
baseline.get("data_fingerprint"),
candidate_entry.get("data_fingerprint"),
)
if any(not isinstance(value, str) or not value for value in fingerprints):
_append_issue(issues, "provenance", f"{label}: fingerprints must be non-empty strings")
elif fingerprints[0] != fingerprints[1] or fingerprints[2] != fingerprints[3]:
_append_issue(issues, "provenance", f"{label}: baseline/candidate fingerprints differ")
total_dim = TOTAL_DIMS[workload]
baseline_states = 5 * EXPECTED_PARTICLES + (1 if BASELINE_METHODS[workload] == "G8" else 0)
expected_baseline_bytes = baseline_states * total_dim * 4
raw_ratio = candidate_entry.get("ratio")
if not _is_finite_number(raw_ratio) or not (0.0 < float(raw_ratio) <= 1.0):
_append_issue(issues, "state", f"{label}: invalid candidate ratio")
expected_candidate_bytes = -1
else:
expected_latent_dim = compute_latent_dim(total_dim, float(raw_ratio))
expected_candidate_bytes = compute_core_swarm_state_bytes(EXPECTED_PARTICLES, expected_latent_dim)
if candidate_entry.get("total_dim") != total_dim:
_append_issue(issues, "state", f"{label}: total_dim mismatch")
if candidate_entry.get("latent_dim") != expected_latent_dim:
_append_issue(issues, "state", f"{label}: latent_dim mismatch")
if candidate_entry.get("core_swarm_state_bytes") != expected_candidate_bytes:
_append_issue(issues, "state", f"{label}: candidate state bytes mismatch")
if candidate_entry.get("baseline_core_swarm_state_bytes") != expected_baseline_bytes:
_append_issue(issues, "state", f"{label}: candidate baseline state bytes mismatch")
ratio = expected_candidate_bytes / expected_baseline_bytes
state_ratios[workload] = max(state_ratios.get(workload, 0.0), ratio)
if not _is_finite_number(candidate_entry.get("state_ratio")) or not math.isclose(
float(candidate_entry.get("state_ratio", -1.0)), ratio, rel_tol=1e-6, abs_tol=1e-6
):
_append_issue(issues, "state", f"{label}: reported state_ratio mismatch")
baseline_runs, b_queries, b_samples = _validate_runs(
baseline,
expected_seeds,
expected_baseline_bytes,
f"{label}/baseline",
False,
issues,
)
candidate_runs, c_queries, c_samples = _validate_runs(
candidate_entry,
expected_seeds,
expected_candidate_bytes,
f"{label}/candidate",
True,
issues,
)
observed_runs += len(baseline_runs) + len(candidate_runs)
observed_queries += b_queries + c_queries
observed_samples += b_samples + c_samples
baseline_stats = _validate_stats(baseline, baseline_runs, f"{label}/baseline", issues)
candidate_stats = _validate_stats(candidate_entry, candidate_runs, f"{label}/candidate", issues)
if baseline_stats is not None and candidate_stats is not None:
baseline_acc, baseline_nll = baseline_stats
candidate_acc, candidate_nll = candidate_stats
nll_reduction = (
(baseline_nll - candidate_nll) / baseline_nll
if baseline_nll > 0.0
else float("nan")
)
if not math.isfinite(nll_reduction):
_append_issue(issues, "finite", f"{label}: NLL reduction is non-finite")
else:
cells.append(
{
"phase": phase,
"split_seed": split_seed,
"workload_id": workload,
"baseline_acc": baseline_acc,
"candidate_acc": candidate_acc,
"baseline_nll": baseline_nll,
"candidate_nll": candidate_nll,
"acc_gain_pp": candidate_acc - baseline_acc,
"nll_reduction_fraction": nll_reduction,
}
)
expected_runs = len(expected_splits) * len(WORKLOADS) * len(expected_seeds) * 2
if observed_runs != expected_runs:
_append_issue(issues, "accounting", f"{phase}: observed {observed_runs} runs, expected {expected_runs}")
resources = artifact.get("resource_totals")
if not isinstance(resources, dict):
_append_issue(issues, "accounting", f"{phase}: missing resource_totals")
resources = {}
if resources.get("total_runs") != observed_runs:
_append_issue(issues, "accounting", f"{phase}: total_runs does not match records")
if resources.get("total_queries") != observed_queries:
_append_issue(issues, "accounting", f"{phase}: total_queries does not match records")
if resources.get("total_samples_evaluated") != observed_samples:
_append_issue(issues, "accounting", f"{phase}: total_samples_evaluated does not match records")
if resources.get("official_test_evaluations") != 0:
_append_issue(issues, "test", f"{phase}: resource official_test_evaluations must be 0")
max_state_ratio = max(state_ratios.values(), default=1.0)
policy_signature = {
"candidate_config": candidate_config,
"workloads": workload_config,
}
return {
"issues": issues,
"cells": cells,
"state_ratios": state_ratios,
"max_state_ratio": max_state_ratio,
"policy_signature": policy_signature,
}
def evaluate_heavy_cross_split(
development_artifact: Dict[str, Any],
confirmation_artifact: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
dev_result = _validate_artifact(development_artifact, "development")
conf_result = (
_validate_artifact(confirmation_artifact, "confirmation")
if confirmation_artifact is not None
else None
)
gates: Dict[str, Dict[str, Any]] = {}
failed_gates: List[str] = []
development_gate_names: List[str] = []
def record_gate(
name: str,
passed: bool,
observed: Any,
expected: Any,
details: str,
development_gate: bool = False,
) -> None:
gates[name] = {
"pass": bool(passed),
"observed": observed,
"expected": expected,
"details": details,
}
if not passed:
failed_gates.append(name)
if development_gate:
development_gate_names.append(name)
all_results = [dev_result] + ([conf_result] if conf_result is not None else [])
record_gate(
"schema_and_phase_seeds",
all(not result["issues"]["schema"] for result in all_results),
[message for result in all_results for message in result["issues"]["schema"]][:10],
"complete artifacts with exact declared phase/split/swarm seeds",
"Missing evidence is rejected rather than defaulted",
True,
)
record_gate(
"official_test_sealed",
all(not result["issues"]["test"] for result in all_results),
[message for result in all_results for message in result["issues"]["test"]][:10],
"loaded=false and evaluations=0 at artifact, resource, and run levels",
"Official test data must never be loaded or evaluated",
True,
)
record_gate(
"all_runs_finite",
all(not result["issues"]["finite"] for result in all_results),
[message for result in all_results for message in result["issues"]["finite"]][:10],
"all aggregate and per-run validation metrics and times finite",
"Every recorded validation metric must be finite",
True,
)
record_gate(
"split_and_fingerprint_matched",
all(not result["issues"]["provenance"] for result in all_results),
[message for result in all_results for message in result["issues"]["provenance"]][:10],
"non-empty matching split/data fingerprints and split seeds per baseline/candidate cell",
"Every delta must use a baseline rerun on the identical partition",
True,
)
policy_matches = conf_result is None or (
dev_result["policy_signature"] == conf_result["policy_signature"]
)
config_issues = [message for result in all_results for message in result["issues"]["config"]]
if not policy_matches:
config_issues.append("confirmation candidate policy differs from the frozen development policy")
record_gate(
"configuration_and_policy_matched",
not config_issues,
config_issues[:10],
"12p x 80e x fixed10k, exact seeds, matching baseline, identical frozen candidate policy",
"Confirmation cannot change the development-selected policy",
True,
)
record_gate(
"query_and_sample_accounting_exact",
all(not result["issues"]["accounting"] for result in all_results),
[message for result in all_results for message in result["issues"]["accounting"]][:10],
f"{EXPECTED_QUERIES} queries and {EXPECTED_SAMPLES} sample evaluations per run",
"Per-run and aggregate accounting must agree exactly",
True,
)
max_state_ratio = max(result["max_state_ratio"] for result in all_results)
state_issues = [message for result in all_results for message in result["issues"]["state"]]
state_ok = not state_issues and 0.0 < max_state_ratio <= 0.5 + 1e-12
record_gate(
"maximum_state_ratio_each_workload",
state_ok,
{"max_state_ratio": max_state_ratio, "issues": state_issues[:10]},
"analytically verified state ratio <= 0.5 for every cell",
"No absent or reported-only state evidence is accepted",
True,
)
dev_cells = dev_result["cells"]
conf_cells = conf_result["cells"] if conf_result is not None else []
all_cells = dev_cells + conf_cells
expected_dev_cells = len(EXPECTED_DEV_SPLIT_SEEDS) * len(WORKLOADS)
complete_dev_cells = len(dev_cells) == expected_dev_cells
def nonregression(cells: List[Dict[str, Any]]) -> Tuple[bool, float, float]:
if not cells:
return False, float("inf"), float("inf")
max_acc_regression = max(-cell["acc_gain_pp"] for cell in cells)
max_nll_regression = max(-cell["nll_reduction_fraction"] for cell in cells)
return (
max_acc_regression <= 1.0 + 1e-12 and max_nll_regression <= 0.05 + 1e-12,
max_acc_regression,
max_nll_regression,
)
all_nonreg, max_acc_reg, max_nll_reg = nonregression(all_cells)
record_gate(
"maximum_accuracy_regression_percentage_points_each_split_workload",
bool(all_cells) and max_acc_reg <= 1.0 + 1e-12,
max_acc_reg,
"<= 1.0 pp",
"No evaluated split-workload cell may regress accuracy by more than 1 pp",
True,
)
record_gate(
"maximum_nll_regression_fraction_each_split_workload",
bool(all_cells) and max_nll_reg <= 0.05 + 1e-12,
max_nll_reg,
"<= 0.05",
"No evaluated split-workload cell may regress NLL by more than 5%",
True,
)
dev_nonreg, dev_acc_reg, dev_nll_reg = nonregression(dev_cells)
dev_acc_mean = _mean([cell["acc_gain_pp"] for cell in dev_cells]) if dev_cells else float("-inf")
dev_nll_mean = _mean([cell["nll_reduction_fraction"] for cell in dev_cells]) if dev_cells else float("-inf")
record_gate(
"development_grand_mean_accuracy_gain_minimum_pp",
complete_dev_cells and dev_acc_mean >= 0.0,
dev_acc_mean,
">= 0.0 pp",
"Development grand mean accuracy must not regress",
True,
)
record_gate(
"development_grand_mean_nll_reduction_minimum_fraction",
complete_dev_cells and dev_nll_mean >= 0.0,
dev_nll_mean,
">= 0.0",
"Development grand mean NLL must not regress",
True,
)
dev_mw = [cell for cell in dev_cells if cell["workload_id"] == "mnist_wide"]
dev_mw_acc = _mean([cell["acc_gain_pp"] for cell in dev_mw]) if dev_mw else float("-inf")
dev_mw_nll = _mean([cell["nll_reduction_fraction"] for cell in dev_mw]) if dev_mw else float("-inf")
record_gate(
"development_mnist_wide_improvement",
len(dev_mw) == len(EXPECTED_DEV_SPLIT_SEEDS) and (dev_mw_acc >= 2.0 or dev_mw_nll >= 0.05),
{"accuracy_gain_pp": dev_mw_acc, "nll_reduction_fraction": dev_mw_nll},
"mean accuracy gain >=2pp OR mean NLL reduction >=5%",
"The prior worst baseline workload must materially improve across development partitions",
True,
)
development_pass = (
all(not messages for messages in dev_result["issues"].values())
and 0.0 < dev_result["max_state_ratio"] <= 0.5 + 1e-12
and complete_dev_cells
and dev_nonreg
and dev_acc_mean >= 0.0
and dev_nll_mean >= 0.0
and len(dev_mw) == len(EXPECTED_DEV_SPLIT_SEEDS)
and (dev_mw_acc >= 2.0 or dev_mw_nll >= 0.05)
)
if confirmation_artifact is None:
record_gate(
"confirmation_executed",
False,
"not executed",
"one exact confirmation artifact after development_pass",
"A development pass only qualifies the frozen policy for one-shot confirmation",
)
else:
record_gate(
"confirmation_executed",
True,
{"split_seeds": confirmation_artifact.get("split_seeds"), "swarm_seeds": confirmation_artifact.get("swarm_seeds")},
{"split_seeds": EXPECTED_CONF_SPLIT_SEEDS, "swarm_seeds": EXPECTED_CONF_SWARM_SEEDS},
"Confirmation evidence is evaluated only with the exact sealed phase contract",
)
expected_conf_cells = len(EXPECTED_CONF_SPLIT_SEEDS) * len(WORKLOADS)
complete_conf_cells = len(conf_cells) == expected_conf_cells
conf_nonreg, conf_acc_reg, conf_nll_reg = nonregression(conf_cells)
record_gate(
"confirmation_per_cell_non_regression",
complete_conf_cells and conf_nonreg,
{"cells": len(conf_cells), "max_acc_regression_pp": conf_acc_reg, "max_nll_regression_fraction": conf_nll_reg},
"4 cells; accuracy regression <=1pp and NLL regression <=5% in each",
"The sealed partition must remain safe workload by workload",
)
conf_acc_mean = _mean([cell["acc_gain_pp"] for cell in conf_cells]) if conf_cells else float("-inf")
conf_nll_mean = _mean([cell["nll_reduction_fraction"] for cell in conf_cells]) if conf_cells else float("-inf")
record_gate(
"confirmation_grand_mean_accuracy_gain_minimum_pp",
complete_conf_cells and conf_acc_mean >= 1.5,
conf_acc_mean,
">= 1.5 pp",
"One-shot confirmation must retain the predeclared accuracy effect",
)
record_gate(
"confirmation_grand_mean_nll_reduction_minimum_fraction",
complete_conf_cells and conf_nll_mean >= 0.02,
conf_nll_mean,
">= 0.02",
"One-shot confirmation must retain the predeclared NLL effect",
)
conf_mw = [cell for cell in conf_cells if cell["workload_id"] == "mnist_wide"]
conf_mw_acc = _mean([cell["acc_gain_pp"] for cell in conf_mw]) if conf_mw else float("-inf")
conf_mw_nll = _mean([cell["nll_reduction_fraction"] for cell in conf_mw]) if conf_mw else float("-inf")
record_gate(
"confirmation_mnist_wide_improvement",
len(conf_mw) == 1 and (conf_mw_acc >= 1.0 or conf_mw_nll >= 0.03),
{"accuracy_gain_pp": conf_mw_acc, "nll_reduction_fraction": conf_mw_nll},
"accuracy gain >=1pp OR NLL reduction >=3%",
"The prior worst workload must improve on the sealed partition",
)
combined_mw = dev_mw + conf_mw
combined_mw_acc = _mean([cell["acc_gain_pp"] for cell in combined_mw]) if combined_mw else float("-inf")
combined_mw_nll = _mean([cell["nll_reduction_fraction"] for cell in combined_mw]) if combined_mw else float("-inf")
record_gate(
"combined_mnist_wide_improvement",
len(combined_mw) == 3 and (combined_mw_acc >= 2.0 or combined_mw_nll >= 0.05),
{"accuracy_gain_pp": combined_mw_acc, "nll_reduction_fraction": combined_mw_nll},
"three-split mean accuracy gain >=2pp OR NLL reduction >=5%",
"The material worst-workload improvement must hold across all new partitions",
)
score_cells = all_cells if confirmation_artifact is not None else dev_cells
mean_acc_gain = _mean([cell["acc_gain_pp"] for cell in score_cells]) if score_cells else 0.0
mean_nll_reduction = _mean([cell["nll_reduction_fraction"] for cell in score_cells]) if score_cells else 0.0
score_gate_names = list(gates) if confirmation_artifact is not None else development_gate_names
score_failed_gates = sum(not gates[name]["pass"] for name in score_gate_names)
state_points = 10.0 * math.log2(1.0 / max_state_ratio) if 0.0 < max_state_ratio <= 1.0 else 0.0
score = (
100.0 * mean_nll_reduction
+ mean_acc_gain
+ state_points
- 100.0 * score_failed_gates
)
mission_pass = confirmation_artifact is not None and not failed_gates
return {
"pass": bool(mission_pass),
"development_pass": bool(development_pass),
"eligible_for_confirmation": bool(development_pass and confirmation_artifact is None),
"score": float(score),
"evaluator_version": EVALUATOR_VERSION,
"failed_hard_gate_count": len(failed_gates),
"failed_gates": failed_gates,
"score_failed_gate_count": score_failed_gates,
"gates": gates,
"score_components": {
"mean_relative_nll_reduction_pct": 100.0 * mean_nll_reduction,
"mean_accuracy_gain_pp": mean_acc_gain,
"state_efficiency_points": state_points,
"gate_penalty_points": 100.0 * score_failed_gates,
"max_state_ratio": max_state_ratio,
},
"summary_metrics": {
"development_cells": len(dev_cells),
"confirmation_cells": len(conf_cells),
"development_grand_mean_accuracy_gain_pp": dev_acc_mean,
"development_grand_mean_nll_reduction_fraction": dev_nll_mean,
"development_mnist_wide_accuracy_gain_pp": dev_mw_acc,
"development_mnist_wide_nll_reduction_fraction": dev_mw_nll,
},
"cell_metrics": all_cells,
"state_ratios": {
"development": dev_result["state_ratios"],
"confirmation": conf_result["state_ratios"] if conf_result is not None else None,
},
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Strict Heavy PSO cross-split evaluator")
parser.add_argument("--development", required=True, help="Development artifact JSON")
parser.add_argument("--confirmation", default=None, help="Optional one-shot confirmation artifact JSON")
parser.add_argument("--output", default=None, help="Optional evaluation JSON output")
return parser
def main() -> None:
args = build_parser().parse_args()
development = load_artifact(Path(args.development))
confirmation = load_artifact(Path(args.confirmation)) if args.confirmation else None
result = evaluate_heavy_cross_split(development, confirmation)
if args.output:
save_json_atomic(result, Path(args.output))
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,974 @@
"""Independent evaluator for the post-training model-convergence protocol.
This module is deliberately data-only: it reads JSON manifests, result records and
stored prediction records. It never imports an adapter, optional detection
package, dataset, checkpoint, or model. A result is useful only when all of the
frozen matrix and sealing invariants can be demonstrated from the saved evidence.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import random
import sys
import tempfile
from pathlib import Path
from typing import Any, Iterable, Mapping, Sequence
EVALUATOR_VERSION = "POST-TRAINING-MODEL-CONVERGENCE-EVALUATOR 1.0.0"
PROTOCOL_VERSION = "post-training-model-convergence-1.0.0"
WORKLOADS = ("cifar10_resnet18", "cifar10_resnet50", "voc_yolo11n")
CLASSIFICATION_WORKLOADS = WORKLOADS[:2]
DETECTION_WORKLOAD = WORKLOADS[2]
BASE_SEEDS = (501, 502, 503)
SWARM_SEEDS = (601, 602, 603)
SPLIT_SEED = 20260908
PROJECTION_SEED = 20260909
BOOTSTRAP_SEED = 20260910
PARTICLES = 12
PRIMARY_GENERATIONS = 60
ENSEMBLE_GENERATIONS = 20
PRIMARY_QUERIES = PARTICLES * PRIMARY_GENERATIONS
ENSEMBLE_QUERIES = PARTICLES * ENSEMBLE_GENERATIONS
PRIMARY_RUNS = len(WORKLOADS) * len(BASE_SEEDS) * len(SWARM_SEEDS)
ENSEMBLE_RUNS = len(WORKLOADS) * len(SWARM_SEEDS)
TOTAL_PSO_QUERIES = PRIMARY_RUNS * PRIMARY_QUERIES + ENSEMBLE_RUNS * ENSEMBLE_QUERIES
OBJECTIVE_SAMPLES = {"cifar10_resnet18": 1024, "cifar10_resnet50": 1024, "voc_yolo11n": 512}
TOTAL_CANDIDATE_SAMPLES = sum(
(len(BASE_SEEDS) * len(SWARM_SEEDS) * PRIMARY_QUERIES + len(SWARM_SEEDS) * ENSEMBLE_QUERIES)
* OBJECTIVE_SAMPLES[w] for w in WORKLOADS
)
BOOTSTRAP_RESAMPLES = 2000
BOOTSTRAP_ALPHA = 0.05 / 6.0
ISSUE_CATEGORIES = (
"schema", "provenance", "matrix", "accounting", "seal", "selection",
"finite", "metrics", "plateau", "overfit", "leakage", "bootstrap", "gates",
)
def _issue(issues: dict[str, list[str]], category: str, message: str) -> None:
issues.setdefault(category, []).append(message)
def _finite(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(float(value))
def _walk_nonfinite(value: Any, path: str = "") -> list[str]:
out: list[str] = []
if isinstance(value, float) and not math.isfinite(value):
out.append(path or "$")
elif isinstance(value, Mapping):
for key, item in value.items():
out.extend(_walk_nonfinite(item, f"{path}.{key}" if path else str(key)))
elif isinstance(value, (list, tuple)):
for i, item in enumerate(value):
out.extend(_walk_nonfinite(item, f"{path}[{i}]"))
return out
def _json(path: Path) -> Any:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def _canonical(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _atomic_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(value, handle, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(name, path)
except BaseException:
try:
os.unlink(name)
except OSError:
pass
raise
def _number(value: Any, *keys: str) -> float | None:
if isinstance(value, Mapping):
for key in keys:
candidate = value.get(key)
if _finite(candidate):
return float(candidate)
for candidate in value.values():
found = _number(candidate, *keys)
if found is not None:
return found
elif isinstance(value, (list, tuple)):
for candidate in value:
found = _number(candidate, *keys)
if found is not None:
return found
return None
def _int(value: Any, *keys: str) -> int | None:
number = _number(value, *keys)
if number is None or not number.is_integer():
return None
return int(number)
def _same(a: Any, b: Any, tol: float = 1e-9) -> bool:
return _finite(a) and _finite(b) and math.isclose(float(a), float(b), rel_tol=tol, abs_tol=tol)
def _resolve_json(root: Path, value: Any) -> Any:
"""Resolve a saved JSON prediction reference without opening model/data files."""
if isinstance(value, Mapping):
for key in ("path", "file", "artifact", "prediction_artifact", "predictions_path"):
ref = value.get(key)
if isinstance(ref, str) and ref.lower().endswith((".json", ".jsonl", ".pt")):
return _resolve_json(root, ref)
return value
if isinstance(value, str) and value.lower().endswith((".json", ".jsonl", ".pt")):
path = (root / value).resolve()
if root.resolve() not in path.parents:
raise ValueError(f"prediction artifact escapes run root: {value}")
if not path.is_file():
raise FileNotFoundError(path)
if path.suffix == ".jsonl":
return [_json_line for _json_line in (json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()) if _json_line]
if path.suffix == ".pt":
try:
import torch
return torch.load(path, map_location="cpu", weights_only=True)
except (ImportError, OSError, RuntimeError, TypeError, ValueError) as exc:
raise ValueError(f"unable to load weights-only prediction artifact: {path}: {exc}") from exc
return _json(path)
return value
def _hash_manifest(root: Path, manifest: Mapping[str, Any], issues: dict[str, list[str]]) -> bool:
good = True
if manifest.get("protocol_version") != PROTOCOL_VERSION:
_issue(issues, "provenance", f"frozen manifest protocol mismatch: {manifest.get('protocol_version')!r}")
good = False
if manifest.get("state") != "frozen":
_issue(issues, "seal", f"frozen manifest state must be 'frozen', got {manifest.get('state')!r}")
good = False
declared = manifest.get("manifest_hash")
payload = {key: manifest[key] for key in ("protocol_version", "config", "artifacts", "state") if key in manifest}
if not isinstance(declared, str) or hashlib.sha256(_canonical(payload)).hexdigest() != declared:
_issue(issues, "seal", "frozen_manifest.json self-hash mismatch")
good = False
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, Mapping) or not artifacts:
_issue(issues, "schema", "frozen manifest requires a non-empty artifacts map")
return False
for name, expected in artifacts.items():
if not isinstance(name, str) or not isinstance(expected, str) or len(expected) != 64:
_issue(issues, "schema", f"invalid frozen artifact hash declaration: {name!r}")
good = False
continue
path = (root / name).resolve()
if root.resolve() not in path.parents:
_issue(issues, "seal", f"frozen artifact escapes run root: {name}")
good = False
elif not path.is_file():
_issue(issues, "seal", f"frozen artifact is missing: {name}")
good = False
elif _sha256(path) != expected:
_issue(issues, "seal", f"frozen artifact hash drift: {name}")
good = False
return good
def _config_checks(config: Any, issues: dict[str, list[str]], workload_id: str | None = None) -> None:
if not isinstance(config, Mapping):
_issue(issues, "schema", "missing or non-object config")
return
expected: dict[str, Any] = {
"protocol_version": PROTOCOL_VERSION, "split_seed": SPLIT_SEED,
"projection_seed": PROJECTION_SEED, "bootstrap_seed": BOOTSTRAP_SEED,
"particle_count": PARTICLES, "pso_generations": PRIMARY_GENERATIONS,
"residual_dimension": 64, "residual_bound": 1.0, "initial_radius": 0.25,
"objective_checkpoints": [0, 10, 20, 30, 40, 50, 60],
"base_seeds": list(BASE_SEEDS), "swarm_seeds": list(SWARM_SEEDS),
}
for key, value in expected.items():
got = config.get(key)
if got != value and not (isinstance(got, tuple) and list(got) == value):
_issue(issues, "provenance", f"config.{key} must be {value!r}, got {got!r}")
ids = config.get("workload_ids")
if ids is not None and sorted(ids) != sorted(WORKLOADS):
_issue(issues, "matrix", f"config.workload_ids must be {list(WORKLOADS)!r}")
if workload_id and config.get("workload_id") not in (None, workload_id):
_issue(issues, "matrix", f"config.workload_id disagrees with {workload_id}")
def _counter(value: Any, keys: Sequence[str]) -> int | None:
if not isinstance(value, Mapping):
return None
for key in keys:
candidate = value.get(key)
if isinstance(candidate, int) and not isinstance(candidate, bool):
return candidate
return None
def _check_leakage(result: Mapping[str, Any], issues: dict[str, list[str]], workload: str) -> None:
leakage = result.get("leakage_counters")
if not isinstance(leakage, Mapping):
_issue(issues, "schema", f"{workload}: missing leakage_counters")
leakage = {}
loaded_before = leakage.get("official_test_data_loaded_before_freeze", leakage.get("test_data_loaded_before_freeze"))
if loaded_before is not False:
_issue(issues, "leakage", f"{workload}: official test data must be explicitly marked not loaded before freeze")
evaluated_before = leakage.get("official_test_evaluations_before_freeze", leakage.get("test_evaluations_before_freeze", leakage.get("official_test_forward_passes_before_freeze")))
if evaluated_before != 0:
_issue(issues, "leakage", f"{workload}: official test exposure before freeze must be explicitly zero")
for key in ("official_test_data_loaded_before_freeze", "official_test_evaluations_before_freeze"):
if key in result and ((key.endswith("freeze") and result[key] not in (False, 0))):
_issue(issues, "leakage", f"{workload}: contradictory top-level {key}")
construction = _counter(leakage, ("official_test_construction", "official_test_dataset_construction"))
if construction != 1:
_issue(issues, "leakage", f"{workload}: official test construction must equal one, got {construction}")
forwards = _counter(leakage, ("official_test_forward_passes", "official_test_evaluations"))
if forwards is None or forwards < 1:
_issue(issues, "leakage", f"{workload}: official test forward/evaluation ledger must be positive, got {forwards}")
confirmation = result.get("confirmation")
if not isinstance(confirmation, Mapping):
_issue(issues, "schema", f"{workload}: missing confirmation record")
return
for key in ("second_confirmation", "confirmation_repeated", "post_test_tuning", "post_test_reruns"):
if confirmation.get(key) not in (None, False, 0, []):
_issue(issues, "leakage", f"{workload}: forbidden repeated confirmation/tuning flag {key}")
for key in ("official_test_data_loaded_before_freeze", "official_test_evaluations_before_freeze"):
if key in confirmation and confirmation[key] not in (False, 0):
_issue(issues, "leakage", f"{workload}: confirmation contradicts sealed pre-freeze {key}")
def _record_queries(record: Mapping[str, Any]) -> tuple[int | None, int | None]:
counters = record.get("counters") if isinstance(record.get("counters"), Mapping) else record
queries = _int(counters, "objective_queries", "queries", "query_count", "total_queries", "evaluated_queries")
samples = _int(counters, "objective_samples", "samples", "sample_evaluations", "candidate_sample_evaluations", "total_sample_evaluations")
return queries, samples
def _seed_from_key(key: Any) -> int | None:
try:
text = str(key)
if text.isdigit():
return int(text)
except Exception:
pass
return None
def _collect_cells(node: Any, base: int | None = None, swarm: int | None = None) -> list[dict[str, Any]]:
"""Collect cells from either explicit records or base->swarm mappings."""
found: list[dict[str, Any]] = []
if isinstance(node, Mapping):
b = _int(node, "base_seed") if _int(node, "base_seed") is not None else base
s = _int(node, "swarm_seed") if _int(node, "swarm_seed") is not None else swarm
if s is None:
s = _int(node, "seed")
if b is not None and s is not None and any(k in node for k in ("counters", "objective_queries", "objective_samples", "queries", "samples", "generation", "endpoint", "metrics")):
item = dict(node); item.setdefault("base_seed", b); item.setdefault("swarm_seed", s); found.append(item)
for key, value in node.items():
key_seed = _seed_from_key(key)
if key_seed in BASE_SEEDS:
found.extend(_collect_cells(value, key_seed, s))
elif key_seed in SWARM_SEEDS:
found.extend(_collect_cells(value, b, key_seed))
elif key not in {"base_seed", "swarm_seed"}:
found.extend(_collect_cells(value, b, s))
elif isinstance(node, list):
for value in node:
found.extend(_collect_cells(value, base, swarm))
return found
def _collect_base_cells(node: Any, base: int | None = None) -> list[dict[str, Any]]:
"""Collect one record per base seed from flattened or base->record schemas."""
found: list[dict[str, Any]] = []
if isinstance(node, Mapping):
b = _int(node, "base_seed") if _int(node, "base_seed") is not None else base
if b is not None and any(k in node for k in ("updates", "gradient_updates", "counters", "metrics", "checkpoint", "endpoint", "objective")):
item = dict(node); item.setdefault("base_seed", b); found.append(item)
for key, value in node.items():
key_seed = _seed_from_key(key)
if key_seed in BASE_SEEDS: found.extend(_collect_base_cells(value, key_seed))
elif key not in {"base_seed", "swarm_seed"}: found.extend(_collect_base_cells(value, b))
elif isinstance(node, list):
for value in node: found.extend(_collect_base_cells(value, base))
return found
def _method_base_cells(result: Mapping[str, Any], method: str) -> list[dict[str, Any]]:
arms = result.get("arms")
return _collect_base_cells(arms.get(method)) if isinstance(arms, Mapping) and method in arms else []
def _method_evidence(value: Any, method: str, family: str, root: Path) -> bool:
"""Require non-empty metric or prediction evidence below an exact method key."""
if isinstance(value, Mapping):
for key, item in value.items():
normalized = str(key).lower().replace("-", "_")
if normalized == method:
if _prediction_records(item, root) is not None: return True
if isinstance(item, Mapping) and (_number(item, "nll", "loss", "accuracy", "map50_95", "map50", "mAP50-95") is not None): return True
if _method_evidence(item, method, family, root): return True
if _method_evidence(item, method, family, root): return True
elif isinstance(value, list):
return bool(value) and any(_method_evidence(item, method, family, root) for item in value)
return False
def _require_confirmation_methods(result: Mapping[str, Any], workload: str, family: str, root: Path, issues: dict[str, list[str]]) -> None:
confirmation = result.get("confirmation")
required = {"feature_pso", "feature_random", "feature_adam", "head_adam"}
required |= ({"uniform", "uniform_temperature", "slsqp_weights", "ensemble_pso"} if family == "classification" else {"uniform_wbf", "ensemble_pso", "ensemble_random"})
for method in sorted(required):
if not _method_evidence(confirmation, method, family, root):
_issue(issues, "metrics", f"{workload}: confirmation lacks predictions/metrics for required method {method}")
def _method_cells(result: Mapping[str, Any], method: str) -> list[dict[str, Any]]:
arms = result.get("arms")
if not isinstance(arms, Mapping):
return []
value = arms.get(method)
return _collect_cells(value) if value is not None else []
def _verify_matrix(result: Mapping[str, Any], workload: str, issues: dict[str, list[str]]) -> dict[str, Any]:
arms = result.get("arms")
if not isinstance(arms, Mapping):
_issue(issues, "schema", f"{workload}: arms must be an object")
arms = {}
required = {"feature_pso", "feature_random", "feature_adam", "head_adam"}
missing = required - set(arms)
if missing:
_issue(issues, "matrix", f"{workload}: missing arms {sorted(missing)}")
stats: dict[str, Any] = {"primary_queries": 0, "primary_random_queries": 0, "ensemble_queries": 0, "primary_samples": 0, "primary_random_samples": 0, "ensemble_samples": 0, "cells": {}}
for method in ("feature_pso", "feature_random"):
cells = _method_cells(result, method)
stats["cells"][method] = len(cells)
expected = len(BASE_SEEDS) * len(SWARM_SEEDS)
if len(cells) != expected:
_issue(issues, "matrix", f"{workload}: {method} requires {expected} base/swarm cells, got {len(cells)}")
seen: set[tuple[int, int]] = set()
for cell in cells:
key = (_int(cell, "base_seed") or -1, _int(cell, "swarm_seed") or -1)
if key in seen or key[0] not in BASE_SEEDS or key[1] not in SWARM_SEEDS:
_issue(issues, "matrix", f"{workload}: invalid or duplicate {method} cell {key}")
seen.add(key)
queries, samples = _record_queries(cell)
if queries != PRIMARY_QUERIES:
_issue(issues, "accounting", f"{workload}: {method} {key} queries must be {PRIMARY_QUERIES}, got {queries}")
if samples != PRIMARY_QUERIES * OBJECTIVE_SAMPLES[workload]:
_issue(issues, "accounting", f"{workload}: {method} {key} samples must be {PRIMARY_QUERIES * OBJECTIVE_SAMPLES[workload]}, got {samples}")
if method == "feature_pso":
if queries is not None: stats["primary_queries"] += queries
if samples is not None: stats["primary_samples"] += samples
else:
if queries is not None: stats["primary_random_queries"] += queries
if samples is not None: stats["primary_random_samples"] += samples
for method in ("feature_adam", "head_adam"):
cells = _method_base_cells(result, method); stats["cells"][method] = len(cells)
if len(cells) != len(BASE_SEEDS): _issue(issues, "matrix", f"{workload}: {method} requires exactly three base cells, got {len(cells)}")
seen = set()
for cell in cells:
seed = _int(cell, "base_seed")
if seed in seen or seed not in BASE_SEEDS: _issue(issues, "matrix", f"{workload}: invalid or duplicate {method} base cell {seed}")
if seed is not None: seen.add(seed)
ensemble = result.get("ensemble")
if not isinstance(ensemble, Mapping):
_issue(issues, "schema", f"{workload}: ensemble must be an object")
ensemble = {}
required_ensemble = ("uniform", "uniform_temperature", "slsqp_weights", "ensemble_pso") if workload in CLASSIFICATION_WORKLOADS else ("uniform_wbf", "ensemble_pso", "ensemble_random")
for method in required_ensemble:
if method not in ensemble or ensemble.get(method) in (None, {}, []): _issue(issues, "matrix", f"{workload}: missing required ensemble method {method}")
ens_pso = ensemble.get("ensemble_pso", ensemble.get("pso"))
cells = _collect_cells(ens_pso) if ens_pso is not None else []
stats["cells"]["ensemble_pso"] = len(cells)
if len(cells) != len(SWARM_SEEDS):
_issue(issues, "matrix", f"{workload}: ensemble_pso requires three swarm cells, got {len(cells)}")
seen_swarm: set[int] = set()
for cell in cells:
seed = _int(cell, "swarm_seed", "seed")
if seed in seen_swarm or seed not in SWARM_SEEDS:
_issue(issues, "matrix", f"{workload}: invalid or duplicate ensemble swarm cell {seed}")
if seed is not None: seen_swarm.add(seed)
queries, samples = _record_queries(cell)
if queries != ENSEMBLE_QUERIES:
_issue(issues, "accounting", f"{workload}: ensemble_pso queries must be {ENSEMBLE_QUERIES}, got {queries}")
if samples != ENSEMBLE_QUERIES * OBJECTIVE_SAMPLES[workload]:
_issue(issues, "accounting", f"{workload}: ensemble_pso samples must be {ENSEMBLE_QUERIES * OBJECTIVE_SAMPLES[workload]}, got {samples}")
if queries is not None: stats["ensemble_queries"] += queries
if samples is not None: stats["ensemble_samples"] += samples
if workload == DETECTION_WORKLOAD:
random_cells = _collect_cells(ensemble.get("ensemble_random")) if ensemble.get("ensemble_random") is not None else []
stats["cells"]["ensemble_random"] = len(random_cells)
if len(random_cells) != len(SWARM_SEEDS): _issue(issues, "matrix", f"{workload}: ensemble_random requires three swarm cells, got {len(random_cells)}")
seen_random = set()
for cell in random_cells:
seed = _int(cell, "swarm_seed", "seed")
if seed in seen_random or seed not in SWARM_SEEDS: _issue(issues, "matrix", f"{workload}: invalid or duplicate ensemble_random seed {seed}")
if seed is not None: seen_random.add(seed)
return stats
def _verify_selection(result: Mapping[str, Any], workload: str, issues: dict[str, list[str]]) -> None:
selection = result.get("development_selection")
if not isinstance(selection, Mapping):
_issue(issues, "selection", f"{workload}: missing development_selection")
return
selected = selection.get("primary", selection.get("feature_pso", selection.get("selected")))
if not isinstance(selected, Mapping):
_issue(issues, "selection", f"{workload}: missing primary selected endpoints")
return
for base in BASE_SEEDS:
entry = selected.get(str(base), selected.get(base))
if not isinstance(entry, Mapping):
_issue(issues, "selection", f"{workload}: no selected endpoint for base seed {base}")
continue
swarm = _int(entry, "swarm_seed", "seed")
generation = _int(entry, "generation", "final_generation")
if swarm not in SWARM_SEEDS:
_issue(issues, "selection", f"{workload}: selected seed {base} has invalid swarm {swarm}")
if generation != PRIMARY_GENERATIONS:
_issue(issues, "selection", f"{workload}: selected endpoint {base} is not final generation 60")
matches = [c for c in _method_cells(result, "feature_pso") if _int(c, "base_seed") == base and _int(c, "swarm_seed") == swarm]
if not matches:
_issue(issues, "selection", f"{workload}: selected endpoint {base}/{swarm} is not a feature_pso cell")
elif entry.get("vector_hash") and matches[0].get("vector_hash") and entry["vector_hash"] != matches[0]["vector_hash"]:
_issue(issues, "selection", f"{workload}: selected vector hash drift for base {base}")
def _prediction_records(value: Any, root: Path) -> list[dict[str, Any]] | None:
try: value = _resolve_json(root, value)
except (OSError, ValueError, json.JSONDecodeError): return None
if isinstance(value, Mapping):
if "probabilities" in value and "targets" in value:
probabilities, targets = value["probabilities"], value["targets"]
for attr in ("detach", "cpu"):
if hasattr(probabilities, attr): probabilities = getattr(probabilities, attr)()
if hasattr(targets, attr): targets = getattr(targets, attr)()
if hasattr(probabilities, "tolist"): probabilities = probabilities.tolist()
if hasattr(targets, "tolist"): targets = targets.tolist()
if isinstance(probabilities, (list, tuple)) and isinstance(targets, (list, tuple)) and len(probabilities) == len(targets):
return [{"probabilities": list(probability), "target": int(target)} for probability, target in zip(probabilities, targets)]
return None
for key in ("records", "predictions", "images", "examples", "data"):
if key in value:
got = _prediction_records(value[key], root)
if got is not None: return got
return None
if isinstance(value, list) and all(isinstance(x, Mapping) for x in value):
return [dict(x) for x in value]
return None
def _find_prediction_sets(value: Any, root: Path, prefix: str = "$") -> dict[str, list[dict[str, Any]]]:
found: dict[str, list[dict[str, Any]]] = {}
if isinstance(value, Mapping):
for key, item in value.items():
name = f"{prefix}.{key}"
if "prediction" in str(key).lower() or str(key).lower() in {"base", "pso", "feature_pso", "test"}:
records = _prediction_records(item, root)
if records is not None: found[name] = records
found.update(_find_prediction_sets(item, root, name))
elif isinstance(value, list) and value and isinstance(value[0], Mapping):
records = _prediction_records(value, root)
if records is not None: found[prefix] = records
return found
def classification_metrics(records: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
"""Recompute unrounded NLL and accuracy from per-image probabilities/targets."""
probs: list[list[float]] = []; targets: list[int] = []
for i, record in enumerate(records):
p = record.get("probabilities", record.get("probs", record.get("prob")))
target = record.get("target", record.get("label", record.get("class_id")))
if not isinstance(p, (list, tuple)) or not p or not isinstance(target, int) or isinstance(target, bool) or target < 0 or target >= len(p):
raise ValueError(f"invalid classification prediction at index {i}")
vals = [float(x) for x in p]
if not all(math.isfinite(x) and x >= 0.0 for x in vals) or not math.isclose(math.fsum(vals), 1.0, rel_tol=1e-6, abs_tol=1e-6):
raise ValueError(f"probabilities must be finite and sum to one at index {i}")
probs.append(vals); targets.append(target)
nll = math.fsum(
-math.log(max(p[t], 1e-300)) for p, t in zip(probs, targets)
) / len(probs)
predictions = [
max(range(len(p)), key=p.__getitem__) for p in probs
]
accuracy = sum(
prediction == target
for prediction, target in zip(predictions, targets)
) / len(probs)
brier = math.fsum(
math.fsum(
(probability - float(index == target)) ** 2
for index, probability in enumerate(p)
)
for p, target in zip(probs, targets)
) / len(probs)
confidences = [p[prediction] for p, prediction in zip(probs, predictions)]
ece = 0.0
for bin_index in range(15):
lower = bin_index / 15.0
upper = (bin_index + 1) / 15.0
members = [
index
for index, confidence in enumerate(confidences)
if lower <= confidence <= upper
if bin_index == 14 or confidence < upper
]
if members:
bin_accuracy = math.fsum(
predictions[index] == targets[index] for index in members
) / len(members)
bin_confidence = math.fsum(
confidences[index] for index in members
) / len(members)
ece += (
abs(bin_accuracy - bin_confidence)
* len(members)
/ len(probs)
)
return {
"n": len(probs),
"nll": nll,
"accuracy": accuracy,
"brier": brier,
"ece15": ece,
"probabilities": probs,
"targets": targets,
}
def _box(record: Mapping[str, Any]) -> tuple[float, float, float, float] | None:
value = record.get("box", record.get("bbox", record.get("xyxy")))
if not isinstance(value, (list, tuple)) or len(value) != 4 or not all(_finite(x) for x in value): return None
x1, y1, x2, y2 = map(float, value)
return (x1, y1, x2, y2) if x2 >= x1 and y2 >= y1 else None
def _iou(a: Sequence[float], b: Sequence[float]) -> float:
x1, y1 = max(a[0], b[0]), max(a[1], b[1]); x2, y2 = min(a[2], b[2]), min(a[3], b[3])
inter = max(0.0, x2 - x1) * max(0.0, y2 - y1)
area_a = max(0.0, a[2] - a[0]) * max(0.0, a[3] - a[1]); area_b = max(0.0, b[2] - b[0]) * max(0.0, b[3] - b[1])
return inter / (area_a + area_b - inter) if area_a + area_b - inter > 0 else 0.0
def _interp_ap_101(recall: Sequence[float], precision: Sequence[float]) -> float:
"""Ultralytics compute_ap: precision envelope and 101-point trapezoid."""
mrec = [0.0, *map(float, recall), 1.0]
mpre = [1.0, *map(float, precision), 0.0]
for i in range(len(mpre) - 2, -1, -1): mpre[i] = max(mpre[i], mpre[i + 1])
values: list[float] = []
for k in range(101):
x = k / 100.0; j = 0
while j + 1 < len(mrec) and mrec[j + 1] <= x: j += 1
if j + 1 >= len(mrec): values.append(mpre[-1]); continue
span = mrec[j + 1] - mrec[j]
values.append(mpre[j] if span <= 0 else mpre[j] + (mpre[j + 1] - mpre[j]) * (x - mrec[j]) / span)
return sum((values[i] + values[i + 1]) * 0.5 / 100.0 for i in range(100))
def detection_metrics(records: Sequence[Mapping[str, Any]], class_count: int | None = None) -> dict[str, Any]:
"""Recompute Ultralytics-style whole-dataset AP at IoU .50:.95.
Matching is performed independently per image. Candidate matches are sorted
by IoU and deduplicated by prediction and ground truth, as in
DetectionValidator.process_batch; AP then uses the pinned 101-point
interpolated trapezoid.
"""
thresholds = [0.50 + 0.05 * i for i in range(10)]
parsed: list[tuple[list[dict[str, Any]], list[dict[str, Any]]]] = []; max_class = -1
for i, image in enumerate(records):
predictions = image.get("predictions", image.get("detections", image.get("pred", [])))
truth = image.get("ground_truth", image.get("targets", image.get("gt", image.get("labels", []))))
if not isinstance(predictions, list) or not isinstance(truth, list): raise ValueError(f"invalid detection image record {i}")
pp: list[dict[str, Any]] = []; gg: list[dict[str, Any]] = []
for item in predictions:
if not isinstance(item, Mapping) or _box(item) is None: raise ValueError(f"invalid detection prediction {i}")
cls = item.get("class_id", item.get("class", item.get("cls"))); score = item.get("score", item.get("confidence", item.get("conf")))
if not isinstance(cls, int) or isinstance(cls, bool) or not _finite(score): raise ValueError(f"invalid detection prediction fields {i}")
pp.append({"box": _box(item), "class_id": cls, "score": float(score)}); max_class = max(max_class, cls)
for item in truth:
if not isinstance(item, Mapping) or _box(item) is None: raise ValueError(f"invalid ground truth {i}")
cls = item.get("class_id", item.get("class", item.get("cls")))
if not isinstance(cls, int) or isinstance(cls, bool): raise ValueError(f"invalid ground truth class {i}")
gg.append({"box": _box(item), "class_id": cls}); max_class = max(max_class, cls)
parsed.append((pp, gg))
present = sorted({g["class_id"] for _, gt in parsed for g in gt})
classes = present if class_count is None else [c for c in range(class_count) if c in present]
if not classes: classes = list(range(class_count or (max_class + 1))) or [0]
aps: dict[str, list[float]] = {}; precision50: list[float] = []; recall50: list[float] = []
for cls in classes:
gt_count = sum(sum(x["class_id"] == cls for x in gt) for _, gt in parsed); class_aps: list[float] = []
for threshold in thresholds:
true_by_image: list[list[bool]] = []
for preds, gt in parsed:
candidates = []
for pi, pred in enumerate(preds):
if pred["class_id"] != cls: continue
for gi, target in enumerate(gt):
if target["class_id"] == cls:
overlap = _iou(pred["box"], target["box"])
if overlap >= threshold: candidates.append((overlap, pi, gi))
candidates.sort(key=lambda x: -x[0]); used_pred: set[int] = set(); used_gt: set[int] = set(); matched: set[int] = set()
for overlap, pi, gi in candidates:
if pi not in used_pred and gi not in used_gt: used_pred.add(pi); used_gt.add(gi); matched.add(pi)
true_by_image.append([i in matched for i in range(len(preds))])
ranked = sorted(((pred["score"], hit) for (preds, _), hits in zip(parsed, true_by_image) for pred, hit in zip(preds, hits) if pred["class_id"] == cls), key=lambda x: -x[0])
tp=[]; fp=[]; ctp=cfp=0
for _, hit in ranked:
ctp += int(hit); cfp += int(not hit); tp.append(ctp); fp.append(cfp)
if gt_count == 0 or not ranked:
# Ultralytics ap_per_class skips classes with no predictions; AP is zero.
class_aps.append(0.0); continue
recalls = [x / gt_count for x in tp]; precisions = [x / max(x + y, 1) for x, y in zip(tp, fp)]
class_aps.append(_interp_ap_101(recalls, precisions))
if threshold == 0.5:
precision50.append(precisions[-1] if precisions else 0.0); recall50.append(recalls[-1] if recalls else 0.0)
aps[str(cls)] = class_aps
map50 = math.fsum(v[0] for v in aps.values()) / len(aps); map5095 = math.fsum(x for values in aps.values() for x in values) / (len(aps) * 10)
return {"per_class_ap": aps, "n": len(records), "map50": map50, "map50_95": map5095,
"precision": math.fsum(precision50) / len(precision50) if precision50 else 0.0,
"recall": math.fsum(recall50) / len(recall50) if recall50 else 0.0,
"ground_truth": sum(len(gt) for _, gt in parsed), "predictions": sum(len(preds) for preds, _ in parsed)}
def _metric_from_record(record: Any, family: str) -> dict[str, float] | None:
if not isinstance(record, Mapping): return None
keys = ("nll", "loss") if family == "classification" else ("map50_95", "map50-95", "mAP50-95", "map5095")
primary = _number(record, *keys); accuracy = _number(record, "accuracy", "acc")
map50 = _number(record, "map50", "mAP50")
if family == "classification" and primary is not None and accuracy is not None: return {"nll": primary, "accuracy": accuracy}
if family == "detection" and primary is not None: return {"map50_95": primary, **({"map50": map50} if map50 is not None else {})}
return None
def _quantile(values: Sequence[float], q: float) -> float:
ordered = sorted(float(x) for x in values)
if not ordered: raise ValueError("cannot quantile an empty sequence")
position = (len(ordered) - 1) * q; lower = int(math.floor(position)); upper = min(lower + 1, len(ordered) - 1)
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
def _record_identity(record: Mapping[str, Any], index: int) -> Any:
for key in ("image_id", "id", "key", "filename", "path", "index"):
if key in record: return (key, str(record[key]))
return ("position", index)
def _record_ground_truth(record: Mapping[str, Any]) -> Any:
return record.get("ground_truth", record.get("targets", record.get("gt", record.get("labels", []))))
def _bootstrap_alignment(pairs: Sequence[tuple[Sequence[Mapping[str, Any]], Sequence[Mapping[str, Any]]]], family: str) -> tuple[bool, str]:
if not pairs or any(len(a) != len(b) or not a for a, b in pairs): return False, "incomplete or length-mismatched paired records"
reference_ids = [_record_identity(x, i) for i, x in enumerate(pairs[0][0])]
reference_gt = [_record_ground_truth(x) for x in pairs[0][0]]
for pair_index, (base, pso) in enumerate(pairs):
if [_record_identity(x, i) for i, x in enumerate(base)] != reference_ids or [_record_identity(x, i) for i, x in enumerate(pso)] != reference_ids:
return False, f"pair {pair_index} image IDs/order differ"
if family == "classification":
base_targets = [x.get("target", x.get("label", x.get("class_id"))) for x in base]
pso_targets = [x.get("target", x.get("label", x.get("class_id"))) for x in pso]
ref_targets = [x.get("target", x.get("label", x.get("class_id"))) for x in pairs[0][0]]
if base_targets != ref_targets or pso_targets != ref_targets: return False, f"pair {pair_index} targets differ"
elif [_canonical(_record_ground_truth(x)) for x in base] != [_canonical(x) for x in reference_gt] or [_canonical(_record_ground_truth(x)) for x in pso] != [_canonical(x) for x in reference_gt]:
return False, f"pair {pair_index} ground truth differs"
return True, "aligned"
def _bootstrap_from_records(pairs: Sequence[tuple[Sequence[Mapping[str, Any]], Sequence[Mapping[str, Any]]]], family: str) -> dict[str, Any]:
aligned, reason = _bootstrap_alignment(pairs, family)
if not aligned: return {"available": False, "seed": BOOTSTRAP_SEED, "resamples": BOOTSTRAP_RESAMPLES, "reason": reason}
rng = random.Random(BOOTSTRAP_SEED); stats: list[float] = []
if family == "classification":
parsed = [(classification_metrics(a), classification_metrics(b)) for a, b in pairs]
by_class: dict[int, list[int]] = {}
for i, target in enumerate(parsed[0][0]["targets"]): by_class.setdefault(target, []).append(i)
for _ in range(BOOTSTRAP_RESAMPLES):
# One class-stratified draw is shared by every paired base/PSO model.
indices = [rng.choice(class_indices) for class_indices in by_class.values() for _ in class_indices]
deltas = []
for base, pso in parsed:
b_nll = math.fsum(-math.log(max(base["probabilities"][i][base["targets"][i]], 1e-300)) for i in indices) / len(indices)
p_nll = math.fsum(-math.log(max(pso["probabilities"][i][pso["targets"][i]], 1e-300)) for i in indices) / len(indices)
deltas.append((b_nll - p_nll) / b_nll if b_nll else 0.0)
stats.append(math.fsum(deltas) / len(deltas))
else:
for _ in range(BOOTSTRAP_RESAMPLES):
# One whole-image draw is shared by every paired base/PSO model.
indices = [rng.randrange(len(pairs[0][0])) for _ in pairs[0][0]]
deltas = []
for base, pso in pairs:
b = detection_metrics([base[i] for i in indices])["map50_95"]
p = detection_metrics([pso[i] for i in indices])["map50_95"]
deltas.append(p - b)
stats.append(math.fsum(deltas) / len(deltas))
lo = _quantile(stats, BOOTSTRAP_ALPHA); hi = _quantile(stats, 1.0 - BOOTSTRAP_ALPHA)
return {"available": True, "seed": BOOTSTRAP_SEED, "resamples": BOOTSTRAP_RESAMPLES, "alpha": BOOTSTRAP_ALPHA, "lower": lo, "upper": hi, "statistic": math.fsum(stats) / len(stats), "excludes_zero": lo > 0 or hi < 0}
def _prediction_pairs(result: Mapping[str, Any], root: Path, family: str) -> list[tuple[int, list[dict[str, Any]], list[dict[str, Any]]]]:
"""Find one test base/selected pair per frozen base seed by explicit names."""
sets = _find_prediction_sets(result.get("confirmation", {}), root)
out: list[tuple[int, list[dict[str, Any]], list[dict[str, Any]]]] = []
for seed in BASE_SEEDS:
candidates = [(name, records) for name, records in sets.items() if str(seed) in name]
base = next((records for name, records in candidates if any(x in name.lower() for x in ("base", "frozen"))), None)
pso = next((records for name, records in candidates if any(x in name.lower() for x in ("feature_pso", "selected", "pso")) and "ensemble" not in name.lower()), None)
if base is not None and pso is not None: out.append((seed, base, pso))
return out
def _audit_series(value: Any) -> list[dict[str, Any]]:
found: list[dict[str, Any]] = []
if isinstance(value, Mapping):
for item in value.values(): found.extend(_audit_series(item))
elif isinstance(value, list) and value and all(isinstance(item, Mapping) for item in value):
if any("epoch" in item or "step" in item for item in value) and any(_number(item, "loss", "audit_loss") is not None for item in value):
found.append({"records": value})
else:
for item in value: found.extend(_audit_series(item))
return found
def _plateau_flags(result: Mapping[str, Any], workload: str, family: str, issues: dict[str, list[str]]) -> dict[str, Any]:
baselines = result.get("baselines", {})
if not isinstance(baselines, Mapping): return {"available": False, "passed": False}
per_seed: dict[str, bool] = {}; details: dict[str, Any] = {}
for seed in BASE_SEEDS:
entry = baselines.get(str(seed), baselines.get(seed))
series = _audit_series(entry)
records = series[0]["records"] if series else []
records = records[-11:] if len(records) >= 11 else []
losses = [_number(x, "loss", "audit_loss") for x in records]
metric_keys = ("accuracy", "primary_metric", "selection_accuracy") if family == "classification" else ("map50_95", "mAP50-95", "primary_metric", "selection_metric")
metrics = [_number(x, *metric_keys) for x in records]
loss_ok = len(losses) == 11 and all(x is not None for x in losses)
metric_ok = len(metrics) == 11 and all(x is not None for x in metrics)
if loss_ok:
mean = math.fsum(float(x) for x in losses) / len(losses)
loss_ok = (max(losses) - min(losses)) / max(abs(mean), 1e-12) <= 0.01
if metric_ok: metric_ok = max(metrics) - min(metrics) <= 0.005
passed = bool(loss_ok and metric_ok); per_seed[str(seed)] = passed
details[str(seed)] = {"loss_ok": bool(loss_ok), "metric_ok": bool(metric_ok), "observations": len(records)}
declared = _number(entry, "baseline_plateau") if isinstance(entry, Mapping) else None
if isinstance(entry, Mapping) and "baseline_plateau" in entry and bool(entry["baseline_plateau"]) != passed:
_issue(issues, "plateau", f"{workload}: baseline seed {seed} inflated/incorrect plateau flag")
return {"available": bool(per_seed), "per_seed": per_seed, "details": details, "passed": bool(per_seed) and all(per_seed.values())}
def _workload_gates(result: Mapping[str, Any], workload: str, family: str, root: Path, issues: dict[str, list[str]]) -> dict[str, Any]:
pairs = _prediction_pairs(result, root, family)
per_seed: dict[str, Any] = {}
pair_records: list[tuple[Sequence[Mapping[str, Any]], Sequence[Mapping[str, Any]]]] = []
for seed, base_records, pso_records in pairs:
try:
base = classification_metrics(base_records) if family == "classification" else detection_metrics(base_records, 20)
pso = classification_metrics(pso_records) if family == "classification" else detection_metrics(pso_records, 20)
except (ValueError, ZeroDivisionError) as exc:
_issue(issues, "metrics", f"{workload}: test pair {seed} cannot be recomputed: {exc}"); continue
pair_records.append((base_records, pso_records))
if family == "classification": per_seed[str(seed)] = {"base": {"nll": base["nll"], "accuracy": base["accuracy"]}, "pso": {"nll": pso["nll"], "accuracy": pso["accuracy"]}, "relative_nll_reduction": (base["nll"] - pso["nll"]) / base["nll"], "accuracy_delta": pso["accuracy"] - base["accuracy"]}
else: per_seed[str(seed)] = {"base": {"map50_95": base["map50_95"], "map50": base["map50"]}, "pso": {"map50_95": pso["map50_95"], "map50": pso["map50"]}, "map50_95_delta": pso["map50_95"] - base["map50_95"], "map50_delta": pso["map50"] - base["map50"]}
ci = _bootstrap_from_records(pair_records, family)
# Confirmation predictions and the paired CI are required integrity evidence.
# A numerically negative CI is a valid scientific result; an unavailable CI
# means the frozen/confirmed artifact set is incomplete or tampered.
if len(pairs) != len(BASE_SEEDS):
_issue(issues, "metrics", f"{workload}: required confirmation pairs are incomplete ({len(pairs)}/{len(BASE_SEEDS)})")
if not ci.get("available", False):
_issue(issues, "metrics", f"{workload}: required paired bootstrap unavailable: {ci.get('reason', 'missing prediction evidence')}")
if family == "classification" and per_seed:
reductions = [x["relative_nll_reduction"] for x in per_seed.values()]; acc_deltas = [x["accuracy_delta"] for x in per_seed.values()]
gate = len(reductions) == 3 and math.fsum(reductions) / 3 >= 0.01 and math.fsum(acc_deltas) / 3 >= -0.002 and min(acc_deltas) >= -0.005 and sum(x > 0 for x in reductions) >= 2 and ci.get("excludes_zero", False)
elif family == "detection" and per_seed:
deltas = [x["map50_95_delta"] for x in per_seed.values()]
gate = len(deltas) == 3 and math.fsum(deltas) / 3 >= 0.005 and min(deltas) >= -0.005 and sum(x > 0 for x in deltas) >= 2 and ci.get("excludes_zero", False)
else: gate = False
if len(pairs) != len(BASE_SEEDS): _issue(issues, "bootstrap", f"{workload}: missing complete official-test base/feature_pso prediction pairs ({len(pairs)}/3)")
return {"available": bool(per_seed), "per_seed": per_seed, "bootstrap": ci, "generalization_pass": bool(gate)}
def _verify_hashes(result: Mapping[str, Any], root: Path, issues: dict[str, list[str]], workload: str) -> None:
declared = result.get("artifact_hashes")
if not isinstance(declared, Mapping):
_issue(issues, "schema", f"{workload}: missing artifact_hashes")
return
for name, expected in declared.items():
if not isinstance(name, str) or not isinstance(expected, str):
_issue(issues, "seal", f"{workload}: malformed artifact hash entry")
continue
path = (root / name).resolve()
if root.resolve() not in path.parents or not path.is_file():
_issue(issues, "seal", f"{workload}: missing/escaping artifact {name}")
elif _sha256(path) != expected:
_issue(issues, "seal", f"{workload}: artifact hash drift {name}")
def _compare_development_snapshot(development: Any, current: Mapping[str, Any], workload: str, issues: dict[str, list[str]]) -> None:
"""Ensure confirmation did not alter any sealed development decision/state."""
if not isinstance(development, Mapping):
_issue(issues, "schema", f"{workload}: development_result.json must contain an object")
return
development_leakage = development.get("leakage_counters")
if not isinstance(development_leakage, Mapping):
_issue(issues, "leakage", f"{workload}: development snapshot lacks leakage_counters")
else:
loaded = development_leakage.get("official_test_data_loaded_before_freeze", development_leakage.get("test_data_loaded_before_freeze"))
evaluated = development_leakage.get("official_test_evaluations_before_freeze", development_leakage.get("test_evaluations_before_freeze", development_leakage.get("official_test_forward_passes_before_freeze")))
if loaded is not False or evaluated != 0:
_issue(issues, "leakage", f"{workload}: development snapshot records pre-freeze official-test exposure")
for key in ("official_test_construction", "official_test_dataset_construction", "official_test_forward_passes", "official_test_evaluations"):
if key in development_leakage and development_leakage[key] != 0:
_issue(issues, "leakage", f"{workload}: development snapshot {key} must be zero")
development_confirmation = development.get("confirmation")
if development_confirmation not in ({}, None):
_issue(issues, "leakage", f"{workload}: development snapshot confirmation must be empty")
fields = ("config", "manifests", "provenance", "baselines", "arms", "ensemble", "development_selection", "integrity", "resource_ledger", "artifact_hashes")
for field in fields:
if field not in development:
_issue(issues, "seal", f"{workload}: development_result.json missing sealed field {field}")
continue
if field not in current:
_issue(issues, "seal", f"{workload}: current result missing sealed field {field}")
continue
if field == "artifact_hashes":
# Confirmation may add test prediction hashes. Every development hash
# must nevertheless remain present and byte-identical.
old_hashes = development[field]; new_hashes = current[field]
if not isinstance(old_hashes, Mapping) or not isinstance(new_hashes, Mapping):
_issue(issues, "seal", f"{workload}: artifact_hashes changed shape across confirmation")
else:
for name, value in old_hashes.items():
if new_hashes.get(name) != value: _issue(issues, "seal", f"{workload}: sealed artifact hash drift for {name}")
elif field == "integrity":
old_integrity = development[field]; new_integrity = current[field]
if not isinstance(old_integrity, Mapping) or not isinstance(new_integrity, Mapping):
if old_integrity != new_integrity: _issue(issues, "seal", f"{workload}: pre-confirmation integrity changed")
else:
for name, value in old_integrity.items():
if new_integrity.get(name) != value: _issue(issues, "seal", f"{workload}: pre-confirmation integrity field changed: {name}")
elif _canonical(development[field]) != _canonical(current[field]):
_issue(issues, "seal", f"{workload}: sealed pre-confirmation field changed: {field}")
def _evaluate_workload(result: Any, root: Path, workload: str, issues: dict[str, list[str]], development: Any = None) -> dict[str, Any]:
if not isinstance(result, Mapping):
_issue(issues, "schema", f"{workload}: result must be an object"); return {"workload_id": workload, "valid": False}
if result.get("workload_id") != workload: _issue(issues, "matrix", f"{workload}: workload_id mismatch")
if development is not None: _compare_development_snapshot(development, result, workload, issues)
family = "detection" if workload == DETECTION_WORKLOAD else "classification"
if result.get("family") != family: _issue(issues, "matrix", f"{workload}: family must be {family}")
for key in ("manifests", "provenance", "baselines", "arms", "ensemble", "development_selection", "confirmation", "integrity", "leakage_counters", "resource_ledger", "artifact_hashes"):
if key not in result: _issue(issues, "schema", f"{workload}: missing top-level {key}")
_config_checks(result.get("config"), issues, workload); _check_leakage(result, issues, workload); _verify_hashes(result, root, issues, workload)
accounting = _verify_matrix(result, workload, issues); _verify_selection(result, workload, issues); _require_confirmation_methods(result, workload, family, root, issues)
if result.get("integrity", {}).get("confirmed") is False if isinstance(result.get("integrity"), Mapping) else False:
_issue(issues, "seal", f"{workload}: integrity declares confirmation failure")
prediction_sets = _find_prediction_sets(result.get("confirmation", {}), root)
recomputed: dict[str, Any] = {}
for name, records in prediction_sets.items():
try: recomputed[name] = classification_metrics(records) if family == "classification" else detection_metrics(records, 20)
except (ValueError, ZeroDivisionError) as exc: _issue(issues, "metrics", f"{workload}: invalid stored predictions at {name}: {exc}")
# Check every explicitly stored metric that has a corresponding recomputation.
for name, metric in recomputed.items():
if "pso" in name.lower() and isinstance(metric, Mapping):
stored = _metric_from_record(result.get("confirmation"), family)
if stored and family == "classification" and not (_same(stored.get("nll"), metric.get("nll"), 1e-7) and _same(stored.get("accuracy"), metric.get("accuracy"), 1e-7)):
_issue(issues, "metrics", f"{workload}: stored classification metric disagrees with probabilities at {name}")
plateau = _plateau_flags(result, workload, family, issues)
gates = _workload_gates(result, workload, family, root, issues)
objective = _number(result.get("development_selection"), "objective_improvement", "relative_objective_improvement")
selection_metric = _number(result.get("development_selection"), "selection_metric", "selection_nll", "selection_map50_95")
overfit = bool(objective is not None and objective >= 0.01 and selection_metric is not None and ((family == "classification" and selection_metric > 0.01) or (family == "detection" and selection_metric < -0.005)))
if overfit: _issue(issues, "overfit", f"{workload}: objective improvement conflicts with held-out/selection metric")
return {"workload_id": workload, "family": family, "valid": True, "accounting": accounting, "recomputed": recomputed, "plateau": plateau, "gates": gates, "overfit_signal": overfit}
def evaluate_run(run_root: str | os.PathLike[str]) -> dict[str, Any]:
"""Evaluate one frozen run, returning findings even when artifacts are malformed."""
root = Path(run_root); issues = {key: [] for key in ISSUE_CATEGORIES}; workloads: dict[str, Any] = {}
manifest: Any = None
try: manifest = _json(root / "frozen_manifest.json")
except (OSError, ValueError, json.JSONDecodeError) as exc: _issue(issues, "schema", f"cannot load frozen_manifest.json: {exc}")
if isinstance(manifest, Mapping):
for location in _walk_nonfinite(manifest): _issue(issues, "finite", f"non-finite value in frozen manifest at {location}")
_hash_manifest(root, manifest, issues); _config_checks(manifest.get("config"), issues)
frozen_config = manifest.get("config") if isinstance(manifest.get("config"), Mapping) else {}
if sorted(frozen_config.get("workload_ids", ())) != sorted(WORKLOADS): _issue(issues, "matrix", "frozen manifest does not seal all three workloads")
for workload in WORKLOADS:
path = root / "workloads" / workload / "result.json"
development_path = root / "workloads" / workload / "development_result.json"
if isinstance(manifest, Mapping):
manifest_artifacts = manifest.get("artifacts", {})
expected_development = f"workloads/{workload}/development_result.json"
if not isinstance(manifest_artifacts, Mapping) or expected_development not in manifest_artifacts:
_issue(issues, "seal", f"{workload}: frozen manifest must seal {expected_development}")
try: result = _json(path)
except (OSError, ValueError, json.JSONDecodeError) as exc:
_issue(issues, "schema", f"{workload}: cannot load result.json: {exc}"); continue
try: development = _json(development_path)
except (OSError, ValueError, json.JSONDecodeError) as exc:
_issue(issues, "seal", f"{workload}: cannot load development_result.json: {exc}"); development = None
for location in _walk_nonfinite(result): _issue(issues, "finite", f"{workload}: non-finite value at {location}")
if development is not None:
for location in _walk_nonfinite(development): _issue(issues, "finite", f"{workload}: non-finite development value at {location}")
workloads[workload] = _evaluate_workload(result, root, workload, issues, development)
# Cross-workload exact accounting is intentionally independent of stored totals.
totals = {key: sum(int(w.get("accounting", {}).get(key, 0)) for w in workloads.values()) for key in ("primary_queries", "primary_random_queries", "ensemble_queries", "primary_samples", "primary_random_samples", "ensemble_samples")}
totals["pso_queries"] = totals["primary_queries"] + totals["ensemble_queries"]
totals["candidate_samples"] = totals["primary_samples"] + totals["ensemble_samples"]
if totals["pso_queries"] != TOTAL_PSO_QUERIES: _issue(issues, "accounting", f"total PSO queries must be {TOTAL_PSO_QUERIES}, got {totals['pso_queries']}")
if totals["candidate_samples"] != TOTAL_CANDIDATE_SAMPLES: _issue(issues, "accounting", f"total candidate-sample evaluations must be {TOTAL_CANDIDATE_SAMPLES}, got {totals['candidate_samples']}")
# A success flag is never consumed; it is checked against independently observed integrity.
integrity_ok = not any(issues[key] for key in ("schema", "provenance", "matrix", "accounting", "seal", "selection", "finite", "leakage", "metrics"))
payload = {"evaluator_version": EVALUATOR_VERSION, "protocol_version": PROTOCOL_VERSION, "run_root": str(root), "pass": integrity_ok, "integrity_pass": integrity_ok, "workloads": workloads, "accounting": {**totals, "expected_pso_queries": TOTAL_PSO_QUERIES, "expected_candidate_samples": TOTAL_CANDIDATE_SAMPLES}, "issues": issues, "issue_counts": {key: len(value) for key, value in issues.items()}}
return payload
def build_cli_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Evaluate frozen post-training model-convergence artifacts")
parser.add_argument("--run-root", type=Path, required=True)
parser.add_argument("--output", type=Path, default=None)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = build_cli_parser().parse_args(argv); payload = evaluate_run(args.run_root)
destination = args.output or args.run_root / "evaluation.json"
try: _atomic_json(destination, payload)
except OSError as exc:
print(f"evaluator output failed: {exc}", file=sys.stderr); return 2
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
return 0 if payload["pass"] else 1
__all__ = [
"BASE_SEEDS", "BOOTSTRAP_ALPHA", "BOOTSTRAP_RESAMPLES", "BOOTSTRAP_SEED", "CLASSIFICATION_WORKLOADS",
"DETECTION_WORKLOAD", "EVALUATOR_VERSION", "ENSEMBLE_QUERIES", "TOTAL_CANDIDATE_SAMPLES", "TOTAL_PSO_QUERIES",
"WORKLOADS", "classification_metrics", "detection_metrics", "evaluate_run", "main", "build_cli_parser",
]
if __name__ == "__main__":
raise SystemExit(main())
+95 -72
View File
@@ -1,87 +1,110 @@
# %%
import json
import os
import sys
import argparse
import torch
import torch.nn as nn
import numpy as np
import tensorflow as tf
from keras.datasets import fashion_mnist
from keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from keras.models import Sequential
from pso import optimizer
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
def get_data():
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
def get_data(seed: int = 42):
from sklearn.decomposition import PCA
from torchvision.datasets import FashionMNIST
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
train_dataset = FashionMNIST(root="./data", train=True, download=True)
test_dataset = FashionMNIST(root="./data", train=False, download=True)
y_train, y_test = tf.one_hot(y_train, 10), tf.one_hot(y_test, 10)
x_train_raw = (train_dataset.data[:3000].float() / 255.0).reshape(3000, -1).numpy()
y_train = train_dataset.targets[:3000].long()
x_train, x_test = tf.convert_to_tensor(x_train), tf.convert_to_tensor(x_test)
y_train, y_test = tf.convert_to_tensor(y_train), tf.convert_to_tensor(y_test)
x_test_raw = (test_dataset.data[:1000].float() / 255.0).reshape(1000, -1).numpy()
y_test = test_dataset.targets[:1000].long()
print(f"x_train : {x_train[0].shape} | y_train : {y_train[0].shape}")
print(f"x_test : {x_test[0].shape} | y_test : {y_test[0].shape}")
pca = PCA(n_components=32, whiten=True, random_state=seed)
x_train_pca = pca.fit_transform(x_train_raw)
x_test_pca = pca.transform(x_test_raw)
x_train = torch.tensor(x_train_pca, dtype=torch.float32)
x_test = torch.tensor(x_test_pca, dtype=torch.float32)
print(f"x_train : {x_train.shape} | y_train : {y_train.shape}")
print(f"x_test : {x_test.shape} | y_test : {y_test.shape}")
return x_train, y_train, x_test, y_test
def make_model():
model = Sequential()
model.add(
Conv2D(32, kernel_size=(5, 5), activation="relu", input_shape=(28, 28, 1))
def make_model(seed: int = 42):
torch.manual_seed(seed)
return nn.Linear(32, 10)
def main():
parser = argparse.ArgumentParser(description="PSO Fashion-MNIST Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "original",
"initialization": "model_noise",
"evaluation": "fixed_subset",
"convergence": "particle_reset",
"refinement": "adam",
"n_particles": 30,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.0,
"mutation_swarm": 0.05,
"particle_min": -3.0,
"particle_max": 3.0,
"velocity_limit_ratio": 0.1,
"boundary_strategy": "reflect",
"seed": 42,
"epochs": 80,
"batch_size": 1000,
"fitness_size": 2000,
"renewal": "loss",
"output_dir": "output/fashion_mnist",
"checkpoint_interval": 25,
"refinement_epochs": 10,
"refinement_lr": 0.001,
},
)
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dropout(0.25))
model.add(Dense(256, activation="relu"))
model.add(Dense(128, activation="relu"))
model.add(Dense(10, activation="softmax"))
args = parser.parse_args()
return model
model = make_model(seed=args.seed)
x_train, y_train, x_test, y_test = get_data(seed=args.seed)
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.CrossEntropyLoss(),
task="multiclass",
inertia_profile={"c0": 0.7, "c1": 0.5, "w_min": 0.1, "w_max": 0.8},
)
pso_fashion = Optimizer(**kwargs)
print(f"Optimizer device: {pso_fashion.device}")
best_score = pso_fashion.fit(
x_train,
y_train,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
validation_data=(x_test, y_test),
output_dir=args.output_dir,
checkpoint_interval=25,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
# %%
model = make_model()
x_train, y_train, x_test, y_test = get_data()
pso_mnist = optimizer(
model,
loss="categorical_crossentropy",
n_particles=200,
c0=0.7,
c1=0.5,
w_min=0.1,
w_max=0.8,
negative_swarm=0.0,
mutation_swarm=0.05,
convergence_reset=True,
convergence_reset_patience=10,
convergence_reset_monitor="loss",
)
best_score = pso_mnist.fit(
x_train,
y_train,
epochs=1000,
save_info=True,
log=2,
log_name="fashion_mnist",
renewal="loss",
check_point=25,
batch_size=5000,
)
print("Done!")
sys.exit(0)
if __name__ == "__main__":
main()
-115
View File
@@ -1,115 +0,0 @@
from keras.models import Sequential
from keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from keras.datasets import mnist, fashion_mnist
from keras.utils import to_categorical
# from tensorflow.data.Dataset import from_tensor_slices
import tensorflow as tf
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
gpus = tf.config.experimental.list_physical_devices("GPU")
if gpus:
try:
tf.config.experimental.set_memory_growth(gpus[0], True)
except Exception as e:
print(e)
finally:
del gpus
def get_data():
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
print(f"y_train : {y_train[0]} | y_test : {y_test[0]}")
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
print(f"x_train : {x_train[0].shape} | y_train : {y_train[0].shape}")
print(f"x_test : {x_test[0].shape} | y_test : {y_test[0].shape}")
return x_train, y_train, x_test, y_test
class _batch_generator:
def __init__(self, x, y, batch_size: int = 32):
self.batch_size = batch_size
self.index = 0
self.x = x
self.y = y
self.setBatchSize(batch_size)
def next(self):
self.index += 1
if self.index >= self.max_index:
self.index = 0
return self.dataset[self.index][0], self.dataset[self.index][1]
def getMaxIndex(self):
return self.max_index
def getIndex(self):
return self.index
def setIndex(self, index):
self.index = index
def getBatchSize(self):
return self.batch_size
def setBatchSize(self, batch_size):
self.batch_size = batch_size
self.dataset = list(
tf.data.Dataset.from_tensor_slices(
(self.x, self.y)).batch(batch_size)
)
self.max_index = len(self.dataset)
def getDataset(self):
return self.dataset
def make_model():
model = Sequential()
model.add(
Conv2D(32, kernel_size=(5, 5), activation="sigmoid",
input_shape=(28, 28, 1))
)
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, kernel_size=(3, 3), activation="sigmoid"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dropout(0.25))
model.add(Dense(128, activation="sigmoid"))
model.add(Dense(10, activation="softmax"))
return model
model = make_model()
x_train, y_train, x_test, y_test = get_data()
print(x_train.shape)
y_train = tf.one_hot(y_train, 10)
y_test = tf.one_hot(y_test, 10)
dataset = _batch_generator(x_train, y_train, 32)
model.compile(optimizer="adam", loss="mse", metrics=["accuracy"])
count = 0
while count < 100:
x_batch, y_batch = dataset.next()
count += 1
print("Training model...")
model.fit(x_batch, y_batch, epochs=1, batch_size=1, verbose=1)
print(count)
print(f"Max index : {dataset.getMaxIndex()}")
print("Evaluating model...")
model.evaluate(x_test, y_test, verbose=2)
weights = model.get_weights()
+129
View File
@@ -0,0 +1,129 @@
"""Fashion-MNIST dataset gradient baseline (PyTorch standard backprop optimizer, non-PSO)."""
import copy
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
class FashionMNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=5)
self.sig1 = nn.Sigmoid()
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3)
self.sig2 = nn.Sigmoid()
self.pool2 = nn.MaxPool2d(2, 2)
self.drop = nn.Dropout(0.25)
self.fc1 = nn.Linear(64 * 5 * 5, 128)
self.sig3 = nn.Sigmoid()
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.pool1(self.sig1(self.conv1(x)))
x = self.pool2(self.sig2(self.conv2(x)))
x = torch.flatten(x, 1)
x = self.drop(x)
x = self.sig3(self.fc1(x))
x = self.fc2(x)
return x
def get_data(download: bool = True):
from torchvision import datasets, transforms
transform = transforms.ToTensor()
train_dataset = datasets.FashionMNIST(
root="./data", train=True, transform=transform, download=download
)
test_dataset = datasets.FashionMNIST(
root="./data", train=False, transform=transform, download=download
)
return train_dataset, test_dataset
def get_device() -> torch.device:
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_built()
and torch.backends.mps.is_available()
):
return torch.device("mps")
elif torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
def main():
torch.manual_seed(42)
np.random.seed(42)
device = get_device()
print(f"Selected device: {device}")
train_dataset, test_dataset = get_data(download=True)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
model = FashionMNISTModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
best_val_loss = float("inf")
best_state = None
for epoch in range(10):
model.train()
for bx, by in train_loader:
bx, by = bx.to(device), by.to(device)
optimizer.zero_grad()
out = model(bx)
loss = criterion(out, by)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0.0
total = 0
with torch.no_grad():
for bx, by in test_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
val_loss += loss.item() * bx.size(0)
total += bx.size(0)
val_loss /= total
if val_loss < best_val_loss:
best_val_loss = val_loss
best_state = copy.deepcopy(model.state_dict())
if best_state is not None:
model.load_state_dict(best_state)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for bx, by in test_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
test_loss += loss.item() * bx.size(0)
preds = out.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
test_loss /= total
test_acc = correct / total
print(f"Final test loss: {test_loss:.4f}, accuracy: {test_acc:.4f}")
if __name__ == "__main__":
main()
+707
View File
@@ -0,0 +1,707 @@
"""
Full-MNIST 60k/10k Particle Swarm Optimization Trajectory Analysis
Evaluates the selected Adaptive Moment PSO configuration on full official MNIST (60,000 train / 10,000 test)
across 120 particles for 240 continuous epochs, scoring all 60,000 training examples for every particle
at every epoch.
Predeclared Diagnostic Criteria:
1. Post-80 Training Convergence: Mean training loss falls >= 1% from epoch 80 to epoch 240.
2. Epoch 240 Test Gain: Mean test accuracy at epoch 240 rises >= 1 percentage point vs epoch 80.
3. Late Plateau (200->240): Training loss improvement < 1% AND absolute test accuracy change < 0.5 percentage points.
"""
import argparse
import csv
import datetime
import json
import math
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
# Ensure test/ directory is in sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from benchmark_suite import (
calc_stats,
extract_plugin_metadata,
compute_data_fingerprint,
compute_model_fingerprint,
get_hardware_provenance,
make_mnist_model,
resolve_execution_device,
save_json_atomic,
sync_device,
)
from pso import Optimizer, __version__ as pso_version
from reproduce_scaling import validate_and_load_baseline
FULL_MNIST_PROTOCOL_VERSION = "1.0.0"
def prepare_full_mnist_data() -> Tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
str,
Dict[str, Any],
]:
"""
Load official torchvision MNIST full train (60,000) and test (10,000).
Normalize pixels, flatten to 784, fit PCA(n_components=32, whiten=True, random_state=42)
on train only, transform test. Validate 60,000/10,000 sample counts and label range [0, 9].
"""
from torchvision.datasets import MNIST
from sklearn.decomposition import PCA
cache_dir = Path("result/cache")
cache_dir.mkdir(parents=True, exist_ok=True)
train_dataset = MNIST(root=str(cache_dir), train=True, download=True)
test_dataset = MNIST(root=str(cache_dir), train=False, download=True)
n_train = len(train_dataset.data)
n_test = len(test_dataset.data)
if n_train != 60000:
raise ValueError(f"Expected 60,000 training samples; got {n_train}")
if n_test != 10000:
raise ValueError(f"Expected 10,000 test samples; got {n_test}")
y_train_60000 = train_dataset.targets.long()
y_test_10000 = test_dataset.targets.long()
min_tr_lbl, max_tr_lbl = int(y_train_60000.min()), int(y_train_60000.max())
min_te_lbl, max_te_lbl = int(y_test_10000.min()), int(y_test_10000.max())
if min_tr_lbl != 0 or max_tr_lbl != 9:
raise ValueError(f"Train label range must be [0, 9]; got [{min_tr_lbl}, {max_tr_lbl}]")
if min_te_lbl != 0 or max_te_lbl != 9:
raise ValueError(f"Test label range must be [0, 9]; got [{min_te_lbl}, {max_te_lbl}]")
x_train_raw = (train_dataset.data.float() / 255.0).reshape(60000, -1).numpy()
x_test_raw = (test_dataset.data.float() / 255.0).reshape(10000, -1).numpy()
pca = PCA(n_components=32, whiten=True, random_state=42)
x_full_tr = torch.tensor(pca.fit_transform(x_train_raw), dtype=torch.float32)
x_full_test = torch.tensor(pca.transform(x_test_raw), dtype=torch.float32)
data_fp = compute_data_fingerprint(x_full_tr, x_full_test, y_train_60000, y_test_10000)
pca_provenance = {
"n_components": 32,
"whiten": True,
"random_state": 42,
"fit_scope": "official_train_split_60000_only",
"train_samples_fit": 60000,
"test_samples_transformed": 10000,
"explained_variance_ratio_sum": float(np.sum(pca.explained_variance_ratio_)),
}
return (
x_full_tr,
y_train_60000,
x_full_test,
y_test_10000,
data_fp,
pca_provenance,
)
def render_trajectory_plot(
checkpoint_stats: Dict[int, Dict[str, Any]],
subset_comparison: Dict[str, Any],
figure_path: Path,
):
"""
Render a readable two-panel mean +/- SD trajectory plot for training loss and test accuracy,
with optional dashed subset-study mean comparison and epoch 80 marker.
"""
figure_path.parent.mkdir(parents=True, exist_ok=True)
eps = sorted(list(checkpoint_stats.keys()))
tr_loss_mean = [checkpoint_stats[ep]["train_loss"]["mean"] for ep in eps]
tr_loss_std = [checkpoint_stats[ep]["train_loss"]["std"] for ep in eps]
te_acc_mean = [checkpoint_stats[ep]["test_acc"]["mean"] for ep in eps]
te_acc_std = [checkpoint_stats[ep]["test_acc"]["std"] for ep in eps]
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
# Panel 1: Training Loss
ax1.plot(eps, tr_loss_mean, "o-", color="tab:blue", linewidth=2, label="Full MNIST Train Loss (Mean)")
ax1.fill_between(
eps,
np.array(tr_loss_mean) - np.array(tr_loss_std),
np.array(tr_loss_mean) + np.array(tr_loss_std),
color="tab:blue",
alpha=0.2,
label="±1 SD",
)
if "matching_epoch_deltas" in subset_comparison:
sub_deltas = subset_comparison["matching_epoch_deltas"]
sub_eps = sorted([ep for ep in eps if str(ep) in sub_deltas or ep in sub_deltas])
if sub_eps:
sub_tr_loss = [sub_deltas.get(str(ep), sub_deltas.get(ep, {}))["subset_study_train_loss_mean"] for ep in sub_eps]
ax1.plot(sub_eps, sub_tr_loss, "--", color="gray", alpha=0.8, label="Subset Study (2k sample) Train Loss")
if 80 in eps:
ax1.axvline(80, color="red", linestyle=":", label="Epoch 80 Marker")
ax1.set_ylabel("Training Loss")
ax1.set_title("Full MNIST (60,000 Train / 10,000 Test) PSO Trajectory (120 Particles, AM)")
ax1.grid(True, linestyle="--", alpha=0.5)
ax1.legend(loc="upper right")
# Panel 2: Test Accuracy
ax2.plot(eps, te_acc_mean, "s-", color="tab:green", linewidth=2, label="Full MNIST Test Accuracy (Mean)")
ax2.fill_between(
eps,
np.array(te_acc_mean) - np.array(te_acc_std),
np.array(te_acc_mean) + np.array(te_acc_std),
color="tab:green",
alpha=0.2,
label="±1 SD",
)
if "matching_epoch_deltas" in subset_comparison:
sub_deltas = subset_comparison["matching_epoch_deltas"]
sub_eps = sorted([ep for ep in eps if str(ep) in sub_deltas or ep in sub_deltas])
if sub_eps:
sub_te_acc = [sub_deltas.get(str(ep), sub_deltas.get(ep, {}))["subset_study_test_acc_mean"] for ep in sub_eps]
ax2.plot(sub_eps, sub_te_acc, "--", color="gray", alpha=0.8, label="Subset Study (2k sample) Test Acc")
if 80 in eps:
ax2.axvline(80, color="red", linestyle=":", label="Epoch 80 Marker")
ax2.set_xlabel("Epoch")
ax2.set_ylabel("Test Accuracy")
ax2.grid(True, linestyle="--", alpha=0.5)
ax2.legend(loc="lower right")
plt.tight_layout()
fig.savefig(figure_path, dpi=300)
plt.close(fig)
def run_full_mnist_study(
baseline_path: Path,
subset_study_path: Path,
output_json_path: Path,
output_csv_path: Path,
figure_path: Path,
device_str: str = "auto",
epochs: int = 240,
seeds: List[int] = None,
) -> bool:
if seeds is None:
seeds = [71, 72, 73, 74, 75]
if not isinstance(epochs, int) or isinstance(epochs, bool) or epochs <= 0:
raise ValueError("epochs must be a positive integer")
if epochs % 20 != 0:
raise ValueError("epochs must be a multiple of 20 so every final checkpoint exists")
if not seeds or len(seeds) != len(set(seeds)):
raise ValueError("seeds must be a non-empty list of unique integers")
if any(isinstance(seed, bool) or not isinstance(seed, int) or seed < 0 for seed in seeds):
raise ValueError("every seed must be a non-negative integer")
dev_input = None if device_str == "auto" else device_str
device = resolve_execution_device(dev_input)
hw_provenance = get_hardware_provenance(device)
# 1. Validate baseline JSON & load winner config
baseline_data, _baseline_records, winner_cfg, _expected_fp = validate_and_load_baseline(
baseline_path
)
if baseline_data.get("device") != device.type:
raise ValueError(
f"Baseline device mismatch: baseline requires {baseline_data.get('device')!r}, got {device.type!r}"
)
if baseline_data.get("pso_version") != pso_version:
raise ValueError(
f"PSO version mismatch: baseline requires {baseline_data.get('pso_version')!r}, got {pso_version!r}"
)
if baseline_data.get("torch_version") != torch.__version__:
raise ValueError(
f"Torch version mismatch: baseline requires {baseline_data.get('torch_version')!r}, got {torch.__version__!r}"
)
# 2. Prepare Full MNIST PCA Data (60k train / 10k test)
(
x_full_tr,
y_train_60000,
x_full_test,
y_test_10000,
data_fp,
pca_provenance,
) = prepare_full_mnist_data()
opt_kwargs = winner_cfg.to_optimizer_kwargs(quick=False)
opt_kwargs["evaluation"] = "full"
opt_kwargs.pop("fitness_size", None)
n_particles = 120
target_epochs = epochs
batch_size = 60000
checkpoint_interval = 20
ckpt_epochs = [ep for ep in range(checkpoint_interval, target_epochs + 1, checkpoint_interval)]
if not ckpt_epochs or ckpt_epochs[-1] != target_epochs:
if target_epochs not in ckpt_epochs:
ckpt_epochs.append(target_epochs)
ckpt_epochs = sorted(list(set(ckpt_epochs)))
runs: List[Dict[str, Any]] = []
flat_csv_rows: List[Dict[str, Any]] = []
plugin_meta: Dict[str, Any] | None = None
# 3. Seed Runs
for seed in sorted(seeds):
# Warmup Phase (2 epochs full eval)
warmup_model = make_mnist_model(seed=seed)
warmup_loss = nn.CrossEntropyLoss()
warmup_opt = Optimizer(
model=warmup_model,
loss=warmup_loss,
task="multiclass",
n_particles=n_particles,
seed=seed,
device=device,
**opt_kwargs,
)
warmup_opt.fit(
x_full_tr,
y_train_60000,
epochs=2,
batch_size=batch_size,
renewal="loss",
)
sync_device(device)
del warmup_opt, warmup_model, warmup_loss
# Timed Continuous Trajectory
model = make_mnist_model(seed=seed)
model_fp = compute_model_fingerprint(model)
loss_inst = nn.CrossEntropyLoss()
opt = Optimizer(
model=model,
loss=loss_inst,
task="multiclass",
n_particles=n_particles,
seed=seed,
device=device,
**opt_kwargs,
)
current_plugin_meta = extract_plugin_metadata(opt)
if plugin_meta is None:
plugin_meta = current_plugin_meta
elif current_plugin_meta != plugin_meta:
raise RuntimeError("Resolved plugin metadata changed across seeds")
with tempfile.TemporaryDirectory() as temp_dir_str:
output_dir = Path(temp_dir_str)
sync_device(device)
t0 = time.perf_counter()
train_loss_final, train_acc_final, train_mse_final = opt.fit(
x_full_tr,
y_train_60000,
epochs=target_epochs,
batch_size=batch_size,
renewal="loss",
output_dir=output_dir,
log_format="csv",
checkpoint_interval=checkpoint_interval,
)
sync_device(device)
t1 = time.perf_counter()
fit_time_sec = t1 - t0
if not (math.isfinite(train_loss_final) and math.isfinite(train_acc_final) and math.isfinite(train_mse_final)):
raise RuntimeError(f"Seed {seed} final metrics non-finite: loss={train_loss_final}, acc={train_acc_final}")
history_csv_path = output_dir / "history.csv"
epoch_history = []
if history_csv_path.exists():
with open(history_csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
epoch_history.append({
"epoch": int(row["epoch"]),
"loss": float(row["loss"]),
"accuracy": float(row["accuracy"]),
"mse": float(row["mse"]),
})
prev_best_loss = float("inf")
improvement_count = 0
last_improvement_epoch = 1
for row in epoch_history:
ep_num = row["epoch"]
l_val = row["loss"]
if l_val < prev_best_loss:
improvement_count += 1
last_improvement_epoch = ep_num
prev_best_loss = l_val
checkpoints: List[Dict[str, Any]] = []
ckpt_dir = output_dir / "checkpoints"
for ep in ckpt_epochs:
ckpt_path = ckpt_dir / f"epoch-{ep}.pt"
if not ckpt_path.exists():
raise FileNotFoundError(f"Missing checkpoint file: {ckpt_path}")
payload = torch.load(ckpt_path, map_location=device, weights_only=True)
ckpt_tr_loss, ckpt_tr_acc, ckpt_tr_mse = payload["score"]
if not (math.isfinite(ckpt_tr_loss) and math.isfinite(ckpt_tr_acc) and math.isfinite(ckpt_tr_mse)):
raise RuntimeError(f"Seed {seed} epoch {ep} score non-finite: {payload['score']}")
opt.eval_model.load_state_dict(payload["model_state_dict"])
opt._global_best_weights = opt.codec.encode(opt.eval_model)
test_loss, test_acc, test_mse = opt.evaluate(x_full_test, y_test_10000)
if not (math.isfinite(test_loss) and math.isfinite(test_acc) and math.isfinite(test_mse)):
raise RuntimeError(f"Seed {seed} epoch {ep} test metrics non-finite: loss={test_loss}, acc={test_acc}")
ckpt_record = {
"epoch": ep,
"train_loss": float(ckpt_tr_loss),
"train_acc": float(ckpt_tr_acc),
"train_mse": float(ckpt_tr_mse),
"test_loss": float(test_loss),
"test_acc": float(test_acc),
"test_mse": float(test_mse),
}
checkpoints.append(ckpt_record)
flat_csv_rows.append({
"seed": seed,
"epoch": ep,
"train_loss": float(ckpt_tr_loss),
"train_acc": float(ckpt_tr_acc),
"train_mse": float(ckpt_tr_mse),
"test_loss": float(test_loss),
"test_acc": float(test_acc),
"test_mse": float(test_mse),
"fit_time_sec": round(fit_time_sec, 4),
})
runs.append({
"seed": seed,
"model_fingerprint": model_fp,
"fit_time_sec": round(fit_time_sec, 4),
"improvement_count": improvement_count,
"last_improvement_epoch": last_improvement_epoch,
"checkpoints": checkpoints,
"completed": True,
"error": None,
"plugins": current_plugin_meta,
"epoch_history": epoch_history,
})
# 4. Aggregations & Statistics
checkpoint_stats: Dict[int, Dict[str, Any]] = {}
for ep in ckpt_epochs:
ep_train_losses = [next(c["train_loss"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_train_accs = [next(c["train_acc"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_train_mses = [next(c["train_mse"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_losses = [next(c["test_loss"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_accs = [next(c["test_acc"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
ep_test_mses = [next(c["test_mse"] for c in r["checkpoints"] if c["epoch"] == ep) for r in runs]
checkpoint_stats[ep] = {
"train_loss": calc_stats(ep_train_losses),
"train_acc": calc_stats(ep_train_accs),
"train_mse": calc_stats(ep_train_mses),
"test_loss": calc_stats(ep_test_losses),
"test_acc": calc_stats(ep_test_accs),
"test_mse": calc_stats(ep_test_mses),
}
# Endpoint paired deltas (80->240 and 200->240 if available)
paired_deltas: Dict[str, Any] = {}
paired_endpoint_deltas_by_seed: List[Dict[str, Any]] = []
has_80 = 80 in checkpoint_stats
has_200 = 200 in checkpoint_stats
has_240 = 240 in checkpoint_stats
if has_80 and has_240:
deltas_80_to_240_train_rel = []
deltas_80_to_240_test_acc = []
for r in runs:
c80 = next(c for c in r["checkpoints"] if c["epoch"] == 80)
c240 = next(c for c in r["checkpoints"] if c["epoch"] == 240)
tl80, tl240 = c80["train_loss"], c240["train_loss"]
ta80, ta240 = c80["test_acc"], c240["test_acc"]
rel_red = (tl80 - tl240) / tl80 if tl80 > 0 else 0.0
acc_delta = ta240 - ta80
deltas_80_to_240_train_rel.append(rel_red)
deltas_80_to_240_test_acc.append(acc_delta)
paired_rec = {
"seed": r["seed"],
"train_loss_relative_reduction_80_to_240": rel_red,
"test_accuracy_delta_80_to_240": acc_delta,
}
if has_200:
c200 = next(c for c in r["checkpoints"] if c["epoch"] == 200)
tl200, ta200 = c200["train_loss"], c200["test_acc"]
rel_red_200 = (tl200 - tl240) / tl200 if tl200 > 0 else 0.0
acc_delta_200 = ta240 - ta200
paired_rec["train_loss_relative_reduction_200_to_240"] = rel_red_200
paired_rec["test_accuracy_delta_200_to_240"] = acc_delta_200
paired_endpoint_deltas_by_seed.append(paired_rec)
paired_deltas["80_to_240"] = {
"train_loss_rel_reduction": calc_stats(deltas_80_to_240_train_rel),
"test_acc_delta": calc_stats(deltas_80_to_240_test_acc),
}
if has_200 and has_240:
deltas_200_to_240_train_rel = []
deltas_200_to_240_test_acc = []
deltas_200_to_240_test_acc_abs = []
for r in runs:
c200 = next(c for c in r["checkpoints"] if c["epoch"] == 200)
c240 = next(c for c in r["checkpoints"] if c["epoch"] == 240)
tl200, tl240 = c200["train_loss"], c240["train_loss"]
ta200, ta240 = c200["test_acc"], c240["test_acc"]
rel_red_200 = (tl200 - tl240) / tl200 if tl200 > 0 else 0.0
acc_delta_200 = ta240 - ta200
acc_abs_200 = abs(ta240 - ta200)
deltas_200_to_240_train_rel.append(rel_red_200)
deltas_200_to_240_test_acc.append(acc_delta_200)
deltas_200_to_240_test_acc_abs.append(acc_abs_200)
paired_deltas["200_to_240"] = {
"train_loss_rel_reduction": calc_stats(deltas_200_to_240_train_rel),
"test_acc_delta": calc_stats(deltas_200_to_240_test_acc),
"test_acc_abs_change": calc_stats(deltas_200_to_240_test_acc_abs),
}
# Predeclared Diagnostics
diagnostics = {}
if has_80 and has_240:
mean_tl80 = checkpoint_stats[80]["train_loss"]["mean"]
mean_tl240 = checkpoint_stats[240]["train_loss"]["mean"]
mean_ta80 = checkpoint_stats[80]["test_acc"]["mean"]
mean_ta240 = checkpoint_stats[240]["test_acc"]["mean"]
post80_train_improvement_pct = (mean_tl80 - mean_tl240) / mean_tl80 if mean_tl80 > 0 else 0.0
epoch240_test_gain = mean_ta240 - mean_ta80
diagnostics["post80_train_improvement_pct"] = round(post80_train_improvement_pct, 6)
diagnostics["post80_train_improvement_passed"] = bool(post80_train_improvement_pct >= 0.01)
diagnostics["epoch240_test_gain"] = round(epoch240_test_gain, 6)
diagnostics["epoch240_test_gain_passed"] = bool(epoch240_test_gain >= 0.01)
if has_200:
mean_tl200 = checkpoint_stats[200]["train_loss"]["mean"]
mean_ta200 = checkpoint_stats[200]["test_acc"]["mean"]
late_rel_red = (mean_tl200 - mean_tl240) / mean_tl200 if mean_tl200 > 0 else 0.0
late_acc_abs = abs(mean_ta240 - mean_ta200)
is_late_plateau = bool(late_rel_red < 0.01 and late_acc_abs < 0.005)
diagnostics["late_plateau_200_240"] = is_late_plateau
diagnostics["late_plateau_train_loss_rel_reduction_200_240"] = round(late_rel_red, 6)
diagnostics["late_plateau_test_acc_abs_change_200_240"] = round(late_acc_abs, 6)
# 5. Descriptive Comparison vs Subset Study
subset_comparison: Dict[str, Any] = {}
if subset_study_path.exists():
try:
with open(subset_study_path, "r", encoding="utf-8") as f:
sub_json_data = json.load(f)
sub_ckpt_stats = sub_json_data.get("summary", {}).get("checkpoint_stats", {})
matching_stats = {}
for ep in ckpt_epochs:
ep_str = str(ep)
if ep_str in sub_ckpt_stats:
sub_e = sub_ckpt_stats[ep_str]
full_tr_l = checkpoint_stats[ep]["train_loss"]["mean"]
sub_tr_l = sub_e["train_loss"]["mean"]
full_te_a = checkpoint_stats[ep]["test_acc"]["mean"]
sub_te_a = sub_e["test_acc"]["mean"]
matching_stats[ep_str] = {
"full_mnist_train_loss_mean": full_tr_l,
"subset_study_train_loss_mean": sub_tr_l,
"train_loss_delta_full_minus_subset": round(full_tr_l - sub_tr_l, 6),
"full_mnist_test_acc_mean": full_te_a,
"subset_study_test_acc_mean": sub_te_a,
"test_acc_delta_full_minus_subset": round(full_te_a - sub_te_a, 6),
}
subset_comparison = {
"subset_study_path": str(subset_study_path),
"disclaimer": (
"The full and subset studies optimize different training objectives and the evaluation "
"plugin changes RNG consumption (all 60,000 train samples versus a fixed 2,000-sample "
"fitness subset). Matching-seed and matching-epoch comparisons are descriptive, not an "
"exact paired causal isolation."
),
"matching_epoch_deltas": matching_stats,
}
except Exception as err:
subset_comparison = {"error": f"Failed to parse subset study JSON: {err}"}
# 6. Save Artifacts
# JSON output
out_dict = {
"full_mnist_protocol_version": FULL_MNIST_PROTOCOL_VERSION,
"pso_version": pso_version,
"torch_version": torch.__version__,
"hardware": hw_provenance,
"device": device.type,
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"baseline_path": str(baseline_path),
"subset_study_path": str(subset_study_path),
"sample_counts": {
"train_samples": 60000,
"test_samples": 10000,
},
"pca_provenance": pca_provenance,
"data_fingerprint": data_fp,
"fitness_evaluation_contract": {
"selector": "full",
"fitness_size": None,
"train_samples_per_particle_per_epoch": 60000,
"particle_evaluations": n_particles * target_epochs * len(seeds),
"particle_sample_evaluations": (
n_particles * target_epochs * len(seeds) * 60000
),
"test_samples_per_checkpoint": 10000,
},
"candidate_label": winner_cfg.candidate_label,
"config": {
**opt_kwargs,
"n_particles": n_particles,
"epochs": target_epochs,
"batch_size": batch_size,
"renewal": "loss",
"checkpoint_interval": checkpoint_interval,
},
"plugins": plugin_meta,
"timing_scope": "fit_only_after_full-evaluation_two-epoch_warmup",
"diagnostics": diagnostics,
"descriptive_subset_comparison": subset_comparison,
"summary": {
"epochs": ckpt_epochs,
"checkpoint_stats": {str(k): v for k, v in checkpoint_stats.items()},
"paired_deltas": paired_deltas,
"paired_endpoint_deltas_by_seed": paired_endpoint_deltas_by_seed,
},
"runs": runs,
"completed": True,
"valid": True,
"error": None,
}
save_json_atomic(out_dict, output_json_path)
# CSV output
output_csv_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"seed",
"epoch",
"train_loss",
"train_acc",
"train_mse",
"test_loss",
"test_acc",
"test_mse",
"fit_time_sec",
]
with open(output_csv_path, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for r in flat_csv_rows:
writer.writerow(r)
# Plot output
render_trajectory_plot(checkpoint_stats, subset_comparison, figure_path)
return True
def main():
parser = argparse.ArgumentParser(
description="Full-MNIST 60k/10k Particle Swarm Optimization Trajectory Analysis"
)
parser.add_argument(
"--baseline-json",
type=Path,
default=Path("benchmark_results/pso_v4_tuning.json"),
help="Path to baseline tuning JSON (default: benchmark_results/pso_v4_tuning.json)",
)
parser.add_argument(
"--subset-study-json",
type=Path,
default=Path("benchmark_results/pso_v4_epoch_convergence.json"),
help="Path to subset epoch convergence JSON (default: benchmark_results/pso_v4_epoch_convergence.json)",
)
parser.add_argument(
"--output-json",
type=Path,
default=Path("benchmark_results/pso_v4_full_mnist.json"),
help="Path to output JSON (default: benchmark_results/pso_v4_full_mnist.json)",
)
parser.add_argument(
"--output-csv",
type=Path,
default=Path("benchmark_results/pso_v4_full_mnist.csv"),
help="Path to output CSV (default: benchmark_results/pso_v4_full_mnist.csv)",
)
parser.add_argument(
"--figure",
type=Path,
default=Path("history_plt/pso_v4_full_mnist.png"),
help="Path to output plot figure PNG (default: history_plt/pso_v4_full_mnist.png)",
)
parser.add_argument(
"--device",
type=str,
default="auto",
help="Execution device: auto, mps, cpu, cuda (default: auto)",
)
parser.add_argument(
"--epochs",
type=int,
default=240,
help="Number of PSO training epochs (default: 240)",
)
parser.add_argument(
"--seeds",
type=str,
default="71,72,73,74,75",
help="Comma-separated random seeds (default: 71,72,73,74,75)",
)
args = parser.parse_args()
seed_list = [int(s.strip()) for s in args.seeds.split(",") if s.strip()]
run_full_mnist_study(
baseline_path=args.baseline_json,
subset_study_path=args.subset_study_json,
output_json_path=args.output_json,
output_csv_path=args.output_csv,
figure_path=args.figure,
device_str=args.device,
epochs=args.epochs,
seeds=seed_list,
)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
"""
Heavy PSO Cross-Split Experiment Runner.
Protocol Version: HEAVY-PSO-CROSS-SPLIT 1.0.0
Runs matching baseline and candidate PSO experiments across development or confirmation
data splits under sealed official test conditions (official_test_evaluations = 0).
Phase Specifications:
- Development: split_seeds = (20260905, 20260906), swarm_seeds = (101, 102, 103)
- Confirmation: split_seeds = (20260907,), swarm_seeds = (111, 112, 113)
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
# Ensure test directory and repo root are in Python path
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).resolve().parent.name != "PSO" else Path(__file__).resolve().parent
TEST_DIR = REPO_ROOT / "test"
if str(TEST_DIR) not in sys.path:
sys.path.insert(0, str(TEST_DIR))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from benchmark_suite import (
get_hardware_provenance,
resolve_execution_device,
save_json_atomic,
)
from heavy_pso_autoresearch import (
DEFAULT_GEOMETRY_MULTIPLIER,
parse_projection_seed_arg,
parse_projection_scope_arg,
validate_projection_scope_config,
get_effective_projection_scope,
run_heavy_pso_autoresearch,
)
from heavy_task_feasibility import (
WORKLOADS,
run_heavy_task_confirm,
)
from pso import __version__ as pso_version
PROTOCOL_VERSION = "HEAVY-PSO-CROSS-SPLIT 1.0.0"
PHASE_CONFIGS = {
"development": {
"split_seeds": [20260905, 20260906],
"swarm_seeds": [101, 102, 103],
},
"confirmation": {
"split_seeds": [20260907],
"swarm_seeds": [111, 112, 113],
},
}
WORKLOAD_BASELINE_METHODS = {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
}
FROZEN_PROJECTION_SEEDS = {
"mnist_compact": 1800044939,
"mnist_wide": 592157828,
"fashion_compact": 1363313651,
"fashion_wide": 189641451,
}
def run_heavy_pso_cross_split(
phase: str = "development",
ratio: float = 0.5,
geometry_policy: str = "baseline_aligned",
projection_scope: Union[str, Dict[str, str]] = "global",
projection_seed_mode: str = "explicit",
projection_seed: Optional[Union[int, Dict[str, int]]] = None,
geometry_multiplier: float = DEFAULT_GEOMETRY_MULTIPLIER,
particles: int = 12,
epochs: int = 80,
subset_size: int = 10000,
device_str: Optional[str] = None,
cache_dir: Optional[Path] = None,
output_path: Optional[Path] = None,
) -> Dict[str, Any]:
"""
Runs cross-split evaluation for development or confirmation phase.
Enforces exact split and swarm seed contracts for each phase.
Reruns matching baseline and candidate models per split seed.
"""
projection_scope = parse_projection_scope_arg(projection_scope)
validate_projection_scope_config(projection_scope)
if phase not in PHASE_CONFIGS:
raise ValueError(
f"Invalid phase '{phase}'. Must be one of {list(PHASE_CONFIGS.keys())}"
)
phase_spec = PHASE_CONFIGS[phase]
split_seeds = phase_spec["split_seeds"]
swarm_seeds = phase_spec["swarm_seeds"]
if projection_seed_mode == "explicit" and projection_seed is None:
projection_seed = dict(FROZEN_PROJECTION_SEEDS)
start_time = time.time()
device = resolve_execution_device(device_str)
hardware_info = get_hardware_provenance(device)
if cache_dir is None:
cache_dir = REPO_ROOT / "result" / "cache"
splits_payload: Dict[str, Any] = {}
total_runs = 0
total_queries = 0
total_samples_evaluated = 0
for split_seed in split_seeds:
# 1. Baseline runs for this split seed
selected_baseline_methods = {
wl_id: [WORKLOAD_BASELINE_METHODS[wl_id]] for wl_id in WORKLOADS
}
baseline_res = run_heavy_task_confirm(
workloads=WORKLOADS,
selected_methods=selected_baseline_methods,
particles=particles,
epochs=epochs,
seeds=swarm_seeds,
split_seed=split_seed,
device=device,
cache_dir=cache_dir,
)
# 2. Candidate runs for this split seed
candidate_res = run_heavy_pso_autoresearch(
ratios=[ratio],
particles=particles,
epochs=epochs,
subset_size=subset_size,
seeds=swarm_seeds,
geometry_policy=geometry_policy,
device_str=device_str,
cache_dir=cache_dir,
split_seed=split_seed,
projection_scope=projection_scope,
projection_seed_mode=projection_seed_mode,
projection_seed=projection_seed,
geometry_multiplier=geometry_multiplier,
)
# Extract candidate ratio payload
candidate_ratio_runs = list(candidate_res["candidate_runs"].values())[0]
baselines_split: Dict[str, Any] = {}
candidates_split: Dict[str, Any] = {}
dataset_fingerprints: Dict[str, str] = {}
split_fingerprints: Dict[str, str] = {}
for wl_id in WORKLOADS:
b_method = WORKLOAD_BASELINE_METHODS[wl_id]
b_entry = baseline_res[wl_id][b_method]
baselines_split[wl_id] = b_entry
c_entry = candidate_ratio_runs[wl_id]
candidates_split[wl_id] = c_entry
dataset_name = WORKLOADS[wl_id].dataset_name
dataset_fingerprints[dataset_name] = c_entry["data_fingerprint"]
split_fingerprints[dataset_name] = c_entry["split_fingerprint"]
# Resource accumulation
for r in b_entry["per_seed_runs"]:
total_runs += 1
total_queries += r["total_queries"]
total_samples_evaluated += r["total_sample_evaluations"]
for r in c_entry["per_seed_runs"]:
total_runs += 1
total_queries += r["total_queries"]
total_samples_evaluated += r["total_sample_evaluations"]
splits_payload[str(split_seed)] = {
"split_seed": split_seed,
"data_fingerprints": dataset_fingerprints,
"split_fingerprints": split_fingerprints,
"baselines": baselines_split,
"candidates": candidates_split,
}
payload = {
"version": PROTOCOL_VERSION,
"protocol_version": PROTOCOL_VERSION,
"phase": phase,
"split_seeds": list(split_seeds),
"swarm_seeds": list(swarm_seeds),
"official_test_data_loaded": False,
"official_test_evaluations": 0,
"candidate_config": {
"ratio": ratio,
"geometry_policy": geometry_policy,
"projection_scope": projection_scope,
"projection_seed_mode": projection_seed_mode,
"projection_seed": projection_seed,
"geometry_multiplier": float(geometry_multiplier),
"particles": particles,
"epochs": epochs,
"subset_size": subset_size,
},
"workloads": {
wl_id: {
"workload_id": wl_id,
"dataset_name": wl_cfg.dataset_name,
"model_name": wl_cfg.model_name,
"baseline_method": WORKLOAD_BASELINE_METHODS[wl_id],
"projection_scope": get_effective_projection_scope(projection_scope, wl_id),
"effective_projection_seed": (
projection_seed[wl_id]
if isinstance(projection_seed, dict)
else projection_seed
),
}
for wl_id, wl_cfg in WORKLOADS.items()
},
"splits": splits_payload,
"resource_totals": {
"total_runs": total_runs,
"total_queries": total_queries,
"total_samples_evaluated": total_samples_evaluated,
"official_test_evaluations": 0,
"wall_time_sec": round(time.time() - start_time, 4),
},
"provenance": {
"hardware": hardware_info,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"pso_version": pso_version,
},
}
if output_path is not None:
save_json_atomic(payload, output_path)
return payload
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Heavy PSO Cross-Split Experiment Runner (Development / Confirmation)"
)
parser.add_argument(
"--phase",
type=str,
default="development",
choices=list(PHASE_CONFIGS.keys()),
help="Experiment phase ('development' or 'confirmation')",
)
parser.add_argument("--device", type=str, default=None, help="Device (cpu, mps, cuda)")
parser.add_argument("--cache-dir", type=str, default=None, help="Dataset cache directory")
parser.add_argument("--output", type=str, default=None, help="Output artifact JSON path")
parser.add_argument(
"--ratio",
type=float,
default=0.5,
help="Subspace ratio for candidate PSO (default: 0.5)",
)
parser.add_argument(
"--geometry-policy",
type=str,
default="baseline_aligned",
help="Geometry policy (default: 'baseline_aligned')",
)
parser.add_argument(
"--projection-scope",
type=parse_projection_scope_arg,
default="global",
help="Projection scope ('global', 'tensor_local', 'balanced_global', 'two_hash_global', 'largest_tensor_hash', 'largest_tensor_row_hash', 'adjacent_pair', 'adjacent_difference', or workload dict)",
)
parser.add_argument(
"--projection-seed-mode",
type=str,
default="explicit",
help="Projection seed mode (default: 'explicit')",
)
parser.add_argument(
"--projection-seed",
type=parse_projection_seed_arg,
default=None,
help="Exact projection seed (int or dict) for explicit mode",
)
parser.add_argument(
"--geometry-multiplier",
type=float,
default=DEFAULT_GEOMETRY_MULTIPLIER,
help="Geometry multiplier (default: 1.0)",
)
parser.add_argument("--particles", type=int, default=12, help="Swarm size (default: 12)")
parser.add_argument("--epochs", type=int, default=80, help="PSO epochs (default: 80)")
parser.add_argument(
"--subset-size",
type=int,
default=10000,
help="Subset size (default: 10000)",
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
cache_path = Path(args.cache_dir) if args.cache_dir else None
out_path = Path(args.output) if args.output else None
run_heavy_pso_cross_split(
phase=args.phase,
ratio=args.ratio,
geometry_policy=args.geometry_policy,
projection_scope=args.projection_scope,
projection_seed_mode=args.projection_seed_mode,
projection_seed=args.projection_seed,
geometry_multiplier=args.geometry_multiplier,
particles=args.particles,
epochs=args.epochs,
subset_size=args.subset_size,
device_str=args.device,
cache_dir=cache_path,
output_path=out_path,
)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+100 -62
View File
@@ -1,73 +1,111 @@
import gc
import os
import sys
import argparse
import torch
import torch.nn as nn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
from sklearn.preprocessing import StandardScaler
from pso import optimizer
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
def make_model():
model = Sequential()
model.add(layers.Dense(10, activation="relu", input_shape=(4,)))
model.add(layers.Dense(10, activation="relu"))
model.add(layers.Dense(3, activation="softmax"))
return model
def load_data():
iris = load_iris()
x = iris.data
y = iris.target
y = keras.utils.to_categorical(y, 3)
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.2, shuffle=True, stratify=y
def make_model(seed: int = 42):
torch.manual_seed(seed)
return nn.Sequential(
nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 3),
)
return x_train, x_test, y_train, y_test
def load_data(seed: int = 42):
iris = load_iris()
x = iris.data.astype("float32")
y = iris.target.astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.2, shuffle=True, stratify=y, random_state=seed
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.transform(x_test)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(y_test, dtype=torch.int64),
)
model = make_model()
x_train, x_test, y_train, y_test = load_data()
def main():
parser = argparse.ArgumentParser(description="PSO Iris Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "original",
"initialization": "model_noise",
"evaluation": "full",
"convergence": "particle_reset",
"refinement": "adam",
"n_particles": 24,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.0,
"mutation_swarm": 0.1,
"particle_min": -3.0,
"particle_max": 3.0,
"velocity_limit_ratio": 0.1,
"boundary_strategy": "reflect",
"seed": 42,
"epochs": 70,
"renewal": "loss",
"output_dir": "output/iris",
"checkpoint_interval": 25,
"refinement_epochs": 10,
"refinement_lr": 0.001,
},
)
args = parser.parse_args()
model = make_model(seed=args.seed)
x_train, x_test, y_train, y_test = load_data(seed=args.seed)
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.CrossEntropyLoss(),
task="multiclass",
inertia_profile={"c0": 0.5, "c1": 0.3, "w_min": 0.1, "w_max": 0.9},
)
pso_iris = Optimizer(**kwargs)
print(f"Optimizer device: {pso_iris.device}")
best_score = pso_iris.fit(
x_train,
y_train,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
validation_data=(x_test, y_test),
output_dir=args.output_dir,
checkpoint_interval=25,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
pso_iris = optimizer(
model=model,
loss="categorical_crossentropy",
n_particles=100,
c0=0.5,
c1=0.3,
w_min=0.1,
w_max=0.9,
negative_swarm=0,
mutation_swarm=0.1,
convergence_reset=True,
convergence_reset_patience=10,
convergence_reset_monitor="loss",
convergence_reset_min_delta=0.001,
)
best_score = pso_iris.fit(
x_train,
y_train,
epochs=500,
save_info=True,
log=2,
log_name="iris",
renewal="loss",
check_point=25,
validate_data=(x_test, y_test),
)
gc.collect()
print("Done!")
sys.exit(0)
if __name__ == "__main__":
main()
-55
View File
@@ -1,55 +0,0 @@
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import tensorflow as tf
gpus = tf.config.experimental.list_physical_devices("GPU")
if gpus:
try:
# tf.config.experimental.set_visible_devices(gpus[0], "GPU")
tf.config.experimental.set_memory_growth(gpus[0], True)
except RuntimeError as e:
print(e)
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
def make_model():
model = Sequential()
model.add(layers.Dense(10, activation="relu", input_shape=(4,)))
model.add(layers.Dense(10, activation="relu"))
model.add(layers.Dense(3, activation="softmax"))
return model
def load_data():
iris = load_iris()
x = iris.data
y = iris.target
y = keras.utils.to_categorical(y, 3)
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.2, shuffle=True, stratify=y
)
return x_train, x_test, y_train, y_test
if __name__ == "__main__":
model = make_model()
x_train, x_test, y_train, y_test = load_data()
print(x_train.shape, y_train.shape)
loss = ["categorical_crossentropy", "accuracy", "mse"]
metrics = ["accuracy"]
model.compile(optimizer="sgd", loss=loss[0], metrics=metrics[0])
model.fit(x_train, y_train, epochs=200, batch_size=32, validation_split=0.2)
model.evaluate(x_test, y_test, batch_size=32)
+127
View File
@@ -0,0 +1,127 @@
"""Iris dataset gradient baseline (PyTorch standard backprop optimizer, non-PSO)."""
import copy
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
class IrisModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 3),
)
def forward(self, x):
return self.net(x)
def load_data(seed: int = 42):
iris = load_iris()
X = iris.data.astype("float32")
y = iris.target.astype("int64")
x_train, x_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=True, stratify=y, random_state=seed
)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(y_test, dtype=torch.int64),
)
def get_device() -> torch.device:
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_built()
and torch.backends.mps.is_available()
):
return torch.device("mps")
elif torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
def main():
torch.manual_seed(42)
np.random.seed(42)
device = get_device()
print(f"Selected device: {device}")
x_train, x_test, y_train, y_test = load_data(seed=42)
train_loader = DataLoader(
TensorDataset(x_train, y_train), batch_size=32, shuffle=True
)
val_loader = DataLoader(
TensorDataset(x_test, y_test), batch_size=32, shuffle=False
)
model = IrisModel().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
best_val_loss = float("inf")
best_state = None
for epoch in range(200):
model.train()
for bx, by in train_loader:
bx, by = bx.to(device), by.to(device)
optimizer.zero_grad()
out = model(bx)
loss = criterion(out, by)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0.0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
val_loss += loss.item() * bx.size(0)
total += bx.size(0)
val_loss /= total
if val_loss < best_val_loss:
best_val_loss = val_loss
best_state = copy.deepcopy(model.state_dict())
if best_state is not None:
model.load_state_dict(best_state)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for bx, by in val_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
test_loss += loss.item() * bx.size(0)
preds = out.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
test_loss /= total
test_acc = correct / total
print(f"Final test loss: {test_loss:.4f}, accuracy: {test_acc:.4f}")
if __name__ == "__main__":
main()
+94 -71
View File
@@ -1,88 +1,111 @@
# %%
import os
import sys
import argparse
import torch
import torch.nn as nn
from pso import optimizer
import tensorflow as tf
from keras.datasets import mnist
from keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from keras.models import Sequential
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
def get_data(seed: int = 42):
from sklearn.decomposition import PCA
from torchvision.datasets import MNIST
train_dataset = MNIST(root="./data", train=True, download=True)
test_dataset = MNIST(root="./data", train=False, download=True)
def get_data():
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train_raw = (train_dataset.data[:3000].float() / 255.0).reshape(3000, -1).numpy()
y_train = train_dataset.targets[:3000].long()
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
x_test_raw = (test_dataset.data[:1000].float() / 255.0).reshape(1000, -1).numpy()
y_test = test_dataset.targets[:1000].long()
y_train, y_test = tf.one_hot(y_train, 10), tf.one_hot(y_test, 10)
pca = PCA(n_components=32, whiten=True, random_state=seed)
x_train_pca = pca.fit_transform(x_train_raw)
x_test_pca = pca.transform(x_test_raw)
x_train, x_test = tf.convert_to_tensor(x_train), tf.convert_to_tensor(x_test)
y_train, y_test = tf.convert_to_tensor(y_train), tf.convert_to_tensor(y_test)
x_train = torch.tensor(x_train_pca, dtype=torch.float32)
x_test = torch.tensor(x_test_pca, dtype=torch.float32)
print(f"x_train : {x_train[0].shape} | y_train : {y_train[0].shape}")
print(f"x_test : {x_test[0].shape} | y_test : {y_test[0].shape}")
print(f"x_train : {x_train.shape} | y_train : {y_train.shape}")
print(f"x_test : {x_test.shape} | y_test : {y_test.shape}")
return x_train, y_train, x_test, y_test
def make_model():
model = Sequential()
model.add(
Conv2D(32, kernel_size=(5, 5), activation="relu", input_shape=(28, 28, 1))
def make_model(seed: int = 42):
torch.manual_seed(seed)
return nn.Linear(32, 10)
def main():
parser = argparse.ArgumentParser(description="PSO MNIST Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "inertia",
"initialization": "model_noise",
"evaluation": "fixed_subset",
"convergence": "none",
"refinement": "adam",
"n_particles": 30,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.0,
"mutation_swarm": 0.02,
"particle_min": -3.0,
"particle_max": 3.0,
"velocity_limit_ratio": 0.025,
"boundary_strategy": "reflect",
"initial_position_noise": 0.05,
"seed": 42,
"epochs": 80,
"batch_size": 1000,
"fitness_size": 2000,
"renewal": "loss",
"output_dir": "output/mnist",
"checkpoint_interval": 25,
"refinement_epochs": 100,
"refinement_lr": 0.01,
},
)
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(64, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dropout(0.5))
model.add(Dense(256, activation="relu"))
model.add(Dense(128, activation="relu"))
model.add(Dense(10, activation="softmax"))
args = parser.parse_args()
return model
model = make_model(seed=args.seed)
x_train, y_train, x_test, y_test = get_data(seed=args.seed)
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.CrossEntropyLoss(),
task="multiclass",
inertia_profile={"c0": 1.49618, "c1": 1.49618, "w_min": 0.7298, "w_max": 0.7298},
)
pso_mnist = Optimizer(**kwargs)
print(f"Optimizer device: {pso_mnist.device}")
best_score = pso_mnist.fit(
x_train,
y_train,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
validation_data=(x_test, y_test),
output_dir=args.output_dir,
checkpoint_interval=25,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
# %%
model = make_model()
x_train, y_train, x_test, y_test = get_data()
pso_mnist = optimizer(
model,
loss="categorical_crossentropy",
n_particles=200,
c0=0.7,
c1=0.4,
w_min=0.1,
w_max=0.9,
negative_swarm=0.0,
mutation_swarm=0.05,
convergence_reset=True,
convergence_reset_patience=10,
convergence_reset_monitor="loss",
convergence_reset_min_delta=0.005,
)
best_score = pso_mnist.fit(
x_train,
y_train,
epochs=1000,
save_info=True,
log=2,
log_name="mnist",
renewal="loss",
check_point=25,
batch_size=5000,
validate_data=(x_test, y_test),
)
print("Done!")
sys.exit(0)
if __name__ == "__main__":
main()
-132
View File
@@ -1,132 +0,0 @@
from keras.models import Sequential
from keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from keras.datasets import mnist
from keras.utils import to_categorical
from sklearn.model_selection import train_test_split
# from tensorflow.data.Dataset import from_tensor_slices
import tensorflow as tf
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
gpus = tf.config.experimental.list_physical_devices("GPU")
if gpus:
try:
tf.config.experimental.set_memory_growth(gpus[0], True)
except Exception as e:
print(e)
finally:
del gpus
def get_data():
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
print(f"x_train : {x_train[0].shape} | y_train : {y_train[0].shape}")
print(f"x_test : {x_test[0].shape} | y_test : {y_test[0].shape}")
return x_train, y_train, x_test, y_test
class _batch_generator_:
def __init__(self, x, y, batch_size: int = None):
self.index = 0
self.x = x
self.y = y
self.setBatchSize(batch_size)
def next(self):
self.index += 1
if self.index >= self.max_index:
self.index = 0
self.__getBatchSlice(self.batch_size)
return self.dataset[self.index][0], self.dataset[self.index][1]
def getMaxIndex(self):
return self.max_index
def getIndex(self):
return self.index
def setIndex(self, index):
self.index = index
def getBatchSize(self):
return self.batch_size
def setBatchSize(self, batch_size: int = None):
if batch_size is None:
batch_size = len(self.x) // 10
elif batch_size > len(self.x):
batch_size = len(self.x)
self.batch_size = batch_size
print(f"batch size : {self.batch_size}")
self.dataset = self.__getBatchSlice(self.batch_size)
self.max_index = len(self.dataset)
def __getBatchSlice(self, batch_size):
return list(
tf.data.Dataset.from_tensor_slices((self.x, self.y))
.shuffle(len(self.x))
.batch(batch_size)
)
def getDataset(self):
return self.dataset
def make_model():
model = Sequential()
model.add(
Conv2D(64, kernel_size=(5, 5), activation="relu", input_shape=(28, 28, 1))
)
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(128, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dropout(0.5))
model.add(Dense(2048, activation="relu"))
model.add(Dropout(0.8))
model.add(Dense(1024, activation="relu"))
model.add(Dropout(0.8))
model.add(Dense(10, activation="softmax"))
return model
model = make_model()
x_train, y_train, x_test, y_test = get_data()
y_train = tf.one_hot(y_train, 10)
y_test = tf.one_hot(y_test, 10)
batch = 64
dataset = _batch_generator_(x_train, y_train, batch)
model.compile(
optimizer="adam",
loss="categorical_crossentropy",
metrics=["accuracy", "mse"],
)
count = 0
print(f"batch size : {batch}")
print("iter " + str(dataset.getMaxIndex()))
print("Training model...")
# while count < dataset.getMaxIndex():
# x_batch, y_batch = dataset.next()
# count += 1
# print(f"iter {count}/{dataset.getMaxIndex()}")
model.fit(x_train, y_train, epochs=1000, batch_size=batch, verbose=1)
print(count)
print("Evaluating model...")
model.evaluate(x_test, y_test, verbose=1)
weights = model.get_weights()
+136
View File
@@ -0,0 +1,136 @@
"""MNIST dataset gradient baseline (PyTorch standard backprop optimizer, non-PSO)."""
import copy
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
class MNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 64, kernel_size=5)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(2, 2)
self.drop1 = nn.Dropout(0.5)
self.conv2 = nn.Conv2d(64, 128, kernel_size=3)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(2, 2)
self.drop2 = nn.Dropout(0.5)
self.fc1 = nn.Linear(128 * 5 * 5, 2048)
self.relu3 = nn.ReLU()
self.drop3 = nn.Dropout(0.8)
self.fc2 = nn.Linear(2048, 1024)
self.relu4 = nn.ReLU()
self.drop4 = nn.Dropout(0.8)
self.fc3 = nn.Linear(1024, 10)
def forward(self, x):
x = self.drop1(self.pool1(self.relu1(self.conv1(x))))
x = self.pool2(self.relu2(self.conv2(x)))
x = torch.flatten(x, 1)
x = self.drop3(self.relu3(self.fc1(self.drop2(x))))
x = self.drop4(self.relu4(self.fc2(x)))
x = self.fc3(x)
return x
def get_data(download: bool = True):
from torchvision import datasets, transforms
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(
root="./data", train=True, transform=transform, download=download
)
test_dataset = datasets.MNIST(
root="./data", train=False, transform=transform, download=download
)
return train_dataset, test_dataset
def get_device() -> torch.device:
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_built()
and torch.backends.mps.is_available()
):
return torch.device("mps")
elif torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
def main():
torch.manual_seed(42)
np.random.seed(42)
device = get_device()
print(f"Selected device: {device}")
train_dataset, test_dataset = get_data(download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
model = MNISTModel().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
best_val_loss = float("inf")
best_state = None
for epoch in range(10):
model.train()
for bx, by in train_loader:
bx, by = bx.to(device), by.to(device)
optimizer.zero_grad()
out = model(bx)
loss = criterion(out, by)
loss.backward()
optimizer.step()
model.eval()
val_loss = 0.0
total = 0
with torch.no_grad():
for bx, by in test_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
val_loss += loss.item() * bx.size(0)
total += bx.size(0)
val_loss /= total
if val_loss < best_val_loss:
best_val_loss = val_loss
best_state = copy.deepcopy(model.state_dict())
if best_state is not None:
model.load_state_dict(best_state)
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for bx, by in test_loader:
bx, by = bx.to(device), by.to(device)
out = model(bx)
loss = criterion(out, by)
test_loss += loss.item() * bx.size(0)
preds = out.argmax(dim=1)
correct += (preds == by).sum().item()
total += bx.size(0)
test_loss /= total
test_acc = correct / total
print(f"Final test loss: {test_loss:.4f}, accuracy: {test_acc:.4f}")
if __name__ == "__main__":
main()
@@ -0,0 +1,269 @@
"""Emit live, read-only TensorBoard progress for a convergence-study run."""
from __future__ import annotations
import argparse
import csv
import json
import time
from pathlib import Path
from typing import Any
from torch.utils.tensorboard import SummaryWriter
BASE_SEEDS = (501, 502, 503)
SWARM_SEEDS = (601, 602, 603)
CIFAR_WORKLOADS = ("cifar10_resnet18", "cifar10_resnet50")
YOLO_WORKLOAD = "voc_yolo11n"
def _cifar_seed(root: Path, seed: int) -> dict[str, Any]:
baseline = root / f"baseline-{seed}.pt"
searches = sum(
(root / f"feature-{method}-{seed}-{swarm}.json").is_file()
for method in ("pso", "random")
for swarm in SWARM_SEEDS
)
controls = sum(
(root / f"{method}-{seed}.pt").is_file()
for method in ("feature-adam", "head-adam")
)
complete = baseline.is_file() and searches == 6 and controls == 2
if complete:
stage = "complete"
elif not baseline.is_file():
stage = "baseline_training"
elif searches < 6:
stage = f"pso_random_search_{searches}_of_6"
else:
stage = f"adam_controls_{controls}_of_2"
return {
"seed": seed,
"stage": stage,
"complete": complete,
"searches": searches,
"controls": controls,
}
def _read_yolo_metrics(path: Path) -> list[dict[str, float]]:
if not path.is_file():
return []
try:
with path.open(newline="", encoding="utf-8") as stream:
rows = []
for source in csv.DictReader(stream):
rows.append(
{
key.strip(): float(value)
for key, value in source.items()
if key is not None
and value is not None
and value.strip()
}
)
return rows
except (OSError, ValueError):
return []
def _yolo_seed(
root: Path,
run_root: Path,
seed: int,
) -> dict[str, Any]:
baseline = root / "baselines" / str(seed) / "ema_fp32.pt"
training_root = (
run_root / "ultralytics" / f"base-{seed}-100e"
)
metrics = _read_yolo_metrics(training_root / "results.csv")
baseline_complete = (
baseline.is_file()
and len(metrics) >= 100
and (training_root / "weights" / "last.pt").is_file()
)
arm_root = root / "arms" / str(seed)
searches = sum(
(arm_root / f"feature_{method}-{swarm}.pt").is_file()
for method in ("pso", "random")
for swarm in SWARM_SEEDS
)
controls = sum(
(arm_root / f"{method}.pt").is_file()
for method in ("feature_adam", "head_adam")
)
complete = (arm_root / "record.json").is_file()
if complete:
stage = "complete"
elif not baseline_complete:
stage = f"baseline_training_epoch_{len(metrics)}_of_100"
elif searches < 6:
stage = f"pso_random_search_{searches}_of_6"
elif controls < 2:
stage = f"adam_controls_{controls}_of_2"
else:
stage = "selection"
return {
"seed": seed,
"stage": stage,
"complete": complete,
"searches": searches,
"controls": controls,
"training_metrics": metrics,
}
def snapshot(run_root: Path) -> dict[str, Any]:
workloads = run_root / "workloads"
status: dict[str, Any] = {}
completed = 0
for workload in CIFAR_WORKLOADS:
seeds = [
_cifar_seed(workloads / workload, seed)
for seed in BASE_SEEDS
]
completed += sum(item["complete"] for item in seeds)
status[workload] = seeds
yolo = [
_yolo_seed(
workloads / YOLO_WORKLOAD,
run_root,
seed,
)
for seed in BASE_SEEDS
]
completed += sum(item["complete"] for item in yolo)
status[YOLO_WORKLOAD] = yolo
state_path = run_root / "state.json"
state = "missing"
if state_path.is_file():
try:
state = str(
json.loads(
state_path.read_text(encoding="utf-8")
).get("state", "unknown")
)
except (OSError, ValueError):
state = "unreadable"
active = next(
(
f"{workload}/seed-{item['seed']}/{item['stage']}"
for workload, items in status.items()
for item in items
if not item["complete"]
),
"",
)
if not active:
for workload in CIFAR_WORKLOADS:
selected = sum(
(
workloads
/ workload
/ f"selected-feature_{method}-{seed}.pt"
).is_file()
for method in ("pso", "random")
for seed in BASE_SEEDS
)
if selected < 6:
active = (
f"{workload}/development_selection_"
f"{selected}_of_6"
)
break
if not active:
active = "development_complete"
return {
"state": state,
"active": active,
"completed_base_seeds": int(completed),
"total_base_seeds": 9,
"workloads": status,
"observed_at": time.time(),
}
def emit(
writer: SummaryWriter,
value: dict[str, Any],
step: int,
) -> None:
writer.add_scalar(
"progress/completed_base_seeds",
value["completed_base_seeds"],
step,
)
writer.add_scalar(
"progress/completion_fraction",
value["completed_base_seeds"] / value["total_base_seeds"],
step,
)
for workload, seeds in value["workloads"].items():
writer.add_scalar(
f"progress/{workload}/completed_base_seeds",
sum(item["complete"] for item in seeds),
step,
)
for item in value["workloads"][YOLO_WORKLOAD]:
seed = item["seed"]
for row in item["training_metrics"]:
epoch = int(row["epoch"])
for metric, metric_value in row.items():
if metric in {"epoch", "time"}:
continue
writer.add_scalar(
f"training/{YOLO_WORKLOAD}/seed_{seed}/{metric}",
metric_value,
epoch,
)
writer.add_text(
"progress/current",
f"`{value['active']}`",
step,
)
writer.add_text(
"progress/snapshot",
f"```json\n{json.dumps(value, indent=2)}\n```",
step,
)
writer.flush()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--run-root", required=True, type=Path)
parser.add_argument("--interval", type=float, default=10.0)
parser.add_argument("--once", action="store_true")
args = parser.parse_args()
if args.interval <= 0:
raise SystemExit("--interval must be positive")
log_dir = args.run_root / "tensorboard"
writer = SummaryWriter(log_dir=str(log_dir))
print(f"monitoring {args.run_root} -> {log_dir}", flush=True)
step = 0
try:
while True:
value = snapshot(args.run_root)
emit(writer, value, step)
print(
json.dumps(
{
"active": value["active"],
"completed_base_seeds": value[
"completed_base_seeds"
],
"state": value["state"],
},
sort_keys=True,
),
flush=True,
)
step += 1
if args.once:
break
time.sleep(args.interval)
finally:
writer.close()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+1751
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+655
View File
@@ -0,0 +1,655 @@
"""
Heavy PSO Cross-Split Experiment Results Publisher.
Protocol Version: HEAVY-PSO-CROSS-SPLIT-PUBLISH 1.0.0
Reads raw candidate and evaluation artifacts from a completed cross-split mission run,
validates integrity, accounting, test seals, and candidate/evaluation alignment across
all development variants, and exports deterministic public benchmark artifacts:
1. benchmark_results/pso_v7_heavy_cross_split.json
2. benchmark_results/pso_v7_heavy_cross_split.csv
3. history_plt/pso_v7_heavy_cross_split.png
"""
from __future__ import annotations
import argparse
import csv
import json
import math
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
# Ensure test directory and repo root are in sys.path
REPO_ROOT = Path(__file__).resolve().parent.parent if Path(__file__).resolve().parent.name != "PSO" else Path(__file__).resolve().parent
TEST_DIR = REPO_ROOT / "test"
if str(TEST_DIR) not in sys.path:
sys.path.insert(0, str(TEST_DIR))
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from benchmark_suite import save_json_atomic
from pso import __version__ as pso_version
PUBLISH_PROTOCOL_VERSION = "HEAVY-PSO-CROSS-SPLIT-PUBLISH 1.0.0"
DEFAULT_SOURCE_DIR = Path(".omc/autoresearch/heavy-pso-cross-split-robustness/runs/20260903T162504Z")
DEFAULT_JSON_OUTPUT = Path("benchmark_results/pso_v7_heavy_cross_split.json")
DEFAULT_CSV_OUTPUT = Path("benchmark_results/pso_v7_heavy_cross_split.csv")
DEFAULT_PLOT_OUTPUT = Path("history_plt/pso_v7_heavy_cross_split.png")
WORKLOADS = ["mnist_compact", "mnist_wide", "fashion_compact", "fashion_wide"]
BASELINE_METHODS = {
"mnist_compact": "G8",
"mnist_wide": "G5",
"fashion_compact": "G8",
"fashion_wide": "G5",
}
EXPECTED_DEV_SPLITS = [20260905, 20260906]
EXPECTED_DEV_SWARM_SEEDS = [101, 102, 103]
EXPECTED_VARIANTS_COUNT = 9
EXPECTED_VARIANT_IDS = (
"iteration-0001-development",
"iteration-0002-development",
"iteration-0003-replica1-development",
"iteration-0003-replica2-development",
"iteration-0004-development",
"iteration-0005-development",
"iteration-0006-development",
"iteration-0007-development",
"iteration-0008-development",
)
EXPECTED_CELLS_PER_VARIANT = 8
EXPECTED_RUNS_PER_VARIANT = 48
EXPECTED_QUERIES_PER_VARIANT = 46080
EXPECTED_SAMPLES_PER_VARIANT = 460800000
EXPECTED_TOTAL_RUNS = EXPECTED_VARIANTS_COUNT * EXPECTED_RUNS_PER_VARIANT # 432
EXPECTED_TOTAL_QUERIES = EXPECTED_VARIANTS_COUNT * EXPECTED_QUERIES_PER_VARIANT # 414720
EXPECTED_TOTAL_SAMPLES = EXPECTED_VARIANTS_COUNT * EXPECTED_SAMPLES_PER_VARIANT # 4147200000
def discover_and_load_variants(
source_dir: Path,
) -> List[Tuple[Path, Path, Dict[str, Any], Dict[str, Any]]]:
"""
Discovers candidate and evaluation JSON file pairs in source_dir.
Supports both subdirectories (candidates/ & evaluations/) and direct directory structure.
"""
if not source_dir.exists():
raise FileNotFoundError(f"Source directory does not exist: {source_dir}")
cand_dir = source_dir / "candidates"
eval_dir = source_dir / "evaluations"
if cand_dir.is_dir() and eval_dir.is_dir():
candidate_files = sorted(cand_dir.glob("*.json"))
else:
candidate_files = sorted(source_dir.glob("*candidate*.json"))
if not candidate_files:
candidate_files = sorted(source_dir.glob("*.json"))
if not candidate_files:
raise ValueError(f"No candidate JSON files found in {source_dir}")
observed_ids = tuple(path.stem for path in candidate_files)
if observed_ids != EXPECTED_VARIANT_IDS:
raise ValueError(
f"Expected exact development variants {list(EXPECTED_VARIANT_IDS)}, "
f"got {list(observed_ids)}"
)
pairs = []
for cf in candidate_files:
if cand_dir.is_dir() and eval_dir.is_dir():
ef = eval_dir / cf.name
else:
ef_name = cf.name.replace("candidate", "evaluation")
ef = source_dir / ef_name
if not ef.exists():
ef = cf
if not ef.exists():
raise FileNotFoundError(f"Missing corresponding evaluation file for candidate {cf.name}: {ef}")
with cf.open("r", encoding="utf-8") as f:
cdata = json.load(f)
with ef.open("r", encoding="utf-8") as f:
edata = json.load(f)
pairs.append((cf, ef, cdata, edata))
return pairs
def validate_variant_pair(
cf_path: Path,
ef_path: Path,
cdata: Dict[str, Any],
edata: Dict[str, Any],
) -> Dict[str, Any]:
"""
Validates each source candidate/evaluation artifact pair for expected IDs,
phase, test seals, no development pass, cell count 8, and resource accounting.
"""
variant_id = cf_path.stem
# Phase check
c_phase = cdata.get("phase")
e_phase = edata.get("phase")
if c_phase != "development":
raise ValueError(f"[{variant_id}] Candidate phase must be 'development', got: {c_phase}")
if e_phase not in (None, "development"):
raise ValueError(f"[{variant_id}] Evaluation phase must be 'development', got: {e_phase}")
# Official test sealed check
if cdata.get("official_test_data_loaded") is not False:
raise ValueError(f"[{variant_id}] candidate.official_test_data_loaded must be False")
if cdata.get("official_test_evaluations") != 0:
raise ValueError(f"[{variant_id}] candidate.official_test_evaluations must be 0")
gates = edata.get("gates", {})
if gates and "official_test_sealed" in gates:
if not gates["official_test_sealed"].get("pass", False):
raise ValueError(f"[{variant_id}] evaluation gate 'official_test_sealed' must pass")
# No development pass check
if edata.get("development_pass") is not False:
raise ValueError(f"[{variant_id}] development_pass must be False for all variants")
if edata.get("pass") is not False:
raise ValueError(f"[{variant_id}] pass must be False for all variants")
if edata.get("eligible_for_confirmation") is not False:
raise ValueError(f"[{variant_id}] eligible_for_confirmation must be False for all variants")
# Matching cell count 8
summary_metrics = edata.get("summary_metrics", {})
dev_cells = summary_metrics.get("development_cells")
cell_metrics = edata.get("cell_metrics", [])
if dev_cells != EXPECTED_CELLS_PER_VARIANT or len(cell_metrics) != EXPECTED_CELLS_PER_VARIANT:
raise ValueError(
f"[{variant_id}] Expected {EXPECTED_CELLS_PER_VARIANT} development cells, got "
f"summary_metrics.development_cells={dev_cells}, len(cell_metrics)={len(cell_metrics)}"
)
# Resource accounting check across per-seed runs
splits = cdata.get("splits", {})
c_runs = 0
c_queries = 0
c_samples = 0
c_test_evals = 0
for split_key, split_data in splits.items():
for role in ("baselines", "candidates"):
for wl_key, wl_data in split_data.get(role, {}).items():
for run in wl_data.get("per_seed_runs", []):
c_runs += 1
c_queries += run.get("total_queries", 0)
c_samples += run.get("total_sample_evaluations", 0)
c_test_evals += run.get("official_test_evaluations", 0)
if c_runs != EXPECTED_RUNS_PER_VARIANT:
raise ValueError(f"[{variant_id}] Expected {EXPECTED_RUNS_PER_VARIANT} runs, got {c_runs}")
if c_queries != EXPECTED_QUERIES_PER_VARIANT:
raise ValueError(f"[{variant_id}] Expected {EXPECTED_QUERIES_PER_VARIANT} total queries, got {c_queries}")
if c_samples != EXPECTED_SAMPLES_PER_VARIANT:
raise ValueError(f"[{variant_id}] Expected {EXPECTED_SAMPLES_PER_VARIANT} total sample evaluations, got {c_samples}")
if c_test_evals != 0:
raise ValueError(f"[{variant_id}] Official test evaluations must be 0, got {c_test_evals}")
resource_totals = cdata.get("resource_totals")
if not isinstance(resource_totals, dict):
raise ValueError(f"[{variant_id}] candidate.resource_totals must be present")
expected_resource_totals = {
"total_runs": c_runs,
"total_queries": c_queries,
"total_samples_evaluated": c_samples,
"official_test_evaluations": c_test_evals,
}
for field, expected in expected_resource_totals.items():
if resource_totals.get(field) != expected:
raise ValueError(
f"[{variant_id}] candidate.resource_totals.{field} must be {expected}, "
f"got {resource_totals.get(field)!r}"
)
wall_time = resource_totals.get("wall_time_sec")
if (
not isinstance(wall_time, (int, float))
or isinstance(wall_time, bool)
or not math.isfinite(float(wall_time))
or wall_time < 0
):
raise ValueError(
f"[{variant_id}] candidate.resource_totals.wall_time_sec must be finite and non-negative"
)
return {
"variant_id": variant_id,
"candidate_file": cf_path.name,
"evaluation_file": ef_path.name,
"candidate_path": _repository_relative_path(cf_path),
"evaluation_path": _repository_relative_path(ef_path),
"phase": "development",
"candidate_config": cdata.get("candidate_config", {}),
"score": float(edata.get("score", 0.0)),
"pass": False,
"development_pass": False,
"eligible_for_confirmation": False,
"failed_hard_gate_count": edata.get("failed_hard_gate_count", 0),
"failed_gates": edata.get("failed_gates", []),
"summary_metrics": summary_metrics,
"state_ratios": edata.get("state_ratios", {}),
"cell_metrics": cell_metrics,
"cdata": cdata,
"edata": edata,
"resources": {
"runs": c_runs,
"queries": c_queries,
"sample_evaluations": c_samples,
"wall_time_sec": float(wall_time),
"official_test_evaluations": 0,
},
}
def validate_all_variants(
variant_summaries: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""
Validates cumulative resources across all variants, identifies the best-observed variant,
and constructs cumulative summary dictionary.
"""
observed_ids = tuple(v["variant_id"] for v in variant_summaries)
if observed_ids != EXPECTED_VARIANT_IDS:
raise ValueError(
f"Expected exact development variants {list(EXPECTED_VARIANT_IDS)}, got {list(observed_ids)}"
)
total_runs = sum(v["resources"]["runs"] for v in variant_summaries)
total_queries = sum(v["resources"]["queries"] for v in variant_summaries)
total_samples = sum(v["resources"]["sample_evaluations"] for v in variant_summaries)
official_test_evals = sum(v["resources"]["official_test_evaluations"] for v in variant_summaries)
wall_time_sec = round(
math.fsum(v["resources"]["wall_time_sec"] for v in variant_summaries),
4,
)
if total_runs != EXPECTED_TOTAL_RUNS:
raise ValueError(f"Cumulative total runs must be {EXPECTED_TOTAL_RUNS}, got {total_runs}")
if total_queries != EXPECTED_TOTAL_QUERIES:
raise ValueError(f"Cumulative total queries must be {EXPECTED_TOTAL_QUERIES}, got {total_queries}")
if total_samples != EXPECTED_TOTAL_SAMPLES:
raise ValueError(f"Cumulative total sample evaluations must be {EXPECTED_TOTAL_SAMPLES}, got {total_samples}")
if official_test_evals != 0:
raise ValueError(f"Cumulative official test evaluations must be 0, got {official_test_evals}")
# Mark best-observed-but-rejected variant (highest evaluation score)
best_variant = max(variant_summaries, key=lambda v: v["score"])
for v in variant_summaries:
v["is_best_observed"] = (v["variant_id"] == best_variant["variant_id"])
return {
"n_variants": len(variant_summaries),
"total_runs": total_runs,
"total_queries": total_queries,
"total_sample_evaluations": total_samples,
"official_test_evaluations": 0,
"best_observed_variant_id": best_variant["variant_id"],
"wall_time_sec": wall_time_sec,
"best_observed_score": best_variant["score"],
}
def build_publish_json(
source_dir: Path,
variant_summaries: List[Dict[str, Any]],
cum_resources: Dict[str, Any],
) -> Dict[str, Any]:
"""
Constructs the compact JSON dictionary matching all publication criteria.
"""
now_iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
clean_variants = []
for v in sorted(variant_summaries, key=lambda item: item["variant_id"]):
clean_variants.append({
"variant_id": v["variant_id"],
"candidate_file": v["candidate_file"],
"evaluation_file": v["evaluation_file"],
"phase": v["phase"],
"candidate_config": v["candidate_config"],
"score": v["score"],
"pass": v["pass"],
"development_pass": v["development_pass"],
"eligible_for_confirmation": v["eligible_for_confirmation"],
"failed_hard_gate_count": v["failed_hard_gate_count"],
"failed_gates": v["failed_gates"],
"is_best_observed": v["is_best_observed"],
"summary_metrics": v["summary_metrics"],
"state_ratios": v["state_ratios"],
"cell_metrics": v["cell_metrics"],
"resources": v["resources"],
})
payload = {
"protocol_version": PUBLISH_PROTOCOL_VERSION,
"pso_version": pso_version,
"timestamp": now_iso,
"mission_contract": {
"phase": "development",
"official_test_data_loaded": False,
"official_test_evaluations": 0,
"confirmation_executed": False,
"retained_policy": None,
},
"official_test_data_loaded": False,
"official_test_evaluations": 0,
"confirmation_executed": False,
"retained_policy": None,
"verdict": {
"status": "NO_RETAINED_POLICY_NO_CONFIRMATION",
"retained_policy": None,
"confirmation_executed": False,
"official_test_data_loaded": False,
"official_test_evaluations": 0,
"best_observed_variant_id": cum_resources["best_observed_variant_id"],
"best_observed_score": cum_resources["best_observed_score"],
"description": (
"All 9 development candidates failed the frozen evaluator gates (specifically maximum accuracy "
"regression and/or development wide CNN improvement). No candidate qualified for confirmation. "
"Confirmation split 20260907 and official test data remained completely unexecuted and sealed."
),
},
"source_provenance": {
"source_dir": _repository_relative_path(source_dir),
"n_variants": cum_resources["n_variants"],
"candidate_files": [v["candidate_file"] for v in clean_variants],
"evaluation_files": [v["evaluation_file"] for v in clean_variants],
},
"cumulative_resources": cum_resources,
"total_runs": cum_resources["total_runs"],
"total_wall_time_sec": cum_resources["wall_time_sec"],
"total_queries": cum_resources["total_queries"],
"total_sample_evaluations": cum_resources["total_sample_evaluations"],
"variants": clean_variants,
}
return payload
def build_publish_csv(
variant_summaries: List[Dict[str, Any]],
csv_path: Path,
) -> None:
"""
Writes CSV summary with header + exactly 72 data rows (9 variants x 2 splits x 4 workloads).
Uses deterministic ordering and atomic writing.
"""
csv_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"variant_id",
"phase",
"split_seed",
"workload_id",
"baseline_method",
"baseline_acc",
"candidate_acc",
"acc_gain_pp",
"baseline_nll",
"candidate_nll",
"nll_reduction_fraction",
"state_ratio",
"score",
"pass",
"is_best_observed",
"candidate_path",
"evaluation_path",
]
rows = []
# Sort variants deterministically
sorted_variants = sorted(variant_summaries, key=lambda v: v["variant_id"])
for v in sorted_variants:
dev_ratios = v.get("state_ratios", {}).get("development", {})
cell_metrics = v.get("cell_metrics", [])
# Sort cells deterministically by split_seed then workload_id order
def cell_sort_key(cm):
wl_idx = WORKLOADS.index(cm["workload_id"]) if cm["workload_id"] in WORKLOADS else 99
return (cm["split_seed"], wl_idx)
sorted_cells = sorted(cell_metrics, key=cell_sort_key)
for cm in sorted_cells:
workload_id = cm["workload_id"]
baseline_method = BASELINE_METHODS.get(workload_id, "G8" if "compact" in workload_id else "G5")
st_ratio = dev_ratios.get(workload_id, 0.5) if isinstance(dev_ratios, dict) else 0.5
row = {
"variant_id": v["variant_id"],
"phase": cm["phase"],
"split_seed": cm["split_seed"],
"workload_id": workload_id,
"baseline_method": baseline_method,
"baseline_acc": cm["baseline_acc"],
"candidate_acc": cm["candidate_acc"],
"acc_gain_pp": cm["acc_gain_pp"],
"baseline_nll": cm["baseline_nll"],
"candidate_nll": cm["candidate_nll"],
"nll_reduction_fraction": cm["nll_reduction_fraction"],
"state_ratio": st_ratio,
"score": v["score"],
"pass": False,
"is_best_observed": v["is_best_observed"],
"candidate_path": v["candidate_path"],
"evaluation_path": v["evaluation_path"],
}
rows.append(row)
tmp_csv = csv_path.with_suffix(".csv.tmp")
with tmp_csv.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
tmp_csv.replace(csv_path)
def _repository_relative_path(path: Path) -> str:
try:
return str(path.resolve().relative_to(REPO_ROOT.resolve()))
except ValueError:
return str(path)
def render_publish_plot(
variant_summaries: List[Dict[str, Any]],
plot_path: Path,
) -> None:
"""
Renders readable 2-panel figure comparing mean gains and worst-cell regressions with frozen thresholds.
Clearly marks all variants failed and confirmation withheld.
"""
plot_path.parent.mkdir(parents=True, exist_ok=True)
sorted_variants = sorted(variant_summaries, key=lambda v: v["variant_id"])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6.5))
variant_labels = [
v["variant_id"].replace("-development", "").replace("iteration-", "iter-")
for v in sorted_variants
]
x = np.arange(len(variant_labels))
width = 0.35
# Panel 1: Development Grand Mean Performance Gains
acc_gains = [v["summary_metrics"].get("development_grand_mean_accuracy_gain_pp", 0.0) for v in sorted_variants]
nll_reductions = [v["summary_metrics"].get("development_grand_mean_nll_reduction_fraction", 0.0) * 100.0 for v in sorted_variants]
ax1.bar(x - width/2, acc_gains, width, label="Grand Mean Acc Gain (pp)", color="#1f77b4")
ax1.bar(x + width/2, nll_reductions, width, label="Grand Mean NLL Red. (%)", color="#2ca02c")
ax1.axhline(0.0, color="black", linestyle="--", linewidth=1.0, alpha=0.7)
ax1.set_xticks(x)
ax1.set_xticklabels(variant_labels, rotation=35, ha="right", fontsize=9)
ax1.set_ylabel("Percentage Points (pp) / Percentage (%)")
ax1.set_title("Panel A: Development Grand Mean Performance Gains")
ax1.legend(loc="upper left")
ax1.grid(True, linestyle="--", alpha=0.4)
# Panel 2: Worst-Cell Regressions & Wide CNN Improvement vs Gate Thresholds
worst_acc_regs = []
worst_nll_regs = []
mnist_wide_accs = []
for v in sorted_variants:
cell_acc_gains = [cm["acc_gain_pp"] for cm in v["cell_metrics"]]
cell_nll_reds = [cm["nll_reduction_fraction"] * 100.0 for cm in v["cell_metrics"]]
worst_acc_regs.append(min(cell_acc_gains))
worst_nll_regs.append(min(cell_nll_reds))
mnist_wide_accs.append(v["summary_metrics"].get("development_mnist_wide_accuracy_gain_pp", 0.0))
ax2.plot(x, worst_acc_regs, "o-", color="#d62728", linewidth=2, label="Worst-Cell Acc Delta (pp)")
ax2.plot(x, worst_nll_regs, "s--", color="#ff7f0e", linewidth=2, label="Worst-Cell NLL Red. (%)")
ax2.plot(x, mnist_wide_accs, "^-.", color="#9467bd", linewidth=2, label="MNIST Wide Acc Gain (pp)")
# Gate threshold lines
ax2.axhline(-1.0, color="#d62728", linestyle=":", linewidth=1.5, label="Gate: Max Acc Reg. (-1.0 pp)")
ax2.axhline(-5.0, color="#ff7f0e", linestyle=":", linewidth=1.5, label="Gate: Max NLL Reg. (-5.0%)")
ax2.axhline(2.0, color="#2ca02c", linestyle="--", linewidth=1.5, label="Gate: MNIST Wide Gain (>= +2 pp)")
panel_two_values = worst_acc_regs + worst_nll_regs + mnist_wide_accs + [-5.0, 2.0]
panel_two_span = max(panel_two_values) - min(panel_two_values)
panel_two_margin = max(1.0, panel_two_span * 0.08)
ax2.set_ylim(
min(panel_two_values) - panel_two_margin,
max(panel_two_values) + panel_two_margin,
)
ax2.set_xticks(x)
ax2.set_xticklabels(variant_labels, rotation=35, ha="right", fontsize=9)
ax2.set_ylabel("Metrics vs Gate Thresholds")
ax2.set_title("Panel B: Worst-Cell Regressions & Wide CNN Improvement vs Gates")
ax2.legend(loc="lower left", fontsize=8)
ax2.grid(True, linestyle="--", alpha=0.4)
# Highlight best observed candidate
best_idx = next(i for i, v in enumerate(sorted_variants) if v["is_best_observed"])
ax1.annotate(
f"Best observed, still rejected\nScore: {sorted_variants[best_idx]['score']:.2f}",
xy=(best_idx, acc_gains[best_idx]),
xycoords="data",
xytext=(0.62, 0.94),
textcoords="axes fraction",
arrowprops=dict(facecolor="black", shrink=0.05, width=1, headwidth=5),
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", edgecolor="black", alpha=0.95),
fontsize=8,
ha="center",
va="top",
weight="bold",
)
# Mission Outcome Text Banner
fig.suptitle(
"HEAVY PSO CROSS-SPLIT ROBUSTNESS MISSION REPORT\n"
"STATUS: ALL 9 VARIANTS FAILED FROZEN EVALUATOR GATES | CONFIRMATION WITHHELD & SEALED OFFICIAL TEST UNEXECUTED | RETAINED POLICY: NONE",
fontsize=11,
weight="bold",
color="#8b0000",
y=0.99,
)
plt.tight_layout(rect=[0, 0, 1, 0.93])
tmp_plot = plot_path.with_name(plot_path.stem + "_tmp.png")
fig.savefig(tmp_plot, dpi=200, bbox_inches="tight")
plt.close(fig)
tmp_plot.replace(plot_path)
def publish_heavy_cross_split(
source_dir: Union[str, Path] = DEFAULT_SOURCE_DIR,
output_json: Union[str, Path] = DEFAULT_JSON_OUTPUT,
output_csv: Union[str, Path] = DEFAULT_CSV_OUTPUT,
output_plot: Union[str, Path] = DEFAULT_PLOT_OUTPUT,
) -> Dict[str, Any]:
"""
Main programmatic interface for Heavy PSO Cross-Split results publication.
Parses artifacts, validates all contracts and resource totals, and writes
the compact JSON, CSV summary, and PNG plot atomically.
"""
source_dir = Path(source_dir).resolve()
output_json = Path(output_json).resolve()
output_csv = Path(output_csv).resolve()
output_plot = Path(output_plot).resolve()
pairs = discover_and_load_variants(source_dir)
variant_summaries = []
for cf, ef, cdata, edata in pairs:
summary = validate_variant_pair(cf, ef, cdata, edata)
variant_summaries.append(summary)
cum_resources = validate_all_variants(variant_summaries)
json_payload = build_publish_json(source_dir, variant_summaries, cum_resources)
save_json_atomic(json_payload, output_json)
build_publish_csv(variant_summaries, output_csv)
render_publish_plot(variant_summaries, output_plot)
return json_payload
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Publish Heavy PSO Cross-Split Robustness Mission Results"
)
parser.add_argument(
"--source-dir",
type=Path,
default=DEFAULT_SOURCE_DIR,
help=f"Raw experiment runs directory (default: {DEFAULT_SOURCE_DIR})",
)
parser.add_argument(
"--output-json",
type=Path,
default=DEFAULT_JSON_OUTPUT,
help=f"Output compact JSON path (default: {DEFAULT_JSON_OUTPUT})",
)
parser.add_argument(
"--output-csv",
type=Path,
default=DEFAULT_CSV_OUTPUT,
help=f"Output CSV summary path (default: {DEFAULT_CSV_OUTPUT})",
)
parser.add_argument(
"--output-plot",
type=Path,
default=DEFAULT_PLOT_OUTPUT,
help=f"Output PNG plot path (default: {DEFAULT_PLOT_OUTPUT})",
)
return parser
def main():
parser = build_parser()
args = parser.parse_args()
publish_heavy_cross_split(
source_dir=args.source_dir,
output_json=args.output_json,
output_csv=args.output_csv,
output_plot=args.output_plot,
)
print(f"[{PUBLISH_PROTOCOL_VERSION}] Successfully published cross-split results!")
print(f" JSON: {args.output_json}")
print(f" CSV: {args.output_csv}")
print(f" Plot: {args.output_plot}")
if __name__ == "__main__":
main()
+464
View File
@@ -0,0 +1,464 @@
"""
Adaptive Moment 120-Particle x 80-Epoch MNIST Scaling Replication Check
Validates the published 120-particle x 80-epoch fixed-epoch Adaptive Moment MNIST scaling result.
Performs exact replay on seeds 71-75 and fresh independent cohort evaluation on seeds 81-85.
Predeclared Acceptance Criteria:
1. Exact Replay (seeds 71-75): Max per-seed absolute test accuracy delta <= 0.005 (0.5%p).
2. Independent Cohort (seeds 81-85): Mean test accuracy absolute difference <= 0.03 (3%p)
AND 95% t-confidence intervals overlap between baseline and independent cohorts.
"""
import argparse
import csv
import datetime
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
import torch
from sklearn.decomposition import PCA
# Path setup for imports from test/ directory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from benchmark_suite import (
calc_stats,
compute_data_fingerprint,
get_hardware_provenance,
resolve_execution_device,
save_json_atomic,
)
from pso import __version__ as pso_version
from tuning_suite import (
TUNING_PROTOCOL_VERSION,
CandidateConfig,
get_mnist_raw_data,
get_search_candidates,
run_single_experiment,
)
REPLAY_TOLERANCE = 0.005
INDEPENDENT_MEAN_MARGIN = 0.03
REPLICATION_PROTOCOL_VERSION = "1.0.0"
REPLAY_SEEDS = [71, 72, 73, 74, 75]
INDEPENDENT_SEEDS = [81, 82, 83, 84, 85]
def validate_and_load_baseline(
baseline_path: Path,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]], CandidateConfig, str]:
if not baseline_path.exists():
raise FileNotFoundError(f"Baseline JSON file not found at: {baseline_path}")
with open(baseline_path, "r", encoding="utf-8") as f:
baseline_data = json.load(f)
if baseline_data.get("tuning_protocol_version") != TUNING_PROTOCOL_VERSION:
raise ValueError(
"Baseline tuning protocol mismatch: "
f"expected {TUNING_PROTOCOL_VERSION}, "
f"got {baseline_data.get('tuning_protocol_version')}"
)
if baseline_data.get("quick") is not False:
raise ValueError("Replication requires the full, non-quick tuning baseline.")
winners = baseline_data.get("winners", {})
if "adaptive_moment" not in winners:
raise ValueError(f"Baseline JSON {baseline_path} missing 'adaptive_moment' winner entry.")
am_winner_info = winners["adaptive_moment"]
winner_label = am_winner_info.get("candidate_label")
all_candidates = get_search_candidates()
am_candidates = all_candidates.get("adaptive_moment", [])
winner_cfg = None
for cfg in am_candidates:
if cfg.candidate_label == winner_label:
winner_cfg = cfg
break
if winner_cfg is None:
raise ValueError(
f"Could not find CandidateConfig matching label '{winner_label}' in search candidates."
)
expected_optimizer_config = winner_cfg.to_optimizer_kwargs(quick=False)
if am_winner_info.get("config") != expected_optimizer_config:
raise ValueError(
"Adaptive Moment winner configuration in the baseline no longer matches "
f"CandidateConfig '{winner_label}'."
)
scaling_runs = baseline_data.get("scaling_runs", [])
baseline_records = []
for r in scaling_runs:
if (
r.get("completed")
and r.get("method") == "adaptive_moment"
and r.get("candidate_label") == winner_label
and r.get("n_particles") == 120
and r.get("epochs") == 80
and r.get("regimen") == "fixed_epoch"
and r.get("seed") in REPLAY_SEEDS
):
baseline_records.append(r)
baseline_records.sort(key=lambda x: x["seed"])
if len(baseline_records) != 5:
raise ValueError(
f"Expected exactly 5 baseline records for seeds {REPLAY_SEEDS}, "
f"found {len(baseline_records)} in {baseline_path}."
)
expected_seeds = sorted(REPLAY_SEEDS)
actual_seeds = [r["seed"] for r in baseline_records]
if actual_seeds != expected_seeds:
raise ValueError(f"Baseline seeds mismatch: expected {expected_seeds}, got {actual_seeds}")
expected_fp = baseline_data.get("split_fingerprints", {}).get("full")
if not isinstance(expected_fp, str) or not expected_fp:
raise ValueError("Baseline JSON is missing split_fingerprints.full.")
for r in baseline_records:
if r.get("data_fingerprint") != expected_fp:
raise ValueError(
f"Baseline run seed {r['seed']} data_fingerprint {r.get('data_fingerprint')} "
f"does not match split_fingerprints.full {expected_fp}"
)
run_config = r.get("config", {})
for key, value in expected_optimizer_config.items():
if run_config.get(key) != value:
raise ValueError(
f"Baseline run seed {r['seed']} config[{key!r}]={run_config.get(key)!r} "
f"does not match selected winner value {value!r}."
)
expected_run_config = {
"n_particles": 120,
"epochs": 80,
"batch_size": 1000,
"renewal": "loss",
}
for key, value in expected_run_config.items():
if run_config.get(key) != value:
raise ValueError(
f"Baseline run seed {r['seed']} config[{key!r}]={run_config.get(key)!r}; "
f"expected {value!r}."
)
return baseline_data, baseline_records, winner_cfg, expected_fp
def prepare_full_pca_data() -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, str]:
x_train_raw, x_test_raw, y_train_3000, y_test_1000 = get_mnist_raw_data()
pca_full = PCA(n_components=32, whiten=True, random_state=42)
x_full_tr = torch.tensor(pca_full.fit_transform(x_train_raw), dtype=torch.float32)
x_full_test = torch.tensor(pca_full.transform(x_test_raw), dtype=torch.float32)
data_fp = compute_data_fingerprint(x_full_tr, x_full_test, y_train_3000, y_test_1000)
return x_full_tr, y_train_3000, x_full_test, y_test_1000, data_fp
def run_replication_cohort(
cfg: CandidateConfig,
seeds: List[int],
x_train: torch.Tensor,
y_train: torch.Tensor,
x_eval: torch.Tensor,
y_eval: torch.Tensor,
device: torch.device,
data_fp: str,
run_type: str,
) -> List[Dict[str, Any]]:
runs = []
for seed in seeds:
res = run_single_experiment(
cfg=cfg,
seed=seed,
x_train=x_train,
y_train=y_train,
x_eval=x_eval,
y_eval=y_eval,
n_particles=120,
epochs=80,
batch_size=1000,
device=device,
quick=False,
eval_metric_name="test",
data_fp=data_fp,
run_type=run_type,
extra_meta={"regimen": "fixed_epoch"},
)
runs.append(res)
return runs
def evaluate_replication(
baseline_records: List[Dict[str, Any]],
replay_runs: List[Dict[str, Any]],
independent_runs: List[Dict[str, Any]],
) -> Tuple[Dict[str, Any], Dict[str, float], Dict[str, float], Dict[str, float]]:
base_acc_by_seed = {r["seed"]: float(r["test_acc"]) for r in baseline_records}
replay_acc_by_seed = {r["seed"]: float(r["test_acc"]) for r in replay_runs}
expected_seeds = set(REPLAY_SEEDS)
if set(base_acc_by_seed) != expected_seeds or set(replay_acc_by_seed) != expected_seeds:
raise ValueError("Baseline and replay cohorts must each contain exactly seeds 71-75.")
if len(independent_runs) != len(INDEPENDENT_SEEDS) or {
r["seed"] for r in independent_runs
} != set(INDEPENDENT_SEEDS):
raise ValueError("Independent cohort must contain exactly seeds 81-85.")
baseline_model_fp = {r["seed"]: r.get("model_fingerprint") for r in baseline_records}
replay_model_fp = {r["seed"]: r.get("model_fingerprint") for r in replay_runs}
replay_model_fingerprint_match = baseline_model_fp == replay_model_fp
replay_deltas = {}
max_replay_delta = 0.0
for seed in sorted(base_acc_by_seed.keys()):
b_acc = base_acc_by_seed[seed]
r_acc = replay_acc_by_seed[seed]
delta = abs(r_acc - b_acc)
replay_deltas[str(seed)] = round(delta, 6)
if delta > max_replay_delta:
max_replay_delta = delta
replay_pass = bool(max_replay_delta <= REPLAY_TOLERANCE)
baseline_accs = [base_acc_by_seed[s] for s in sorted(base_acc_by_seed.keys())]
replay_accs = [replay_acc_by_seed[s] for s in sorted(replay_acc_by_seed.keys())]
indep_accs = [float(r["test_acc"]) for r in independent_runs]
baseline_stats = calc_stats(baseline_accs)
replay_stats = calc_stats(replay_accs)
independent_stats = calc_stats(indep_accs)
indep_mean_diff = abs(independent_stats["mean"] - baseline_stats["mean"])
independent_mean_pass = bool(indep_mean_diff <= INDEPENDENT_MEAN_MARGIN)
baseline_ci_low = round(baseline_stats["mean"] - baseline_stats["ci95_t"], 6)
baseline_ci_high = round(baseline_stats["mean"] + baseline_stats["ci95_t"], 6)
indep_ci_low = round(independent_stats["mean"] - independent_stats["ci95_t"], 6)
indep_ci_high = round(independent_stats["mean"] + independent_stats["ci95_t"], 6)
ci_overlap_pass = bool(max(baseline_ci_low, indep_ci_low) <= min(baseline_ci_high, indep_ci_high))
independent_pass = bool(independent_mean_pass and ci_overlap_pass)
comparison = {
"replay_per_seed_deltas": replay_deltas,
"replay_max_abs_delta": round(max_replay_delta, 6),
"replay_model_fingerprint_match": replay_model_fingerprint_match,
"replay_pass": bool(replay_pass and replay_model_fingerprint_match),
"independent_mean_abs_diff": round(indep_mean_diff, 6),
"independent_mean_pass": independent_mean_pass,
"baseline_ci95_t_interval": [baseline_ci_low, baseline_ci_high],
"independent_ci95_t_interval": [indep_ci_low, indep_ci_high],
"ci_overlap_pass": ci_overlap_pass,
"independent_pass": independent_pass,
"overall_pass": bool(
replay_pass and replay_model_fingerprint_match and independent_pass
),
}
return comparison, baseline_stats, replay_stats, independent_stats
def write_replication_csv(
baseline_records: List[Dict[str, Any]],
replay_runs: List[Dict[str, Any]],
independent_runs: List[Dict[str, Any]],
output_csv: Path,
):
output_csv.parent.mkdir(parents=True, exist_ok=True)
fields = [
"cohort",
"method",
"candidate_label",
"regimen",
"seed",
"n_particles",
"epochs",
"particle_epochs",
"train_loss",
"train_acc",
"test_loss",
"test_acc",
"test_mse",
"fit_time_sec",
"data_fingerprint",
"model_fingerprint",
"device",
"completed",
"error",
]
all_rows = []
for r in baseline_records:
row = dict(r)
row["cohort"] = "baseline"
all_rows.append(row)
for r in replay_runs:
row = dict(r)
row["cohort"] = "replay"
all_rows.append(row)
for r in independent_runs:
row = dict(r)
row["cohort"] = "independent"
all_rows.append(row)
with open(output_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
for r in all_rows:
writer.writerow(r)
def main():
parser = argparse.ArgumentParser(
description="Replicate and verify published Adaptive Moment 120p x 80e MNIST scaling result"
)
parser.add_argument(
"--baseline-json",
type=Path,
default=Path("benchmark_results/pso_v4_tuning.json"),
help="Path to baseline tuning JSON",
)
parser.add_argument(
"--output-json",
type=Path,
default=Path("benchmark_results/pso_v4_120p80_replication.json"),
help="Path for replication output JSON",
)
parser.add_argument(
"--output-csv",
type=Path,
default=Path("benchmark_results/pso_v4_120p80_replication.csv"),
help="Path for replication output CSV",
)
parser.add_argument(
"--device",
type=str,
default=None,
help="Execution device (cpu, cuda, mps)",
)
args = parser.parse_args()
device = resolve_execution_device(args.device)
print("=== Adaptive Moment 120p x 80e Replication Check ===")
print(f"Device: {device}")
print(f"Baseline JSON: {args.baseline_json}")
print(f"Output JSON: {args.output_json}")
print(f"Output CSV: {args.output_csv}")
baseline_data, baseline_records, winner_cfg, expected_fp = validate_and_load_baseline(
args.baseline_json
)
print(f"Validated baseline winner '{winner_cfg.candidate_label}' across 5 records.")
if baseline_data.get("device") != str(device):
raise ValueError(
f"Exact replay requires baseline device {baseline_data.get('device')!r}; "
f"got {str(device)!r}."
)
if baseline_data.get("pso_version") != pso_version:
raise ValueError(
f"Exact replay requires pso version {baseline_data.get('pso_version')!r}; "
f"got {pso_version!r}."
)
if baseline_data.get("torch_version") != torch.__version__:
raise ValueError(
f"Exact replay requires torch version {baseline_data.get('torch_version')!r}; "
f"got {torch.__version__!r}."
)
x_full_tr, y_train_3000, x_full_test, y_test_1000, data_fp = prepare_full_pca_data()
if expected_fp and data_fp != expected_fp:
raise ValueError(
f"Reconstructed data fingerprint {data_fp} does not match baseline {expected_fp}"
)
print(f"Reconstructed PCA32 data (fingerprint: {data_fp})")
print("\n--- Running Cohort 1: Exact Replay (Seeds 71-75) ---")
replay_runs = run_replication_cohort(
cfg=winner_cfg,
seeds=REPLAY_SEEDS,
x_train=x_full_tr,
y_train=y_train_3000,
x_eval=x_full_test,
y_eval=y_test_1000,
device=device,
data_fp=data_fp,
run_type="replication_replay",
)
print("\n--- Running Cohort 2: Independent Fresh Seeds (Seeds 81-85) ---")
independent_runs = run_replication_cohort(
cfg=winner_cfg,
seeds=INDEPENDENT_SEEDS,
x_train=x_full_tr,
y_train=y_train_3000,
x_eval=x_full_test,
y_eval=y_test_1000,
device=device,
data_fp=data_fp,
run_type="replication_independent",
)
comparison, baseline_stats, replay_stats, independent_stats = evaluate_replication(
baseline_records, replay_runs, independent_runs
)
payload = {
"replication_protocol_version": REPLICATION_PROTOCOL_VERSION,
"source_tuning_protocol_version": baseline_data["tuning_protocol_version"],
"pso_version": pso_version,
"torch_version": torch.__version__,
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"device": str(device),
"hardware": get_hardware_provenance(device),
"baseline_json": str(args.baseline_json),
"source_tuning_timestamp": baseline_data.get("timestamp"),
"candidate_label": winner_cfg.candidate_label,
"config": winner_cfg.to_optimizer_kwargs(),
"data_fingerprint": data_fp,
"criteria": {
"replay_seeds": REPLAY_SEEDS,
"replay_max_abs_delta_tolerance": REPLAY_TOLERANCE,
"require_replay_model_fingerprint_match": True,
"independent_seeds": INDEPENDENT_SEEDS,
"independent_mean_abs_diff_margin": INDEPENDENT_MEAN_MARGIN,
"require_ci_overlap": True,
},
"summaries": {
"baseline": baseline_stats,
"replay": replay_stats,
"independent": independent_stats,
},
"comparison": comparison,
"baseline_runs": baseline_records,
"replay_runs": replay_runs,
"independent_runs": independent_runs,
"completed": True,
"error": None,
}
save_json_atomic(payload, args.output_json)
write_replication_csv(baseline_records, replay_runs, independent_runs, args.output_csv)
print("\n=== Replication Results Summary ===")
print(f"Baseline Mean Test Acc: {baseline_stats['mean']:.4f} ± {baseline_stats['std']:.4f}")
print(f"Replay Mean Test Acc: {replay_stats['mean']:.4f} ± {replay_stats['std']:.4f}")
print(f"Independent Mean Test Acc: {independent_stats['mean']:.4f} ± {independent_stats['std']:.4f}")
print(f"Max Replay Delta: {comparison['replay_max_abs_delta']:.6f} (Limit: {REPLAY_TOLERANCE}) -> Pass: {comparison['replay_pass']}")
print(f"Indep Mean Diff: {comparison['independent_mean_abs_diff']:.6f} (Limit: {INDEPENDENT_MEAN_MARGIN}) -> Pass: {comparison['independent_mean_pass']}")
print(f"CI Overlap Pass: {comparison['ci_overlap_pass']} (Baseline CI: {comparison['baseline_ci95_t_interval']}, Indep CI: {comparison['independent_ci95_t_interval']})")
print(f"OVERALL PASS: {comparison['overall_pass']}")
if not comparison["overall_pass"]:
print("\nREPLICATION CHECK FAILED!")
sys.exit(1)
else:
print("\nREPLICATION CHECK PASSED!")
if __name__ == "__main__":
main()
+97 -81
View File
@@ -1,23 +1,16 @@
# %%
import json
import os
import sys
import numpy as np
import pandas as pd
import tensorflow as tf
from keras.layers import Dense
from keras.models import Sequential
from keras.utils import to_categorical
import argparse
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from tensorflow import keras
from sklearn.preprocessing import StandardScaler
from pso import optimizer
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
def get_data():
def get_data(seed: int = 42):
with open("data/seeds/seeds_dataset.txt", "r", encoding="utf-8") as f:
data = f.readlines()
df = pd.DataFrame([d.split() for d in data])
@@ -33,80 +26,103 @@ def get_data():
]
df = df.astype(float)
df["target"] = df["target"].astype(int)
df["target"] = df["target"].astype(int) - 1
x = df.iloc[:, :-1].values.round(0).astype(int)
y = df.iloc[:, -1].values
y_class = to_categorical(y)
x = df.iloc[:, :-1].values.astype(np.float32)
y = df.iloc[:, -1].values.astype(np.int64)
x_train, x_test, y_train, y_test = train_test_split(
x, y_class, test_size=0.2, shuffle=True
x, y, test_size=0.2, shuffle=True, random_state=seed
)
scaler = StandardScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.transform(x_test)
return (
torch.tensor(x_train, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.int64),
torch.tensor(x_test, dtype=torch.float32),
torch.tensor(y_test, dtype=torch.int64),
)
return x_train, y_train, x_test, y_test
def make_model(seed: int = 42):
torch.manual_seed(seed)
return nn.Sequential(
nn.Linear(7, 16),
nn.ReLU(),
nn.Linear(16, 32),
nn.ReLU(),
nn.Linear(32, 3),
)
def make_model():
model = Sequential()
model.add(Dense(16, activation="relu", input_shape=(7,)))
model.add(Dense(32, activation="relu"))
model.add(Dense(4, activation="softmax"))
def main():
parser = argparse.ArgumentParser(description="PSO Seeds Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "original",
"initialization": "model_noise",
"evaluation": "full",
"convergence": "particle_reset",
"refinement": "adam",
"n_particles": 24,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.0,
"mutation_swarm": 0.3,
"particle_min": -3.0,
"particle_max": 3.0,
"velocity_limit_ratio": 0.1,
"boundary_strategy": "reflect",
"seed": 42,
"epochs": 80,
"renewal": "acc",
"output_dir": "output/seeds",
"checkpoint_interval": 25,
"refinement_epochs": 10,
"refinement_lr": 0.001,
},
)
args = parser.parse_args()
return model
model = make_model(seed=args.seed)
x_train, y_train, x_test, y_test = get_data(seed=args.seed)
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.CrossEntropyLoss(),
task="multiclass",
inertia_profile={"c0": 0.5, "c1": 1.0, "w_min": 0.7, "w_max": 1.2},
)
pso_seeds = Optimizer(**kwargs)
print(f"Optimizer device: {pso_seeds.device}")
best_score = pso_seeds.fit(
x_train,
y_train,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
validation_data=(x_test, y_test),
output_dir=args.output_dir,
checkpoint_interval=25,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
# %%
model = make_model()
x_train, y_train, x_test, y_test = get_data()
loss = [
"mean_squared_error",
"categorical_crossentropy",
"sparse_categorical_crossentropy",
"binary_crossentropy",
"kullback_leibler_divergence",
"poisson",
"cosine_similarity",
"log_cosh",
"huber_loss",
"mean_absolute_error",
"mean_absolute_percentage_error",
]
# rs = random_state()
pso_mnist = optimizer(
model,
loss="categorical_crossentropy",
n_particles=100,
c0=0.5,
c1=1.0,
w_min=0.7,
w_max=1.2,
negative_swarm=0.0,
mutation_swarm=0.3,
convergence_reset=True,
convergence_reset_patience=10,
convergence_reset_monitor="mse",
convergence_reset_min_delta=0.0005,
)
best_score = pso_mnist.fit(
x_train,
y_train,
epochs=500,
save_info=True,
log=2,
log_name="seeds",
renewal="acc",
check_point=25,
empirical_balance=False,
dispersion=False,
back_propagation=False,
validate_data=(x_test, y_test),
)
print("Done!")
sys.exit(0)
if __name__ == "__main__":
main()
+1432
View File
File diff suppressed because it is too large Load Diff
+77 -64
View File
@@ -1,76 +1,89 @@
# %%
import os
import sys
import argparse
import torch
import torch.nn as nn
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
from pso import optimizer
from pso import Optimizer
from cli import add_pso_args, build_optimizer_kwargs
def get_data():
x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], dtype=torch.float32)
y = torch.tensor([[0.0], [1.0], [1.0], [0.0]], dtype=torch.float32)
return x, y
def make_model():
model = Sequential()
model.add(layers.Dense(2, activation="sigmoid", input_shape=(2,)))
model.add(layers.Dense(1, activation="sigmoid"))
return model
def make_model(seed: int = 101):
torch.manual_seed(seed)
return nn.Sequential(
nn.Linear(2, 4),
nn.Tanh(),
nn.Linear(4, 1),
)
# %%
model = make_model()
x_test, y_test = get_data()
def main():
parser = argparse.ArgumentParser(description="PSO XOR Benchmark Script")
add_pso_args(
parser,
defaults={
"method": "original",
"initialization": "model_noise",
"evaluation": "fixed_subset",
"convergence": "none",
"refinement": "adam",
"n_particles": 40,
"c0": None,
"c1": None,
"w_min": None,
"w_max": None,
"negative_swarm": 0.1,
"mutation_swarm": 0.03,
"particle_min": -5.0,
"particle_max": 5.0,
"velocity_limit_ratio": 0.1,
"boundary_strategy": "reflect",
"initial_position_noise": 1.0,
"seed": 101,
"epochs": 120,
"fitness_size": 4,
"renewal": "loss",
"output_dir": "output/xor",
"refinement_epochs": 100,
"refinement_lr": 0.03,
},
)
args = parser.parse_args()
x, y = get_data()
model = make_model(seed=args.seed)
loss = [
"mean_squared_error",
"mean_squared_logarithmic_error",
"binary_crossentropy",
"categorical_crossentropy",
"sparse_categorical_crossentropy",
"kullback_leibler_divergence",
"poisson",
"cosine_similarity",
"log_cosh",
"huber_loss",
"mean_absolute_error",
"mean_absolute_percentage_error",
]
fitness_size = args.fitness_size if args.evaluation == "fixed_subset" else None
refinement_epochs = args.refinement_epochs if args.refinement == "adam" else 0
pso_xor = optimizer(
model,
loss=loss[0],
n_particles=100,
c0=0.35,
c1=0.8,
w_min=0.6,
w_max=1.2,
negative_swarm=0.1,
mutation_swarm=0.2,
particle_min=-3,
particle_max=3,
)
best_score = pso_xor.fit(
x_test,
y_test,
epochs=200,
save_info=True,
log=2,
log_name="xor",
renewal="acc",
check_point=25,
)
kwargs = build_optimizer_kwargs(
args,
model=model,
loss=nn.BCEWithLogitsLoss(),
task="binary",
inertia_profile={"c0": 0.7, "c1": 0.9, "w_min": 0.3, "w_max": 0.8},
)
pso_xor = Optimizer(**kwargs)
print(f"Optimizer device: {pso_xor.device}")
print("Done!")
sys.exit(0)
# %%
best_score = pso_xor.fit(
x,
y,
epochs=args.epochs,
batch_size=args.batch_size,
fitness_size=fitness_size,
renewal=args.renewal,
output_dir=args.output_dir,
save_info=True,
refinement_epochs=refinement_epochs,
refinement_lr=args.refinement_lr,
)
print(f"Done! Best score: {best_score}")
if __name__ == "__main__":
main()