Files
2026-07-13 12:40:42 +08:00

154 lines
5.2 KiB
Python

# Copyright (c) 2026 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 numpy as np
import paddle
from paddle.distribution import constraint, distribution
from paddle.tensor import multinomial
class Categorical(distribution.Distribution):
arg_constraints = {
"probs": constraint.simplex,
"logits": constraint.real_vector,
}
has_enumerate_support = True
def __init__(
self,
probs=None,
logits=None,
validate_args: bool | None = None,
) -> None:
if (probs is None) == (logits is None):
raise ValueError(
"Either `probs` or `logits` must be specified, but not both."
)
if probs is not None:
if probs.dim() < 1:
raise ValueError(
"`probs` parameter must be at least one-dimensional."
)
self._probs = probs / probs.sum(-1, keepdim=True)
self._logits = None
self._param = self._probs
else:
if logits.dim() < 1:
raise ValueError(
"`logits` parameter must be at least one-dimensional."
)
self._logits = logits - paddle.logsumexp(
logits, axis=-1, keepdim=True
)
self._probs = None
self._param = self._logits
self._num_events = self._param.shape[-1]
batch_shape = (
tuple(self._param.shape[:-1]) if self._param.dim() > 1 else ()
)
super().__init__(batch_shape, validate_args=validate_args)
def expand(self, batch_shape, _instance=None):
new = (
self.__class__.__new__(self.__class__)
if _instance is None
else _instance
)
batch_shape = tuple(batch_shape)
param_shape = (*batch_shape, self._num_events)
new._probs = (
self._probs.expand(param_shape) if self._probs is not None else None
)
new._logits = (
self._logits.expand(param_shape)
if self._logits is not None
else None
)
new._param = new._logits if new._logits is not None else new._probs
new._num_events = self._num_events
super(Categorical, new).__init__(batch_shape, validate_args=False)
new._validate_args_enabled = self._validate_args_enabled
return new
@property
def support(self):
return constraint.integer_interval(0, self._num_events - 1)
@property
def logits(self):
if self._logits is None:
eps = paddle.finfo(self.probs.dtype).eps
probs = paddle.clip(self.probs, min=eps, max=1 - eps)
self._logits = paddle.log(probs)
return self._logits
@property
def probs(self):
if self._probs is None:
self._probs = paddle.nn.functional.softmax(self.logits, axis=-1)
return self._probs
@property
def param_shape(self):
return self._param.shape
@property
def mean(self):
return paddle.full_like(self.probs[..., 0], float('nan'))
@property
def mode(self):
return paddle.argmax(self.probs, axis=-1)
@property
def variance(self):
return paddle.full_like(self.probs[..., 0], float('nan'))
def sample(self, sample_shape=()):
sample_shape = tuple(sample_shape)
probs_2d = self.probs.reshape([-1, self._num_events])
samples_2d = multinomial(probs_2d, int(np.prod(sample_shape)), True).T
return samples_2d.reshape(self._extend_shape(sample_shape))
def log_prob(self, value):
if self._validate_args_enabled and paddle.in_dynamic_mode():
self._validate_sample(value)
value = paddle.cast(value, dtype='int64').unsqueeze(-1)
log_pmf = self.logits
output_shape = paddle.broadcast_shape(value.shape, log_pmf.shape)
value = paddle.broadcast_to(value, [*output_shape[:-1], 1])
log_pmf = paddle.broadcast_to(log_pmf, output_shape)
return paddle.take_along_axis(log_pmf, value, axis=-1).squeeze(-1)
def entropy(self):
min_real = paddle.finfo(self.logits.dtype).min
logits = paddle.clip(self.logits, min=min_real)
p_log_p = logits * self.probs
return -p_log_p.sum(-1)
def enumerate_support(self, expand=True):
values = paddle.arange(self._num_events, dtype='int64')
values = values.reshape(
[self._num_events] + [1] * len(self._batch_shape)
)
if expand:
values = paddle.broadcast_to(
values, [self._num_events, *self._batch_shape]
)
return values