drlab 0.2.3__tar.gz → 0.2.4__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.
Files changed (44) hide show
  1. {drlab-0.2.3/src/drlab.egg-info → drlab-0.2.4}/PKG-INFO +1 -1
  2. {drlab-0.2.3 → drlab-0.2.4}/pyproject.toml +1 -1
  3. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/__init__.py +1 -1
  4. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/off_policy/base.py +32 -16
  5. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/off_policy/configs.py +6 -3
  6. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/off_policy/dqn.py +1 -0
  7. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/off_policy/sac.py +38 -30
  8. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/base.py +1 -1
  9. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/configs.py +1 -1
  10. {drlab-0.2.3 → drlab-0.2.4/src/drlab.egg-info}/PKG-INFO +1 -1
  11. {drlab-0.2.3 → drlab-0.2.4}/tests/test_learners.py +129 -16
  12. {drlab-0.2.3 → drlab-0.2.4}/tests/test_public_api.py +1 -1
  13. {drlab-0.2.3 → drlab-0.2.4}/LICENSE +0 -0
  14. {drlab-0.2.3 → drlab-0.2.4}/README.md +0 -0
  15. {drlab-0.2.3 → drlab-0.2.4}/setup.cfg +0 -0
  16. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/__init__.py +0 -0
  17. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/base.py +0 -0
  18. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/e_greedy.py +0 -0
  19. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/gaussian.py +0 -0
  20. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/greedy.py +0 -0
  21. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/stochastic.py +0 -0
  22. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/controllers/warmup.py +0 -0
  23. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/experiments/__init__.py +0 -0
  24. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/experiments/off_policy_experiment.py +0 -0
  25. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/experiments/on_policy_experiment.py +0 -0
  26. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/__init__.py +0 -0
  27. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/off_policy/__init__.py +0 -0
  28. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/__init__.py +0 -0
  29. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/actor_critic.py +0 -0
  30. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/ppo.py +0 -0
  31. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/learners/on_policy/reinforce.py +0 -0
  32. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/replay/__init__.py +0 -0
  33. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/replay/replay_buffer.py +0 -0
  34. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/replay/transition_batch.py +0 -0
  35. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/runners/__init__.py +0 -0
  36. {drlab-0.2.3 → drlab-0.2.4}/src/drlab/runners/runner.py +0 -0
  37. {drlab-0.2.3 → drlab-0.2.4}/src/drlab.egg-info/SOURCES.txt +0 -0
  38. {drlab-0.2.3 → drlab-0.2.4}/src/drlab.egg-info/dependency_links.txt +0 -0
  39. {drlab-0.2.3 → drlab-0.2.4}/src/drlab.egg-info/requires.txt +0 -0
  40. {drlab-0.2.3 → drlab-0.2.4}/src/drlab.egg-info/top_level.txt +0 -0
  41. {drlab-0.2.3 → drlab-0.2.4}/tests/test_controllers.py +0 -0
  42. {drlab-0.2.3 → drlab-0.2.4}/tests/test_off_policy_experiment.py +0 -0
  43. {drlab-0.2.3 → drlab-0.2.4}/tests/test_replay.py +0 -0
  44. {drlab-0.2.3 → drlab-0.2.4}/tests/test_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drlab
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Deep Reinforcement Learning kit for research.
5
5
  Author: Tomas Osarte
6
6
  Keywords: reinforcement-learning,deep-learning,pytorch,gymnasium
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "drlab"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Deep Reinforcement Learning kit for research."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -35,7 +35,7 @@ from drlab.learners import (
35
35
  from drlab.replay import ReplayBuffer, TransitionBatch
36
36
  from drlab.runners import Runner
37
37
 
38
- __version__ = "0.2.3"
38
+ __version__ = "0.2.4"
39
39
 
