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.

Files changed (158) hide show
  1. morphml/__init__.py +14 -0
  2. morphml/api/__init__.py +26 -0
  3. morphml/api/app.py +326 -0
  4. morphml/api/auth.py +193 -0
  5. morphml/api/client.py +338 -0
  6. morphml/api/models.py +132 -0
  7. morphml/api/rate_limit.py +192 -0
  8. morphml/benchmarking/__init__.py +36 -0
  9. morphml/benchmarking/comparison.py +430 -0
  10. morphml/benchmarks/__init__.py +56 -0
  11. morphml/benchmarks/comparator.py +409 -0
  12. morphml/benchmarks/datasets.py +280 -0
  13. morphml/benchmarks/metrics.py +199 -0
  14. morphml/benchmarks/openml_suite.py +201 -0
  15. morphml/benchmarks/problems.py +289 -0
  16. morphml/benchmarks/suite.py +318 -0
  17. morphml/cli/__init__.py +5 -0
  18. morphml/cli/commands/experiment.py +329 -0
  19. morphml/cli/main.py +457 -0
  20. morphml/cli/quickstart.py +312 -0
  21. morphml/config.py +278 -0
  22. morphml/constraints/__init__.py +19 -0
  23. morphml/constraints/handler.py +205 -0
  24. morphml/constraints/predicates.py +285 -0
  25. morphml/core/__init__.py +3 -0
  26. morphml/core/crossover.py +449 -0
  27. morphml/core/dsl/README.md +359 -0
  28. morphml/core/dsl/__init__.py +72 -0
  29. morphml/core/dsl/ast_nodes.py +364 -0
  30. morphml/core/dsl/compiler.py +318 -0
  31. morphml/core/dsl/layers.py +368 -0
  32. morphml/core/dsl/lexer.py +336 -0
  33. morphml/core/dsl/parser.py +455 -0
  34. morphml/core/dsl/search_space.py +386 -0
  35. morphml/core/dsl/syntax.py +199 -0
  36. morphml/core/dsl/type_system.py +361 -0
  37. morphml/core/dsl/validator.py +386 -0
  38. morphml/core/graph/__init__.py +40 -0
  39. morphml/core/graph/edge.py +124 -0
  40. morphml/core/graph/graph.py +507 -0
  41. morphml/core/graph/mutations.py +409 -0
  42. morphml/core/graph/node.py +196 -0
  43. morphml/core/graph/serialization.py +361 -0
  44. morphml/core/graph/visualization.py +431 -0
  45. morphml/core/objectives/__init__.py +20 -0
  46. morphml/core/search/__init__.py +33 -0
  47. morphml/core/search/individual.py +252 -0
  48. morphml/core/search/parameters.py +453 -0
  49. morphml/core/search/population.py +375 -0
  50. morphml/core/search/search_engine.py +340 -0
  51. morphml/distributed/__init__.py +76 -0
  52. morphml/distributed/fault_tolerance.py +497 -0
  53. morphml/distributed/health_monitor.py +348 -0
  54. morphml/distributed/master.py +709 -0
  55. morphml/distributed/proto/README.md +224 -0
  56. morphml/distributed/proto/__init__.py +74 -0
  57. morphml/distributed/proto/worker.proto +170 -0
  58. morphml/distributed/proto/worker_pb2.py +79 -0
  59. morphml/distributed/proto/worker_pb2_grpc.py +423 -0
  60. morphml/distributed/resource_manager.py +416 -0
  61. morphml/distributed/scheduler.py +567 -0
  62. morphml/distributed/storage/__init__.py +33 -0
  63. morphml/distributed/storage/artifacts.py +381 -0
  64. morphml/distributed/storage/cache.py +366 -0
  65. morphml/distributed/storage/checkpointing.py +329 -0
  66. morphml/distributed/storage/database.py +459 -0
  67. morphml/distributed/worker.py +549 -0
  68. morphml/evaluation/__init__.py +5 -0
  69. morphml/evaluation/heuristic.py +237 -0
  70. morphml/exceptions.py +55 -0
  71. morphml/execution/__init__.py +5 -0
  72. morphml/execution/local_executor.py +350 -0
  73. morphml/integrations/__init__.py +28 -0
  74. morphml/integrations/jax_adapter.py +206 -0
  75. morphml/integrations/pytorch_adapter.py +530 -0
  76. morphml/integrations/sklearn_adapter.py +206 -0
  77. morphml/integrations/tensorflow_adapter.py +230 -0
  78. morphml/logging_config.py +93 -0
  79. morphml/meta_learning/__init__.py +66 -0
  80. morphml/meta_learning/architecture_similarity.py +277 -0
  81. morphml/meta_learning/experiment_database.py +240 -0
  82. morphml/meta_learning/knowledge_base/__init__.py +19 -0
  83. morphml/meta_learning/knowledge_base/embedder.py +179 -0
  84. morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
  85. morphml/meta_learning/knowledge_base/meta_features.py +265 -0
  86. morphml/meta_learning/knowledge_base/vector_store.py +271 -0
  87. morphml/meta_learning/predictors/__init__.py +27 -0
  88. morphml/meta_learning/predictors/ensemble.py +221 -0
  89. morphml/meta_learning/predictors/gnn_predictor.py +552 -0
  90. morphml/meta_learning/predictors/learning_curve.py +231 -0
  91. morphml/meta_learning/predictors/proxy_metrics.py +261 -0
  92. morphml/meta_learning/strategy_evolution/__init__.py +27 -0
  93. morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
  94. morphml/meta_learning/strategy_evolution/bandit.py +276 -0
  95. morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
  96. morphml/meta_learning/transfer.py +581 -0
  97. morphml/meta_learning/warm_start.py +286 -0
  98. morphml/optimizers/__init__.py +74 -0
  99. morphml/optimizers/adaptive_operators.py +399 -0
  100. morphml/optimizers/bayesian/__init__.py +52 -0
  101. morphml/optimizers/bayesian/acquisition.py +387 -0
  102. morphml/optimizers/bayesian/base.py +319 -0
  103. morphml/optimizers/bayesian/gaussian_process.py +635 -0
  104. morphml/optimizers/bayesian/smac.py +534 -0
  105. morphml/optimizers/bayesian/tpe.py +411 -0
  106. morphml/optimizers/differential_evolution.py +220 -0
  107. morphml/optimizers/evolutionary/__init__.py +61 -0
  108. morphml/optimizers/evolutionary/cma_es.py +416 -0
  109. morphml/optimizers/evolutionary/differential_evolution.py +556 -0
  110. morphml/optimizers/evolutionary/encoding.py +426 -0
  111. morphml/optimizers/evolutionary/particle_swarm.py +449 -0
  112. morphml/optimizers/genetic_algorithm.py +486 -0
  113. morphml/optimizers/gradient_based/__init__.py +22 -0
  114. morphml/optimizers/gradient_based/darts.py +550 -0
  115. morphml/optimizers/gradient_based/enas.py +585 -0
  116. morphml/optimizers/gradient_based/operations.py +474 -0
  117. morphml/optimizers/gradient_based/utils.py +601 -0
  118. morphml/optimizers/hill_climbing.py +169 -0
  119. morphml/optimizers/multi_objective/__init__.py +56 -0
  120. morphml/optimizers/multi_objective/indicators.py +504 -0
  121. morphml/optimizers/multi_objective/nsga2.py +647 -0
  122. morphml/optimizers/multi_objective/visualization.py +427 -0
  123. morphml/optimizers/nsga2.py +308 -0
  124. morphml/optimizers/random_search.py +172 -0
  125. morphml/optimizers/simulated_annealing.py +181 -0
  126. morphml/plugins/__init__.py +35 -0
  127. morphml/plugins/custom_evaluator_example.py +81 -0
  128. morphml/plugins/custom_optimizer_example.py +63 -0
  129. morphml/plugins/plugin_system.py +454 -0
  130. morphml/reports/__init__.py +30 -0
  131. morphml/reports/generator.py +362 -0
  132. morphml/tracking/__init__.py +7 -0
  133. morphml/tracking/experiment.py +309 -0
  134. morphml/tracking/logger.py +301 -0
  135. morphml/tracking/reporter.py +357 -0
  136. morphml/utils/__init__.py +6 -0
  137. morphml/utils/checkpoint.py +189 -0
  138. morphml/utils/comparison.py +390 -0
  139. morphml/utils/export.py +407 -0
  140. morphml/utils/progress.py +392 -0
  141. morphml/utils/validation.py +392 -0
  142. morphml/version.py +7 -0
  143. morphml/visualization/__init__.py +50 -0
  144. morphml/visualization/analytics.py +423 -0
  145. morphml/visualization/architecture_diagrams.py +353 -0
  146. morphml/visualization/architecture_plot.py +223 -0
  147. morphml/visualization/convergence_plot.py +174 -0
  148. morphml/visualization/crossover_viz.py +386 -0
  149. morphml/visualization/graph_viz.py +338 -0
  150. morphml/visualization/pareto_plot.py +149 -0
  151. morphml/visualization/plotly_dashboards.py +422 -0
  152. morphml/visualization/population.py +309 -0
  153. morphml/visualization/progress.py +260 -0
  154. morphml-1.0.0.dist-info/METADATA +434 -0
  155. morphml-1.0.0.dist-info/RECORD +158 -0
  156. morphml-1.0.0.dist-info/WHEEL +4 -0
  157. morphml-1.0.0.dist-info/entry_points.txt +3 -0
  158. morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,411 @@
