x-evolution 0.0.1__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.
x_evolution/__init__.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch import tensor, is_tensor
|
|
6
|
+
from torch.nn import Module
|
|
7
|
+
import torch.nn.functional as F
|
|
8
|
+
from torch.func import functional_call, vmap
|
|
9
|
+
|
|
10
|
+
from beartype import beartype
|
|
11
|
+
from beartype.door import is_bearable
|
|
12
|
+
|
|
13
|
+
from accelerate import Accelerator
|
|
14
|
+
|
|
15
|
+
from x_mlps_pytorch.noisable import (
|
|
16
|
+
Noisable,
|
|
17
|
+
with_seed
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# constants
|
|
21
|
+
|
|
22
|
+
MAX_SEED_VALUE = int(2 ** 32)
|
|
23
|
+
|
|
24
|
+
# helper functions
|
|
25
|
+
|
|
26
|
+
def exists(v):
|
|
27
|
+
return v is not None
|
|
28
|
+
|
|
29
|
+
def default(v, d):
|
|
30
|
+
return v if exists(v) else d
|
|
31
|
+
|
|
32
|
+
def normalize(t, eps = 1e-6):
|
|
33
|
+
return F.layer_norm(t, t.shape[-1:], eps = eps)
|
|
34
|
+
|
|
35
|
+
# class
|
|
36
|
+
|
|
37
|
+
class EvoStrategy(Module):
|
|
38
|
+
|
|
39
|
+
@beartype
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
model: Module,
|
|
43
|
+
*,
|
|
44
|
+
environment: Callable[[Module], float], # the environment is simply a function that takes in the model and returns a fitness score
|
|
45
|
+
num_generations,
|
|
46
|
+
population_size = 30,
|
|
47
|
+
learning_rate = 1e-3, # todo - optimizer
|
|
48
|
+
noise_scale = 1e-3, # the noise scaling during rollouts with environment, todo - figure out right value and make sure it can also be customized per parameter name through a dict
|
|
49
|
+
param_names_to_optimize: list[str] | None = None,
|
|
50
|
+
fitness_to_weighted_factor: Callable[[Tensor], Tensor] = normalize,
|
|
51
|
+
cpu = False,
|
|
52
|
+
accelerate_kwargs: dict = dict(),
|
|
53
|
+
):
|
|
54
|
+
super().__init__()
|
|
55
|
+
|
|
56
|
+
self.accelerate = Accelerator(cpu = cpu, **accelerate_kwargs)
|
|
57
|
+
|
|
58
|
+
self.model = model
|
|
59
|
+
self.noisable_model = Noisable(model)
|
|
60
|
+
|
|
61
|
+
self.environment = environment
|
|
62
|
+
|
|
63
|
+
param_names = set(dict(model.named_parameters()).keys())
|
|
64
|
+
|
|
65
|
+
# default to all parameters to optimize with evo strategy
|
|
66
|
+
|
|
67
|
+
param_names_to_optimize = default(param_names_to_optimize, param_names)
|
|
68
|
+
|
|
69
|
+
# validate
|
|
70
|
+
|
|
71
|
+
assert all([name in param_names for name in param_names_to_optimize])
|
|
72
|
+
assert len(param_names_to_optimize) > 0, 'nothing to optimize'
|
|
73
|
+
|
|
74
|
+
# sort param names and store
|
|
75
|
+
|
|
76
|
+
param_names_list = list(param_names_to_optimize)
|
|
77
|
+
param_names_list.sort()
|
|
78
|
+
|
|
79
|
+
self.param_names_to_optimize = param_names_list
|
|
80
|
+
|
|
81
|
+
# hyperparameters
|
|
82
|
+
|
|
83
|
+
self.population_size = population_size
|
|
84
|
+
self.num_params = len(param_names_list) # just convenience for generating all the seeds for all the randn for the proposed memory efficient way
|
|
85
|
+
|
|
86
|
+
self.num_generations = num_generations
|
|
87
|
+
|
|
88
|
+
# the function that transforms a tensor of fitness floats to the weight for the weighted average of the noise for rolling out 1x1 ES
|
|
89
|
+
|
|
90
|
+
self.fitness_to_weighted_factor = fitness_to_weighted_factor
|
|
91
|
+
|
|
92
|
+
self.noise_scale = noise_scale
|
|
93
|
+
self.learning_rate = learning_rate
|
|
94
|
+
|
|
95
|
+
self.register_buffer('_dummy', tensor(0), persistent = False)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def device(self):
|
|
99
|
+
return self._dummy.device
|
|
100
|
+
|
|
101
|
+
def print(self, *args, **kwargs):
|
|
102
|
+
return self.accelerate.print(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
@torch.inference_mode()
|
|
105
|
+
def evolve_(
|
|
106
|
+
self,
|
|
107
|
+
fitnesses: list[float] | Tensor,
|
|
108
|
+
seeds_for_population: list[int] | Tensor
|
|
109
|
+
):
|
|
110
|
+
model = self.noisable_model
|
|
111
|
+
|
|
112
|
+
if isinstance(fitnesses, list):
|
|
113
|
+
fitnesses = tensor(fitnesses)
|
|
114
|
+
|
|
115
|
+
if isinstance(seeds_for_population, list):
|
|
116
|
+
seeds_for_population = tensor(seeds_for_population)
|
|
117
|
+
|
|
118
|
+
fitnesses = fitnesses.to(self.device)
|
|
119
|
+
seeds_for_population.to(self.device)
|
|
120
|
+
|
|
121
|
+
# they use a simple z-score for the fitnesses, need to figure out the natural ES connection
|
|
122
|
+
|
|
123
|
+
noise_weights = self.fitness_to_weighted_factor(fitnesses)
|
|
124
|
+
|
|
125
|
+
noise_weights *= self.learning_rate # some learning rate that subsumes another constant
|
|
126
|
+
|
|
127
|
+
# update one seed at a time for enabling evolutionary strategy for large models
|
|
128
|
+
|
|
129
|
+
for individual_seed, noise_weight in zip(seeds_for_population.tolist(), noise_weights.tolist()):
|
|
130
|
+
|
|
131
|
+
individual_param_seeds = with_seed(individual_seed)(torch.randint)(0, MAX_SEED_VALUE, (self.num_params,))
|
|
132
|
+
|
|
133
|
+
noise_config = dict(zip(self.param_names_to_optimize, individual_param_seeds.tolist()))
|
|
134
|
+
|
|
135
|
+
# set the noise weight
|
|
136
|
+
|
|
137
|
+
noise_config = {param_name: (seed, noise_weight) for param_name, seed in noise_config.items()}
|
|
138
|
+
|
|
139
|
+
# now update
|
|
140
|
+
|
|
141
|
+
model.add_noise_(noise_config)
|
|
142
|
+
|
|
143
|
+
@torch.inference_mode()
|
|
144
|
+
def forward(
|
|
145
|
+
self
|
|
146
|
+
):
|
|
147
|
+
|
|
148
|
+
model = self.noisable_model
|
|
149
|
+
|
|
150
|
+
for index in range(self.num_generations):
|
|
151
|
+
generation = index + 1
|
|
152
|
+
|
|
153
|
+
fitnesses = []
|
|
154
|
+
|
|
155
|
+
# predetermine the seeds for each population
|
|
156
|
+
# each seed is then used as a seed for all the parameters
|
|
157
|
+
|
|
158
|
+
seeds_for_population = torch.randint(0, MAX_SEED_VALUE, (self.population_size,))
|
|
159
|
+
|
|
160
|
+
# now loop through the entire population of noise
|
|
161
|
+
|
|
162
|
+
for individual_seed in seeds_for_population.tolist():
|
|
163
|
+
|
|
164
|
+
individual_param_seeds = with_seed(individual_seed)(torch.randint)(0, MAX_SEED_VALUE, (self.num_params,))
|
|
165
|
+
|
|
166
|
+
noise_config = dict(zip(self.param_names_to_optimize, individual_param_seeds.tolist()))
|
|
167
|
+
noise_config = {param_name: (seed, self.noise_scale) for param_name, seed in noise_config.items()}
|
|
168
|
+
|
|
169
|
+
with model.temp_add_noise_(noise_config):
|
|
170
|
+
fitness = self.environment(model)
|
|
171
|
+
|
|
172
|
+
if is_tensor(fitness):
|
|
173
|
+
assert fitness.numel() == 1
|
|
174
|
+
fitness = fitness.item()
|
|
175
|
+
|
|
176
|
+
fitnesses.append(fitness)
|
|
177
|
+
|
|
178
|
+
# normalize the fitness and weighted sum of all the noise is the update
|
|
179
|
+
|
|
180
|
+
fitnesses = tensor(fitnesses).float()
|
|
181
|
+
|
|
182
|
+
self.evolve_(fitnesses, seeds_for_population)
|
|
183
|
+
|
|
184
|
+
# log
|
|
185
|
+
|
|
186
|
+
self.print(f'[{generation}] average fitness: {fitnesses.mean():.3f} | fitness std: {fitnesses.std():.3f}')
|
|
187
|
+
|
|
188
|
+
self.print('evolution complete')
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: x-evolution
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: x-evolution
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/x-evolution/
|
|
6
|
+
Project-URL: Repository, https://github.com/lucidrains/x-evolution
|
|
7
|
+
Author-email: Phil Wang <lucidrains@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 Phil Wang
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: artificial intelligence,evolution,evolutionary algorithms
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
36
|
+
Requires-Python: >=3.9
|
|
37
|
+
Requires-Dist: accelerate
|
|
38
|
+
Requires-Dist: beartype
|
|
39
|
+
Requires-Dist: einops>=0.8.0
|
|
40
|
+
Requires-Dist: torch>=2.4
|
|
41
|
+
Requires-Dist: x-mlps-pytorch>=0.1.8
|
|
42
|
+
Requires-Dist: x-transformers>=2.11.23
|
|
43
|
+
Provides-Extra: examples
|
|
44
|
+
Provides-Extra: test
|
|
45
|
+
Requires-Dist: pytest; extra == 'test'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
## x-evolution (wip)
|
|
49
|
+
|
|
50
|
+
Implementation of various evolutionary algorithms, starting with evolutionary strategies
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
$ pip install x-evolution
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import torch
|
|
62
|
+
from x_evolution import EvoStrategy
|
|
63
|
+
|
|
64
|
+
# model
|
|
65
|
+
|
|
66
|
+
from torch import nn
|
|
67
|
+
model = torch.nn.Sequential(
|
|
68
|
+
nn.Linear(8, 16),
|
|
69
|
+
nn.ReLU(),
|
|
70
|
+
nn.Linear(16, 4)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# evolution wrapper
|
|
74
|
+
|
|
75
|
+
evo_strat = EvoStrategy(
|
|
76
|
+
model,
|
|
77
|
+
environment = lambda model: torch.randint(0, 100, ()), # environment is just a function that takes in the individual model (with unique noise) and outputs the fitness - you can select for whatever you want here, does not have to be differentiable.
|
|
78
|
+
population_size = 30,
|
|
79
|
+
num_generations = 100,
|
|
80
|
+
learning_rate = 1e-3,
|
|
81
|
+
noise_scale = 1e-3
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# do evolution with your desired fitness function for so many generations
|
|
85
|
+
|
|
86
|
+
evo_strat()
|
|
87
|
+
|
|
88
|
+
# then save your evolved model, maybe for alternating with gradient based training
|
|
89
|
+
|
|
90
|
+
torch.save(model.state_dict(), './evolved.pt')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Citations
|
|
94
|
+
|
|
95
|
+
```bibtex
|
|
96
|
+
@article{Qiu2025EvolutionSA,
|
|
97
|
+
title = {Evolution Strategies at Scale: LLM Fine-Tuning Beyond Reinforcement Learning},
|
|
98
|
+
author = {Xin Qiu and Yulu Gan and Conor F. Hayes and Qiyao Liang and Elliot Meyerson and Babak Hodjat and Risto Miikkulainen},
|
|
99
|
+
journal = {ArXiv},
|
|
100
|
+
year = {2025},
|
|
101
|
+
volume = {abs/2509.24372},
|
|
102
|
+
url = {https://api.semanticscholar.org/CorpusID:281674745}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
x_evolution/__init__.py,sha256=XcwXJgIMPnCWGfGws3-vKgoR_7IfVslJBtiMvmEeSg0,57
|
|
2
|
+
x_evolution/x_evolution.py,sha256=Ww7RLTRDG2x_p1WuLVHWZle50GnleibQVihd3DEGow4,5941
|
|
3
|
+
x_evolution-0.0.1.dist-info/METADATA,sha256=3uYkZheDcENk76UMz94nK_KScz3xjfekEcJs-J8oW_U,3586
|
|
4
|
+
x_evolution-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
x_evolution-0.0.1.dist-info/licenses/LICENSE,sha256=1yCiA9b5nhslTavxPjsQAO-wpOnwJR9-l8LTVi7GJuk,1066
|
|
6
|
+
x_evolution-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Phil Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|