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,556 @@
|
|
|
1
|
+
"""Differential Evolution (DE) for neural architecture search.
|
|
2
|
+
|
|
3
|
+
Differential Evolution is a powerful evolutionary algorithm that uses vector
|
|
4
|
+
differences for mutation. It's particularly effective for continuous optimization
|
|
5
|
+
and has few parameters to tune.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Vector difference-based mutation
|
|
9
|
+
- Multiple mutation strategies (rand/1, best/1, rand/2)
|
|
10
|
+
- Binomial/exponential crossover
|
|
11
|
+
- Greedy selection
|
|
12
|
+
- Self-adaptive variants
|
|
13
|
+
|
|
14
|
+
Reference:
|
|
15
|
+
Storn, R., and Price, K. "Differential Evolution - A Simple and Efficient
|
|
16
|
+
Heuristic for Global Optimization over Continuous Spaces." Journal of Global
|
|
17
|
+
Optimization, 1997.
|
|
18
|
+
|
|
19
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
20
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import random
|
|
24
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from morphml.core.dsl import SearchSpace
|
|
29
|
+
from morphml.core.search import Individual
|
|
30
|
+
from morphml.logging_config import get_logger
|
|
31
|
+
from morphml.optimizers.evolutionary.encoding import ArchitectureEncoder
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DifferentialEvolution:
|
|
37
|
+
"""
|
|
38
|
+
Differential Evolution optimizer for architecture search.
|
|
39
|
+
|
|
40
|
+
DE uses vector differences for mutation, creating trial vectors that
|
|
41
|
+
are compared against target vectors. The algorithm is simple yet
|
|
42
|
+
powerful for continuous optimization.
|
|
43
|
+
|
|
44
|
+
Mutation Strategies:
|
|
45
|
+
1. **DE/rand/1:** mutant = x_r1 + F * (x_r2 - x_r3)
|
|
46
|
+
- Random base vector with one difference
|
|
47
|
+
- Good exploration
|
|
48
|
+
|
|
49
|
+
2. **DE/best/1:** mutant = x_best + F * (x_r1 - x_r2)
|
|
50
|
+
- Best vector as base
|
|
51
|
+
- Faster convergence, risk of premature convergence
|
|
52
|
+
|
|
53
|
+
3. **DE/rand/2:** mutant = x_r1 + F * (x_r2 - x_r3) + F * (x_r4 - x_r5)
|
|
54
|
+
- Two difference vectors
|
|
55
|
+
- More exploration
|
|
56
|
+
|
|
57
|
+
4. **DE/current-to-best/1:** mutant = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)
|
|
58
|
+
- Moves toward best
|
|
59
|
+
- Balanced exploration/exploitation
|
|
60
|
+
|
|
61
|
+
Crossover:
|
|
62
|
+
- **Binomial:** Each parameter inherited from mutant with probability CR
|
|
63
|
+
- **Exponential:** Consecutive parameters inherited from mutant
|
|
64
|
+
|
|
65
|
+
Configuration:
|
|
66
|
+
population_size: Population size (default: 50)
|
|
67
|
+
max_generations: Maximum generations (default: 100)
|
|
68
|
+
F: Mutation scaling factor (default: 0.8)
|
|
69
|
+
CR: Crossover probability (default: 0.9)
|
|
70
|
+
strategy: Mutation strategy (default: 'rand/1')
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> from morphml.optimizers.evolutionary import DifferentialEvolution
|
|
74
|
+
>>> optimizer = DifferentialEvolution(
|
|
75
|
+
... search_space=space,
|
|
76
|
+
... config={
|
|
77
|
+
... 'population_size': 50,
|
|
78
|
+
... 'F': 0.8,
|
|
79
|
+
... 'CR': 0.9,
|
|
80
|
+
... 'strategy': 'rand/1'
|
|
81
|
+
... }
|
|
82
|
+
... )
|
|
83
|
+
>>> best = optimizer.optimize(evaluator)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
|
|
87
|
+
"""
|
|
88
|
+
Initialize Differential Evolution optimizer.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
search_space: SearchSpace for architecture sampling
|
|
92
|
+
config: Configuration dictionary
|
|
93
|
+
"""
|
|
94
|
+
self.search_space = search_space
|
|
95
|
+
self.config = config or {}
|
|
96
|
+
|
|
97
|
+
# DE parameters
|
|
98
|
+
self.population_size = self.config.get("population_size", 50)
|
|
99
|
+
self.max_generations = self.config.get("max_generations", 100)
|
|
100
|
+
|
|
101
|
+
# F: Differential weight (scaling factor)
|
|
102
|
+
self.F = self.config.get("F", 0.8)
|
|
103
|
+
self.F_min = self.config.get("F_min", 0.5)
|
|
104
|
+
self.F_max = self.config.get("F_max", 1.0)
|
|
105
|
+
|
|
106
|
+
# CR: Crossover probability
|
|
107
|
+
self.CR = self.config.get("CR", 0.9)
|
|
108
|
+
|
|
109
|
+
# Strategy
|
|
110
|
+
self.strategy = self.config.get("strategy", "rand/1")
|
|
111
|
+
self.crossover_type = self.config.get("crossover_type", "binomial")
|
|
112
|
+
|
|
113
|
+
# Adaptive parameters
|
|
114
|
+
self.adaptive = self.config.get("adaptive", False)
|
|
115
|
+
|
|
116
|
+
# Architecture encoding
|
|
117
|
+
self.max_nodes = self.config.get("max_nodes", 20)
|
|
118
|
+
self.encoder = ArchitectureEncoder(search_space, self.max_nodes)
|
|
119
|
+
self.dim = self.encoder.get_dimension()
|
|
120
|
+
|
|
121
|
+
# Population state
|
|
122
|
+
self.population: List[Individual] = []
|
|
123
|
+
self.population_vectors: List[np.ndarray] = []
|
|
124
|
+
self.fitnesses: List[float] = []
|
|
125
|
+
|
|
126
|
+
self.best_individual: Optional[Individual] = None
|
|
127
|
+
self.best_vector: Optional[np.ndarray] = None
|
|
128
|
+
self.best_fitness: float = -np.inf
|
|
129
|
+
|
|
130
|
+
# History
|
|
131
|
+
self.generation = 0
|
|
132
|
+
self.history: List[Dict[str, Any]] = []
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
f"Initialized DE: strategy={self.strategy}, "
|
|
136
|
+
f"pop_size={self.population_size}, F={self.F}, CR={self.CR}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def initialize_population(self, evaluator: Callable) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Initialize random population and evaluate.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
evaluator: Fitness evaluation function
|
|
145
|
+
"""
|
|
146
|
+
self.population = []
|
|
147
|
+
self.population_vectors = []
|
|
148
|
+
self.fitnesses = []
|
|
149
|
+
|
|
150
|
+
for _i in range(self.population_size):
|
|
151
|
+
# Random vector in [0, 1]^dim
|
|
152
|
+
vector = np.random.rand(self.dim)
|
|
153
|
+
|
|
154
|
+
# Decode to architecture
|
|
155
|
+
graph = self.encoder.decode(vector)
|
|
156
|
+
|
|
157
|
+
# Evaluate
|
|
158
|
+
fitness = evaluator(graph)
|
|
159
|
+
|
|
160
|
+
# Store
|
|
161
|
+
individual = Individual(graph)
|
|
162
|
+
individual.fitness = fitness
|
|
163
|
+
|
|
164
|
+
self.population.append(individual)
|
|
165
|
+
self.population_vectors.append(vector)
|
|
166
|
+
self.fitnesses.append(fitness)
|
|
167
|
+
|
|
168
|
+
# Update best
|
|
169
|
+
if fitness > self.best_fitness:
|
|
170
|
+
self.best_fitness = fitness
|
|
171
|
+
self.best_vector = vector.copy()
|
|
172
|
+
self.best_individual = individual
|
|
173
|
+
|
|
174
|
+
logger.debug(f"Initialized population: best_fitness={self.best_fitness:.4f}")
|
|
175
|
+
|
|
176
|
+
def mutate(self, target_idx: int) -> np.ndarray:
|
|
177
|
+
"""
|
|
178
|
+
Create mutant vector using selected strategy.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
target_idx: Index of target individual
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Mutant vector
|
|
185
|
+
"""
|
|
186
|
+
# Get indices excluding target
|
|
187
|
+
indices = [i for i in range(self.population_size) if i != target_idx]
|
|
188
|
+
|
|
189
|
+
if self.strategy == "rand/1":
|
|
190
|
+
return self._mutate_rand_1(indices)
|
|
191
|
+
elif self.strategy == "best/1":
|
|
192
|
+
return self._mutate_best_1(indices)
|
|
193
|
+
elif self.strategy == "rand/2":
|
|
194
|
+
return self._mutate_rand_2(indices)
|
|
195
|
+
elif self.strategy == "current-to-best/1":
|
|
196
|
+
return self._mutate_current_to_best_1(target_idx, indices)
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError(f"Unknown strategy: {self.strategy}")
|
|
199
|
+
|
|
200
|
+
def _mutate_rand_1(self, indices: List[int]) -> np.ndarray:
|
|
201
|
+
"""
|
|
202
|
+
DE/rand/1 mutation.
|
|
203
|
+
|
|
204
|
+
mutant = x_r1 + F * (x_r2 - x_r3)
|
|
205
|
+
"""
|
|
206
|
+
r1, r2, r3 = random.sample(indices, 3)
|
|
207
|
+
|
|
208
|
+
mutant = self.population_vectors[r1] + self.F * (
|
|
209
|
+
self.population_vectors[r2] - self.population_vectors[r3]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return mutant
|
|
213
|
+
|
|
214
|
+
def _mutate_best_1(self, indices: List[int]) -> np.ndarray:
|
|
215
|
+
"""
|
|
216
|
+
DE/best/1 mutation.
|
|
217
|
+
|
|
218
|
+
mutant = x_best + F * (x_r1 - x_r2)
|
|
219
|
+
"""
|
|
220
|
+
r1, r2 = random.sample(indices, 2)
|
|
221
|
+
|
|
222
|
+
mutant = self.best_vector + self.F * (
|
|
223
|
+
self.population_vectors[r1] - self.population_vectors[r2]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return mutant
|
|
227
|
+
|
|
228
|
+
def _mutate_rand_2(self, indices: List[int]) -> np.ndarray:
|
|
229
|
+
"""
|
|
230
|
+
DE/rand/2 mutation.
|
|
231
|
+
|
|
232
|
+
mutant = x_r1 + F * (x_r2 - x_r3) + F * (x_r4 - x_r5)
|
|
233
|
+
"""
|
|
234
|
+
r1, r2, r3, r4, r5 = random.sample(indices, 5)
|
|
235
|
+
|
|
236
|
+
mutant = (
|
|
237
|
+
self.population_vectors[r1]
|
|
238
|
+
+ self.F * (self.population_vectors[r2] - self.population_vectors[r3])
|
|
239
|
+
+ self.F * (self.population_vectors[r4] - self.population_vectors[r5])
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return mutant
|
|
243
|
+
|
|
244
|
+
def _mutate_current_to_best_1(self, target_idx: int, indices: List[int]) -> np.ndarray:
|
|
245
|
+
"""
|
|
246
|
+
DE/current-to-best/1 mutation.
|
|
247
|
+
|
|
248
|
+
mutant = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)
|
|
249
|
+
"""
|
|
250
|
+
r1, r2 = random.sample(indices, 2)
|
|
251
|
+
|
|
252
|
+
mutant = (
|
|
253
|
+
self.population_vectors[target_idx]
|
|
254
|
+
+ self.F * (self.best_vector - self.population_vectors[target_idx])
|
|
255
|
+
+ self.F * (self.population_vectors[r1] - self.population_vectors[r2])
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return mutant
|
|
259
|
+
|
|
260
|
+
def crossover(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
|
|
261
|
+
"""
|
|
262
|
+
Crossover target and mutant to create trial vector.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
target: Target vector
|
|
266
|
+
mutant: Mutant vector
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Trial vector
|
|
270
|
+
"""
|
|
271
|
+
if self.crossover_type == "binomial":
|
|
272
|
+
return self._crossover_binomial(target, mutant)
|
|
273
|
+
elif self.crossover_type == "exponential":
|
|
274
|
+
return self._crossover_exponential(target, mutant)
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError(f"Unknown crossover type: {self.crossover_type}")
|
|
277
|
+
|
|
278
|
+
def _crossover_binomial(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
|
|
279
|
+
"""
|
|
280
|
+
Binomial crossover.
|
|
281
|
+
|
|
282
|
+
Each dimension inherited from mutant with probability CR.
|
|
283
|
+
"""
|
|
284
|
+
trial = target.copy()
|
|
285
|
+
|
|
286
|
+
# Ensure at least one dimension from mutant
|
|
287
|
+
j_rand = random.randint(0, self.dim - 1)
|
|
288
|
+
|
|
289
|
+
for j in range(self.dim):
|
|
290
|
+
if random.random() < self.CR or j == j_rand:
|
|
291
|
+
trial[j] = mutant[j]
|
|
292
|
+
|
|
293
|
+
return trial
|
|
294
|
+
|
|
295
|
+
def _crossover_exponential(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
|
|
296
|
+
"""
|
|
297
|
+
Exponential crossover.
|
|
298
|
+
|
|
299
|
+
Copy consecutive dimensions from mutant.
|
|
300
|
+
"""
|
|
301
|
+
trial = target.copy()
|
|
302
|
+
|
|
303
|
+
# Start position
|
|
304
|
+
n = random.randint(0, self.dim - 1)
|
|
305
|
+
L = 0
|
|
306
|
+
|
|
307
|
+
# Copy consecutive dimensions
|
|
308
|
+
while True:
|
|
309
|
+
trial[n] = mutant[n]
|
|
310
|
+
n = (n + 1) % self.dim
|
|
311
|
+
L += 1
|
|
312
|
+
|
|
313
|
+
if random.random() >= self.CR or L >= self.dim:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
return trial
|
|
317
|
+
|
|
318
|
+
def select(
|
|
319
|
+
self,
|
|
320
|
+
target_fitness: float,
|
|
321
|
+
trial_fitness: float,
|
|
322
|
+
target_vector: np.ndarray,
|
|
323
|
+
trial_vector: np.ndarray,
|
|
324
|
+
) -> tuple:
|
|
325
|
+
"""
|
|
326
|
+
Greedy selection between target and trial.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
target_fitness: Target fitness
|
|
330
|
+
trial_fitness: Trial fitness
|
|
331
|
+
target_vector: Target vector
|
|
332
|
+
trial_vector: Trial vector
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
(selected_fitness, selected_vector) tuple
|
|
336
|
+
"""
|
|
337
|
+
if trial_fitness >= target_fitness:
|
|
338
|
+
return trial_fitness, trial_vector
|
|
339
|
+
else:
|
|
340
|
+
return target_fitness, target_vector
|
|
341
|
+
|
|
342
|
+
def optimize(self, evaluator: Callable) -> Individual:
|
|
343
|
+
"""
|
|
344
|
+
Run Differential Evolution optimization.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
evaluator: Function that evaluates ModelGraph -> fitness
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Best Individual found
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
>>> def my_evaluator(graph):
|
|
354
|
+
... return train_and_evaluate(graph)
|
|
355
|
+
>>> best = optimizer.optimize(my_evaluator)
|
|
356
|
+
>>> print(f"Best fitness: {best.fitness:.4f}")
|
|
357
|
+
"""
|
|
358
|
+
logger.info(f"Starting DE optimization for {self.max_generations} generations")
|
|
359
|
+
|
|
360
|
+
# Initialize population
|
|
361
|
+
self.initialize_population(evaluator)
|
|
362
|
+
|
|
363
|
+
# Evolution loop
|
|
364
|
+
for generation in range(self.max_generations):
|
|
365
|
+
self.generation = generation
|
|
366
|
+
|
|
367
|
+
new_population = []
|
|
368
|
+
new_vectors = []
|
|
369
|
+
new_fitnesses = []
|
|
370
|
+
|
|
371
|
+
# For each individual
|
|
372
|
+
for i in range(self.population_size):
|
|
373
|
+
target_vector = self.population_vectors[i]
|
|
374
|
+
target_fitness = self.fitnesses[i]
|
|
375
|
+
|
|
376
|
+
# Mutation
|
|
377
|
+
mutant_vector = self.mutate(i)
|
|
378
|
+
|
|
379
|
+
# Boundary handling (clamp to [0, 1])
|
|
380
|
+
mutant_vector = np.clip(mutant_vector, 0.0, 1.0)
|
|
381
|
+
|
|
382
|
+
# Crossover
|
|
383
|
+
trial_vector = self.crossover(target_vector, mutant_vector)
|
|
384
|
+
|
|
385
|
+
# Decode and evaluate trial
|
|
386
|
+
trial_graph = self.encoder.decode(trial_vector)
|
|
387
|
+
trial_fitness = evaluator(trial_graph)
|
|
388
|
+
|
|
389
|
+
# Selection
|
|
390
|
+
selected_fitness, selected_vector = self.select(
|
|
391
|
+
target_fitness, trial_fitness, target_vector, trial_vector
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Update population
|
|
395
|
+
if selected_fitness == trial_fitness:
|
|
396
|
+
# Trial won
|
|
397
|
+
individual = Individual(trial_graph)
|
|
398
|
+
individual.fitness = trial_fitness
|
|
399
|
+
else:
|
|
400
|
+
# Target won
|
|
401
|
+
individual = self.population[i]
|
|
402
|
+
|
|
403
|
+
new_population.append(individual)
|
|
404
|
+
new_vectors.append(selected_vector)
|
|
405
|
+
new_fitnesses.append(selected_fitness)
|
|
406
|
+
|
|
407
|
+
# Update global best
|
|
408
|
+
if selected_fitness > self.best_fitness:
|
|
409
|
+
self.best_fitness = selected_fitness
|
|
410
|
+
self.best_vector = selected_vector.copy()
|
|
411
|
+
self.best_individual = individual
|
|
412
|
+
|
|
413
|
+
# Update population
|
|
414
|
+
self.population = new_population
|
|
415
|
+
self.population_vectors = new_vectors
|
|
416
|
+
self.fitnesses = new_fitnesses
|
|
417
|
+
|
|
418
|
+
# Record history
|
|
419
|
+
avg_fitness = np.mean(self.fitnesses)
|
|
420
|
+
|
|
421
|
+
self.history.append(
|
|
422
|
+
{
|
|
423
|
+
"generation": generation,
|
|
424
|
+
"best_fitness": self.best_fitness,
|
|
425
|
+
"avg_fitness": avg_fitness,
|
|
426
|
+
"std_fitness": np.std(self.fitnesses),
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Logging
|
|
431
|
+
if generation % 10 == 0 or generation == self.max_generations - 1:
|
|
432
|
+
logger.info(
|
|
433
|
+
f"Generation {generation}/{self.max_generations}: "
|
|
434
|
+
f"best={self.best_fitness:.4f}, "
|
|
435
|
+
f"avg={avg_fitness:.4f}"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
logger.info(f"DE complete. Best fitness: {self.best_fitness:.4f}")
|
|
439
|
+
|
|
440
|
+
return self.best_individual
|
|
441
|
+
|
|
442
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
443
|
+
"""Get optimization history."""
|
|
444
|
+
return self.history
|
|
445
|
+
|
|
446
|
+
def plot_convergence(self, save_path: Optional[str] = None) -> None:
|
|
447
|
+
"""
|
|
448
|
+
Plot DE convergence.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
save_path: Optional path to save plot
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
import matplotlib.pyplot as plt
|
|
455
|
+
except ImportError:
|
|
456
|
+
logger.warning("matplotlib not available, cannot plot")
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
if not self.history:
|
|
460
|
+
logger.warning("No history to plot")
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
generations = [h["generation"] for h in self.history]
|
|
464
|
+
best_fitness = [h["best_fitness"] for h in self.history]
|
|
465
|
+
avg_fitness = [h["avg_fitness"] for h in self.history]
|
|
466
|
+
|
|
467
|
+
plt.figure(figsize=(10, 6))
|
|
468
|
+
plt.plot(generations, best_fitness, "b-", linewidth=2, label="Best Fitness")
|
|
469
|
+
plt.plot(generations, avg_fitness, "r--", linewidth=2, label="Average Fitness")
|
|
470
|
+
|
|
471
|
+
plt.xlabel("Generation", fontsize=12)
|
|
472
|
+
plt.ylabel("Fitness", fontsize=12)
|
|
473
|
+
plt.title("Differential Evolution Convergence", fontsize=14, fontweight="bold")
|
|
474
|
+
plt.legend()
|
|
475
|
+
plt.grid(True, alpha=0.3)
|
|
476
|
+
plt.tight_layout()
|
|
477
|
+
|
|
478
|
+
if save_path:
|
|
479
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
480
|
+
logger.info(f"Convergence plot saved to {save_path}")
|
|
481
|
+
else:
|
|
482
|
+
plt.show()
|
|
483
|
+
|
|
484
|
+
plt.close()
|
|
485
|
+
|
|
486
|
+
def __repr__(self) -> str:
|
|
487
|
+
"""String representation."""
|
|
488
|
+
return (
|
|
489
|
+
f"DifferentialEvolution("
|
|
490
|
+
f"strategy={self.strategy}, "
|
|
491
|
+
f"pop_size={self.population_size}, "
|
|
492
|
+
f"F={self.F:.2f}, "
|
|
493
|
+
f"CR={self.CR:.2f})"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# Convenience function
|
|
498
|
+
def optimize_with_de(
|
|
499
|
+
search_space: SearchSpace,
|
|
500
|
+
evaluator: Callable,
|
|
501
|
+
population_size: int = 50,
|
|
502
|
+
max_generations: int = 100,
|
|
503
|
+
F: float = 0.8,
|
|
504
|
+
CR: float = 0.9,
|
|
505
|
+
strategy: str = "rand/1",
|
|
506
|
+
verbose: bool = True,
|
|
507
|
+
) -> Individual:
|
|
508
|
+
"""
|
|
509
|
+
Quick Differential Evolution optimization with sensible defaults.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
search_space: SearchSpace to optimize over
|
|
513
|
+
evaluator: Fitness evaluation function
|
|
514
|
+
population_size: Population size
|
|
515
|
+
max_generations: Maximum generations
|
|
516
|
+
F: Mutation scaling factor
|
|
517
|
+
CR: Crossover probability
|
|
518
|
+
strategy: Mutation strategy ('rand/1', 'best/1', 'rand/2')
|
|
519
|
+
verbose: Print progress
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Best Individual found
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
>>> from morphml.optimizers.evolutionary import optimize_with_de
|
|
526
|
+
>>> best = optimize_with_de(
|
|
527
|
+
... search_space=space,
|
|
528
|
+
... evaluator=my_evaluator,
|
|
529
|
+
... population_size=50,
|
|
530
|
+
... strategy='rand/1'
|
|
531
|
+
... )
|
|
532
|
+
"""
|
|
533
|
+
optimizer = DifferentialEvolution(
|
|
534
|
+
search_space=search_space,
|
|
535
|
+
config={
|
|
536
|
+
"population_size": population_size,
|
|
537
|
+
"max_generations": max_generations,
|
|
538
|
+
"F": F,
|
|
539
|
+
"CR": CR,
|
|
540
|
+
"strategy": strategy,
|
|
541
|
+
},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
best = optimizer.optimize(evaluator)
|
|
545
|
+
|
|
546
|
+
if verbose:
|
|
547
|
+
print(f"\n{'='*60}")
|
|
548
|
+
print("Differential Evolution Complete")
|
|
549
|
+
print(f"{'='*60}")
|
|
550
|
+
print(f"Strategy: {strategy}")
|
|
551
|
+
print(f"Best Fitness: {best.fitness:.4f}")
|
|
552
|
+
print(f"Generations: {max_generations}")
|
|
553
|
+
print(f"Population Size: {population_size}")
|
|
554
|
+
print(f"{'='*60}\n")
|
|
555
|
+
|
|
556
|
+
return best
|