Source code for embodichain.learning.models.normalizer
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# 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.
# ----------------------------------------------------------------------------
"""Running feature normalization shared by learning models."""
from __future__ import annotations
import torch
from torch import nn
__all__ = ["EmpiricalNormalizer"]
[docs]
class EmpiricalNormalizer(nn.Module):
"""Normalize observations with online mean and variance estimates.
Statistics are stored as module buffers so checkpoints preserve the
exact transformation used during training and evaluation.
Args:
feature_dim: Number of features in the final observation dimension.
epsilon: Stabilizer added to the running standard deviation.
"""
[docs]
def __init__(self, feature_dim: int, epsilon: float = 1e-2) -> None:
super().__init__()
if feature_dim <= 0:
raise ValueError("feature_dim must be positive")
if epsilon <= 0.0:
raise ValueError("epsilon must be positive")
self.epsilon = float(epsilon)
self.register_buffer("mean", torch.zeros(1, feature_dim))
self.register_buffer("variance", torch.ones(1, feature_dim))
self.register_buffer("standard_deviation", torch.ones(1, feature_dim))
self.register_buffer("count", torch.zeros((), dtype=torch.long))
[docs]
def forward(self, value: torch.Tensor) -> torch.Tensor:
"""Return observations normalized by the current running moments."""
return (value - self.mean) / (self.standard_deviation + self.epsilon)
[docs]
@torch.no_grad()
def update(self, value: torch.Tensor) -> None:
"""Merge one observation batch into the running moments."""
if not self.training:
return
if value.ndim != 2 or value.shape[-1] != self.mean.shape[-1]:
raise ValueError(
"normalizer input must have shape [batch, feature_dim], got "
f"{tuple(value.shape)}"
)
batch_count = int(value.shape[0])
if batch_count == 0:
return
batch_mean = value.mean(dim=0, keepdim=True)
batch_variance = value.var(dim=0, unbiased=False, keepdim=True)
self.count.add_(batch_count)
rate = batch_count / self.count
delta = batch_mean - self.mean
self.mean.add_(rate * delta)
self.variance.add_(
rate * (batch_variance - self.variance + delta * (batch_mean - self.mean))
)
self.variance.clamp_min_(0.0)
self.standard_deviation.copy_(torch.sqrt(self.variance))