x-evolution 0.1.5__tar.gz → 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x-evolution
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: x-evolution
5
5
  Project-URL: Homepage, https://pypi.org/project/x-evolution/
6
6
  Project-URL: Repository, https://github.com/lucidrains/x-evolution
@@ -38,7 +38,7 @@ Requires-Dist: accelerate
38
38
  Requires-Dist: beartype
39
39
  Requires-Dist: einops>=0.8.0
40
40
  Requires-Dist: torch>=2.4
41
- Requires-Dist: x-mlps-pytorch>=0.1.30
41
+ Requires-Dist: x-mlps-pytorch>=0.1.31
42
42
  Requires-Dist: x-transformers>=2.11.23
43
43
  Provides-Extra: examples
44
44
  Requires-Dist: gymnasium[box2d]>=1.0.0; extra == 'examples'
@@ -78,7 +78,7 @@ evo_strat = EvoStrategy(
78
78
  model,
79
79
  environment = lambda model: torch.randint(0, 100, ()), # environment is just a function that takes in the individual model (with unique noise) and outputs a scalar (the fitness) the measure you are selecting for
80
80
  noise_population_size = 30, # increase this for better gradient estimates
81
- noise_scale = 1e-3, # the scale of the perturbation noise
81
+ noise_scale = 1e-2, # the scale of the perturbation noise, also the initial noise scale (sigma) if `learned_noise_scale` = True
82
82
  num_generations = 100, # number of generations / training steps
83
83
  learning_rate = 1e-3, # scale on update derived by fitness and perturb noises
84
84
  params_to_optimize = None # defaults to all parameters, but can be [str {param name}] or [Parameter]
@@ -29,7 +29,7 @@ evo_strat = EvoStrategy(
29
29
  model,
30
30
  environment = lambda model: torch.randint(0, 100, ()), # environment is just a function that takes in the individual model (with unique noise) and outputs a scalar (the fitness) the measure you are selecting for
31
31
  noise_population_size = 30, # increase this for better gradient estimates
32
- noise_scale = 1e-3, # the scale of the perturbation noise
32
+ noise_scale = 1e-2, # the scale of the perturbation noise, also the initial noise scale (sigma) if `learned_noise_scale` = True
33
33
  num_generations = 100, # number of generations / training steps
34
34
  learning_rate = 1e-3, # scale on update derived by fitness and perturb noises
35
35
  params_to_optimize = None # defaults to all parameters, but can be [str {param name}] or [Parameter]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "x-evolution"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "x-evolution"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -27,7 +27,7 @@ dependencies = [
27
27
  "beartype",
28
28
  "einops>=0.8.0",
29
29
  "torch>=2.4",
30
- "x-mlps-pytorch>=0.1.30",
30
+ "x-mlps-pytorch>=0.1.31",
31
31
  "x-transformers>=2.11.23"
32
32
  ]
33
33
 
@@ -11,12 +11,16 @@ model = MLP(8, 16, 4)
11
11
  @param('noise_low_rank', (None, 1))
12
12
  @param('mirror_sampling', (False, True))
13
13
  @param('multi_models', (False, True))
14
+ @param('learned_sigma', (False, True))
15
+ @param('use_sigma_optimizer', (False, True))
14
16
  def test_evo_strat(
15
17
  params_to_optimize,
16
18
  use_optimizer,
17
19
  noise_low_rank,
18
20
  mirror_sampling,
19
- multi_models
21
+ multi_models,
22
+ learned_sigma,
23
+ use_sigma_optimizer
20
24
  ):
21
25
  from random import randrange
22
26
 
@@ -31,10 +35,12 @@ def test_evo_strat(
31
35
  to_optim,
32
36
  environment = lambda model: float(randrange(100)),
33
37
  num_generations = 2,
38
+ learned_noise_scale = learned_sigma,
34
39
  params_to_optimize = params_to_optimize,
35
40
  use_optimizer = use_optimizer,
36
41
  noise_low_rank = noise_low_rank,
37
- mirror_sampling = mirror_sampling
42
+ mirror_sampling = mirror_sampling,
43
+ use_sigma_optimizer = use_sigma_optimizer
38
44
  )
39
45
 
40
46
  evo_strat('evolve', 1)
@@ -19,6 +19,7 @@ class LunarEnvironment(Module):
19
19
  video_folder = './recordings',
20
20
  render_every_eps = 500,
21
21
  max_steps = 500,
22
+ repeats = 1
22
23
  ):