40
40
  __all__ = [
41
41
  "__version__",
@@ -1,6 +1,7 @@
1
1
  import torch as th
2
2
  from copy import deepcopy
3
3
  from abc import ABC, abstractmethod
4
+ from collections.abc import Iterable
4
5
 
5
6
  from .configs import OffPolicyConfig, TargetUpdate
6
7
 
@@ -15,8 +16,7 @@ class OffPolicyLearner(ABC):
15
16
  self.regularizers = config.regularizers
16
17
  self.reg_lams = config.reg_lams
17
18
 
18
- self.clip_grad = config.clip_grad
19
- self.grad_norm_clip = config.grad_norm_clip
19
+ self.clipnorm = config.clipnorm
20
20
 
21
21
  self.use_target_model = config.use_target_model
22
22
  self.target_update = TargetUpdate(config.target_update)
@@ -52,25 +52,39 @@ class OffPolicyLearner(ABC):
52
52
 
53
53
  return target
54
54
 
55
- def hard_update(self, target: th.nn.Module, source: th.nn.Module):
55
+ def _hard_update(self, target: th.nn.Module, source: th.nn.Module):
56
56
  target.load_state_dict(source.state_dict())
57
57
  target.eval()
58
58
 
59
- def soft_update(self, target: th.nn.Module, source: th.nn.Module):
60
- tau = self.soft_target_update_param
59
+ def _soft_update(
60
+ self,
61
+ pairs: list[tuple[th.nn.Module, th.nn.Module]],
62
+ ) -> None:
63
+ target_parameters = [
64
+ parameter
65
+ for target, _ in pairs
66
+ for parameter in target.parameters()
67
+ ]
68
+ source_parameters = [
69
+ parameter
70
+ for _, source in pairs
71
+ for parameter in source.parameters()
72
+ ]
61
73
 
62
74
  with th.no_grad():
63
- for target_p, source_p in zip(target.parameters(), source.parameters()):
64
- target_p.data.mul_(1.0 - tau)
65
- target_p.data.add_(tau * source_p.data)
75
+ th._foreach_mul_(target_parameters, 1.0 - self.soft_target_update_param)
76
+ scaled_sources = th._foreach_mul(
77
+ source_parameters,
78
+ self.soft_target_update_param,
79
+ )
80
+ th._foreach_add_(target_parameters, scaled_sources)
66
81
 
67
82
  def update_targets(
68
83
  self,
69
84
  pairs: list[tuple[th.nn.Module, th.nn.Module]],
70
85
  ):
71
86
  if self.target_update == TargetUpdate.SOFT:
72
- for target, source in pairs:
73
- self.soft_update(target, source)
87
+ self._soft_update(pairs)
74
88
  return
75
89
 
76
90
  if self.target_update == TargetUpdate.HARD:
@@ -78,7 +92,7 @@ class OffPolicyLearner(ABC):
78
92
 
79
93
  if self.target_update_calls % self.target_update_interval == 0:
80
94
  for target, source in pairs:
81
- self.hard_update(target, source)
95
+ self._hard_update(target, source)
82
96
 
83
97
  return
84
98
 
@@ -119,15 +133,17 @@ class OffPolicyLearner(ABC):
119
133
  self,
120
134
  loss: th.Tensor,
121
135
  optimizer: th.optim.Optimizer,
122
- parameters: list[th.nn.Parameter]
123
- ):
136
+ parameters: Iterable[th.nn.Parameter],
137
+ clipnorm: float | None = None,
138
+ ) -> None:
139
+ parameters = tuple(parameters)
124
140
  optimizer.zero_grad(set_to_none=True)
125
- loss.backward()
141
+ loss.backward(inputs=parameters)
126
142
 
127
- if self.clip_grad:
143
+ if clipnorm is not None:
128
144
  th.nn.utils.clip_grad_norm_(
129
145
  parameters,
130
- self.grad_norm_clip,
146
+ clipnorm,
131
147
  )
132
148
 
133
149
  optimizer.step()
@@ -20,8 +20,7 @@ class OffPolicyConfig:
20
20
  regularizers: list[Callable[..., th.Tensor | float]] = field(default_factory=list)
21
21
  reg_lams: list[float] = field(default_factory=list)
22
22
 
23
- clip_grad: bool = True
24
- grad_norm_clip: float = 1.0
23
+ clipnorm: float | None = 1.0
25
24
 
26
25
  use_target_model: bool = True
27
26
  target_update: TargetUpdate | str = TargetUpdate.SOFT
@@ -47,6 +46,7 @@ class SACConfig(OffPolicyConfig):
47
46
  criterion: th.nn.Module = field(default_factory=th.nn.MSELoss)
48
47
 
49
48
  target_entropy: float | None = None
49
+ initial_alpha: float = 1.0
50
50
  alpha_lr: float = 3e-4
51
51
 
52
52
  min_log_std: float = -20.0
@@ -65,8 +65,11 @@ class SACConfig(OffPolicyConfig):
65
65
  if self.action_dim <= 0:
