66
77from __future__ import annotations
88
9+ import numpy as np
910import torch
1011import torch .nn as nn
1112from torch .distributions import Normal
@@ -129,37 +130,50 @@ def init_mlp_weights(self, mlp: nn.Module) -> None:
129130
130131
131132class 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
228243class 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