evolutionary-policy-optimization 0.4.1__tar.gz → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evolutionary-policy-optimization
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: EPO - Pytorch
5
5
  Project-URL: Homepage, https://pypi.org/project/evolutionary-policy-optimization/
6
6
  Project-URL: Repository, https://github.com/lucidrains/evolutionary-policy-optimization
@@ -44,6 +44,7 @@ Requires-Dist: mean-conc-beta>=0.2.7
44
44
  Requires-Dist: torch>=2.2
45
45
  Requires-Dist: tqdm
46
46
  Requires-Dist: x-mlps-pytorch>=0.3.0
47
+ Requires-Dist: x-ppo>=0.0.21
47
48
  Provides-Extra: examples
48
49
  Requires-Dist: fire; extra == 'examples'
49
50
  Requires-Dist: gymnasium[box2d]>=1.0.0; extra == 'examples'
@@ -15,7 +15,6 @@ import torch
15
15
  import torch.distributed as dist
16
16
  import torch.nn.functional as F
17
17
  from accelerate import Accelerator
18
- from torch.optim import AdamW
19
18
  from assoc_scan import AssocScan
20
19
  from einops import einsum, rearrange, reduce, repeat
21
20
  from einops.layers.torch import Rearrange
@@ -25,10 +24,12 @@ from mean_conc_beta import Beta
25
24
  from torch import Tensor, cat, from_numpy, is_tensor, nn, stack, tensor
26
25
  from torch.distributions import Categorical, Distribution
27
26
  from torch.nn import Linear, Module, ModuleList
27
+ from torch.optim import AdamW
28
28
  from torch.utils._pytree import tree_map
29
29
  from torch.utils.data import DataLoader, TensorDataset
30
30
  from tqdm import tqdm
31
31
  from x_mlps_pytorch import AttnResidualNormedMLP
32
+ from x_ppo import hpo_actor_loss, ppo_actor_loss, spo_actor_loss
32
33
 
33
34
  from evolutionary_policy_optimization.distributed import all_gather, get_world_and_rank, is_distributed, maybe_barrier, maybe_sync_seed
34
35
 
@@ -437,20 +438,32 @@ class MLP(Module):
437
438
  depth,
438
439
  dim_latent = 0,
439
440
  latent_mapping_network_depth = 2,
440
- expansion_factor = 2.
441
+ expansion_factor = 2.,
442
+ latent_conditioning = 'film', # film | concat (direct z injection, x-ppo style)
441
443
  ):
442
444
  super().__init__()
443
445
  dim_latent = default(dim_latent, 0)
444
446
 
445
447
  self.dim_latent = dim_latent
448
+ self.latent_conditioning = latent_conditioning
446
449
 
447
450
  self.needs_latent = dim_latent > 0
448
451
 
449
- self.encode_latent = nn.Sequential(
450
- LatentMappingNetwork(dim_latent, depth = latent_mapping_network_depth),
451
- Linear(dim_latent, dim * 2),
452
- nn.SiLU()
453
- ) if self.needs_latent else None
452
+ assert latent_conditioning in ('film', 'concat')
453
+
454
+ self.encode_latent = None
455
+
456
+ if self.needs_latent and latent_conditioning == 'film':
457
+ self.encode_latent = nn.Sequential(
458
+ LatentMappingNetwork(dim_latent, depth = latent_mapping_network_depth),
459
+ Linear(dim_latent, dim * 2),
460
+ nn.SiLU()
461
+ )
462
+
463
+ self.latent_proj = None
464
+
465
+ if self.needs_latent and latent_conditioning == 'concat':
466
+ self.latent_proj = nn.Linear(dim_latent, dim, bias = False)
454
467
 
455
468
  dim_hidden = int(dim * expansion_factor)
456
469
 
@@ -463,7 +476,7 @@ class MLP(Module):
463
476
 
464
477
  film = None
465
478
 
466
- if self.needs_latent:
479
+ if self.needs_latent and latent_conditioning == 'film':
467
480
  film = FiLM(dim * 2, dim)
468
481
 
469
482
  lime = DynamicLIMe(dim, num_layers = ind + 1) if not is_first else None
@@ -497,16 +510,17 @@ class MLP(Module):
497
510
  assert xnor(self.needs_latent, exists(latent))
498
511
 
499
512
  if exists(latent):
