rlmodule 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rlmodule/__init__.py +3 -0
- rlmodule/skrl/__init__.py +0 -0
- rlmodule/skrl/examples/gymnasium/skrl_gymnasium_pendulum.py +120 -0
- rlmodule/skrl/examples/gymnasium/skrl_gymnasium_pendulum_no_module.py +123 -0
- rlmodule/source/__init__.py +0 -0
- rlmodule/source/deprecated.py +378 -0
- rlmodule/source/model.py +840 -0
- rlmodule/source/modules.py +1003 -0
- rlmodule/source/rlmodel.py +771 -0
- rlmodule-0.0.5.dist-info/LICENSE +21 -0
- rlmodule-0.0.5.dist-info/METADATA +34 -0
- rlmodule-0.0.5.dist-info/RECORD +14 -0
- rlmodule-0.0.5.dist-info/WHEEL +5 -0
- rlmodule-0.0.5.dist-info/top_level.txt +1 -0
rlmodule/source/model.py
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
from typing import Any, Mapping, Optional, Sequence, Tuple, Union
|
|
2
|
+
|
|
3
|
+
import collections
|
|
4
|
+
import gym
|
|
5
|
+
import gymnasium
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
# TODO remove this
|
|
15
|
+
class Shape(Enum):
|
|
16
|
+
"""
|
|
17
|
+
Enum to select the shape of the model's inputs and outputs
|
|
18
|
+
"""
|
|
19
|
+
ONE = 1
|
|
20
|
+
STATES = 0
|
|
21
|
+
OBSERVATIONS = 0
|
|
22
|
+
ACTIONS = -1
|
|
23
|
+
STATES_ACTIONS = -2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Model(torch.nn.Module):
|
|
27
|
+
def __init__(self,
|
|
28
|
+
observation_space: Union[int, Sequence[int], gym.Space, gymnasium.Space],
|
|
29
|
+
action_space: Union[int, Sequence[int], gym.Space, gymnasium.Space],
|
|
30
|
+
device: Optional[Union[str, torch.device]] = None) -> None:
|
|
31
|
+
"""Base class representing a function approximator
|
|
32
|
+
|
|
33
|
+
The following properties are defined:
|
|
34
|
+
|
|
35
|
+
- ``device`` (torch.device): Device to be used for the computations
|
|
36
|
+
- ``observation_space`` (int, sequence of int, gym.Space, gymnasium.Space): Observation/state space
|
|
37
|
+
- ``action_space`` (int, sequence of int, gym.Space, gymnasium.Space): Action space
|
|
38
|
+
- ``num_observations`` (int): Number of elements in the observation/state space
|
|
39
|
+
- ``num_actions`` (int): Number of elements in the action space
|
|
40
|
+
|
|
41
|
+
:param observation_space: Observation/state space or shape.
|
|
42
|
+
The ``num_observations`` property will contain the size of that space
|
|
43
|
+
:type observation_space: int, sequence of int, gym.Space, gymnasium.Space
|
|
44
|
+
:param action_space: Action space or shape.
|
|
45
|
+
The ``num_actions`` property will contain the size of that space
|
|
46
|
+
:type action_space: int, sequence of int, gym.Space, gymnasium.Space
|
|
47
|
+
:param device: Device on which a tensor/array is or will be allocated (default: ``None``).
|
|
48
|
+
If None, the device will be either ``"cuda"`` if available or ``"cpu"``
|
|
49
|
+
:type device: str or torch.device, optional
|
|
50
|
+
|
|
51
|
+
Custom models should override the ``act`` method::
|
|
52
|
+
|
|
53
|
+
import torch
|
|
54
|
+
from skrl.models.torch import Model
|
|
55
|
+
|
|
56
|
+
class CustomModel(Model):
|
|
57
|
+
def __init__(self, observation_space, action_space, device="cuda:0"):
|
|
58
|
+
Model.__init__(self, observation_space, action_space, device)
|
|
59
|
+
|
|
60
|
+
self.layer_1 = nn.Linear(self.num_observations, 64)
|
|
61
|
+
self.layer_2 = nn.Linear(64, self.num_actions)
|
|
62
|
+
|
|
63
|
+
def act(self, inputs, role=""):
|
|
64
|
+
x = F.relu(self.layer_1(inputs["states"]))
|
|
65
|
+
x = F.relu(self.layer_2(x))
|
|
66
|
+
return x, None, {}
|
|
67
|
+
"""
|
|
68
|
+
super(Model, self).__init__()
|
|
69
|
+
|
|
70
|
+
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") if device is None else torch.device(device)
|
|
71
|
+
|
|
72
|
+
self.observation_space = observation_space
|
|
73
|
+
self.action_space = action_space
|
|
74
|
+
self.num_observations = None if observation_space is None else self._get_space_size(observation_space)
|
|
75
|
+
self.num_actions = None if action_space is None else self._get_space_size(action_space)
|
|
76
|
+
|
|
77
|
+
self._random_distribution = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### NEW methods: TODO maybe remove
|
|
81
|
+
def _get_num_units_by_shape(self, shape: Shape) -> int:
|
|
82
|
+
"""Get the number of units in a layer by shape
|
|
83
|
+
|
|
84
|
+
:param model: Model to get the number of units for
|
|
85
|
+
:type model: Model
|
|
86
|
+
:param shape: Shape of the layer
|
|
87
|
+
:type shape: Shape or int
|
|
88
|
+
|
|
89
|
+
:return: Number of units in the layer
|
|
90
|
+
:rtype: int
|
|
91
|
+
"""
|
|
92
|
+
num_units = {Shape.ONE: 1,
|
|
93
|
+
Shape.STATES: self.num_observations,
|
|
94
|
+
Shape.ACTIONS: self.num_actions,
|
|
95
|
+
Shape.STATES_ACTIONS: self.num_observations + self.num_actions}
|
|
96
|
+
try:
|
|
97
|
+
return num_units[shape]
|
|
98
|
+
except:
|
|
99
|
+
return shape
|
|
100
|
+
|
|
101
|
+
def _get_all_inputs(self, inputs):
|
|
102
|
+
"""TODO(ll) comment"""
|
|
103
|
+
# TODO(ll) is it a bad design to invoke self.input_shape that only lives in inherited classes
|
|
104
|
+
if self.input_shape == Shape.STATES:
|
|
105
|
+
return inputs["states"]
|
|
106
|
+
# TODO(ll) does this ever makes sense to call without states?
|
|
107
|
+
elif self.input_shape == Shape.ACTIONS:
|
|
108
|
+
return inputs["taken_actions"]
|
|
109
|
+
elif self.input_shape == Shape.STATES_ACTIONS:
|
|
110
|
+
return torch.cat((inputs["states"], inputs["taken_actions"]), dim=1)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Unknown input shape: {self.input_shape}")
|
|
113
|
+
|
|
114
|
+
### new methods
|
|
115
|
+
|
|
116
|
+
def _get_space_size(self,
|
|
117
|
+
space: Union[int, Sequence[int], gym.Space, gymnasium.Space],
|
|
118
|
+
number_of_elements: bool = True) -> int:
|
|
119
|
+
"""Get the size (number of elements) of a space
|
|
120
|
+
|
|
121
|
+
:param space: Space or shape from which to obtain the number of elements
|
|
122
|
+
:type space: int, sequence of int, gym.Space, or gymnasium.Space
|
|
123
|
+
:param number_of_elements: Whether the number of elements occupied by the space is returned (default: ``True``).
|
|
124
|
+
If ``False``, the shape of the space is returned.
|
|
125
|
+
It only affects Discrete and MultiDiscrete spaces
|
|
126
|
+
:type number_of_elements: bool, optional
|
|
127
|
+
|
|
128
|
+
:raises ValueError: If the space is not supported
|
|
129
|
+
|
|
130
|
+
:return: Size of the space (number of elements)
|
|
131
|
+
:rtype: int
|
|
132
|
+
|
|
133
|
+
Example::
|
|
134
|
+
|
|
135
|
+
# from int
|
|
136
|
+
>>> model._get_space_size(2)
|
|
137
|
+
2
|
|
138
|
+
|
|
139
|
+
# from sequence of int
|
|
140
|
+
>>> model._get_space_size([2, 3])
|
|
141
|
+
6
|
|
142
|
+
|
|
143
|
+
# Box space
|
|
144
|
+
>>> space = gym.spaces.Box(low=-1, high=1, shape=(2, 3))
|
|
145
|
+
>>> model._get_space_size(space)
|
|
146
|
+
6
|
|
147
|
+
|
|
148
|
+
# Discrete space
|
|
149
|
+
>>> space = gym.spaces.Discrete(4)
|
|
150
|
+
>>> model._get_space_size(space)
|
|
151
|
+
4
|
|
152
|
+
>>> model._get_space_size(space, number_of_elements=False)
|
|
153
|
+
1
|
|
154
|
+
|
|
155
|
+
# MultiDiscrete space
|
|
156
|
+
>>> space = gym.spaces.MultiDiscrete([5, 3, 2])
|
|
157
|
+
>>> model._get_space_size(space)
|
|
158
|
+
10
|
|
159
|
+
>>> model._get_space_size(space, number_of_elements=False)
|
|
160
|
+
3
|
|
161
|
+
|
|
162
|
+
# Dict space
|
|
163
|
+
>>> space = gym.spaces.Dict({'a': gym.spaces.Box(low=-1, high=1, shape=(2, 3)),
|
|
164
|
+
... 'b': gym.spaces.Discrete(4)})
|
|
165
|
+
>>> model._get_space_size(space)
|
|
166
|
+
10
|
|
167
|
+
>>> model._get_space_size(space, number_of_elements=False)
|
|
168
|
+
7
|
|
169
|
+
"""
|
|
170
|
+
size = None
|
|
171
|
+
if type(space) in [int, float]:
|
|
172
|
+
size = space
|
|
173
|
+
elif type(space) in [tuple, list]:
|
|
174
|
+
size = np.prod(space)
|
|
175
|
+
elif issubclass(type(space), gym.Space):
|
|
176
|
+
if issubclass(type(space), gym.spaces.Discrete):
|
|
177
|
+
if number_of_elements:
|
|
178
|
+
size = space.n
|
|
179
|
+
else:
|
|
180
|
+
size = 1
|
|
181
|
+
elif issubclass(type(space), gym.spaces.MultiDiscrete):
|
|
182
|
+
if number_of_elements:
|
|
183
|
+
size = np.sum(space.nvec)
|
|
184
|
+
else:
|
|
185
|
+
size = space.nvec.shape[0]
|
|
186
|
+
elif issubclass(type(space), gym.spaces.Box):
|
|
187
|
+
size = np.prod(space.shape)
|
|
188
|
+
elif issubclass(type(space), gym.spaces.Dict):
|
|
189
|
+
size = sum([self._get_space_size(space.spaces[key], number_of_elements) for key in space.spaces])
|
|
190
|
+
elif issubclass(type(space), gymnasium.Space):
|
|
191
|
+
if issubclass(type(space), gymnasium.spaces.Discrete):
|
|
192
|
+
if number_of_elements:
|
|
193
|
+
size = space.n
|
|
194
|
+
else:
|
|
195
|
+
size = 1
|
|
196
|
+
elif issubclass(type(space), gymnasium.spaces.MultiDiscrete):
|
|
197
|
+
if number_of_elements:
|
|
198
|
+
size = np.sum(space.nvec)
|
|
199
|
+
else:
|
|
200
|
+
size = space.nvec.shape[0]
|
|
201
|
+
elif issubclass(type(space), gymnasium.spaces.Box):
|
|
202
|
+
size = np.prod(space.shape)
|
|
203
|
+
elif issubclass(type(space), gymnasium.spaces.Dict):
|
|
204
|
+
size = sum([self._get_space_size(space.spaces[key], number_of_elements) for key in space.spaces])
|
|
205
|
+
if size is None:
|
|
206
|
+
raise ValueError(f"Space type {type(space)} not supported")
|
|
207
|
+
return int(size)
|
|
208
|
+
|
|
209
|
+
def tensor_to_space(self,
|
|
210
|
+
tensor: torch.Tensor,
|
|
211
|
+
space: Union[gym.Space, gymnasium.Space],
|
|
212
|
+
start: int = 0) -> Union[torch.Tensor, dict]:
|
|
213
|
+
"""Map a flat tensor to a Gym/Gymnasium space
|
|
214
|
+
|
|
215
|
+
The mapping is done in the following way:
|
|
216
|
+
|
|
217
|
+
- Tensors belonging to Discrete spaces are returned without modification
|
|
218
|
+
- Tensors belonging to Box spaces are reshaped to the corresponding space shape
|
|
219
|
+
keeping the first dimension (number of samples) as they are
|
|
220
|
+
- Tensors belonging to Dict spaces are mapped into a dictionary with the same keys as the original space
|
|
221
|
+
|
|
222
|
+
:param tensor: Tensor to map from
|
|
223
|
+
:type tensor: torch.Tensor
|
|
224
|
+
:param space: Space to map the tensor to
|
|
225
|
+
:type space: gym.Space or gymnasium.Space
|
|
226
|
+
:param start: Index of the first element of the tensor to map (default: ``0``)
|
|
227
|
+
:type start: int, optional
|
|
228
|
+
|
|
229
|
+
:raises ValueError: If the space is not supported
|
|
230
|
+
|
|
231
|
+
:return: Mapped tensor or dictionary
|
|
232
|
+
:rtype: torch.Tensor or dict
|
|
233
|
+
|
|
234
|
+
Example::
|
|
235
|
+
|
|
236
|
+
>>> space = gym.spaces.Dict({'a': gym.spaces.Box(low=-1, high=1, shape=(2, 3)),
|
|
237
|
+
... 'b': gym.spaces.Discrete(4)})
|
|
238
|
+
>>> tensor = torch.tensor([[-0.3, -0.2, -0.1, 0.1, 0.2, 0.3, 2]])
|
|
239
|
+
>>>
|
|
240
|
+
>>> model.tensor_to_space(tensor, space)
|
|
241
|
+
{'a': tensor([[[-0.3000, -0.2000, -0.1000],
|
|
242
|
+
[ 0.1000, 0.2000, 0.3000]]]),
|
|
243
|
+
'b': tensor([[2.]])}
|
|
244
|
+
"""
|
|
245
|
+
if issubclass(type(space), gym.Space):
|
|
246
|
+
if issubclass(type(space), gym.spaces.Discrete):
|
|
247
|
+
return tensor
|
|
248
|
+
elif issubclass(type(space), gym.spaces.Box):
|
|
249
|
+
return tensor.view(tensor.shape[0], *space.shape)
|
|
250
|
+
elif issubclass(type(space), gym.spaces.Dict):
|
|
251
|
+
output = {}
|
|
252
|
+
for k in sorted(space.keys()):
|
|
253
|
+
end = start + self._get_space_size(space[k], number_of_elements=False)
|
|
254
|
+
output[k] = self.tensor_to_space(tensor[:, start:end], space[k], end)
|
|
255
|
+
start = end
|
|
256
|
+
return output
|
|
257
|
+
else:
|
|
258
|
+
if issubclass(type(space), gymnasium.spaces.Discrete):
|
|
259
|
+
return tensor
|
|
260
|
+
elif issubclass(type(space), gymnasium.spaces.Box):
|
|
261
|
+
return tensor.view(tensor.shape[0], *space.shape)
|
|
262
|
+
elif issubclass(type(space), gymnasium.spaces.Dict):
|
|
263
|
+
output = {}
|
|
264
|
+
for k in sorted(space.keys()):
|
|
265
|
+
end = start + self._get_space_size(space[k], number_of_elements=False)
|
|
266
|
+
output[k] = self.tensor_to_space(tensor[:, start:end], space[k], end)
|
|
267
|
+
start = end
|
|
268
|
+
return output
|
|
269
|
+
raise ValueError(f"Space type {type(space)} not supported")
|
|
270
|
+
|
|
271
|
+
def random_act(self,
|
|
272
|
+
inputs: Mapping[str, Union[torch.Tensor, Any]],
|
|
273
|
+
role: str = "") -> Tuple[torch.Tensor, None, Mapping[str, Union[torch.Tensor, Any]]]:
|
|
274
|
+
"""Act randomly according to the action space
|
|
275
|
+
|
|
276
|
+
:param inputs: Model inputs. The most common keys are:
|
|
277
|
+
|
|
278
|
+
- ``"states"``: state of the environment used to make the decision
|
|
279
|
+
- ``"taken_actions"``: actions taken by the policy for the given states
|
|
280
|
+
:type inputs: dict where the values are typically torch.Tensor
|
|
281
|
+
:param role: Role play by the model (default: ``""``)
|
|
282
|
+
:type role: str, optional
|
|
283
|
+
|
|
284
|
+
:raises NotImplementedError: Unsupported action space
|
|
285
|
+
|
|
286
|
+
:return: Model output. The first component is the action to be taken by the agent
|
|
287
|
+
:rtype: tuple of torch.Tensor, None, and dict
|
|
288
|
+
"""
|
|
289
|
+
# discrete action space (Discrete)
|
|
290
|
+
if issubclass(type(self.action_space), gym.spaces.Discrete) or issubclass(type(self.action_space), gymnasium.spaces.Discrete):
|
|
291
|
+
return torch.randint(self.action_space.n, (inputs["states"].shape[0], 1), device=self.device), None, {}
|
|
292
|
+
# continuous action space (Box)
|
|
293
|
+
elif issubclass(type(self.action_space), gym.spaces.Box) or issubclass(type(self.action_space), gymnasium.spaces.Box):
|
|
294
|
+
if self._random_distribution is None:
|
|
295
|
+
self._random_distribution = torch.distributions.uniform.Uniform(
|
|
296
|
+
low=torch.tensor(self.action_space.low[0], device=self.device, dtype=torch.float32),
|
|
297
|
+
high=torch.tensor(self.action_space.high[0], device=self.device, dtype=torch.float32))
|
|
298
|
+
|
|
299
|
+
return self._random_distribution.sample(sample_shape=(inputs["states"].shape[0], self.num_actions)), None, {}
|
|
300
|
+
else:
|
|
301
|
+
raise NotImplementedError(f"Action space type ({type(self.action_space)}) not supported")
|
|
302
|
+
|
|
303
|
+
def init_parameters(self, method_name: str = "normal_", *args, **kwargs) -> None:
|
|
304
|
+
"""Initialize the model parameters according to the specified method name
|
|
305
|
+
|
|
306
|
+
Method names are from the `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ module.
|
|
307
|
+
Allowed method names are *uniform_*, *normal_*, *constant_*, etc.
|
|
308
|
+
|
|
309
|
+
:param method_name: `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ method name (default: ``"normal_"``)
|
|
310
|
+
:type method_name: str, optional
|
|
311
|
+
:param args: Positional arguments of the method to be called
|
|
312
|
+
:type args: tuple, optional
|
|
313
|
+
:param kwargs: Key-value arguments of the method to be called
|
|
314
|
+
:type kwargs: dict, optional
|
|
315
|
+
|
|
316
|
+
Example::
|
|
317
|
+
|
|
318
|
+
# initialize all parameters with an orthogonal distribution with a gain of 0.5
|
|
319
|
+
>>> model.init_parameters("orthogonal_", gain=0.5)
|
|
320
|
+
|
|
321
|
+
# initialize all parameters as a sparse matrix with a sparsity of 0.1
|
|
322
|
+
>>> model.init_parameters("sparse_", sparsity=0.1)
|
|
323
|
+
"""
|
|
324
|
+
for parameters in self.parameters():
|
|
325
|
+
exec(f"torch.nn.init.{method_name}(parameters, *args, **kwargs)")
|
|
326
|
+
|
|
327
|
+
def init_weights(self, method_name: str = "orthogonal_", *args, **kwargs) -> None:
|
|
328
|
+
"""Initialize the model weights according to the specified method name
|
|
329
|
+
|
|
330
|
+
Method names are from the `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ module.
|
|
331
|
+
Allowed method names are *uniform_*, *normal_*, *constant_*, etc.
|
|
332
|
+
|
|
333
|
+
The following layers will be initialized:
|
|
334
|
+
- torch.nn.Linear
|
|
335
|
+
|
|
336
|
+
:param method_name: `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ method name (default: ``"orthogonal_"``)
|
|
337
|
+
:type method_name: str, optional
|
|
338
|
+
:param args: Positional arguments of the method to be called
|
|
339
|
+
:type args: tuple, optional
|
|
340
|
+
:param kwargs: Key-value arguments of the method to be called
|
|
341
|
+
:type kwargs: dict, optional
|
|
342
|
+
|
|
343
|
+
Example::
|
|
344
|
+
|
|
345
|
+
# initialize all weights with uniform distribution in range [-0.1, 0.1]
|
|
346
|
+
>>> model.init_weights(method_name="uniform_", a=-0.1, b=0.1)
|
|
347
|
+
|
|
348
|
+
# initialize all weights with normal distribution with mean 0 and standard deviation 0.25
|
|
349
|
+
>>> model.init_weights(method_name="normal_", mean=0.0, std=0.25)
|
|
350
|
+
"""
|
|
351
|
+
def _update_weights(module, method_name, args, kwargs):
|
|
352
|
+
for layer in module:
|
|
353
|
+
if isinstance(layer, torch.nn.Sequential):
|
|
354
|
+
_update_weights(layer, method_name, args, kwargs)
|
|
355
|
+
elif isinstance(layer, torch.nn.Linear):
|
|
356
|
+
exec(f"torch.nn.init.{method_name}(layer.weight, *args, **kwargs)")
|
|
357
|
+
|
|
358
|
+
_update_weights(self.children(), method_name, args, kwargs)
|
|
359
|
+
|
|
360
|
+
def init_biases(self, method_name: str = "constant_", *args, **kwargs) -> None:
|
|
361
|
+
"""Initialize the model biases according to the specified method name
|
|
362
|
+
|
|
363
|
+
Method names are from the `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ module.
|
|
364
|
+
Allowed method names are *uniform_*, *normal_*, *constant_*, etc.
|
|
365
|
+
|
|
366
|
+
The following layers will be initialized:
|
|
367
|
+
- torch.nn.Linear
|
|
368
|
+
|
|
369
|
+
:param method_name: `torch.nn.init <https://pytorch.org/docs/stable/nn.init.html>`_ method name (default: ``"constant_"``)
|
|
370
|
+
:type method_name: str, optional
|
|
371
|
+
:param args: Positional arguments of the method to be called
|
|
372
|
+
:type args: tuple, optional
|
|
373
|
+
:param kwargs: Key-value arguments of the method to be called
|
|
374
|
+
:type kwargs: dict, optional
|
|
375
|
+
|
|
376
|
+
Example::
|
|
377
|
+
|
|
378
|
+
# initialize all biases with a constant value (0)
|
|
379
|
+
>>> model.init_biases(method_name="constant_", val=0)
|
|
380
|
+
|
|
381
|
+
# initialize all biases with normal distribution with mean 0 and standard deviation 0.25
|
|
382
|
+
>>> model.init_biases(method_name="normal_", mean=0.0, std=0.25)
|
|
383
|
+
"""
|
|
384
|
+
def _update_biases(module, method_name, args, kwargs):
|
|
385
|
+
for layer in module:
|
|
386
|
+
if isinstance(layer, torch.nn.Sequential):
|
|
387
|
+
_update_biases(layer, method_name, args, kwargs)
|
|
388
|
+
elif isinstance(layer, torch.nn.Linear):
|
|
389
|
+
exec(f"torch.nn.init.{method_name}(layer.bias, *args, **kwargs)")
|
|
390
|
+
|
|
391
|
+
_update_biases(self.children(), method_name, args, kwargs)
|
|
392
|
+
|
|
393
|
+
def get_specification(self) -> Mapping[str, Any]:
|
|
394
|
+
"""Returns the specification of the model
|
|
395
|
+
|
|
396
|
+
The following keys are used by the agents for initialization:
|
|
397
|
+
|
|
398
|
+
- ``"rnn"``: Recurrent Neural Network (RNN) specification for RNN, LSTM and GRU layers/cells
|
|
399
|
+
|
|
400
|
+
- ``"sizes"``: List of RNN shapes (number of layers, number of environments, number of features in the RNN state).
|
|
401
|
+
There must be as many tuples as there are states in the recurrent layer/cell. E.g., LSTM has 2 states (hidden and cell).
|
|
402
|
+
|
|
403
|
+
:return: Dictionary containing advanced specification of the model
|
|
404
|
+
:rtype: dict
|
|
405
|
+
|
|
406
|
+
Example::
|
|
407
|
+
|
|
408
|
+
# model with a LSTM layer.
|
|
409
|
+
# - number of layers: 1
|
|
410
|
+
# - number of environments: 4
|
|
411
|
+
# - number of features in the RNN state: 64
|
|
412
|
+
>>> model.get_specification()
|
|
413
|
+
{'rnn': {'sizes': [(1, 4, 64), (1, 4, 64)]}}
|
|
414
|
+
"""
|
|
415
|
+
return {}
|
|
416
|
+
|
|
417
|
+
def forward(self,
|
|
418
|
+
inputs: Mapping[str, Union[torch.Tensor, Any]],
|
|
419
|
+
role: str = "") -> Tuple[torch.Tensor, Union[torch.Tensor, None], Mapping[str, Union[torch.Tensor, Any]]]:
|
|
420
|
+
"""Forward pass of the model
|
|
421
|
+
|
|
422
|
+
This method calls the ``.act()`` method and returns its outputs
|
|
423
|
+
|
|
424
|
+
:param inputs: Model inputs. The most common keys are:
|
|
425
|
+
|
|
426
|
+
- ``"states"``: state of the environment used to make the decision
|
|
427
|
+
- ``"taken_actions"``: actions taken by the policy for the given states
|
|
428
|
+
:type inputs: dict where the values are typically torch.Tensor
|
|
429
|
+
:param role: Role play by the model (default: ``""``)
|
|
430
|
+
:type role: str, optional
|
|
431
|
+
|
|
432
|
+
:return: Model output. The first component is the action to be taken by the agent.
|
|
433
|
+
The second component is the log of the probability density function for stochastic models
|
|
434
|
+
or None for deterministic models. The third component is a dictionary containing extra output values
|
|
435
|
+
:rtype: tuple of torch.Tensor, torch.Tensor or None, and dict
|
|
436
|
+
"""
|
|
437
|
+
return self.act(inputs, role)
|
|
438
|
+
|
|
439
|
+
def compute(self,
|
|
440
|
+
inputs: Mapping[str, Union[torch.Tensor, Any]],
|
|
441
|
+
role: str = "") -> Tuple[Union[torch.Tensor, Mapping[str, Union[torch.Tensor, Any]]]]:
|
|
442
|
+
"""Define the computation performed (to be implemented by the inheriting classes) by the models
|
|
443
|
+
|
|
444
|
+
:param inputs: Model inputs. The most common keys are:
|
|
445
|
+
|
|
446
|
+
- ``"states"``: state of the environment used to make the decision
|
|
447
|
+
- ``"taken_actions"``: actions taken by the policy for the given states
|
|
448
|
+
:type inputs: dict where the values are typically torch.Tensor
|
|
449
|
+
:param role: Role play by the model (default: ``""``)
|
|
450
|
+
:type role: str, optional
|
|
451
|
+
|
|
452
|
+
:raises NotImplementedError: Child class must implement this method
|
|
453
|
+
|
|
454
|
+
:return: Computation performed by the models
|
|
455
|
+
:rtype: tuple of torch.Tensor and dict
|
|
456
|
+
"""
|
|
457
|
+
raise NotImplementedError("The computation performed by the models (.compute()) is not implemented")
|
|
458
|
+
|
|
459
|
+
def act(self,
|
|
460
|
+
inputs: Mapping[str, Union[torch.Tensor, Any]],
|
|
461
|
+
role: str = "") -> Tuple[torch.Tensor, Union[torch.Tensor, None], Mapping[str, Union[torch.Tensor, Any]]]:
|
|
462
|
+
"""Act according to the specified behavior (to be implemented by the inheriting classes)
|
|
463
|
+
|
|
464
|
+
Agents will call this method to obtain the decision to be taken given the state of the environment.
|
|
465
|
+
This method is currently implemented by the helper models (**GaussianModel**, etc.).
|
|
466
|
+
The classes that inherit from the latter must only implement the ``.compute()`` method
|
|
467
|
+
|
|
468
|
+
:param inputs: Model inputs. The most common keys are:
|
|
469
|
+
|
|
470
|
+
- ``"states"``: state of the environment used to make the decision
|
|
471
|
+
- ``"taken_actions"``: actions taken by the policy for the given states
|
|
472
|
+
:type inputs: dict where the values are typically torch.Tensor
|
|
473
|
+
:param role: Role play by the model (default: ``""``)
|
|
474
|
+
:type role: str, optional
|
|
475
|
+
|
|
476
|
+
:raises NotImplementedError: Child class must implement this method
|
|
477
|
+
|
|
478
|
+
:return: Model output. The first component is the action to be taken by the agent.
|
|
479
|
+
The second component is the log of the probability density function for stochastic models
|
|
480
|
+
or None for deterministic models. The third component is a dictionary containing extra output values
|
|
481
|
+
:rtype: tuple of torch.Tensor, torch.Tensor or None, and dict
|
|
482
|
+
"""
|
|
483
|
+
#logger.warning("Make sure to place Mixins before Model during model definition")
|
|
484
|
+
raise NotImplementedError("The action to be taken by the agent (.act()) is not implemented")
|
|
485
|
+
|
|
486
|
+
def set_mode(self, mode: str) -> None:
|
|
487
|
+
"""Set the model mode (training or evaluation)
|
|
488
|
+
|
|
489
|
+
:param mode: Mode: ``"train"`` for training or ``"eval"`` for evaluation.
|
|
490
|
+
See `torch.nn.Module.train <https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.train>`_
|
|
491
|
+
:type mode: str
|
|
492
|
+
|
|
493
|
+
:raises ValueError: If the mode is not ``"train"`` or ``"eval"``
|
|
494
|
+
"""
|
|
495
|
+
if mode == "train":
|
|
496
|
+
self.train(True)
|
|
497
|
+
elif mode == "eval":
|
|
498
|
+
self.train(False)
|
|
499
|
+
else:
|
|
500
|
+
raise ValueError("Invalid mode. Use 'train' for training or 'eval' for evaluation")
|
|
501
|
+
|
|
502
|
+
def save(self, path: str, state_dict: Optional[dict] = None) -> None:
|
|
503
|
+
"""Save the model to the specified path
|
|
504
|
+
|
|
505
|
+
:param path: Path to save the model to
|
|
506
|
+
:type path: str
|
|
507
|
+
:param state_dict: State dictionary to save (default: ``None``).
|
|
508
|
+
If None, the model's state_dict will be saved
|
|
509
|
+
:type state_dict: dict, optional
|
|
510
|
+
|
|
511
|
+
Example::
|
|
512
|
+
|
|
513
|
+
# save the current model to the specified path
|
|
514
|
+
>>> model.save("/tmp/model.pt")
|
|
515
|
+
|
|
516
|
+
# save an older version of the model to the specified path
|
|
517
|
+
>>> old_state_dict = copy.deepcopy(model.state_dict())
|
|
518
|
+
>>> # ...
|
|
519
|
+
>>> model.save("/tmp/model.pt", old_state_dict)
|
|
520
|
+
"""
|
|
521
|
+
torch.save(self.state_dict() if state_dict is None else state_dict, path)
|
|
522
|
+
|
|
523
|
+
def load(self, path: str) -> None:
|
|
524
|
+
"""Load the model from the specified path
|
|
525
|
+
|
|
526
|
+
The final storage device is determined by the constructor of the model
|
|
527
|
+
|
|
528
|
+
:param path: Path to load the model from
|
|
529
|
+
:type path: str
|
|
530
|
+
|
|
531
|
+
Example::
|
|
532
|
+
|
|
533
|
+
# load the model onto the CPU
|
|
534
|
+
>>> model = Model(observation_space, action_space, device="cpu")
|
|
535
|
+
>>> model.load("model.pt")
|
|
536
|
+
|
|
537
|
+
# load the model onto the GPU 1
|
|
538
|
+
>>> model = Model(observation_space, action_space, device="cuda:1")
|
|
539
|
+
>>> model.load("model.pt")
|
|
540
|
+
"""
|
|
541
|
+
self.load_state_dict(torch.load(path, map_location=self.device))
|
|
542
|
+
self.eval()
|
|
543
|
+
|
|
544
|
+
def migrate(self,
|
|
545
|
+
state_dict: Optional[Mapping[str, torch.Tensor]] = None,
|
|
546
|
+
path: Optional[str] = None,
|
|
547
|
+
name_map: Mapping[str, str] = {},
|
|
548
|
+
auto_mapping: bool = True,
|
|
549
|
+
verbose: bool = False) -> bool:
|
|
550
|
+
"""Migrate the specified extrernal model's state dict to the current model
|
|
551
|
+
|
|
552
|
+
The final storage device is determined by the constructor of the model
|
|
553
|
+
|
|
554
|
+
Only one of ``state_dict`` or ``path`` can be specified.
|
|
555
|
+
The ``path`` parameter allows automatic loading the ``state_dict`` only from files generated
|
|
556
|
+
by the *rl_games* and *stable-baselines3* libraries at the moment
|
|
557
|
+
|
|
558
|
+
For ambiguous models (where 2 or more parameters, for source or current model, have equal shape)
|
|
559
|
+
it is necessary to define the ``name_map``, at least for those parameters, to perform the migration successfully
|
|
560
|
+
|
|
561
|
+
:param state_dict: External model's state dict to migrate from (default: ``None``)
|
|
562
|
+
:type state_dict: Mapping[str, torch.Tensor], optional
|
|
563
|
+
:param path: Path to the external checkpoint to migrate from (default: ``None``)
|
|
564
|
+
:type path: str, optional
|
|
565
|
+
:param name_map: Name map to use for the migration (default: ``{}``).
|
|
566
|
+
Keys are the current parameter names and values are the external parameter names
|
|
567
|
+
:type name_map: Mapping[str, str], optional
|
|
568
|
+
:param auto_mapping: Automatically map the external state dict to the current state dict (default: ``True``)
|
|
569
|
+
:type auto_mapping: bool, optional
|
|
570
|
+
:param verbose: Show model names and migration (default: ``False``)
|
|
571
|
+
:type verbose: bool, optional
|
|
572
|
+
|
|
573
|
+
:raises ValueError: If neither or both of ``state_dict`` and ``path`` parameters have been set
|
|
574
|
+
:raises ValueError: If the correct file type cannot be identified from the ``path`` parameter
|
|
575
|
+
|
|
576
|
+
:return: True if the migration was successful, False otherwise.
|
|
577
|
+
Migration is successful if all parameters of the current model are found in the external model
|
|
578
|
+
:rtype: bool
|
|
579
|
+
|
|
580
|
+
Example::
|
|
581
|
+
|
|
582
|
+
# migrate a rl_games checkpoint with unambiguous state_dict
|
|
583
|
+
>>> model.migrate(path="./runs/Ant/nn/Ant.pth")
|
|
584
|
+
True
|
|
585
|
+
|
|
586
|
+
# migrate a rl_games checkpoint with ambiguous state_dict
|
|
587
|
+
>>> model.migrate(path="./runs/Cartpole/nn/Cartpole.pth", verbose=False)
|
|
588
|
+
[skrl:WARNING] Ambiguous match for log_std_parameter <- [value_mean_std.running_mean, value_mean_std.running_var, a2c_network.sigma]
|
|
589
|
+
[skrl:WARNING] Ambiguous match for net.0.bias <- [a2c_network.actor_mlp.0.bias, a2c_network.actor_mlp.2.bias]
|
|
590
|
+
[skrl:WARNING] Ambiguous match for net.2.bias <- [a2c_network.actor_mlp.0.bias, a2c_network.actor_mlp.2.bias]
|
|
591
|
+
[skrl:WARNING] Ambiguous match for net.4.weight <- [a2c_network.value.weight, a2c_network.mu.weight]
|
|
592
|
+
[skrl:WARNING] Ambiguous match for net.4.bias <- [a2c_network.value.bias, a2c_network.mu.bias]
|
|
593
|
+
[skrl:WARNING] Multiple use of a2c_network.actor_mlp.0.bias -> [net.0.bias, net.2.bias]
|
|
594
|
+
[skrl:WARNING] Multiple use of a2c_network.actor_mlp.2.bias -> [net.0.bias, net.2.bias]
|
|
595
|
+
False
|
|
596
|
+
>>> name_map = {"log_std_parameter": "a2c_network.sigma",
|
|
597
|
+
... "net.0.bias": "a2c_network.actor_mlp.0.bias",
|
|
598
|
+
... "net.2.bias": "a2c_network.actor_mlp.2.bias",
|
|
599
|
+
... "net.4.weight": "a2c_network.mu.weight",
|
|
600
|
+
... "net.4.bias": "a2c_network.mu.bias"}
|
|
601
|
+
>>> model.migrate(path="./runs/Cartpole/nn/Cartpole.pth", name_map=name_map, verbose=True)
|
|
602
|
+
[skrl:INFO] Models
|
|
603
|
+
[skrl:INFO] |-- current: 7 items
|
|
604
|
+
[skrl:INFO] | |-- log_std_parameter : torch.Size([1])
|
|
605
|
+
[skrl:INFO] | |-- net.0.weight : torch.Size([32, 4])
|
|
606
|
+
[skrl:INFO] | |-- net.0.bias : torch.Size([32])
|
|
607
|
+
[skrl:INFO] | |-- net.2.weight : torch.Size([32, 32])
|
|
608
|
+
[skrl:INFO] | |-- net.2.bias : torch.Size([32])
|
|
609
|
+
[skrl:INFO] | |-- net.4.weight : torch.Size([1, 32])
|
|
610
|
+
[skrl:INFO] | |-- net.4.bias : torch.Size([1])
|
|
611
|
+
[skrl:INFO] |-- source: 15 items
|
|
612
|
+
[skrl:INFO] | |-- value_mean_std.running_mean : torch.Size([1])
|
|
613
|
+
[skrl:INFO] | |-- value_mean_std.running_var : torch.Size([1])
|
|
614
|
+
[skrl:INFO] | |-- value_mean_std.count : torch.Size([])
|
|
615
|
+
[skrl:INFO] | |-- running_mean_std.running_mean : torch.Size([4])
|
|
616
|
+
[skrl:INFO] | |-- running_mean_std.running_var : torch.Size([4])
|
|
617
|
+
[skrl:INFO] | |-- running_mean_std.count : torch.Size([])
|
|
618
|
+
[skrl:INFO] | |-- a2c_network.sigma : torch.Size([1])
|
|
619
|
+
[skrl:INFO] | |-- a2c_network.actor_mlp.0.weight : torch.Size([32, 4])
|
|
620
|
+
[skrl:INFO] | |-- a2c_network.actor_mlp.0.bias : torch.Size([32])
|
|
621
|
+
[skrl:INFO] | |-- a2c_network.actor_mlp.2.weight : torch.Size([32, 32])
|
|
622
|
+
[skrl:INFO] | |-- a2c_network.actor_mlp.2.bias : torch.Size([32])
|
|
623
|
+
[skrl:INFO] | |-- a2c_network.value.weight : torch.Size([1, 32])
|
|
624
|
+
[skrl:INFO] | |-- a2c_network.value.bias : torch.Size([1])
|
|
625
|
+
[skrl:INFO] | |-- a2c_network.mu.weight : torch.Size([1, 32])
|
|
626
|
+
[skrl:INFO] | |-- a2c_network.mu.bias : torch.Size([1])
|
|
627
|
+
[skrl:INFO] Migration
|
|
628
|
+
[skrl:INFO] |-- map: log_std_parameter <- a2c_network.sigma
|
|
629
|
+
[skrl:INFO] |-- auto: net.0.weight <- a2c_network.actor_mlp.0.weight
|
|
630
|
+
[skrl:INFO] |-- map: net.0.bias <- a2c_network.actor_mlp.0.bias
|
|
631
|
+
[skrl:INFO] |-- auto: net.2.weight <- a2c_network.actor_mlp.2.weight
|
|
632
|
+
[skrl:INFO] |-- map: net.2.bias <- a2c_network.actor_mlp.2.bias
|
|
633
|
+
[skrl:INFO] |-- map: net.4.weight <- a2c_network.mu.weight
|
|
634
|
+
[skrl:INFO] |-- map: net.4.bias <- a2c_network.mu.bias
|
|
635
|
+
False
|
|
636
|
+
|
|
637
|
+
# migrate a stable-baselines3 checkpoint with unambiguous state_dict
|
|
638
|
+
>>> model.migrate(path="./ddpg_pendulum.zip")
|
|
639
|
+
True
|
|
640
|
+
|
|
641
|
+
# migrate from any exported model by loading its state_dict (unambiguous state_dict)
|
|
642
|
+
>>> state_dict = torch.load("./external_model.pt")
|
|
643
|
+
>>> model.migrate(state_dict=state_dict)
|
|
644
|
+
True
|
|
645
|
+
"""
|
|
646
|
+
if (state_dict is not None) + (path is not None) != 1:
|
|
647
|
+
raise ValueError("Exactly one of state_dict or path may be specified")
|
|
648
|
+
|
|
649
|
+
# load state_dict from path
|
|
650
|
+
if path is not None:
|
|
651
|
+
state_dict = {}
|
|
652
|
+
# rl_games checkpoint
|
|
653
|
+
if path.endswith(".pt") or path.endswith(".pth"):
|
|
654
|
+
checkpoint = torch.load(path, map_location=self.device)
|
|
655
|
+
if type(checkpoint) is dict:
|
|
656
|
+
state_dict = checkpoint.get("model", {})
|
|
657
|
+
# stable-baselines3
|
|
658
|
+
elif path.endswith(".zip"):
|
|
659
|
+
import zipfile
|
|
660
|
+
try:
|
|
661
|
+
archive = zipfile.ZipFile(path, 'r')
|
|
662
|
+
with archive.open('policy.pth', mode="r") as file:
|
|
663
|
+
state_dict = torch.load(file, map_location=self.device)
|
|
664
|
+
except KeyError as e:
|
|
665
|
+
#logger.warning(str(e))
|
|
666
|
+
state_dict = {}
|
|
667
|
+
else:
|
|
668
|
+
raise ValueError("Cannot identify file type")
|
|
669
|
+
|
|
670
|
+
# # show state_dict
|
|
671
|
+
# if verbose:
|
|
672
|
+
# logger.info("Models")
|
|
673
|
+
# logger.info(f" |-- current: {len(self.state_dict().keys())} items")
|
|
674
|
+
# for name, tensor in self.state_dict().items():
|
|
675
|
+
# logger.info(f" | |-- {name} : {list(tensor.shape)}")
|
|
676
|
+
# logger.info(f" |-- source: {len(state_dict.keys())} items")
|
|
677
|
+
# for name, tensor in state_dict.items():
|
|
678
|
+
# logger.info(f" | |-- {name} : {list(tensor.shape)}")
|
|
679
|
+
# logger.info("Migration")
|
|
680
|
+
|
|
681
|
+
# migrate the state_dict to current model
|
|
682
|
+
new_state_dict = collections.OrderedDict()
|
|
683
|
+
match_counter = collections.defaultdict(list)
|
|
684
|
+
used_counter = collections.defaultdict(list)
|
|
685
|
+
for name, tensor in self.state_dict().items():
|
|
686
|
+
for external_name, external_tensor in state_dict.items():
|
|
687
|
+
# mapped names
|
|
688
|
+
if name_map.get(name, "") == external_name:
|
|
689
|
+
if tensor.shape == external_tensor.shape:
|
|
690
|
+
new_state_dict[name] = external_tensor
|
|
691
|
+
match_counter[name].append(external_name)
|
|
692
|
+
used_counter[external_name].append(name)
|
|
693
|
+
# if verbose:
|
|
694
|
+
# logger.info(f" |-- map: {name} <- {external_name}")
|
|
695
|
+
break
|
|
696
|
+
# else:
|
|
697
|
+
# logger.warning(f"Shape mismatch for {name} <- {external_name} : {tensor.shape} != {external_tensor.shape}")
|
|
698
|
+
# auto-mapped names
|
|
699
|
+
if auto_mapping and name not in name_map:
|
|
700
|
+
if tensor.shape == external_tensor.shape:
|
|
701
|
+
if name.endswith(".weight"):
|
|
702
|
+
if external_name.endswith(".weight"):
|
|
703
|
+
new_state_dict[name] = external_tensor
|
|
704
|
+
match_counter[name].append(external_name)
|
|
705
|
+
used_counter[external_name].append(name)
|
|
706
|
+
# if verbose:
|
|
707
|
+
# logger.info(f" |-- auto: {name} <- {external_name}")
|
|
708
|
+
elif name.endswith(".bias"):
|
|
709
|
+
if external_name.endswith(".bias"):
|
|
710
|
+
new_state_dict[name] = external_tensor
|
|
711
|
+
match_counter[name].append(external_name)
|
|
712
|
+
used_counter[external_name].append(name)
|
|
713
|
+
# if verbose:
|
|
714
|
+
# logger.info(f" |-- auto: {name} <- {external_name}")
|
|
715
|
+
else:
|
|
716
|
+
if not external_name.endswith(".weight") and not external_name.endswith(".bias"):
|
|
717
|
+
new_state_dict[name] = external_tensor
|
|
718
|
+
match_counter[name].append(external_name)
|
|
719
|
+
used_counter[external_name].append(name)
|
|
720
|
+
# if verbose:
|
|
721
|
+
# logger.info(f" |-- auto: {name} <- {external_name}")
|
|
722
|
+
|
|
723
|
+
# show ambiguous matches
|
|
724
|
+
status = True
|
|
725
|
+
for name, tensor in self.state_dict().items():
|
|
726
|
+
if len(match_counter.get(name, [])) > 1:
|
|
727
|
+
# logger.warning("Ambiguous match for {} <- [{}]".format(name, ", ".join(match_counter.get(name, []))))
|
|
728
|
+
status = False
|
|
729
|
+
# show missing matches
|
|
730
|
+
for name, tensor in self.state_dict().items():
|
|
731
|
+
if not match_counter.get(name, []):
|
|
732
|
+
# logger.warning(f"Missing match for {name}")
|
|
733
|
+
status = False
|
|
734
|
+
# show multiple uses
|
|
735
|
+
for name, tensor in state_dict.items():
|
|
736
|
+
if len(used_counter.get(name, [])) > 1:
|
|
737
|
+
# logger.warning("Multiple use of {} -> [{}]".format(name, ", ".join(used_counter.get(name, []))))
|
|
738
|
+
status = False
|
|
739
|
+
|
|
740
|
+
# load new state dict
|
|
741
|
+
self.load_state_dict(new_state_dict, strict=False)
|
|
742
|
+
self.eval()
|
|
743
|
+
|
|
744
|
+
return status
|
|
745
|
+
|
|
746
|
+
def freeze_parameters(self, freeze: bool = True) -> None:
|
|
747
|
+
"""Freeze or unfreeze internal parameters
|
|
748
|
+
|
|
749
|
+
- Freeze: disable gradient computation (``parameters.requires_grad = False``)
|
|
750
|
+
- Unfreeze: enable gradient computation (``parameters.requires_grad = True``)
|
|
751
|
+
|
|
752
|
+
:param freeze: Freeze the internal parameters if True, otherwise unfreeze them (default: ``True``)
|
|
753
|
+
:type freeze: bool, optional
|
|
754
|
+
|
|
755
|
+
Example::
|
|
756
|
+
|
|
757
|
+
# freeze model parameters
|
|
758
|
+
>>> model.freeze_parameters(True)
|
|
759
|
+
|
|
760
|
+
# unfreeze model parameters
|
|
761
|
+
>>> model.freeze_parameters(False)
|
|
762
|
+
"""
|
|
763
|
+
for parameters in self.parameters():
|
|
764
|
+
parameters.requires_grad = not freeze
|
|
765
|
+
|
|
766
|
+
def update_parameters(self, model: torch.nn.Module, polyak: float = 1) -> None:
|
|
767
|
+
"""Update internal parameters by hard or soft (polyak averaging) update
|
|
768
|
+
|
|
769
|
+
- Hard update: :math:`\\theta = \\theta_{net}`
|
|
770
|
+
- Soft (polyak averaging) update: :math:`\\theta = (1 - \\rho) \\theta + \\rho \\theta_{net}`
|
|
771
|
+
|
|
772
|
+
:param model: Model used to update the internal parameters
|
|
773
|
+
:type model: torch.nn.Module (skrl.models.torch.Model)
|
|
774
|
+
:param polyak: Polyak hyperparameter between 0 and 1 (default: ``1``).
|
|
775
|
+
A hard update is performed when its value is 1
|
|
776
|
+
:type polyak: float, optional
|
|
777
|
+
|
|
778
|
+
Example::
|
|
779
|
+
|
|
780
|
+
# hard update (from source model)
|
|
781
|
+
>>> model.update_parameters(source_model)
|
|
782
|
+
|
|
783
|
+
# soft update (from source model)
|
|
784
|
+
>>> model.update_parameters(source_model, polyak=0.005)
|
|
785
|
+
"""
|
|
786
|
+
with torch.no_grad():
|
|
787
|
+
# hard update
|
|
788
|
+
if polyak == 1:
|
|
789
|
+
for parameters, model_parameters in zip(self.parameters(), model.parameters()):
|
|
790
|
+
parameters.data.copy_(model_parameters.data)
|
|
791
|
+
# soft update (use in-place operations to avoid creating new parameters)
|
|
792
|
+
else:
|
|
793
|
+
for parameters, model_parameters in zip(self.parameters(), model.parameters()):
|
|
794
|
+
parameters.data.mul_(1 - polyak)
|
|
795
|
+
parameters.data.add_(polyak * model_parameters.data)
|
|
796
|
+
|
|
797
|
+
def broadcast_parameters(self, rank: int = 0):
|
|
798
|
+
"""Broadcast model parameters to the whole group (e.g.: across all nodes) in distributed runs
|
|
799
|
+
|
|
800
|
+
After calling this method, the distributed model will contain the broadcasted parameters from ``rank``
|
|
801
|
+
|
|
802
|
+
:param rank: Worker/process rank from which to broadcast model parameters (default: ``0``)
|
|
803
|
+
:type rank: int
|
|
804
|
+
|
|
805
|
+
Example::
|
|
806
|
+
|
|
807
|
+
# broadcast model parameter from worker/process with rank 1
|
|
808
|
+
>>> if config.torch.is_distributed:
|
|
809
|
+
... model.update_parameters(source_model, rank=1)
|
|
810
|
+
"""
|
|
811
|
+
object_list = [self.state_dict()]
|
|
812
|
+
torch.distributed.broadcast_object_list(object_list, rank)
|
|
813
|
+
self.load_state_dict(object_list[0])
|
|
814
|
+
|
|
815
|
+
def reduce_parameters(self):
|
|
816
|
+
"""Reduce model parameters across all workers/processes in the whole group (e.g.: across all nodes)
|
|
817
|
+
|
|
818
|
+
After calling this method, the distributed model parameters will be bitwise identical for all workers/processes
|
|
819
|
+
|
|
820
|
+
Example::
|
|
821
|
+
|
|
822
|
+
# reduce model parameter across all workers/processes
|
|
823
|
+
>>> if config.torch.is_distributed:
|
|
824
|
+
... model.reduce_parameters()
|
|
825
|
+
"""
|
|
826
|
+
# batch all_reduce ops: https://github.com/entity-neural-network/incubator/pull/220
|
|
827
|
+
gradients = []
|
|
828
|
+
for parameters in self.parameters():
|
|
829
|
+
if parameters.grad is not None:
|
|
830
|
+
gradients.append(parameters.grad.view(-1))
|
|
831
|
+
gradients = torch.cat(gradients)
|
|
832
|
+
|
|
833
|
+
torch.distributed.all_reduce(gradients, op=torch.distributed.ReduceOp.SUM)
|
|
834
|
+
|
|
835
|
+
offset = 0
|
|
836
|
+
for parameters in self.parameters():
|
|
837
|
+
if parameters.grad is not None:
|
|
838
|
+
parameters.grad.data.copy_(gradients[offset:offset + parameters.numel()] \
|
|
839
|
+
.view_as(parameters.grad.data) / int(os.getenv("WORLD_SIZE", "1")))
|
|
840
|
+
offset += parameters.numel()
|