morphml 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of morphml might be problematic. Click here for more details.
- morphml/__init__.py +14 -0
- morphml/api/__init__.py +26 -0
- morphml/api/app.py +326 -0
- morphml/api/auth.py +193 -0
- morphml/api/client.py +338 -0
- morphml/api/models.py +132 -0
- morphml/api/rate_limit.py +192 -0
- morphml/benchmarking/__init__.py +36 -0
- morphml/benchmarking/comparison.py +430 -0
- morphml/benchmarks/__init__.py +56 -0
- morphml/benchmarks/comparator.py +409 -0
- morphml/benchmarks/datasets.py +280 -0
- morphml/benchmarks/metrics.py +199 -0
- morphml/benchmarks/openml_suite.py +201 -0
- morphml/benchmarks/problems.py +289 -0
- morphml/benchmarks/suite.py +318 -0
- morphml/cli/__init__.py +5 -0
- morphml/cli/commands/experiment.py +329 -0
- morphml/cli/main.py +457 -0
- morphml/cli/quickstart.py +312 -0
- morphml/config.py +278 -0
- morphml/constraints/__init__.py +19 -0
- morphml/constraints/handler.py +205 -0
- morphml/constraints/predicates.py +285 -0
- morphml/core/__init__.py +3 -0
- morphml/core/crossover.py +449 -0
- morphml/core/dsl/README.md +359 -0
- morphml/core/dsl/__init__.py +72 -0
- morphml/core/dsl/ast_nodes.py +364 -0
- morphml/core/dsl/compiler.py +318 -0
- morphml/core/dsl/layers.py +368 -0
- morphml/core/dsl/lexer.py +336 -0
- morphml/core/dsl/parser.py +455 -0
- morphml/core/dsl/search_space.py +386 -0
- morphml/core/dsl/syntax.py +199 -0
- morphml/core/dsl/type_system.py +361 -0
- morphml/core/dsl/validator.py +386 -0
- morphml/core/graph/__init__.py +40 -0
- morphml/core/graph/edge.py +124 -0
- morphml/core/graph/graph.py +507 -0
- morphml/core/graph/mutations.py +409 -0
- morphml/core/graph/node.py +196 -0
- morphml/core/graph/serialization.py +361 -0
- morphml/core/graph/visualization.py +431 -0
- morphml/core/objectives/__init__.py +20 -0
- morphml/core/search/__init__.py +33 -0
- morphml/core/search/individual.py +252 -0
- morphml/core/search/parameters.py +453 -0
- morphml/core/search/population.py +375 -0
- morphml/core/search/search_engine.py +340 -0
- morphml/distributed/__init__.py +76 -0
- morphml/distributed/fault_tolerance.py +497 -0
- morphml/distributed/health_monitor.py +348 -0
- morphml/distributed/master.py +709 -0
- morphml/distributed/proto/README.md +224 -0
- morphml/distributed/proto/__init__.py +74 -0
- morphml/distributed/proto/worker.proto +170 -0
- morphml/distributed/proto/worker_pb2.py +79 -0
- morphml/distributed/proto/worker_pb2_grpc.py +423 -0
- morphml/distributed/resource_manager.py +416 -0
- morphml/distributed/scheduler.py +567 -0
- morphml/distributed/storage/__init__.py +33 -0
- morphml/distributed/storage/artifacts.py +381 -0
- morphml/distributed/storage/cache.py +366 -0
- morphml/distributed/storage/checkpointing.py +329 -0
- morphml/distributed/storage/database.py +459 -0
- morphml/distributed/worker.py +549 -0
- morphml/evaluation/__init__.py +5 -0
- morphml/evaluation/heuristic.py +237 -0
- morphml/exceptions.py +55 -0
- morphml/execution/__init__.py +5 -0
- morphml/execution/local_executor.py +350 -0
- morphml/integrations/__init__.py +28 -0
- morphml/integrations/jax_adapter.py +206 -0
- morphml/integrations/pytorch_adapter.py +530 -0
- morphml/integrations/sklearn_adapter.py +206 -0
- morphml/integrations/tensorflow_adapter.py +230 -0
- morphml/logging_config.py +93 -0
- morphml/meta_learning/__init__.py +66 -0
- morphml/meta_learning/architecture_similarity.py +277 -0
- morphml/meta_learning/experiment_database.py +240 -0
- morphml/meta_learning/knowledge_base/__init__.py +19 -0
- morphml/meta_learning/knowledge_base/embedder.py +179 -0
- morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
- morphml/meta_learning/knowledge_base/meta_features.py +265 -0
- morphml/meta_learning/knowledge_base/vector_store.py +271 -0
- morphml/meta_learning/predictors/__init__.py +27 -0
- morphml/meta_learning/predictors/ensemble.py +221 -0
- morphml/meta_learning/predictors/gnn_predictor.py +552 -0
- morphml/meta_learning/predictors/learning_curve.py +231 -0
- morphml/meta_learning/predictors/proxy_metrics.py +261 -0
- morphml/meta_learning/strategy_evolution/__init__.py +27 -0
- morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
- morphml/meta_learning/strategy_evolution/bandit.py +276 -0
- morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
- morphml/meta_learning/transfer.py +581 -0
- morphml/meta_learning/warm_start.py +286 -0
- morphml/optimizers/__init__.py +74 -0
- morphml/optimizers/adaptive_operators.py +399 -0
- morphml/optimizers/bayesian/__init__.py +52 -0
- morphml/optimizers/bayesian/acquisition.py +387 -0
- morphml/optimizers/bayesian/base.py +319 -0
- morphml/optimizers/bayesian/gaussian_process.py +635 -0
- morphml/optimizers/bayesian/smac.py +534 -0
- morphml/optimizers/bayesian/tpe.py +411 -0
- morphml/optimizers/differential_evolution.py +220 -0
- morphml/optimizers/evolutionary/__init__.py +61 -0
- morphml/optimizers/evolutionary/cma_es.py +416 -0
- morphml/optimizers/evolutionary/differential_evolution.py +556 -0
- morphml/optimizers/evolutionary/encoding.py +426 -0
- morphml/optimizers/evolutionary/particle_swarm.py +449 -0
- morphml/optimizers/genetic_algorithm.py +486 -0
- morphml/optimizers/gradient_based/__init__.py +22 -0
- morphml/optimizers/gradient_based/darts.py +550 -0
- morphml/optimizers/gradient_based/enas.py +585 -0
- morphml/optimizers/gradient_based/operations.py +474 -0
- morphml/optimizers/gradient_based/utils.py +601 -0
- morphml/optimizers/hill_climbing.py +169 -0
- morphml/optimizers/multi_objective/__init__.py +56 -0
- morphml/optimizers/multi_objective/indicators.py +504 -0
- morphml/optimizers/multi_objective/nsga2.py +647 -0
- morphml/optimizers/multi_objective/visualization.py +427 -0
- morphml/optimizers/nsga2.py +308 -0
- morphml/optimizers/random_search.py +172 -0
- morphml/optimizers/simulated_annealing.py +181 -0
- morphml/plugins/__init__.py +35 -0
- morphml/plugins/custom_evaluator_example.py +81 -0
- morphml/plugins/custom_optimizer_example.py +63 -0
- morphml/plugins/plugin_system.py +454 -0
- morphml/reports/__init__.py +30 -0
- morphml/reports/generator.py +362 -0
- morphml/tracking/__init__.py +7 -0
- morphml/tracking/experiment.py +309 -0
- morphml/tracking/logger.py +301 -0
- morphml/tracking/reporter.py +357 -0
- morphml/utils/__init__.py +6 -0
- morphml/utils/checkpoint.py +189 -0
- morphml/utils/comparison.py +390 -0
- morphml/utils/export.py +407 -0
- morphml/utils/progress.py +392 -0
- morphml/utils/validation.py +392 -0
- morphml/version.py +7 -0
- morphml/visualization/__init__.py +50 -0
- morphml/visualization/analytics.py +423 -0
- morphml/visualization/architecture_diagrams.py +353 -0
- morphml/visualization/architecture_plot.py +223 -0
- morphml/visualization/convergence_plot.py +174 -0
- morphml/visualization/crossover_viz.py +386 -0
- morphml/visualization/graph_viz.py +338 -0
- morphml/visualization/pareto_plot.py +149 -0
- morphml/visualization/plotly_dashboards.py +422 -0
- morphml/visualization/population.py +309 -0
- morphml/visualization/progress.py +260 -0
- morphml-1.0.0.dist-info/METADATA +434 -0
- morphml-1.0.0.dist-info/RECORD +158 -0
- morphml-1.0.0.dist-info/WHEEL +4 -0
- morphml-1.0.0.dist-info/entry_points.txt +3 -0
- morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Benchmark suite for running comprehensive optimizer comparisons."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from morphml.benchmarks.problems import BenchmarkProblem
|
|
7
|
+
from morphml.logging_config import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BenchmarkResult:
|
|
13
|
+
"""Results from a single benchmark run."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
problem_name: str,
|
|
18
|
+
optimizer_name: str,
|
|
19
|
+
best_fitness: float,
|
|
20
|
+
mean_fitness: float,
|
|
21
|
+
evaluations: int,
|
|
22
|
+
time_seconds: float,
|
|
23
|
+
convergence_generation: Optional[int] = None,
|
|
24
|
+
final_diversity: Optional[float] = None,
|
|
25
|
+
):
|
|
26
|
+
"""Initialize benchmark result."""
|
|
27
|
+
self.problem_name = problem_name
|
|
28
|
+
self.optimizer_name = optimizer_name
|
|
29
|
+
self.best_fitness = best_fitness
|
|
30
|
+
self.mean_fitness = mean_fitness
|
|
31
|
+
self.evaluations = evaluations
|
|
32
|
+
self.time_seconds = time_seconds
|
|
33
|
+
self.convergence_generation = convergence_generation
|
|
34
|
+
self.final_diversity = final_diversity
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
"""Convert to dictionary."""
|
|
38
|
+
return {
|
|
39
|
+
"problem": self.problem_name,
|
|
40
|
+
"optimizer": self.optimizer_name,
|
|
41
|
+
"best_fitness": self.best_fitness,
|
|
42
|
+
"mean_fitness": self.mean_fitness,
|
|
43
|
+
"evaluations": self.evaluations,
|
|
44
|
+
"time_seconds": self.time_seconds,
|
|
45
|
+
"convergence_generation": self.convergence_generation,
|
|
46
|
+
"final_diversity": self.final_diversity,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return (
|
|
51
|
+
f"BenchmarkResult({self.optimizer_name} on {self.problem_name}: "
|
|
52
|
+
f"best={self.best_fitness:.4f}, time={self.time_seconds:.2f}s)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BenchmarkSuite:
|
|
57
|
+
"""
|
|
58
|
+
Comprehensive benchmark suite for NAS optimizers.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
>>> suite = BenchmarkSuite()
|
|
62
|
+
>>> suite.add_optimizer("GA", GeneticAlgorithm, {...})
|
|
63
|
+
>>> suite.add_optimizer("RS", RandomSearch, {...})
|
|
64
|
+
>>> results = suite.run(problems, num_runs=5)
|
|
65
|
+
>>> suite.print_summary(results)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
"""Initialize benchmark suite."""
|
|
70
|
+
self.optimizers: Dict[str, tuple] = {}
|
|
71
|
+
self.results: List[BenchmarkResult] = []
|
|
72
|
+
|
|
73
|
+
def add_optimizer(self, name: str, optimizer_class: type, config: Dict[str, Any]) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Add an optimizer to benchmark.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: Optimizer name
|
|
79
|
+
optimizer_class: Optimizer class
|
|
80
|
+
config: Configuration dict
|
|
81
|
+
"""
|
|
82
|
+
self.optimizers[name] = (optimizer_class, config)
|
|
83
|
+
logger.info(f"Added optimizer: {name}")
|
|
84
|
+
|
|
85
|
+
def run_single(
|
|
86
|
+
self,
|
|
87
|
+
problem: BenchmarkProblem,
|
|
88
|
+
optimizer_name: str,
|
|
89
|
+
optimizer_class: type,
|
|
90
|
+
config: Dict[str, Any],
|
|
91
|
+
) -> BenchmarkResult:
|
|
92
|
+
"""Run single benchmark."""
|
|
93
|
+
logger.info(f"Running {optimizer_name} on {problem.name}...")
|
|
94
|
+
|
|
95
|
+
start_time = time.time()
|
|
96
|
+
|
|
97
|
+
# Create optimizer
|
|
98
|
+
optimizer = optimizer_class(search_space=problem.search_space, **config)
|
|
99
|
+
|
|
100
|
+
# Run optimization
|
|
101
|
+
try:
|
|
102
|
+
if hasattr(optimizer, "optimize"):
|
|
103
|
+
best = optimizer.optimize(problem.evaluate)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Optimizer {optimizer_name} has no optimize method")
|
|
106
|
+
|
|
107
|
+
end_time = time.time()
|
|
108
|
+
elapsed = end_time - start_time
|
|
109
|
+
|
|
110
|
+
# Collect metrics
|
|
111
|
+
best_fitness = best.fitness if best else 0.0
|
|
112
|
+
|
|
113
|
+
# Get population stats if available
|
|
114
|
+
mean_fitness = 0.0
|
|
115
|
+
final_diversity = None
|
|
116
|
+
convergence_gen = None
|
|
117
|
+
|
|
118
|
+
if hasattr(optimizer, "population"):
|
|
119
|
+
pop = optimizer.population
|
|
120
|
+
if pop.size() > 0:
|
|
121
|
+
stats = pop.get_statistics()
|
|
122
|
+
mean_fitness = stats.get("mean_fitness", 0.0)
|
|
123
|
+
final_diversity = pop.get_diversity()
|
|
124
|
+
convergence_gen = pop.generation
|
|
125
|
+
|
|
126
|
+
# Count evaluations
|
|
127
|
+
evaluations = config.get("num_generations", 0) * config.get("population_size", 1)
|
|
128
|
+
if hasattr(optimizer, "num_samples"):
|
|
129
|
+
evaluations = config.get("num_samples", 0)
|
|
130
|
+
|
|
131
|
+
result = BenchmarkResult(
|
|
132
|
+
problem_name=problem.name,
|
|
133
|
+
optimizer_name=optimizer_name,
|
|
134
|
+
best_fitness=best_fitness,
|
|
135
|
+
mean_fitness=mean_fitness,
|
|
136
|
+
evaluations=evaluations,
|
|
137
|
+
time_seconds=elapsed,
|
|
138
|
+
convergence_generation=convergence_gen,
|
|
139
|
+
final_diversity=final_diversity,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
logger.info(f"Completed: {result}")
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Benchmark failed: {e}")
|
|
147
|
+
# Return failure result
|
|
148
|
+
return BenchmarkResult(
|
|
149
|
+
problem_name=problem.name,
|
|
150
|
+
optimizer_name=optimizer_name,
|
|
151
|
+
best_fitness=0.0,
|
|
152
|
+
mean_fitness=0.0,
|
|
153
|
+
evaluations=0,
|
|
154
|
+
time_seconds=time.time() - start_time,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def run(self, problems: List[BenchmarkProblem], num_runs: int = 5) -> List[BenchmarkResult]:
|
|
158
|
+
"""
|
|
159
|
+
Run full benchmark suite.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
problems: List of benchmark problems
|
|
163
|
+
num_runs: Number of runs per optimizer-problem pair
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of benchmark results
|
|
167
|
+
"""
|
|
168
|
+
all_results = []
|
|
169
|
+
|
|
170
|
+
total_runs = len(self.optimizers) * len(problems) * num_runs
|
|
171
|
+
current_run = 0
|
|
172
|
+
|
|
173
|
+
logger.info(f"Starting benchmark: {total_runs} total runs")
|
|
174
|
+
|
|
175
|
+
for problem in problems:
|
|
176
|
+
for opt_name, (opt_class, config) in self.optimizers.items():
|
|
177
|
+
for run in range(num_runs):
|
|
178
|
+
current_run += 1
|
|
179
|
+
logger.info(
|
|
180
|
+
f"Run {current_run}/{total_runs}: "
|
|
181
|
+
f"{opt_name} on {problem.name} (run {run + 1}/{num_runs})"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
result = self.run_single(problem, opt_name, opt_class, config)
|
|
185
|
+
all_results.append(result)
|
|
186
|
+
|
|
187
|
+
self.results = all_results
|
|
188
|
+
logger.info("Benchmark complete!")
|
|
189
|
+
|
|
190
|
+
return all_results
|
|
191
|
+
|
|
192
|
+
def get_summary(self, results: Optional[List[BenchmarkResult]] = None) -> Dict[str, Any]:
|
|
193
|
+
"""
|
|
194
|
+
Get summary statistics.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
results: Results to summarize (uses self.results if None)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Summary dictionary
|
|
201
|
+
"""
|
|
202
|
+
if results is None:
|
|
203
|
+
results = self.results
|
|
204
|
+
|
|
205
|
+
if not results:
|
|
206
|
+
return {}
|
|
207
|
+
|
|
208
|
+
summary = {}
|
|
209
|
+
|
|
210
|
+
# Group by optimizer and problem
|
|
211
|
+
for result in results:
|
|
212
|
+
key = (result.optimizer_name, result.problem_name)
|
|
213
|
+
|
|
214
|
+
if key not in summary:
|
|
215
|
+
summary[key] = {
|
|
216
|
+
"optimizer": result.optimizer_name,
|
|
217
|
+
"problem": result.problem_name,
|
|
218
|
+
"best_fitnesses": [],
|
|
219
|
+
"mean_fitnesses": [],
|
|
220
|
+
"times": [],
|
|
221
|
+
"evaluations": [],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
summary[key]["best_fitnesses"].append(result.best_fitness)
|
|
225
|
+
summary[key]["mean_fitnesses"].append(result.mean_fitness)
|
|
226
|
+
summary[key]["times"].append(result.time_seconds)
|
|
227
|
+
summary[key]["evaluations"].append(result.evaluations)
|
|
228
|
+
|
|
229
|
+
# Compute statistics
|
|
230
|
+
import statistics
|
|
231
|
+
|
|
232
|
+
for key, data in summary.items():
|
|
233
|
+
data["mean_best_fitness"] = statistics.mean(data["best_fitnesses"])
|
|
234
|
+
data["std_best_fitness"] = (
|
|
235
|
+
statistics.stdev(data["best_fitnesses"]) if len(data["best_fitnesses"]) > 1 else 0.0
|
|
236
|
+
)
|
|
237
|
+
data["median_best_fitness"] = statistics.median(data["best_fitnesses"])
|
|
238
|
+
data["mean_time"] = statistics.mean(data["times"])
|
|
239
|
+
data["mean_evaluations"] = statistics.mean(data["evaluations"])
|
|
240
|
+
|
|
241
|
+
return summary
|
|
242
|
+
|
|
243
|
+
def print_summary(self, results: Optional[List[BenchmarkResult]] = None) -> None:
|
|
244
|
+
"""Print summary of results."""
|
|
245
|
+
summary = self.get_summary(results)
|
|
246
|
+
|
|
247
|
+
if not summary:
|
|
248
|
+
print("No results to summarize")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
print("\n" + "=" * 80)
|
|
252
|
+
print("BENCHMARK SUMMARY")
|
|
253
|
+
print("=" * 80)
|
|
254
|
+
|
|
255
|
+
# Group by problem
|
|
256
|
+
problems = {data["problem"] for data in summary.values()}
|
|
257
|
+
|
|
258
|
+
for problem in sorted(problems):
|
|
259
|
+
print(f"\n{problem}")
|
|
260
|
+
print("-" * 80)
|
|
261
|
+
print(f"{'Optimizer':<20} {'Best (Mean±Std)':<25} {'Median':<10} {'Time (s)':<12}")
|
|
262
|
+
print("-" * 80)
|
|
263
|
+
|
|
264
|
+
problem_results = [
|
|
265
|
+
(data["optimizer"], data)
|
|
266
|
+
for key, data in summary.items()
|
|
267
|
+
if data["problem"] == problem
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
# Sort by mean best fitness (descending)
|
|
271
|
+
problem_results.sort(key=lambda x: x[1]["mean_best_fitness"], reverse=True)
|
|
272
|
+
|
|
273
|
+
for opt_name, data in problem_results:
|
|
274
|
+
mean_best = data["mean_best_fitness"]
|
|
275
|
+
std_best = data["std_best_fitness"]
|
|
276
|
+
median_best = data["median_best_fitness"]
|
|
277
|
+
mean_time = data["mean_time"]
|
|
278
|
+
|
|
279
|
+
print(
|
|
280
|
+
f"{opt_name:<20} "
|
|
281
|
+
f"{mean_best:.4f}±{std_best:.4f} "
|
|
282
|
+
f"{median_best:.4f} "
|
|
283
|
+
f"{mean_time:>10.2f}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
print("\n" + "=" * 80)
|
|
287
|
+
|
|
288
|
+
def get_winner(
|
|
289
|
+
self, problem_name: str, results: Optional[List[BenchmarkResult]] = None
|
|
290
|
+
) -> Optional[str]:
|
|
291
|
+
"""Get best optimizer for a problem."""
|
|
292
|
+
summary = self.get_summary(results)
|
|
293
|
+
|
|
294
|
+
problem_results = [
|
|
295
|
+
(data["optimizer"], data["mean_best_fitness"])
|
|
296
|
+
for key, data in summary.items()
|
|
297
|
+
if data["problem"] == problem_name
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
if not problem_results:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
problem_results.sort(key=lambda x: x[1], reverse=True)
|
|
304
|
+
return problem_results[0][0]
|
|
305
|
+
|
|
306
|
+
def export_results(self, filename: str) -> None:
|
|
307
|
+
"""Export results to JSON."""
|
|
308
|
+
import json
|
|
309
|
+
|
|
310
|
+
data = {
|
|
311
|
+
"results": [r.to_dict() for r in self.results],
|
|
312
|
+
"summary": {str(k): v for k, v in self.get_summary().items()},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
with open(filename, "w") as f:
|
|
316
|
+
json.dump(data, f, indent=2)
|
|
317
|
+
|
|
318
|
+
logger.info(f"Results exported to {filename}")
|
morphml/cli/__init__.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Enhanced experiment management CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides interactive commands for managing NAS experiments.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
morphml experiment create --name my-experiment
|
|
7
|
+
morphml experiment list
|
|
8
|
+
morphml experiment show exp_123
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.progress import Progress
|
|
16
|
+
from rich.prompt import Confirm, Prompt
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from morphml.logging_config import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
def experiment():
|
|
27
|
+
"""Manage NAS experiments."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@experiment.command()
|
|
32
|
+
@click.option("--name", "-n", help="Experiment name")
|
|
33
|
+
@click.option("--optimizer", "-o", default="genetic", help="Optimizer type")
|
|
34
|
+
@click.option("--budget", "-b", default=500, type=int, help="Evaluation budget")
|
|
35
|
+
@click.option("--interactive", "-i", is_flag=True, help="Interactive mode")
|
|
36
|
+
def create(name: Optional[str], optimizer: str, budget: int, interactive: bool):
|
|
37
|
+
"""
|
|
38
|
+
Create a new experiment.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
morphml experiment create --name cifar10-search
|
|
42
|
+
morphml experiment create -i # Interactive mode
|
|
43
|
+
"""
|
|
44
|
+
console.print("[bold cyan]Create New Experiment[/bold cyan]\n")
|
|
45
|
+
|
|
46
|
+
# Interactive mode
|
|
47
|
+
if interactive or not name:
|
|
48
|
+
name = Prompt.ask("Experiment name", default="my-experiment")
|
|
49
|
+
optimizer = Prompt.ask(
|
|
50
|
+
"Optimizer",
|
|
51
|
+
choices=["genetic", "random", "hillclimbing", "bayesian"],
|
|
52
|
+
default="genetic",
|
|
53
|
+
)
|
|
54
|
+
budget = int(Prompt.ask("Evaluation budget", default="500"))
|
|
55
|
+
|
|
56
|
+
# Define search space interactively
|
|
57
|
+
console.print("\n[bold]Define Search Space[/bold]")
|
|
58
|
+
|
|
59
|
+
layers = []
|
|
60
|
+
|
|
61
|
+
# Input layer
|
|
62
|
+
console.print("Input layer:")
|
|
63
|
+
input_shape = Prompt.ask(" Input shape (e.g., 3,32,32)", default="3,32,32")
|
|
64
|
+
shape = tuple(map(int, input_shape.split(",")))
|
|
65
|
+
layers.append({"type": "input", "shape": shape})
|
|
66
|
+
|
|
67
|
+
# Add layers
|
|
68
|
+
while True:
|
|
69
|
+
add_layer = Confirm.ask("Add a layer?", default=True)
|
|
70
|
+
if not add_layer:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
layer_type = Prompt.ask(
|
|
74
|
+
" Layer type",
|
|
75
|
+
choices=["conv2d", "dense", "maxpool", "flatten", "relu", "dropout"],
|
|
76
|
+
default="conv2d",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
layer_config = {"type": layer_type}
|
|
80
|
+
|
|
81
|
+
if layer_type == "conv2d":
|
|
82
|
+
filters = int(Prompt.ask(" Filters", default="64"))
|
|
83
|
+
kernel = int(Prompt.ask(" Kernel size", default="3"))
|
|
84
|
+
layer_config.update({"filters": filters, "kernel_size": kernel})
|
|
85
|
+
|
|
86
|
+
elif layer_type == "dense":
|
|
87
|
+
units = int(Prompt.ask(" Units", default="128"))
|
|
88
|
+
layer_config.update({"units": units})
|
|
89
|
+
|
|
90
|
+
elif layer_type == "maxpool":
|
|
91
|
+
pool_size = int(Prompt.ask(" Pool size", default="2"))
|
|
92
|
+
layer_config.update({"pool_size": pool_size})
|
|
93
|
+
|
|
94
|
+
elif layer_type == "dropout":
|
|
95
|
+
rate = float(Prompt.ask(" Dropout rate", default="0.5"))
|
|
96
|
+
layer_config.update({"rate": rate})
|
|
97
|
+
|
|
98
|
+
layers.append(layer_config)
|
|
99
|
+
|
|
100
|
+
# Create experiment config
|
|
101
|
+
|
|
102
|
+
# Display summary
|
|
103
|
+
console.print("\n[bold green]Experiment Configuration:[/bold green]")
|
|
104
|
+
console.print(f" Name: {name}")
|
|
105
|
+
console.print(f" Optimizer: {optimizer}")
|
|
106
|
+
console.print(f" Budget: {budget}")
|
|
107
|
+
console.print(f" Layers: {len(layers)}")
|
|
108
|
+
|
|
109
|
+
# Confirm
|
|
110
|
+
if Confirm.ask("\nCreate experiment?", default=True):
|
|
111
|
+
# TODO: Actually create experiment via API or directly
|
|
112
|
+
console.print(f"\n[green]✓ Created experiment: {name}[/green]")
|
|
113
|
+
console.print(f" Run with: morphml experiment start {name}")
|
|
114
|
+
else:
|
|
115
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@experiment.command()
|
|
119
|
+
@click.option("--status", "-s", help="Filter by status")
|
|
120
|
+
@click.option("--limit", "-l", default=20, help="Maximum results")
|
|
121
|
+
def list(status: Optional[str], limit: int):
|
|
122
|
+
"""
|
|
123
|
+
List all experiments.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
morphml experiment list
|
|
127
|
+
morphml experiment list --status running
|
|
128
|
+
"""
|
|
129
|
+
console.print("[bold cyan]Experiments[/bold cyan]\n")
|
|
130
|
+
|
|
131
|
+
# TODO: Fetch from API
|
|
132
|
+
# For now, show example data
|
|
133
|
+
experiments = [
|
|
134
|
+
{
|
|
135
|
+
"id": "exp_abc123",
|
|
136
|
+
"name": "CIFAR-10 Search",
|
|
137
|
+
"status": "running",
|
|
138
|
+
"best_accuracy": 0.9234,
|
|
139
|
+
"generation": 25,
|
|
140
|
+
"total_generations": 50,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "exp_def456",
|
|
144
|
+
"name": "ImageNet Transfer",
|
|
145
|
+
"status": "completed",
|
|
146
|
+
"best_accuracy": 0.8567,
|
|
147
|
+
"generation": 100,
|
|
148
|
+
"total_generations": 100,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "exp_ghi789",
|
|
152
|
+
"name": "Quick Test",
|
|
153
|
+
"status": "failed",
|
|
154
|
+
"best_accuracy": None,
|
|
155
|
+
"generation": 5,
|
|
156
|
+
"total_generations": 20,
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Filter by status
|
|
161
|
+
if status:
|
|
162
|
+
experiments = [e for e in experiments if e["status"] == status]
|
|
163
|
+
|
|
164
|
+
# Limit results
|
|
165
|
+
experiments = experiments[:limit]
|
|
166
|
+
|
|
167
|
+
# Create table
|
|
168
|
+
table = Table(title=f"Experiments ({len(experiments)} total)")
|
|
169
|
+
table.add_column("ID", style="cyan")
|
|
170
|
+
table.add_column("Name", style="white")
|
|
171
|
+
table.add_column("Status", style="yellow")
|
|
172
|
+
table.add_column("Progress", style="blue")
|
|
173
|
+
table.add_column("Best Accuracy", style="green")
|
|
174
|
+
|
|
175
|
+
for exp in experiments:
|
|
176
|
+
# Status color
|
|
177
|
+
status_color = {
|
|
178
|
+
"running": "yellow",
|
|
179
|
+
"completed": "green",
|
|
180
|
+
"failed": "red",
|
|
181
|
+
"pending": "blue",
|
|
182
|
+
}.get(exp["status"], "white")
|
|
183
|
+
|
|
184
|
+
# Progress
|
|
185
|
+
progress = f"{exp['generation']}/{exp['total_generations']}"
|
|
186
|
+
|
|
187
|
+
# Accuracy
|
|
188
|
+
accuracy = f"{exp['best_accuracy']:.4f}" if exp["best_accuracy"] else "-"
|
|
189
|
+
|
|
190
|
+
table.add_row(
|
|
191
|
+
exp["id"],
|
|
192
|
+
exp["name"],
|
|
193
|
+
f"[{status_color}]{exp['status']}[/{status_color}]",
|
|
194
|
+
progress,
|
|
195
|
+
accuracy,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
console.print(table)
|
|
199
|
+
|
|
200
|
+
console.print(f"\n[dim]Showing {len(experiments)} of {len(experiments)} experiments[/dim]")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@experiment.command()
|
|
204
|
+
@click.argument("experiment_id")
|
|
205
|
+
def show(experiment_id: str):
|
|
206
|
+
"""
|
|
207
|
+
Show detailed experiment information.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
morphml experiment show exp_abc123
|
|
211
|
+
"""
|
|
212
|
+
console.print(f"[bold cyan]Experiment: {experiment_id}[/bold cyan]\n")
|
|
213
|
+
|
|
214
|
+
# TODO: Fetch from API
|
|
215
|
+
# Example data
|
|
216
|
+
exp = {
|
|
217
|
+
"id": experiment_id,
|
|
218
|
+
"name": "CIFAR-10 Search",
|
|
219
|
+
"status": "running",
|
|
220
|
+
"optimizer": "genetic",
|
|
221
|
+
"created_at": "2024-11-11T05:00:00Z",
|
|
222
|
+
"started_at": "2024-11-11T05:01:00Z",
|
|
223
|
+
"best_accuracy": 0.9234,
|
|
224
|
+
"generation": 25,
|
|
225
|
+
"total_generations": 50,
|
|
226
|
+
"population_size": 20,
|
|
227
|
+
"best_architecture": {"parameters": 1234567, "depth": 8, "nodes": 12},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Display info
|
|
231
|
+
console.print(f"[bold]Name:[/bold] {exp['name']}")
|
|
232
|
+
console.print(f"[bold]Status:[/bold] {exp['status']}")
|
|
233
|
+
console.print(f"[bold]Optimizer:[/bold] {exp['optimizer']}")
|
|
234
|
+
console.print(f"[bold]Created:[/bold] {exp['created_at']}")
|
|
235
|
+
console.print(
|
|
236
|
+
f"[bold]Progress:[/bold] {exp['generation']}/{exp['total_generations']} generations"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
console.print("\n[bold green]Best Architecture:[/bold green]")
|
|
240
|
+
console.print(f" Accuracy: {exp['best_accuracy']:.4f}")
|
|
241
|
+
console.print(f" Parameters: {exp['best_architecture']['parameters']:,}")
|
|
242
|
+
console.print(f" Depth: {exp['best_architecture']['depth']}")
|
|
243
|
+
console.print(f" Nodes: {exp['best_architecture']['nodes']}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@experiment.command()
|
|
247
|
+
@click.argument("experiment_id")
|
|
248
|
+
def start(experiment_id: str):
|
|
249
|
+
"""
|
|
250
|
+
Start an experiment.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
morphml experiment start exp_abc123
|
|
254
|
+
"""
|
|
255
|
+
console.print(f"[bold cyan]Starting experiment: {experiment_id}[/bold cyan]\n")
|
|
256
|
+
|
|
257
|
+
with Progress() as progress:
|
|
258
|
+
task = progress.add_task("[cyan]Initializing...", total=100)
|
|
259
|
+
|
|
260
|
+
# Simulate startup
|
|
261
|
+
import time
|
|
262
|
+
|
|
263
|
+
for _i in range(100):
|
|
264
|
+
time.sleep(0.01)
|
|
265
|
+
progress.update(task, advance=1)
|
|
266
|
+
|
|
267
|
+
console.print("[green]✓ Experiment started[/green]")
|
|
268
|
+
console.print(f" Monitor with: morphml experiment show {experiment_id}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@experiment.command()
|
|
272
|
+
@click.argument("experiment_id")
|
|
273
|
+
def stop(experiment_id: str):
|
|
274
|
+
"""
|
|
275
|
+
Stop a running experiment.
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
morphml experiment stop exp_abc123
|
|
279
|
+
"""
|
|
280
|
+
if Confirm.ask(f"Stop experiment {experiment_id}?"):
|
|
281
|
+
console.print(f"[yellow]Stopping experiment: {experiment_id}[/yellow]")
|
|
282
|
+
# TODO: Call API to stop
|
|
283
|
+
console.print("[green]✓ Experiment stopped[/green]")
|
|
284
|
+
else:
|
|
285
|
+
console.print("[dim]Cancelled[/dim]")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@experiment.command()
|
|
289
|
+
@click.argument("experiment_id")
|
|
290
|
+
@click.option("--force", "-f", is_flag=True, help="Force delete without confirmation")
|
|
291
|
+
def delete(experiment_id: str, force: bool):
|
|
292
|
+
"""
|
|
293
|
+
Delete an experiment.
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
morphml experiment delete exp_abc123
|
|
297
|
+
morphml experiment delete exp_abc123 --force
|
|
298
|
+
"""
|
|
299
|
+
if force or Confirm.ask(f"[red]Delete experiment {experiment_id}?[/red]"):
|
|
300
|
+
console.print(f"[red]Deleting experiment: {experiment_id}[/red]")
|
|
301
|
+
# TODO: Call API to delete
|
|
302
|
+
console.print("[green]✓ Experiment deleted[/green]")
|
|
303
|
+
else:
|
|
304
|
+
console.print("[dim]Cancelled[/dim]")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@experiment.command()
|
|
308
|
+
@click.argument("experiment_id")
|
|
309
|
+
@click.option("--format", "-f", type=click.Choice(["pytorch", "keras", "both"]), default="pytorch")
|
|
310
|
+
@click.option("--output", "-o", help="Output file path")
|
|
311
|
+
def export(experiment_id: str, format: str, output: Optional[str]):
|
|
312
|
+
"""
|
|
313
|
+
Export best architecture from experiment.
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
morphml experiment export exp_abc123 --format pytorch
|
|
317
|
+
morphml experiment export exp_abc123 --format keras -o model.py
|
|
318
|
+
"""
|
|
319
|
+
console.print(f"[bold cyan]Exporting architecture from: {experiment_id}[/bold cyan]\n")
|
|
320
|
+
|
|
321
|
+
# TODO: Fetch best architecture and export
|
|
322
|
+
|
|
323
|
+
if not output:
|
|
324
|
+
output = f"{experiment_id}_model.py"
|
|
325
|
+
|
|
326
|
+
console.print(f"[bold]Format:[/bold] {format}")
|
|
327
|
+
console.print(f"[bold]Output:[/bold] {output}")
|
|
328
|
+
|
|
329
|
+
console.print(f"\n[green]✓ Exported to {output}[/green]")
|