Skip to content

Commit 8068577

Browse files
epalmaEthepalmaEthClemensSchwarke
authored
Add clip and constant std functionalities to Gaussian Distribution (#201)
--------- Co-authored-by: epalmaEth <epalma@student.ethz.ch> Co-authored-by: ClemensSchwarke <clemens.schwarke@gmail.com>
1 parent 64f8ee4 commit 8068577

6 files changed

Lines changed: 223 additions & 43 deletions

File tree

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "RSL-RL: A Learning Library for Robotics Research"
33
message: "If you use this work, please cite the following paper."
44
repository-code: "https://github.com/leggedrobotics/rsl_rl"
55
license: BSD-3-Clause
6-
version: 5.1.0
6+
version: 5.2.0
77
type: software
88
authors:
99
- family-names: Schwarke

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Please keep the lists sorted alphabetically.
3030
## Contributors
3131

3232
* Bikram Pandit
33+
* Emilio Palma
3334
* Eric Vollenweider
3435
* Fabian Jenelten
3536
* Lorenzo Terenzi

docs/guide/configuration.rst

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ Currently, RSL-RL implements two runner classes:
5252
* - ``obs_groups``
5353
- dict[str, list[str]]
5454
- required
55-
- Mapping from observation sets to observation groups coming from the environment. See :ref:`here <observation-configuration>` for more details.
55+
- Mapping from observation sets to observation groups coming from the environment. See :ref:`here
56+
<observation-configuration>` for more details.
5657
* - ``save_interval``
5758
- int
5859
- required
@@ -290,33 +291,7 @@ MLPModel
290291
- dict | None
291292
- ``None``
292293
- Optional output distribution configuration. If provided, the model can output stochastic values sampled from
293-
the distribution.
294-
295-
The ``distribution_cfg`` dictionary contains all parameters required by a specific distribution. RSL-RL implements two
296-
distributions by default: A simple Gaussian distribution (:class:`~rsl_rl.modules.distribution.GaussianDistribution`)
297-
and a Gaussian distribution with state-dependent standard deviation
298-
(:class:`~rsl_rl.modules.distribution.HeteroscedasticGaussianDistribution`). Both require the same parameters:
299-
300-
.. list-table::
301-
:header-rows: 1
302-
:class: no-wrap-type-column
303-
304-
* - Key
305-
- Type
306-
- Default
307-
- Description
308-
* - ``class_name``
309-
- str
310-
- required
311-
- Distribution class name. Valid values: ``"GaussianDistribution"``, ``"HeteroscedasticGaussianDistribution"``.
312-
* - ``init_std``
313-
- float
314-
- ``1.0``
315-
- Initial standard deviation.
316-
* - ``std_type``
317-
- str
318-
- ``"scalar"``
319-
- Parameterization of the standard deviation. Valid values: ``"scalar"``, ``"log"``.
294+
the specified distribution.
320295

321296
RNNModel
322297
^^^^^^^^
@@ -441,6 +416,75 @@ configuration includes the following parameters:
441416
- ``True``
442417
- Whether to flatten the output tensor.
443418

419+
420+
Distribution Configuration
421+
--------------------------
422+
423+
RSL-RL implements two distributions that enable stochastic model outputs: A
424+
:class:`~rsl_rl.modules.distribution.GaussianDistribution` and a
425+
:class:`~rsl_rl.modules.distribution.HeteroscedasticGaussianDistribution` with state-dependent standard deviation, which
426+
may be configured as follows.
427+
428+
GaussianDistribution
429+
^^^^^^^^^^^^^^^^^^^^
430+
431+
.. list-table::
432+
:header-rows: 1
433+
:class: no-wrap-type-column
434+
435+
* - Key
436+
- Type
437+
- Default
438+
- Description
439+
* - ``class_name``
440+
- str
441+
- required
442+
- Distribution class name. Valid values: ``"GaussianDistribution"``.
443+
* - ``init_std``
444+
- float
445+
- ``1.0``
446+
- Initial standard deviation for all dimensions.
447+
* - ``std_range``
448+
- tuple[float, float]
449+
- ``(1e-6, 1e6)``
450+
- Minimum and maximum allowed values for the standard deviation for numerical stability.
451+
* - ``std_type``
452+
- str
453+
- ``"scalar"``
454+
- Whether the standard deviation is stored directly or in log-space. Valid values: ``"scalar"``, ``"log"``.
455+
* - ``learn_std``
456+
- bool
457+
- ``True``
458+
- Whether the standard deviation is learnable or fixed.
459+
460+
HeteroscedasticGaussianDistribution
461+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
462+
463+
.. list-table::
464+
:header-rows: 1
465+
:class: no-wrap-type-column
466+
467+
* - Key
468+
- Type
469+
- Default
470+
- Description
471+
* - ``class_name``
472+
- str
473+
- required
474+
- Distribution class name. Valid values: ``"HeteroscedasticGaussianDistribution"``.
475+
* - ``init_std``
476+
- float
477+
- ``1.0``
478+
- Initial standard deviation (used to initialize the std head bias).
479+
* - ``std_range``
480+
- tuple[float, float]
481+
- ``(1e-6, 1e6)``
482+
- Minimum and maximum allowed values for the standard deviation for numerical stability.
483+
* - ``std_type``
484+
- str
485+
- ``"scalar"``
486+
- Whether the standard deviation is stored directly or in log-space. Valid values: ``"scalar"``, ``"log"``.
487+
444488
Extension Configuration
445489
-----------------------
446490

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "rsl-rl-lib"
7-
version = "5.1.0"
7+
version = "5.2.0"
88
keywords = ["reinforcement-learning", "robotics"]
99
maintainers = [
1010
{ name="Clemens Schwarke", email="cschwarke@ethz.ch" },

rsl_rl/modules/distribution.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import numpy as np
910
import torch
1011
import torch.nn as nn
1112
from torch.distributions import Normal
@@ -129,37 +130,50 @@ def init_mlp_weights(self, mlp: nn.Module) -> None:
129130

130131

131132
class GaussianDistribution(Distribution):
132-
"""Gaussian (Normal) distribution module with state-independent standard deviation.
133+
"""Gaussian distribution module with state-independent standard deviation.
133134
134-
This distribution parameterizes actions using a multivariate Gaussian with diagonal covariance. The standard
135-
deviation is a learnable parameter that is independent of the model input. It can be parameterized in either
136-
"scalar" space (directly) or "log" space.
135+
This distribution parameterizes stochastic outputs using a multivariate Gaussian with diagonal covariance. The
136+
standard deviation can be a learnable parameter or a constant. It can be parameterized in either "scalar" space or
137+
"log" space and is clamped to a specified range.
138+
139+
.. note::
140+
If the standard deviation type is set to "log", the provided arguments are still interpreted in scalar space,
141+
and converted to log space internally.
137142
"""
138143

139144
def __init__(
140145
self,
141146
output_dim: int,
142147
init_std: float = 1.0,
148+
std_range: tuple[float, float] = (1e-6, 1e6),
143149
std_type: str = "scalar",
150+
learn_std: bool = True,
144151
) -> None:
145152
"""Initialize the Gaussian distribution module.
146153
147154
Args:
148155
output_dim: Dimension of the action/output space.
149156
init_std: Initial standard deviation.
157+
std_range: Range for the standard deviation. Should be a tuple of (min, max) values for clamping.
150158
std_type: Parameterization of the standard deviation: "scalar" or "log".
159+
learn_std: Whether the standard deviation should be learnable. If False, it will be fixed to `init_std`.
151160
"""
152161
super().__init__(output_dim)
153162
self.std_type = std_type
154163

155164
# Learnable std parameters
156165
if std_type == "scalar":
157-
self.std_param = nn.Parameter(init_std * torch.ones(output_dim))
166+
self.std_param = nn.Parameter(init_std * torch.ones(output_dim), requires_grad=learn_std)
158167
elif std_type == "log":
159-
self.log_std_param = nn.Parameter(torch.log(init_std * torch.ones(output_dim)))
168+
self.log_std_param = nn.Parameter(torch.log(init_std * torch.ones(output_dim)), requires_grad=learn_std)
160169
else:
161170
raise ValueError(f"Unknown standard deviation type: {std_type}. Should be 'scalar' or 'log'.")
162171

172+
# Clamp the std range to ensure numerical stability and store log space range if needed
173+
self.std_range = list(std_range)
174+
self.std_range[0] = max(self.std_range[0], 1e-6) # Avoid zero std for numerical stability
175+
self.log_std_range = [float(np.log(self.std_range[0])), float(np.log(self.std_range[1]))]
176+
163177
# Internal torch distribution (populated by update())
164178
self._distribution: Normal | None = None
165179

@@ -170,9 +184,10 @@ def update(self, mlp_output: torch.Tensor) -> None:
170184
"""Update the Gaussian distribution from MLP output."""
171185
mean = mlp_output
172186
if self.std_type == "scalar":
173-
std = self.std_param.expand_as(mean)
187+
std = self.std_param.clamp(self.std_range[0], self.std_range[1])
174188
elif self.std_type == "log":
175-
std = torch.exp(self.log_std_param).expand_as(mean)
189+
log_std = self.log_std_param.clamp(self.log_std_range[0], self.log_std_range[1])
190+
std = torch.exp(log_std)
176191
self._distribution = Normal(mean, std)
177192

178193
def sample(self) -> torch.Tensor:
@@ -226,24 +241,30 @@ def kl_divergence(self, old_params: tuple[torch.Tensor, ...], new_params: tuple[
226241

227242

228243
class HeteroscedasticGaussianDistribution(GaussianDistribution):
229-
"""Gaussian (Normal) distribution module with state-dependent standard deviation.
244+
"""Gaussian distribution module with state-dependent standard deviation.
230245
231-
This distribution parameterizes actions using a multivariate Gaussian with diagonal covariance. The standard
232-
deviation is output by the MLP alongside the mean, making it state-dependent (heteroscedastic). It can be
233-
parameterized in either "scalar" space (directly) or "log" space.
246+
This distribution parameterizes stochastic outputs using a multivariate Gaussian with diagonal covariance. The
247+
standard deviation is output by the MLP alongside the mean, making it state-dependent. It can be parameterized in
248+
either "scalar" space or "log" space, and is clamped to a specified range.
249+
250+
.. note::
251+
If the standard deviation type is set to "log", the provided arguments are still interpreted in scalar space,
252+
and converted to log space internally.
234253
"""
235254

236255
def __init__(
237256
self,
238257
output_dim: int,
239258
init_std: float = 1.0,
259+
std_range: tuple[float, float] = (1e-6, 1e6),
240260
std_type: str = "scalar",
241261
) -> None:
242262
"""Initialize the heteroscedastic Gaussian distribution module.
243263
244264
Args:
245265
output_dim: Dimension of the action/output space.
246-
init_std: Initial standard deviation (used to initialize MLP std head bias).
266+
init_std: Initial standard deviation (used to initialize the MLP's std head bias).
267+
std_range: Range for the standard deviation. Should be a tuple of (min, max) values for clamping.
247268
std_type: Parameterization of the standard deviation: "scalar" or "log".
248269
"""
249270
# Skip GaussianDistribution.__init__ to avoid creating unnecessary learnable std parameters.
@@ -254,6 +275,11 @@ def __init__(
254275
if std_type not in ("scalar", "log"):
255276
raise ValueError(f"Unknown standard deviation type: {std_type}. Should be 'scalar' or 'log'.")
256277

278+
# Clamp the std range to ensure numerical stability and store log space range if needed
279+
self.std_range = list(std_range)
280+
self.std_range[0] = max(self.std_range[0], 1e-6) # Avoid zero std for numerical stability
281+
self.log_std_range = [float(np.log(self.std_range[0])), float(np.log(self.std_range[1]))]
282+
257283
# Internal torch distribution (populated by update())
258284
self._distribution: Normal | None = None
259285

@@ -264,8 +290,10 @@ def update(self, mlp_output: torch.Tensor) -> None:
264290
"""Update the Gaussian distribution from MLP output."""
265291
if self.std_type == "scalar":
266292
mean, std = torch.unbind(mlp_output, dim=-2)
293+
std = torch.clamp(std, self.std_range[0], self.std_range[1])
267294
elif self.std_type == "log":
268295
mean, log_std = torch.unbind(mlp_output, dim=-2)
296+
log_std = torch.clamp(log_std, self.log_std_range[0], self.log_std_range[1])
269297
std = torch.exp(log_std)
270298
self._distribution = Normal(mean, std)
271299

0 commit comments

Comments
 (0)