1
+ """Tree-structured Parzen Estimator (TPE) for Bayesian optimization.
2
+
3
+ TPE is a sequential model-based optimization algorithm that models p(x|y) instead
4
+ of p(y|x). It splits observations into "good" and "bad" based on a quantile,
5
+ then models the density of x for each group separately.
6
+
7
+ Key advantages over GP:
8
+ - Scales better to high dimensions
9
+ - Handles categorical variables naturally
10
+ - Computationally efficient
11
+ - Works well for neural architecture search
12
+
13
+ Reference:
14
+ Bergstra, J., et al. "Algorithms for Hyper-Parameter Optimization." NIPS 2011.
15
+
16
+ Author: Eshan Roy <eshanized@proton.me>
17
+ Organization: TONMOY INFRASTRUCTURE & VISION
18
+ """
19
+
20
+ import random
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+ import numpy as np
24
+ from scipy.stats import gaussian_kde
25
+
26
+ from morphml.core.dsl import SearchSpace
27
+ from morphml.core.graph import GraphMutator, ModelGraph
28
+ from morphml.core.search import Individual
29
+ from morphml.logging_config import get_logger
30
+ from morphml.optimizers.bayesian.base import BaseBayesianOptimizer
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ class TPEOptimizer(BaseBayesianOptimizer):
36
+ """
37
+ Tree-structured Parzen Estimator for Bayesian optimization.
38
+
39
+ TPE takes a different approach than GP-based BO:
40
+ 1. Split observations into "good" (top γ quantile) and "bad" (rest)
41
+ 2. Model p(x|y=good) using kernel density estimation
42
+ 3. Model p(x|y=bad) similarly
43
+ 4. Select x that maximizes p(x|y=good) / p(x|y=bad)
44
+
45
+ This approach:
46
+ - Is more scalable than GP (O(n) vs O(n³))
47
+ - Handles mixed continuous/discrete spaces naturally
48
+ - Performs well on neural architecture search
49
+
50
+ Configuration:
51
+ n_initial_points: Random samples before TPE (default: 20)
52
+ gamma: Quantile for good/bad split (default: 0.25)
53
+ n_ei_candidates: Candidates to evaluate EI on (default: 24)
54
+ bandwidth: KDE bandwidth (default: 'scott')
55
+ prior_weight: Weight of prior in density estimation (default: 1.0)
56
+
57
+ Example:
58
+ >>> from morphml.optimizers.bayesian import TPEOptimizer
59
+ >>> optimizer = TPEOptimizer(
60
+ ... search_space=space,
61
+ ... config={
62
+ ... 'n_initial_points': 20,
63
+ ... 'gamma': 0.25,
64
+ ... 'n_ei_candidates': 24
65
+ ... }
66
+ ... )
67
+ >>> best = optimizer.optimize(evaluator, max_evaluations=100)
68
+ """
69
+
70
+ def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
71
+ """
72
+ Initialize TPE optimizer.
73
+
74
+ Args:
75
+ search_space: SearchSpace defining architecture options
76
+ config: Configuration dictionary with optional keys:
77
+ - n_initial_points: Initial random samples
78
+ - gamma: Quantile for splitting good/bad
79
+ - n_ei_candidates: Number of EI candidates
80
+ - bandwidth: KDE bandwidth method
81
+ - prior_weight: Prior weight for densities
82
+ """
83
+ super().__init__(search_space, config or {})
84
+
85
+ # TPE-specific configuration
86
+ self.gamma = self.config.get("gamma", 0.25)
87
+ self.n_ei_candidates = self.config.get("n_ei_candidates", 24)
88
+ self.bandwidth = self.config.get("bandwidth", "scott")
89
+ self.prior_weight = self.config.get("prior_weight", 1.0)
90
+
91
+ # Override n_initial_points (TPE typically needs more)
92
+ self.n_initial_points = self.config.get("n_initial_points", 20)
93
+
94
+ # Mutation for candidate generation
95
+ self.mutator = GraphMutator()
96
+
97
+ # Observation storage
98
+ self.observations: List[Dict[str, Any]] = []
99
+
100
+ logger.info(
101
+ f"Initialized TPEOptimizer with gamma={self.gamma}, "
102
+ f"n_ei_candidates={self.n_ei_candidates}"
103
+ )
104
+
105
+ def ask(self) -> List[ModelGraph]:
106
+ """
107
+ Generate next candidate using TPE.
108
+
109
+ Returns:
110
+ List containing single ModelGraph candidate
111
+ """
112
+ # Random exploration during initialization
113
+ if len(self.observations) < self.n_initial_points:
114
+ candidate = self.search_space.sample()
115
+ logger.debug(f"Random sampling ({len(self.observations)}/{self.n_initial_points})")
116
+ return [candidate]
117
+
118
+ # Split observations into good and bad
119
+ good_obs, bad_obs = self._split_observations()
120
+
121
+ logger.debug(f"TPE split: {len(good_obs)} good, {len(bad_obs)} bad observations")
122
+
123
+ # Generate candidates and select best by EI
124
+ best_candidate = None
125
+ best_ei = -float("inf")
126
+
127
+ for _ in range(self.n_ei_candidates):
128
+ # Sample from good distribution
129
+ candidate = self._sample_from_good(good_obs)
130
+
131
+ # Compute expected improvement
132
+ ei = self._compute_ei(candidate, good_obs, bad_obs)
133
+
134
+ if ei > best_ei:
135
+ best_ei = ei
136
+ best_candidate = candidate
137
+
138
+ logger.debug(f"Selected candidate with EI={best_ei:.4f}")
139
+
140
+ return [best_candidate]
141
+
142
+ def tell(self, results: List[Tuple[ModelGraph, float]]) -> None:
143
+ """
144
+ Update observations with new results.
145
+
146
+ Args:
147
+ results: List of (graph, fitness) tuples
148
+ """
149
+ for graph, fitness in results:
150
+ # Encode architecture
151
+ x = self._encode_architecture(graph)
152
+
153
+ # Store observation
154
+ self.observations.append(
155
+ {"graph": graph, "fitness": fitness, "encoding": x, "generation": self.generation}
156
+ )
157
+
158
+ # Update history
159
+ self.history.append(
160
+ {"generation": self.generation, "genome": graph, "fitness": fitness, "encoding": x}
161
+ )
162
+
163
+ logger.debug(f"Added observation: fitness={fitness:.4f}")
164
+
165
+ self.generation += 1
166
+
167
+ def _split_observations(self) -> Tuple[List[Dict], List[Dict]]:
168
+ """
169
+ Split observations into good and bad based on quantile.
170
+
171
+ Returns:
172
+ (good_observations, bad_observations) tuple
173
+ """
174
+ # Sort by fitness (descending)
175
+ sorted_obs = sorted(self.observations, key=lambda x: x["fitness"], reverse=True)
176
+
177
+ # Split at gamma quantile
178
+ n_good = max(1, int(self.gamma * len(sorted_obs)))
179
+
180
+ good_obs = sorted_obs[:n_good]
181
+ bad_obs = sorted_obs[n_good:]
182
+
183
+ return good_obs, bad_obs
184
+
185
+ def _sample_from_good(self, good_obs: List[Dict]) -> ModelGraph:
186
+ """
187
+ Sample architecture from "good" distribution.
188
+
189
+ Strategy: Pick random good observation and apply small mutations.
190
+
191
+ Args:
192
+ good_obs: List of good observations
193
+
194
+ Returns:
195
+ Sampled ModelGraph
196
+ """
197
+ if not good_obs:
198
+ return self.search_space.sample()
199
+
200
+ # Pick random good architecture as template
201
+ template = random.choice(good_obs)["graph"]
202
+
203
+ # Clone and mutate slightly
204
+ candidate = self._mutate_slightly(template)
205
+
206
+ return candidate
207
+
208
+ def _mutate_slightly(self, graph: ModelGraph, n_mutations: int = 1) -> ModelGraph:
209
+ """
210
+ Apply small mutations to architecture.
211
+
212
+ Args:
213
+ graph: Template graph
214
+ n_mutations: Number of mutations to apply
215
+
216
+ Returns:
217
+ Mutated graph
218
+ """
219
+ try:
220
+ # Clone graph
221
+ mutated = graph.clone()
222
+
223
+ # Apply mutations
224
+ for _ in range(n_mutations):
225
+ mutation_type = random.choice(["modify_node", "add_node", "remove_node"])
226
+
227
+ if mutation_type == "modify_node" and len(mutated.nodes) > 2:
228
+ self.mutator.mutate_node_params(mutated)
229
+ elif mutation_type == "add_node":
230
+ self.mutator.add_node(mutated)
231
+ elif mutation_type == "remove_node" and len(mutated.nodes) > 3:
232
+ self.mutator.remove_node(mutated)
233
+
234
+ # Validate
235
+ if mutated.is_valid_dag():
236
+ return mutated
237
+ else:
238
+ return graph.clone()
239
+
240
+ except Exception as e:
241
+ logger.warning(f"Mutation failed: {e}. Returning original.")
242
+ return graph.clone()
243
+
244
+ def _compute_ei(
245
+ self, candidate: ModelGraph, good_obs: List[Dict], bad_obs: List[Dict]
246
+ ) -> float:
247
+ """
248
+ Compute expected improvement as ratio of densities.
249
+
250
+ EI(x) ≈ p(x|y=good) / p(x|y=bad)
251
+
252
+ Args:
253
+ candidate: Candidate architecture
254
+ good_obs: Good observations
255
+ bad_obs: Bad observations
256
+
257
+ Returns:
258
+ Expected improvement value
259
+ """
260
+ # Encode candidate
261
+ x = self._encode_architecture(candidate)
262
+
263
+ # Estimate densities
264
+ p_good = self._estimate_density(x, good_obs)
265
+ p_bad = self._estimate_density(x, bad_obs)
266
+
267
+ # Compute EI
268
+ if p_bad == 0:
269
+ return float("inf") if p_good > 0 else 0.0
270
+
271
+ ei = p_good / p_bad
272
+
273
+ return ei
274
+
275
+ def _estimate_density(self, x: np.ndarray, observations: List[Dict]) -> float:
276
+ """
277
+ Estimate probability density at point x.
278
+
279
+ Uses kernel density estimation (KDE) with Gaussian kernel.
280
+
281
+ Args:
282
+ x: Point to evaluate density at
283
+ observations: List of observations for density estimation
284
+
285
+ Returns:
286
+ Estimated density value
287
+ """
288
+ if not observations:
289
+ return 1e-10 # Small positive value
290
+
291
+ # Extract encodings
292
+ X_obs = np.array([obs["encoding"] for obs in observations])
293
+
294
+ # Handle single observation
295
+ if len(X_obs) == 1:
296
+ # Gaussian centered at the single observation
297
+ dist = np.linalg.norm(x - X_obs[0])
298
+ return np.exp(-0.5 * dist**2)
299
+
300
+ try:
301
+ # Kernel density estimation
302
+ kde = gaussian_kde(X_obs.T, bw_method=self.bandwidth)
303
+
304
+ density = kde(x.reshape(-1, 1))[0]
305
+
306
+ # Add prior weight
307
+ prior_density = 1.0 / (len(x) ** 0.5) # Rough prior
308
+ density = (density + self.prior_weight * prior_density) / (1 + self.prior_weight)
309
+
310
+ return max(density, 1e-10) # Avoid zero density
311
+
312
+ except Exception as e:
313
+ logger.warning(f"KDE failed: {e}. Using fallback.")
314
+ # Fallback: distance-based density
315
+ distances = np.linalg.norm(X_obs - x, axis=1)
316
+ mean_dist = np.mean(distances)
317
+ return np.exp(-mean_dist)
318
+
319
+ def get_good_architectures(self, n: int = 10) -> List[ModelGraph]:
320
+ """
321
+ Get top-n architectures by fitness.
322
+
323
+ Args:
324
+ n: Number of architectures to return
325
+
326
+ Returns:
327
+ List of best ModelGraph instances
328
+ """
329
+ sorted_obs = sorted(self.observations, key=lambda x: x["fitness"], reverse=True)
330
+
331
+ return [obs["graph"] for obs in sorted_obs[:n]]
332
+
333
+ def get_density_statistics(self) -> Dict[str, Any]:
334
+ """
335
+ Get statistics about density estimation.
336
+
337
+ Returns:
338
+ Dictionary with density statistics
339
+ """
340
+ if len(self.observations) < self.n_initial_points:
341
+ return {"status": "initializing", "n_obs": len(self.observations)}
342
+
343
+ good_obs, bad_obs = self._split_observations()
344
+
345
+ return {
346
+ "status": "active",
347
+ "n_observations": len(self.observations),
348
+ "n_good": len(good_obs),
349
+ "n_bad": len(bad_obs),
350
+ "gamma": self.gamma,
351
+ "best_fitness": max(obs["fitness"] for obs in self.observations),
352
+ "good_threshold": good_obs[-1]["fitness"] if good_obs else None,
353
+ }
354
+
355
+ def __repr__(self) -> str:
356
+ """String representation."""
357
+ return (
358
+ f"TPEOptimizer("
359
+ f"gamma={self.gamma}, "
360
+ f"n_ei_candidates={self.n_ei_candidates}, "
361
+ f"n_obs={len(self.observations)})"
362
+ )
363
+
364
+
365
+ # Convenience function for quick TPE optimization
366
+ def optimize_with_tpe(
367
+ search_space: SearchSpace,
368
+ evaluator: Any,
369
+ n_iterations: int = 100,
370
+ n_initial: int = 20,
371
+ gamma: float = 0.25,
372
+ verbose: bool = True,
373
+ ) -> Individual:
374
+ """
375
+ Quick TPE optimization with sensible defaults.
376
+
377
+ Args:
378
+ search_space: SearchSpace to optimize over
379
+ evaluator: Fitness evaluation function
380
+ n_iterations: Total number of evaluations
381
+ n_initial: Random samples before TPE
382
+ gamma: Quantile for good/bad split
383
+ verbose: Print progress
384
+
385
+ Returns:
386
+ Best Individual found
387
+
388
+ Example:
389
+ >>> from morphml.core.dsl import create_cnn_space
390
+ >>> space = create_cnn_space(num_classes=10)
391
+ >>> best = optimize_with_tpe(
392
+ ... search_space=space,
393
+ ... evaluator=my_evaluator,
394
+ ... n_iterations=100,
395
+ ... gamma=0.25
396
+ ... )
397
+ """
398
+ optimizer = TPEOptimizer(
399
+ search_space=search_space,
400
+ config={"n_initial_points": n_initial, "gamma": gamma, "max_iterations": n_iterations},
401
+ )
402
+
403
+ def callback(iteration: int, best: Individual, history: List) -> None:
404
+ if verbose and iteration % 10 == 0:
405
+ print(f"Iteration {iteration}: Best fitness = {best.fitness:.4f}")
406
+
407
+ best = optimizer.optimize(
408
+ evaluator=evaluator, max_evaluations=n_iterations, callback=callback if verbose else None
409
+ )
410
+
411
+ return best
@@ -0,0 +1,220 @@
1
+ """Differential Evolution optimizer adapted for graph-based NAS.
2
+
3
+ Uses difference vectors between individuals to guide search.
4
+ """
5
+
6
+ import random
7
+ from typing import Callable, 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 DifferentialEvolution:
19
+ """
20
+ Differential Evolution for NAS.
21
+
22
+ Adapted DE/rand/1/bin strategy for graph-based architectures.
23
+ Uses parameter-space mutations guided by population diversity.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ search_space: SearchSpace,
29
+ population_size: int = 50,
30
+ num_generations: int = 100,
31
+ mutation_factor: float = 0.8,
32
+ crossover_prob: float = 0.9,
33
+ strategy: str = "rand/1/bin",
34
+ **kwargs,
35
+ ):
36
+ """Initialize DE optimizer."""
37
+ self.search_space = search_space
38
+ self.population_size = population_size
39
+ self.num_generations = num_generations
40
+ self.mutation_factor = mutation_factor
41
+ self.crossover_prob = crossover_prob
42
+ self.strategy = strategy
43
+
44
+ self.population = Population(max_size=population_size, elitism=0)
45
+ self.mutator = GraphMutator()
46
+ self.best_individual: Optional[Individual] = None
47
+ self.history: List[dict] = []
48
+
49
+ logger.info(f"Created DifferentialEvolution: pop={population_size}, F={mutation_factor}")
50
+
51
+ def initialize_population(self) -> None:
52
+ """Initialize population."""
53
+ logger.info(f"Initializing population of size {self.population_size}")
54
+
55
+ for i in range(self.population_size):
56
+ try:
57
+ graph = self.search_space.sample()
58
+ individual = Individual(graph)
59
+ self.population.add(individual)
60
+ except Exception as e:
61
+ logger.warning(f"Failed to sample individual {i}: {e}")
62
+ continue
63
+
64
+ logger.info(f"Population initialized with {self.population.size()} individuals")
65
+
66
+ def mutate_individual(self, target_idx: int) -> Individual:
67
+ """Create mutant individual using DE mutation."""
68
+ individuals = list(self.population.individuals)
69
+
70
+ # Select random individuals for mutation (excluding target)
71
+ candidates = [i for i in range(len(individuals)) if i != target_idx]
72
+
73
+ if len(candidates) < 3:
74
+ # Not enough diversity, just mutate target
75
+ mutated = self.mutator.mutate(individuals[target_idx].graph)
76
+ return Individual(mutated)
77
+
78
+ # DE/rand/1 strategy: mutant = r1 + F * (r2 - r3)
79
+ r1, r2, r3 = random.sample(candidates, 3)
80
+
81
+ # For graphs, we interpret this as:
82
+ # 1. Clone r1 as base
83
+ # 2. Apply mutations inspired by differences between r2 and r3
84
+ base_graph = individuals[r1].graph.clone()
85
+
86
+ # Calculate "difference" as parameter variations
87
+ diff_mutations = max(1, int(self.mutation_factor * 5))
88
+
89
+ mutated_graph = self.mutator.mutate(
90
+ base_graph, mutation_rate=self.mutation_factor, max_mutations=diff_mutations
91
+ )
92
+
93
+ mutant = Individual(mutated_graph)
94
+ return mutant
95
+
96
+ def crossover(self, target: Individual, mutant: Individual) -> Individual:
97
+ """Binomial crossover between target and mutant."""
98
+ # For graphs, we do probabilistic node/edge selection
99
+ if random.random() < self.crossover_prob:
100
+ # Use mutant (more exploration)
101
+ return mutant
102
+ else:
103
+ # Use target (more exploitation)
104
+ return target.clone(keep_fitness=False)
105
+
106
+ def evaluate_population(self, evaluator: Callable[[ModelGraph], float]) -> None:
107
+ """Evaluate all unevaluated individuals."""
108
+ unevaluated = self.population.get_unevaluated()
109
+
110
+ if not unevaluated:
111
+ return
112
+
113
+ logger.info(f"Evaluating {len(unevaluated)} individuals")
114
+
115
+ for _i, individual in enumerate(unevaluated):
116
+ try:
117
+ fitness = evaluator(individual.graph)
118
+ individual.set_fitness(fitness)
119
+
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}")
123
+
124
+ except Exception as e:
125
+ logger.error(f"Evaluation failed: {e}")
126
+ individual.set_fitness(0.0)
127
+
128
+ def evolve_generation(self, evaluator: Callable[[ModelGraph], float]) -> None:
129
+ """Evolve one generation."""
130
+ individuals = list(self.population.individuals)
131
+ new_population = []
132
+
133
+ for i, target in enumerate(individuals):
134
+ # Generate mutant
135
+ mutant = self.mutate_individual(i)
136
+
137
+ # Crossover
138
+ trial = self.crossover(target, mutant)
139
+
140
+ # Evaluate trial
141
+ trial_fitness = evaluator(trial.graph)
142
+ trial.set_fitness(trial_fitness)
143
+
144
+ # Selection
145
+ if trial_fitness > target.fitness:
146
+ new_population.append(trial)
147
+ if trial_fitness > self.best_individual.fitness:
148
+ self.best_individual = trial
149
+ else:
150
+ new_population.append(target)
151
+
152
+ # Update population
153
+ self.population.clear()
154
+ self.population.add_many(new_population)
155
+
156
+ def optimize(
157
+ self,
158
+ evaluator: Callable[[ModelGraph], float],
159
+ callback: Optional[Callable[[int, Population], None]] = None,
160
+ ) -> Individual:
161
+ """Run DE optimization."""
162
+ try:
163
+ # Initialize
164
+ self.initialize_population()
165
+ self.evaluate_population(evaluator)
166
+
167
+ # Record initial stats
168
+ stats = self.population.get_statistics()
169
+ self.history.append(stats)
170
+
171
+ # Evolution loop
172
+ for gen in range(self.num_generations):
173
+ logger.info(f"Generation {gen + 1}/{self.num_generations}")
174
+
175
+ # Evolve
176
+ self.evolve_generation(evaluator)
177
+
178
+ # Record stats
179
+ stats = self.population.get_statistics()
180
+ self.history.append(stats)
181
+
182
+ logger.info(
183
+ f"Gen {gen + 1}: Best={stats['best_fitness']:.4f}, "
184
+ f"Mean={stats['mean_fitness']:.4f}"
185
+ )
186
+
187
+ # Callback
188
+ if callback:
189
+ callback(gen + 1, self.population)
190
+
191
+ # Advance generation
192
+ self.population.next_generation()
193
+
194
+ logger.info(f"DE complete: Best fitness = {self.best_individual.fitness:.4f}")
195
+ return self.best_individual
196
+
197
+ except Exception as e:
198
+ logger.error(f"DE optimization failed: {e}")
199
+ raise OptimizerError(f"DE optimization failed: {e}") from e
200
+
201
+ def get_history(self) -> List[dict]:
202
+ """Get optimization history."""
203
+ return self.history
204
+
205
+ def get_best_n(self, n: int = 10) -> List[Individual]:
206
+ """Get top N individuals."""
207
+ return self.population.get_best(n=n)
208
+
209
+ def reset(self) -> None:
210
+ """Reset optimizer."""
211
+ self.population.clear()
212
+ self.history.clear()
213
+ self.best_individual = None
214
+ logger.info("DE reset")
215
+
216
+ def __repr__(self) -> str:
217
+ return (
218
+ f"DifferentialEvolution(pop={self.population_size}, "
219
+ f"F={self.mutation_factor}, CR={self.crossover_prob})"
220
+ )
@@ -0,0 +1,61 @@
1
+ """Advanced evolutionary optimization algorithms.
2
+
3
+ This module contains sophisticated evolutionary algorithms beyond basic GA:
4
+ - Particle Swarm Optimization (PSO) - Swarm intelligence
5
+ - Differential Evolution (DE) - Vector difference mutations
6
+ - CMA-ES (Covariance Matrix Adaptation Evolution Strategy) - Adaptive covariance
7
+
8
+ These algorithms work in continuous spaces using architecture encoding.
9
+
10
+ Example:
11
+ >>> from morphml.optimizers.evolutionary import ParticleSwarmOptimizer, optimize_with_pso
12
+ >>> optimizer = ParticleSwarmOptimizer(
13
+ ... search_space=space,
14
+ ... config={'num_particles': 30, 'max_iterations': 100}
15
+ ... )
16
+ >>> best = optimizer.optimize(evaluator)
17
+
18
+ # Or use convenience function
19
+ >>> best = optimize_with_pso(space, evaluator, num_particles=30)
20
+ """
21
+
22
+ from morphml.optimizers.evolutionary.cma_es import CMAES, optimize_with_cmaes
23
+ from morphml.optimizers.evolutionary.differential_evolution import (
24
+ DifferentialEvolution,
25
+ optimize_with_de,
26
+ )
27
+ from morphml.optimizers.evolutionary.encoding import (
28
+ ArchitectureEncoder,
29
+ ContinuousArchitectureSpace,
30
+ decode_architecture,
31
+ encode_architecture,
32
+ )
33
+ from morphml.optimizers.evolutionary.particle_swarm import (
34
+ Particle,
35
+ ParticleSwarmOptimizer,
36
+ optimize_with_pso,
37
+ )
38
+
39
+ # Aliases for backward compatibility
40
+ CMAESOptimizer = CMAES
41
+ DifferentialEvolutionOptimizer = DifferentialEvolution
42
+
43
+ __all__ = [
44
+ # Encoding utilities
45
+ "ArchitectureEncoder",
46
+ "ContinuousArchitectureSpace",
47
+ "encode_architecture",
48
+ "decode_architecture",
49
+ # Particle Swarm Optimization
50
+ "ParticleSwarmOptimizer",
51
+ "Particle",
52
+ "optimize_with_pso",
53
+ # Differential Evolution
54
+ "DifferentialEvolution",
55
+ "DifferentialEvolutionOptimizer", # Alias
56
+ "optimize_with_de",
57
+ # CMA-ES
58
+ "CMAES",
59
+ "CMAESOptimizer", # Alias
60
+ "optimize_with_cmaes",
61
+ ]