66
66
  raise ValueError("action_dim must be > 0.")
67
67
 
68
+ if self.initial_alpha <= 0.0:
69
+ raise ValueError("initial_alpha must be > 0.")
70
+
68
71
  if self.alpha_lr <= 0.0:
69
72
  raise ValueError("alpha_lr must be > 0.")
70
73
 
71
74
  if self.min_log_std >= self.max_log_std:
72
- raise ValueError("min_log_std must be < max_log_std.")
75
+ raise ValueError("min_log_std must be < max_log_std.")
@@ -89,6 +89,7 @@ class DQNLearner(OffPolicyLearner):
89
89
  loss,
90
90
  optimizer=self.optimizer,
91
91
  parameters=self.model.parameters(),
92
+ clipnorm=self.clipnorm,
92
93
  )
93
94
 
94
95
  if self.target_model is not None:
@@ -1,5 +1,6 @@
1
+ import math
2
+
1
3
  import torch as th
2
- from torch.distributions import Normal
3
4
 
4
5
  from .base import OffPolicyLearner
5
6
  from .configs import SACConfig
@@ -42,7 +43,7 @@ class SACLearner(OffPolicyLearner):
42
43
  self.target_entropy = -float(config.action_dim)
43
44
 
44
45
  self.log_alpha = th.nn.Parameter(
45
- th.tensor(0.0, device=self.device)
46
+ th.tensor(math.log(config.initial_alpha), device=self.device)
46
47
  )
47
48
 
48
49
  self.alpha_optimizer = th.optim.Adam(
@@ -54,15 +55,15 @@ class SACLearner(OffPolicyLearner):
54
55
  def alpha(self) -> th.Tensor:
55
56
  return self.log_alpha.exp()
56
57
 
57
- def _set_requires_grad(self, model: th.nn.Module, requires_grad: bool) -> None:
58
- for param in model.parameters():
59
- param.requires_grad_(requires_grad)
60
-
61
- def get_policy_dist(self, states: th.Tensor):
58
+ def _get_policy_params(self, states: th.Tensor) -> tuple[th.Tensor, th.Tensor]:
62
59
  output = self.actor(states)
63
60
  mean = output[:, : self.config.action_dim]
64
61
  log_std = output[:, self.config.action_dim :]
65
62
  log_std = th.clamp(log_std, self.config.min_log_std, self.config.max_log_std)
63
+ return mean, log_std
64
+
65
+ def get_policy_dist(self, states: th.Tensor) -> tuple[th.Tensor, th.Tensor]:
66
+ mean, log_std = self._get_policy_params(states)
66
67
  std = th.exp(log_std)
67
68
  return mean, std
68
69
 
@@ -71,19 +72,23 @@ class SACLearner(OffPolicyLearner):
71
72
  obs: th.Tensor,
72
73
  deterministic: bool = False,
73
74
  ) -> tuple[th.Tensor, th.Tensor]:
74
- mean, std = self.get_policy_dist(obs)
75
+ mean, log_std = self._get_policy_params(obs)
75
76
 
76
77
  if deterministic:
77
78
  u = mean
78
79
  return th.tanh(u), th.zeros(obs.shape[0], 1, device=obs.device)
79
-
80
- dist = Normal(mean, std)
81
- u = dist.rsample() # reparameterization trick
80
+
81
+ # Specialized reparameterized Gaussian sampling avoids the generic
82
+ # distribution object's redundant scale and normalized-noise work.
83
+ eps = th.randn_like(mean)
84
+ u = mean + th.exp(log_std) * eps
82
85
  action = th.tanh(u)
83
86
 
84
- log_prob = dist.log_prob(u) # Gaussian log prob before tanh
85
- log_prob -= th.log(1.0 - action.pow(2) + 1e-6) # Tanh correction
86
- log_prob = log_prob.sum(dim=-1, keepdim=True) # Sum over action dimensions
87
+ log_prob = -0.5 * (
88
+ eps.square() + 2.0 * log_std + math.log(2.0 * math.pi)
89
+ )
90
+ log_prob -= th.log(1.0 - action.pow(2) + 1e-6) # Tanh correction
91
+ log_prob = log_prob.sum(dim=-1, keepdim=True) # Sum over action dimensions
87
92
 
88
93
  return action, log_prob
89
94
 
