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,486 @@
|
|
|
1
|
+
"""Genetic Algorithm optimizer for neural architecture search.
|
|
2
|
+
|
|
3
|
+
A complete implementation of evolutionary NAS using genetic algorithms.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from morphml.optimizers import GeneticAlgorithm
|
|
7
|
+
>>> from morphml.core.dsl import create_cnn_space
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Define search space
|
|
10
|
+
>>> space = create_cnn_space(num_classes=10)
|
|
11
|
+
>>>
|
|
12
|
+
>>> # Create optimizer
|
|
13
|
+
>>> ga = GeneticAlgorithm(
|
|
14
|
+
... search_space=space,
|
|
15
|
+
... population_size=50,
|
|
16
|
+
... num_generations=100,
|
|
17
|
+
... mutation_rate=0.2,
|
|
18
|
+
... crossover_rate=0.8,
|
|
19
|
+
... elitism=5
|
|
20
|
+
... )
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Run optimization
|
|
23
|
+
>>> best_individual = ga.optimize(evaluator=my_evaluator)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from morphml.core.dsl.search_space import SearchSpace
|
|
29
|
+
from morphml.core.graph import GraphMutator, ModelGraph
|
|
30
|
+
from morphml.core.search import Individual, Population
|
|
31
|
+
from morphml.exceptions import OptimizerError
|
|
32
|
+
from morphml.logging_config import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GeneticAlgorithm:
|
|
38
|
+
"""
|
|
39
|
+
Genetic Algorithm optimizer for neural architecture search.
|
|
40
|
+
|
|
41
|
+
Implements a complete evolutionary algorithm with:
|
|
42
|
+
- Population initialization from search space
|
|
43
|
+
- Selection (tournament, roulette, rank, random)
|
|
44
|
+
- Crossover (graph-level)
|
|
45
|
+
- Mutation (using GraphMutator)
|
|
46
|
+
- Elitism preservation
|
|
47
|
+
- Convergence tracking
|
|
48
|
+
- History recording
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
search_space: SearchSpace to sample from
|
|
52
|
+
population: Current population
|
|
53
|
+
mutator: GraphMutator for mutations
|
|
54
|
+
config: Algorithm configuration
|
|
55
|
+
history: Optimization history
|
|
56
|
+
best_individual: Best individual found so far
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> ga = GeneticAlgorithm(
|
|
60
|
+
... search_space=space,
|
|
61
|
+
... population_size=50,
|
|
62
|
+
... num_generations=100
|
|
63
|
+
... )
|
|
64
|
+
>>> result = ga.optimize(evaluator=evaluate_func)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
search_space: SearchSpace,
|
|
70
|
+
population_size: int = 50,
|
|
71
|
+
num_generations: int = 100,
|
|
72
|
+
mutation_rate: float = 0.2,
|
|
73
|
+
crossover_rate: float = 0.8,
|
|
74
|
+
elitism: int = 5,
|
|
75
|
+
selection_method: str = "tournament",
|
|
76
|
+
tournament_size: int = 3,
|
|
77
|
+
max_mutations: int = 3,
|
|
78
|
+
early_stopping_patience: Optional[int] = None,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Initialize Genetic Algorithm optimizer.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
search_space: SearchSpace to sample architectures from
|
|
86
|
+
population_size: Number of individuals in population
|
|
87
|
+
num_generations: Maximum number of generations
|
|
88
|
+
mutation_rate: Probability of mutating an offspring
|
|
89
|
+
crossover_rate: Probability of crossover
|
|
90
|
+
elitism: Number of best individuals to preserve
|
|
91
|
+
selection_method: Selection strategy ('tournament', 'roulette', 'rank')
|
|
92
|
+
tournament_size: Size of tournament for tournament selection
|
|
93
|
+
max_mutations: Maximum mutations per individual
|
|
94
|
+
early_stopping_patience: Stop if no improvement for N generations
|
|
95
|
+
**kwargs: Additional configuration
|
|
96
|
+
"""
|
|
97
|
+
self.search_space = search_space
|
|
98
|
+
self.config = {
|
|
99
|
+
"population_size": population_size,
|
|
100
|
+
"num_generations": num_generations,
|
|
101
|
+
"mutation_rate": mutation_rate,
|
|
102
|
+
"crossover_rate": crossover_rate,
|
|
103
|
+
"elitism": elitism,
|
|
104
|
+
"selection_method": selection_method,
|
|
105
|
+
"tournament_size": tournament_size,
|
|
106
|
+
"max_mutations": max_mutations,
|
|
107
|
+
"early_stopping_patience": early_stopping_patience,
|
|
108
|
+
**kwargs,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Initialize components
|
|
112
|
+
self.population = Population(max_size=population_size, elitism=elitism)
|
|
113
|
+
self.mutator = GraphMutator()
|
|
114
|
+
|
|
115
|
+
# State tracking
|
|
116
|
+
self.history: List[Dict[str, Any]] = []
|
|
117
|
+
self.best_individual: Optional[Individual] = None
|
|
118
|
+
self._generations_without_improvement = 0
|
|
119
|
+
self._initialized = False
|
|
120
|
+
|
|
121
|
+
logger.info(
|
|
122
|
+
f"Created GeneticAlgorithm: "
|
|
123
|
+
f"pop_size={population_size}, "
|
|
124
|
+
f"generations={num_generations}, "
|
|
125
|
+
f"mutation_rate={mutation_rate}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def initialize_population(self) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Initialize population by sampling from search space.
|
|
131
|
+
|
|
132
|
+
Creates population_size individuals by sampling random
|
|
133
|
+
architectures from the search space.
|
|
134
|
+
"""
|
|
135
|
+
logger.info(f"Initializing population of size {self.config['population_size']}")
|
|
136
|
+
|
|
137
|
+
self.population.clear()
|
|
138
|
+
|
|
139
|
+
for i in range(self.config["population_size"]):
|
|
140
|
+
try:
|
|
141
|
+
graph = self.search_space.sample()
|
|
142
|
+
individual = Individual(graph)
|
|
143
|
+
self.population.add(individual)
|
|
144
|
+
|
|
145
|
+
if (i + 1) % 10 == 0:
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Initialized {i + 1}/{self.config['population_size']} individuals"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Failed to sample individual {i}: {e}")
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
self._initialized = True
|
|
155
|
+
logger.info(f"Population initialized with {self.population.size()} individuals")
|
|
156
|
+
|
|
157
|
+
def evaluate_population(self, evaluator: Callable[[ModelGraph], float]) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Evaluate all unevaluated individuals in the population.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
evaluator: Function that takes ModelGraph and returns fitness score
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
>>> def my_evaluator(graph):
|
|
166
|
+
... # Your evaluation logic
|
|
167
|
+
... return accuracy_score
|
|
168
|
+
>>> ga.evaluate_population(my_evaluator)
|
|
169
|
+
"""
|
|
170
|
+
unevaluated = self.population.get_unevaluated()
|
|
171
|
+
|
|
172
|
+
if not unevaluated:
|
|
173
|
+
logger.debug("No unevaluated individuals")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
logger.info(f"Evaluating {len(unevaluated)} individuals")
|
|
177
|
+
|
|
178
|
+
for i, individual in enumerate(unevaluated):
|
|
179
|
+
try:
|
|
180
|
+
# Evaluate architecture
|
|
181
|
+
fitness = evaluator(individual.graph)
|
|
182
|
+
individual.set_fitness(fitness)
|
|
183
|
+
|
|
184
|
+
# Track best
|
|
185
|
+
if (
|
|
186
|
+
self.best_individual is None
|
|
187
|
+
or self.best_individual.fitness is None
|
|
188
|
+
or fitness > self.best_individual.fitness
|
|
189
|
+
):
|
|
190
|
+
self.best_individual = individual
|
|
191
|
+
self._generations_without_improvement = 0
|
|
192
|
+
logger.info(f"New best fitness: {fitness:.4f}")
|
|
193
|
+
|
|
194
|
+
if (i + 1) % 10 == 0:
|
|
195
|
+
logger.debug(f"Evaluated {i + 1}/{len(unevaluated)} individuals")
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Evaluation failed for individual {individual.id[:12]}: {e}")
|
|
199
|
+
# Assign low fitness on failure
|
|
200
|
+
individual.set_fitness(0.0)
|
|
201
|
+
|
|
202
|
+
def select_parents(self, n: int) -> List[Individual]:
|
|
203
|
+
"""
|
|
204
|
+
Select parent individuals for breeding.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
n: Number of parents to select
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
List of selected parent individuals
|
|
211
|
+
"""
|
|
212
|
+
return self.population.select(
|
|
213
|
+
n=n,
|
|
214
|
+
method=self.config["selection_method"],
|
|
215
|
+
k=self.config.get("tournament_size", 3),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def crossover(self, parent1: Individual, parent2: Individual) -> Individual:
|
|
219
|
+
"""
|
|
220
|
+
Perform crossover between two parents.
|
|
221
|
+
|
|
222
|
+
Uses single-point crossover to combine parent graphs.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
parent1: First parent
|
|
226
|
+
parent2: Second parent
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Offspring individual
|
|
230
|
+
"""
|
|
231
|
+
import random
|
|
232
|
+
|
|
233
|
+
from morphml.core.graph.mutations import crossover as graph_crossover
|
|
234
|
+
|
|
235
|
+
# Perform graph crossover
|
|
236
|
+
offspring_graph1, offspring_graph2 = graph_crossover(parent1.graph, parent2.graph)
|
|
237
|
+
|
|
238
|
+
# Randomly select one of the two offspring
|
|
239
|
+
selected_graph = random.choice([offspring_graph1, offspring_graph2])
|
|
240
|
+
|
|
241
|
+
# Create new individual
|
|
242
|
+
offspring = Individual(selected_graph)
|
|
243
|
+
offspring.parent_ids = [parent1.id, parent2.id]
|
|
244
|
+
offspring.metadata["crossover"] = "single_point"
|
|
245
|
+
|
|
246
|
+
return offspring
|
|
247
|
+
|
|
248
|
+
def mutate(self, individual: Individual) -> Individual:
|
|
249
|
+
"""
|
|
250
|
+
Mutate an individual's architecture.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
individual: Individual to mutate
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Mutated individual (new instance)
|
|
257
|
+
"""
|
|
258
|
+
mutated_graph = self.mutator.mutate(
|
|
259
|
+
individual.graph,
|
|
260
|
+
mutation_rate=self.config["mutation_rate"],
|
|
261
|
+
max_mutations=self.config.get("max_mutations", 3),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
mutated_individual = Individual(mutated_graph, parent_ids=[individual.id])
|
|
265
|
+
|
|
266
|
+
return mutated_individual
|
|
267
|
+
|
|
268
|
+
def generate_offspring(self, num_offspring: int) -> List[Individual]:
|
|
269
|
+
"""
|
|
270
|
+
Generate offspring through selection, crossover, and mutation.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
num_offspring: Number of offspring to generate
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of offspring individuals
|
|
277
|
+
"""
|
|
278
|
+
offspring: List[Individual] = []
|
|
279
|
+
|
|
280
|
+
while len(offspring) < num_offspring:
|
|
281
|
+
# Select parents
|
|
282
|
+
parents = self.select_parents(n=2)
|
|
283
|
+
|
|
284
|
+
if len(parents) < 2:
|
|
285
|
+
logger.warning("Not enough parents for crossover")
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
# Crossover
|
|
289
|
+
import random
|
|
290
|
+
|
|
291
|
+
if random.random() < self.config["crossover_rate"]:
|
|
292
|
+
child = self.crossover(parents[0], parents[1])
|
|
293
|
+
else:
|
|
294
|
+
# No crossover, just clone
|
|
295
|
+
child = random.choice(parents).clone(keep_fitness=False)
|
|
296
|
+
|
|
297
|
+
# Mutation
|
|
298
|
+
if random.random() < self.config["mutation_rate"]:
|
|
299
|
+
child = self.mutate(child)
|
|
300
|
+
|
|
301
|
+
offspring.append(child)
|
|
302
|
+
|
|
303
|
+
return offspring
|
|
304
|
+
|
|
305
|
+
def evolve_generation(self) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Evolve one generation.
|
|
308
|
+
|
|
309
|
+
Steps:
|
|
310
|
+
1. Generate offspring
|
|
311
|
+
2. Add to population
|
|
312
|
+
3. Trim to max size (keeps elite + best)
|
|
313
|
+
4. Advance generation
|
|
314
|
+
"""
|
|
315
|
+
# Generate offspring
|
|
316
|
+
num_offspring = self.config["population_size"] - self.config["elitism"]
|
|
317
|
+
offspring = self.generate_offspring(num_offspring)
|
|
318
|
+
|
|
319
|
+
logger.debug(f"Generated {len(offspring)} offspring")
|
|
320
|
+
|
|
321
|
+
# Add offspring to population
|
|
322
|
+
self.population.add_many(offspring)
|
|
323
|
+
|
|
324
|
+
# Trim to max size
|
|
325
|
+
self.population.trim()
|
|
326
|
+
|
|
327
|
+
# Advance generation
|
|
328
|
+
self.population.next_generation()
|
|
329
|
+
|
|
330
|
+
def check_convergence(self) -> bool:
|
|
331
|
+
"""
|
|
332
|
+
Check if optimization should stop.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
True if converged (should stop), False otherwise
|
|
336
|
+
"""
|
|
337
|
+
# Check generation limit
|
|
338
|
+
if self.population.generation >= self.config["num_generations"]:
|
|
339
|
+
logger.info(f"Reached max generations: {self.config['num_generations']}")
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
# Check early stopping
|
|
343
|
+
patience = self.config.get("early_stopping_patience")
|
|
344
|
+
if patience and self._generations_without_improvement >= patience:
|
|
345
|
+
logger.info(
|
|
346
|
+
f"Early stopping: No improvement for "
|
|
347
|
+
f"{self._generations_without_improvement} generations"
|
|
348
|
+
)
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
def optimize(
|
|
354
|
+
self,
|
|
355
|
+
evaluator: Callable[[ModelGraph], float],
|
|
356
|
+
callback: Optional[Callable[[int, Population], None]] = None,
|
|
357
|
+
) -> Individual:
|
|
358
|
+
"""
|
|
359
|
+
Run the genetic algorithm optimization.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
evaluator: Function to evaluate fitness of architectures
|
|
363
|
+
callback: Optional callback function called each generation
|
|
364
|
+
callback(generation: int, population: Population)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Best individual found
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
OptimizerError: If optimization fails
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
>>> def evaluate(graph):
|
|
374
|
+
... # Your evaluation
|
|
375
|
+
... return accuracy
|
|
376
|
+
>>>
|
|
377
|
+
>>> def progress_callback(gen, pop):
|
|
378
|
+
... stats = pop.get_statistics()
|
|
379
|
+
... print(f"Gen {gen}: {stats['best_fitness']:.4f}")
|
|
380
|
+
>>>
|
|
381
|
+
>>> best = ga.optimize(evaluate, callback=progress_callback)
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
# Initialize if needed
|
|
385
|
+
if not self._initialized:
|
|
386
|
+
self.initialize_population()
|
|
387
|
+
|
|
388
|
+
# Evaluate initial population
|
|
389
|
+
logger.info("Evaluating initial population")
|
|
390
|
+
self.evaluate_population(evaluator)
|
|
391
|
+
|
|
392
|
+
# Record initial stats
|
|
393
|
+
self._record_generation()
|
|
394
|
+
|
|
395
|
+
# Evolution loop
|
|
396
|
+
while not self.check_convergence():
|
|
397
|
+
gen = self.population.generation
|
|
398
|
+
|
|
399
|
+
logger.info(f"Generation {gen + 1}/{self.config['num_generations']}")
|
|
400
|
+
|
|
401
|
+
# Evolve
|
|
402
|
+
self.evolve_generation()
|
|
403
|
+
|
|
404
|
+
# Evaluate new individuals
|
|
405
|
+
self.evaluate_population(evaluator)
|
|
406
|
+
|
|
407
|
+
# Record statistics
|
|
408
|
+
self._record_generation()
|
|
409
|
+
|
|
410
|
+
# Track improvement
|
|
411
|
+
current_best = self.population.get_best(n=1)[0]
|
|
412
|
+
if (
|
|
413
|
+
self.best_individual
|
|
414
|
+
and self.best_individual.fitness is not None
|
|
415
|
+
and current_best.fitness is not None
|
|
416
|
+
and current_best.fitness <= self.best_individual.fitness
|
|
417
|
+
):
|
|
418
|
+
self._generations_without_improvement += 1
|
|
419
|
+
else:
|
|
420
|
+
self._generations_without_improvement = 0
|
|
421
|
+
|
|
422
|
+
# Callback
|
|
423
|
+
if callback:
|
|
424
|
+
callback(self.population.generation, self.population)
|
|
425
|
+
|
|
426
|
+
# Final results
|
|
427
|
+
logger.info("Optimization complete")
|
|
428
|
+
stats = self.population.get_statistics()
|
|
429
|
+
logger.info(
|
|
430
|
+
f"Final: Best={stats['best_fitness']:.4f}, "
|
|
431
|
+
f"Mean={stats['mean_fitness']:.4f}, "
|
|
432
|
+
f"Generation={self.population.generation}"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return self.best_individual or self.population.get_best(n=1)[0]
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.error(f"Optimization failed: {e}")
|
|
439
|
+
raise OptimizerError(f"Genetic algorithm optimization failed: {e}") from e
|
|
440
|
+
|
|
441
|
+
def _record_generation(self) -> None:
|
|
442
|
+
"""Record current generation statistics to history."""
|
|
443
|
+
stats = self.population.get_statistics()
|
|
444
|
+
stats["diversity"] = self.population.get_diversity()
|
|
445
|
+
stats["best_individual_id"] = self.best_individual.id[:12] if self.best_individual else None
|
|
446
|
+
self.history.append(stats)
|
|
447
|
+
|
|
448
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
449
|
+
"""
|
|
450
|
+
Get optimization history.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List of dictionaries with statistics for each generation
|
|
454
|
+
"""
|
|
455
|
+
return self.history
|
|
456
|
+
|
|
457
|
+
def get_best_n(self, n: int = 10) -> List[Individual]:
|
|
458
|
+
"""
|
|
459
|
+
Get top N individuals from final population.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
n: Number of individuals to return
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
List of best individuals
|
|
466
|
+
"""
|
|
467
|
+
return self.population.get_best(n=n)
|
|
468
|
+
|
|
469
|
+
def reset(self) -> None:
|
|
470
|
+
"""Reset optimizer state."""
|
|
471
|
+
self.population.clear()
|
|
472
|
+
self.history.clear()
|
|
473
|
+
self.best_individual = None
|
|
474
|
+
self._generations_without_improvement = 0
|
|
475
|
+
self._initialized = False
|
|
476
|
+
logger.info("Optimizer reset")
|
|
477
|
+
|
|
478
|
+
def __repr__(self) -> str:
|
|
479
|
+
"""String representation."""
|
|
480
|
+
return (
|
|
481
|
+
f"GeneticAlgorithm("
|
|
482
|
+
f"pop_size={self.config['population_size']}, "
|
|
483
|
+
f"generations={self.config['num_generations']}, "
|
|
484
|
+
f"mutation_rate={self.config['mutation_rate']}, "
|
|
485
|
+
f"current_gen={self.population.generation})"
|
|
486
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Gradient-based Neural Architecture Search algorithms.
|
|
2
|
+
|
|
3
|
+
This module implements differentiable NAS methods that use gradient descent:
|
|
4
|
+
- DARTS (Differentiable Architecture Search)
|
|
5
|
+
- ENAS (Efficient Neural Architecture Search with weight sharing)
|
|
6
|
+
|
|
7
|
+
These methods require GPU acceleration and PyTorch.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from morphml.optimizers.gradient_based import DARTS
|
|
11
|
+
>>> optimizer = DARTS(
|
|
12
|
+
... search_space=space,
|
|
13
|
+
... epochs=50,
|
|
14
|
+
... learning_rate=0.025
|
|
15
|
+
... )
|
|
16
|
+
>>> best = optimizer.optimize(evaluator)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from morphml.optimizers.gradient_based.darts import DARTSOptimizer as DARTS
|
|
20
|
+
from morphml.optimizers.gradient_based.enas import ENASOptimizer as ENAS
|
|
21
|
+
|
|
22
|
+
__all__ = ["DARTS", "ENAS"]
|