drlab 0.2.1__tar.gz → 0.2.2__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.
- {drlab-0.2.1/src/drlab.egg-info → drlab-0.2.2}/PKG-INFO +1 -1
- {drlab-0.2.1 → drlab-0.2.2}/pyproject.toml +1 -1
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/__init__.py +3 -1
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/controllers/__init__.py +4 -2
- drlab-0.2.2/src/drlab/controllers/warmup.py +42 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/experiments/off_policy_experiment.py +43 -17
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/off_policy/sac.py +12 -2
- {drlab-0.2.1 → drlab-0.2.2/src/drlab.egg-info}/PKG-INFO +1 -1
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab.egg-info/SOURCES.txt +4 -2
- {drlab-0.2.1 → drlab-0.2.2}/tests/test_controllers.py +46 -0
- {drlab-0.2.1 → drlab-0.2.2}/tests/test_learners.py +51 -1
- drlab-0.2.2/tests/test_off_policy_experiment.py +154 -0
- {drlab-0.2.1 → drlab-0.2.2}/tests/test_public_api.py +2 -1
- {drlab-0.2.1 → drlab-0.2.2}/LICENSE +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/README.md +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/setup.cfg +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/controllers/base.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/controllers/e_greedy.py +0 -0
- /drlab-0.2.1/src/drlab/controllers/gaussian_controller.py → /drlab-0.2.2/src/drlab/controllers/gaussian.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/controllers/greedy.py +0 -0
- /drlab-0.2.1/src/drlab/controllers/stochastic_controller.py → /drlab-0.2.2/src/drlab/controllers/stochastic.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/experiments/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/experiments/on_policy_experiment.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/off_policy/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/off_policy/base.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/off_policy/configs.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/off_policy/dqn.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/actor_critic.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/base.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/configs.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/ppo.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/learners/on_policy/reinforce.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/replay/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/replay/replay_buffer.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/replay/transition_batch.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/runners/__init__.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab/runners/runner.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab.egg-info/dependency_links.txt +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab.egg-info/requires.txt +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/src/drlab.egg-info/top_level.txt +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/tests/test_replay.py +0 -0
- {drlab-0.2.1 → drlab-0.2.2}/tests/test_runner.py +0 -0
|
@@ -6,6 +6,7 @@ from drlab.controllers import (
|
|
|
6
6
|
GaussianController,
|
|
7
7
|
GreedyController,
|
|
8
8
|
StochasticController,
|
|
9
|
+
WarmupController,
|
|
9
10
|
)
|
|
10
11
|
from drlab.experiments import (
|
|
11
12
|
OnPolicyExperiment,
|
|
@@ -34,7 +35,7 @@ from drlab.learners import (
|
|
|
34
35
|
from drlab.replay import ReplayBuffer, TransitionBatch
|
|
35
36
|
from drlab.runners import Runner
|
|
36
37
|
|
|
37
|
-
__version__ = "0.2.
|
|
38
|
+
__version__ = "0.2.2"
|
|
38
39
|
|
|
39
40
|
__all__ = [
|
|
40
41
|
"__version__",
|
|
@@ -68,4 +69,5 @@ __all__ = [
|
|
|
68
69
|
"StochasticController",
|
|
69
70
|
"TransitionBatch",
|
|
70
71
|
"ValueTargets",
|
|
72
|
+
"WarmupController",
|
|
71
73
|
]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from .base import ContinuousActionController, Controller, DiscreteActionController
|
|
2
2
|
from .greedy import GreedyController
|
|
3
3
|
from .e_greedy import EpsilonGreedyController
|
|
4
|
-
from .
|
|
5
|
-
from .
|
|
4
|
+
from .stochastic import StochasticController
|
|
5
|
+
from .gaussian import GaussianController
|
|
6
|
+
from .warmup import WarmupController
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"ContinuousActionController",
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"GaussianController",
|
|
13
14
|
"GreedyController",
|
|
14
15
|
"StochasticController",
|
|
16
|
+
"WarmupController",
|
|
15
17
|
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch as th
|
|
5
|
+
|
|
6
|
+
from .base import ContinuousActionController, Controller, DiscreteActionController
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WarmupController(ContinuousActionController, DiscreteActionController):
|
|
10
|
+
def __init__(self, controller: Controller, action_space: Any, warmup_steps: int):
|
|
11
|
+
self.controller = controller
|
|
12
|
+
self.action_space = action_space
|
|
13
|
+
self.warmup_steps = warmup_steps
|
|
14
|
+
self.steps = 0
|
|
15
|
+
self.model = controller.model
|
|
16
|
+
|
|
17
|
+
if hasattr(controller, "num_actions"):
|
|
18
|
+
self.num_actions = controller.num_actions
|
|
19
|
+
if hasattr(controller, "action_dim"):
|
|
20
|
+
self.action_dim = controller.action_dim
|
|
21
|
+
|
|
22
|
+
if warmup_steps < 0:
|
|
23
|
+
raise ValueError("warmup_steps must be >= 0.")
|
|
24
|
+
|
|
25
|
+
def choose(
|
|
26
|
+
self,
|
|
27
|
+
obs: th.Tensor,
|
|
28
|
+
**kwargs,
|
|
29
|
+
) -> th.Tensor:
|
|
30
|
+
if self.steps >= self.warmup_steps:
|
|
31
|
+
return self.controller.choose(obs, **kwargs)
|
|
32
|
+
|
|
33
|
+
batch_size = obs.shape[0]
|
|
34
|
+
self.steps += batch_size
|
|
35
|
+
actions = [self.action_space.sample() for _ in range(batch_size)]
|
|
36
|
+
return th.as_tensor(np.asarray(actions), device=obs.device)
|
|
37
|
+
|
|
38
|
+
def probabilities(self, obs: th.Tensor, **kwargs) -> th.Tensor:
|
|
39
|
+
if self.steps >= self.warmup_steps:
|
|
40
|
+
return self.controller.probabilities(obs, **kwargs)
|
|
41
|
+
|
|
42
|
+
return th.ones((obs.shape[0], self.num_actions), device=obs.device) / self.num_actions
|
|
@@ -2,12 +2,12 @@ import numpy as np
|
|
|
2
2
|
from tqdm import tqdm
|
|
3
3
|
import gymnasium as gym
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Callable
|
|
5
|
+
from typing import Callable
|
|
6
6
|
from torch.utils.tensorboard import SummaryWriter
|
|
7
7
|
|
|
8
8
|
from drlab.learners import OffPolicyLearner, SACLearner
|
|
9
9
|
from drlab.runners import Runner
|
|
10
|
-
from drlab.controllers import Controller
|
|
10
|
+
from drlab.controllers import Controller, WarmupController
|
|
11
11
|
from drlab.replay import TransitionBatch, ReplayBuffer
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -24,6 +24,8 @@ class OffPolicyExperimentConfig:
|
|
|
24
24
|
grad_repeats: int = 1
|
|
25
25
|
step_callback: Callable[[int], None] | None = None
|
|
26
26
|
step_callback_interval: int | None = None
|
|
27
|
+
learning_starts: int | None = None
|
|
28
|
+
warmup_steps: int = 0
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class OffPolicyExperiment:
|
|
@@ -44,9 +46,22 @@ class OffPolicyExperiment:
|
|
|
44
46
|
self.run_steps = config.run_steps
|
|
45
47
|
self.step_callback = config.step_callback
|
|
46
48
|
self.step_callback_interval = config.step_callback_interval
|
|
49
|
+
self.steps = 0
|
|
50
|
+
self.learning_starts = (
|
|
51
|
+
config.batch_size if config.learning_starts is None else config.learning_starts
|
|
52
|
+
)
|
|
53
|
+
self.warmup_steps = config.warmup_steps
|
|
54
|
+
|
|
55
|
+
self._validate_config()
|
|
47
56
|
|
|
48
57
|
# Init drl components
|
|
49
58
|
continous_actions = isinstance(learner, SACLearner)
|
|
59
|
+
if self.warmup_steps > 0:
|
|
60
|
+
controller = WarmupController(
|
|
61
|
+
controller,
|
|
62
|
+
env.action_space,
|
|
63
|
+
warmup_steps=self.warmup_steps,
|
|
64
|
+
)
|
|
50
65
|
self.runner = Runner(
|
|
51
66
|
env,
|
|
52
67
|
controller,
|
|
@@ -73,9 +88,17 @@ class OffPolicyExperiment:
|
|
|
73
88
|
self.writer = SummaryWriter(log_dir=config.log_dir)
|
|
74
89
|
self.experiment_name = config.experiment_name
|
|
75
90
|
|
|
91
|
+
|
|
92
|
+
def _validate_config(self):
|
|
76
93
|
if self.step_callback is not None and self.step_callback_interval is None:
|
|
77
94
|
raise ValueError("step_callback_interval must be set when step_callback is provided.")
|
|
78
95
|
|
|
96
|
+
if self.learning_starts < self.batch_size:
|
|
97
|
+
raise ValueError("learning_starts must be greater than or equal to batch_size.")
|
|
98
|
+
|
|
99
|
+
if self.warmup_steps < 0:
|
|
100
|
+
raise ValueError("warmup_steps must be greater than or equal to 0.")
|
|
101
|
+
|
|
79
102
|
def _make_minibatch(self, batch: TransitionBatch, last_episode: TransitionBatch | None) -> TransitionBatch:
|
|
80
103
|
|
|
81
104
|
# No replay buffer
|
|
@@ -97,7 +120,11 @@ class OffPolicyExperiment:
|
|
|
97
120
|
rest = self.replay_buffer.sample(self.batch_size - ep_len)
|
|
98
121
|
return rest.cat(episode)
|
|
99
122
|
|
|
100
|
-
def _learn_from_batch(
|
|
123
|
+
def _learn_from_batch(
|
|
124
|
+
self,
|
|
125
|
+
batch: TransitionBatch,
|
|
126
|
+
last_episode: TransitionBatch | None,
|
|
127
|
+
) -> float:
|
|
101
128
|
# 1) Add batch to replay buffer
|
|
102
129
|
self.replay_buffer.add(
|
|
103
130
|
states=batch.states,
|
|
@@ -107,7 +134,7 @@ class OffPolicyExperiment:
|
|
|
107
134
|
next_states=batch.next_states,
|
|
108
135
|
returns=batch.returns,
|
|
109
136
|
)
|
|
110
|
-
if self.
|
|
137
|
+
if self.steps < self.learning_starts:
|
|
111
138
|
return 0.0
|
|
112
139
|
|
|
113
140
|
# 2) train repeats
|
|
@@ -121,13 +148,13 @@ class OffPolicyExperiment:
|
|
|
121
148
|
return total_loss / self.grad_repeats
|
|
122
149
|
|
|
123
150
|
def run(self):
|
|
124
|
-
|
|
125
|
-
steps = 0
|
|
151
|
+
|
|
152
|
+
self.steps = 0
|
|
126
153
|
if self.step_callback is not None:
|
|
127
|
-
self.step_callback(steps)
|
|
154
|
+
self.step_callback(self.steps)
|
|
128
155
|
next_callback_step = self.step_callback_interval
|
|
129
156
|
pbar = tqdm(total=self.max_steps, desc=self.experiment_name, dynamic_ncols=True, mininterval=1.0)
|
|
130
|
-
while steps < self.max_steps:
|
|
157
|
+
while self.steps < self.max_steps:
|
|
131
158
|
|
|
132
159
|
# 1. Run batch of transitions
|
|
133
160
|
batch, ep_returns, ep_lengths, last_episode = self.runner.run(
|
|
@@ -135,16 +162,20 @@ class OffPolicyExperiment:
|
|
|
135
162
|
as_numpy=True,
|
|
136
163
|
)
|
|
137
164
|
|
|
165
|
+
step_inc = batch.states.shape[0]
|
|
166
|
+
self.steps += step_inc
|
|
167
|
+
pbar.update(step_inc)
|
|
168
|
+
|
|
138
169
|
# 2. Learn from batch
|
|
139
170
|
loss = self._learn_from_batch(batch, last_episode)
|
|
140
171
|
|
|
141
172
|
# 3. Log results
|
|
142
|
-
self.writer.add_scalar("Loss", loss, steps)
|
|
173
|
+
self.writer.add_scalar("Loss", loss, self.steps)
|
|
143
174
|
if ep_returns:
|
|
144
175
|
mean_return = np.mean(ep_returns)
|
|
145
176
|
mean_length = np.mean(ep_lengths)
|
|
146
|
-
self.writer.add_scalar("Episode return", mean_return, steps)
|
|
147
|
-
self.writer.add_scalar("Episode Length", mean_length, steps)
|
|
177
|
+
self.writer.add_scalar("Episode return", mean_return, self.steps)
|
|
178
|
+
self.writer.add_scalar("Episode Length", mean_length, self.steps)
|
|
148
179
|
|
|
149
180
|
pbar.set_postfix({
|
|
150
181
|
"loss": f"{loss:.3f}",
|
|
@@ -152,12 +183,7 @@ class OffPolicyExperiment:
|
|
|
152
183
|
"len": f"{mean_length:.1f}",
|
|
153
184
|
}, refresh=False)
|
|
154
185
|
|
|
155
|
-
|
|
156
|
-
step_inc = batch.states.shape[0]
|
|
157
|
-
steps += step_inc
|
|
158
|
-
pbar.update(step_inc)
|
|
159
|
-
|
|
160
|
-
while self.step_callback is not None and steps >= next_callback_step:
|
|
186
|
+
while self.step_callback is not None and self.steps >= next_callback_step:
|
|
161
187
|
self.step_callback(next_callback_step)
|
|
162
188
|
next_callback_step += self.step_callback_interval
|
|
163
189
|
|
|
@@ -145,7 +145,16 @@ class SACLearner(OffPolicyLearner):
|
|
|
145
145
|
new_q2 = self.critic2(new_action_states)
|
|
146
146
|
new_q = th.min(new_q1, new_q2)
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
policy_loss = (self.alpha.detach() * log_probs - new_q).mean()
|
|
149
|
+
reg_loss = self.regularization_loss(
|
|
150
|
+
self.actor,
|
|
151
|
+
rewards=rewards,
|
|
152
|
+
dones=dones,
|
|
153
|
+
states=states,
|
|
154
|
+
actions=actions,
|
|
155
|
+
next_states=next_states,
|
|
156
|
+
)
|
|
157
|
+
actor_loss = policy_loss + reg_loss
|
|
149
158
|
self.optimize(
|
|
150
159
|
actor_loss,
|
|
151
160
|
optimizer=self.actor_optimizer,
|
|
@@ -176,9 +185,10 @@ class SACLearner(OffPolicyLearner):
|
|
|
176
185
|
|
|
177
186
|
loss = actor_loss + critic_loss + alpha_loss
|
|
178
187
|
self.last_losses = {
|
|
179
|
-
"actor": float(
|
|
188
|
+
"actor": float(policy_loss.item()),
|
|
180
189
|
"critic": float(critic_loss.item()),
|
|
181
190
|
"alpha": float(alpha_loss.item()),
|
|
191
|
+
"regularization": float(reg_loss.item()),
|
|
182
192
|
"total": float(loss.item()),
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -10,9 +10,10 @@ src/drlab.egg-info/top_level.txt
|
|
|
10
10
|
src/drlab/controllers/__init__.py
|
|
11
11
|
src/drlab/controllers/base.py
|
|
12
12
|
src/drlab/controllers/e_greedy.py
|
|
13
|
-
src/drlab/controllers/
|
|
13
|
+
src/drlab/controllers/gaussian.py
|
|
14
14
|
src/drlab/controllers/greedy.py
|
|
15
|
-
src/drlab/controllers/
|
|
15
|
+
src/drlab/controllers/stochastic.py
|
|
16
|
+
src/drlab/controllers/warmup.py
|
|
16
17
|
src/drlab/experiments/__init__.py
|
|
17
18
|
src/drlab/experiments/off_policy_experiment.py
|
|
18
19
|
src/drlab/experiments/on_policy_experiment.py
|
|
@@ -35,6 +36,7 @@ src/drlab/runners/__init__.py
|
|
|
35
36
|
src/drlab/runners/runner.py
|
|
36
37
|
tests/test_controllers.py
|
|
37
38
|
tests/test_learners.py
|
|
39
|
+
tests/test_off_policy_experiment.py
|
|
38
40
|
tests/test_public_api.py
|
|
39
41
|
tests/test_replay.py
|
|
40
42
|
tests/test_runner.py
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
3
4
|
import torch as th
|
|
4
5
|
|
|
5
6
|
from drlab.controllers import (
|
|
@@ -9,6 +10,7 @@ from drlab.controllers import (
|
|
|
9
10
|
GaussianController,
|
|
10
11
|
GreedyController,
|
|
11
12
|
StochasticController,
|
|
13
|
+
WarmupController,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
|
|
@@ -25,6 +27,14 @@ class FixedGaussianOutput(FixedLogits):
|
|
|
25
27
|
pass
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
class FixedActionSpace:
|
|
31
|
+
def __init__(self, action):
|
|
32
|
+
self.action = action
|
|
33
|
+
|
|
34
|
+
def sample(self):
|
|
35
|
+
return self.action
|
|
36
|
+
|
|
37
|
+
|
|
28
38
|
class ControllerTest(unittest.TestCase):
|
|
29
39
|
def test_discrete_controllers_share_discrete_base(self):
|
|
30
40
|
model = FixedLogits([0.1, 2.0, -1.0])
|
|
@@ -115,3 +125,39 @@ class ControllerTest(unittest.TestCase):
|
|
|
115
125
|
self.assertTrue(th.allclose(probs.sum(dim=-1), th.ones(5)))
|
|
116
126
|
self.assertEqual(actions.shape, (5,))
|
|
117
127
|
self.assertTrue(th.all((0 <= actions) & (actions < 3)))
|
|
128
|
+
|
|
129
|
+
def test_warmup_controller_samples_actions_before_delegating(self):
|
|
130
|
+
model = FixedLogits([1.0, 0.0, 0.0])
|
|
131
|
+
greedy = GreedyController(model, num_actions=3)
|
|
132
|
+
controller = WarmupController(greedy, FixedActionSpace(2), warmup_steps=2)
|
|
133
|
+
obs = th.zeros(1, 2)
|
|
134
|
+
|
|
135
|
+
self.assertIsInstance(controller, ContinuousActionController)
|
|
136
|
+
self.assertIsInstance(controller, DiscreteActionController)
|
|
137
|
+
self.assertTrue(th.equal(controller.choose(obs), th.tensor([2])))
|
|
138
|
+
self.assertTrue(th.equal(controller.choose(obs), th.tensor([2])))
|
|
139
|
+
self.assertTrue(th.equal(controller.choose(obs), th.tensor([0])))
|
|
140
|
+
self.assertEqual(controller.steps, 2)
|
|
141
|
+
|
|
142
|
+
def test_warmup_controller_returns_uniform_discrete_probabilities(self):
|
|
143
|
+
model = FixedLogits([1.0, 0.0, 0.0])
|
|
144
|
+
greedy = GreedyController(model, num_actions=3)
|
|
145
|
+
controller = WarmupController(greedy, FixedActionSpace(2), warmup_steps=1)
|
|
146
|
+
|
|
147
|
+
probs = controller.probabilities(th.zeros(1, 2))
|
|
148
|
+
|
|
149
|
+
self.assertTrue(th.allclose(probs, th.full((1, 3), 1 / 3)))
|
|
150
|
+
|
|
151
|
+
def test_warmup_controller_samples_continuous_actions(self):
|
|
152
|
+
model = FixedGaussianOutput([0.0, 0.0, -3.0, -3.0])
|
|
153
|
+
gaussian = GaussianController(model, action_dim=2, deterministic=True)
|
|
154
|
+
action_space = FixedActionSpace(np.asarray([0.5, -0.25], dtype=np.float32))
|
|
155
|
+
controller = WarmupController(gaussian, action_space, warmup_steps=1)
|
|
156
|
+
obs = th.zeros(2, 2)
|
|
157
|
+
|
|
158
|
+
warmup_actions = controller.choose(obs)
|
|
159
|
+
policy_actions = controller.choose(obs)
|
|
160
|
+
|
|
161
|
+
self.assertEqual(warmup_actions.shape, (2, 2))
|
|
162
|
+
self.assertTrue(th.allclose(warmup_actions[0], th.tensor([0.5, -0.25])))
|
|
163
|
+
self.assertTrue(th.allclose(policy_actions, th.zeros(2, 2)))
|
|
@@ -101,9 +101,59 @@ class LearnerSmokeTest(unittest.TestCase):
|
|
|
101
101
|
self.assertTrue(parameters_changed(actor, before))
|
|
102
102
|
self.assertEqual(
|
|
103
103
|
set(learner.last_losses),
|
|
104
|
-
{"actor", "critic", "alpha", "total"},
|
|
104
|
+
{"actor", "critic", "alpha", "regularization", "total"},
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
+
def test_sac_actor_regularizer_contributes_to_loss(self):
|
|
108
|
+
th.manual_seed(0)
|
|
109
|
+
obs_dim = 4
|
|
110
|
+
action_dim = 2
|
|
111
|
+
actor = th.nn.Sequential(
|
|
112
|
+
th.nn.Linear(obs_dim, 8),
|
|
113
|
+
th.nn.ReLU(),
|
|
114
|
+
th.nn.Linear(8, 2 * action_dim),
|
|
115
|
+
)
|
|
116
|
+
critic1 = th.nn.Sequential(
|
|
117
|
+
th.nn.Linear(obs_dim + action_dim, 8),
|
|
118
|
+
th.nn.ReLU(),
|
|
119
|
+
th.nn.Linear(8, 1),
|
|
120
|
+
)
|
|
121
|
+
critic2 = th.nn.Sequential(
|
|
122
|
+
th.nn.Linear(obs_dim + action_dim, 8),
|
|
123
|
+
th.nn.ReLU(),
|
|
124
|
+
th.nn.Linear(8, 1),
|
|
125
|
+
)
|
|
126
|
+
calls = []
|
|
127
|
+
|
|
128
|
+
def actor_l2(model, rewards, dones, states, actions, next_states):
|
|
129
|
+
calls.append(model)
|
|
130
|
+
return sum(param.square().sum() for param in model.parameters())
|
|
131
|
+
|
|
132
|
+
learner = SACLearner(
|
|
133
|
+
actor=actor,
|
|
134
|
+
critic1=critic1,
|
|
135
|
+
critic2=critic2,
|
|
136
|
+
actor_optimizer=th.optim.SGD(actor.parameters(), lr=0.05),
|
|
137
|
+
critic1_optimizer=th.optim.SGD(critic1.parameters(), lr=0.05),
|
|
138
|
+
critic2_optimizer=th.optim.SGD(critic2.parameters(), lr=0.05),
|
|
139
|
+
config=SACConfig(
|
|
140
|
+
action_shape=(action_dim,),
|
|
141
|
+
regularizers=[actor_l2],
|
|
142
|
+
reg_lams=[0.01],
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
learner.train(
|
|
147
|
+
rewards=th.tensor([[1.0], [0.0], [1.0]]),
|
|
148
|
+
dones=th.tensor([[False], [True], [False]]),
|
|
149
|
+
states=th.randn(3, obs_dim),
|
|
150
|
+
actions=th.randn(3, action_dim).tanh(),
|
|
151
|
+
next_states=th.randn(3, obs_dim),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self.assertEqual(calls, [actor])
|
|
155
|
+
self.assertGreater(learner.last_losses["regularization"], 0.0)
|
|
156
|
+
|
|
107
157
|
def test_reinforce_train_step_returns_float_and_updates_parameters(self):
|
|
108
158
|
th.manual_seed(0)
|
|
109
159
|
actor = th.nn.Sequential(
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import gymnasium as gym
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch as th
|
|
7
|
+
|
|
8
|
+
from drlab.controllers import GreedyController, WarmupController
|
|
9
|
+
from drlab.experiments import OffPolicyExperiment, OffPolicyExperimentConfig
|
|
10
|
+
from drlab.learners import OffPolicyConfig, OffPolicyLearner
|
|
11
|
+
from drlab.replay import TransitionBatch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FixedLogits(th.nn.Module):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.register_buffer("logits", th.tensor([1.0, 0.0]))
|
|
18
|
+
|
|
19
|
+
def forward(self, obs):
|
|
20
|
+
return self.logits.unsqueeze(0).expand(obs.shape[0], -1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OneStepEnv(gym.Env):
|
|
24
|
+
metadata = {}
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.observation_space = gym.spaces.Box(
|
|
28
|
+
low=-np.inf,
|
|
29
|
+
high=np.inf,
|
|
30
|
+
shape=(1,),
|
|
31
|
+
dtype=np.float32,
|
|
32
|
+
)
|
|
33
|
+
self.action_space = gym.spaces.Discrete(2)
|
|
34
|
+
|
|
35
|
+
def reset(self, *, seed=None, options=None):
|
|
36
|
+
super().reset(seed=seed)
|
|
37
|
+
return np.asarray([0.0], dtype=np.float32), {}
|
|
38
|
+
|
|
39
|
+
def step(self, action):
|
|
40
|
+
obs = np.asarray([0.0], dtype=np.float32)
|
|
41
|
+
return obs, 0.0, True, False, {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CountingLearner(OffPolicyLearner):
|
|
45
|
+
def __init__(self):
|
|
46
|
+
super().__init__(OffPolicyConfig())
|
|
47
|
+
self.train_calls = 0
|
|
48
|
+
|
|
49
|
+
def train(
|
|
50
|
+
self,
|
|
51
|
+
rewards: th.Tensor,
|
|
52
|
+
dones: th.Tensor,
|
|
53
|
+
states: th.Tensor,
|
|
54
|
+
actions: th.Tensor,
|
|
55
|
+
next_states: th.Tensor,
|
|
56
|
+
) -> float:
|
|
57
|
+
self.train_calls += 1
|
|
58
|
+
return 7.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def one_transition() -> TransitionBatch:
|
|
62
|
+
return TransitionBatch(
|
|
63
|
+
states=np.asarray([[0.0]], dtype=np.float32),
|
|
64
|
+
actions=np.asarray([[0]], dtype=np.int64),
|
|
65
|
+
rewards=np.asarray([[1.0]], dtype=np.float32),
|
|
66
|
+
dones=np.asarray([[False]], dtype=np.bool_),
|
|
67
|
+
next_states=np.asarray([[1.0]], dtype=np.float32),
|
|
68
|
+
returns=np.asarray([[1.0]], dtype=np.float32),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OffPolicyExperimentTest(unittest.TestCase):
|
|
73
|
+
def make_experiment(self, config: OffPolicyExperimentConfig) -> OffPolicyExperiment:
|
|
74
|
+
env = OneStepEnv()
|
|
75
|
+
learner = CountingLearner()
|
|
76
|
+
controller = GreedyController(FixedLogits(), num_actions=2)
|
|
77
|
+
return OffPolicyExperiment(env, controller, learner, config)
|
|
78
|
+
|
|
79
|
+
def test_learning_starts_default_tracks_batch_size(self):
|
|
80
|
+
with tempfile.TemporaryDirectory() as log_dir:
|
|
81
|
+
experiment = self.make_experiment(
|
|
82
|
+
OffPolicyExperimentConfig(
|
|
83
|
+
max_steps=1,
|
|
84
|
+
batch_size=256,
|
|
85
|
+
log_dir=log_dir,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
self.assertEqual(experiment.learning_starts, 256)
|
|
90
|
+
finally:
|
|
91
|
+
experiment.writer.close()
|
|
92
|
+
|
|
93
|
+
def test_learning_starts_uses_current_steps_after_increment(self):
|
|
94
|
+
with tempfile.TemporaryDirectory() as log_dir:
|
|
95
|
+
experiment = self.make_experiment(
|
|
96
|
+
OffPolicyExperimentConfig(
|
|
97
|
+
max_steps=2,
|
|
98
|
+
batch_size=2,
|
|
99
|
+
replay_buffer_size=4,
|
|
100
|
+
use_last_episode=False,
|
|
101
|
+
log_dir=log_dir,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
batch = one_transition()
|
|
106
|
+
|
|
107
|
+
experiment.steps = 1
|
|
108
|
+
loss = experiment._learn_from_batch(batch, None)
|
|
109
|
+
self.assertEqual(loss, 0.0)
|
|
110
|
+
self.assertEqual(experiment.learner.train_calls, 0)
|
|
111
|
+
|
|
112
|
+
experiment.steps = 2
|
|
113
|
+
loss = experiment._learn_from_batch(batch, None)
|
|
114
|
+
self.assertEqual(loss, 7.0)
|
|
115
|
+
self.assertEqual(experiment.learner.train_calls, 1)
|
|
116
|
+
finally:
|
|
117
|
+
experiment.writer.close()
|
|
118
|
+
|
|
119
|
+
def test_warmup_steps_wraps_controller(self):
|
|
120
|
+
with tempfile.TemporaryDirectory() as log_dir:
|
|
121
|
+
experiment = self.make_experiment(
|
|
122
|
+
OffPolicyExperimentConfig(
|
|
123
|
+
max_steps=1,
|
|
124
|
+
warmup_steps=10,
|
|
125
|
+
log_dir=log_dir,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
self.assertEqual(experiment.warmup_steps, 10)
|
|
130
|
+
self.assertIsInstance(experiment.runner.controller, WarmupController)
|
|
131
|
+
self.assertEqual(experiment.runner.controller.warmup_steps, 10)
|
|
132
|
+
finally:
|
|
133
|
+
experiment.writer.close()
|
|
134
|
+
|
|
135
|
+
def test_zero_warmup_steps_keeps_original_controller(self):
|
|
136
|
+
with tempfile.TemporaryDirectory() as log_dir:
|
|
137
|
+
experiment = self.make_experiment(
|
|
138
|
+
OffPolicyExperimentConfig(max_steps=1, log_dir=log_dir)
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
self.assertNotIsInstance(experiment.runner.controller, WarmupController)
|
|
142
|
+
finally:
|
|
143
|
+
experiment.writer.close()
|
|
144
|
+
|
|
145
|
+
def test_negative_warmup_steps_is_rejected(self):
|
|
146
|
+
with tempfile.TemporaryDirectory() as log_dir:
|
|
147
|
+
with self.assertRaisesRegex(ValueError, "warmup_steps"):
|
|
148
|
+
self.make_experiment(
|
|
149
|
+
OffPolicyExperimentConfig(
|
|
150
|
+
max_steps=1,
|
|
151
|
+
warmup_steps=-1,
|
|
152
|
+
log_dir=log_dir,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
@@ -34,9 +34,10 @@ class PublicAPITest(unittest.TestCase):
|
|
|
34
34
|
"StochasticController",
|
|
35
35
|
"TransitionBatch",
|
|
36
36
|
"ValueTargets",
|
|
37
|
+
"WarmupController",
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
self.assertEqual(drlab.__version__, "0.2.
|
|
40
|
+
self.assertEqual(drlab.__version__, "0.2.2")
|
|
40
41
|
self.assertTrue(expected.issubset(set(drlab.__all__)))
|
|
41
42
|
for name in expected:
|
|
42
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|