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,375 @@
|
|
|
1
|
+
"""Population management for evolutionary algorithms.
|
|
2
|
+
|
|
3
|
+
Manages a collection of individuals with selection, sorting, and diversity tracking.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from morphml.core.search import Population, Individual
|
|
7
|
+
>>>
|
|
8
|
+
>>> population = Population(max_size=50)
|
|
9
|
+
>>> population.add(individual1)
|
|
10
|
+
>>> population.add(individual2)
|
|
11
|
+
>>>
|
|
12
|
+
>>> # Get best individuals
|
|
13
|
+
>>> best = population.get_best(n=10)
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Select for breeding
|
|
16
|
+
>>> parents = population.select(n=20, method='tournament')
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import random
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from morphml.core.search.individual import Individual
|
|
23
|
+
from morphml.exceptions import SearchSpaceError
|
|
24
|
+
from morphml.logging_config import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Population:
|
|
30
|
+
"""
|
|
31
|
+
Manages a collection of individuals in evolutionary search.
|
|
32
|
+
|
|
33
|
+
Provides methods for:
|
|
34
|
+
- Adding/removing individuals
|
|
35
|
+
- Selection strategies
|
|
36
|
+
- Sorting and filtering
|
|
37
|
+
- Diversity metrics
|
|
38
|
+
- Statistics tracking
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
max_size: Maximum population size
|
|
42
|
+
individuals: List of Individual instances
|
|
43
|
+
generation: Current generation number
|
|
44
|
+
history: Historical statistics
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> pop = Population(max_size=100)
|
|
48
|
+
>>> pop.add_many(initial_individuals)
|
|
49
|
+
>>> best = pop.get_best(n=10)
|
|
50
|
+
>>> parents = pop.select(n=20, method='tournament', k=3)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, max_size: int = 100, elitism: int = 5):
|
|
54
|
+
"""
|
|
55
|
+
Initialize population.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
max_size: Maximum population size
|
|
59
|
+
elitism: Number of best individuals to always keep
|
|
60
|
+
"""
|
|
61
|
+
self.max_size = max_size
|
|
62
|
+
self.elitism = elitism
|
|
63
|
+
self.individuals: List[Individual] = []
|
|
64
|
+
self.generation = 0
|
|
65
|
+
self.history: List[Dict[str, Any]] = []
|
|
66
|
+
|
|
67
|
+
logger.debug(f"Created Population: max_size={max_size}, elitism={elitism}")
|
|
68
|
+
|
|
69
|
+
def add(self, individual: Individual) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Add an individual to the population.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
individual: Individual to add
|
|
75
|
+
"""
|
|
76
|
+
self.individuals.append(individual)
|
|
77
|
+
individual.birth_generation = self.generation
|
|
78
|
+
|
|
79
|
+
def add_many(self, individuals: List[Individual]) -> None:
|
|
80
|
+
"""Add multiple individuals."""
|
|
81
|
+
for ind in individuals:
|
|
82
|
+
self.add(ind)
|
|
83
|
+
|
|
84
|
+
def remove(self, individual: Individual) -> None:
|
|
85
|
+
"""Remove an individual from the population."""
|
|
86
|
+
if individual in self.individuals:
|
|
87
|
+
self.individuals.remove(individual)
|
|
88
|
+
|
|
89
|
+
def clear(self) -> None:
|
|
90
|
+
"""Remove all individuals."""
|
|
91
|
+
self.individuals.clear()
|
|
92
|
+
|
|
93
|
+
def size(self) -> int:
|
|
94
|
+
"""Get current population size."""
|
|
95
|
+
return len(self.individuals)
|
|
96
|
+
|
|
97
|
+
def is_full(self) -> bool:
|
|
98
|
+
"""Check if population is at maximum size."""
|
|
99
|
+
return self.size() >= self.max_size
|
|
100
|
+
|
|
101
|
+
def get_best(self, n: int = 1) -> List[Individual]:
|
|
102
|
+
"""
|
|
103
|
+
Get the n best individuals.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
n: Number of individuals to return
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of top n individuals sorted by fitness
|
|
110
|
+
"""
|
|
111
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
112
|
+
sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0, reverse=True)
|
|
113
|
+
return sorted_inds[:n]
|
|
114
|
+
|
|
115
|
+
def get_worst(self, n: int = 1) -> List[Individual]:
|
|
116
|
+
"""Get the n worst individuals."""
|
|
117
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
118
|
+
sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0)
|
|
119
|
+
return sorted_inds[:n]
|
|
120
|
+
|
|
121
|
+
def get_unevaluated(self) -> List[Individual]:
|
|
122
|
+
"""Get all unevaluated individuals."""
|
|
123
|
+
return [ind for ind in self.individuals if not ind.is_evaluated()]
|
|
124
|
+
|
|
125
|
+
def select(
|
|
126
|
+
self,
|
|
127
|
+
n: int,
|
|
128
|
+
method: str = "tournament",
|
|
129
|
+
**kwargs: Any,
|
|
130
|
+
) -> List[Individual]:
|
|
131
|
+
"""
|
|
132
|
+
Select individuals for breeding.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
n: Number of individuals to select
|
|
136
|
+
method: Selection method ('tournament', 'roulette', 'rank', 'random')
|
|
137
|
+
**kwargs: Method-specific parameters
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of selected individuals
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> parents = pop.select(20, method='tournament', k=3)
|
|
144
|
+
"""
|
|
145
|
+
if method == "tournament":
|
|
146
|
+
return self._tournament_selection(n, k=kwargs.get("k", 3))
|
|
147
|
+
elif method == "roulette":
|
|
148
|
+
return self._roulette_selection(n)
|
|
149
|
+
elif method == "rank":
|
|
150
|
+
return self._rank_selection(n)
|
|
151
|
+
elif method == "random":
|
|
152
|
+
return self._random_selection(n)
|
|
153
|
+
else:
|
|
154
|
+
raise SearchSpaceError(f"Unknown selection method: {method}")
|
|
155
|
+
|
|
156
|
+
def _tournament_selection(self, n: int, k: int = 3) -> List[Individual]:
|
|
157
|
+
"""
|
|
158
|
+
Tournament selection.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
n: Number of individuals to select
|
|
162
|
+
k: Tournament size
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Selected individuals
|
|
166
|
+
"""
|
|
167
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
168
|
+
|
|
169
|
+
if len(evaluated) < k:
|
|
170
|
+
logger.warning(f"Not enough evaluated individuals for tournament size {k}")
|
|
171
|
+
k = max(1, len(evaluated))
|
|
172
|
+
|
|
173
|
+
selected = []
|
|
174
|
+
for _ in range(n):
|
|
175
|
+
# Run tournament
|
|
176
|
+
tournament = random.sample(evaluated, k)
|
|
177
|
+
winner = max(tournament, key=lambda x: x.fitness or 0)
|
|
178
|
+
selected.append(winner)
|
|
179
|
+
|
|
180
|
+
return selected
|
|
181
|
+
|
|
182
|
+
def _roulette_selection(self, n: int) -> List[Individual]:
|
|
183
|
+
"""
|
|
184
|
+
Roulette wheel selection (fitness-proportionate).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
n: Number of individuals to select
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Selected individuals
|
|
191
|
+
"""
|
|
192
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
193
|
+
|
|
194
|
+
if not evaluated:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# Shift fitnesses to be positive
|
|
198
|
+
min_fitness = min(ind.fitness or 0 for ind in evaluated)
|
|
199
|
+
if min_fitness < 0:
|
|
200
|
+
fitnesses = [(ind.fitness or 0) - min_fitness + 1e-6 for ind in evaluated]
|
|
201
|
+
else:
|
|
202
|
+
fitnesses = [ind.fitness or 1e-6 for ind in evaluated]
|
|
203
|
+
|
|
204
|
+
# Normalize to probabilities
|
|
205
|
+
total_fitness = sum(fitnesses)
|
|
206
|
+
probabilities = [f / total_fitness for f in fitnesses]
|
|
207
|
+
|
|
208
|
+
# Select with replacement
|
|
209
|
+
selected = random.choices(evaluated, weights=probabilities, k=n)
|
|
210
|
+
|
|
211
|
+
return selected
|
|
212
|
+
|
|
213
|
+
def _rank_selection(self, n: int) -> List[Individual]:
|
|
214
|
+
"""
|
|
215
|
+
Rank-based selection.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
n: Number of individuals to select
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Selected individuals
|
|
222
|
+
"""
|
|
223
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
224
|
+
|
|
225
|
+
if not evaluated:
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
# Sort by fitness
|
|
229
|
+
sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0)
|
|
230
|
+
|
|
231
|
+
# Assign ranks (linear ranking)
|
|
232
|
+
ranks = list(range(1, len(sorted_inds) + 1))
|
|
233
|
+
|
|
234
|
+
# Select based on ranks
|
|
235
|
+
selected = random.choices(sorted_inds, weights=ranks, k=n)
|
|
236
|
+
|
|
237
|
+
return selected
|
|
238
|
+
|
|
239
|
+
def _random_selection(self, n: int) -> List[Individual]:
|
|
240
|
+
"""Random selection."""
|
|
241
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
242
|
+
return random.sample(evaluated, min(n, len(evaluated)))
|
|
243
|
+
|
|
244
|
+
def trim(self, target_size: Optional[int] = None) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Trim population to target size, keeping best individuals.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
target_size: Target size (defaults to max_size)
|
|
250
|
+
"""
|
|
251
|
+
target_size = target_size or self.max_size
|
|
252
|
+
|
|
253
|
+
if self.size() <= target_size:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Always keep elite
|
|
257
|
+
elite = self.get_best(self.elitism)
|
|
258
|
+
elite_ids = {ind.id for ind in elite}
|
|
259
|
+
|
|
260
|
+
# Get remaining individuals
|
|
261
|
+
others = [ind for ind in self.individuals if ind.id not in elite_ids]
|
|
262
|
+
|
|
263
|
+
# Sort others by fitness
|
|
264
|
+
others_evaluated = [ind for ind in others if ind.is_evaluated()]
|
|
265
|
+
[ind for ind in others if not ind.is_evaluated()]
|
|
266
|
+
|
|
267
|
+
others_evaluated.sort(key=lambda x: x.fitness or 0, reverse=True)
|
|
268
|
+
|
|
269
|
+
# Keep best from others
|
|
270
|
+
remaining_slots = target_size - len(elite)
|
|
271
|
+
keep_others = others_evaluated[:remaining_slots]
|
|
272
|
+
|
|
273
|
+
# Update population
|
|
274
|
+
self.individuals = elite + keep_others
|
|
275
|
+
|
|
276
|
+
logger.debug(f"Trimmed population to {len(self.individuals)} individuals")
|
|
277
|
+
|
|
278
|
+
def increment_ages(self) -> None:
|
|
279
|
+
"""Increment age of all individuals."""
|
|
280
|
+
for ind in self.individuals:
|
|
281
|
+
ind.increment_age()
|
|
282
|
+
|
|
283
|
+
def next_generation(self) -> None:
|
|
284
|
+
"""Advance to next generation."""
|
|
285
|
+
self.generation += 1
|
|
286
|
+
self.increment_ages()
|
|
287
|
+
|
|
288
|
+
# Record statistics
|
|
289
|
+
stats = self.get_statistics()
|
|
290
|
+
self.history.append(stats)
|
|
291
|
+
|
|
292
|
+
logger.info(
|
|
293
|
+
f"Generation {self.generation}: "
|
|
294
|
+
f"size={self.size()}, "
|
|
295
|
+
f"best={stats.get('best_fitness', 0):.4f}, "
|
|
296
|
+
f"mean={stats.get('mean_fitness', 0):.4f}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
300
|
+
"""
|
|
301
|
+
Get population statistics.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dictionary of statistics
|
|
305
|
+
"""
|
|
306
|
+
evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
|
|
307
|
+
|
|
308
|
+
if not evaluated:
|
|
309
|
+
return {
|
|
310
|
+
"generation": self.generation,
|
|
311
|
+
"size": self.size(),
|
|
312
|
+
"evaluated": 0,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fitnesses = [ind.fitness for ind in evaluated if ind.fitness is not None]
|
|
316
|
+
|
|
317
|
+
stats = {
|
|
318
|
+
"generation": self.generation,
|
|
319
|
+
"size": self.size(),
|
|
320
|
+
"evaluated": len(evaluated),
|
|
321
|
+
"best_fitness": max(fitnesses) if fitnesses else 0,
|
|
322
|
+
"worst_fitness": min(fitnesses) if fitnesses else 0,
|
|
323
|
+
"mean_fitness": sum(fitnesses) / len(fitnesses) if fitnesses else 0,
|
|
324
|
+
"median_fitness": sorted(fitnesses)[len(fitnesses) // 2] if fitnesses else 0,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return stats
|
|
328
|
+
|
|
329
|
+
def get_diversity(self, method: str = "hash") -> float:
|
|
330
|
+
"""
|
|
331
|
+
Calculate population diversity.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
method: Diversity metric ('hash', 'hamming', 'depth')
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Diversity score (0-1, higher = more diverse)
|
|
338
|
+
"""
|
|
339
|
+
if self.size() <= 1:
|
|
340
|
+
return 0.0
|
|
341
|
+
|
|
342
|
+
if method == "hash":
|
|
343
|
+
# Count unique graph hashes
|
|
344
|
+
hashes = {ind.graph.hash() for ind in self.individuals}
|
|
345
|
+
return len(hashes) / self.size()
|
|
346
|
+
|
|
347
|
+
elif method == "depth":
|
|
348
|
+
# Variance in graph depths
|
|
349
|
+
depths = [ind.graph.get_depth() for ind in self.individuals]
|
|
350
|
+
if len(set(depths)) == 1:
|
|
351
|
+
return 0.0
|
|
352
|
+
mean_depth = sum(depths) / len(depths)
|
|
353
|
+
variance = sum((d - mean_depth) ** 2 for d in depths) / len(depths)
|
|
354
|
+
# Normalize
|
|
355
|
+
return min(1.0, variance / (mean_depth + 1))
|
|
356
|
+
|
|
357
|
+
else:
|
|
358
|
+
return 0.0
|
|
359
|
+
|
|
360
|
+
def __len__(self) -> int:
|
|
361
|
+
"""Return population size."""
|
|
362
|
+
return self.size()
|
|
363
|
+
|
|
364
|
+
def __iter__(self) -> Any:
|
|
365
|
+
"""Iterate over individuals."""
|
|
366
|
+
return iter(self.individuals)
|
|
367
|
+
|
|
368
|
+
def __repr__(self) -> str:
|
|
369
|
+
"""String representation."""
|
|
370
|
+
stats = self.get_statistics()
|
|
371
|
+
return (
|
|
372
|
+
f"Population(generation={self.generation}, "
|
|
373
|
+
f"size={self.size()}/{self.max_size}, "
|
|
374
|
+
f"best_fitness={stats.get('best_fitness', 0):.4f})"
|
|
375
|
+
)
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Base search engine for optimization algorithms.
|
|
2
|
+
|
|
3
|
+
Provides a unified interface for all search/optimization algorithms
|
|
4
|
+
with common functionality for initialization, sampling, and termination.
|
|
5
|
+
|
|
6
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
7
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from morphml.core.dsl.search_space import SearchSpace
|
|
14
|
+
from morphml.core.graph import ModelGraph
|
|
15
|
+
from morphml.core.search.individual import Individual
|
|
16
|
+
from morphml.core.search.population import Population
|
|
17
|
+
from morphml.logging_config import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SearchEngine(ABC):
|
|
23
|
+
"""
|
|
24
|
+
Base class for all search/optimization algorithms.
|
|
25
|
+
|
|
26
|
+
Provides common interface and functionality for:
|
|
27
|
+
- Population initialization
|
|
28
|
+
- Architecture sampling
|
|
29
|
+
- Evolution/search steps
|
|
30
|
+
- History tracking
|
|
31
|
+
- Termination criteria
|
|
32
|
+
|
|
33
|
+
Subclass this to implement specific search algorithms
|
|
34
|
+
(genetic algorithms, Bayesian optimization, random search, etc.)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
search_space: SearchSpace,
|
|
40
|
+
config: Optional[Dict[str, Any]] = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize search engine.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
search_space: Search space definition
|
|
47
|
+
config: Configuration dictionary
|
|
48
|
+
"""
|
|
49
|
+
self.search_space = search_space
|
|
50
|
+
self.config = config or {}
|
|
51
|
+
self.generation = 0
|
|
52
|
+
self.history: List[Dict[str, Any]] = []
|
|
53
|
+
self.best_individual: Optional[Individual] = None
|
|
54
|
+
self.num_evaluations = 0
|
|
55
|
+
|
|
56
|
+
logger.info(f"Initialized {self.__class__.__name__}")
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def initialize_population(self, size: int) -> Population:
|
|
60
|
+
"""
|
|
61
|
+
Create initial population.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
size: Population size
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Initialized population
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def step(self, population: Population, evaluator: Callable) -> Population:
|
|
73
|
+
"""
|
|
74
|
+
Execute one search iteration.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
population: Current population
|
|
78
|
+
evaluator: Function to evaluate fitness
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Updated population after one step
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def should_stop(self) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Check if search should terminate.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if termination criteria met
|
|
91
|
+
"""
|
|
92
|
+
max_generations = self.config.get("max_generations", float("inf"))
|
|
93
|
+
max_evaluations = self.config.get("max_evaluations", float("inf"))
|
|
94
|
+
|
|
95
|
+
if self.generation >= max_generations:
|
|
96
|
+
logger.info(f"Stopping: reached max generations ({max_generations})")
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
if self.num_evaluations >= max_evaluations:
|
|
100
|
+
logger.info(f"Stopping: reached max evaluations ({max_evaluations})")
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
# Early stopping based on improvement
|
|
104
|
+
patience = self.config.get("early_stopping_patience", 0)
|
|
105
|
+
if patience > 0 and len(self.history) >= patience:
|
|
106
|
+
recent_fitness = [h["best_fitness"] for h in self.history[-patience:]]
|
|
107
|
+
if len(set(recent_fitness)) == 1: # No improvement
|
|
108
|
+
logger.info(f"Stopping: no improvement for {patience} generations")
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def search(
|
|
114
|
+
self,
|
|
115
|
+
evaluator: Callable,
|
|
116
|
+
population_size: int,
|
|
117
|
+
max_generations: int,
|
|
118
|
+
callbacks: Optional[List[Callable]] = None,
|
|
119
|
+
) -> Individual:
|
|
120
|
+
"""
|
|
121
|
+
Main search loop.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
evaluator: Function to evaluate fitness
|
|
125
|
+
population_size: Size of population
|
|
126
|
+
max_generations: Maximum number of generations
|
|
127
|
+
callbacks: Optional callbacks to call each generation
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Best individual found
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> engine = MySearchEngine(search_space)
|
|
134
|
+
>>> best = engine.search(evaluator, population_size=50, max_generations=100)
|
|
135
|
+
"""
|
|
136
|
+
self.config["max_generations"] = max_generations
|
|
137
|
+
callbacks = callbacks or []
|
|
138
|
+
|
|
139
|
+
logger.info(
|
|
140
|
+
f"Starting search with population_size={population_size}, max_generations={max_generations}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Initialize population
|
|
144
|
+
population = self.initialize_population(population_size)
|
|
145
|
+
|
|
146
|
+
# Evaluate initial population
|
|
147
|
+
self._evaluate_population(population, evaluator)
|
|
148
|
+
|
|
149
|
+
# Track best
|
|
150
|
+
self._update_best(population)
|
|
151
|
+
|
|
152
|
+
# Main search loop
|
|
153
|
+
while not self.should_stop():
|
|
154
|
+
# Evolution/search step
|
|
155
|
+
population = self.step(population, evaluator)
|
|
156
|
+
|
|
157
|
+
# Evaluate new individuals
|
|
158
|
+
self._evaluate_population(population, evaluator)
|
|
159
|
+
|
|
160
|
+
# Update tracking
|
|
161
|
+
self._update_best(population)
|
|
162
|
+
self._record_history(population)
|
|
163
|
+
|
|
164
|
+
# Call callbacks
|
|
165
|
+
for callback in callbacks:
|
|
166
|
+
callback(self, population)
|
|
167
|
+
|
|
168
|
+
# Log progress
|
|
169
|
+
if self.generation % 10 == 0:
|
|
170
|
+
stats = self.get_statistics()
|
|
171
|
+
logger.info(
|
|
172
|
+
f"Generation {self.generation}: "
|
|
173
|
+
f"best={stats['best_fitness']:.4f}, "
|
|
174
|
+
f"mean={stats['mean_fitness']:.4f}, "
|
|
175
|
+
f"evaluations={self.num_evaluations}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.generation += 1
|
|
179
|
+
|
|
180
|
+
logger.info(
|
|
181
|
+
f"Search complete after {self.generation} generations, {self.num_evaluations} evaluations"
|
|
182
|
+
)
|
|
183
|
+
logger.info(f"Best fitness: {self.best_individual.fitness:.4f}")
|
|
184
|
+
|
|
185
|
+
return self.best_individual
|
|
186
|
+
|
|
187
|
+
def _evaluate_population(self, population: Population, evaluator: Callable) -> None:
|
|
188
|
+
"""Evaluate all unevaluated individuals."""
|
|
189
|
+
for individual in population.individuals:
|
|
190
|
+
if individual.fitness is None:
|
|
191
|
+
individual.set_fitness(evaluator(individual.graph))
|
|
192
|
+
self.num_evaluations += 1
|
|
193
|
+
|
|
194
|
+
def _update_best(self, population: Population) -> None:
|
|
195
|
+
"""Update best individual if improvement found."""
|
|
196
|
+
current_best = population.get_best(1)[0]
|
|
197
|
+
|
|
198
|
+
if self.best_individual is None or current_best.fitness > self.best_individual.fitness:
|
|
199
|
+
self.best_individual = current_best.clone()
|
|
200
|
+
logger.debug(f"New best: {self.best_individual.fitness:.4f}")
|
|
201
|
+
|
|
202
|
+
def _record_history(self, population: Population) -> None:
|
|
203
|
+
"""Record generation statistics."""
|
|
204
|
+
entry = {
|
|
205
|
+
"generation": self.generation,
|
|
206
|
+
"best_fitness": population.best_fitness(),
|
|
207
|
+
"mean_fitness": population.average_fitness(),
|
|
208
|
+
"worst_fitness": min(
|
|
209
|
+
ind.fitness for ind in population.individuals if ind.fitness is not None
|
|
210
|
+
),
|
|
211
|
+
"diversity": population.diversity_metric(),
|
|
212
|
+
"num_evaluations": self.num_evaluations,
|
|
213
|
+
}
|
|
214
|
+
self.history.append(entry)
|
|
215
|
+
|
|
216
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
217
|
+
"""
|
|
218
|
+
Get current statistics.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary with statistics
|
|
222
|
+
"""
|
|
223
|
+
if not self.history:
|
|
224
|
+
return {
|
|
225
|
+
"generation": 0,
|
|
226
|
+
"num_evaluations": 0,
|
|
227
|
+
"best_fitness": 0.0,
|
|
228
|
+
"mean_fitness": 0.0,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
latest = self.history[-1]
|
|
232
|
+
return {
|
|
233
|
+
"generation": self.generation,
|
|
234
|
+
"num_evaluations": self.num_evaluations,
|
|
235
|
+
"best_fitness": latest["best_fitness"],
|
|
236
|
+
"mean_fitness": latest["mean_fitness"],
|
|
237
|
+
"diversity": latest["diversity"],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def get_best(self) -> ModelGraph:
|
|
241
|
+
"""
|
|
242
|
+
Get best architecture found.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Best ModelGraph
|
|
246
|
+
"""
|
|
247
|
+
if self.best_individual is None:
|
|
248
|
+
raise ValueError("No evaluations performed yet")
|
|
249
|
+
|
|
250
|
+
return self.best_individual.graph
|
|
251
|
+
|
|
252
|
+
def get_best_fitness(self) -> float:
|
|
253
|
+
"""
|
|
254
|
+
Get best fitness found.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Best fitness value
|
|
258
|
+
"""
|
|
259
|
+
if self.best_individual is None:
|
|
260
|
+
raise ValueError("No evaluations performed yet")
|
|
261
|
+
|
|
262
|
+
return self.best_individual.fitness
|
|
263
|
+
|
|
264
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
|
265
|
+
"""
|
|
266
|
+
Get complete search history.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of generation statistics
|
|
270
|
+
"""
|
|
271
|
+
return self.history
|
|
272
|
+
|
|
273
|
+
def reset(self) -> None:
|
|
274
|
+
"""Reset search engine state."""
|
|
275
|
+
self.generation = 0
|
|
276
|
+
self.history = []
|
|
277
|
+
self.best_individual = None
|
|
278
|
+
self.num_evaluations = 0
|
|
279
|
+
logger.info("Search engine reset")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class RandomSearchEngine(SearchEngine):
|
|
283
|
+
"""
|
|
284
|
+
Random search engine (baseline).
|
|
285
|
+
|
|
286
|
+
Simply samples random architectures from search space
|
|
287
|
+
without any guided search.
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
>>> engine = RandomSearchEngine(search_space)
|
|
291
|
+
>>> best = engine.search(evaluator, population_size=1, max_generations=100)
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def initialize_population(self, size: int) -> Population:
|
|
295
|
+
"""Create initial random population."""
|
|
296
|
+
individuals = []
|
|
297
|
+
for _ in range(size):
|
|
298
|
+
graph = self.search_space.sample()
|
|
299
|
+
individual = Individual(graph=graph)
|
|
300
|
+
individuals.append(individual)
|
|
301
|
+
|
|
302
|
+
return Population(individuals)
|
|
303
|
+
|
|
304
|
+
def step(self, population: Population, evaluator: Callable) -> Population:
|
|
305
|
+
"""Sample new random individuals."""
|
|
306
|
+
# Replace entire population with new random samples
|
|
307
|
+
return self.initialize_population(len(population))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class GridSearchEngine(SearchEngine):
|
|
311
|
+
"""
|
|
312
|
+
Grid search engine.
|
|
313
|
+
|
|
314
|
+
Systematically explores all combinations of parameter values.
|
|
315
|
+
Only practical for small discrete search spaces.
|
|
316
|
+
|
|
317
|
+
Note: This is a placeholder - full grid search requires
|
|
318
|
+
combinatorial enumeration of the search space.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
|
|
322
|
+
"""Initialize grid search engine."""
|
|
323
|
+
super().__init__(search_space, config)
|
|
324
|
+
self.grid_iterator = None
|
|
325
|
+
|
|
326
|
+
def initialize_population(self, size: int) -> Population:
|
|
327
|
+
"""Initialize with first batch from grid."""
|
|
328
|
+
individuals = []
|
|
329
|
+
for _ in range(size):
|
|
330
|
+
graph = self.search_space.sample()
|
|
331
|
+
individual = Individual(graph=graph)
|
|
332
|
+
individuals.append(individual)
|
|
333
|
+
|
|
334
|
+
return Population(individuals)
|
|
335
|
+
|
|
336
|
+
def step(self, population: Population, evaluator: Callable) -> Population:
|
|
337
|
+
"""Get next batch from grid."""
|
|
338
|
+
# Simplified: just sample randomly
|
|
339
|
+
# Full implementation would enumerate combinations
|
|
340
|
+
return self.initialize_population(len(population))
|