@@ -122,23 +127,27 @@ class SACLearner(OffPolicyLearner):
122
127
  critic1_loss = self.criterion(current_q1, targets)
123
128
  critic2_loss = self.criterion(current_q2, targets)
124
129
  critic_loss = critic1_loss + critic2_loss
125
- self.optimize(
126
- critic1_loss,
127
- optimizer=self.critic1_optimizer,
128
- parameters=self.critic1.parameters(),
129
- )
130
- self.optimize(
131
- critic2_loss,
132
- optimizer=self.critic2_optimizer,
133
- parameters=self.critic2.parameters(),
134
- )
130
+ self.critic1_optimizer.zero_grad(set_to_none=True)
131
+ self.critic2_optimizer.zero_grad(set_to_none=True)
132
+ critic_loss.backward()
133
+
134
+ if self.clipnorm is not None:
135
+ # Clip each critic independently to preserve the previous behavior.
136
+ th.nn.utils.clip_grad_norm_(
137
+ self.critic1.parameters(),
138
+ self.clipnorm,
139
+ )
140
+ th.nn.utils.clip_grad_norm_(
141
+ self.critic2.parameters(),
142
+ self.clipnorm,
143
+ )
144
+
145
+ self.critic1_optimizer.step()
146
+ self.critic2_optimizer.step()
135
147
 
136
148
  # --------------------------------------------------
137
149
  # 2. Update actor
138
150
  # --------------------------------------------------
139
- self._set_requires_grad(self.critic1, False)
140
- self._set_requires_grad(self.critic2, False)
141
-
142
151
  new_actions, log_probs = self.sample_action_and_log_prob(states)
143
152
  new_action_states = th.cat([states, new_actions], dim=-1)
144
153
  new_q1 = self.critic1(new_action_states)
@@ -159,11 +168,9 @@ class SACLearner(OffPolicyLearner):
159
168
  actor_loss,
160
169
  optimizer=self.actor_optimizer,
161
170
  parameters=self.actor.parameters(),
171
+ clipnorm=self.clipnorm,
162
172
  )
163
173
 
164
- self._set_requires_grad(self.critic1, True)
165
- self._set_requires_grad(self.critic2, True)
166
-
167
174
  # --------------------------------------------------
168
175
  # 3. Update alpha
169
176
  # --------------------------------------------------
@@ -175,6 +182,7 @@ class SACLearner(OffPolicyLearner):
175
182
  alpha_loss,
176
183
  optimizer=self.alpha_optimizer,
177
184
  parameters=[self.log_alpha],
185
+ clipnorm=self.clipnorm,
178
186
  )
179
187
 
180
188
  # Update target networks
@@ -87,7 +87,7 @@ class OnPolicyLearner:
87
87
  if self.config.clip_grad:
88
88
  th.nn.utils.clip_grad_norm_(
89
89
  self.parameters,
90
- self.config.grad_norm_clip,
90
+ self.config.clipnorm,
91
91
  )
92
92
  self.optimizer.step()
93
93
 
@@ -18,7 +18,7 @@ class OnPolicyConfig:
18
18
  num_actions: int = 2
19
19
 
20
20
  clip_grad: bool = True
21
- grad_norm_clip: float = 1.0
21
+ clipnorm: float = 1.0
22
22
 
23
23
  use_entropy: bool = False
24
24
  entropy_max_lambda: float = 0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drlab
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Deep Reinforcement Learning kit for research.
5
5
  Author: Tomas Osarte
6
6
  Keywords: reinforcement-learning,deep-learning,pytorch,gymnasium
@@ -1,4 +1,5 @@
1
1
  import unittest
2
+ from unittest.mock import patch
2
3
 
3
4
  import torch as th
4
5
 
@@ -25,6 +26,39 @@ def parameters_changed(model, before):
25
26
 
26
27
 
27
28
  class LearnerSmokeTest(unittest.TestCase):
