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.
@@ -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()