500
- # start with naive concatenative conditioning
501
- # but will also offer some alternatives once a spark is seen (film, adaptive linear from stylegan, etc)
502
-
503
- latent = self.encode_latent(latent)
513
+ if self.latent_conditioning == 'film':
514
+ latent = self.encode_latent(latent)
504
515
 
505
516
  if latent.ndim == 1:
506
517
  latent = repeat(latent, 'd -> b d', b = batch)
507
518
 
508
519
  assert latent.shape[0] == x.shape[0], f'received state with batch size {x.shape[0]} but latent ids received had batch size {latent.shape[0]}'
509
520
 
521
+ if self.latent_conditioning == 'concat':
522
+ x = x + self.latent_proj(latent)
523
+
510
524
  # layers
511
525
 
512
526
  prev_layer_inputs = [x]
@@ -527,43 +541,66 @@ class MLP(Module):
527
541
 
528
542
  return self.final_lime(x, prev_layer_inputs)
529
543
 
530
- # discriminator for predicting latent code from states or state transitions (DIAYN / DADS)
544
+ # discriminator for predicting latent code from states, transitions, and optionally actions (DIAYN / DADS)
531
545
 
532
546
  class DiversityDiscr(Module):
533
547
  def __init__(
534
548
  self,
535
549
  dim_state,
536
- num_latents,
550
+ num_latents = None,
537
551
  dim = 64,
538
552
  depth = 2,
539
- time_gap = 1
553
+ time_gap = 1,
554
+ dim_latent = None, # continuous latent -> regression head, DADS style
555
+ dim_action = None, # condition on action q(z | s, a, s')
556
+ action_is_continuous = False,
540
557
  ):
541
558
  super().__init__()
542
- self.time_gap = time_gap
559
+
560
+ assert exists(num_latents) ^ exists(dim_latent), 'give either num_latents (discrete) or dim_latent (continuous)'
561
+
543
562
  self.state_transition = time_gap > 0
563
+ self.has_action = exists(dim_action)
564
+ self.action_is_continuous = action_is_continuous
544
565
 
545
566
  self.state_proj = nn.Linear(dim_state, dim)
546
567
 
568
+ self.action_proj = None
569
+
570
+ if self.has_action:
571
+ self.action_proj = nn.Linear(dim_action, dim) if action_is_continuous else nn.Embedding(dim_action, dim)
572
+
573
+ dim_in = dim * (2 if self.state_transition else 1) + (dim if self.has_action else 0)
574
+ dim_out = default(dim_latent, num_latents)
575
+
547
576
  self.net = AttnResidualNormedMLP(
548
577
  dim = dim,
549
578
  depth = depth,
550
- dim_in = dim * 2 if self.state_transition else dim,
551
- dim_out = num_latents
579
+ dim_in = dim_in,
580
+ dim_out = dim_out
552
581
  )
553
582
 
554
583
  def reset_parameters(self):
555
584
  for module in self.modules():
556
- if isinstance(module, (nn.Linear, nn.LayerNorm, nn.RMSNorm)):
585
+ if isinstance(module, (nn.Linear, nn.LayerNorm, nn.RMSNorm, nn.Embedding)):
557
586
  module.reset_parameters()
558
587
 
559
- def forward(self, state, next_state = None):
588
+ def forward(self, state, next_state = None, action = None):
560
589
  state_embed = self.state_proj(state)
561
590
 
562
- if not self.state_transition or not exists(next_state):
563
- return self.net(state_embed)
591
+ inputs = [state_embed]
564
592
 
565
- next_state_embed = self.state_proj(next_state)
566
- return self.net((state_embed, next_state_embed))
593
+ if self.state_transition:
594
+ assert exists(next_state), 'discriminator was built with transitions but no next_state was given'
595
+ inputs.append(self.state_proj(next_state))
596
+
597
+ assert not self.has_action or exists(action), 'discriminator was built with actions but none were given'
598
+
599
+ if self.has_action:
600
+ action_inp = action if self.action_is_continuous else action.long()
601
+ inputs.append(self.action_proj(action_inp))
602
+
603
+ return self.net(inputs[0] if len(inputs) == 1 else tuple(inputs))
567
604
 
568
605
  # action distributions - the actor always returns a `Distribution`, either
569
606
  # categorical (discrete) or beta mean-conc (continuous). beta is defined over
@@ -660,6 +697,7 @@ class Actor(Module):
660
697
  dim_latent = 0,
661
698
  action_is_continuous = False, # continuous control - beta policy
