evograd-diff 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/.gitignore +1 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/PKG-INFO +50 -6
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/README.md +49 -5
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/__init__.py +4 -3
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/__init__.py +8 -8
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/cmaes.py +134 -31
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/ga.py +11 -2
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/pso.py +47 -16
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/shade.py +21 -9
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/basic.py +14 -6
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/composition.py +23 -24
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/simple.py +7 -1
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/smoothed_funnel.py +14 -5
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/run_benchmark_functions.py +16 -5
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/algorithm.py +26 -13
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/maximize.py +54 -7
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/minimize.py +118 -26
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/problem.py +6 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/result.py +2 -1
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/crossover.py +55 -33
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/mutation.py +27 -17
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/relaxations.py +18 -1
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/repair.py +9 -8
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/run_all.py +7 -0
- evograd_diff-0.3.0/evograd/tests/test_regressions.py +440 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/callbacks.py +2 -1
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/pyproject.toml +1 -1
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/LICENSE +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/algorithms/de.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/base.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/_base.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/hybrid.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/classical.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/benchmarks/functions/transforms.py +7 -7
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/core/termination.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/sampling.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/selection.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/operators/survival.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_cmaes.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_core.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_de.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_ga.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_operators.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_per_individual.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_pso.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_shade.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/tests/test_utils.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/__init__.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/device.py +0 -0
- {evograd_diff-0.2.1 → evograd_diff-0.3.0}/evograd/utils/duplicates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evograd-diff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: PyTorch-based framework for differentiable evolutionary computation and swarm intelligence
|
|
5
5
|
Project-URL: Homepage, https://github.com/andreatangherloni/EvoGrad
|
|
6
6
|
Project-URL: Repository, https://github.com/andreatangherloni/EvoGrad
|
|
@@ -85,22 +85,26 @@ problem = Problem(
|
|
|
85
85
|
|
|
86
86
|
# Run with Genetic Algorithm
|
|
87
87
|
ga = GA(pop_size=100, differentiable=True)
|
|
88
|
-
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42
|
|
88
|
+
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42,
|
|
89
|
+
lr_pop=-1, lr_hyper=-1)
|
|
89
90
|
print(f"GA Best: {result.best_fitness:.6f}")
|
|
90
91
|
|
|
91
92
|
# Run with Differential Evolution
|
|
92
93
|
de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
93
|
-
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42
|
|
94
|
+
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42,
|
|
95
|
+
lr_hyper=-1)
|
|
94
96
|
print(f"DE Best: {result.best_fitness:.6f}")
|
|
95
97
|
|
|
96
98
|
# Run with Particle Swarm Optimisation
|
|
97
99
|
pso = PSO(pop_size=100, adaptive=True, differentiable=True)
|
|
98
|
-
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42
|
|
100
|
+
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42,
|
|
101
|
+
lr_pop=-1, lr_hyper=-1)
|
|
99
102
|
print(f"PSO Best: {result.best_fitness:.6f}")
|
|
100
103
|
|
|
101
104
|
# Run with CMA-ES
|
|
102
105
|
cmaes = CMAES(sigma=0.5, adaptive=True)
|
|
103
|
-
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42
|
|
106
|
+
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42,
|
|
107
|
+
lr_hyper=-1)
|
|
104
108
|
print(f"CMA-ES Best: {result.best_fitness:.6f}")
|
|
105
109
|
```
|
|
106
110
|
|
|
@@ -140,6 +144,12 @@ ga = GA(
|
|
|
140
144
|
| Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
|
|
141
145
|
| Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
|
|
142
146
|
|
|
147
|
+
Gradient optimizers are opt-in at the `minimize()` call. `lr_pop=None` and
|
|
148
|
+
`lr_hyper=None` (the defaults) perform no backpropagation; use `-1` to select
|
|
149
|
+
EvoGrad's algorithm-specific learning rate or pass a numeric value. For strict
|
|
150
|
+
classical behavior, also construct the algorithm with `differentiable=False`
|
|
151
|
+
and `adaptive=False`.
|
|
152
|
+
|
|
143
153
|
### Differential Evolution (DE)
|
|
144
154
|
|
|
145
155
|
DE uses **algorithm-level flags** for adaptive hyperparameters and differentiable population:
|
|
@@ -453,6 +463,9 @@ The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenb
|
|
|
453
463
|
The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
|
|
454
464
|
|
|
455
465
|
```bash
|
|
466
|
+
# Install the manuscript's pinned cross-library baseline
|
|
467
|
+
pip install -r requirements-benchmarks.txt
|
|
468
|
+
|
|
456
469
|
# 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
|
|
457
470
|
python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
|
|
458
471
|
|
|
@@ -494,6 +507,37 @@ The **Adaptive** variant reaches the lowest mean/variance, while **Full** matche
|
|
|
494
507
|
|
|
495
508
|
> Full experimental details are in the paper (see [Citation](#-citation)).
|
|
496
509
|
|
|
510
|
+
### Reproducibility note (v0.3.0)
|
|
511
|
+
|
|
512
|
+
The results reported above and in the paper were produced with the **paper-era
|
|
513
|
+
code (`evograd-diff` v0.1.2)**. Version 0.3.0 incorporates a set of
|
|
514
|
+
post-publication audit fixes that can change the *absolute* numbers, so results
|
|
515
|
+
obtained with v0.3.0 may differ from the published tables. The corrections that
|
|
516
|
+
affect benchmark values include:
|
|
517
|
+
|
|
518
|
+
- a restored cyclic term in the Expanded Schaffer F6 component, which changes
|
|
519
|
+
CEC 2017 **F16, F19, F26–F30**;
|
|
520
|
+
- NaN-free CEC 2017 composition weights at component centres and the true CEC
|
|
521
|
+
**F9** optimum location (F8/F9 numerics otherwise preserved);
|
|
522
|
+
- matched **pymoo** GA/CMA-ES baseline configurations (population size, per-gene
|
|
523
|
+
operator probabilities, un-normalised CMA-ES step size);
|
|
524
|
+
- a consistent **Adam** baseline (fitness paired with the evaluated iterate);
|
|
525
|
+
- **scale-invariant** softmax recombination in differentiable CMA-ES; and
|
|
526
|
+
- consistent differentiable-**PSO** state (the committed position is re-evaluated so
|
|
527
|
+
population and fitness agree; optimisation quality is unchanged).
|
|
528
|
+
|
|
529
|
+
These changes primarily affect absolute fitness values and the cross-library
|
|
530
|
+
baseline comparisons; the paper's qualitative findings (all EvoGrad variants and
|
|
531
|
+
baselines within a run share the same objective implementation) are not expected
|
|
532
|
+
to change, but the exact significance percentages and per-function rankings will.
|
|
533
|
+
**To reproduce the paper exactly, install the paper-era release:**
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
pip install evograd-diff==0.1.2
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
See [`CHANGELOG.md`](CHANGELOG.md) for the complete list of changes.
|
|
540
|
+
|
|
497
541
|
## 📖 Citation
|
|
498
542
|
|
|
499
543
|
EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
|
|
@@ -524,4 +568,4 @@ Contributions are welcome! Please feel free to submit a Pull Request. For major
|
|
|
524
568
|
## 🙏 Acknowledgements
|
|
525
569
|
|
|
526
570
|
- Inspired by [pymoo](https://pymoo.org/) for API design
|
|
527
|
-
- Built with [PyTorch](https://pytorch.org/) for automatic differentiation
|
|
571
|
+
- Built with [PyTorch](https://pytorch.org/) for automatic differentiation
|
|
@@ -58,22 +58,26 @@ problem = Problem(
|
|
|
58
58
|
|
|
59
59
|
# Run with Genetic Algorithm
|
|
60
60
|
ga = GA(pop_size=100, differentiable=True)
|
|
61
|
-
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42
|
|
61
|
+
result = minimize(problem, ga, termination=MaxEvaluations(10000), seed=42,
|
|
62
|
+
lr_pop=-1, lr_hyper=-1)
|
|
62
63
|
print(f"GA Best: {result.best_fitness:.6f}")
|
|
63
64
|
|
|
64
65
|
# Run with Differential Evolution
|
|
65
66
|
de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
66
|
-
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42
|
|
67
|
+
result = minimize(problem, de, termination=MaxEvaluations(10000), seed=42,
|
|
68
|
+
lr_hyper=-1)
|
|
67
69
|
print(f"DE Best: {result.best_fitness:.6f}")
|
|
68
70
|
|
|
69
71
|
# Run with Particle Swarm Optimisation
|
|
70
72
|
pso = PSO(pop_size=100, adaptive=True, differentiable=True)
|
|
71
|
-
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42
|
|
73
|
+
result = minimize(problem, pso, termination=MaxEvaluations(10000), seed=42,
|
|
74
|
+
lr_pop=-1, lr_hyper=-1)
|
|
72
75
|
print(f"PSO Best: {result.best_fitness:.6f}")
|
|
73
76
|
|
|
74
77
|
# Run with CMA-ES
|
|
75
78
|
cmaes = CMAES(sigma=0.5, adaptive=True)
|
|
76
|
-
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42
|
|
79
|
+
result = minimize(problem, cmaes, termination=MaxEvaluations(10000), seed=42,
|
|
80
|
+
lr_hyper=-1)
|
|
77
81
|
print(f"CMA-ES Best: {result.best_fitness:.6f}")
|
|
78
82
|
```
|
|
79
83
|
|
|
@@ -113,6 +117,12 @@ ga = GA(
|
|
|
113
117
|
| Operator `adaptive=True` | Operator uses Gumbel-Softmax/Binary-Concrete for gradient flow |
|
|
114
118
|
| Operator `learn_*=True` | Operator hyperparameters become learnable `nn.Parameter` |
|
|
115
119
|
|
|
120
|
+
Gradient optimizers are opt-in at the `minimize()` call. `lr_pop=None` and
|
|
121
|
+
`lr_hyper=None` (the defaults) perform no backpropagation; use `-1` to select
|
|
122
|
+
EvoGrad's algorithm-specific learning rate or pass a numeric value. For strict
|
|
123
|
+
classical behavior, also construct the algorithm with `differentiable=False`
|
|
124
|
+
and `adaptive=False`.
|
|
125
|
+
|
|
116
126
|
### Differential Evolution (DE)
|
|
117
127
|
|
|
118
128
|
DE uses **algorithm-level flags** for adaptive hyperparameters and differentiable population:
|
|
@@ -426,6 +436,9 @@ The **Multi-Basin** functions aggregate `K` basins (each a full Rastrigin/Rosenb
|
|
|
426
436
|
The runner evaluates the four EvoGrad modes — **Classical**, **Differentiable**, **Adaptive**, **Full** — and, by default, the **pymoo** and **Adam** (multi-start) baselines:
|
|
427
437
|
|
|
428
438
|
```bash
|
|
439
|
+
# Install the manuscript's pinned cross-library baseline
|
|
440
|
+
pip install -r requirements-benchmarks.txt
|
|
441
|
+
|
|
429
442
|
# 30 runs of DE on the full CEC 2017 suite in 30D (vs pymoo + Adam)
|
|
430
443
|
python -m evograd.benchmarks.run_benchmark_functions -a DE -s cec2017 -D 30 -r 30
|
|
431
444
|
|
|
@@ -467,6 +480,37 @@ The **Adaptive** variant reaches the lowest mean/variance, while **Full** matche
|
|
|
467
480
|
|
|
468
481
|
> Full experimental details are in the paper (see [Citation](#-citation)).
|
|
469
482
|
|
|
483
|
+
### Reproducibility note (v0.3.0)
|
|
484
|
+
|
|
485
|
+
The results reported above and in the paper were produced with the **paper-era
|
|
486
|
+
code (`evograd-diff` v0.1.2)**. Version 0.3.0 incorporates a set of
|
|
487
|
+
post-publication audit fixes that can change the *absolute* numbers, so results
|
|
488
|
+
obtained with v0.3.0 may differ from the published tables. The corrections that
|
|
489
|
+
affect benchmark values include:
|
|
490
|
+
|
|
491
|
+
- a restored cyclic term in the Expanded Schaffer F6 component, which changes
|
|
492
|
+
CEC 2017 **F16, F19, F26–F30**;
|
|
493
|
+
- NaN-free CEC 2017 composition weights at component centres and the true CEC
|
|
494
|
+
**F9** optimum location (F8/F9 numerics otherwise preserved);
|
|
495
|
+
- matched **pymoo** GA/CMA-ES baseline configurations (population size, per-gene
|
|
496
|
+
operator probabilities, un-normalised CMA-ES step size);
|
|
497
|
+
- a consistent **Adam** baseline (fitness paired with the evaluated iterate);
|
|
498
|
+
- **scale-invariant** softmax recombination in differentiable CMA-ES; and
|
|
499
|
+
- consistent differentiable-**PSO** state (the committed position is re-evaluated so
|
|
500
|
+
population and fitness agree; optimisation quality is unchanged).
|
|
501
|
+
|
|
502
|
+
These changes primarily affect absolute fitness values and the cross-library
|
|
503
|
+
baseline comparisons; the paper's qualitative findings (all EvoGrad variants and
|
|
504
|
+
baselines within a run share the same objective implementation) are not expected
|
|
505
|
+
to change, but the exact significance percentages and per-function rankings will.
|
|
506
|
+
**To reproduce the paper exactly, install the paper-era release:**
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
pip install evograd-diff==0.1.2
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
See [`CHANGELOG.md`](CHANGELOG.md) for the complete list of changes.
|
|
513
|
+
|
|
470
514
|
## 📖 Citation
|
|
471
515
|
|
|
472
516
|
EvoGrad was accepted at the **IEEE Congress on Evolutionary Computation (CEC) 2026**. If you use EvoGrad in your research, please cite:
|
|
@@ -497,4 +541,4 @@ Contributions are welcome! Please feel free to submit a Pull Request. For major
|
|
|
497
541
|
## 🙏 Acknowledgements
|
|
498
542
|
|
|
499
543
|
- Inspired by [pymoo](https://pymoo.org/) for API design
|
|
500
|
-
- Built with [PyTorch](https://pytorch.org/) for automatic differentiation
|
|
544
|
+
- Built with [PyTorch](https://pytorch.org/) for automatic differentiation
|
|
@@ -36,7 +36,8 @@ Quick Start
|
|
|
36
36
|
... )
|
|
37
37
|
>>>
|
|
38
38
|
>>> # Run optimization
|
|
39
|
-
>>> result = minimize(problem, algorithm, termination=MaxEvaluations(10000), seed=42
|
|
39
|
+
>>> result = minimize(problem, algorithm, termination=MaxEvaluations(10000), seed=42,
|
|
40
|
+
... lr_pop=-1, lr_hyper=-1)
|
|
40
41
|
>>> print(f"Best fitness: {result.best_fitness:.6f}")
|
|
41
42
|
|
|
42
43
|
Architecture
|
|
@@ -57,11 +58,11 @@ Authors
|
|
|
57
58
|
Andrea Tangherloni <andrea.tangherloni@unibocconi.it>
|
|
58
59
|
"""
|
|
59
60
|
|
|
60
|
-
__version__ = "0.
|
|
61
|
+
__version__ = "0.3.0"
|
|
61
62
|
__author__ = "Andrea Tangherloni"
|
|
62
63
|
|
|
63
64
|
__all__ = [
|
|
64
65
|
# Version info
|
|
65
66
|
"__version__",
|
|
66
67
|
"__author__",
|
|
67
|
-
]
|
|
68
|
+
]
|
|
@@ -18,7 +18,7 @@ the dependency injection pattern for operators.
|
|
|
18
18
|
|
|
19
19
|
Example:
|
|
20
20
|
>>> from evograd.algorithms import GA, DE, SHADE, PSO, CMAES
|
|
21
|
-
>>> from evograd.core import Problem, minimize
|
|
21
|
+
>>> from evograd.core import Problem, minimize, MaxEvaluations
|
|
22
22
|
>>>
|
|
23
23
|
>>> problem = Problem(
|
|
24
24
|
... objective=lambda x: (x**2).sum(dim=-1),
|
|
@@ -29,27 +29,27 @@ Example:
|
|
|
29
29
|
>>>
|
|
30
30
|
>>> # Genetic Algorithm
|
|
31
31
|
>>> ga = GA(pop_size=100, differentiable=True)
|
|
32
|
-
>>> result = minimize(problem, ga,
|
|
32
|
+
>>> result = minimize(problem, ga, MaxEvaluations(10000), lr_pop=-1, lr_hyper=-1)
|
|
33
33
|
>>>
|
|
34
34
|
>>> # Differential Evolution
|
|
35
35
|
>>> de = DE(pop_size=100, variant="DE/rand/1/bin", adaptive=True)
|
|
36
|
-
>>> result = minimize(problem, de,
|
|
36
|
+
>>> result = minimize(problem, de, MaxEvaluations(10000), lr_hyper=-1)
|
|
37
37
|
>>>
|
|
38
38
|
>>> # SHADE (Self-Adaptive DE)
|
|
39
39
|
>>> shade = SHADE(pop_size=100, memory_size=100)
|
|
40
|
-
>>> result = minimize(problem, shade,
|
|
40
|
+
>>> result = minimize(problem, shade, MaxEvaluations(10000))
|
|
41
41
|
>>>
|
|
42
42
|
>>> # L-SHADE (SHADE with population reduction)
|
|
43
43
|
>>> lshade = LSHADE(pop_size_init=18*30, pop_size_min=4)
|
|
44
|
-
>>> result = minimize(problem, lshade,
|
|
44
|
+
>>> result = minimize(problem, lshade, MaxEvaluations(10000))
|
|
45
45
|
>>>
|
|
46
46
|
>>> # Particle Swarm Optimisation
|
|
47
47
|
>>> pso = PSO(pop_size=100, adaptive=True)
|
|
48
|
-
>>> result = minimize(problem, pso,
|
|
48
|
+
>>> result = minimize(problem, pso, MaxEvaluations(10000), lr_hyper=-1)
|
|
49
49
|
>>>
|
|
50
50
|
>>> # CMA-ES
|
|
51
51
|
>>> cmaes = CMAES(pop_size=50, adaptive=True)
|
|
52
|
-
>>> result = minimize(problem, cmaes,
|
|
52
|
+
>>> result = minimize(problem, cmaes, MaxEvaluations(10000), lr_hyper=-1)
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
# Genetic Algorithm
|
|
@@ -135,4 +135,4 @@ __all__ = [
|
|
|
135
135
|
"cmaes_adaptive",
|
|
136
136
|
"cmaes_ipop",
|
|
137
137
|
"cmaes_bipop",
|
|
138
|
-
]
|
|
138
|
+
]
|
|
@@ -234,6 +234,7 @@ class CMAES(Algorithm):
|
|
|
234
234
|
repair: Optional[nn.Module] = None,
|
|
235
235
|
adaptive: bool = False,
|
|
236
236
|
differentiable: bool = False,
|
|
237
|
+
selection_temperature: float = 1.0,
|
|
237
238
|
dtype: torch.dtype = torch.float32,
|
|
238
239
|
) -> None:
|
|
239
240
|
self.adaptive = adaptive
|
|
@@ -244,6 +245,9 @@ class CMAES(Algorithm):
|
|
|
244
245
|
self._init_c1 = c1
|
|
245
246
|
self._init_cmu = cmu
|
|
246
247
|
self._init_damps = damps
|
|
248
|
+
if selection_temperature <= 0:
|
|
249
|
+
raise ValueError("selection_temperature must be > 0")
|
|
250
|
+
self._init_selection_temperature = selection_temperature
|
|
247
251
|
|
|
248
252
|
# Restart parameters
|
|
249
253
|
self._restarts = restarts
|
|
@@ -287,7 +291,9 @@ class CMAES(Algorithm):
|
|
|
287
291
|
# Compute default population size if not provided
|
|
288
292
|
if self._requested_pop_size is None:
|
|
289
293
|
n_var = self.problem.n_var
|
|
290
|
-
|
|
294
|
+
default_size = 4 + int(3 * math.log(n_var))
|
|
295
|
+
factor = float(getattr(self, "_pop_size_factor", 1.0))
|
|
296
|
+
self.pop_size = max(2, int(round(default_size * factor)))
|
|
291
297
|
self.n_offsprings = self.pop_size
|
|
292
298
|
|
|
293
299
|
def _setup(self) -> None:
|
|
@@ -303,6 +309,16 @@ class CMAES(Algorithm):
|
|
|
303
309
|
|
|
304
310
|
# Compute recombination weights
|
|
305
311
|
self._setup_weights()
|
|
312
|
+
|
|
313
|
+
log_temperature = torch.tensor(
|
|
314
|
+
math.log(self._init_selection_temperature),
|
|
315
|
+
device=self.device,
|
|
316
|
+
dtype=self.dtype,
|
|
317
|
+
)
|
|
318
|
+
if self.adaptive:
|
|
319
|
+
self._log_selection_temperature = nn.Parameter(log_temperature)
|
|
320
|
+
else:
|
|
321
|
+
self.register_buffer("_log_selection_temperature", log_temperature)
|
|
306
322
|
|
|
307
323
|
# Setup mean
|
|
308
324
|
self._setup_mean(n_var)
|
|
@@ -517,6 +533,37 @@ class CMAES(Algorithm):
|
|
|
517
533
|
"""Covariance matrix C = L @ L.T."""
|
|
518
534
|
L = self.L
|
|
519
535
|
return L @ L.T
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def selection_temperature(self) -> Tensor:
|
|
539
|
+
"""Temperature used by differentiable fitness recombination."""
|
|
540
|
+
return self._log_selection_temperature.exp()
|
|
541
|
+
|
|
542
|
+
def _soft_recombination_weights(self, fitness: Tensor) -> Tensor:
|
|
543
|
+
"""Scale-invariant temperature-controlled fitness weights."""
|
|
544
|
+
spread = fitness.detach().std(unbiased=False).clamp_min(
|
|
545
|
+
torch.finfo(fitness.dtype).tiny
|
|
546
|
+
)
|
|
547
|
+
temperature = self.selection_temperature * spread
|
|
548
|
+
# Center first so a constant, non-zero fitness vector does not overflow
|
|
549
|
+
# when its (correctly tiny) scale is used as the temperature.
|
|
550
|
+
centered = fitness - fitness.detach().mean()
|
|
551
|
+
return torch.softmax(-centered / temperature, dim=0)
|
|
552
|
+
|
|
553
|
+
def _coefficients_for_mass(self, mu_eff: Tensor, soft: bool) -> tuple:
|
|
554
|
+
"""Return CMA adaptation coefficients consistent with selection mass."""
|
|
555
|
+
if not soft or self.adaptive:
|
|
556
|
+
return self.cc, self.cs, self.c1, self.cmu, self.damps
|
|
557
|
+
|
|
558
|
+
d = float(self.n_var)
|
|
559
|
+
m = float(mu_eff.detach())
|
|
560
|
+
cc = (4 + m / d) / (d + 4 + 2 * m / d)
|
|
561
|
+
cs = (m + 2) / (d + m + 5)
|
|
562
|
+
c1 = 2 / ((d + 1.3) ** 2 + m)
|
|
563
|
+
cmu = max(0.0, min(1 - c1, 2 * (m - 2 + 1 / m) / ((d + 2) ** 2 + m)))
|
|
564
|
+
damps = 1 + 2 * max(0, math.sqrt((m - 1) / (d + 1)) - 1) + cs
|
|
565
|
+
values = (cc, cs, c1, cmu, damps)
|
|
566
|
+
return tuple(torch.as_tensor(v, device=self.device, dtype=self.dtype) for v in values)
|
|
520
567
|
|
|
521
568
|
@property
|
|
522
569
|
def p_sigma(self) -> Tensor:
|
|
@@ -616,6 +663,20 @@ class CMAES(Algorithm):
|
|
|
616
663
|
offspring = torch.clamp(offspring, self.xl, self.xu)
|
|
617
664
|
|
|
618
665
|
return offspring
|
|
666
|
+
|
|
667
|
+
def forward(self, reduction: str = "mean", live_selection: bool = True) -> Tensor:
|
|
668
|
+
"""Build a generation graph with soft fitness recombination.
|
|
669
|
+
|
|
670
|
+
Classical CMA-ES keeps the requested generic reduction. Differentiable
|
|
671
|
+
and adaptive variants use temperature-controlled softmax weights so
|
|
672
|
+
selection itself contributes gradients, matching the manuscript model.
|
|
673
|
+
"""
|
|
674
|
+
loss = super().forward(reduction=reduction, live_selection=live_selection)
|
|
675
|
+
if not (self.differentiable or self.adaptive):
|
|
676
|
+
return loss
|
|
677
|
+
fitness = self._pending_fitness
|
|
678
|
+
weights = self._soft_recombination_weights(fitness)
|
|
679
|
+
return (weights * fitness).sum()
|
|
619
680
|
|
|
620
681
|
def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
|
|
621
682
|
"""
|
|
@@ -625,30 +686,52 @@ class CMAES(Algorithm):
|
|
|
625
686
|
offspring: Offspring population [pop_size, n_var].
|
|
626
687
|
offspring_fitness: Fitness values [pop_size].
|
|
627
688
|
"""
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
689
|
+
if self.differentiable or self.adaptive:
|
|
690
|
+
recombination_weights = self._soft_recombination_weights(offspring_fitness)
|
|
691
|
+
y_selected = self._pending_y
|
|
692
|
+
soft_recombination = True
|
|
693
|
+
else:
|
|
694
|
+
sorted_indices = torch.argsort(offspring_fitness)
|
|
695
|
+
selected_indices = sorted_indices[:self._mu]
|
|
696
|
+
y_selected = self._pending_y[selected_indices]
|
|
697
|
+
recombination_weights = self._weights
|
|
698
|
+
soft_recombination = False
|
|
699
|
+
|
|
700
|
+
mu_eff = 1.0 / recombination_weights.square().sum().clamp_min(self._eps)
|
|
701
|
+
cc, cs, c1, cmu, damps = self._coefficients_for_mass(
|
|
702
|
+
mu_eff,
|
|
703
|
+
soft_recombination,
|
|
704
|
+
)
|
|
705
|
+
self._last_mu_eff = float(mu_eff.detach())
|
|
706
|
+
|
|
638
707
|
# Weighted recombination in y-space
|
|
639
|
-
y_w = (
|
|
708
|
+
y_w = (recombination_weights.unsqueeze(-1) * y_selected).sum(dim=0)
|
|
640
709
|
|
|
641
710
|
# Update mean
|
|
642
711
|
new_mean = self.mean + self.sigma * y_w
|
|
643
712
|
|
|
644
713
|
# Update evolution paths
|
|
645
|
-
new_p_sigma, new_p_c, h_sigma = self._update_evolution_paths(
|
|
714
|
+
new_p_sigma, new_p_c, h_sigma = self._update_evolution_paths(
|
|
715
|
+
y_w,
|
|
716
|
+
mu_eff,
|
|
717
|
+
cc,
|
|
718
|
+
cs,
|
|
719
|
+
soft_recombination,
|
|
720
|
+
)
|
|
646
721
|
|
|
647
722
|
# Update covariance
|
|
648
|
-
new_L = self._update_covariance(
|
|
723
|
+
new_L = self._update_covariance(
|
|
724
|
+
y_selected,
|
|
725
|
+
new_p_c,
|
|
726
|
+
h_sigma,
|
|
727
|
+
recombination_weights,
|
|
728
|
+
cc,
|
|
729
|
+
c1,
|
|
730
|
+
cmu,
|
|
731
|
+
)
|
|
649
732
|
|
|
650
733
|
# Update step-size
|
|
651
|
-
new_sigma = self._update_sigma(new_p_sigma)
|
|
734
|
+
new_sigma = self._update_sigma(new_p_sigma, cs, damps)
|
|
652
735
|
|
|
653
736
|
# Commit updates
|
|
654
737
|
self._commit_updates(
|
|
@@ -682,7 +765,14 @@ class CMAES(Algorithm):
|
|
|
682
765
|
del self._pending_z
|
|
683
766
|
del self._pending_y
|
|
684
767
|
|
|
685
|
-
def _update_evolution_paths(
|
|
768
|
+
def _update_evolution_paths(
|
|
769
|
+
self,
|
|
770
|
+
y_w: Tensor,
|
|
771
|
+
mu_eff: Tensor,
|
|
772
|
+
cc: Tensor,
|
|
773
|
+
cs: Tensor,
|
|
774
|
+
soft: bool,
|
|
775
|
+
) -> tuple:
|
|
686
776
|
"""
|
|
687
777
|
Update evolution paths p_σ and p_c.
|
|
688
778
|
|
|
@@ -693,7 +783,6 @@ class CMAES(Algorithm):
|
|
|
693
783
|
Tuple of (new_p_sigma, new_p_c, h_sigma).
|
|
694
784
|
"""
|
|
695
785
|
D = self.n_var
|
|
696
|
-
mu_eff = self._mu_eff
|
|
697
786
|
|
|
698
787
|
# Compute C^(-1/2) @ y_w using L^(-1) @ y_w
|
|
699
788
|
L = self.L
|
|
@@ -701,8 +790,15 @@ class CMAES(Algorithm):
|
|
|
701
790
|
z_w = torch.linalg.solve_triangular(L, y_w.unsqueeze(-1), upper=False).squeeze(-1)
|
|
702
791
|
|
|
703
792
|
# Update p_sigma (conjugate evolution path)
|
|
704
|
-
|
|
705
|
-
|
|
793
|
+
if soft:
|
|
794
|
+
sigma_path_scale = torch.sqrt(cs * (2 - cs) * mu_eff)
|
|
795
|
+
covariance_path_scale = torch.sqrt(cc * (2 - cc) * mu_eff)
|
|
796
|
+
else:
|
|
797
|
+
# Preserve the original classical implementation's scalar path
|
|
798
|
+
# exactly; only relaxed recombination needs tensor-valued scales.
|
|
799
|
+
sigma_path_scale = math.sqrt(cs * (2 - cs) * mu_eff)
|
|
800
|
+
covariance_path_scale = math.sqrt(cc * (2 - cc) * mu_eff)
|
|
801
|
+
new_p_sigma = (1 - cs) * self.p_sigma + sigma_path_scale * z_w
|
|
706
802
|
|
|
707
803
|
# Heaviside function h_sigma (smooth approximation)
|
|
708
804
|
norm_p_sigma = new_p_sigma.norm()
|
|
@@ -710,8 +806,7 @@ class CMAES(Algorithm):
|
|
|
710
806
|
h_sigma = torch.sigmoid(10 * (threshold - norm_p_sigma / self._chi_n))
|
|
711
807
|
|
|
712
808
|
# Update p_c (evolution path for covariance)
|
|
713
|
-
|
|
714
|
-
new_p_c = (1 - cc) * self.p_c + h_sigma * math.sqrt(cc * (2 - cc) * mu_eff) * y_w
|
|
809
|
+
new_p_c = (1 - cc) * self.p_c + h_sigma * covariance_path_scale * y_w
|
|
715
810
|
|
|
716
811
|
return new_p_sigma, new_p_c, h_sigma
|
|
717
812
|
|
|
@@ -720,6 +815,10 @@ class CMAES(Algorithm):
|
|
|
720
815
|
y_selected: Tensor,
|
|
721
816
|
new_p_c: Tensor,
|
|
722
817
|
h_sigma: Tensor,
|
|
818
|
+
recombination_weights: Tensor,
|
|
819
|
+
cc: Tensor,
|
|
820
|
+
c1: Tensor,
|
|
821
|
+
cmu: Tensor,
|
|
723
822
|
) -> Tensor:
|
|
724
823
|
"""
|
|
725
824
|
Update covariance matrix C.
|
|
@@ -733,9 +832,6 @@ class CMAES(Algorithm):
|
|
|
733
832
|
New Cholesky factor L.
|
|
734
833
|
"""
|
|
735
834
|
D = self.n_var
|
|
736
|
-
c1 = self.c1
|
|
737
|
-
cmu = self.cmu
|
|
738
|
-
cc = self.cc
|
|
739
835
|
|
|
740
836
|
# Current covariance
|
|
741
837
|
L = self.L
|
|
@@ -746,7 +842,7 @@ class CMAES(Algorithm):
|
|
|
746
842
|
|
|
747
843
|
# Rank-mu update
|
|
748
844
|
rank_mu = (
|
|
749
|
-
|
|
845
|
+
recombination_weights.unsqueeze(-1).unsqueeze(-1) *
|
|
750
846
|
y_selected.unsqueeze(-1) * y_selected.unsqueeze(-2)
|
|
751
847
|
).sum(dim=0)
|
|
752
848
|
|
|
@@ -850,7 +946,12 @@ class CMAES(Algorithm):
|
|
|
850
946
|
|
|
851
947
|
return L_identity
|
|
852
948
|
|
|
853
|
-
def _update_sigma(
|
|
949
|
+
def _update_sigma(
|
|
950
|
+
self,
|
|
951
|
+
new_p_sigma: Tensor,
|
|
952
|
+
cs: Tensor,
|
|
953
|
+
damps: Tensor,
|
|
954
|
+
) -> Tensor:
|
|
854
955
|
"""
|
|
855
956
|
Update step-size using CSA (Cumulative Step-size Adaptation).
|
|
856
957
|
|
|
@@ -860,9 +961,6 @@ class CMAES(Algorithm):
|
|
|
860
961
|
Returns:
|
|
861
962
|
New step-size.
|
|
862
963
|
"""
|
|
863
|
-
cs = self.cs
|
|
864
|
-
damps = self.damps
|
|
865
|
-
|
|
866
964
|
# Step-size update factor
|
|
867
965
|
norm_p_sigma = new_p_sigma.norm()
|
|
868
966
|
factor = torch.exp((cs / damps) * (norm_p_sigma / self._chi_n - 1))
|
|
@@ -1098,6 +1196,10 @@ class CMAES(Algorithm):
|
|
|
1098
1196
|
if self.adaptive:
|
|
1099
1197
|
# Sigma in (1e-10, 1e10)
|
|
1100
1198
|
self._log_sigma.clamp_(min=-23, max=23)
|
|
1199
|
+
self._log_selection_temperature.clamp_(
|
|
1200
|
+
min=math.log(0.05),
|
|
1201
|
+
max=math.log(100.0),
|
|
1202
|
+
)
|
|
1101
1203
|
|
|
1102
1204
|
# Ensure c1 + cmu <= 1
|
|
1103
1205
|
c1 = torch.sigmoid(self._c1_logit)
|
|
@@ -1118,7 +1220,8 @@ class CMAES(Algorithm):
|
|
|
1118
1220
|
return {
|
|
1119
1221
|
'pop_size': self.pop_size,
|
|
1120
1222
|
'mu': self._mu,
|
|
1121
|
-
'
|
|
1223
|
+
'selection_temperature': float(self.selection_temperature.detach()),
|
|
1224
|
+
'mu_eff': getattr(self, '_last_mu_eff', self._mu_eff),
|
|
1122
1225
|
'sigma': float(self.sigma.item()),
|
|
1123
1226
|
'cc': float(self.cc.item()),
|
|
1124
1227
|
'cs': float(self.cs.item()),
|
|
@@ -1362,4 +1465,4 @@ def cmaes_bipop(
|
|
|
1362
1465
|
incpopsize=incpopsize,
|
|
1363
1466
|
bipop=True,
|
|
1364
1467
|
**kwargs,
|
|
1365
|
-
)
|
|
1468
|
+
)
|
|
@@ -57,7 +57,7 @@ Example:
|
|
|
57
57
|
Reference:
|
|
58
58
|
Holland, J. H. (1992). Genetic Algorithms. Scientific American.
|
|
59
59
|
Goldberg, D. E. (1989). Genetic Algorithms in Search, Optimization,
|
|
60
|
-
|
|
60
|
+
and Machine Learning.
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
from __future__ import annotations
|
|
@@ -69,6 +69,7 @@ import torch.nn as nn
|
|
|
69
69
|
from torch import Tensor
|
|
70
70
|
|
|
71
71
|
from evograd.core.algorithm import Algorithm
|
|
72
|
+
from evograd.operators.repair import clamp_to_bounds
|
|
72
73
|
|
|
73
74
|
if TYPE_CHECKING:
|
|
74
75
|
from evograd.core.problem import Problem
|
|
@@ -274,7 +275,15 @@ class GA(Algorithm):
|
|
|
274
275
|
|
|
275
276
|
# 3. MUTATION: Introduce variation
|
|
276
277
|
offspring = self.mutation(offspring, self.xl, self.xu)
|
|
277
|
-
|
|
278
|
+
|
|
279
|
+
# 4. Bound safety: when no repair operator is configured, clamp to the
|
|
280
|
+
# box so out-of-bounds points are never evaluated regardless of the
|
|
281
|
+
# mutation/crossover operators (mirrors DE/PSO/CMA-ES/SHADE). When a
|
|
282
|
+
# repair IS configured the base step/forward applies it. This is a
|
|
283
|
+
# no-op for the bounded default PolynomialMutation.
|
|
284
|
+
if self.repair is None:
|
|
285
|
+
offspring = clamp_to_bounds(offspring, self.xl, self.xu)
|
|
286
|
+
|
|
278
287
|
return offspring
|
|
279
288
|
|
|
280
289
|
def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
|