Spaces:
Sleeping
Sleeping
Download scripts/evolve_meta_model.py from spell0/Batch-effects-leaderboard: direct link, hf CLI and curl.
- Browser
- Download file 32.6 kB
-
https://huggingface.co/spaces/spell0/Batch-effects-leaderboard/resolve/main/scripts/evolve_meta_model.py
- Command line
-
hf download hf://spaces/spell0/Batch-effects-leaderboard/scripts/evolve_meta_model.py
-
curl -L -o evolve_meta_model.py https://huggingface.co/spaces/spell0/Batch-effects-leaderboard/resolve/main/scripts/evolve_meta_model.py
32.6 kB
| #!/usr/bin/env python3 | |
| """Evolve a dataset-conditioned BERNN policy across whole biological datasets.""" | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import hashlib | |
| import importlib.metadata | |
| import json | |
| import os | |
| import sys | |
| import uuid | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parent.parent | |
| if str(ROOT) not in sys.path: | |
| sys.path.insert(0, str(ROOT)) | |
| from scripts import hp_search | |
| from src.dataset_splits import load_dataset_partitions | |
| from src.evolutionary_meta import ( | |
| EvolutionConfig, | |
| PolicyShape, | |
| aggregate_dataset_scores, | |
| breed_next_population, | |
| config_dict, | |
| decode_config, | |
| genome_digest, | |
| initialize_population, | |
| normalize_meta_features, | |
| recommended_batch_size, | |
| ) | |
| from src.zero_shot_recommender.meta_features import ( | |
| META_FEATURE_NAMES, | |
| extract_meta_features, | |
| ) | |
| from src.meta_policy import META_POLICY_REPO, load_policy_metadata, publish_policy_if_improved | |
| SCORE_THRESHOLDS = { | |
| "massbench_adenocarcinoma": {"reference": 0.24, "acceptable": 0.19}, | |
| "massbench_alzheimer": {"reference": 0.44, "acceptable": 0.39}, | |
| "massbench_benchmark": {"reference": 0.75, "acceptable": 0.70}, | |
| } | |
| def parse_args(argv=None): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--split-manifest", type=Path, default=ROOT / "config" / "evolution_development_datasets.json") | |
| parser.add_argument("--output-dir", type=Path, default=ROOT / "results" / "meta_evolution") | |
| parser.add_argument("--population-size", type=int, default=12) | |
| parser.add_argument("--generations", type=int, default=25) | |
| parser.add_argument( | |
| "--candidate-budget", type=int, default=None, | |
| help="Exact total number of source-only candidate policies to evaluate; " | |
| "defaults to population-size * generations and may end mid-generation.", | |
| ) | |
| parser.add_argument("--hidden-size", type=int, default=16) | |
| parser.add_argument("--elite-count", type=int, default=2) | |
| parser.add_argument("--tournament-size", type=int, default=3) | |
| parser.add_argument("--crossover-rate", type=float, default=0.9) | |
| parser.add_argument("--mutation-rate", type=float, default=0.05) | |
| parser.add_argument("--mutation-scale", type=float, default=0.1) | |
| parser.add_argument("--worst-dataset-weight", type=float, default=0.25) | |
| parser.add_argument("--validation-patience", type=int, default=8) | |
| parser.add_argument("--n-epochs", type=int, default=100) | |
| parser.add_argument("--n-repeats", type=int, default=3) | |
| parser.add_argument("--num-workers", type=int, default=4) | |
| parser.add_argument( | |
| "--validation-epoch-interval", type=int, default=10, | |
| help="Log held-out validation epoch telemetry at epoch 0 and this interval.", | |
| ) | |
| parser.add_argument("--device", default="cuda", choices=["cpu", "cuda"]) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--resume", action="store_true") | |
| parser.add_argument("--smoke-evaluator", action="store_true", help="Exercise evolution without training BERNN") | |
| parser.add_argument("--no-wandb", action="store_true", help="Disable live Weights & Biases telemetry") | |
| parser.add_argument("--wandb-project", default="BE_leaderboard_meta_evolution") | |
| parser.add_argument("--wandb-run-name", default=None) | |
| parser.add_argument("--hf-meta-policy-repo", default=META_POLICY_REPO) | |
| parser.add_argument( | |
| "--no-hf-meta-policy-push", action="store_true", | |
| help="Do not compare/promote the validation champion on Hugging Face.", | |
| ) | |
| return parser.parse_args(argv) | |
| def _sha256(path: Path) -> str: | |
| return hashlib.sha256(path.read_bytes()).hexdigest() | |
| def _atomic_json(path: Path, value: dict) -> None: | |
| temporary = path.with_suffix(path.suffix + ".tmp") | |
| temporary.write_text(json.dumps(value, indent=2, default=str) + "\n") | |
| os.replace(temporary, path) | |
| def _load_datasets(dataset_ids): | |
| datasets = {} | |
| meta = {} | |
| for dataset_id in dataset_ids: | |
| X, y, batches = hp_search.load_dataset(dataset_id) | |
| datasets[dataset_id] = (X, y, batches) | |
| extracted = extract_meta_features(X, y, batches) | |
| meta[dataset_id] = np.asarray([extracted[name] for name in META_FEATURE_NAMES], dtype=np.float32) | |
| return datasets, meta | |
| def predict_config_for_dataset( | |
| X, | |
| y, | |
| batches, | |
| *, | |
| model_path=None, | |
| meta_model_path=None, | |
| checkpoint_path=None, | |
| max_warmup=50, | |
| **kwargs, | |
| ): | |
| """Predict exactly one BERNN config for one unseen dataset. | |
| The frozen evolutionary meta-policy is loaded from ``best_policy.npz``. | |
| Meta-features for the unseen dataset are normalized with the mean/scale | |
| learned from meta-training datasets and stored in that artifact. | |
| """ | |
| policy_path = model_path or meta_model_path or checkpoint_path | |
| if policy_path is None: | |
| policy_path = ROOT / "results" / "meta_evolution" / "best_policy.npz" | |
| policy_path = Path(policy_path) | |
| if policy_path.is_dir(): | |
| policy_path = policy_path / "best_policy.npz" | |
| if not policy_path.exists(): | |
| raise FileNotFoundError( | |
| f"Meta-policy not found: {policy_path}. " | |
| "Pass --meta-model-path /path/to/best_policy.npz." | |
| ) | |
| saved = np.load(policy_path) | |
| required = {"genome", "meta_mean", "meta_scale", "n_inputs", "hidden_size"} | |
| missing = required.difference(saved.files) | |
| if missing: | |
| raise ValueError( | |
| f"Meta-policy {policy_path} is missing required arrays: {sorted(missing)}" | |
| ) | |
| genome = saved["genome"] | |
| meta_mean = np.asarray(saved["meta_mean"], dtype=np.float32) | |
| meta_scale = np.asarray(saved["meta_scale"], dtype=np.float32) | |
| n_inputs = int(np.asarray(saved["n_inputs"]).reshape(-1)[0]) | |
| hidden_size = int(np.asarray(saved["hidden_size"]).reshape(-1)[0]) | |
| extracted = extract_meta_features(X, y, batches) | |
| raw_meta = np.asarray( | |
| [extracted[name] for name in META_FEATURE_NAMES], | |
| dtype=np.float32, | |
| ) | |
| if raw_meta.shape[0] != n_inputs: | |
| raise ValueError( | |
| f"Meta-feature count mismatch: policy expects {n_inputs}, " | |
| f"current extractor produced {raw_meta.shape[0]}." | |
| ) | |
| if meta_mean.shape != raw_meta.shape or meta_scale.shape != raw_meta.shape: | |
| raise ValueError( | |
| "Saved meta normalization shape does not match current meta-features: " | |
| f"mean={meta_mean.shape}, scale={meta_scale.shape}, current={raw_meta.shape}." | |
| ) | |
| safe_scale = np.where(np.abs(meta_scale) < 1e-12, 1.0, meta_scale) | |
| normalized_meta = (raw_meta - meta_mean) / safe_scale | |
| shape = PolicyShape(n_inputs=n_inputs, hidden_size=hidden_size) | |
| return decode_config( | |
| genome, | |
| normalized_meta, | |
| shape, | |
| max_warmup=int(max_warmup), | |
| ) | |
| def _append_solution_record(output_dir: Path, record: dict, train_datasets: list[str]) -> None: | |
| jsonl_path = output_dir / "solutions.jsonl" | |
| with jsonl_path.open("a", encoding="utf-8") as stream: | |
| stream.write(json.dumps(record, default=str) + "\n") | |
| csv_path = output_dir / "solutions.csv" | |
| columns = [ | |
| "solution_step", | |
| "generation", | |
| "population_index", | |
| "genome_digest", | |
| "train_fitness", | |
| "mean_mcc", | |
| "worst_mcc", | |
| *[f"mcc_{name}" for name in train_datasets], | |
| *[f"test_mcc_{name}" for name in train_datasets], | |
| ] | |
| row = {name: record.get(name) for name in columns} | |
| for name in train_datasets: | |
| row[f"mcc_{name}"] = record["train_scores"][name] | |
| row[f"test_mcc_{name}"] = record["test_scores"].get(name) | |
| write_header = not csv_path.exists() or csv_path.stat().st_size == 0 | |
| with csv_path.open("a", newline="", encoding="utf-8") as stream: | |
| writer = csv.DictWriter(stream, fieldnames=columns) | |
| if write_header: | |
| writer.writeheader() | |
| writer.writerow(row) | |
| def _mean_epoch_metric(metrics: dict, metric_name: str) -> dict[int, float]: | |
| """Average one BERNN epoch metric over grouped outer-CV folds/repeats.""" | |
| values: dict[int, list[float]] = {} | |
| for rows in metrics.get("_epoch_traces_by_fold", {}).values(): | |
| for row in rows: | |
| epoch = int(row["epoch"]) | |
| matching = [ | |
| float(value) for key, value in row.items() | |
| if key == metric_name or key.endswith(f"/{metric_name}") | |
| ] | |
| if matching: | |
| values.setdefault(epoch, []).extend(matching) | |
| return {epoch: float(np.mean(epoch_values)) for epoch, epoch_values in values.items()} | |
| def _classifier_widths(config: dict) -> list[int]: | |
| widths = [max(16, int(config["layer1"]))] | |
| for _ in range(1, max(1, int(config["n_layers"]))): | |
| widths.append(max(widths[-1] // 2, 16)) | |
| return widths | |
| def _numeric_config_telemetry(dataset_id: str, config: dict) -> dict[str, float]: | |
| telemetry = {} | |
| for name, value in config.items(): | |
| if isinstance(value, (bool, np.bool_)): | |
| telemetry[f"solutions/config/{dataset_id}/{name}"] = float(value) | |
| elif isinstance(value, (int, float, np.integer, np.floating)): | |
| telemetry[f"solutions/config/{dataset_id}/{name}"] = float(value) | |
| widths = _classifier_widths(config) | |
| telemetry[f"solutions/config/{dataset_id}/n_neurons"] = float(sum(widths)) | |
| for layer_index, width in enumerate(widths, 1): | |
| telemetry[f"solutions/config/{dataset_id}/layer{layer_index}_width"] = float(width) | |
| return telemetry | |
| def _score_figure( | |
| dataset_id: str, | |
| rows: list[tuple[int, float, float]], | |
| thresholds: dict[str, float] | None = None, | |
| ): | |
| """Plot validation/test MCC plus BERNN-best and acceptable-goal traces.""" | |
| import plotly.graph_objects as go | |
| x = [row[0] for row in rows] | |
| valid = [row[1] for row in rows] | |
| test = [row[2] for row in rows] | |
| figure = go.Figure() | |
| figure.add_trace(go.Scatter(x=x, y=valid, mode="lines+markers", name="valid_mcc")) | |
| figure.add_trace(go.Scatter(x=x, y=test, mode="lines+markers", name="test_mcc")) | |
| thresholds = dict(SCORE_THRESHOLDS.get(dataset_id, {}) if thresholds is None else thresholds) | |
| if "reference" in thresholds: | |
| figure.add_trace(go.Scatter( | |
| x=x, y=[thresholds["reference"]] * len(x), mode="lines", | |
| line={"dash": "solid", "width": 2}, name="Best valid MCC", | |
| )) | |
| if "acceptable" in thresholds: | |
| figure.add_trace(go.Scatter( | |
| x=x, y=[thresholds["acceptable"]] * len(x), mode="lines", | |
| line={"dash": "dash", "width": 2}, name="goal", | |
| )) | |
| figure.update_layout( | |
| title=f"{dataset_id}: MCC versus BERNN thresholds", | |
| xaxis_title="solution step", yaxis_title="grouped-CV MCC", | |
| yaxis_range=[-1.0, 1.0], | |
| ) | |
| return figure | |
| def _solution_records(output_dir: Path) -> list[dict]: | |
| path = output_dir / "solutions.jsonl" | |
| if not path.exists(): | |
| return [] | |
| return [json.loads(line) for line in path.read_text().splitlines() if line.strip()] | |
| def _save_source_champion( | |
| output_dir: Path, | |
| genome: np.ndarray, | |
| *, | |
| mean: np.ndarray, | |
| scale: np.ndarray, | |
| shape: PolicyShape, | |
| record: dict, | |
| ) -> None: | |
| np.savez_compressed( | |
| output_dir / "best_policy.npz", | |
| genome=np.asarray(genome, dtype=np.float32), | |
| meta_mean=mean, | |
| meta_scale=scale, | |
| n_inputs=np.asarray([shape.n_inputs]), | |
| hidden_size=np.asarray([shape.hidden_size]), | |
| ) | |
| _atomic_json(output_dir / "best_policy.json", record) | |
| def _replay_source_champion( | |
| output_dir: Path, | |
| shape: PolicyShape, | |
| evolution: EvolutionConfig, | |
| seed: int, | |
| ) -> tuple[np.ndarray, dict] | None: | |
| """Reconstruct evaluated genomes without rerunning any BERNN fit. | |
| Evolution and breeding use source fitness only. Replaying the deterministic | |
| initializer and breeder therefore recovers the exact genome corresponding | |
| to every durable solution record, including legacy runs whose saved policy | |
| was selected with target validation telemetry. | |
| """ | |
| records = _solution_records(output_dir) | |
| if not records: | |
| return None | |
| replay_rng = np.random.default_rng(seed) | |
| population = initialize_population(shape, evolution, replay_rng) | |
| best_genome = None | |
| best_record = None | |
| generations = sorted({int(row["generation"]) for row in records}) | |
| for generation in generations: | |
| rows = sorted( | |
| (row for row in records if int(row["generation"]) == generation), | |
| key=lambda row: int(row["population_index"]), | |
| ) | |
| fitness = np.full(evolution.population_size, np.nan, dtype=float) | |
| for row in rows: | |
| index = int(row["population_index"]) | |
| if not 0 <= index < evolution.population_size: | |
| raise ValueError(f"Invalid population index in generation {generation}: {index}") | |
| observed_digest = str(row["genome_digest"]) | |
| recovered_digest = genome_digest(population[index]) | |
| if observed_digest != recovered_digest: | |
| raise ValueError( | |
| f"Cannot replay generation {generation} index {index}: " | |
| f"digest {recovered_digest} != {observed_digest}" | |
| ) | |
| value = float(row["train_fitness"]) | |
| fitness[index] = value | |
| if best_record is None or value > float(best_record["train_fitness"]): | |
| best_genome = population[index].copy() | |
| best_record = { | |
| "selection_protocol": "source_only_best_fitness", | |
| "generation": generation, | |
| "population_index": index, | |
| "solution_step": int(row["solution_step"]), | |
| "train_fitness": value, | |
| "train_scores": row["train_scores"], | |
| "genome_digest": observed_digest, | |
| "target_scores_observed_for_selection": False, | |
| } | |
| if len(rows) == evolution.population_size: | |
| if not np.isfinite(fitness).all(): | |
| raise ValueError(f"Incomplete fitness vector for generation {generation}") | |
| population = breed_next_population(population, fitness, evolution, replay_rng) | |
| elif generation != generations[-1]: | |
| raise ValueError(f"Only the final generation may be partial; generation {generation} is incomplete") | |
| return best_genome, best_record | |
| def main(argv=None) -> int: | |
| args = parse_args(argv) | |
| if args.n_repeats != 3: | |
| raise ValueError("Meta-evolution requires grouped CV=3") | |
| if importlib.metadata.version("bernn") != "1.0.6": | |
| raise RuntimeError("This experiment requires bernn==1.0.6") | |
| if args.generations < 1: | |
| raise ValueError("generations must be positive") | |
| candidate_budget = int( | |
| args.candidate_budget | |
| if args.candidate_budget is not None | |
| else args.population_size * args.generations | |
| ) | |
| if candidate_budget < 1: | |
| raise ValueError("candidate_budget must be positive") | |
| partitions = load_dataset_partitions(args.split_manifest) | |
| # Target datasets provide descriptors for a frozen recommendation only. | |
| # Their BERNN scores and fixed tests are never opened by source search. | |
| heldout_ids = tuple(getattr(partitions, "test", ())) | |
| datasets, raw_meta = _load_datasets(partitions.train) | |
| fixed_tests = { | |
| dataset_id: hp_search.load_fixed_test_dataset(dataset_id) | |
| for dataset_id in partitions.train | |
| } | |
| train_matrix = np.stack([raw_meta[name] for name in partitions.train]) | |
| normalized_train, mean, scale = normalize_meta_features(train_matrix) | |
| normalized_meta = { | |
| name: row for name, row in zip(partitions.train, normalized_train) | |
| } | |
| evolution = EvolutionConfig( | |
| population_size=args.population_size, | |
| elite_count=args.elite_count, | |
| tournament_size=args.tournament_size, | |
| crossover_rate=args.crossover_rate, | |
| mutation_rate=args.mutation_rate, | |
| mutation_scale=args.mutation_scale, | |
| worst_dataset_weight=args.worst_dataset_weight, | |
| ) | |
| evolution.validate() | |
| shape = PolicyShape(len(META_FEATURE_NAMES), args.hidden_size) | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| manifest_hash = _sha256(args.split_manifest) | |
| checkpoint_path = args.output_dir / "checkpoint.npz" | |
| state_path = args.output_dir / "state.json" | |
| rng = np.random.default_rng(args.seed) | |
| generation_start = 0 | |
| best_train_fitness = -np.inf | |
| solution_step = 0 | |
| if args.resume: | |
| if not checkpoint_path.exists() or not state_path.exists(): | |
| raise FileNotFoundError("--resume requires checkpoint.npz and state.json") | |
| state = json.loads(state_path.read_text()) | |
| if state["development_manifest_sha256"] != manifest_hash: | |
| raise ValueError("Development dataset manifest changed since checkpoint creation") | |
| expected_shape = {"n_inputs": shape.n_inputs, "hidden_size": shape.hidden_size} | |
| if state.get("policy_shape") != expected_shape: | |
| raise ValueError("Policy shape differs from the saved checkpoint") | |
| if state.get("evolution_config") != config_dict(evolution): | |
| raise ValueError("Evolution configuration differs from the saved checkpoint") | |
| saved = np.load(checkpoint_path) | |
| population = saved["population"] | |
| generation_start = int(state["next_generation"]) | |
| best_train_fitness = float(state.get("best_train_fitness", -np.inf)) | |
| solution_step = int(state.get("solution_step", generation_start * args.population_size)) | |
| rng.bit_generator.state = state["rng_state"] | |
| else: | |
| population = initialize_population(shape, evolution, rng) | |
| hp_args = hp_search.parse_args([]) | |
| hp_args.n_epochs = args.n_epochs | |
| hp_args.n_repeats = args.n_repeats | |
| hp_args.num_workers = args.num_workers | |
| hp_args.log1p = True | |
| hp_args.device = args.device | |
| hp_args.no_wandb = True | |
| hp_args.combine_test = False | |
| hp_args.max_warmup = max(1, min(50, args.n_epochs)) | |
| score_cache = {} | |
| run_metadata_path = args.output_dir / "run_metadata.json" | |
| if run_metadata_path.exists(): | |
| run_metadata = json.loads(run_metadata_path.read_text()) | |
| else: | |
| run_metadata = {"wandb_run_id": uuid.uuid4().hex[:8]} | |
| run_metadata.update({ | |
| "arm": "meta_evolution", | |
| "candidate_budget": candidate_budget, | |
| "budget_unit": "candidate_policy", | |
| "population_size": int(args.population_size), | |
| "generations": int(args.generations), | |
| "bernn_version": importlib.metadata.version("bernn"), | |
| "log1p": True, | |
| "num_workers": int(args.num_workers), | |
| "train_datasets": list(partitions.train), | |
| "validation_datasets": list(partitions.validation), | |
| "selection_protocol": "source_only_best_fitness", | |
| "target_scores_observed": False, | |
| "cross_test": 1, | |
| "fixed_test_labels": "monitoring_only_excluded_from_fitness_and_selection", | |
| }) | |
| _atomic_json(run_metadata_path, run_metadata) | |
| wandb_run = None | |
| if not args.no_wandb: | |
| import wandb | |
| wandb_run = wandb.init( | |
| project=args.wandb_project, | |
| name=args.wandb_run_name, | |
| id=run_metadata["wandb_run_id"], | |
| resume="allow", | |
| config={ | |
| **vars(args), | |
| "split_manifest": str(args.split_manifest), | |
| "output_dir": str(args.output_dir), | |
| "train_datasets": list(partitions.train), | |
| "validation_datasets": list(partitions.validation), | |
| "heldout_datasets": list(heldout_ids), | |
| }, | |
| ) | |
| wandb.define_metric("solution_step") | |
| wandb.define_metric("solutions/*", step_metric="solution_step") | |
| wandb.define_metric("generation") | |
| wandb.define_metric("champion/*", step_metric="generation") | |
| for dataset_id, thresholds in SCORE_THRESHOLDS.items(): | |
| wandb_run.summary[f"reference_mcc/{dataset_id}"] = thresholds["reference"] | |
| wandb_run.summary[f"acceptable_mcc/{dataset_id}"] = thresholds["acceptable"] | |
| wandb_run.summary["selection_protocol"] = "source_only_best_fitness" | |
| wandb_run.summary["target_scores_observed_after_contract_fix"] = False | |
| def evaluate(genome, dataset_id): | |
| key = f"{genome_digest(genome)}:{dataset_id}" | |
| if key in score_cache: | |
| return score_cache[key] | |
| metrics = {} | |
| try: | |
| if args.smoke_evaluator: | |
| target = raw_meta[dataset_id] | |
| score = float(np.tanh(np.mean(genome[: min(len(genome), len(target))]) + np.mean(target) * 1e-4)) | |
| else: | |
| X, y, batches = datasets[dataset_id] | |
| config = decode_config(genome, normalized_meta[dataset_id], shape, max_warmup=hp_args.max_warmup) | |
| run_args = argparse.Namespace(**vars(hp_args)) | |
| run_args.dataset = dataset_id | |
| run_args.seed = args.seed | |
| run_args.bs = recommended_batch_size(batches, cap=hp_args.bs) | |
| run_args.results_dir = str(args.output_dir / dataset_id) | |
| run_args.cv_split_cache = str(args.output_dir / "cv_splits" / f"{dataset_id}.npz") | |
| run_args.resolved_n_repeats = hp_search.resolve_n_repeats(run_args.n_repeats, batches) | |
| exp_id = f"meta_evolution_{dataset_id}_{genome_digest(genome)[:12]}" | |
| score, metrics = hp_search.run_trial( | |
| config, run_args, (X, y, batches), exp_id, | |
| fixed_test_data=fixed_tests[dataset_id], | |
| ) | |
| except Exception as exc: # noqa: BLE001 - invalid candidates receive sentinel fitness | |
| score = -1.0 | |
| print( | |
| f"[evolution] failed genome={genome_digest(genome)[:12]} " | |
| f"dataset={dataset_id}: {type(exc).__name__}: {exc}", | |
| flush=True, | |
| ) | |
| score_cache[key] = (float(score), metrics) | |
| return score_cache[key] | |
| replayed = _replay_source_champion(args.output_dir, shape, evolution, args.seed) | |
| if replayed is not None: | |
| replayed_genome, replayed_record = replayed | |
| best_train_fitness = float(replayed_record["train_fitness"]) | |
| _save_source_champion( | |
| args.output_dir, replayed_genome, mean=mean, scale=scale, | |
| shape=shape, record=replayed_record, | |
| ) | |
| ledger = args.output_dir / "source_generations.jsonl" | |
| existing_solutions = _solution_records(args.output_dir) | |
| score_history = {dataset_id: [] for dataset_id in partitions.train} | |
| for row in existing_solutions: | |
| for dataset_id in partitions.train: | |
| test_score = float(row.get("test_scores", {}).get(dataset_id, np.nan)) | |
| score_history[dataset_id].append(( | |
| int(row["solution_step"]), float(row["train_scores"][dataset_id]), test_score, | |
| )) | |
| def save_state(next_generation: int, checkpoint_population: np.ndarray) -> None: | |
| np.savez_compressed( | |
| checkpoint_path, population=checkpoint_population, | |
| meta_mean=mean, meta_scale=scale, | |
| ) | |
| _atomic_json(state_path, { | |
| "next_generation": next_generation, | |
| "best_train_fitness": best_train_fitness, | |
| "solution_step": solution_step, | |
| "rng_state": rng.bit_generator.state, | |
| "selection_protocol": "source_only_best_fitness", | |
| "development_manifest_sha256": manifest_hash, | |
| "policy_shape": {"n_inputs": shape.n_inputs, "hidden_size": shape.hidden_size}, | |
| "evolution_config": config_dict(evolution), | |
| "train_datasets": list(partitions.train), | |
| "validation_datasets": list(partitions.validation), | |
| "heldout_datasets": list(heldout_ids), | |
| }) | |
| for generation in range(generation_start, args.generations): | |
| prior_rows = sorted( | |
| (row for row in _solution_records(args.output_dir) | |
| if int(row["generation"]) == generation), | |
| key=lambda row: int(row["population_index"]), | |
| ) | |
| expected_indices = list(range(len(prior_rows))) | |
| observed_indices = [int(row["population_index"]) for row in prior_rows] | |
| if observed_indices != expected_indices: | |
| raise ValueError( | |
| f"Generation {generation} durable records are not a contiguous prefix: " | |
| f"{observed_indices}" | |
| ) | |
| train_scores = np.full((len(population), len(partitions.train)), np.nan, dtype=np.float64) | |
| train_fitness = np.full(len(population), np.nan, dtype=np.float64) | |
| for row in prior_rows: | |
| index = int(row["population_index"]) | |
| if genome_digest(population[index]) != row["genome_digest"]: | |
| raise ValueError(f"Checkpoint/ledger digest mismatch at generation {generation} index {index}") | |
| train_fitness[index] = float(row["train_fitness"]) | |
| train_scores[index] = [float(row["train_scores"][name]) for name in partitions.train] | |
| for population_index in range(len(prior_rows), len(population)): | |
| if solution_step >= candidate_budget: | |
| break | |
| genome = population[population_index] | |
| scores = np.asarray([evaluate(genome, name)[0] for name in partitions.fitness_datasets()]) | |
| fitness = aggregate_dataset_scores(scores, evolution.worst_dataset_weight) | |
| train_scores[population_index] = scores | |
| train_fitness[population_index] = fitness | |
| decoded = { | |
| name: decode_config(genome, normalized_meta[name], shape, max_warmup=hp_args.max_warmup) | |
| for name in partitions.train | |
| } | |
| solution_record = { | |
| "solution_step": solution_step, | |
| "generation": generation, | |
| "population_index": population_index, | |
| "genome_digest": genome_digest(genome), | |
| "train_fitness": float(fitness), | |
| "mean_mcc": float(np.mean(scores)), | |
| "worst_mcc": float(np.min(scores)), | |
| "train_scores": dict(zip(partitions.train, scores.tolist())), | |
| "test_scores": { | |
| name: float(evaluate(genome, name)[1].get("test_mcc", np.nan)) | |
| for name in partitions.train | |
| }, | |
| "config_by_dataset": decoded, | |
| "target_scores_observed": False, | |
| } | |
| solution_record["total_valid_mcc"] = float(fitness) | |
| solution_record["total_test_mcc"] = float(aggregate_dataset_scores( | |
| solution_record["test_scores"].values(), | |
| evolution.worst_dataset_weight, | |
| )) | |
| for dataset_id, score in zip(partitions.train, scores): | |
| score_history[dataset_id].append(( | |
| solution_step, float(score), solution_record["test_scores"][dataset_id], | |
| )) | |
| _append_solution_record(args.output_dir, solution_record, list(partitions.train)) | |
| if fitness > best_train_fitness: | |
| best_train_fitness = float(fitness) | |
| champion_record = { | |
| "selection_protocol": "source_only_best_fitness", | |
| "generation": generation, | |
| "population_index": population_index, | |
| "solution_step": solution_step, | |
| "train_fitness": float(fitness), | |
| "train_scores": solution_record["train_scores"], | |
| "genome_digest": solution_record["genome_digest"], | |
| "target_scores_observed_for_selection": False, | |
| } | |
| _save_source_champion( | |
| args.output_dir, genome, mean=mean, scale=scale, | |
| shape=shape, record=champion_record, | |
| ) | |
| if wandb_run is not None: | |
| config_telemetry = {} | |
| for dataset_id, dataset_config in decoded.items(): | |
| config_telemetry.update(_numeric_config_telemetry(dataset_id, dataset_config)) | |
| figure_telemetry = { | |
| f"solutions/mcc/{dataset_id}": _score_figure( | |
| dataset_id, score_history[dataset_id] | |
| ) | |
| for dataset_id in partitions.train | |
| } | |
| wandb_run.log({ | |
| "solution_step": solution_step, | |
| "solutions/train_fitness": float(fitness), | |
| "solutions/mean_mcc": float(np.mean(scores)), | |
| "solutions/worst_mcc": float(np.min(scores)), | |
| "solutions/total_valid_mcc": solution_record["total_valid_mcc"], | |
| **{ | |
| f"solutions/metrics/{name}/valid_mcc": float(score) | |
| for name, score in zip(partitions.train, scores) | |
| }, | |
| **{ | |
| f"solutions/metrics/{name}/test_mcc": solution_record["test_scores"][name] | |
| for name in partitions.train | |
| }, | |
| "solutions/total_test_mcc": solution_record["total_test_mcc"], | |
| **config_telemetry, | |
| **figure_telemetry, | |
| }) | |
| print(f"[evolution] source solution {solution_step} complete", flush=True) | |
| solution_step += 1 | |
| evaluated = int(np.isfinite(train_fitness).sum()) | |
| if evaluated < len(population): | |
| save_state(generation, population) | |
| print( | |
| f"[evolution] exact candidate budget reached at {solution_step}; " | |
| f"generation {generation} is {evaluated}/{len(population)} complete", | |
| flush=True, | |
| ) | |
| break | |
| champion_index = int(np.argmax(train_fitness)) | |
| source_record = { | |
| "generation": generation, | |
| "selection_protocol": "source_only_best_fitness", | |
| "train_champion_fitness": float(train_fitness[champion_index]), | |
| "train_scores": dict(zip(partitions.train, train_scores[champion_index].tolist())), | |
| "champion_digest": genome_digest(population[champion_index]), | |
| "target_scores_observed": False, | |
| } | |
| with ledger.open("a", encoding="utf-8") as stream: | |
| stream.write(json.dumps(source_record, default=str) + "\n") | |
| if wandb_run is not None: | |
| wandb_run.log({ | |
| "generation": generation, | |
| "champion/train_fitness": float(train_fitness[champion_index]), | |
| }) | |
| next_population = breed_next_population(population, train_fitness, evolution, rng) | |
| save_state(generation + 1, next_population) | |
| population = next_population | |
| best_policy_path = args.output_dir / "best_policy.npz" | |
| if not best_policy_path.exists(): | |
| raise FileNotFoundError("Source-only evolution did not produce best_policy.npz") | |
| best_policy = np.load(best_policy_path) | |
| best_genome = best_policy["genome"] | |
| _, target_raw_meta = _load_datasets(partitions.validation) | |
| target_matrix = np.stack([target_raw_meta[name] for name in partitions.validation]) | |
| normalized_targets, _, _ = normalize_meta_features(train_matrix, target_matrix) | |
| target_meta = { | |
| name: row for name, row in zip(partitions.validation, normalized_targets) | |
| } | |
| target_configs = { | |
| name: decode_config( | |
| best_genome, target_meta[name], shape, | |
| max_warmup=hp_args.max_warmup, | |
| ) | |
| for name in partitions.validation | |
| } | |
| _atomic_json(args.output_dir / "final_recommendations.json", { | |
| "selection_protocol": "source_only_best_fitness", | |
| "source_candidates": int(solution_step), | |
| "source_best_train_fitness": float(best_train_fitness), | |
| "target_scores_observed": False, | |
| "policy": best_policy_path.name, | |
| "policy_digest": genome_digest(best_genome), | |
| "target_configs": target_configs, | |
| }) | |
| if wandb_run is not None: | |
| wandb_run.summary["source_best_train_fitness"] = float(best_train_fitness) | |
| wandb_run.summary["source_candidates"] = int(solution_step) | |
| if wandb_run is not None: | |
| wandb_run.finish() | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |