100enigma's picture
SimQuantum — AMD Developer Hackathon
da98415
Raw
History Blame Contribute Delete
11 kB
"""
experiments/train_phase1.py
============================
Phase 1 training script.
Generates CIM training data → trains 5-model ensemble → fits OOD detector
→ saves checkpoint → (optional) evaluates on QFlow.
Usage:
python experiments/train_phase1.py # full 51k training run
python experiments/train_phase1.py --fast # 3k samples, 10 epochs (dev)
python experiments/train_phase1.py --qflow PATH # evaluate on QFlow after training
Outputs (in experiments/checkpoints/phase1/):
model_0.pt ... model_4.pt — ensemble weights
ood_detector.pkl — fitted Mahalanobis OOD detector
training_log.json — metrics, config snapshot, timestamps
Phase 1 benchmark targets (blueprint §8):
≥96% accuracy on QFlow held-out test set
OOD detector flags 100% of QFlow test samples (since they're OOD by design)
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
import numpy as np
import torch
def main():
parser = argparse.ArgumentParser(description="Phase 1 training")
parser.add_argument("--fast", action="store_true",
help="Use small dataset (3k samples, 10 epochs) for dev")
parser.add_argument("--qflow", type=str, default=None,
help="Path to QFlow held-out test set (NPZ or directory)")
parser.add_argument("--device", type=str, default="cpu",
help="Torch device: 'cpu' or 'cuda'")
parser.add_argument("--out", type=str, default="experiments/checkpoints/phase1",
help="Output directory for checkpoints")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--out", type=str, default="experiments/checkpoints/phase1",
help="Checkpoint output directory")
args = parser.parse_args()
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
print(f"\n{'='*60}")
print("PHASE 1 TRAINING — CIM → EnsembleCNN + OOD Detector")
print(f"{'='*60}\n")
# -----------------------------------------------------------------------
# 1. Generate training data
# -----------------------------------------------------------------------
from qdot.perception.dataset import CIMDataset, DatasetConfig
if args.fast:
cfg = DatasetConfig(n_per_class=1_000, seed=args.seed, augment=True)
n_epochs = 10
print("FAST MODE: 3k samples, 10 epochs")
else:
cfg = DatasetConfig(n_per_class=17_000, seed=args.seed, augment=True)
n_epochs = 30
print("FULL MODE: ~51k samples, 30 epochs")
t0 = time.time()
dataset = CIMDataset(cfg)
X_all, y_all = dataset.generate()
print(f"Data generation: {time.time()-t0:.1f}s | shape={X_all.shape}")
# Apply log preprocessing to match inference pipeline.
# EnsembleCNN._prepare() applies log_preprocess() at inference time,
# so training data must go through the same transform. Without this,
# models train on raw conductance but infer on log-conductance — the
# distribution mismatch collapses val accuracy from ~70% to ~33% (random).
from qdot.perception.features import log_preprocess
X_all = np.stack(
[log_preprocess(x[0])[np.newaxis] for x in X_all], axis=0
).astype(np.float32)
X_train, X_val, y_train, y_val = CIMDataset.split(
X_all, y_all, val_frac=0.15, seed=args.seed
)
print(
f"Split: {len(X_train)} train | {len(X_val)} val\n"
f"Class counts (train): {np.bincount(y_train)}\n"
)
# -----------------------------------------------------------------------
# 2. Train ensemble
# -----------------------------------------------------------------------
from qdot.perception.classifier import EnsembleCNN
print("Training 5-model ensemble...")
t1 = time.time()
ensemble = EnsembleCNN.train_from_data(
X_train=X_train,
y_train=y_train,
X_val=X_val,
y_val=y_val,
n_epochs=n_epochs,
batch_size=128,
lr=3e-4,
device=args.device,
model_dir=str(out_dir),
verbose=True,
)
train_time = time.time() - t1
print(f"\nEnsemble trained in {train_time:.1f}s")
# Validate directly — X_val already has log_preprocess applied,
# so we bypass _prepare() and go straight to the model.
device_t = torch.device(args.device)
X_val_t = torch.from_numpy(X_val).float().to(device_t)
y_val_t = torch.from_numpy(y_val).long()
correct = 0
ensemble_models = ensemble.models
with torch.no_grad():
for i in range(0, len(X_val), 128):
xb = X_val_t[i:i+128]
# Mean logits across ensemble
logits = torch.stack([m(xb) for m in ensemble_models]).mean(0)
preds = logits.argmax(dim=1).cpu()
correct += (preds == y_val_t[i:i+128]).sum().item()
val_acc = correct / len(y_val)
print(f"Final val accuracy: {val_acc:.4f}")
# -----------------------------------------------------------------------
# 3. Fit OOD detector on training features
# -----------------------------------------------------------------------
from qdot.perception.ood import MahalanobisOOD, extract_features_batch
print("\nFitting OOD detector on training features...")
t2 = time.time()
train_features = extract_features_batch(ensemble, X_train, device=args.device)
ood = MahalanobisOOD(n_components=16, calibration_percentile=95.0)
ood.fit(train_features)
# Sanity check FPR on validation set
val_features = extract_features_batch(ensemble, X_val, device=args.device)
_, val_flags = ood.score_batch(val_features)
fpr = float(val_flags.mean())
print(f"OOD detector fitted in {time.time()-t2:.1f}s | val FPR={fpr:.3f} (target ≤0.05)")
ood.save(str(out_dir / "ood_detector.pkl"))
# -----------------------------------------------------------------------
# 4. (Optional) QFlow evaluation — sim-to-real transfer test
# -----------------------------------------------------------------------
qflow_acc = None
qflow_ood_recall = None
if args.qflow:
print(f"\nEvaluating on QFlow: {args.qflow}")
qflow_acc, qflow_ood_recall = _evaluate_qflow(
args.qflow, ensemble, ood, args.device
)
print(f"QFlow accuracy: {qflow_acc:.4f} (target ≥0.96)")
print(f"QFlow OOD recall: {qflow_ood_recall:.4f} (target =1.00 — all real data is OOD)")
# -----------------------------------------------------------------------
# 5. Save training log
# -----------------------------------------------------------------------
log = {
"timestamp": time.time(),
"config": {
"n_per_class": cfg.n_per_class,
"n_epochs": n_epochs,
"batch_size": 128,
"lr": 3e-4,
"seed": args.seed,
"device": args.device,
"fast_mode": args.fast,
},
"results": {
"train_samples": len(X_train),
"val_samples": len(X_val),
"val_accuracy_cim": val_acc,
"ood_val_fpr": fpr,
"qflow_accuracy": qflow_acc,
"qflow_ood_recall": qflow_ood_recall,
"train_time_s": train_time,
},
"benchmarks": {
"val_acc_target": 0.96,
"ood_fpr_target": 0.05,
"qflow_acc_target": 0.96,
"qflow_ood_recall_target": 1.0,
},
}
with open(out_dir / "training_log.json", "w") as f:
json.dump(log, f, indent=2)
print(f"\nCheckpoints saved to: {out_dir}")
print("Training complete.")
print(f"\n{'='*60}")
print("PHASE 1 BENCHMARK SUMMARY")
print(f"{'='*60}")
print(f" Val accuracy (CIM): {val_acc:.4f} {'✓' if val_acc >= 0.96 else '✗'} (target ≥0.96)")
print(f" OOD FPR (CIM val): {fpr:.4f} {'✓' if fpr <= 0.05 else '✗'} (target ≤0.05)")
if qflow_acc is not None:
print(f" QFlow accuracy: {qflow_acc:.4f} {'✓' if qflow_acc >= 0.96 else '✗'} (target ≥0.96)")
print(f" QFlow OOD recall: {qflow_ood_recall:.4f} {'✓' if qflow_ood_recall >= 0.95 else '✗'} (target ~1.0)")
print(f"{'='*60}\n")
def _evaluate_qflow(
qflow_path: str,
ensemble,
ood_detector,
device: str,
) -> tuple[float, float]:
"""
Evaluate ensemble on QFlow held-out test set.
QFlow format: each sample is a 2D stability diagram labelled as
one of {SC, Barrier, SD, DD}.
Label mapping to our 3-class system:
SC → MISC (2)
Barrier → MISC (2)
SD → SINGLE_DOT (1)
DD → DOUBLE_DOT (0)
Returns:
(accuracy, ood_recall)
accuracy: fraction of QFlow labels correctly predicted
ood_recall: fraction of QFlow samples flagged as OOD (should be ~1.0)
"""
from qdot.perception.ood import extract_features_batch
from qdot.perception.dataset import CIMDataset
import os
qflow_path = Path(qflow_path)
# Load QFlow — support both NPZ and directory of images
if qflow_path.suffix == ".npz":
data = np.load(qflow_path)
X_qflow = data["arrays"].astype(np.float32) # (N, H, W) or (N, 1, H, W)
y_qflow = data["labels"].astype(np.int64) # QFlow integer labels
qflow_label_map = {0: 2, 1: 2, 2: 1, 3: 0} # SC,Barrier→MISC; SD→SD; DD→DD
else:
raise NotImplementedError(
"QFlow directory loading not yet implemented. "
"Convert to NPZ format first: arrays (N, H, W), labels (N,) with "
"SC=0, Barrier=1, SD=2, DD=3."
)
# Normalise shape to (N, 1, 64, 64)
if X_qflow.ndim == 3:
X_qflow = X_qflow[:, np.newaxis, :, :]
if X_qflow.shape[-1] != 64:
from scipy.ndimage import zoom
n = X_qflow.shape[0]
resized = np.zeros((n, 1, 64, 64), dtype=np.float32)
for i in range(n):
scale = 64.0 / X_qflow.shape[-1]
resized[i, 0] = np.clip(
zoom(X_qflow[i, 0].astype(np.float64), scale, order=1), 0, 1
).astype(np.float32)
X_qflow = resized
# Map QFlow labels to our 3-class system
y_ours = np.array([qflow_label_map[int(l)] for l in y_qflow], dtype=np.int64)
# Classify
preds = []
for arr in X_qflow:
pred, _, _ = ensemble.classify(arr.squeeze())
preds.append(pred)
accuracy = float(np.mean(np.array(preds) == y_ours))
# OOD: all QFlow samples are real hardware → should be flagged as OOD
qflow_features = extract_features_batch(ensemble, X_qflow, device=device)
_, ood_flags = ood_detector.score_batch(qflow_features)
ood_recall = float(ood_flags.mean())
return accuracy, ood_recall
if __name__ == "__main__":
main()