evograd-diff 0.2.0__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.0 → evograd_diff-0.3.0}/.gitignore +1 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/PKG-INFO +50 -6
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/README.md +49 -5
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/__init__.py +4 -3
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/__init__.py +8 -8
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/cmaes.py +138 -38
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/de.py +4 -4
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/ga.py +11 -2
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/pso.py +50 -18
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/algorithms/shade.py +26 -15
- evograd_diff-0.3.0/evograd/benchmarks/functions/cec2017/_base.py +51 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/basic.py +14 -6
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/composition.py +23 -24
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/hybrid.py +10 -40
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/simple.py +16 -47
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/smoothed_funnel.py +14 -5
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/run_benchmark_functions.py +16 -5
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/algorithm.py +26 -13
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/maximize.py +54 -7
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/minimize.py +118 -26
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/problem.py +6 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/result.py +2 -1
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/crossover.py +58 -38
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/mutation.py +33 -25
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/relaxations.py +48 -2
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/repair.py +20 -9
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/sampling.py +5 -7
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/selection.py +2 -4
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/run_all.py +28 -0
- evograd_diff-0.3.0/evograd/tests/test_de.py +62 -0
- evograd_diff-0.3.0/evograd/tests/test_pso.py +62 -0
- evograd_diff-0.3.0/evograd/tests/test_regressions.py +440 -0
- evograd_diff-0.3.0/evograd/tests/test_shade.py +64 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/callbacks.py +2 -1
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/pyproject.toml +1 -1
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/LICENSE +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/base.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/cec2017/data.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/classical.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/benchmarks/functions/transforms.py +7 -7
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/core/termination.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/operators/survival.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_cmaes.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_core.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_ga.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_operators.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_per_individual.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/tests/test_utils.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/__init__.py +0 -0
- {evograd_diff-0.2.0 → evograd_diff-0.3.0}/evograd/utils/device.py +0 -0
- {evograd_diff-0.2.0 → 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
|
+
]
|
|
@@ -83,6 +83,7 @@ import torch.nn as nn
|
|
|
83
83
|
from torch import Tensor
|
|
84
84
|
|
|
85
85
|
from evograd.core.algorithm import Algorithm
|
|
86
|
+
from evograd.operators.relaxations import standard_normal, log_param
|
|
86
87
|
|
|
87
88
|
if TYPE_CHECKING:
|
|
88
89
|
from evograd.core.problem import Problem
|
|
@@ -233,6 +234,7 @@ class CMAES(Algorithm):
|
|
|
233
234
|
repair: Optional[nn.Module] = None,
|
|
234
235
|
adaptive: bool = False,
|
|
235
236
|
differentiable: bool = False,
|
|
237
|
+
selection_temperature: float = 1.0,
|
|
236
238
|
dtype: torch.dtype = torch.float32,
|
|
237
239
|
) -> None:
|
|
238
240
|
self.adaptive = adaptive
|
|
@@ -243,6 +245,9 @@ class CMAES(Algorithm):
|
|
|
243
245
|
self._init_c1 = c1
|
|
244
246
|
self._init_cmu = cmu
|
|
245
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
|
|
246
251
|
|
|
247
252
|
# Restart parameters
|
|
248
253
|
self._restarts = restarts
|
|
@@ -286,7 +291,9 @@ class CMAES(Algorithm):
|
|
|
286
291
|
# Compute default population size if not provided
|
|
287
292
|
if self._requested_pop_size is None:
|
|
288
293
|
n_var = self.problem.n_var
|
|
289
|
-
|
|
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)))
|
|
290
297
|
self.n_offsprings = self.pop_size
|
|
291
298
|
|
|
292
299
|
def _setup(self) -> None:
|
|
@@ -302,6 +309,16 @@ class CMAES(Algorithm):
|
|
|
302
309
|
|
|
303
310
|
# Compute recombination weights
|
|
304
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)
|
|
305
322
|
|
|
306
323
|
# Setup mean
|
|
307
324
|
self._setup_mean(n_var)
|
|
@@ -386,9 +403,7 @@ class CMAES(Algorithm):
|
|
|
386
403
|
with torch.no_grad():
|
|
387
404
|
self._log_sigma.fill_(math.log(sigma_val))
|
|
388
405
|
else:
|
|
389
|
-
self._log_sigma =
|
|
390
|
-
torch.tensor(sigma_val, device=self.device, dtype=self.dtype).log()
|
|
391
|
-
)
|
|
406
|
+
self._log_sigma = log_param(sigma_val, device=self.device, dtype=self.dtype)
|
|
392
407
|
else:
|
|
393
408
|
if hasattr(self, '_sigma_buffer'):
|
|
394
409
|
self._sigma_buffer.fill_(sigma_val)
|
|
@@ -467,9 +482,7 @@ class CMAES(Algorithm):
|
|
|
467
482
|
self._to_logit(cmu).to(device=self.device, dtype=self.dtype)
|
|
468
483
|
)
|
|
469
484
|
# damps is positive, store as log
|
|
470
|
-
self._log_damps =
|
|
471
|
-
torch.tensor(damps, device=self.device, dtype=self.dtype).log()
|
|
472
|
-
)
|
|
485
|
+
self._log_damps = log_param(damps, device=self.device, dtype=self.dtype)
|
|
473
486
|
else:
|
|
474
487
|
if not hasattr(self, '_cc'):
|
|
475
488
|
self.register_buffer("_cc", torch.tensor(cc, device=self.device, dtype=self.dtype))
|
|
@@ -520,6 +533,37 @@ class CMAES(Algorithm):
|
|
|
520
533
|
"""Covariance matrix C = L @ L.T."""
|
|
521
534
|
L = self.L
|
|
522
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)
|
|
523
567
|
|
|
524
568
|
@property
|
|
525
569
|
def p_sigma(self) -> Tensor:
|
|
@@ -587,7 +631,7 @@ class CMAES(Algorithm):
|
|
|
587
631
|
N, D = self.pop_size, self.n_var
|
|
588
632
|
|
|
589
633
|
# Sample standard normal
|
|
590
|
-
z =
|
|
634
|
+
z = standard_normal(N, D, device=self.device, dtype=self.dtype)
|
|
591
635
|
|
|
592
636
|
# Transform: y = L @ z
|
|
593
637
|
L = self.L
|
|
@@ -619,6 +663,20 @@ class CMAES(Algorithm):
|
|
|
619
663
|
offspring = torch.clamp(offspring, self.xl, self.xu)
|
|
620
664
|
|
|
621
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()
|
|
622
680
|
|
|
623
681
|
def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
|
|
624
682
|
"""
|
|
@@ -628,30 +686,52 @@ class CMAES(Algorithm):
|
|
|
628
686
|
offspring: Offspring population [pop_size, n_var].
|
|
629
687
|
offspring_fitness: Fitness values [pop_size].
|
|
630
688
|
"""
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
+
|
|
641
707
|
# Weighted recombination in y-space
|
|
642
|
-
y_w = (
|
|
708
|
+
y_w = (recombination_weights.unsqueeze(-1) * y_selected).sum(dim=0)
|
|
643
709
|
|
|
644
710
|
# Update mean
|
|
645
711
|
new_mean = self.mean + self.sigma * y_w
|
|
646
712
|
|
|
647
713
|
# Update evolution paths
|
|
648
|
-
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
|
+
)
|
|
649
721
|
|
|
650
722
|
# Update covariance
|
|
651
|
-
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
|
+
)
|
|
652
732
|
|
|
653
733
|
# Update step-size
|
|
654
|
-
new_sigma = self._update_sigma(new_p_sigma)
|
|
734
|
+
new_sigma = self._update_sigma(new_p_sigma, cs, damps)
|
|
655
735
|
|
|
656
736
|
# Commit updates
|
|
657
737
|
self._commit_updates(
|
|
@@ -685,7 +765,14 @@ class CMAES(Algorithm):
|
|
|
685
765
|
del self._pending_z
|
|
686
766
|
del self._pending_y
|
|
687
767
|
|
|
688
|
-
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:
|
|
689
776
|
"""
|
|
690
777
|
Update evolution paths p_σ and p_c.
|
|
691
778
|
|
|
@@ -696,7 +783,6 @@ class CMAES(Algorithm):
|
|
|
696
783
|
Tuple of (new_p_sigma, new_p_c, h_sigma).
|
|
697
784
|
"""
|
|
698
785
|
D = self.n_var
|
|
699
|
-
mu_eff = self._mu_eff
|
|
700
786
|
|
|
701
787
|
# Compute C^(-1/2) @ y_w using L^(-1) @ y_w
|
|
702
788
|
L = self.L
|
|
@@ -704,8 +790,15 @@ class CMAES(Algorithm):
|
|
|
704
790
|
z_w = torch.linalg.solve_triangular(L, y_w.unsqueeze(-1), upper=False).squeeze(-1)
|
|
705
791
|
|
|
706
792
|
# Update p_sigma (conjugate evolution path)
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|
709
802
|
|
|
710
803
|
# Heaviside function h_sigma (smooth approximation)
|
|
711
804
|
norm_p_sigma = new_p_sigma.norm()
|
|
@@ -713,8 +806,7 @@ class CMAES(Algorithm):
|
|
|
713
806
|
h_sigma = torch.sigmoid(10 * (threshold - norm_p_sigma / self._chi_n))
|
|
714
807
|
|
|
715
808
|
# Update p_c (evolution path for covariance)
|
|
716
|
-
|
|
717
|
-
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
|
|
718
810
|
|
|
719
811
|
return new_p_sigma, new_p_c, h_sigma
|
|
720
812
|
|
|
@@ -723,6 +815,10 @@ class CMAES(Algorithm):
|
|
|
723
815
|
y_selected: Tensor,
|
|
724
816
|
new_p_c: Tensor,
|
|
725
817
|
h_sigma: Tensor,
|
|
818
|
+
recombination_weights: Tensor,
|
|
819
|
+
cc: Tensor,
|
|
820
|
+
c1: Tensor,
|
|
821
|
+
cmu: Tensor,
|
|
726
822
|
) -> Tensor:
|
|
727
823
|
"""
|
|
728
824
|
Update covariance matrix C.
|
|
@@ -736,9 +832,6 @@ class CMAES(Algorithm):
|
|
|
736
832
|
New Cholesky factor L.
|
|
737
833
|
"""
|
|
738
834
|
D = self.n_var
|
|
739
|
-
c1 = self.c1
|
|
740
|
-
cmu = self.cmu
|
|
741
|
-
cc = self.cc
|
|
742
835
|
|
|
743
836
|
# Current covariance
|
|
744
837
|
L = self.L
|
|
@@ -749,7 +842,7 @@ class CMAES(Algorithm):
|
|
|
749
842
|
|
|
750
843
|
# Rank-mu update
|
|
751
844
|
rank_mu = (
|
|
752
|
-
|
|
845
|
+
recombination_weights.unsqueeze(-1).unsqueeze(-1) *
|
|
753
846
|
y_selected.unsqueeze(-1) * y_selected.unsqueeze(-2)
|
|
754
847
|
).sum(dim=0)
|
|
755
848
|
|
|
@@ -853,7 +946,12 @@ class CMAES(Algorithm):
|
|
|
853
946
|
|
|
854
947
|
return L_identity
|
|
855
948
|
|
|
856
|
-
def _update_sigma(
|
|
949
|
+
def _update_sigma(
|
|
950
|
+
self,
|
|
951
|
+
new_p_sigma: Tensor,
|
|
952
|
+
cs: Tensor,
|
|
953
|
+
damps: Tensor,
|
|
954
|
+
) -> Tensor:
|
|
857
955
|
"""
|
|
858
956
|
Update step-size using CSA (Cumulative Step-size Adaptation).
|
|
859
957
|
|
|
@@ -863,9 +961,6 @@ class CMAES(Algorithm):
|
|
|
863
961
|
Returns:
|
|
864
962
|
New step-size.
|
|
865
963
|
"""
|
|
866
|
-
cs = self.cs
|
|
867
|
-
damps = self.damps
|
|
868
|
-
|
|
869
964
|
# Step-size update factor
|
|
870
965
|
norm_p_sigma = new_p_sigma.norm()
|
|
871
966
|
factor = torch.exp((cs / damps) * (norm_p_sigma / self._chi_n - 1))
|
|
@@ -1101,6 +1196,10 @@ class CMAES(Algorithm):
|
|
|
1101
1196
|
if self.adaptive:
|
|
1102
1197
|
# Sigma in (1e-10, 1e10)
|
|
1103
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
|
+
)
|
|
1104
1203
|
|
|
1105
1204
|
# Ensure c1 + cmu <= 1
|
|
1106
1205
|
c1 = torch.sigmoid(self._c1_logit)
|
|
@@ -1121,7 +1220,8 @@ class CMAES(Algorithm):
|
|
|
1121
1220
|
return {
|
|
1122
1221
|
'pop_size': self.pop_size,
|
|
1123
1222
|
'mu': self._mu,
|
|
1124
|
-
'
|
|
1223
|
+
'selection_temperature': float(self.selection_temperature.detach()),
|
|
1224
|
+
'mu_eff': getattr(self, '_last_mu_eff', self._mu_eff),
|
|
1125
1225
|
'sigma': float(self.sigma.item()),
|
|
1126
1226
|
'cc': float(self.cc.item()),
|
|
1127
1227
|
'cs': float(self.cs.item()),
|
|
@@ -1365,4 +1465,4 @@ def cmaes_bipop(
|
|
|
1365
1465
|
incpopsize=incpopsize,
|
|
1366
1466
|
bipop=True,
|
|
1367
1467
|
**kwargs,
|
|
1368
|
-
)
|
|
1468
|
+
)
|
|
@@ -63,6 +63,8 @@ import torch.nn as nn
|
|
|
63
63
|
from torch import Tensor
|
|
64
64
|
|
|
65
65
|
from evograd.core.algorithm import Algorithm
|
|
66
|
+
from evograd.operators.repair import clamp_to_bounds
|
|
67
|
+
from evograd.operators.relaxations import log_param
|
|
66
68
|
|
|
67
69
|
if TYPE_CHECKING:
|
|
68
70
|
from evograd.core.problem import Problem
|
|
@@ -308,9 +310,7 @@ class DE(Algorithm):
|
|
|
308
310
|
# Setup F parameter
|
|
309
311
|
if self.adaptive:
|
|
310
312
|
# Learnable F stored as log(F) for positivity
|
|
311
|
-
self._log_F =
|
|
312
|
-
torch.tensor(self._init_F, device=self.device, dtype=self.dtype).log()
|
|
313
|
-
)
|
|
313
|
+
self._log_F = log_param(self._init_F, device=self.device, dtype=self.dtype)
|
|
314
314
|
else:
|
|
315
315
|
self.register_buffer(
|
|
316
316
|
"_F_buffer",
|
|
@@ -627,7 +627,7 @@ class DE(Algorithm):
|
|
|
627
627
|
trial = self.repair(trial, self.xl, self.xu)
|
|
628
628
|
else:
|
|
629
629
|
# Default: clamp to bounds
|
|
630
|
-
trial =
|
|
630
|
+
trial = clamp_to_bounds(trial, self.xl, self.xu)
|
|
631
631
|
|
|
632
632
|
return trial
|
|
633
633
|
|
|
@@ -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:
|