29
+ def test_soft_target_update_matches_original_arithmetic_exactly(self):
30
+ source1 = th.nn.Linear(4, 3)
31
+ source2 = th.nn.Linear(3, 2)
32
+ target1 = th.nn.Linear(4, 3)
33
+ target2 = th.nn.Linear(3, 2)
34
+ expected1 = th.nn.Linear(4, 3)
35
+ expected2 = th.nn.Linear(3, 2)
36
+ expected1.load_state_dict(target1.state_dict())
37
+ expected2.load_state_dict(target2.state_dict())
38
+
39
+ model = th.nn.Linear(1, 2)
40
+ learner = DQNLearner(
41
+ model,
42
+ th.optim.SGD(model.parameters(), lr=0.1),
43
+ DQNConfig(num_actions=2, soft_target_update_param=0.005),
44
+ )
45
+
46
+ with th.no_grad():
47
+ for target, source in ((expected1, source1), (expected2, source2)):
48
+ for target_param, source_param in zip(
49
+ target.parameters(), source.parameters()
50
+ ):
51
+ target_param.data.mul_(0.995)
52
+ target_param.data.add_(0.005 * source_param.data)
53
+
54
+ learner.update_targets([(target1, source1), (target2, source2)])
55
+
56
+ for actual, expected in zip(
57
+ [*target1.parameters(), *target2.parameters()],
58
+ [*expected1.parameters(), *expected2.parameters()],
59
+ ):
60
+ self.assertTrue(th.equal(actual, expected))
61
+
28
62
  def test_dqn_train_step_returns_float_and_updates_parameters(self):
29
63
  th.manual_seed(0)
30
64
  model = th.nn.Sequential(
@@ -45,18 +79,45 @@ class LearnerSmokeTest(unittest.TestCase):
45
79
  )
46
80
  before = [param.detach().clone() for param in model.parameters()]
47
81
 
48
- loss = learner.train(
49
- rewards=th.tensor([[1.0], [0.0], [1.0]]),
50
- terminated=th.tensor([[False], [True], [False]]),
51
- states=th.randn(3, 4),
52
- actions=th.tensor([[0], [1], [0]], dtype=th.long),
53
- next_states=th.randn(3, 4),
54
- )
82
+ with patch(
83
+ "torch.nn.utils.clip_grad_norm_",
84
+ wraps=th.nn.utils.clip_grad_norm_,
85
+ ) as clip_grad_norm:
86
+ loss = learner.train(
87
+ rewards=th.tensor([[1.0], [0.0], [1.0]]),
88
+ terminated=th.tensor([[False], [True], [False]]),
89
+ states=th.randn(3, 4),
90
+ actions=th.tensor([[0], [1], [0]], dtype=th.long),
91
+ next_states=th.randn(3, 4),
92
+ )
55
93
 
56
94
  self.assertIsInstance(loss, float)
57
95
  self.assertIsInstance(learner, OffPolicyLearner)
58
96
  self.assertTrue(th.isfinite(th.tensor(loss)))
59
97
  self.assertTrue(parameters_changed(model, before))
98
+ self.assertEqual(clip_grad_norm.call_count, 1)
99
+
100
+ def test_dqn_gradient_clipping_can_be_disabled(self):
101
+ model = th.nn.Linear(4, 2)
102
+ learner = DQNLearner(
103
+ model,
104
+ th.optim.SGD(model.parameters(), lr=0.05),
105
+ DQNConfig(
106
+ num_actions=2,
107
+ clipnorm=None,
108
+ ),
109
+ )
110
+
111
+ with patch("torch.nn.utils.clip_grad_norm_") as clip_grad_norm:
112
+ learner.train(
113
+ rewards=th.tensor([[1.0], [0.0]]),
114
+ terminated=th.tensor([[False], [True]]),
115
+ states=th.randn(2, 4),
116
+ actions=th.tensor([[0], [1]], dtype=th.long),
117
+ next_states=th.randn(2, 4),
118
+ )
119
+
120
+ clip_grad_norm.assert_not_called()
60
121
 
61
122
  def test_sac_train_step_returns_float_and_updates_parameters(self):
62
123
  th.manual_seed(0)
@@ -84,25 +145,77 @@ class LearnerSmokeTest(unittest.TestCase):
84
145
  actor_optimizer=th.optim.SGD(actor.parameters(), lr=0.05),
85
146
  critic1_optimizer=th.optim.SGD(critic1.parameters(), lr=0.05),
86
147
  critic2_optimizer=th.optim.SGD(critic2.parameters(), lr=0.05),
87
- config=SACConfig(action_shape=(action_dim,)),
148
+ config=SACConfig(action_shape=(action_dim,), initial_alpha=0.2),
88
149
  )
89
- before = [param.detach().clone() for param in actor.parameters()]
150
+ actor_before = [param.detach().clone() for param in actor.parameters()]
151
+ critic1_before = [param.detach().clone() for param in critic1.parameters()]
152
+ critic2_before = [param.detach().clone() for param in critic2.parameters()]
153
+ critic_grad_calls = {
154
+ id(parameter): 0
155
+ for parameter in (*critic1.parameters(), *critic2.parameters())
156
+ }
157
+ hooks = []
158
+ for parameter in (*critic1.parameters(), *critic2.parameters()):
159
+ parameter_id = id(parameter)
160
+ hooks.append(
161
+ parameter.register_hook(
162
+ lambda gradient, parameter_id=parameter_id: critic_grad_calls.__setitem__(
163
+ parameter_id,
164
+ critic_grad_calls[parameter_id] + 1,
165
+ )
166
+ )
167
+ )
90
168
 
91
- loss = learner.train(
92
- rewards=th.tensor([[1.0], [0.0], [1.0]]),
93
- terminated=th.tensor([[False], [True], [False]]),
94
- states=th.randn(3, obs_dim),
95
- actions=th.randn(3, action_dim).tanh(),
96
- next_states=th.randn(3, obs_dim),
169
+ self.assertAlmostEqual(learner.alpha.item(), 0.2)
170
+
171
+ probe_states = th.randn(3, obs_dim)
172
+ th.manual_seed(123)
173
+ sampled_actions, sampled_log_probs = learner.sample_action_and_log_prob(
174
+ probe_states
97
175
  )
176
+ th.manual_seed(123)
177
+ mean, std = learner.get_policy_dist(probe_states)
178
+ dist = th.distributions.Normal(mean, std, validate_args=False)
179
+ unsquashed_actions = dist.rsample()
180
+ expected_actions = th.tanh(unsquashed_actions)
181
+ expected_log_probs = dist.log_prob(unsquashed_actions)
182
+ expected_log_probs -= th.log(1.0 - expected_actions.pow(2) + 1e-6)
183
+ expected_log_probs = expected_log_probs.sum(dim=-1, keepdim=True)
184
+
185
+ self.assertTrue(th.equal(sampled_actions, expected_actions))
186
+ self.assertTrue(
187
+ th.allclose(sampled_log_probs, expected_log_probs, atol=1e-6, rtol=1e-5)
188
+ )
189
+
190
+ with patch(
191
+ "torch.nn.utils.clip_grad_norm_",
192
+ wraps=th.nn.utils.clip_grad_norm_,
193
+ ) as clip_grad_norm:
194
+ loss = learner.train(
195
+ rewards=th.tensor([[1.0], [0.0], [1.0]]),
196
+ terminated=th.tensor([[False], [True], [False]]),
197
+ states=th.randn(3, obs_dim),
198
+ actions=th.randn(3, action_dim).tanh(),
199
+ next_states=th.randn(3, obs_dim),
200
+ )
98
201
 
99
202
  self.assertIsInstance(loss, float)
100
203
  self.assertTrue(th.isfinite(th.tensor(loss)))
101
- self.assertTrue(parameters_changed(actor, before))
204
+ self.assertTrue(parameters_changed(actor, actor_before))
205
+ self.assertTrue(parameters_changed(critic1, critic1_before))
206
+ self.assertTrue(parameters_changed(critic2, critic2_before))
207
+ self.assertTrue(all(call_count == 1 for call_count in critic_grad_calls.values()))
208
+ self.assertEqual(clip_grad_norm.call_count, 4)
102
209
  self.assertEqual(
103
210
  set(learner.last_losses),
104
211
  {"actor", "critic", "alpha", "regularization", "total"},
105
212
  )
213
+ for hook in hooks:
214
+ hook.remove()
215
+
216
+ def test_sac_initial_alpha_must_be_positive(self):
217
+ with self.assertRaisesRegex(ValueError, "initial_alpha must be > 0"):
218
+ SACConfig(initial_alpha=0.0)
106
219
 
107
220
  def test_sac_actor_regularizer_contributes_to_loss(self):
108
221
  th.manual_seed(0)
@@ -37,7 +37,7 @@ class PublicAPITest(unittest.TestCase):
37
37
  "WarmupController",
38
38
  }
39
39
 
40
- self.assertEqual(drlab.__version__, "0.2.3")
40
+ self.assertEqual(drlab.__version__, "0.2.4")
41
41
  self.assertTrue(expected.issubset(set(drlab.__all__)))
42
42
  for name in expected:
43
43
  self.assertTrue(hasattr(drlab, name), name)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes