Rate this Page
★ ★ ★ ★ ★

Source code for torchrl.collectors._evaluation_stats

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""Bootstrap statistics for comparing evaluation results."""

from __future__ import annotations

from collections.abc import Callable
from typing import Literal

import torch
from tensordict import TensorDict


def _mean(values: torch.Tensor) -> torch.Tensor:
    return values.mean(-1)


def _median(values: torch.Tensor) -> torch.Tensor:
    return values.quantile(0.5, dim=-1)


def _interquartile_mean(values: torch.Tensor) -> torch.Tensor:
    num_samples = values.shape[-1]
    trimmed = int(0.25 * num_samples)
    ordered = values.sort(dim=-1).values
    return ordered[..., trimmed : num_samples - trimmed].mean(-1)


_STATISTICS: dict[str, Callable[[torch.Tensor], torch.Tensor]] = {
    "mean": _mean,
    "median": _median,
    "iqm": _interquartile_mean,
}


[docs] def bootstrap_estimate( values: torch.Tensor, dim: int = -1, *, statistic: Literal["mean", "median", "iqm"] = "iqm", confidence: float = 0.95, num_resamples: int = 1000, generator: torch.Generator | None = None, ) -> TensorDict: """Reduce ``values`` along ``dim`` to an estimate with a percentile bootstrap interval. Args: values (torch.Tensor): the samples, e.g. per-episode returns. dim (int, optional): the dimension holding the samples. Defaults to ``-1``. Keyword Args: statistic (str, optional): ``"mean"``, ``"median"`` or ``"iqm"`` (the mean of the samples left after dropping the lowest and highest 25%). Defaults to ``"iqm"``. confidence (float, optional): the coverage of the interval, in ``(0, 1)``. Defaults to ``0.95``. num_resamples (int, optional): the number of bootstrap resamples, each drawn with replacement. Defaults to ``1000``. generator (torch.Generator, optional): the generator used to draw resamples, for reproducible intervals. Defaults to ``None``. Returns: a :class:`~tensordict.TensorDict` with entries ``"estimate"``, ``"ci_low"`` and ``"ci_high"`` whose batch size is the shape of ``values`` without ``dim``. Examples: >>> import torch >>> from torchrl.collectors import bootstrap_estimate >>> returns = torch.tensor([[0.0, 0.0, 0.0, 100.0], [5.0, 5.0, 5.0, 5.0]]) >>> summary = bootstrap_estimate(returns, statistic="iqm") >>> summary["estimate"] tensor([0., 5.]) >>> summary["ci_low"][1], summary["ci_high"][1] (tensor(5.), tensor(5.)) """ if statistic not in _STATISTICS: raise ValueError( f"Unknown statistic {statistic!r}. Choose among {sorted(_STATISTICS)}." ) if not 0 < confidence < 1: raise ValueError(f"confidence must be in (0, 1), got {confidence}.") reduce = _STATISTICS[statistic] samples = values.movedim(dim, -1).to(torch.get_default_dtype()) num_samples = samples.shape[-1] indices = torch.randint( num_samples, (num_resamples, num_samples), generator=generator, device=generator.device if generator is not None else samples.device, ).to(samples.device) alpha = (1 - confidence) / 2 bounds = reduce(samples[..., indices]).quantile( torch.tensor([alpha, 1 - alpha], dtype=samples.dtype, device=samples.device), dim=-1, ) return TensorDict( estimate=reduce(samples), ci_low=bounds[0], ci_high=bounds[1], batch_size=samples.shape[:-1], )
[docs] def paired_comparison( first: torch.Tensor, second: torch.Tensor, dim: int = -1, *, statistic: Literal["mean", "median", "iqm"] = "mean", confidence: float = 0.95, num_resamples: int = 1000, generator: torch.Generator | None = None, ) -> TensorDict: """Compare two sets of paired samples, e.g. two policies evaluated on the same seeds. Sample ``i`` of ``first`` and ``second`` must come from the same conditions, as with :meth:`~torchrl.collectors.Evaluator.evaluate_episodes` called with the same ``seed``. Args: first (torch.Tensor): the samples of the first candidate. second (torch.Tensor): the samples of the second candidate, with the same shape as ``first``. dim (int, optional): the dimension holding the samples. Defaults to ``-1``. Keyword Args: statistic (str, optional): the statistic of the paired differences ``first - second``: ``"mean"``, ``"median"`` or ``"iqm"``. Defaults to ``"mean"``. confidence (float, optional): see :func:`bootstrap_estimate`. num_resamples (int, optional): see :func:`bootstrap_estimate`. generator (torch.Generator, optional): see :func:`bootstrap_estimate`. Returns: the output of :func:`bootstrap_estimate` on ``first - second``, plus ``"probability_of_improvement"``: the fraction of pairs where ``first`` is larger, ties counting as half. Examples: >>> import torch >>> from torchrl.collectors import paired_comparison >>> first = torch.tensor([3.0, 5.0, 4.0, 6.0]) >>> result = paired_comparison(first, first - 1) >>> result["estimate"], result["probability_of_improvement"] (tensor(1.), tensor(1.)) """ if first.shape != second.shape: raise ValueError( f"first and second must have the same shape, got {first.shape} and {second.shape}." ) result = bootstrap_estimate( first - second, dim, statistic=statistic, confidence=confidence, num_resamples=num_resamples, generator=generator, ) wins = (first > second).to(result["estimate"].dtype) ties = (first == second).to(result["estimate"].dtype) result["probability_of_improvement"] = (wins + ties / 2).mean(dim) return result