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,416 @@
|
|
|
1
|
+
"""CMA-ES (Covariance Matrix Adaptation Evolution Strategy) for NAS.
|
|
2
|
+
|
|
3
|
+
CMA-ES is a state-of-the-art evolutionary strategy that adapts the full covariance
|
|
4
|
+
matrix of the search distribution. It's particularly effective for ill-conditioned
|
|
5
|
+
and non-separable problems.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Adaptive covariance matrix (learns problem structure)
|
|
9
|
+
- Self-adaptive step-size control
|
|
10
|
+
- Invariant to rotations and scalings
|
|
11
|
+
- No gradient information needed
|
|
12
|
+
- Proven convergence properties
|
|
13
|
+
|
|
14
|
+
Reference:
|
|
15
|
+
Hansen, N., and Ostermeier, A. "Completely Derandomized Self-Adaptation
|
|
16
|
+
in Evolution Strategies." Evolutionary Computation, 2001.
|
|
17
|
+
|
|
18
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
19
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from morphml.core.dsl import SearchSpace
|
|
27
|
+
from morphml.core.search import Individual
|
|
28
|
+
from morphml.logging_config import get_logger
|
|
29
|
+
from morphml.optimizers.evolutionary.encoding import ArchitectureEncoder
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CMAES:
|
|
35
|
+
"""
|
|
36
|
+
CMA-ES (Covariance Matrix Adaptation Evolution Strategy).
|
|
37
|
+
|
|
38
|
+
CMA-ES maintains and adapts a multivariate normal distribution
|
|
39
|
+
N(m, σ² C) where:
|
|
40
|
+
- m: Mean vector (center of search)
|
|
41
|
+
- σ: Step-size (scale of search)
|
|
42
|
+
- C: Covariance matrix (shape of search distribution)
|
|
43
|
+
|
|
44
|
+
The algorithm:
|
|
45
|
+
1. Samples offspring from N(m, σ² C)
|
|
46
|
+
2. Evaluates and ranks offspring
|
|
47
|
+
3. Updates m using weighted recombination of best offspring
|
|
48
|
+
4. Updates evolution paths (momentum-like)
|
|
49
|
+
5. Adapts C based on successful steps
|
|
50
|
+
6. Adapts σ to maintain proper step-size
|
|
51
|
+
|
|
52
|
+
Key Components:
|
|
53
|
+
- **Weighted Recombination:** Best offspring weighted by log-rank
|
|
54
|
+
- **Cumulative Step-size Adaptation:** Adapts σ based on path length
|
|
55
|
+
- **Rank-μ Update:** Updates C using μ best offspring
|
|
56
|
+
- **Evolution Paths:** Track successful mutation directions
|
|
57
|
+
|
|
58
|
+
Configuration:
|
|
59
|
+
population_size: Offspring per generation (default: 4+⌊3*ln(n)⌋)
|
|
60
|
+
sigma: Initial step-size (default: 0.3)
|
|
61
|
+
max_generations: Maximum generations (default: 100)
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> from morphml.optimizers.evolutionary import CMAES
|
|
65
|
+
>>> optimizer = CMAES(
|
|
66
|
+
... search_space=space,
|
|
67
|
+
... config={'sigma': 0.3, 'population_size': 20}
|
|
68
|
+
... )
|
|
69
|
+
>>> best = optimizer.optimize(evaluator)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
|
|
73
|
+
"""
|
|
74
|
+
Initialize CMA-ES optimizer.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
search_space: SearchSpace for architecture sampling
|
|
78
|
+
config: Configuration dictionary
|
|
79
|
+
"""
|
|
80
|
+
self.search_space = search_space
|
|
81
|
+
self.config = config or {}
|
|
82
|
+
|
|
83
|
+
# Architecture encoding
|
|
84
|
+
self.max_nodes = self.config.get("max_nodes", 20)
|
|
85
|
+
self.encoder = ArchitectureEncoder(search_space, self.max_nodes)
|
|
86
|
+
self.dim = self.encoder.get_dimension()
|
|
87
|
+
|
|
88
|
+
# Population parameters
|
|
89
|
+
self.lambda_ = self.config.get("population_size", 4 + int(3 * np.log(self.dim)))
|
|
90
|
+
self.mu = self.lambda_ // 2 # Number of parents
|
|
91
|
+
|
|
92
|
+
# Recombination weights (log-rank weighted)
|
|
93
|
+
self.weights = np.log(self.mu + 0.5) - np.log(np.arange(1, self.mu + 1))
|
|
94
|
+
self.weights /= self.weights.sum()
|
|
95
|
+
self.mu_eff = 1.0 / (self.weights**2).sum() # Variance effective selection mass
|
|
96
|
+
|
|
97
|
+
# Step-size control parameters
|
|
98
|
+
self.sigma = self.config.get("sigma", 0.3)
|
|
99
|
+
self.cs = (self.mu_eff + 2) / (self.dim + self.mu_eff + 5)
|
|
100
|
+
self.damps = 1 + 2 * max(0, np.sqrt((self.mu_eff - 1) / (self.dim + 1)) - 1) + self.cs
|
|
101
|
+
|
|
102
|
+
# Covariance matrix adaptation parameters
|
|
103
|
+
self.cc = (4 + self.mu_eff / self.dim) / (self.dim + 4 + 2 * self.mu_eff / self.dim)
|
|
104
|
+
self.c1 = 2 / ((self.dim + 1.3) ** 2 + self.mu_eff) # Rank-one update
|
|
105
|
+
self.cmu = min(
|
|
106
|
+
1 - self.c1,
|
|
107
|
+
2 * (self.mu_eff - 2 + 1 / self.mu_eff) / ((self.dim + 2) ** 2 + self.mu_eff),
|
|
108
|
+
) # Rank-μ update
|
|
109
|
+
|
|
110
|
+
# Dynamic strategy parameters
|
|
111
|
+
self.chiN = np.sqrt(self.dim) * (1 - 1 / (4 * self.dim) + 1 / (21 * self.dim**2))
|
|
112
|
+
|
|
113
|
+
# Initialize distribution
|
|
114
|
+
self.mean = np.random.rand(self.dim) # Start at random point
|
|
115
|
+
self.C = np.eye(self.dim) # Covariance matrix (initially identity)
|
|
116
|
+
self.pc = np.zeros(self.dim) # Evolution path for C
|
|
117
|
+
self.ps = np.zeros(self.dim) # Evolution path for sigma
|
|
118
|
+
|
|
119
|
+
# Eigendecomposition (for efficient sampling)
|
|
120
|
+
self.B = np.eye(self.dim) # Eigenvectors
|
|
121
|
+
self.D = np.ones(self.dim) # Eigenvalues
|
|
122
|
+
self.BD = self.B * self.D # B * D for efficient sampling
|
|
123
|
+
|
|
124
|
+
self.eigeneval_count = 0
|
|
125
|
+
self.eigeneval_interval = int(1 / (self.c1 + self.cmu) / self.dim / 10)
|
|
126
|
+
|
|
127
|
+
# Optimization state
|
|
128
|
+
self.max_generations = self.config.get("max_generations", 100)
|
|
129
|
+
self.generation = 0
|
|
130
|
+
|
|
131
|
+
self.best_individual: Optional[Individual] = None
|
|
132
|
+
self.best_fitness: float = -np.inf
|
|
133
|
+
self.best_vector: Optional[np.ndarray] = None
|
|
134
|
+
|
|
135
|
+
# History
|
|
136
|
+
self.history: List[Dict[str, Any]] = []
|
|
137
|
+
|
|
138
|
+
logger.info(
|
|
139
|
+
f"Initialized CMA-ES: dim={self.dim}, lambda={self.lambda_}, "
|
|
140
|
+
f"mu={self.mu}, sigma={self.sigma:.3f}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def sample_offspring(self) -> np.ndarray:
|
|
144
|
+
"""
|
|
145
|
+
Sample offspring from N(m, σ² C).
|
|
146
|
+
|
|
147
|
+
Uses eigendecomposition for efficient sampling:
|
|
148
|
+
x = m + σ * B * D * z where z ~ N(0, I)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Offspring vector
|
|
152
|
+
"""
|
|
153
|
+
z = np.random.randn(self.dim)
|
|
154
|
+
y = self.BD @ z # B * D * z
|
|
155
|
+
x = self.mean + self.sigma * y
|
|
156
|
+
|
|
157
|
+
# Clamp to bounds [0, 1]
|
|
158
|
+
x = np.clip(x, 0.0, 1.0)
|
|
159
|
+
|
|
160
|
+
return x
|
|
161
|
+
|
|
162
|
+
def update_distribution(self, offspring_vectors: List[np.ndarray]) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Update mean, evolution paths, covariance matrix, and step-size.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
offspring_vectors: Sorted offspring vectors (best first)
|
|
168
|
+
"""
|
|
169
|
+
# Store old mean
|
|
170
|
+
old_mean = self.mean.copy()
|
|
171
|
+
|
|
172
|
+
# Recombination: weighted average of best μ offspring
|
|
173
|
+
self.mean = sum(w * x for w, x in zip(self.weights, offspring_vectors[: self.mu]))
|
|
174
|
+
|
|
175
|
+
# Cumulation: update evolution paths
|
|
176
|
+
mean_shift = (self.mean - old_mean) / self.sigma
|
|
177
|
+
|
|
178
|
+
# C^(-1/2) * mean_shift for ps
|
|
179
|
+
self.ps = (1 - self.cs) * self.ps + np.sqrt(self.cs * (2 - self.cs) * self.mu_eff) * (
|
|
180
|
+
self.B @ (mean_shift / self.D)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Cumulation for pc (covariance path)
|
|
184
|
+
hsig = np.linalg.norm(self.ps) / np.sqrt(
|
|
185
|
+
1 - (1 - self.cs) ** (2 * self.generation + 2)
|
|
186
|
+
) < self.chiN * (1.4 + 2 / (self.dim + 1))
|
|
187
|
+
|
|
188
|
+
self.pc = (1 - self.cc) * self.pc + hsig * np.sqrt(
|
|
189
|
+
self.cc * (2 - self.cc) * self.mu_eff
|
|
190
|
+
) * mean_shift
|
|
191
|
+
|
|
192
|
+
# Adapt covariance matrix
|
|
193
|
+
# Rank-one update
|
|
194
|
+
rank_one = np.outer(self.pc, self.pc)
|
|
195
|
+
|
|
196
|
+
# Rank-μ update
|
|
197
|
+
rank_mu = sum(
|
|
198
|
+
w * np.outer((x - old_mean) / self.sigma, (x - old_mean) / self.sigma)
|
|
199
|
+
for w, x in zip(self.weights, offspring_vectors[: self.mu])
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self.C = (1 - self.c1 - self.cmu) * self.C + self.c1 * rank_one + self.cmu * rank_mu
|
|
203
|
+
|
|
204
|
+
# Adapt step-size using cumulative step-size adaptation (CSA)
|
|
205
|
+
self.sigma *= np.exp((self.cs / self.damps) * (np.linalg.norm(self.ps) / self.chiN - 1))
|
|
206
|
+
|
|
207
|
+
# Update eigendecomposition
|
|
208
|
+
if self.generation - self.eigeneval_count > self.eigeneval_interval:
|
|
209
|
+
self.eigeneval_count = self.generation
|
|
210
|
+
|
|
211
|
+
# Enforce symmetry
|
|
212
|
+
self.C = np.triu(self.C) + np.triu(self.C, 1).T
|
|
213
|
+
|
|
214
|
+
# Eigendecomposition
|
|
215
|
+
self.D, self.B = np.linalg.eigh(self.C)
|
|
216
|
+
self.D = np.sqrt(np.maximum(self.D, 0)) # Ensure positive
|
|
217
|
+
self.BD = self.B * self.D
|
|
218
|
+
|
|
219
|
+
def optimize(self, evaluator: Callable) -> Individual:
|
|
220
|
+
"""
|
|
221
|
+
Run CMA-ES optimization.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
evaluator: Function that evaluates ModelGraph -> fitness
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Best Individual found
|
|
228
|
+
|
|
229
|
+
Example:
|
|
230
|
+
>>> def my_evaluator(graph):
|
|
231
|
+
... return train_and_evaluate(graph)
|
|
232
|
+
>>> best = optimizer.optimize(my_evaluator)
|
|
233
|
+
>>> print(f"Best fitness: {best.fitness:.4f}")
|
|
234
|
+
"""
|
|
235
|
+
logger.info(f"Starting CMA-ES optimization for {self.max_generations} generations")
|
|
236
|
+
|
|
237
|
+
# Main CMA-ES loop
|
|
238
|
+
for generation in range(self.max_generations):
|
|
239
|
+
self.generation = generation + 1
|
|
240
|
+
|
|
241
|
+
# Sample offspring
|
|
242
|
+
offspring_vectors = [self.sample_offspring() for _ in range(self.lambda_)]
|
|
243
|
+
|
|
244
|
+
# Evaluate offspring
|
|
245
|
+
offspring_fitnesses = []
|
|
246
|
+
offspring_individuals = []
|
|
247
|
+
|
|
248
|
+
for vector in offspring_vectors:
|
|
249
|
+
graph = self.encoder.decode(vector)
|
|
250
|
+
fitness = evaluator(graph)
|
|
251
|
+
|
|
252
|
+
offspring_fitnesses.append(fitness)
|
|
253
|
+
|
|
254
|
+
individual = Individual(graph)
|
|
255
|
+
individual.fitness = fitness
|
|
256
|
+
offspring_individuals.append(individual)
|
|
257
|
+
|
|
258
|
+
# Update global best
|
|
259
|
+
if fitness > self.best_fitness:
|
|
260
|
+
self.best_fitness = fitness
|
|
261
|
+
self.best_vector = vector.copy()
|
|
262
|
+
self.best_individual = individual
|
|
263
|
+
|
|
264
|
+
# Sort by fitness (descending)
|
|
265
|
+
sorted_indices = np.argsort(offspring_fitnesses)[::-1]
|
|
266
|
+
sorted_vectors = [offspring_vectors[i] for i in sorted_indices]
|
|
267
|
+
[offspring_fitnesses[i] for i in sorted_indices]
|
|
268
|
+
|
|
269
|
+
# Update distribution
|
|
270
|
+
self.update_distribution(sorted_vectors)
|
|
271
|
+
|
|
272
|
+
# Record history
|
|
273
|
+
avg_fitness = np.mean(offspring_fitnesses)
|
|
274
|
+
|
|
275
|
+
self.history.append(
|
|
276
|
+
{
|
|
277
|
+
"generation": generation,
|
|
278
|
+
"best_fitness": self.best_fitness,
|
|
279
|
+
"avg_fitness": avg_fitness,
|
|
280
|
+
"sigma": self.sigma,
|
|
281
|
+
"condition_number": np.max(self.D) / np.min(self.D)
|
|
282
|
+
if np.min(self.D) > 0
|
|
283
|
+
else np.inf,
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Logging
|
|
288
|
+
if generation % 10 == 0 or generation == self.max_generations - 1:
|
|
289
|
+
logger.info(
|
|
290
|
+
f"Generation {generation}/{self.max_generations}: "
|
|
291
|
+
f"best={self.best_fitness:.4f}, "
|
|
292
|
+
f"avg={avg_fitness:.4f}, "
|
|
293
|
+
f"sigma={self.sigma:.4f}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
logger.info(f"CMA-ES complete. Best fitness: {self.best_fitness:.4f}")
|
|
297
|
+
|
|
298
|
+
return self.best_individual
|
|
299
|
+
|
|
300
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
301
|
+
"""Get optimization history."""
|
|
302
|
+
return self.history
|
|
303
|
+
|
|
304
|
+
def plot_convergence(self, save_path: Optional[str] = None) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Plot CMA-ES convergence.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
save_path: Optional path to save plot
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
import matplotlib.pyplot as plt
|
|
313
|
+
except ImportError:
|
|
314
|
+
logger.warning("matplotlib not available, cannot plot")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
if not self.history:
|
|
318
|
+
logger.warning("No history to plot")
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
generations = [h["generation"] for h in self.history]
|
|
322
|
+
best_fitness = [h["best_fitness"] for h in self.history]
|
|
323
|
+
avg_fitness = [h["avg_fitness"] for h in self.history]
|
|
324
|
+
sigma = [h["sigma"] for h in self.history]
|
|
325
|
+
|
|
326
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 10))
|
|
327
|
+
|
|
328
|
+
# Fitness plot
|
|
329
|
+
ax1.plot(generations, best_fitness, "b-", linewidth=2, label="Best Fitness")
|
|
330
|
+
ax1.plot(generations, avg_fitness, "r--", linewidth=2, label="Average Fitness")
|
|
331
|
+
ax1.set_xlabel("Generation", fontsize=12)
|
|
332
|
+
ax1.set_ylabel("Fitness", fontsize=12)
|
|
333
|
+
ax1.set_title("CMA-ES Fitness Convergence", fontsize=14, fontweight="bold")
|
|
334
|
+
ax1.legend()
|
|
335
|
+
ax1.grid(True, alpha=0.3)
|
|
336
|
+
|
|
337
|
+
# Sigma plot
|
|
338
|
+
ax2.plot(generations, sigma, "g-", linewidth=2)
|
|
339
|
+
ax2.set_xlabel("Generation", fontsize=12)
|
|
340
|
+
ax2.set_ylabel("Step-size (σ)", fontsize=12)
|
|
341
|
+
ax2.set_title("Step-size Adaptation", fontsize=14, fontweight="bold")
|
|
342
|
+
ax2.grid(True, alpha=0.3)
|
|
343
|
+
|
|
344
|
+
plt.tight_layout()
|
|
345
|
+
|
|
346
|
+
if save_path:
|
|
347
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
348
|
+
logger.info(f"Convergence plot saved to {save_path}")
|
|
349
|
+
else:
|
|
350
|
+
plt.show()
|
|
351
|
+
|
|
352
|
+
plt.close()
|
|
353
|
+
|
|
354
|
+
def __repr__(self) -> str:
|
|
355
|
+
"""String representation."""
|
|
356
|
+
return (
|
|
357
|
+
f"CMAES("
|
|
358
|
+
f"dim={self.dim}, "
|
|
359
|
+
f"lambda={self.lambda_}, "
|
|
360
|
+
f"mu={self.mu}, "
|
|
361
|
+
f"sigma={self.sigma:.3f})"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# Convenience function
|
|
366
|
+
def optimize_with_cmaes(
|
|
367
|
+
search_space: SearchSpace,
|
|
368
|
+
evaluator: Callable,
|
|
369
|
+
population_size: Optional[int] = None,
|
|
370
|
+
max_generations: int = 100,
|
|
371
|
+
sigma: float = 0.3,
|
|
372
|
+
verbose: bool = True,
|
|
373
|
+
) -> Individual:
|
|
374
|
+
"""
|
|
375
|
+
Quick CMA-ES optimization with sensible defaults.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
search_space: SearchSpace to optimize over
|
|
379
|
+
evaluator: Fitness evaluation function
|
|
380
|
+
population_size: Offspring per generation (None = auto)
|
|
381
|
+
max_generations: Maximum generations
|
|
382
|
+
sigma: Initial step-size
|
|
383
|
+
verbose: Print progress
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Best Individual found
|
|
387
|
+
|
|
388
|
+
Example:
|
|
389
|
+
>>> from morphml.optimizers.evolutionary import optimize_with_cmaes
|
|
390
|
+
>>> best = optimize_with_cmaes(
|
|
391
|
+
... search_space=space,
|
|
392
|
+
... evaluator=my_evaluator,
|
|
393
|
+
... max_generations=50,
|
|
394
|
+
... sigma=0.3
|
|
395
|
+
... )
|
|
396
|
+
"""
|
|
397
|
+
config = {"max_generations": max_generations, "sigma": sigma}
|
|
398
|
+
|
|
399
|
+
if population_size is not None:
|
|
400
|
+
config["population_size"] = population_size
|
|
401
|
+
|
|
402
|
+
optimizer = CMAES(search_space=search_space, config=config)
|
|
403
|
+
|
|
404
|
+
best = optimizer.optimize(evaluator)
|
|
405
|
+
|
|
406
|
+
if verbose:
|
|
407
|
+
print(f"\n{'='*60}")
|
|
408
|
+
print("CMA-ES Optimization Complete")
|
|
409
|
+
print(f"{'='*60}")
|
|
410
|
+
print(f"Best Fitness: {best.fitness:.4f}")
|
|
411
|
+
print(f"Generations: {max_generations}")
|
|
412
|
+
print(f"Population Size: {optimizer.lambda_}")
|
|
413
|
+
print(f"Final σ: {optimizer.sigma:.4f}")
|
|
414
|
+
print(f"{'='*60}\n")
|
|
415
|
+
|
|
416
|
+
return best
|