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,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
|
+
]
|