662
699
  beta_kwargs: dict = dict(),
700
+ latent_conditioning = 'film',
663
701
  ):
664
702
  super().__init__()
665
703
 
@@ -675,7 +713,7 @@ class Actor(Module):
675
713
  nn.SiLU()
676
714
  )
677
715
 
678
- self.mlp = MLP(dim = dim, depth = mlp_depth, dim_latent = dim_latent)
716
+ self.mlp = MLP(dim = dim, depth = mlp_depth, dim_latent = dim_latent, latent_conditioning = latent_conditioning)
679
717
 
680
718
  if self.beta_actions:
681
719
  # beta head - (raw mean, raw concentration) per action dim
@@ -732,7 +770,8 @@ class Critic(Module):
732
770
  min_value = 0.,
733
771
  max_value = 500.,
734
772
  num_bins = 250
735
- )
773
+ ),
774
+ latent_conditioning = 'film',
736
775
  ):
737
776
  super().__init__()
738
777
 
@@ -745,7 +784,7 @@ class Critic(Module):
745
784
  nn.SiLU()
746
785
  )
747
786
 
748
- self.mlp = MLP(dim = dim, depth = mlp_depth, dim_latent = dim_latent)
787
+ self.mlp = MLP(dim = dim, depth = mlp_depth, dim_latent = dim_latent, latent_conditioning = latent_conditioning)
749
788
 
750
789
  self.final_norm = nn.RMSNorm(dim)
751
790
 
@@ -1252,6 +1291,8 @@ class Agent(Module):
1252
1291
  accelerate_kwargs: dict = dict(),
1253
1292
  accelerator = None,
1254
1293
  quiet: bool = False,
1294
+ latent_mode = 'gene_pool', # gene_pool (evolved latents) | fixed (sample z ~ N(0, I))
1295
+ num_latents = 1, # fixed mode: latent samples per rollout cycle
1255
1296
  ):
1256
1297
  super().__init__()
1257
1298
 
@@ -1289,14 +1330,28 @@ class Agent(Module):
1289
1330
  **ema_kwargs
1290
1331
  ) if use_critic_ema else None
1291
1332
 
1333
+ assert latent_mode in ('gene_pool', 'fixed')
1334
+
1335
+ self.latent_mode = latent_mode
1292
1336
  self.latent_gene_pool = latent_gene_pool
1293
- self.num_latents = latent_gene_pool.num_latents if exists(latent_gene_pool) else 1
1294
1337
  self.has_latent_genes = exists(latent_gene_pool)
1338
+ self.has_fixed_latents = latent_mode == 'fixed'
1339
+ self.has_latents = self.has_latent_genes or self.has_fixed_latents
1340
+ self.dim_latent = default(actor.dim_latent, 0)
1295
1341
 
1342
+ assert not (self.has_latent_genes and self.has_fixed_latents)
1296
1343
  assert actor.dim_latent == critic.dim_latent
1297
1344
 
1298
1345
  if self.has_latent_genes:
1299
1346
  assert latent_gene_pool.dim_latent == actor.dim_latent
1347
+ elif self.has_fixed_latents:
1348
+ assert self.dim_latent > 0, 'fixed latent mode needs dim_latent > 0'
1349
+
1350
+ self.num_latents = (
1351
+ latent_gene_pool.num_latents if self.has_latent_genes
1352
+ else num_latents if self.has_fixed_latents
1353
+ else 1
1354
+ )
1300
1355
 
1301
1356
  # gae function
1302
1357
 
@@ -1332,13 +1387,27 @@ class Agent(Module):
1332
1387
  self.use_diversity_discr = use_diversity_discr
1333
1388
  self.diversity_time_gap = diversity_time_gap
1334
1389
 
1390
+ self.diversity_discr_loss_fn = F.mse_loss if self.has_fixed_latents else F.cross_entropy
1391
+
1335
1392
  if use_diversity_discr:
1336
- assert exists(latent_gene_pool), 'latent_gene_pool must be present to use DIAYN / DADS'
1393
+ assert self.has_latents, 'latent gene pool or fixed latents must be present to use DIAYN / DADS'
1394
+
1395
+ if self.has_fixed_latents:
1396
+ assert diversity_time_gap in (0, 1), 'fixed latents support DIAYN (0) or DADS (1) transitions'
1397
+
1337
1398
  dim_state = actor.init_layer[0].in_features
1338
1399
 
1400
+ diversity_discr_kwargs = dict(diversity_discr_kwargs)
1401
+
1402
+ if self.has_fixed_latents:
1403
+ diversity_discr_kwargs['dim_latent'] = self.dim_latent
1404
+ diversity_discr_kwargs['dim_action'] = actor.num_actions
1405
+ diversity_discr_kwargs['action_is_continuous'] = actor.beta_actions
1406
+ else:
1407
+ diversity_discr_kwargs['num_latents'] = latent_gene_pool.num_latents
1408
+
1339
1409
  self.diversity_discr = DiversityDiscr(
1340
1410
  dim_state = dim_state,
1341
- num_latents = latent_gene_pool.num_latents,
1342
1411
  time_gap = diversity_time_gap,
1343
1412
  **diversity_discr_kwargs
1344
1413
  )
@@ -1349,7 +1418,6 @@ class Agent(Module):
1349
1418
  self.diversity_discr_optim = None
1350
1419
 
1351
1420
  self.clip_value = clip_value
1352
- self.critic_loss_kwargs = critic_loss_kwargs
1353
1421
 
1354
1422
  # self-predictive representations (SPR)
1355
1423
 
@@ -1372,6 +1440,7 @@ class Agent(Module):
1372
1440
 
1373
1441
  self.register_buffer('has_diversity_discr_warmed_up', tensor(False))
1374
1442
  self.register_buffer('zero', tensor(0.))
1443
+ self.last_diversity_discr_loss = 0.
1375
1444
 
1376
1445
  # shrink and perturb every
1377
1446
 
@@ -1381,7 +1450,7 @@ class Agent(Module):
1381
1450
 
1382
1451
  # promotes latents to be farther apart for diversity maintenance
1383
1452
 
1384
- self.has_diversity_loss = diversity_aux_loss_weight > 0.
1453
+ self.has_diversity_loss = diversity_aux_loss_weight > 0. and self.has_latent_genes
1385
1454
  self.diversity_aux_loss_weight = diversity_aux_loss_weight
1386
1455
 
1387
1456
  # wrap with accelerate
@@ -1468,6 +1537,9 @@ class Agent(Module):
1468
1537
  actor = unwrap(self.actor).state_dict(),
1469
1538
  critic = unwrap(self.critic).state_dict(),
1470
1539
  critic_ema = self.critic_ema.state_dict() if self.use_critic_ema else None,
1540
+ latent_mode = self.latent_mode,
1541
+ num_latents = self.num_latents,
1542
+ dim_latent = self.dim_latent,
1471
1543
  latents = unwrap(self.latent_gene_pool).state_dict() if self.has_latent_genes else None,
1472
1544
  diversity_discr = unwrap(self.diversity_discr).state_dict() if self.use_diversity_discr else None,
1473
1545
  diversity_time_gap = self.diversity_time_gap,
@@ -1491,6 +1563,9 @@ class Agent(Module):
1491
1563
 
1492
1564
  pkg = torch.load(str(path), weights_only = True)
1493
1565
 
1566
+ if 'latent_mode' in pkg:
1567
+ assert pkg['latent_mode'] == self.latent_mode, 'checkpoint latent_mode does not match the agent'
1568
+
1494
1569
  unwrap(self.actor).load_state_dict(pkg['actor'])
1495
1570
  unwrap(self.critic).load_state_dict(pkg['critic'])
1496
1571
 
@@ -1498,6 +1573,7 @@ class Agent(Module):
1498
1573
  self.critic_ema.load_state_dict(pkg['critic_ema'])
1499
1574
 
1500
1575
  if 'latents' in pkg and exists(pkg['latents']):
1576
+ assert self.has_latent_genes, 'checkpoint carries a latent gene pool but agent is in fixed latent mode'
1501
1577
  unwrap(self.latent_gene_pool).load_state_dict(pkg['latents'])
1502
1578
 
1503
1579
  if self.use_diversity_discr and 'diversity_discr' in pkg and exists(pkg['diversity_discr']):
@@ -1540,6 +1616,8 @@ class Agent(Module):
1540
1616
  if not exists(latent) and exists(latent_id) and exists(self.latent_gene_pool):
1541
1617
  latent = maybe_unwrap(self.latent_gene_pool)(latent_id = latent_id)
1542
1618
 
1619
+ assert not self.has_fixed_latents or exists(latent), 'fixed latent mode needs a latent sampled from N(0, I)'
1620
+
1543
1621
  return maybe_unwrap(self.actor)(state, latent, temperature = temperature)
1544
1622
 
1545
1623
  @move_input_tensors_to_device
@@ -1586,6 +1664,8 @@ class Agent(Module):
1586
1664
  if not exists(latent) and exists(latent_id) and exists(self.latent_gene_pool):
1587
1665
  latent = maybe_unwrap(self.latent_gene_pool)(latent_id = latent_id)
1588
1666
 
1667
+ assert not self.has_fixed_latents or exists(latent), 'fixed latent mode needs a latent sampled from N(0, I)'
1668
+
1589
1669
  critic_forward = maybe_unwrap(self.critic)
1590
1670
 
1591
1671
  if use_ema_if_available and self.use_critic_ema:
@@ -1633,7 +1713,7 @@ class Agent(Module):
1633
1713
  episode_ids,
1634
1714
  states,
1635
1715
  next_states,
1636
- latent_gene_ids,
1716
+ latent_refs,
1637
1717
  actions,
1638
1718
  log_probs,
1639
1719
  rewards,
@@ -1655,7 +1735,7 @@ class Agent(Module):
1655
1735
 
1656
1736
  valid_episode = episode_ids >= 0
1657
1737
 
1658
- fields = tuple(t[valid_episode] for t in (advantages, states, next_states, latent_gene_ids, actions, log_probs, values, dones))
1738
+ fields = tuple(t[valid_episode] for t in (advantages, states, next_states, latent_refs, actions, log_probs, values, dones))
1659
1739
 
1660
1740
  if self.wrap_with_accelerate or self.use_dataloader:
1661
1741
  dataloader = DataLoader(TensorDataset(*fields), batch_size = self.batch_size, shuffle = True)
@@ -1670,12 +1750,15 @@ class Agent(Module):
1670
1750
  self.actor.train()
1671
1751
  self.critic.train()
1672
1752
 
1753
+ if self.use_diversity_discr:
1754
+ self.diversity_discr.train()
1755
+
1673
1756
  for _ in tqdm(range(epochs), desc = 'learning actor/critic epoch', disable = self.quiet or not self.is_main_process):
1674
1757
  for (
1675
1758
  advantages,
1676
1759
  states,
1677
1760
  next_states,
1678
- latent_gene_ids,
1761
+ latent_refs,
1679
1762
  actions,
1680
1763
  log_probs,
1681
1764
  old_values,
@@ -1683,11 +1766,13 @@ class Agent(Module):
1683
1766
  ) in dataloader:
1684
1767
 
1685
1768
  if self.has_latent_genes:
1686
- latents = self.latent_gene_pool(latent_id = latent_gene_ids)
1769
+ latents = self.latent_gene_pool(latent_id = latent_refs)
1687
1770
 
1688
1771
  orig_latents = latents
1689
1772
  latents = latents.detach()
1690
1773
  latents.requires_grad_()
1774
+ elif self.has_fixed_latents:
1775
+ latents = latent_refs
1691
1776
  else:
1692
1777
  latents = None
1693
1778
 
@@ -1756,8 +1841,10 @@ class Agent(Module):
1756
1841
 
1757
1842
  if self.use_diversity_discr:
1758
1843
  discr_inp = (states,) if self.diversity_time_gap == 0 else (states, next_states)
1759
- diversity_discr_logits = self.diversity_discr(*discr_inp)
1760
- diversity_discr_loss = F.cross_entropy(diversity_discr_logits, latent_gene_ids)
1844
+ diversity_discr_pred = self.diversity_discr(*discr_inp, action = actions)
1845
+ diversity_discr_loss = self.diversity_discr_loss_fn(diversity_discr_pred, latent_refs)
1846
+
1847
+ self.last_diversity_discr_loss = float(diversity_discr_loss.detach())
1761
1848
 
1762
1849
  diversity_discr_loss.backward()
1763
1850
 
@@ -1863,39 +1950,31 @@ def actor_loss(
1863
1950
  advantages, # Float[b]
1864
1951
  eps_clip = 0.2,
1865
1952
  entropy_weight = .01,
1866
- eps = 1e-5,
1867
1953
  norm_advantages = True,
1868
- use_spo = False
1954
+ use_spo = False,
1955
+ use_hpo = False
1869
1956
  ):
1870
- batch = advantages.shape[0]
1871
-
1872
1957
  log_probs = sum_to_batch(distr.log_prob(actions))
1873
1958
  entropy = sum_to_batch(distr.entropy())
1874
1959
 
1875
- ratio = (log_probs - old_log_probs).exp()
1876
-
1877
- if norm_advantages:
1878
- advantages = F.layer_norm(advantages, (batch,), eps = eps)
1879
-
1880
- if use_spo:
1881
- # simple policy optimization - line 14 Algorithm 1 https://arxiv.org/abs/2401.16025v9
1882
-
1883
- actor_loss = - (
1884
- ratio * advantages -
1885
- advantages.abs() / (2 * eps_clip) * (ratio - 1.).square()
1886
- )
1960
+ if use_hpo:
1961
+ loss_fn = hpo_actor_loss
1962
+ elif use_spo:
1963
+ loss_fn = spo_actor_loss
1887
1964
  else:
1888
- # classic clipped surrogate loss from ppo
1889
-
1890
- clipped_ratio = ratio.clamp(min = 1. - eps_clip, max = 1. + eps_clip)
1891
-
1892
- actor_loss = -torch.min(clipped_ratio * advantages, ratio * advantages)
1893
-
1894
- # add entropy loss for exploration
1965
+ loss_fn = ppo_actor_loss
1966
+
1967
+ loss = loss_fn(
1968
+ log_probs,
1969
+ old_log_probs,
1970
+ advantages,
1971
+ eps_clip = eps_clip,
1972
+ normalize_advantages = norm_advantages,
1973
+ )
1895
1974
 
1896
- entropy_aux_loss = -entropy_weight * entropy
1975
+ # entropy bonus
1897
1976
 
1898
- return (actor_loss + entropy_aux_loss).mean()
1977
+ return (loss - entropy_weight * entropy).mean()
1899
1978
 
1900
1979
  # agent contains the actor, critic, and the latent genetic pool
1901
1980
 