23
24
  super().__init__()
24
25
 
@@ -36,36 +37,40 @@ class LunarEnvironment(Module):
36
37
 
37
38
  self.env = env
38
39
  self.max_steps = max_steps
40
+ self.repeats = repeats
39
41
 
40
42
  def forward(self, model):
41
43
 
42
44
  device = next(model.parameters()).device
43
45
 
44
46
  seed = torch.randint(0, int(1e6), ())
45
- state, _ = self.env.reset(seed = seed.item())
46
47
 
47
- step = 0
48
48
  cum_reward = 0.
49
49
 
50
- while step < self.max_steps:
50
+ for _ in range(self.repeats):
51
+ state, _ = self.env.reset(seed = seed.item())
51
52
 
52
- state = torch.from_numpy(state).to(device)
53
+ step = 0
53
54
 
54
- action_logits = model(state)
55
+ while step < self.max_steps:
55
56
 
56
- action = F.gumbel_softmax(action_logits, hard = True).argmax(dim = -1)
57
+ state = torch.from_numpy(state).to(device)
57
58
 
58
- next_state, reward, truncated, terminated, *_ = self.env.step(action.item())
59
+ action_logits = model(state)
59
60
 
60
- cum_reward += float(reward)
61
- step += 1
61
+ action = F.gumbel_softmax(action_logits, hard = True).argmax(dim = -1)
62
62
 
63
- state = next_state
63
+ next_state, reward, truncated, terminated, *_ = self.env.step(action.item())
64
64
 
65
- if truncated or terminated:
66
- break
65
+ cum_reward += float(reward)
66
+ step += 1
67
67
 
68
- return cum_reward
68
+ state = next_state
69
+
70
+ if truncated or terminated:
71
+ break
72
+
73
+ return cum_reward / self.repeats
69
74
 
70
75
  # evo strategy
71
76
 
