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,647 @@
|
|
|
1
|
+
"""NSGA-II (Non-dominated Sorting Genetic Algorithm II) implementation.
|
|
2
|
+
|
|
3
|
+
NSGA-II is a state-of-the-art multi-objective evolutionary algorithm that finds
|
|
4
|
+
a set of Pareto-optimal solutions balancing multiple competing objectives.
|
|
5
|
+
|
|
6
|
+
Key Features:
|
|
7
|
+
- Fast non-dominated sorting (O(MN²))
|
|
8
|
+
- Crowding distance for diversity maintenance
|
|
9
|
+
- Elitism (parent + offspring selection)
|
|
10
|
+
- Multiple objective optimization
|
|
11
|
+
|
|
12
|
+
Reference:
|
|
13
|
+
Deb, K., et al. "A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II."
|
|
14
|
+
IEEE Transactions on Evolutionary Computation, 2002.
|
|
15
|
+
|
|
16
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
17
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import random
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from morphml.core.dsl import SearchSpace
|
|
27
|
+
from morphml.core.graph import GraphMutator, ModelGraph
|
|
28
|
+
from morphml.logging_config import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class MultiObjectiveIndividual:
|
|
35
|
+
"""
|
|
36
|
+
Individual with multiple fitness objectives.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
genome: ModelGraph architecture
|
|
40
|
+
objectives: Dictionary of objective values {'accuracy': 0.95, 'latency': 12.5}
|
|
41
|
+
rank: Pareto rank (0 = non-dominated front)
|
|
42
|
+
crowding_distance: Density measure in objective space
|
|
43
|
+
domination_count: Number of solutions dominating this one
|
|
44
|
+
dominated_solutions: List of solutions this one dominates
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> ind = MultiObjectiveIndividual(
|
|
48
|
+
... genome=graph,
|
|
49
|
+
... objectives={'accuracy': 0.95, 'latency': 12.5, 'params': 2.1}
|
|
50
|
+
... )
|
|
51
|
+
>>> ind.rank = 0 # Non-dominated
|
|
52
|
+
>>> ind.crowding_distance = 1.5
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
genome: ModelGraph
|
|
56
|
+
objectives: Dict[str, float] = field(default_factory=dict)
|
|
57
|
+
rank: int = 0
|
|
58
|
+
crowding_distance: float = 0.0
|
|
59
|
+
domination_count: int = 0
|
|
60
|
+
dominated_solutions: List["MultiObjectiveIndividual"] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
def dominates(self, other: "MultiObjectiveIndividual", objective_specs: List[Dict]) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Check if this individual Pareto-dominates another.
|
|
65
|
+
|
|
66
|
+
Individual A dominates B if:
|
|
67
|
+
1. A is better than or equal to B in all objectives
|
|
68
|
+
2. A is strictly better than B in at least one objective
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
other: Other individual to compare
|
|
72
|
+
objective_specs: List of objective specifications with 'maximize' flags
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if this individual dominates the other
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> ind1.objectives = {'acc': 0.95, 'latency': -10}
|
|
79
|
+
>>> ind2.objectives = {'acc': 0.90, 'latency': -15}
|
|
80
|
+
>>> ind1.dominates(ind2, objectives) # True (better in both)
|
|
81
|
+
"""
|
|
82
|
+
better_in_any = False
|
|
83
|
+
|
|
84
|
+
for obj_spec in objective_specs:
|
|
85
|
+
obj_name = obj_spec["name"]
|
|
86
|
+
maximize = obj_spec["maximize"]
|
|
87
|
+
|
|
88
|
+
my_value = self.objectives.get(obj_name, 0.0)
|
|
89
|
+
other_value = other.objectives.get(obj_name, 0.0)
|
|
90
|
+
|
|
91
|
+
# Compare based on optimization direction
|
|
92
|
+
if maximize:
|
|
93
|
+
if my_value < other_value:
|
|
94
|
+
return False # Worse in this objective
|
|
95
|
+
elif my_value > other_value:
|
|
96
|
+
better_in_any = True
|
|
97
|
+
else: # Minimize
|
|
98
|
+
if my_value > other_value:
|
|
99
|
+
return False # Worse in this objective
|
|
100
|
+
elif my_value < other_value:
|
|
101
|
+
better_in_any = True
|
|
102
|
+
|
|
103
|
+
return better_in_any
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
"""String representation."""
|
|
107
|
+
obj_str = ", ".join(f"{k}={v:.4f}" for k, v in self.objectives.items())
|
|
108
|
+
return f"Individual(rank={self.rank}, crowding={self.crowding_distance:.4f}, {obj_str})"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class NSGA2Optimizer:
|
|
112
|
+
"""
|
|
113
|
+
NSGA-II: Non-dominated Sorting Genetic Algorithm II.
|
|
114
|
+
|
|
115
|
+
Multi-objective evolutionary algorithm that finds a diverse set of
|
|
116
|
+
Pareto-optimal solutions balancing multiple competing objectives.
|
|
117
|
+
|
|
118
|
+
Algorithm:
|
|
119
|
+
1. Initialize random population
|
|
120
|
+
2. Evaluate all objectives for each individual
|
|
121
|
+
3. Fast non-dominated sorting (rank assignment)
|
|
122
|
+
4. Crowding distance calculation (diversity measure)
|
|
123
|
+
5. Tournament selection based on rank and crowding
|
|
124
|
+
6. Crossover and mutation to generate offspring
|
|
125
|
+
7. Elitist selection from parent + offspring
|
|
126
|
+
8. Repeat steps 3-7 for N generations
|
|
127
|
+
|
|
128
|
+
Configuration:
|
|
129
|
+
population_size: Population size (default: 100)
|
|
130
|
+
num_generations: Number of generations (default: 100)
|
|
131
|
+
crossover_rate: Crossover probability (default: 0.9)
|
|
132
|
+
mutation_rate: Mutation probability (default: 0.1)
|
|
133
|
+
tournament_size: Tournament selection size (default: 2)
|
|
134
|
+
objectives: List of objective specifications
|
|
135
|
+
Format: [{'name': 'accuracy', 'maximize': True}, ...]
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> from morphml.optimizers.multi_objective import NSGA2Optimizer
|
|
139
|
+
>>> optimizer = NSGA2Optimizer(
|
|
140
|
+
... search_space=space,
|
|
141
|
+
... config={
|
|
142
|
+
... 'population_size': 100,
|
|
143
|
+
... 'num_generations': 100,
|
|
144
|
+
... 'objectives': [
|
|
145
|
+
... {'name': 'accuracy', 'maximize': True},
|
|
146
|
+
... {'name': 'latency', 'maximize': False},
|
|
147
|
+
... {'name': 'params', 'maximize': False}
|
|
148
|
+
... ]
|
|
149
|
+
... }
|
|
150
|
+
... )
|
|
151
|
+
>>> pareto_front = optimizer.optimize(evaluator)
|
|
152
|
+
>>> print(f"Found {len(pareto_front)} Pareto-optimal solutions")
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
|
|
156
|
+
"""
|
|
157
|
+
Initialize NSGA-II optimizer.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
search_space: SearchSpace defining architecture options
|
|
161
|
+
config: Configuration dictionary
|
|
162
|
+
"""
|
|
163
|
+
self.search_space = search_space
|
|
164
|
+
self.config = config or {}
|
|
165
|
+
|
|
166
|
+
# Algorithm parameters
|
|
167
|
+
self.pop_size = self.config.get("population_size", 100)
|
|
168
|
+
self.num_generations = self.config.get("num_generations", 100)
|
|
169
|
+
self.crossover_rate = self.config.get("crossover_rate", 0.9)
|
|
170
|
+
self.mutation_rate = self.config.get("mutation_rate", 0.1)
|
|
171
|
+
self.tournament_size = self.config.get("tournament_size", 2)
|
|
172
|
+
|
|
173
|
+
# Objective specifications
|
|
174
|
+
self.objectives = self.config.get(
|
|
175
|
+
"objectives",
|
|
176
|
+
[{"name": "fitness", "maximize": True}, {"name": "complexity", "maximize": False}],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# State
|
|
180
|
+
self.population: List[MultiObjectiveIndividual] = []
|
|
181
|
+
self.generation = 0
|
|
182
|
+
self.history: List[Dict[str, Any]] = []
|
|
183
|
+
|
|
184
|
+
# Mutation operator
|
|
185
|
+
self.mutator = GraphMutator()
|
|
186
|
+
|
|
187
|
+
logger.info(
|
|
188
|
+
f"Initialized NSGA-II with pop_size={self.pop_size}, "
|
|
189
|
+
f"generations={self.num_generations}, {len(self.objectives)} objectives"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def initialize_population(self) -> None:
|
|
193
|
+
"""Initialize random population from search space."""
|
|
194
|
+
self.population = []
|
|
195
|
+
for _i in range(self.pop_size):
|
|
196
|
+
genome = self.search_space.sample()
|
|
197
|
+
individual = MultiObjectiveIndividual(genome=genome)
|
|
198
|
+
self.population.append(individual)
|
|
199
|
+
|
|
200
|
+
logger.debug(f"Initialized population of {self.pop_size} individuals")
|
|
201
|
+
|
|
202
|
+
def evaluate_individual(
|
|
203
|
+
self, individual: MultiObjectiveIndividual, evaluator: Callable
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Evaluate all objectives for an individual.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
individual: Individual to evaluate
|
|
210
|
+
evaluator: Function that returns dict of objective values
|
|
211
|
+
evaluator(graph) -> {'accuracy': 0.95, 'latency': 12.5, ...}
|
|
212
|
+
"""
|
|
213
|
+
# Call evaluator
|
|
214
|
+
results = evaluator(individual.genome)
|
|
215
|
+
|
|
216
|
+
# Store objective values
|
|
217
|
+
for obj_spec in self.objectives:
|
|
218
|
+
obj_name = obj_spec["name"]
|
|
219
|
+
value = results.get(obj_name, 0.0)
|
|
220
|
+
individual.objectives[obj_name] = value
|
|
221
|
+
|
|
222
|
+
def fast_non_dominated_sort(
|
|
223
|
+
self, population: List[MultiObjectiveIndividual]
|
|
224
|
+
) -> List[List[MultiObjectiveIndividual]]:
|
|
225
|
+
"""
|
|
226
|
+
Fast non-dominated sorting algorithm.
|
|
227
|
+
|
|
228
|
+
Assigns Pareto ranks to individuals and returns fronts.
|
|
229
|
+
|
|
230
|
+
Complexity: O(MN²) where M = objectives, N = population size
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
population: Population to sort
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of Pareto fronts [F0, F1, F2, ...]
|
|
237
|
+
where F0 is the non-dominated front
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> fronts = optimizer.fast_non_dominated_sort(population)
|
|
241
|
+
>>> pareto_front = fronts[0] # Non-dominated solutions
|
|
242
|
+
"""
|
|
243
|
+
# Initialize
|
|
244
|
+
for ind in population:
|
|
245
|
+
ind.domination_count = 0
|
|
246
|
+
ind.dominated_solutions = []
|
|
247
|
+
|
|
248
|
+
fronts: List[List[MultiObjectiveIndividual]] = [[]]
|
|
249
|
+
|
|
250
|
+
# Find dominated solutions
|
|
251
|
+
for p in population:
|
|
252
|
+
for q in population:
|
|
253
|
+
if p is q:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if p.dominates(q, self.objectives):
|
|
257
|
+
p.dominated_solutions.append(q)
|
|
258
|
+
elif q.dominates(p, self.objectives):
|
|
259
|
+
p.domination_count += 1
|
|
260
|
+
|
|
261
|
+
# Non-dominated solutions (rank 0)
|
|
262
|
+
if p.domination_count == 0:
|
|
263
|
+
p.rank = 0
|
|
264
|
+
fronts[0].append(p)
|
|
265
|
+
|
|
266
|
+
# Build subsequent fronts
|
|
267
|
+
i = 0
|
|
268
|
+
while fronts[i]:
|
|
269
|
+
next_front = []
|
|
270
|
+
|
|
271
|
+
for p in fronts[i]:
|
|
272
|
+
for q in p.dominated_solutions:
|
|
273
|
+
q.domination_count -= 1
|
|
274
|
+
if q.domination_count == 0:
|
|
275
|
+
q.rank = i + 1
|
|
276
|
+
next_front.append(q)
|
|
277
|
+
|
|
278
|
+
i += 1
|
|
279
|
+
if next_front:
|
|
280
|
+
fronts.append(next_front)
|
|
281
|
+
|
|
282
|
+
return fronts
|
|
283
|
+
|
|
284
|
+
def calculate_crowding_distance(self, front: List[MultiObjectiveIndividual]) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Calculate crowding distance for individuals in a front.
|
|
287
|
+
|
|
288
|
+
Crowding distance measures the density of solutions around an
|
|
289
|
+
individual in objective space. Higher distance = more isolated.
|
|
290
|
+
|
|
291
|
+
Boundary points get infinite distance to preserve diversity.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
front: List of individuals in the same front
|
|
295
|
+
"""
|
|
296
|
+
n = len(front)
|
|
297
|
+
|
|
298
|
+
if n == 0:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Initialize distances to 0
|
|
302
|
+
for ind in front:
|
|
303
|
+
ind.crowding_distance = 0.0
|
|
304
|
+
|
|
305
|
+
# For each objective
|
|
306
|
+
for obj_spec in self.objectives:
|
|
307
|
+
obj_name = obj_spec["name"]
|
|
308
|
+
|
|
309
|
+
# Sort by objective value
|
|
310
|
+
front_sorted = sorted(front, key=lambda x: x.objectives[obj_name])
|
|
311
|
+
|
|
312
|
+
# Boundary points get infinite distance
|
|
313
|
+
front_sorted[0].crowding_distance = float("inf")
|
|
314
|
+
front_sorted[-1].crowding_distance = float("inf")
|
|
315
|
+
|
|
316
|
+
# Objective range
|
|
317
|
+
obj_min = front_sorted[0].objectives[obj_name]
|
|
318
|
+
obj_max = front_sorted[-1].objectives[obj_name]
|
|
319
|
+
obj_range = obj_max - obj_min
|
|
320
|
+
|
|
321
|
+
if obj_range == 0:
|
|
322
|
+
continue # All values same, no distance contribution
|
|
323
|
+
|
|
324
|
+
# Calculate crowding distance
|
|
325
|
+
for i in range(1, n - 1):
|
|
326
|
+
distance = (
|
|
327
|
+
front_sorted[i + 1].objectives[obj_name]
|
|
328
|
+
- front_sorted[i - 1].objectives[obj_name]
|
|
329
|
+
) / obj_range
|
|
330
|
+
|
|
331
|
+
front_sorted[i].crowding_distance += distance
|
|
332
|
+
|
|
333
|
+
def tournament_selection(self) -> MultiObjectiveIndividual:
|
|
334
|
+
"""
|
|
335
|
+
Binary tournament selection.
|
|
336
|
+
|
|
337
|
+
Select k random individuals and return the best based on:
|
|
338
|
+
1. Lower rank (closer to Pareto front)
|
|
339
|
+
2. If same rank, higher crowding distance (more isolated)
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Selected individual
|
|
343
|
+
"""
|
|
344
|
+
contestants = random.sample(self.population, self.tournament_size)
|
|
345
|
+
|
|
346
|
+
# Sort by rank (ascending), then crowding distance (descending)
|
|
347
|
+
contestants.sort(key=lambda x: (x.rank, -x.crowding_distance))
|
|
348
|
+
|
|
349
|
+
return contestants[0]
|
|
350
|
+
|
|
351
|
+
def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
|
|
352
|
+
"""
|
|
353
|
+
Graph crossover operation.
|
|
354
|
+
|
|
355
|
+
Exchanges subgraphs between two parent architectures.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
parent1: First parent graph
|
|
359
|
+
parent2: Second parent graph
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Two offspring graphs
|
|
363
|
+
"""
|
|
364
|
+
# Clone parents
|
|
365
|
+
child1 = parent1.clone()
|
|
366
|
+
child2 = parent2.clone()
|
|
367
|
+
|
|
368
|
+
# Simple crossover: swap random nodes
|
|
369
|
+
if len(child1.nodes) > 2 and len(child2.nodes) > 2:
|
|
370
|
+
# Get non-terminal nodes
|
|
371
|
+
nodes1 = [n for n in child1.nodes.values() if n.operation not in ["input", "output"]]
|
|
372
|
+
nodes2 = [n for n in child2.nodes.values() if n.operation not in ["input", "output"]]
|
|
373
|
+
|
|
374
|
+
if nodes1 and nodes2:
|
|
375
|
+
# Select random nodes to swap
|
|
376
|
+
idx1 = random.randint(0, len(nodes1) - 1)
|
|
377
|
+
idx2 = random.randint(0, len(nodes2) - 1)
|
|
378
|
+
|
|
379
|
+
# Swap node operations (simplified crossover)
|
|
380
|
+
nodes1[idx1].operation, nodes2[idx2].operation = (
|
|
381
|
+
nodes2[idx2].operation,
|
|
382
|
+
nodes1[idx1].operation,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return child1, child2
|
|
386
|
+
|
|
387
|
+
def mutate(self, graph: ModelGraph) -> ModelGraph:
|
|
388
|
+
"""
|
|
389
|
+
Mutation operation on architecture.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
graph: Graph to mutate
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Mutated graph
|
|
396
|
+
"""
|
|
397
|
+
mutated = graph.clone()
|
|
398
|
+
|
|
399
|
+
# Random mutation type
|
|
400
|
+
mutation_type = random.choice(["add_node", "remove_node", "modify_node"])
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
if mutation_type == "add_node":
|
|
404
|
+
self.mutator.add_node(mutated)
|
|
405
|
+
elif mutation_type == "remove_node" and len(mutated.nodes) > 3:
|
|
406
|
+
self.mutator.remove_node(mutated)
|
|
407
|
+
elif mutation_type == "modify_node":
|
|
408
|
+
self.mutator.mutate_node_params(mutated)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.warning(f"Mutation failed: {e}. Returning original.")
|
|
411
|
+
return graph
|
|
412
|
+
|
|
413
|
+
return mutated if mutated.is_valid_dag() else graph
|
|
414
|
+
|
|
415
|
+
def generate_offspring(self) -> List[MultiObjectiveIndividual]:
|
|
416
|
+
"""
|
|
417
|
+
Generate offspring population via selection, crossover, and mutation.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Offspring population
|
|
421
|
+
"""
|
|
422
|
+
offspring = []
|
|
423
|
+
|
|
424
|
+
while len(offspring) < self.pop_size:
|
|
425
|
+
# Selection
|
|
426
|
+
parent1 = self.tournament_selection()
|
|
427
|
+
parent2 = self.tournament_selection()
|
|
428
|
+
|
|
429
|
+
# Crossover
|
|
430
|
+
if random.random() < self.crossover_rate:
|
|
431
|
+
child1_genome, child2_genome = self.crossover(parent1.genome, parent2.genome)
|
|
432
|
+
else:
|
|
433
|
+
child1_genome = parent1.genome.clone()
|
|
434
|
+
child2_genome = parent2.genome.clone()
|
|
435
|
+
|
|
436
|
+
# Mutation
|
|
437
|
+
if random.random() < self.mutation_rate:
|
|
438
|
+
child1_genome = self.mutate(child1_genome)
|
|
439
|
+
if random.random() < self.mutation_rate:
|
|
440
|
+
child2_genome = self.mutate(child2_genome)
|
|
441
|
+
|
|
442
|
+
# Create offspring individuals
|
|
443
|
+
child1 = MultiObjectiveIndividual(genome=child1_genome)
|
|
444
|
+
child2 = MultiObjectiveIndividual(genome=child2_genome)
|
|
445
|
+
|
|
446
|
+
offspring.extend([child1, child2])
|
|
447
|
+
|
|
448
|
+
return offspring[: self.pop_size]
|
|
449
|
+
|
|
450
|
+
def environmental_selection(
|
|
451
|
+
self, combined_population: List[MultiObjectiveIndividual]
|
|
452
|
+
) -> List[MultiObjectiveIndividual]:
|
|
453
|
+
"""
|
|
454
|
+
Select next generation from combined parent + offspring population.
|
|
455
|
+
|
|
456
|
+
Elitist selection preserving best solutions based on:
|
|
457
|
+
1. Pareto rank (lower is better)
|
|
458
|
+
2. Crowding distance (higher is better for diversity)
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
combined_population: Combined parent + offspring population
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Selected population of size pop_size
|
|
465
|
+
"""
|
|
466
|
+
# Fast non-dominated sorting
|
|
467
|
+
fronts = self.fast_non_dominated_sort(combined_population)
|
|
468
|
+
|
|
469
|
+
# Calculate crowding distances
|
|
470
|
+
for front in fronts:
|
|
471
|
+
self.calculate_crowding_distance(front)
|
|
472
|
+
|
|
473
|
+
# Select individuals for next generation
|
|
474
|
+
next_population = []
|
|
475
|
+
|
|
476
|
+
for front in fronts:
|
|
477
|
+
if len(next_population) + len(front) <= self.pop_size:
|
|
478
|
+
# Add entire front
|
|
479
|
+
next_population.extend(front)
|
|
480
|
+
else:
|
|
481
|
+
# Fill remaining slots with most isolated individuals
|
|
482
|
+
remaining = self.pop_size - len(next_population)
|
|
483
|
+
front_sorted = sorted(front, key=lambda x: -x.crowding_distance)
|
|
484
|
+
next_population.extend(front_sorted[:remaining])
|
|
485
|
+
break
|
|
486
|
+
|
|
487
|
+
return next_population
|
|
488
|
+
|
|
489
|
+
def optimize(self, evaluator: Callable) -> List[MultiObjectiveIndividual]:
|
|
490
|
+
"""
|
|
491
|
+
Run NSGA-II optimization.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
evaluator: Function that evaluates objectives
|
|
495
|
+
evaluator(graph) -> {'accuracy': 0.95, 'latency': 12.5, ...}
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Pareto front (list of non-dominated solutions)
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
>>> def my_evaluator(graph):
|
|
502
|
+
... return {
|
|
503
|
+
... 'accuracy': evaluate_accuracy(graph),
|
|
504
|
+
... 'latency': measure_latency(graph),
|
|
505
|
+
... 'params': count_parameters(graph)
|
|
506
|
+
... }
|
|
507
|
+
>>> pareto_front = optimizer.optimize(my_evaluator)
|
|
508
|
+
"""
|
|
509
|
+
logger.info(f"Starting NSGA-II optimization with {self.pop_size} individuals")
|
|
510
|
+
|
|
511
|
+
# Initialize population
|
|
512
|
+
self.initialize_population()
|
|
513
|
+
|
|
514
|
+
# Evaluate initial population
|
|
515
|
+
for individual in self.population:
|
|
516
|
+
self.evaluate_individual(individual, evaluator)
|
|
517
|
+
|
|
518
|
+
# Evolution loop
|
|
519
|
+
for generation in range(self.num_generations):
|
|
520
|
+
self.generation = generation
|
|
521
|
+
|
|
522
|
+
# Generate offspring
|
|
523
|
+
offspring = self.generate_offspring()
|
|
524
|
+
|
|
525
|
+
# Evaluate offspring
|
|
526
|
+
for individual in offspring:
|
|
527
|
+
self.evaluate_individual(individual, evaluator)
|
|
528
|
+
|
|
529
|
+
# Combine parent + offspring
|
|
530
|
+
combined = self.population + offspring
|
|
531
|
+
|
|
532
|
+
# Environmental selection
|
|
533
|
+
self.population = self.environmental_selection(combined)
|
|
534
|
+
|
|
535
|
+
# Logging
|
|
536
|
+
if generation % 10 == 0 or generation == self.num_generations - 1:
|
|
537
|
+
pareto_front = [ind for ind in self.population if ind.rank == 0]
|
|
538
|
+
logger.info(
|
|
539
|
+
f"Generation {generation}/{self.num_generations}: "
|
|
540
|
+
f"Pareto front size = {len(pareto_front)}"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Log objective statistics
|
|
544
|
+
self._log_pareto_statistics(pareto_front)
|
|
545
|
+
|
|
546
|
+
# Save to history
|
|
547
|
+
self.history.append(
|
|
548
|
+
{
|
|
549
|
+
"generation": generation,
|
|
550
|
+
"pareto_size": len(pareto_front),
|
|
551
|
+
"pareto_front": pareto_front,
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Extract final Pareto front
|
|
556
|
+
pareto_front = [ind for ind in self.population if ind.rank == 0]
|
|
557
|
+
|
|
558
|
+
logger.info(f"NSGA-II complete. Final Pareto front: {len(pareto_front)} solutions")
|
|
559
|
+
|
|
560
|
+
return pareto_front
|
|
561
|
+
|
|
562
|
+
def _log_pareto_statistics(self, pareto_front: List[MultiObjectiveIndividual]) -> None:
|
|
563
|
+
"""Log statistics of Pareto front objectives."""
|
|
564
|
+
if not pareto_front:
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
for obj_spec in self.objectives:
|
|
568
|
+
obj_name = obj_spec["name"]
|
|
569
|
+
values = [ind.objectives[obj_name] for ind in pareto_front]
|
|
570
|
+
|
|
571
|
+
if values:
|
|
572
|
+
logger.info(
|
|
573
|
+
f" {obj_name}: "
|
|
574
|
+
f"min={min(values):.4f}, "
|
|
575
|
+
f"max={max(values):.4f}, "
|
|
576
|
+
f"mean={np.mean(values):.4f}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def get_pareto_front(self) -> List[MultiObjectiveIndividual]:
|
|
580
|
+
"""
|
|
581
|
+
Get current Pareto front (non-dominated solutions).
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
List of non-dominated individuals
|
|
585
|
+
"""
|
|
586
|
+
return [ind for ind in self.population if ind.rank == 0]
|
|
587
|
+
|
|
588
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
589
|
+
"""Get optimization history."""
|
|
590
|
+
return self.history
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# Convenience function
|
|
594
|
+
def optimize_with_nsga2(
|
|
595
|
+
search_space: SearchSpace,
|
|
596
|
+
evaluator: Callable,
|
|
597
|
+
objectives: List[Dict[str, Any]],
|
|
598
|
+
population_size: int = 100,
|
|
599
|
+
num_generations: int = 100,
|
|
600
|
+
verbose: bool = True,
|
|
601
|
+
) -> List[MultiObjectiveIndividual]:
|
|
602
|
+
"""
|
|
603
|
+
Quick NSGA-II optimization with sensible defaults.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
search_space: SearchSpace to optimize over
|
|
607
|
+
evaluator: Multi-objective evaluator function
|
|
608
|
+
objectives: List of objective specifications
|
|
609
|
+
population_size: Population size
|
|
610
|
+
num_generations: Number of generations
|
|
611
|
+
verbose: Print progress
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Pareto front
|
|
615
|
+
|
|
616
|
+
Example:
|
|
617
|
+
>>> pareto_front = optimize_with_nsga2(
|
|
618
|
+
... search_space=space,
|
|
619
|
+
... evaluator=my_evaluator,
|
|
620
|
+
... objectives=[
|
|
621
|
+
... {'name': 'accuracy', 'maximize': True},
|
|
622
|
+
... {'name': 'latency', 'maximize': False}
|
|
623
|
+
... ],
|
|
624
|
+
... population_size=100,
|
|
625
|
+
... num_generations=50
|
|
626
|
+
... )
|
|
627
|
+
"""
|
|
628
|
+
optimizer = NSGA2Optimizer(
|
|
629
|
+
search_space=search_space,
|
|
630
|
+
config={
|
|
631
|
+
"population_size": population_size,
|
|
632
|
+
"num_generations": num_generations,
|
|
633
|
+
"objectives": objectives,
|
|
634
|
+
},
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
pareto_front = optimizer.optimize(evaluator)
|
|
638
|
+
|
|
639
|
+
if verbose:
|
|
640
|
+
print(f"\n{'='*60}")
|
|
641
|
+
print("NSGA-II Optimization Complete")
|
|
642
|
+
print(f"{'='*60}")
|
|
643
|
+
print(f"Pareto Front Size: {len(pareto_front)}")
|
|
644
|
+
print(f"Objectives: {[obj['name'] for obj in objectives]}")
|
|
645
|
+
print(f"{'='*60}\n")
|
|
646
|
+
|
|
647
|
+
return pareto_front
|