564 lines
21 KiB
Python
564 lines
21 KiB
Python
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Sequence
|
|
from typing import TYPE_CHECKING
|
|
|
|
import paddle
|
|
from paddle.base.data_feeder import convert_dtype
|
|
from paddle.distribution import constraint, distribution
|
|
from paddle.framework import in_dynamic_mode
|
|
from paddle.utils.decorator_utils import param_one_alias
|
|
|
|
if TYPE_CHECKING:
|
|
from paddle import Tensor
|
|
from paddle._typing.dtype_like import _DTypeLiteral
|
|
|
|
|
|
class MultivariateNormal(distribution.Distribution):
|
|
r"""The Multivariate Normal distribution is a type multivariate continuous distribution defined on the real set, with parameter: `loc` and any one
|
|
of the following parameters characterizing the variance: `covariance_matrix`, `precision_matrix`, `scale_tril`.
|
|
|
|
Mathematical details
|
|
|
|
The probability density function (pdf) is
|
|
|
|
.. math::
|
|
|
|
p(X ;\mu, \Sigma) = \frac{1}{\sqrt{(2\pi)^k |\Sigma|}} \exp(-\frac{1}{2}(X - \mu)^{\intercal} \Sigma^{-1} (X - \mu))
|
|
|
|
In the above equation:
|
|
|
|
* :math:`X`: is a k-dim random vector.
|
|
* :math:`loc = \mu`: is the k-dim mean vector.
|
|
* :math:`covariance_matrix = \Sigma`: is the k-by-k covariance matrix.
|
|
|
|
Args:
|
|
loc(int|float|Tensor): The mean of Multivariate Normal distribution. If the input data type is int or float, the data type of `loc` will be
|
|
convert to a 1-D Tensor the paddle global default dtype.
|
|
covariance_matrix(Tensor|None): The covariance matrix of Multivariate Normal distribution. The data type of `covariance_matrix` will be convert
|
|
to be the same as the type of loc.
|
|
precision_matrix(Tensor|None): The inverse of the covariance matrix. The data type of `precision_matrix` will be convert to be the same as the
|
|
type of loc.
|
|
scale_tril(Tensor|None): The cholesky decomposition (lower triangular matrix) of the covariance matrix. The data type of `scale_tril` will be
|
|
convert to be the same as the type of loc.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
>>> from paddle.distribution import MultivariateNormal
|
|
>>> paddle.set_device("cpu")
|
|
>>> paddle.seed(100)
|
|
|
|
>>> rv = MultivariateNormal(
|
|
... loc=paddle.to_tensor([2.0, 5.0]),
|
|
... covariance_matrix=paddle.to_tensor([[2.0, 1.0], [1.0, 2.0]]),
|
|
... )
|
|
|
|
>>> print(rv.sample([3, 2]))
|
|
Tensor(shape=[3, 2, 2], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
[[[-0.00339603, 4.31556797],
|
|
[ 2.01385283, 4.63553190]],
|
|
[[ 0.10132277, 3.11323833],
|
|
[ 2.37435842, 3.56635118]],
|
|
[[ 2.89701366, 5.10602522],
|
|
[-0.46329355, 3.14768648]]])
|
|
|
|
>>> print(rv.mean)
|
|
Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
[2., 5.])
|
|
|
|
>>> print(rv.variance)
|
|
Tensor(shape=[2], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
[1.99999988, 2. ])
|
|
|
|
>>> print(rv.entropy())
|
|
Tensor(shape=[], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
3.38718319)
|
|
|
|
>>> rv1 = MultivariateNormal(
|
|
... loc=paddle.to_tensor([2.0, 5.0]),
|
|
... covariance_matrix=paddle.to_tensor([[2.0, 1.0], [1.0, 2.0]]),
|
|
... )
|
|
>>> rv2 = MultivariateNormal(
|
|
... loc=paddle.to_tensor([-1.0, 3.0]), covariance_matrix=paddle.to_tensor([[3.0, 2.0], [2.0, 3.0]])
|
|
... )
|
|
>>> print(rv1.kl_divergence(rv2))
|
|
Tensor(shape=[], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
1.55541301)
|
|
"""
|
|
|
|
loc: Tensor
|
|
covariance_matrix: Tensor | None
|
|
precision_matrix: Tensor | None
|
|
scale_tril: Tensor | None
|
|
dtype: _DTypeLiteral
|
|
|
|
arg_constraints = {
|
|
"loc": constraint.real_vector,
|
|
"covariance_matrix": constraint.positive_definite,
|
|
"precision_matrix": constraint.positive_definite,
|
|
"scale_tril": constraint.lower_cholesky,
|
|
}
|
|
support = constraint.real_vector
|
|
has_rsample = True
|
|
|
|
def __init__(
|
|
self,
|
|
loc: float | Tensor,
|
|
covariance_matrix: Tensor | None = None,
|
|
precision_matrix: Tensor | None = None,
|
|
scale_tril: Tensor | None = None,
|
|
validate_args: bool | None = None,
|
|
):
|
|
self.dtype = paddle.get_default_dtype()
|
|
if isinstance(loc, (float, int)):
|
|
loc = paddle.to_tensor([loc], dtype=self.dtype)
|
|
else:
|
|
self.dtype = convert_dtype(loc.dtype)
|
|
if loc.dim() < 1:
|
|
raise ValueError("loc must be at least one-dimensional.")
|
|
if (covariance_matrix is not None) + (scale_tril is not None) + (
|
|
precision_matrix is not None
|
|
) != 1:
|
|
raise ValueError(
|
|
"Exactly one of covariance_matrix or precision_matrix or scale_tril may be specified."
|
|
)
|
|
|
|
if scale_tril is not None:
|
|
if scale_tril.dim() < 2:
|
|
raise ValueError(
|
|
"scale_tril matrix must be at least two-dimensional, "
|
|
"with optional leading batch dimensions"
|
|
)
|
|
scale_tril = paddle.cast(scale_tril, dtype=self.dtype)
|
|
batch_shape = paddle.broadcast_shape(
|
|
scale_tril.shape[:-2], loc.shape[:-1]
|
|
)
|
|
self.scale_tril = scale_tril.expand(
|
|
[*batch_shape, scale_tril.shape[-2], scale_tril.shape[-1]]
|
|
)
|
|
elif covariance_matrix is not None:
|
|
if covariance_matrix.dim() < 2:
|
|
raise ValueError(
|
|
"covariance_matrix must be at least two-dimensional, "
|
|
"with optional leading batch dimensions"
|
|
)
|
|
covariance_matrix = paddle.cast(covariance_matrix, dtype=self.dtype)
|
|
batch_shape = paddle.broadcast_shape(
|
|
covariance_matrix.shape[:-2], loc.shape[:-1]
|
|
)
|
|
self.covariance_matrix = covariance_matrix.expand(
|
|
[
|
|
*batch_shape,
|
|
covariance_matrix.shape[-2],
|
|
covariance_matrix.shape[-1],
|
|
]
|
|
)
|
|
else:
|
|
if precision_matrix.dim() < 2:
|
|
raise ValueError(
|
|
"precision_matrix must be at least two-dimensional, "
|
|
"with optional leading batch dimensions"
|
|
)
|
|
precision_matrix = paddle.cast(precision_matrix, dtype=self.dtype)
|
|
batch_shape = paddle.broadcast_shape(
|
|
precision_matrix.shape[:-2], loc.shape[:-1]
|
|
)
|
|
self.precision_matrix = precision_matrix.expand(
|
|
[
|
|
*batch_shape,
|
|
precision_matrix.shape[-2],
|
|
precision_matrix.shape[-1],
|
|
]
|
|
)
|
|
self.loc = loc.expand([*batch_shape, -1])
|
|
event_shape = self.loc.shape[-1:]
|
|
super().__init__(batch_shape, event_shape, validate_args=validate_args)
|
|
if in_dynamic_mode() and self._validate_args_enabled:
|
|
self._validate_parameters(
|
|
scale_tril=scale_tril,
|
|
covariance_matrix=covariance_matrix,
|
|
precision_matrix=precision_matrix,
|
|
)
|
|
|
|
if scale_tril is not None:
|
|
self._unbroadcasted_scale_tril = scale_tril
|
|
elif covariance_matrix is not None:
|
|
self._unbroadcasted_scale_tril = paddle.linalg.cholesky(
|
|
covariance_matrix
|
|
)
|
|
else:
|
|
self._unbroadcasted_scale_tril = precision_to_scale_tril(
|
|
precision_matrix
|
|
)
|
|
|
|
def _validate_parameters(
|
|
self,
|
|
*,
|
|
scale_tril: Tensor | None = None,
|
|
covariance_matrix: Tensor | None = None,
|
|
precision_matrix: Tensor | None = None,
|
|
) -> None:
|
|
if scale_tril is not None:
|
|
matrix_name = "scale_tril"
|
|
matrix_value = self.scale_tril
|
|
elif covariance_matrix is not None:
|
|
matrix_name = "covariance_matrix"
|
|
matrix_value = self.covariance_matrix
|
|
else:
|
|
matrix_name = "precision_matrix"
|
|
matrix_value = self.precision_matrix
|
|
|
|
for param, value in (
|
|
("loc", self.loc),
|
|
(matrix_name, matrix_value),
|
|
):
|
|
constraint_ = self.arg_constraints[param]
|
|
valid = constraint_.check(value)
|
|
if not bool(valid.all()):
|
|
raise ValueError(
|
|
f"Expected parameter {param} "
|
|
f"({type(value).__name__} of shape {tuple(value.shape)}) "
|
|
f"of distribution {self!r} "
|
|
f"to satisfy the constraint {constraint_!r}, "
|
|
f"but found invalid values:\n{value}"
|
|
)
|
|
|
|
def expand(self, batch_shape, _instance=None):
|
|
new = (
|
|
self.__class__.__new__(self.__class__)
|
|
if _instance is None
|
|
else _instance
|
|
)
|
|
batch_shape = tuple(batch_shape)
|
|
loc_shape = batch_shape + self.event_shape
|
|
cov_shape = batch_shape + self.event_shape + self.event_shape
|
|
new.loc = self.loc.expand(loc_shape)
|
|
new._unbroadcasted_scale_tril = self._unbroadcasted_scale_tril
|
|
if "_covariance_matrix" in self.__dict__:
|
|
new._covariance_matrix = self.covariance_matrix.expand(cov_shape)
|
|
if "_scale_tril" in self.__dict__:
|
|
new._scale_tril = self.scale_tril.expand(cov_shape)
|
|
if "_precision_matrix" in self.__dict__:
|
|
new._precision_matrix = self.precision_matrix.expand(cov_shape)
|
|
super(MultivariateNormal, new).__init__(
|
|
batch_shape, self.event_shape, validate_args=False
|
|
)
|
|
new._validate_args_enabled = self._validate_args_enabled
|
|
return new
|
|
|
|
@property
|
|
def scale_tril(self) -> Tensor:
|
|
if "_scale_tril" not in self.__dict__:
|
|
self._scale_tril = self._unbroadcasted_scale_tril.expand(
|
|
self._batch_shape + self._event_shape + self._event_shape
|
|
)
|
|
return self._scale_tril
|
|
|
|
@scale_tril.setter
|
|
def scale_tril(self, value: Tensor) -> None:
|
|
self._scale_tril = value
|
|
|
|
@property
|
|
def covariance_matrix(self) -> Tensor:
|
|
if "_covariance_matrix" not in self.__dict__:
|
|
new_perm = list(range(len(self._unbroadcasted_scale_tril.shape)))
|
|
new_perm[-1], new_perm[-2] = new_perm[-2], new_perm[-1]
|
|
self._covariance_matrix = paddle.matmul(
|
|
self._unbroadcasted_scale_tril,
|
|
self._unbroadcasted_scale_tril.transpose(new_perm),
|
|
).expand(self._batch_shape + self._event_shape + self._event_shape)
|
|
return self._covariance_matrix
|
|
|
|
@covariance_matrix.setter
|
|
def covariance_matrix(self, value: Tensor) -> None:
|
|
self._covariance_matrix = value
|
|
|
|
@property
|
|
def precision_matrix(self) -> Tensor:
|
|
if "_precision_matrix" not in self.__dict__:
|
|
self._precision_matrix = paddle.linalg.cholesky_inverse(
|
|
self._unbroadcasted_scale_tril
|
|
).expand(self._batch_shape + self._event_shape + self._event_shape)
|
|
return self._precision_matrix
|
|
|
|
@precision_matrix.setter
|
|
def precision_matrix(self, value: Tensor) -> None:
|
|
self._precision_matrix = value
|
|
|
|
@property
|
|
def mean(self) -> Tensor:
|
|
"""Mean of Multivariate Normal distribution.
|
|
|
|
Returns:
|
|
Tensor: mean value.
|
|
"""
|
|
return self.loc
|
|
|
|
@property
|
|
def variance(self) -> Tensor:
|
|
"""Variance of Multivariate Normal distribution.
|
|
|
|
Returns:
|
|
Tensor: variance value.
|
|
"""
|
|
return (
|
|
paddle.square(self._unbroadcasted_scale_tril)
|
|
.sum(-1)
|
|
.expand(self._batch_shape + self._event_shape)
|
|
)
|
|
|
|
@property
|
|
def mode(self) -> Tensor:
|
|
return self.loc
|
|
|
|
@mode.setter
|
|
def mode(self, value: Tensor) -> None:
|
|
self.loc = value
|
|
|
|
@param_one_alias(["shape", "sample_shape"])
|
|
def sample(self, shape: Sequence[int] = []) -> Tensor:
|
|
"""Generate Multivariate Normal samples of the specified shape. The final shape would be ``sample_shape + batch_shape + event_shape``.
|
|
|
|
Args:
|
|
shape (Sequence[int], optional): Prepended shape of the generated samples.
|
|
|
|
Returns:
|
|
Tensor, Sampled data with shape `sample_shape` + `batch_shape` + `event_shape`. The data type is the same as `self.loc`.
|
|
"""
|
|
with paddle.no_grad():
|
|
return self.rsample(shape)
|
|
|
|
@param_one_alias(["shape", "sample_shape"])
|
|
def rsample(self, shape: Sequence[int] = []) -> Tensor:
|
|
"""Generate Multivariate Normal samples of the specified shape. The final shape would be ``sample_shape + batch_shape + event_shape``.
|
|
|
|
Args:
|
|
shape (Sequence[int], optional): Prepended shape of the generated samples.
|
|
|
|
Returns:
|
|
Tensor, Sampled data with shape `sample_shape` + `batch_shape` + `event_shape`. The data type is the same as `self.loc`.
|
|
"""
|
|
if not isinstance(shape, Sequence):
|
|
raise TypeError('sample shape must be Sequence object.')
|
|
output_shape = self._extend_shape(shape)
|
|
eps = paddle.cast(paddle.normal(shape=output_shape), dtype=self.dtype)
|
|
return self.loc + paddle.matmul(
|
|
self._unbroadcasted_scale_tril, eps.unsqueeze(-1)
|
|
).squeeze(-1)
|
|
|
|
def log_prob(self, value: Tensor) -> Tensor:
|
|
"""Log probability density function.
|
|
|
|
Args:
|
|
value (Tensor): The input tensor.
|
|
|
|
Returns:
|
|
Tensor: log probability. The data type is the same as `self.loc`.
|
|
"""
|
|
value = paddle.cast(value, dtype=self.dtype)
|
|
if in_dynamic_mode() and self._validate_args_enabled:
|
|
self._validate_sample(value)
|
|
|
|
diff = value - self.loc
|
|
M = batch_mahalanobis(self._unbroadcasted_scale_tril, diff)
|
|
half_log_det = (
|
|
self._unbroadcasted_scale_tril.diagonal(axis1=-2, axis2=-1)
|
|
.log()
|
|
.sum(-1)
|
|
)
|
|
return (
|
|
-0.5 * (self._event_shape[0] * math.log(2 * math.pi) + M)
|
|
- half_log_det
|
|
)
|
|
|
|
def prob(self, value: Tensor) -> Tensor:
|
|
"""Probability density function.
|
|
|
|
Args:
|
|
value (Tensor): The input tensor.
|
|
|
|
Returns:
|
|
Tensor: probability. The data type is the same as `self.loc`.
|
|
"""
|
|
return paddle.exp(self.log_prob(value))
|
|
|
|
def entropy(self) -> Tensor:
|
|
r"""Shannon entropy in nats.
|
|
|
|
The entropy is
|
|
|
|
.. math::
|
|
|
|
\mathcal{H}(X) = \frac{n}{2} \log(2\pi) + \log {\det A} + \frac{n}{2}
|
|
|
|
In the above equation:
|
|
|
|
* :math:`\Omega`: is the support of the distribution.
|
|
|
|
Returns:
|
|
Tensor, Shannon entropy of Multivariate Normal distribution. The data type is the same as `self.loc`.
|
|
"""
|
|
half_log_det = (
|
|
self._unbroadcasted_scale_tril.diagonal(axis1=-2, axis2=-1)
|
|
.log()
|
|
.sum(-1)
|
|
)
|
|
H = (
|
|
0.5 * self._event_shape[0] * (1.0 + math.log(2 * math.pi))
|
|
+ half_log_det
|
|
)
|
|
if len(self._batch_shape) == 0:
|
|
return H
|
|
else:
|
|
return H.expand(self._batch_shape)
|
|
|
|
def kl_divergence(self, other: MultivariateNormal) -> Tensor:
|
|
r"""The KL-divergence between two poisson distributions with the same `batch_shape` and `event_shape`.
|
|
|
|
The probability density function (pdf) is
|
|
|
|
.. math::
|
|
|
|
KL\_divergence(\lambda_1, \lambda_2) = \log(\det A_2) - \log(\det A_1) -\frac{n}{2} +\frac{1}{2}[tr [\Sigma_2^{-1} \Sigma_1] + (\mu_1 - \mu_2)^{\intercal} \Sigma_2^{-1} (\mu_1 - \mu_2)]
|
|
|
|
Args:
|
|
other (MultivariateNormal): instance of Multivariate Normal.
|
|
|
|
Returns:
|
|
Tensor, kl-divergence between two Multivariate Normal distributions. The data type is the same as `self.loc`.
|
|
|
|
"""
|
|
if (
|
|
self._batch_shape != other._batch_shape
|
|
and self._event_shape != other._event_shape
|
|
):
|
|
raise ValueError(
|
|
"KL divergence of two Multivariate Normal distributions should share the same `batch_shape` and `event_shape`."
|
|
)
|
|
half_log_det_1 = (
|
|
self._unbroadcasted_scale_tril.diagonal(axis1=-2, axis2=-1)
|
|
.log()
|
|
.sum(-1)
|
|
)
|
|
half_log_det_2 = (
|
|
other._unbroadcasted_scale_tril.diagonal(axis1=-2, axis2=-1)
|
|
.log()
|
|
.sum(-1)
|
|
)
|
|
new_perm = list(range(len(self._unbroadcasted_scale_tril.shape)))
|
|
new_perm[-1], new_perm[-2] = new_perm[-2], new_perm[-1]
|
|
cov_mat_1 = paddle.matmul(
|
|
self._unbroadcasted_scale_tril,
|
|
self._unbroadcasted_scale_tril.transpose(new_perm),
|
|
)
|
|
cov_mat_2 = paddle.matmul(
|
|
other._unbroadcasted_scale_tril,
|
|
other._unbroadcasted_scale_tril.transpose(new_perm),
|
|
)
|
|
expectation = (
|
|
paddle.linalg.solve(cov_mat_2, cov_mat_1)
|
|
.diagonal(axis1=-2, axis2=-1)
|
|
.sum(-1)
|
|
)
|
|
expectation += batch_mahalanobis(
|
|
other._unbroadcasted_scale_tril, self.loc - other.loc
|
|
)
|
|
return (
|
|
half_log_det_2
|
|
- half_log_det_1
|
|
+ 0.5 * (expectation - self._event_shape[0])
|
|
)
|
|
|
|
|
|
def precision_to_scale_tril(P: Tensor) -> Tensor:
|
|
"""Convert precision matrix to scale tril matrix
|
|
|
|
Args:
|
|
P (Tensor): input precision matrix
|
|
|
|
Returns:
|
|
Tensor: scale tril matrix
|
|
"""
|
|
Lf = paddle.linalg.cholesky(paddle.flip(P, (-2, -1)))
|
|
tmp = paddle.flip(Lf, (-2, -1))
|
|
new_perm = list(range(len(tmp.shape)))
|
|
new_perm[-2], new_perm[-1] = new_perm[-1], new_perm[-2]
|
|
L_inv = paddle.transpose(tmp, new_perm)
|
|
Id = paddle.eye(P.shape[-1], dtype=P.dtype)
|
|
L = paddle.linalg.triangular_solve(L_inv, Id, upper=False)
|
|
return L
|
|
|
|
|
|
def batch_mahalanobis(bL: Tensor, bx: Tensor) -> Tensor:
|
|
r"""
|
|
Computes the squared Mahalanobis distance of the Multivariate Normal distribution with cholesky decomposition of the covariance matrix.
|
|
Accepts batches for both bL and bx.
|
|
|
|
Args:
|
|
bL (Tensor): scale trial matrix (batched)
|
|
bx (Tensor): difference vector(batched)
|
|
|
|
Returns:
|
|
Tensor: squared Mahalanobis distance
|
|
"""
|
|
n = bx.shape[-1]
|
|
bx_batch_shape = bx.shape[:-1]
|
|
|
|
# Assume that bL.shape = (i, 1, n, n), bx.shape = (..., i, j, n),
|
|
# we are going to make bx have shape (..., 1, j, i, 1, n) to apply batched tri.solve
|
|
bx_batch_dims = len(bx_batch_shape)
|
|
bL_batch_dims = bL.dim() - 2
|
|
outer_batch_dims = bx_batch_dims - bL_batch_dims
|
|
old_batch_dims = outer_batch_dims + bL_batch_dims
|
|
new_batch_dims = outer_batch_dims + 2 * bL_batch_dims
|
|
# Reshape bx with the shape (..., 1, i, j, 1, n)
|
|
bx_new_shape = bx.shape[:outer_batch_dims]
|
|
for sL, sx in zip(bL.shape[:-2], bx.shape[outer_batch_dims:-1]):
|
|
bx_new_shape += (sx // sL, sL)
|
|
bx_new_shape += (n,)
|
|
bx = bx.reshape(bx_new_shape)
|
|
# Permute bx to make it have shape (..., 1, j, i, 1, n)
|
|
permute_dims = (
|
|
list(range(outer_batch_dims))
|
|
+ list(range(outer_batch_dims, new_batch_dims, 2))
|
|
+ list(range(outer_batch_dims + 1, new_batch_dims, 2))
|
|
+ [new_batch_dims]
|
|
)
|
|
bx = bx.transpose(permute_dims)
|
|
|
|
flat_L = bL.reshape((-1, n, n)) # shape = b x n x n
|
|
flat_x = bx.reshape((-1, flat_L.shape[0], n)) # shape = c x b x n
|
|
flat_x_swap = flat_x.transpose((1, 2, 0)) # shape = b x n x c
|
|
M_swap = (
|
|
paddle.linalg.triangular_solve(flat_L, flat_x_swap, upper=False)
|
|
.pow(2)
|
|
.sum(-2)
|
|
) # shape = b x c
|
|
M = M_swap.t() # shape = c x b
|
|
|
|
# Now we revert the above reshape and permute operators.
|
|
permuted_M = M.reshape(bx.shape[:-1]) # shape = (..., 1, j, i, 1)
|
|
permute_inv_dims = list(range(outer_batch_dims))
|
|
for i in range(bL_batch_dims):
|
|
permute_inv_dims += [outer_batch_dims + i, old_batch_dims + i]
|
|
reshaped_M = permuted_M.transpose(
|
|
permute_inv_dims
|
|
) # shape = (..., 1, i, j, 1)
|
|
return reshaped_M.reshape(bx_batch_shape)
|