@@ -84,12 +89,15 @@ actor = ResidualNormedMLP(
84
89
 
85
90
  evo_strat = EvoStrategy(
86
91
  actor,
87
- environment = LunarEnvironment(),
92
+ environment = LunarEnvironment(repeats = 2),
88
93
  num_generations = 50_000,
89
94
  noise_population_size = 60,
90
95
  noise_low_rank = 2,
91
96
  noise_scale = 1e-1,
92
- learning_rate = 1e-3
97
+ learned_noise_scale = True,
98
+ use_sigma_optimizer = True,
99
+ learning_rate = 5e-2,
100
+ noise_scale_learning_rate = 1e-2
93
101
  )
94
102
 
95
103
  evo_strat()
@@ -20,7 +20,7 @@ model = nn.Sequential(
20
20
  ResidualNormedMLP(dim_in = 784, dim = 512, depth = 8, residual_every = 2, dim_out = 10)
21
21
  ).half()
22
22
 
23
- batch_size = 128
23
+ batch_size = 256
24
24
 
25
25
  # data
26
26
 
@@ -54,7 +54,7 @@ evo_strat = EvoStrategy(
54
54
  noise_scale = 1e-2,
55
55
  noise_low_rank = 2,
56
56
  num_generations = 10_000,
57
- learning_rate = 1e-5
57
+ learning_rate = 1e-3
58
58
  )
59
59
 
60
60
  evo_strat()
@@ -12,6 +12,8 @@ model = nn.Sequential(
12
12
  nn.Linear(16, 2)
13
13
  ).half()
14
14
 
15
+ batch_size = 128
16
+
15
17
  # fitness as inverse of loss
16
18
 
17
19
  from x_evolution import EvoStrategy
@@ -19,7 +21,7 @@ from x_evolution import EvoStrategy
19
21
  def loss_xor(model):
20
22
  device = next(model.parameters()).device
21
23
 
22
- data = torch.randint(0, 2, (32, 2))
24
+ data = torch.randint(0, 2, (batch_size, 2))
23
25
  labels = data[:, 0] ^ data[:, 1]
24
26
 
25
27
  data, labels = tuple(t.to(device) for t in (data, labels))
@@ -36,10 +38,14 @@ evo_strat = EvoStrategy(
36
38
  model,
37
39
  environment = loss_xor,
38
40
  noise_population_size = 100,
39
- noise_scale = 1e-1,
40
41
  noise_low_rank = 1,
41
42
  num_generations = 100_000,
42
- learning_rate = 1e-4
43
+ learning_rate = 1e-2,
44
+ noise_scale = 1e-1,
45
+ learned_noise_scale = True,
46
+ noise_scale_learning_rate = 5e-3,
47
+ sigma_optimizer_klass = torch.optim.Adam,
48
+ sigma_optimizer_kwargs = dict(betas = (0.9, 0.99))
43
49
  )
44
50
 
45
51
  evo_strat()
@@ -7,7 +7,7 @@ from functools import partial
7
7
 
8
8
  import torch
9
9
  from torch import tensor, Tensor, is_tensor, arange, randint
10
- from torch.nn import Module, ModuleList, Parameter
10
+ from torch.nn import Module, ModuleList, Parameter, ParameterList
11
11
  from torch.optim import SGD
12
12
 
13
13
  import torch.nn.functional as F
@@ -56,14 +56,20 @@ class EvoStrategy(Module):
56
56
  num_generations,
57
57
  noise_population_size = 30,
58
58
  learning_rate = 1e-3,
59
- 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
60
59
  mirror_sampling = True,
61
60
  params_to_optimize: list[str] | Module | list[Module] | list[Parameter] | None = None,
62
61
  noise_low_rank: int | None = None,
63
62
  rollout_fixed_seed = True,
63
+ noise_scale = 1e-2, # 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
64
+ learned_noise_scale = False,
65
+ noise_scale_learning_rate = 5e-3,
66
+ noise_scale_clamp_range: tuple[float, float] | None = None,
64
67
  use_optimizer = True,
65
68
  optimizer_klass = partial(SGD, nesterov = True, momentum = 0.1, weight_decay = 1e-2),
66
69
  optimizer_kwargs: dict = dict(),
70
+ use_sigma_optimizer = True,
71
+ sigma_optimizer_klass = partial(SGD, nesterov = True, momentum = 0.1),
72
+ sigma_optimizer_kwargs: dict = dict(),
67
73
  transform_fitness: Callable = identity,
68
74
  fitness_to_weighted_factor: Callable[[Tensor], Tensor] = normalize,
69
75
  checkpoint_every = None, # saving every number of generations
@@ -141,9 +147,24 @@ class EvoStrategy(Module):
141
147
 
142
148
  self.mirror_sampling = mirror_sampling # mirror / antithetical sampling - reducing variance by doing positive + negative of noise and subtracting
143
149
 
150
+ self.learning_rate = learning_rate
151
+
152
+ # noise scale, which can be fixed or learned
153
+
144
154
  self.noise_scale = noise_scale
145
155
 
146
- self.learning_rate = learning_rate
156
+ self.learned_noise_scale = learned_noise_scale
157
+
158
+ if learned_noise_scale:
159
+ self.sigmas = ParameterList([Parameter(torch.ones(()) * noise_scale) for _ in range(len(named_parameters_dict))])
160
+ self.param_name_to_sigma_index = {name: i for i, name in enumerate(named_parameters_dict.keys())}
161
+
162
+ self.sigma_clamp_ = identity
163
+
164
+ if exists(noise_scale_clamp_range):
165
+ self.sigma_clamp_ = lambda t: t.clamp_(*noise_scale_clamp_range)
166
+
167
+ self.noise_scale_learning_rate = noise_scale_learning_rate
147
168
 
148
169
  # rolling out with a fixed seed
149
170
 
@@ -157,6 +178,11 @@ class EvoStrategy(Module):
157
178
  optim_params = [named_parameters_dict[name] for name in params_to_optimize]
158
179
  self.optimizer = optimizer_klass(optim_params, lr = learning_rate, **optimizer_kwargs)
159
180
 
181
+ self.use_sigma_optimizer = use_sigma_optimizer
182
+
183
+ if learned_noise_scale and use_sigma_optimizer:
184
+ self.sigma_optimizer = sigma_optimizer_klass(self.sigmas, lr = noise_scale_learning_rate, **sigma_optimizer_kwargs)
185
+
160
186
  # rejecting the fitnesses for a certain generation if this function is true
161
187
 
162
188
  self.reject_generation_fitnesses_if = reject_generation_fitnesses_if
@@ -175,6 +201,13 @@ class EvoStrategy(Module):
175
201
  def print(self, *args, **kwargs):
176
202
  return self.accelerate.print(*args, **kwargs)
177
203
 
204
+ def _get_noise_scale(self, param_name):
205
+ if not self.learned_noise_scale:
206
+ return self.noise_scale
207
+
208
+ sigma_index = self.param_name_to_sigma_index[param_name]
209
+ return self.sigmas[sigma_index]
210
+
178
211
  @torch.inference_mode()
179
212
  def evolve_(
180
213
  self,
@@ -208,21 +241,30 @@ class EvoStrategy(Module):
208
241
  else:
209
242
  weights = fitnesses
210
243
 
211
- weights /= self.noise_scale * (2. if self.mirror_sampling else 1.)
244
+ weights /= self.noise_population_size * (2. if self.mirror_sampling else 1.)
212
245
 
213
246
  if not use_optimizer:
214
247
  weights *= self.learning_rate # some learning rate that subsumes another constant
215
248
 
249
+ # maybe learned sigma
250
+
251
+ if self.learned_noise_scale:
252
+ grad_sigmas = [torch.zeros_like(sigma) for sigma in self.sigmas]
253
+
216
254
  # update one seed at a time for enabling evolutionary strategy for large models
217
255
 
218
256
  for individual_seed, weight in zip(seeds_for_population.tolist(), weights.tolist()):
219
257
 
220
258
  individual_param_seeds = with_seed(individual_seed)(torch.randint)(0, MAX_SEED_VALUE, (self.num_params,))
221
259
 
260
+ # setup noise config
261
+
222
262
  noise_config = dict(zip(self.param_names_to_optimize, individual_param_seeds.tolist()))
223
263
 
224
264
  noise_config_with_weight = dict()
225
265
 
266
+ # loop through each noise
267
+
226
268
  for param_name, seed in noise_config.items():
227
269
 
228
270
  shape = self.param_shapes[param_name]
@@ -231,12 +273,31 @@ class EvoStrategy(Module):
231
273
  # reproduce the noise from the seed
232
274
 
233
275
  noise = with_seed(seed)(model.create_noise_fn)(shape, dtype = dtype)
276
+ noise = noise.to(self.device)
277
+ # scale the weight
278
+
279
+ noise_scale = self._get_noise_scale(param_name)
280
+
281
+ scaled_weight = weight / noise_scale
234
282
 
235
283
  # set the noise weight
236
284
 
237
- noise_config_with_weight[param_name] = (noise, weight)
285
+ noise_config_with_weight[param_name] = (noise, scaled_weight)
286
+
287
+ # maybe learned sigma
238
288
 
239
- # now update
289
+ if self.learned_noise_scale:
290
+ one_grad_sigma = weight * ((noise ** 2 - 1) / noise_scale)
291
+
292
+ sigma_index = self.param_name_to_sigma_index[param_name]
293
+ sigma = self.sigmas[sigma_index]
294
+
295
+ if sigma.numel() == 1:
296
+ one_grad_sigma = one_grad_sigma.mean()
297
+
298
+ grad_sigmas[sigma_index].add_(one_grad_sigma)
299
+
300
+ # now update params for one seed
240
301
 
241
302
  model.add_noise_(noise_config_with_weight, negate = use_optimizer, add_to_grad = use_optimizer)
242
303
 
@@ -244,6 +305,21 @@ class EvoStrategy(Module):
244
305
  self.optimizer.step()
245
306
  self.optimizer.zero_grad()
246
307
 
308
+ if self.learned_noise_scale:
309
+
310
+ if self.use_sigma_optimizer:
311
+ for sigma, grad_sigma in zip(self.sigmas, grad_sigmas):
312
+ sigma.grad = -grad_sigma
313
+
314
+ self.sigma_optimizer.step()
315
+ self.sigma_optimizer.zero_grad()
316
+ else:
317
+ for sigma, grad_sigma in zip(self.sigmas, grad_sigmas):
318
+ sigma.add_(grad_sigma * self.noise_scale_learning_rate)
319
+
320
+ for sigma in self.sigmas:
321
+ self.sigma_clamp_(sigma)
322
+
247
323
  def checkpoint(self, filename = 'evolved.model'):
248
324
 
249
325
  if self.accelerate.is_main_process:
@@ -262,6 +338,11 @@ class EvoStrategy(Module):
262
338
 
263
339
  model = self.noisable_model.to(self.device)
264
340
 
341
+ # maybe sigmas
342
+
343
+ if self.learned_noise_scale:
344
+ self.sigmas = self.sigmas.to(self.device)
345
+
265
346
  # get world size, rank, and determine if distributed
266
347
 
267
348
  rank = self.accelerate.process_index
@@ -333,11 +414,20 @@ class EvoStrategy(Module):
333
414
  individual_param_seeds = with_seed(individual_seed)(randint)(0, MAX_SEED_VALUE, (self.num_params,))
334
415
 
335
416
  noise_config = dict(zip(self.param_names_to_optimize, individual_param_seeds.tolist()))
336
- noise_config = {param_name: (seed, self.noise_scale) for param_name, seed in noise_config.items()}
417
+
418
+ # determine noise scale, which can be fixed or learned
419
+
420
+ noise_config_with_scale = dict()
421
+
422
+ for param_name, seed in noise_config.items():
423
+
424
+ noise_scale = self._get_noise_scale(param_name)
425
+
426
+ noise_config_with_scale[param_name] = (seed, noise_scale)
337
427
 
338
428
  # maybe roll out with a fixed seed
339
429
 
340
- with model.temp_add_noise_(noise_config):
430
+ with model.temp_add_noise_(noise_config_with_scale):
341
431
  fitness = with_seed(maybe_rollout_seed)(rollout_for_fitness)()
342
432
 
343
433
  if not self.mirror_sampling:
@@ -346,7 +436,7 @@ class EvoStrategy(Module):
346
436
 
347
437
  # handle mirror sampling
348
438
 
349
- with model.temp_add_noise_(noise_config, negate = True):
439
+ with model.temp_add_noise_(noise_config_with_scale, negate = True):
350
440
  fitness_mirrored = with_seed(maybe_rollout_seed)(rollout_for_fitness)()
351
441
 
352
442
  fitnesses.append([fitness, fitness_mirrored])
@@ -360,6 +450,11 @@ class EvoStrategy(Module):
360
450
  if is_distributed:
361
451
  fitnesses = self.accelerate.gather(fitnesses)
362
452
 
453
+ # remove padding
454
+
455
+ fitnesses = fitnesses[:pop_size]
456
+ seeds_for_population = seeds_for_population[:pop_size]
457
+
363
458
  # validate fitnesses
364
459
 
365
460
  if exists(self.reject_generation_fitnesses_if) and self.reject_generation_fitnesses_if(fitnesses):
@@ -368,14 +463,17 @@ class EvoStrategy(Module):
368
463
 
369
464
  # pass fitnesses to evolve function
370
465
 
371
- self.evolve_(
372
- fitnesses[:pop_size],
373
- seeds_for_population[:pop_size]
374
- )
466
+ self.evolve_(fitnesses, seeds_for_population)
375
467
 
376
468
  # log
377
469
 
378
- self.print(f'[{generation}] average fitness: {fitnesses.mean():.3f} | fitness std: {fitnesses.std():.3f}')
470
+ msg = f'[{generation}] average fitness: {fitnesses.mean():.3f} | fitness std: {fitnesses.std():.3f}'
471
+
472
+ if self.learned_noise_scale:
473
+ avg_sigma = torch.stack(list(self.sigmas)).mean().item()
474
+ msg += f' | avg sigma: {avg_sigma:.3f}'
475
+
476
+ self.print(msg)
379
477
 
380
478
  # maybe checkpoint
381
479
 
File without changes
File without changes