Coalition masking and absence policies¶
Business context: LLM attribution depends on how absent prompt pieces are represented. Deletion, masking placeholders, and neutral fillers change model behavior, audit reproducibility, and the cost of coalition sampling at scale.
Goal: Walk through every public masking API in nlp-shap 0.1.3 — partitioning, absence policies, shared views, packed-mask codecs, mask-space projection, and plugin resolution.
Prerequisites: nlp-shap 0.1.3+, Python 3.12.
Theory: See the masking theory page.
Expected output: Token players partitioned from a snapshot; delete / pad / neutral renders; stable mask hashes; MaskSpace projection; structural sharing across views; built-in and entry-point plugin resolution for all absence policies.
import numpy as np
from nlp_shap import CoalitionMask, ConversationSnapshot, ExplainConfig, Message, Role, Turn
from nlp_shap.masking import (
DeletePolicy,
MaskBuilder,
MaskCodec,
MaskSpace,
MaskedSnapshot,
NeutralPolicy,
PackedMask,
PadPolicy,
TokenPartitioner,
)
from nlp_shap.plugins import PluginGroup, PluginRegistry, register_builtin_plugins
Build a conversation snapshot¶
Business: Partitioning defines the unit of accountability — what legal and product teams treat as one explainable “player.”
Technical: TokenPartitioner splits each message into whitespace-delimited tokens and assigns stable player identifiers.
turn = Turn(
messages=(
Message(role=Role.USER, text="Who are you?"),
Message(role=Role.ASSISTANT, text="I am helpful assistant."),
)
)
snapshot = ConversationSnapshot.from_turns((turn,))
partitioner = TokenPartitioner()
players = partitioner.partition(snapshot)
mask = CoalitionMask.from_sequence((True, False, True, True, True, True, True))
print("partition:", partitioner.name)
print("snapshot_id:", snapshot.snapshot_id)
print("num_players:", players.num_players)
print("player_ids:", players.player_ids)
print("coalition mask:", mask.present)
mask.validate_against(players)
partition: tokens
snapshot_id: 1bbc3b933438f952
num_players: 7
player_ids: ('1bbc3b933438f952:0:0:0', '1bbc3b933438f952:0:0:1', '1bbc3b933438f952:0:0:2', '1bbc3b933438f952:0:1:0', '1bbc3b933438f952:0:1:1', '1bbc3b933438f952:0:1:2', '1bbc3b933438f952:0:1:3')
coalition mask: (True, False, True, True, True, True, True)
Compare absence policies¶
Business: The same coalition can produce different model inputs under delete, pad, or neutral semantics — a core variable in faithfulness studies and customer-facing explanations.
Technical: This coalition keeps every token except are; each policy renders that absence differently across both messages.
policies = {
"delete": DeletePolicy(),
"pad": PadPolicy(),
"neutral": NeutralPolicy(),
}
for name, policy in policies.items():
rendered = policy.apply(snapshot, players, mask)
user_text = rendered.turns[0].messages[0].text
assistant_text = rendered.turns[0].messages[1].text
print(f"{name}: user={user_text!r} assistant={assistant_text!r}")
delete: user='Who you?' assistant='I am helpful assistant.'
pad: user='Who [MASK] you?' assistant='I am helpful assistant.'
neutral: user='Who ___ you?' assistant='I am helpful assistant.'
Custom policy parameters¶
Business: Teams often standardize on a tokenizer-specific mask token or redaction style required by policy.
Technical: PadPolicy.placeholder and NeutralPolicy.fill_char configure those renderings.
custom_pad = PadPolicy(placeholder="<mask>")
custom_neutral = NeutralPolicy(fill_char="·")
print("pad:", custom_pad.apply(snapshot, players, mask).turns[0].messages[0].text)
print(
"neutral:",
custom_neutral.apply(snapshot, players, mask).turns[0].messages[0].text,
)
pad: Who <mask> you?
neutral: Who ··· you?
MaskedSnapshot views and MaskBuilder¶
Business: Large explain runs sample thousands of coalitions; structural sharing avoids copying full prompts and keeps infrastructure costs predictable.
Technical: MaskBuilder.view returns a lightweight MaskedSnapshot; render materializes only when needed. mask_hash supports deduplication keys.
builder = MaskBuilder(PadPolicy())
view: MaskedSnapshot = builder.view(snapshot, players, mask)
print("policy_name:", view.policy_name)
print("base_snapshot_id:", view.base_snapshot_id)
print("mask_hash:", view.mask_hash)
print("builder.policy_name:", builder.policy_name)
rendered_via_builder = builder.render(view)
rendered_direct = PadPolicy().apply(view.base, view.players, view.mask)
rendered_via_builder == rendered_direct
policy_name: pad
base_snapshot_id: 1bbc3b933438f952
mask_hash: -3148327582357758996
builder.policy_name: pad
True
MaskCodec — pack, unpack, and stable hash¶
Business: Stable coalition fingerprints let you deduplicate expensive model calls and prove two runs evaluated the same coalition.
Technical: MaskCodec packs boolean masks into little-endian bytes with shape-stable hashing.
packed: PackedMask = MaskCodec.pack(mask.present)
restored = MaskCodec.unpack(packed)
flat_hash = MaskCodec.hash_mask(mask.present)
row_hash = MaskCodec.hash_mask(np.asarray([mask.present], dtype=np.bool_))
print("packed bytes:", packed.words)
print("n_bits:", packed.n_bits)
print("roundtrip:", restored == mask.present)
print("hash stable across shapes:", flat_hash == row_hash)
print("matches view.mask_hash:", flat_hash == view.mask_hash)
packed bytes: b'}'
n_bits: 7
roundtrip: True
hash stable across shapes: True
matches view.mask_hash: True
MaskSpace — project feature splits to full masks¶
Business: System instructions and fixed boilerplate often stay present while only user content is explainable — matching how enterprises scope reviews.
Technical: MaskSpace.materialize maps a feature-level split back to a full-length mask with fixed positions always True.
space = MaskSpace(
explainable_mask=(False, True, True, False, True),
target_length=5,
)
feature_split = CoalitionMask.from_sequence((True, False, True))
full_mask = space.materialize(feature_split)
print("n_features:", space.n_features)
print("feature split:", feature_split.present)
print("full mask:", full_mask)
n_features: 3
feature split: (True, False, True)
full mask: (True, True, False, True, True)
Structural sharing across many coalitions¶
Business: Monte Carlo and exact explainers generate large mask batches; shared base snapshots keep memory flat as coalition count grows.
Technical: Many MaskedSnapshot views reference the same base object and snapshot_id.
import random
builder = MaskBuilder(DeletePolicy())
rng = random.Random(0)
views = []
for _ in range(100):
present = tuple(rng.choice((True, False)) for _ in range(players.num_players))
if not any(present):
present = (True,) + present[1:]
views.append(builder.view(snapshot, players, CoalitionMask.from_sequence(present)))
len({view.base_snapshot_id for view in views}), all(view.base is snapshot for view in views)
(1, True)
Plugin registry — built-ins, entry points, and ExplainConfig¶
Business: Operations teams select partition and absence behaviour through config (players, absence_policy) without code changes — the same keys CI and compliance review.
Technical: register_builtin_plugins and load_entry_points resolve tokens, delete, pad, and neutral.
registry = PluginRegistry()
register_builtin_plugins(registry)
registry.load_entry_points(PluginGroup.PARTITIONS)
registry.load_entry_points(PluginGroup.ABSENCE_POLICIES)
print("partitions:", registry.names(PluginGroup.PARTITIONS))
print("absence policies:", registry.names(PluginGroup.ABSENCE_POLICIES))
resolved = {
name: registry.resolve(PluginGroup.ABSENCE_POLICIES, name).name
for name in registry.names(PluginGroup.ABSENCE_POLICIES)
}
print("resolved absence policies:", resolved)
for absence_policy in ("delete", "pad", "neutral"):
config = ExplainConfig.model_validate(
{
"backend": {"kind": "mock", "model_id": "stub"},
"explanation": {
"players": "tokens",
"absence_policy": absence_policy,
},
}
)
partitioner = registry.resolve(
PluginGroup.PARTITIONS,
config.explanation.players,
)
policy = registry.resolve(
PluginGroup.ABSENCE_POLICIES,
config.explanation.absence_policy,
)
print(
absence_policy,
type(partitioner).__name__,
type(policy).__name__,
policy.name,
)
partitions: ('tokens',)
absence policies: ('delete', 'neutral', 'pad')
resolved absence policies: {'delete': 'delete', 'neutral': 'neutral', 'pad': 'pad'}
delete TokenPartitioner DeletePolicy delete
pad TokenPartitioner PadPolicy pad
neutral TokenPartitioner NeutralPolicy neutral
Validation guards¶
Business: Misaligned masks or empty coalitions can silently corrupt attribution studies; explicit guards surface failures before costly model calls.
Technical: CoalitionMask.validate_against, delete-all-tokens rejection, and builder/policy mismatch checks.
short_mask = CoalitionMask.from_sequence((True, False, True))
try:
short_mask.validate_against(players)
except ValueError as error:
print("mask length guard:", error)
empty_mask = CoalitionMask.from_sequence((False,) * players.num_players)
try:
DeletePolicy().apply(snapshot, players, empty_mask)
except ValueError as error:
print("delete empty coalition guard:", error)
mismatched_view = MaskedSnapshot(
base=snapshot,
players=players,
mask=mask,
policy_name="delete",
)
try:
MaskBuilder(PadPolicy()).render(mismatched_view)
except ValueError as error:
print("builder policy guard:", error)
mask length guard: coalition mask length 3 does not match player count 7
delete empty coalition guard: delete policy cannot remove every token
builder policy guard: masked view policy 'delete' does not match builder policy 'pad'