Source code for mllm_shap.estimation.monte_carlo
"""Monte Carlo coalition sampling estimator.
@domain estimation
@see [[knowledge/architecture/estimation]]
@tested-by [[tests/estimation/test_monte_carlo.py]]
"""
from collections.abc import Iterator
from typing import TYPE_CHECKING
import numpy as np
from ..domain.coalition import CoalitionMask
from ..domain.players import PlayerSet
from ._shared import (
_SnapshotEstimator,
compute_mc_num_samples,
iter_minimal_masks,
present_to_mask_int,
random_present,
)
if TYPE_CHECKING:
pass
[docs]
class MonteCarloEstimator(_SnapshotEstimator):
"""Sample random coalitions and delegate attribution to an estimand plugin."""
@property
def name(self) -> str:
"""Return the registered estimator identifier."""
return "mc"
[docs]
def sample_masks(
self,
player_set: PlayerSet,
budget_fraction: float,
include_minimal_masks: bool,
seed: int,
) -> Iterator[CoalitionMask]:
"""Yield the grand and empty coalitions, then random ones, up to the budget."""
num_players = player_set.num_players
num_samples = compute_mc_num_samples(
num_players,
budget_fraction,
include_minimal_masks,
)
rng = np.random.default_rng(seed)
seen: set[int] = set()
generated = 0
# v(N) and v(empty) are needed by every estimand; they come first and
# count toward the budget.
for present in ((True,) * num_players, (False,) * num_players):
seen.add(present_to_mask_int(present))
generated += 1
yield CoalitionMask.from_sequence(present)
if include_minimal_masks:
for present in iter_minimal_masks(num_players):
mask_int = present_to_mask_int(present)
if mask_int in seen:
continue
seen.add(mask_int)
generated += 1
yield CoalitionMask.from_sequence(present)
if generated >= num_samples:
return
while generated < num_samples:
if len(seen) >= (1 << num_players):
return
present = random_present(rng, num_players)
mask_int = present_to_mask_int(present)
if mask_int in seen:
continue
seen.add(mask_int)
generated += 1
yield CoalitionMask.from_sequence(present)