@@ -1912,6 +1991,7 @@ def create_agent(
1912
1991
  use_critic_ema = True,
1913
1992
  use_state_norm = False,
1914
1993
  action_is_continuous = False, # continuous control - beta policy
1994
+ latent_mode = 'gene_pool', # gene_pool (evolved latents) | fixed (sample z ~ N(0, I))
1915
1995
  diversity_time_gap = 1,
1916
1996
  latent_gene_pool_kwargs: dict = dict(),
1917
1997
  actor_kwargs: dict = dict(),
@@ -1919,9 +1999,13 @@ def create_agent(
1919
1999
  **kwargs
1920
2000
  ) -> Agent:
1921
2001
 
1922
- has_latent_genes = num_latents > 1
2002
+ assert latent_mode in ('gene_pool', 'fixed')
1923
2003
 
1924
- if not has_latent_genes:
2004
+ has_latent_genes = latent_mode == 'gene_pool' and num_latents > 1
2005
+
2006
+ if latent_mode == 'fixed':
2007
+ assert exists(dim_latent) and dim_latent > 0, 'fixed latent mode needs dim_latent > 0'
2008
+ elif not has_latent_genes:
1925
2009
  dim_latent = None
1926
2010
 
1927
2011
  latent_gene_pool = LatentGenePool(
@@ -1932,6 +2016,14 @@ def create_agent(
1932
2016
 
1933
2017
  state_norm = StateNorm(dim = dim_state) if use_state_norm else None
1934
2018
 
2019
+ actor_kwargs = dict(actor_kwargs)
2020
+ critic_kwargs = dict(critic_kwargs)
2021
+
2022
+ latent_conditioning = 'concat' if latent_mode == 'fixed' else 'film'
2023
+
2024
+ actor_kwargs.setdefault('latent_conditioning', latent_conditioning)
2025
+ critic_kwargs.setdefault('latent_conditioning', latent_conditioning)
2026
+
1935
2027
  actor = Actor(
1936
2028
  num_actions = actor_num_actions,
1937
2029
  dim_state = dim_state,
@@ -1957,6 +2049,8 @@ def create_agent(
1957
2049
  critic = critic,
1958
2050
  state_norm = state_norm,
1959
2051
  latent_gene_pool = latent_gene_pool,
2052
+ latent_mode = latent_mode,
2053
+ num_latents = num_latents,
1960
2054
  use_critic_ema = use_critic_ema,
1961
2055
  diversity_time_gap = diversity_time_gap,
1962
2056
  **kwargs
@@ -1967,6 +2061,8 @@ def create_agent(
1967
2061
  # EPO - which is just PPO with natural selection of a population of latent variables conditioning the agent
1968
2062
  # the tricky part is that the latent ids for each episode / trajectory needs to be tracked
1969
2063
 
2064
+ # latent_gene_id is a pool index in gene_pool mode, or the sampled z in fixed mode
2065
+
1970
2066
  Memory = namedtuple('Memory', [
1971
2067
  'episode_id',
1972
2068
  'state',
@@ -2055,7 +2151,7 @@ class EPO(Module):
2055
2151
  self.action_sample_temperature = action_sample_temperature
2056
2152
  self.diversity_reward_weight = diversity_reward_weight
2057
2153
 
2058
- self.num_latents = agent.latent_gene_pool.num_latents if agent.has_latent_genes else 1
2154
+ self.num_latents = agent.num_latents
2059
2155
  self.episodes_per_latent = episodes_per_latent
2060
2156
  self.max_episode_length = max_episode_length
2061
2157
  self.fix_environ_across_latents = fix_environ_across_latents
@@ -2151,7 +2247,12 @@ class EPO(Module):
2151
2247
 
2152
2248
  latent_id, episode_id, maybe_seed = rollout
2153
2249
 
2154
- latent = self.agent.unwrapped_latent_gene_pool(latent_id = latent_id) if self.agent.has_latent_genes else None
2250
+ if self.agent.has_latent_genes:
2251
+ latent = self.agent.unwrapped_latent_gene_pool(latent_id = latent_id)
2252
+ elif self.agent.has_fixed_latents:
2253
+ latent = torch.randn(self.agent.dim_latent, device = self.device)
2254
+ else:
2255
+ latent = None
2155
2256
 
2156
2257
  seed = maybe_seed if fix_environ_across_latents else None
2157
2258
  state, _ = interface_torch_numpy(env.reset_one, device = self.device)(i, seed = seed)
@@ -2163,6 +2264,12 @@ class EPO(Module):
2163
2264
 
2164
2265
  pbar = tqdm(total = num_episodes, desc = 'rollout', disable = self.agent.quiet or not self.agent.is_main_process)
2165
2266
 
2267
+ # some vec envs return the full batch even when only `active` rows were stepped
2268
+
2269
+ def active_rows(x, active):
2270
+ x = np.asarray(x)
2271
+ return x[active] if x.shape[0] != len(active) else x
2272
+
2166
2273
  # each iteration, batch all active slots through the actor and critic,
2167
2274
  # then step the env - slots that finish are refilled and the batch
2168
2275
  # stays full until all episodes have been rolled out
@@ -2171,17 +2278,38 @@ class EPO(Module):
2171
2278
  active = [i for i, slot in enumerate(slots) if exists(slot)]
2172
2279
 
2173
2280
  obs = stack([slots[i].state for i in active])
2174
- latents = stack([slots[i].latent for i in active]) if self.agent.has_latent_genes else None
2281
+ latents = stack([slots[i].latent for i in active]) if self.agent.has_latents else None
2175
2282
 
2176
2283
  actions, log_probs = self.agent.get_actor_actions(obs, latent = latents, sample = True, temperature = self.action_sample_temperature, use_unwrapped_model = True)
2177
2284
  values = self.agent.get_critic_values(obs, latent = latents, use_ema_if_available = True, use_unwrapped_model = True)
2178
2285
 
2179
2286
  next_obs, rewards, terminated, truncated = env.step_batch(actions.cpu().numpy(), worker_ids = active)[:4]
2180
2287
 
2181
- next_states = from_numpy(np.ascontiguousarray(next_obs)).float().to(self.device)
2182
- rewards = from_numpy(np.asarray(rewards)).float().to(self.device)
2183
- terminated = from_numpy(np.asarray(terminated)).to(self.device)
2184
- truncated = from_numpy(np.asarray(truncated)).to(self.device)
2288
+ next_states = from_numpy(np.ascontiguousarray(active_rows(next_obs, active))).float().to(self.device)
2289
+ rewards = from_numpy(active_rows(rewards, active)).float().to(self.device)
2290
+ terminated = from_numpy(active_rows(terminated, active)).to(self.device)
2291
+ truncated = from_numpy(active_rows(truncated, active)).to(self.device)
2292
+
2293
+ # DADS / DIAYN intrinsic reward, batched over every active env
2294
+
2295
+ diversity_rewards = None
2296
+ time_gap = self.agent.diversity_time_gap
2297
+
2298
+ if self.agent.use_diversity_discr and self.agent.has_diversity_discr_warmed_up.item() and time_gap <= 1:
2299
+ with torch.no_grad():
2300
+ discr = self.agent.unwrap_model(self.agent.diversity_discr)
2301
+
2302
+ if self.agent.has_fixed_latents:
2303
+ # -mse(q(z | s, a, s'), z) - Sharma et al. https://arxiv.org/abs/1907.01657
2304
+
2305
+ pred = discr(obs, action = actions) if time_gap == 0 else discr(obs, next_states, action = actions)
2306
+ diversity_rewards = -F.mse_loss(pred, latents, reduction = 'none').mean(dim = -1)
2307
+ else:
2308
+ logits = discr(obs) if time_gap == 0 else discr(obs, next_states)
2309
+ latent_log_probs = logits.log_softmax(dim = -1)
2310
+
2311
+ latent_ids = tensor([slots[i].latent_id for i in active], device = self.device)
2312
+ diversity_rewards = latent_log_probs.gather(1, latent_ids[:, None]).squeeze(1) + math.log(self.agent.num_latents)
2185
2313
 
2186
2314
  for k, worker in enumerate(active):
2187
2315
  slot = slots[worker]
@@ -2194,19 +2322,16 @@ class EPO(Module):
2194
2322
 
2195
2323
  # maybe diversity reward for the latent
2196
2324
 
2197
- if self.agent.use_diversity_discr and self.agent.has_diversity_discr_warmed_up.item():
2325
+ if exists(diversity_rewards):
2326
+ reward = reward + self.diversity_reward_weight * diversity_rewards[k].item()
2327
+ elif self.agent.use_diversity_discr and self.agent.has_diversity_discr_warmed_up.item():
2328
+ # only the discrete > 1 time-gap case falls back to per-transition
2329
+
2198
2330
  with torch.no_grad():
2199
2331
  discr = self.agent.unwrap_model(self.agent.diversity_discr)
2200
- time_gap = self.agent.diversity_time_gap
2201
-
2202
- if time_gap == 0:
2203
- logits = discr(rearrange(state, '... -> 1 ...'))
2204
- elif time_gap == 1 or len(slot.memories) < (time_gap - 1):
2205
- logits = discr(rearrange(state, '... -> 1 ...'), rearrange(next_state, '... -> 1 ...'))
2206
- else:
2207
- past_state = slot.memories[-(time_gap - 1)].state
2208
- logits = discr(rearrange(past_state, '... -> 1 ...'), rearrange(next_state, '... -> 1 ...'))
2332
+ past_state = slot.memories[-(time_gap - 1)].state if len(slot.memories) >= (time_gap - 1) else state
2209
2333
 
2334
+ logits = discr(rearrange(past_state, '... -> 1 ...'), rearrange(next_state, '... -> 1 ...'))
2210
2335
  latent_log_probs = logits.log_softmax(dim = -1)
2211
2336
  diversity_reward = latent_log_probs[0, latent_id] + math.log(self.agent.num_latents)
2212
2337
 
@@ -2218,7 +2343,7 @@ class EPO(Module):
2218
2343
  tensor(episode_id),
2219
2344
  state,
2220
2345
  next_state,
2221
- tensor(latent_id),
2346
+ latent.detach().cpu() if self.agent.has_fixed_latents else tensor(latent_id),
2222
2347
  actions[k],
2223
2348
  log_probs[k],
2224
2349
  reward,
@@ -2231,7 +2356,6 @@ class EPO(Module):
2231
2356
  slot.memories.append(memory)
2232
2357
 
2233
2358
  if self.agent.use_diversity_discr and self.agent.diversity_time_gap > 1:
2234
- time_gap = self.agent.diversity_time_gap
2235
2359
  if len(slot.memories) >= time_gap:
2236
2360
  slot.memories[-time_gap] = slot.memories[-time_gap]._replace(next_state = next_state)
2237
2361
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "evolutionary-policy-optimization"
3
- version = "0.4.1"
3
+ version = "0.5.0"
4
4
  description = "EPO - Pytorch"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -34,6 +34,7 @@ dependencies = [
34
34
  'torch>=2.2',
35
35
  'tqdm',
36
36
  "x-mlps-pytorch>=0.3.0",
37
+ "x-ppo>=0.0.21",
37
38
  ]
38
39
 
39
40
  [project.urls]