RynnValue-4B / value_tokenizer.py
huangsiteng's picture
Upload folder using huggingface_hub
5425ec6 verified
Raw
History Blame Contribute Delete
10.5 kB
# value_tokenizer.py
import torch
def to_symlog(x: torch.Tensor) -> torch.Tensor:
return torch.sign(x) * torch.log1p(torch.abs(x))
def to_symexp(x: torch.Tensor) -> torch.Tensor:
return torch.sign(x) * torch.expm1(torch.abs(x))
TRANSFORMS = {
"symlog": (to_symlog, to_symexp)
}
_INV_SQRT2 = 0.7071067811865476
def _normal_cdf(x: torch.Tensor) -> torch.Tensor:
"""Standard normal CDF Phi(x) via erf."""
return 0.5 * (1.0 + torch.erf(x * _INV_SQRT2))
class ValueTokenizer:
"""
Tokenizer for continuous scalar values using bin discretization.
Two orthogonal choices control the behaviour:
* ``support_transform`` — how a scalar maps to a fractional bin index:
- ``"linear"`` / ``"symlog"``: uniform bins in (transformed) support
space, ``idx = (transform(y) - min_sym) / stride`` over ``n_bins``
bin centers.
- ``"quantile"``: non-uniform bins whose centers are the midpoints of
data-driven ``bin_edges`` (empirical CDF). The scalar is mapped to a
fractional center index by piecewise-linear interpolation.
* ``encoding`` — how the fractional index becomes a target distribution:
- ``"two_hot"``: linear split over the two adjacent bin centers.
- ``"hl_gauss"``: Gaussian centered at the fractional index, integrated
over each bin's unit interval in center-index space (HL-Gauss,
Farebrother et al. 2024), then renormalized (tails absorbed).
All encodings share one internal "center-index space": ``n_bins`` centers at
integer positions ``0 .. n_bins-1``. This keeps ``two_hot`` numerically
identical to the legacy implementation for the linear/symlog modes.
"""
def __init__(
self,
bins: int = 256,
min_value: float = 0.0,
max_value: float = 1000.0,
forward_transform=None,
inverse_transform=None,
support_transform: str = "linear",
encoding: str = "two_hot",
hl_gauss_sigma_ratio: float = 0.75,
bin_edges=None,
device=None,
dtype=torch.float32,
) -> None:
self.n_bins = bins
self.min_val = float(min_value)
self.max_val = float(max_value)
self.support_transform = support_transform
self.encoding = encoding
# sigma expressed in center-index (== bin width) units.
self.hl_gauss_sigma = float(hl_gauss_sigma_ratio)
self.forward_transform = (
forward_transform if forward_transform is not None else self.identity
)
self.inverse_transform = (
inverse_transform if inverse_transform is not None else self.identity
)
self.device = device
self.dtype = dtype
self.is_quantile = support_transform == "quantile"
if self.is_quantile:
if bin_edges is None:
raise ValueError(
"support_transform='quantile' requires bin_edges of length bins+1."
)
edges = torch.as_tensor(bin_edges, dtype=dtype, device=device)
if edges.numel() != self.n_bins + 1:
raise ValueError(
f"bin_edges must have length bins+1 ({self.n_bins + 1}), "
f"got {edges.numel()}."
)
self.edges = edges
# Bin centers in value space: midpoint of each [edge_i, edge_{i+1}].
self.center_values = 0.5 * (edges[:-1] + edges[1:])
# Uniform-space attributes are unused in quantile mode.
self.min_sym = None
self.max_sym = None
self.centers_sym = None
self.bin_stride_sym = None
else:
self.edges = None
self.center_values = None
self.min_sym = self.forward_transform(
torch.tensor(self.min_val, dtype=dtype, device=device)
)
self.max_sym = self.forward_transform(
torch.tensor(self.max_val, dtype=dtype, device=device)
)
self.centers_sym = torch.linspace(
self.min_sym, self.max_sym, self.n_bins, dtype=dtype, device=device
)
if self.n_bins > 1:
self.bin_stride_sym = self.centers_sym[1] - self.centers_sym[0]
else:
self.bin_stride_sym = torch.tensor(1.0, dtype=dtype, device=device)
@classmethod
def from_config(cls, config, **kwargs):
forward_transform = None
inverse_transform = None
support_transform = config.support_transform
if support_transform in TRANSFORMS:
forward_transform, inverse_transform = TRANSFORMS[support_transform]
return cls(
bins=config.bins,
min_value=config.min_value,
max_value=config.max_value,
forward_transform=forward_transform,
inverse_transform=inverse_transform,
support_transform=support_transform,
encoding=getattr(config, "encoding", "two_hot"),
hl_gauss_sigma_ratio=getattr(config, "hl_gauss_sigma_ratio", 0.75),
bin_edges=getattr(config, "bin_edges", None),
**kwargs,
)
@staticmethod
def identity(x: torch.Tensor) -> torch.Tensor:
return x
def _to_tensor(self, x, device=None):
if isinstance(x, torch.Tensor):
return x.to(device=device if device is not None else x.device, dtype=self.dtype)
return torch.tensor(
x,
dtype=self.dtype,
device=device if device is not None else self.device,
)
def _value_to_idx(self, value: torch.Tensor) -> torch.Tensor:
"""Map scalar values to a fractional center index in ``[0, n_bins-1]``.
Args:
value: Tensor with shape (...,)
Returns:
Tensor with shape (...,), the fractional bin-center index.
"""
value = self._to_tensor(value)
device = value.device
if self.is_quantile:
centers = self.center_values.to(device)
v = torch.clamp(value, min=centers[0], max=centers[-1])
# First center strictly greater than v (after equal elements).
pos = torch.searchsorted(centers, v, right=True)
pos = pos.clamp(1, self.n_bins - 1)
c_left = centers[pos - 1]
c_right = centers[pos]
frac = (v - c_left) / (c_right - c_left).clamp_min(1e-12)
return (pos - 1).to(v.dtype) + frac
min_val = torch.tensor(self.min_val, dtype=self.dtype, device=device)
max_val = torch.tensor(self.max_val, dtype=self.dtype, device=device)
min_sym = self.min_sym.to(device)
bin_stride_sym = self.bin_stride_sym.to(device)
value = torch.clamp(value, min=min_val, max=max_val)
v_sym = self.forward_transform(value)
return (v_sym - min_sym) / bin_stride_sym
def encode(self, value: torch.Tensor) -> torch.Tensor:
"""Encode scalars into a target distribution over bins (dispatch)."""
if self.encoding == "hl_gauss":
return self.encode_hl_gauss(value)
return self.encode_two_hot(value)
def encode_two_hot(self, value: torch.Tensor) -> torch.Tensor:
"""
Convert scalar values into a two-hot distribution over bins.
Args:
value: Tensor with shape (...,)
Returns:
Tensor with shape (..., n_bins)
"""
value = self._to_tensor(value)
device = value.device
idx_float = self._value_to_idx(value)
idx_left = torch.floor(idx_float).long()
idx_right = idx_left + 1
weight_right = idx_float - idx_left.to(idx_float.dtype)
weight_left = 1.0 - weight_right
idx_left_clamped = idx_left.clamp(0, self.n_bins - 1)
idx_right_clamped = idx_right.clamp(0, self.n_bins - 1)
target_dist = torch.zeros(
*value.shape, self.n_bins, dtype=self.dtype, device=device
)
target_dist.scatter_add_(
dim=-1,
index=idx_left_clamped.unsqueeze(-1),
src=weight_left.unsqueeze(-1),
)
target_dist.scatter_add_(
dim=-1,
index=idx_right_clamped.unsqueeze(-1),
src=weight_right.unsqueeze(-1),
)
target_dist = target_dist / target_dist.sum(dim=-1, keepdim=True).clamp_min(1e-12)
return target_dist
def encode_hl_gauss(self, value: torch.Tensor) -> torch.Tensor:
"""
Convert scalar values into an HL-Gauss distribution over bins.
A Gaussian centered at the fractional bin-center index (std
``hl_gauss_sigma`` in center-index units) is integrated over each bin's
unit interval ``[i-0.5, i+0.5]``; the tails outside the support are
absorbed by renormalization.
Args:
value: Tensor with shape (...,)
Returns:
Tensor with shape (..., n_bins)
"""
value = self._to_tensor(value)
device = value.device
idx_float = self._value_to_idx(value)
sigma = max(self.hl_gauss_sigma, 1e-6)
centers = torch.arange(self.n_bins, device=device, dtype=idx_float.dtype)
c = idx_float.unsqueeze(-1)
cdf_upper = _normal_cdf((centers + 0.5 - c) / sigma)
cdf_lower = _normal_cdf((centers - 0.5 - c) / sigma)
probs = cdf_upper - cdf_lower
probs = probs / probs.sum(dim=-1, keepdim=True).clamp_min(1e-12)
return probs.to(self.dtype)
def decode_from_bins(self, bin_logits: torch.Tensor) -> torch.Tensor:
"""
Decode scalar predictions from bin logits.
Args:
bin_logits: Tensor with shape (..., n_bins)
Returns:
Tensor with shape (...,)
"""
bin_logits = bin_logits.float()
if bin_logits.shape[-1] != self.n_bins:
raise ValueError(
f"Expected bin_logits last dim == n_bins ({self.n_bins}), got {bin_logits.shape[-1]}"
)
probs = torch.softmax(bin_logits, dim=-1)
if self.is_quantile:
centers = self.center_values.to(device=bin_logits.device, dtype=bin_logits.dtype)
return torch.sum(probs * centers, dim=-1)
centers_sym = self.centers_sym.to(device=bin_logits.device, dtype=bin_logits.dtype)
pred_value_sym = torch.sum(probs * centers_sym, dim=-1)
return self.inverse_transform(pred_value_sym)