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,308 @@
|
|
|
1
|
+
"""NSGA-II - Non-dominated Sorting Genetic Algorithm II.
|
|
2
|
+
|
|
3
|
+
Multi-objective optimization using Pareto dominance and crowding distance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import random
|
|
7
|
+
from typing import Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from morphml.core.dsl.search_space import SearchSpace
|
|
10
|
+
from morphml.core.graph import GraphMutator, ModelGraph
|
|
11
|
+
from morphml.core.search import Individual, Population
|
|
12
|
+
from morphml.exceptions import OptimizerError
|
|
13
|
+
from morphml.logging_config import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NSGA2:
|
|
19
|
+
"""
|
|
20
|
+
NSGA-II for multi-objective NAS.
|
|
21
|
+
|
|
22
|
+
Optimizes multiple objectives simultaneously using:
|
|
23
|
+
- Non-dominated sorting
|
|
24
|
+
- Crowding distance
|
|
25
|
+
- Pareto front preservation
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
search_space: SearchSpace,
|
|
31
|
+
objectives: List[str],
|
|
32
|
+
population_size: int = 100,
|
|
33
|
+
num_generations: int = 100,
|
|
34
|
+
mutation_rate: float = 0.2,
|
|
35
|
+
crossover_rate: float = 0.8,
|
|
36
|
+
**kwargs,
|
|
37
|
+
):
|
|
38
|
+
"""Initialize NSGA-II."""
|
|
39
|
+
self.search_space = search_space
|
|
40
|
+
self.objectives = objectives
|
|
41
|
+
self.population_size = population_size
|
|
42
|
+
self.num_generations = num_generations
|
|
43
|
+
self.mutation_rate = mutation_rate
|
|
44
|
+
self.crossover_rate = crossover_rate
|
|
45
|
+
|
|
46
|
+
self.population = Population(max_size=population_size * 2, elitism=0)
|
|
47
|
+
self.mutator = GraphMutator()
|
|
48
|
+
self.pareto_front: List[Individual] = []
|
|
49
|
+
self.history: List[dict] = []
|
|
50
|
+
|
|
51
|
+
logger.info(f"Created NSGA2: objectives={objectives}, pop={population_size}")
|
|
52
|
+
|
|
53
|
+
def dominates(self, ind1: Individual, ind2: Individual) -> bool:
|
|
54
|
+
"""Check if ind1 dominates ind2."""
|
|
55
|
+
better_in_any = False
|
|
56
|
+
|
|
57
|
+
for obj in self.objectives:
|
|
58
|
+
val1 = ind1.get_metric(obj, 0.0)
|
|
59
|
+
val2 = ind2.get_metric(obj, 0.0)
|
|
60
|
+
|
|
61
|
+
if val1 < val2:
|
|
62
|
+
return False # Worse in this objective
|
|
63
|
+
if val1 > val2:
|
|
64
|
+
better_in_any = True
|
|
65
|
+
|
|
66
|
+
return better_in_any
|
|
67
|
+
|
|
68
|
+
def fast_non_dominated_sort(self, individuals: List[Individual]) -> List[List[Individual]]:
|
|
69
|
+
"""Perform fast non-dominated sorting."""
|
|
70
|
+
fronts: List[List[Individual]] = [[]]
|
|
71
|
+
domination_count: Dict[str, int] = {}
|
|
72
|
+
dominated_solutions: Dict[str, List[Individual]] = {}
|
|
73
|
+
|
|
74
|
+
# Initialize
|
|
75
|
+
for ind in individuals:
|
|
76
|
+
ind_id = ind.id
|
|
77
|
+
domination_count[ind_id] = 0
|
|
78
|
+
dominated_solutions[ind_id] = []
|
|
79
|
+
|
|
80
|
+
# Find domination relationships
|
|
81
|
+
for i, ind1 in enumerate(individuals):
|
|
82
|
+
for ind2 in individuals[i + 1 :]:
|
|
83
|
+
if self.dominates(ind1, ind2):
|
|
84
|
+
dominated_solutions[ind1.id].append(ind2)
|
|
85
|
+
domination_count[ind2.id] += 1
|
|
86
|
+
elif self.dominates(ind2, ind1):
|
|
87
|
+
dominated_solutions[ind2.id].append(ind1)
|
|
88
|
+
domination_count[ind1.id] += 1
|
|
89
|
+
|
|
90
|
+
# If not dominated by anyone, belongs to first front
|
|
91
|
+
if domination_count[ind1.id] == 0:
|
|
92
|
+
fronts[0].append(ind1)
|
|
93
|
+
|
|
94
|
+
# Build subsequent fronts
|
|
95
|
+
current_front = 0
|
|
96
|
+
while fronts[current_front]:
|
|
97
|
+
next_front = []
|
|
98
|
+
for ind in fronts[current_front]:
|
|
99
|
+
for dominated in dominated_solutions[ind.id]:
|
|
100
|
+
domination_count[dominated.id] -= 1
|
|
101
|
+
if domination_count[dominated.id] == 0:
|
|
102
|
+
next_front.append(dominated)
|
|
103
|
+
|
|
104
|
+
current_front += 1
|
|
105
|
+
if next_front:
|
|
106
|
+
fronts.append(next_front)
|
|
107
|
+
else:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
return [f for f in fronts if f] # Remove empty fronts
|
|
111
|
+
|
|
112
|
+
def calculate_crowding_distance(self, front: List[Individual]) -> Dict[str, float]:
|
|
113
|
+
"""Calculate crowding distance for individuals in front."""
|
|
114
|
+
distances: Dict[str, float] = {ind.id: 0.0 for ind in front}
|
|
115
|
+
|
|
116
|
+
if len(front) <= 2:
|
|
117
|
+
for ind in front:
|
|
118
|
+
distances[ind.id] = float("inf")
|
|
119
|
+
return distances
|
|
120
|
+
|
|
121
|
+
# For each objective
|
|
122
|
+
for obj in self.objectives:
|
|
123
|
+
# Sort by objective value
|
|
124
|
+
sorted_front = sorted(front, key=lambda x: x.get_metric(obj, 0.0))
|
|
125
|
+
|
|
126
|
+
# Boundary points get infinite distance
|
|
127
|
+
distances[sorted_front[0].id] = float("inf")
|
|
128
|
+
distances[sorted_front[-1].id] = float("inf")
|
|
129
|
+
|
|
130
|
+
# Get objective range
|
|
131
|
+
obj_min = sorted_front[0].get_metric(obj, 0.0)
|
|
132
|
+
obj_max = sorted_front[-1].get_metric(obj, 0.0)
|
|
133
|
+
obj_range = obj_max - obj_min
|
|
134
|
+
|
|
135
|
+
if obj_range == 0:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Calculate crowding distance
|
|
139
|
+
for i in range(1, len(sorted_front) - 1):
|
|
140
|
+
prev_val = sorted_front[i - 1].get_metric(obj, 0.0)
|
|
141
|
+
next_val = sorted_front[i + 1].get_metric(obj, 0.0)
|
|
142
|
+
distances[sorted_front[i].id] += (next_val - prev_val) / obj_range
|
|
143
|
+
|
|
144
|
+
return distances
|
|
145
|
+
|
|
146
|
+
def environmental_selection(self, individuals: List[Individual]) -> List[Individual]:
|
|
147
|
+
"""Select next generation using NSGA-II selection."""
|
|
148
|
+
# Non-dominated sorting
|
|
149
|
+
fronts = self.fast_non_dominated_sort(individuals)
|
|
150
|
+
|
|
151
|
+
# Select individuals
|
|
152
|
+
selected = []
|
|
153
|
+
for front in fronts:
|
|
154
|
+
if len(selected) + len(front) <= self.population_size:
|
|
155
|
+
selected.extend(front)
|
|
156
|
+
else:
|
|
157
|
+
# Calculate crowding distance for this front
|
|
158
|
+
distances = self.calculate_crowding_distance(front)
|
|
159
|
+
|
|
160
|
+
# Sort by crowding distance (descending)
|
|
161
|
+
front_sorted = sorted(front, key=lambda x: distances[x.id], reverse=True)
|
|
162
|
+
|
|
163
|
+
# Add individuals until population is full
|
|
164
|
+
remaining = self.population_size - len(selected)
|
|
165
|
+
selected.extend(front_sorted[:remaining])
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
return selected
|
|
169
|
+
|
|
170
|
+
def initialize_population(self) -> None:
|
|
171
|
+
"""Initialize population."""
|
|
172
|
+
logger.info(f"Initializing population of size {self.population_size}")
|
|
173
|
+
|
|
174
|
+
for i in range(self.population_size):
|
|
175
|
+
try:
|
|
176
|
+
graph = self.search_space.sample()
|
|
177
|
+
individual = Individual(graph)
|
|
178
|
+
self.population.add(individual)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Failed to sample individual {i}: {e}")
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
def evaluate_population(self, evaluator: Callable[[ModelGraph], Dict[str, float]]) -> None:
|
|
184
|
+
"""Evaluate population on multiple objectives."""
|
|
185
|
+
unevaluated = self.population.get_unevaluated()
|
|
186
|
+
|
|
187
|
+
if not unevaluated:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
logger.info(f"Evaluating {len(unevaluated)} individuals")
|
|
191
|
+
|
|
192
|
+
for ind in unevaluated:
|
|
193
|
+
try:
|
|
194
|
+
# Evaluator returns dict of objective values
|
|
195
|
+
metrics = evaluator(ind.graph)
|
|
196
|
+
|
|
197
|
+
# Set fitness as primary objective
|
|
198
|
+
if self.objectives:
|
|
199
|
+
primary_fitness = metrics.get(self.objectives[0], 0.0)
|
|
200
|
+
ind.set_fitness(primary_fitness, **metrics)
|
|
201
|
+
else:
|
|
202
|
+
ind.set_fitness(sum(metrics.values()) / len(metrics), **metrics)
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Evaluation failed: {e}")
|
|
206
|
+
ind.set_fitness(0.0)
|
|
207
|
+
|
|
208
|
+
def generate_offspring(self, parents: List[Individual]) -> List[Individual]:
|
|
209
|
+
"""Generate offspring through crossover and mutation."""
|
|
210
|
+
offspring = []
|
|
211
|
+
|
|
212
|
+
while len(offspring) < len(parents):
|
|
213
|
+
# Select parents
|
|
214
|
+
parent1, parent2 = random.sample(parents, 2)
|
|
215
|
+
|
|
216
|
+
# Crossover
|
|
217
|
+
if random.random() < self.crossover_rate:
|
|
218
|
+
child = parent1.clone(keep_fitness=False)
|
|
219
|
+
child.parent_ids = [parent1.id, parent2.id]
|
|
220
|
+
else:
|
|
221
|
+
child = random.choice([parent1, parent2]).clone(keep_fitness=False)
|
|
222
|
+
|
|
223
|
+
# Mutation
|
|
224
|
+
if random.random() < self.mutation_rate:
|
|
225
|
+
mutated_graph = self.mutator.mutate(child.graph)
|
|
226
|
+
child = Individual(mutated_graph, parent_ids=child.parent_ids)
|
|
227
|
+
|
|
228
|
+
offspring.append(child)
|
|
229
|
+
|
|
230
|
+
return offspring
|
|
231
|
+
|
|
232
|
+
def optimize(
|
|
233
|
+
self,
|
|
234
|
+
evaluator: Callable[[ModelGraph], Dict[str, float]],
|
|
235
|
+
callback: Optional[Callable[[int, List[Individual]], None]] = None,
|
|
236
|
+
) -> List[Individual]:
|
|
237
|
+
"""Run NSGA-II optimization."""
|
|
238
|
+
try:
|
|
239
|
+
# Initialize
|
|
240
|
+
self.initialize_population()
|
|
241
|
+
self.evaluate_population(evaluator)
|
|
242
|
+
|
|
243
|
+
# Evolution loop
|
|
244
|
+
for gen in range(self.num_generations):
|
|
245
|
+
logger.info(f"Generation {gen + 1}/{self.num_generations}")
|
|
246
|
+
|
|
247
|
+
# Generate offspring
|
|
248
|
+
parents = list(self.population.individuals)
|
|
249
|
+
offspring = self.generate_offspring(parents)
|
|
250
|
+
|
|
251
|
+
# Evaluate offspring
|
|
252
|
+
self.population.add_many(offspring)
|
|
253
|
+
self.evaluate_population(evaluator)
|
|
254
|
+
|
|
255
|
+
# Environmental selection
|
|
256
|
+
combined = list(self.population.individuals)
|
|
257
|
+
selected = self.environmental_selection(combined)
|
|
258
|
+
|
|
259
|
+
# Update population
|
|
260
|
+
self.population.clear()
|
|
261
|
+
self.population.add_many(selected)
|
|
262
|
+
|
|
263
|
+
# Update Pareto front
|
|
264
|
+
fronts = self.fast_non_dominated_sort(selected)
|
|
265
|
+
self.pareto_front = fronts[0] if fronts else []
|
|
266
|
+
|
|
267
|
+
# Record stats
|
|
268
|
+
stats = {
|
|
269
|
+
"generation": gen + 1,
|
|
270
|
+
"pareto_size": len(self.pareto_front),
|
|
271
|
+
"population_size": len(selected),
|
|
272
|
+
}
|
|
273
|
+
self.history.append(stats)
|
|
274
|
+
|
|
275
|
+
logger.info(f"Gen {gen + 1}: Pareto front size = {len(self.pareto_front)}")
|
|
276
|
+
|
|
277
|
+
# Callback
|
|
278
|
+
if callback:
|
|
279
|
+
callback(gen + 1, self.pareto_front)
|
|
280
|
+
|
|
281
|
+
logger.info(f"NSGA-II complete: Pareto front size = {len(self.pareto_front)}")
|
|
282
|
+
return self.pareto_front
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f"NSGA-II optimization failed: {e}")
|
|
286
|
+
raise OptimizerError(f"NSGA-II optimization failed: {e}") from e
|
|
287
|
+
|
|
288
|
+
def get_pareto_front(self) -> List[Individual]:
|
|
289
|
+
"""Get current Pareto front."""
|
|
290
|
+
return self.pareto_front
|
|
291
|
+
|
|
292
|
+
def get_history(self) -> List[dict]:
|
|
293
|
+
"""Get optimization history."""
|
|
294
|
+
return self.history
|
|
295
|
+
|
|
296
|
+
def reset(self) -> None:
|
|
297
|
+
"""Reset optimizer."""
|
|
298
|
+
self.population.clear()
|
|
299
|
+
self.pareto_front.clear()
|
|
300
|
+
self.history.clear()
|
|
301
|
+
logger.info("NSGA-II reset")
|
|
302
|
+
|
|
303
|
+
def __repr__(self) -> str:
|
|
304
|
+
return (
|
|
305
|
+
f"NSGA2(objectives={self.objectives}, "
|
|
306
|
+
f"pop={self.population_size}, "
|
|
307
|
+
f"pareto_size={len(self.pareto_front)})"
|
|
308
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Random Search optimizer - baseline for NAS.
|
|
2
|
+
|
|
3
|
+
Simplest possible search strategy: randomly sample architectures
|
|
4
|
+
and return the best one. Serves as a baseline for comparison.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from morphml.optimizers import RandomSearch
|
|
8
|
+
>>> from morphml.core.dsl import create_cnn_space
|
|
9
|
+
>>>
|
|
10
|
+
>>> space = create_cnn_space(num_classes=10)
|
|
11
|
+
>>> rs = RandomSearch(search_space=space, num_samples=100)
|
|
12
|
+
>>> best = rs.optimize(evaluator=my_evaluator)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Callable, List, Optional
|
|
16
|
+
|
|
17
|
+
from morphml.core.dsl.search_space import SearchSpace
|
|
18
|
+
from morphml.core.graph import ModelGraph
|
|
19
|
+
from morphml.core.search import Individual
|
|
20
|
+
from morphml.exceptions import OptimizerError
|
|
21
|
+
from morphml.logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RandomSearch:
|
|
27
|
+
"""
|
|
28
|
+
Random Search optimizer - baseline for Neural Architecture Search.
|
|
29
|
+
|
|
30
|
+
Randomly samples N architectures from the search space, evaluates
|
|
31
|
+
them all, and returns the best one. Despite its simplicity, often
|
|
32
|
+
surprisingly effective and serves as an important baseline.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
search_space: SearchSpace to sample from
|
|
36
|
+
num_samples: Number of architectures to evaluate
|
|
37
|
+
evaluated: List of evaluated individuals
|
|
38
|
+
best_individual: Best architecture found
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> rs = RandomSearch(
|
|
42
|
+
... search_space=space,
|
|
43
|
+
... num_samples=100,
|
|
44
|
+
... allow_duplicates=False
|
|
45
|
+
... )
|
|
46
|
+
>>> best = rs.optimize(evaluator=evaluate_func)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
search_space: SearchSpace,
|
|
52
|
+
num_samples: int = 100,
|
|
53
|
+
allow_duplicates: bool = False,
|
|
54
|
+
**kwargs,
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Initialize Random Search optimizer.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
search_space: SearchSpace to sample architectures from
|
|
61
|
+
num_samples: Number of architectures to sample and evaluate
|
|
62
|
+
allow_duplicates: Whether to allow duplicate architectures
|
|
63
|
+
**kwargs: Additional configuration (for compatibility)
|
|
64
|
+
"""
|
|
65
|
+
self.search_space = search_space
|
|
66
|
+
self.num_samples = num_samples
|
|
67
|
+
self.allow_duplicates = allow_duplicates
|
|
68
|
+
|
|
69
|
+
self.evaluated: List[Individual] = []
|
|
70
|
+
self.best_individual: Optional[Individual] = None
|
|
71
|
+
|
|
72
|
+
logger.info(f"Created RandomSearch: num_samples={num_samples}")
|
|
73
|
+
|
|
74
|
+
def optimize(self, evaluator: Callable[[ModelGraph], float]) -> Individual:
|
|
75
|
+
"""
|
|
76
|
+
Run random search optimization.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
evaluator: Function to evaluate fitness of architectures
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Best individual found
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
OptimizerError: If optimization fails
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> def evaluate(graph):
|
|
89
|
+
... return accuracy_score
|
|
90
|
+
>>> best = rs.optimize(evaluate)
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
logger.info(f"Starting random search with {self.num_samples} samples")
|
|
94
|
+
|
|
95
|
+
seen_hashes = set()
|
|
96
|
+
sampled = 0
|
|
97
|
+
|
|
98
|
+
while sampled < self.num_samples:
|
|
99
|
+
try:
|
|
100
|
+
# Sample architecture
|
|
101
|
+
graph = self.search_space.sample()
|
|
102
|
+
|
|
103
|
+
# Check for duplicates
|
|
104
|
+
if not self.allow_duplicates:
|
|
105
|
+
graph_hash = graph.hash()
|
|
106
|
+
if graph_hash in seen_hashes:
|
|
107
|
+
logger.debug("Skipping duplicate architecture")
|
|
108
|
+
continue
|
|
109
|
+
seen_hashes.add(graph_hash)
|
|
110
|
+
|
|
111
|
+
# Create individual and evaluate
|
|
112
|
+
individual = Individual(graph)
|
|
113
|
+
fitness = evaluator(graph)
|
|
114
|
+
individual.set_fitness(fitness)
|
|
115
|
+
|
|
116
|
+
self.evaluated.append(individual)
|
|
117
|
+
sampled += 1
|
|
118
|
+
|
|
119
|
+
# Track best
|
|
120
|
+
if self.best_individual is None or fitness > self.best_individual.fitness:
|
|
121
|
+
self.best_individual = individual
|
|
122
|
+
logger.info(f"New best: {fitness:.4f} (sample {sampled})")
|
|
123
|
+
|
|
124
|
+
if sampled % 10 == 0:
|
|
125
|
+
logger.debug(f"Evaluated {sampled}/{self.num_samples} architectures")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"Sample failed: {e}")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Final results
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Random search complete: " f"Best fitness = {self.best_individual.fitness:.4f}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return self.best_individual
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(f"Random search failed: {e}")
|
|
140
|
+
raise OptimizerError(f"Random search optimization failed: {e}") from e
|
|
141
|
+
|
|
142
|
+
def get_all_evaluated(self) -> List[Individual]:
|
|
143
|
+
"""
|
|
144
|
+
Get all evaluated individuals.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of all evaluated individuals
|
|
148
|
+
"""
|
|
149
|
+
return self.evaluated
|
|
150
|
+
|
|
151
|
+
def get_best_n(self, n: int = 10) -> List[Individual]:
|
|
152
|
+
"""
|
|
153
|
+
Get top N individuals.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
n: Number of individuals to return
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of best individuals sorted by fitness
|
|
160
|
+
"""
|
|
161
|
+
sorted_inds = sorted(self.evaluated, key=lambda x: x.fitness or 0, reverse=True)
|
|
162
|
+
return sorted_inds[:n]
|
|
163
|
+
|
|
164
|
+
def reset(self) -> None:
|
|
165
|
+
"""Reset optimizer state."""
|
|
166
|
+
self.evaluated.clear()
|
|
167
|
+
self.best_individual = None
|
|
168
|
+
logger.info("Random search reset")
|
|
169
|
+
|
|
170
|
+
def __repr__(self) -> str:
|
|
171
|
+
"""String representation."""
|
|
172
|
+
return f"RandomSearch(num_samples={self.num_samples}, " f"evaluated={len(self.evaluated)})"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Simulated Annealing optimizer for NAS.
|
|
2
|
+
|
|
3
|
+
Probabilistic hill climbing that accepts worse solutions with
|
|
4
|
+
decreasing probability to escape local optima.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import random
|
|
9
|
+
from typing import Callable, List, Optional
|
|
10
|
+
|
|
11
|
+
from morphml.core.dsl.search_space import SearchSpace
|
|
12
|
+
from morphml.core.graph import GraphMutator, ModelGraph
|
|
13
|
+
from morphml.core.search import Individual
|
|
14
|
+
from morphml.exceptions import OptimizerError
|
|
15
|
+
from morphml.logging_config import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SimulatedAnnealing:
|
|
21
|
+
"""
|
|
22
|
+
Simulated Annealing optimizer.
|
|
23
|
+
|
|
24
|
+
Accepts worse solutions with probability based on temperature
|
|
25
|
+
schedule, enabling escape from local optima.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
search_space: SearchSpace,
|
|
31
|
+
max_iterations: int = 1000,
|
|
32
|
+
initial_temp: float = 100.0,
|
|
33
|
+
final_temp: float = 0.1,
|
|
34
|
+
cooling_rate: float = 0.95,
|
|
35
|
+
cooling_schedule: str = "exponential",
|
|
36
|
+
num_mutations: int = 3,
|
|
37
|
+
mutation_rate: float = 0.3,
|
|
38
|
+
**kwargs,
|
|
39
|
+
):
|
|
40
|
+
"""Initialize SA optimizer."""
|
|
41
|
+
self.search_space = search_space
|
|
42
|
+
self.max_iterations = max_iterations
|
|
43
|
+
self.initial_temp = initial_temp
|
|
44
|
+
self.final_temp = final_temp
|
|
45
|
+
self.cooling_rate = cooling_rate
|
|
46
|
+
self.cooling_schedule = cooling_schedule
|
|
47
|
+
self.num_mutations = num_mutations
|
|
48
|
+
self.mutation_rate = mutation_rate
|
|
49
|
+
|
|
50
|
+
self.mutator = GraphMutator()
|
|
51
|
+
self.current: Optional[Individual] = None
|
|
52
|
+
self.best: Optional[Individual] = None
|
|
53
|
+
self.history: List[dict] = []
|
|
54
|
+
|
|
55
|
+
logger.info(f"Created SimulatedAnnealing: T0={initial_temp}, Tf={final_temp}")
|
|
56
|
+
|
|
57
|
+
def get_temperature(self, iteration: int) -> float:
|
|
58
|
+
"""Calculate temperature at iteration."""
|
|
59
|
+
progress = iteration / self.max_iterations
|
|
60
|
+
|
|
61
|
+
if self.cooling_schedule == "exponential":
|
|
62
|
+
return self.initial_temp * (self.cooling_rate**iteration)
|
|
63
|
+
elif self.cooling_schedule == "linear":
|
|
64
|
+
return self.initial_temp - (self.initial_temp - self.final_temp) * progress
|
|
65
|
+
elif self.cooling_schedule == "logarithmic":
|
|
66
|
+
return self.initial_temp / (1 + math.log(1 + iteration))
|
|
67
|
+
else:
|
|
68
|
+
return self.initial_temp * (self.cooling_rate**iteration)
|
|
69
|
+
|
|
70
|
+
def acceptance_probability(self, old_fitness: float, new_fitness: float, temp: float) -> float:
|
|
71
|
+
"""Calculate acceptance probability."""
|
|
72
|
+
if new_fitness > old_fitness:
|
|
73
|
+
return 1.0
|
|
74
|
+
|
|
75
|
+
if temp <= 0:
|
|
76
|
+
return 0.0
|
|
77
|
+
|
|
78
|
+
delta = new_fitness - old_fitness
|
|
79
|
+
return math.exp(delta / temp)
|
|
80
|
+
|
|
81
|
+
def optimize(self, evaluator: Callable[[ModelGraph], float]) -> Individual:
|
|
82
|
+
"""Run simulated annealing."""
|
|
83
|
+
try:
|
|
84
|
+
# Initialize
|
|
85
|
+
logger.info("Initializing simulated annealing")
|
|
86
|
+
init_graph = self.search_space.sample()
|
|
87
|
+
self.current = Individual(init_graph)
|
|
88
|
+
fitness = evaluator(self.current.graph)
|
|
89
|
+
self.current.set_fitness(fitness)
|
|
90
|
+
self.best = self.current
|
|
91
|
+
|
|
92
|
+
self.history.append(
|
|
93
|
+
{
|
|
94
|
+
"iteration": 0,
|
|
95
|
+
"fitness": fitness,
|
|
96
|
+
"temperature": self.initial_temp,
|
|
97
|
+
"accepted": True,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger.info(f"Initial fitness: {fitness:.4f}")
|
|
102
|
+
|
|
103
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
104
|
+
# Get temperature
|
|
105
|
+
temp = self.get_temperature(iteration)
|
|
106
|
+
|
|
107
|
+
# Generate neighbor
|
|
108
|
+
mutated_graph = self.mutator.mutate(
|
|
109
|
+
self.current.graph,
|
|
110
|
+
mutation_rate=self.mutation_rate,
|
|
111
|
+
max_mutations=self.num_mutations,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
neighbor = Individual(mutated_graph)
|
|
115
|
+
neighbor_fitness = evaluator(neighbor.graph)
|
|
116
|
+
neighbor.set_fitness(neighbor_fitness)
|
|
117
|
+
|
|
118
|
+
# Accept or reject
|
|
119
|
+
prob = self.acceptance_probability(self.current.fitness, neighbor_fitness, temp)
|
|
120
|
+
|
|
121
|
+
accepted = random.random() < prob
|
|
122
|
+
|
|
123
|
+
if accepted:
|
|
124
|
+
self.current = neighbor
|
|
125
|
+
|
|
126
|
+
# Update best
|
|
127
|
+
if neighbor_fitness > self.best.fitness:
|
|
128
|
+
self.best = neighbor
|
|
129
|
+
logger.info(
|
|
130
|
+
f"Iteration {iteration}: New best {neighbor_fitness:.4f} "
|
|
131
|
+
f"(T={temp:.3f})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.history.append(
|
|
135
|
+
{
|
|
136
|
+
"iteration": iteration,
|
|
137
|
+
"fitness": neighbor_fitness,
|
|
138
|
+
"temperature": temp,
|
|
139
|
+
"accepted": accepted,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if iteration % 100 == 0:
|
|
144
|
+
logger.debug(
|
|
145
|
+
f"Iteration {iteration}/{self.max_iterations}: "
|
|
146
|
+
f"Current={self.current.fitness:.4f}, "
|
|
147
|
+
f"Best={self.best.fitness:.4f}, "
|
|
148
|
+
f"T={temp:.3f}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
logger.info(f"SA complete: Best fitness = {self.best.fitness:.4f}")
|
|
152
|
+
return self.best
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Simulated annealing failed: {e}")
|
|
156
|
+
raise OptimizerError(f"SA optimization failed: {e}") from e
|
|
157
|
+
|
|
158
|
+
def get_history(self) -> List[dict]:
|
|
159
|
+
"""Get optimization history."""
|
|
160
|
+
return self.history
|
|
161
|
+
|
|
162
|
+
def get_acceptance_rate(self) -> float:
|
|
163
|
+
"""Calculate overall acceptance rate."""
|
|
164
|
+
if not self.history:
|
|
165
|
+
return 0.0
|
|
166
|
+
accepted = sum(1 for h in self.history if h["accepted"])
|
|
167
|
+
return accepted / len(self.history)
|
|
168
|
+
|
|
169
|
+
def reset(self) -> None:
|
|
170
|
+
"""Reset optimizer."""
|
|
171
|
+
self.current = None
|
|
172
|
+
self.best = None
|
|
173
|
+
self.history.clear()
|
|
174
|
+
logger.info("SA reset")
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
return (
|
|
178
|
+
f"SimulatedAnnealing(T0={self.initial_temp}, "
|
|
179
|
+
f"Tf={self.final_temp}, "
|
|
180
|
+
f"iterations={self.max_iterations})"
|
|
181
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Plugin system for MorphML.
|
|
2
|
+
|
|
3
|
+
Provides extensibility through plugins for:
|
|
4
|
+
- Custom optimizers
|
|
5
|
+
- Custom evaluators
|
|
6
|
+
- Custom mutation operators
|
|
7
|
+
- Custom objectives
|
|
8
|
+
- Custom visualizations
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from morphml.plugins import PluginManager
|
|
12
|
+
>>> manager = PluginManager()
|
|
13
|
+
>>> manager.load_plugin('custom_optimizer')
|
|
14
|
+
>>> plugin = manager.get_plugin('custom_optimizer')
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from morphml.plugins.plugin_system import (
|
|
18
|
+
EvaluatorPlugin,
|
|
19
|
+
MutationPlugin,
|
|
20
|
+
ObjectivePlugin,
|
|
21
|
+
OptimizerPlugin,
|
|
22
|
+
Plugin,
|
|
23
|
+
PluginManager,
|
|
24
|
+
VisualizationPlugin,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Plugin",
|
|
29
|
+
"OptimizerPlugin",
|
|
30
|
+
"EvaluatorPlugin",
|
|
31
|
+
"MutationPlugin",
|
|
32
|
+
"ObjectivePlugin",
|
|
33
|
+
"VisualizationPlugin",
|
|
34
|
+
"PluginManager",
|
|
35
|
+
]
|