map_node_attributions ¶
map_node_attributions(
attributions: Tensor | Sequence[Tensor],
spec: LayeredSpec | AdjacencySpec,
layer: int | None = None,
*,
hop_input: Hop | None = None,
hop_output: Hop | None = None,
axis: str | None = None,
dims: Sequence[str] | None = None,
coords: Mapping[str, Sequence] | None = None
) -> xr.DataArray
Label an attribution tensor's node axis with names from a spec.
Attribution methods return unlabeled tensors whose node axis is
bare positions; the spec knows the name at each one. Reach for
it after Captum or your own gradients rather than zipping names
to columns yourself. Which spec you pass decides the contract: a
LayeredSpec needs exactly one of layer, hop_input,
hop_output, or axis="inputs"; an AdjacencySpec
forbids the first three. Omit axis there to name the
whole state vector, or pass axis="inputs" to name the
input units. The width is never inferred.
Values are detached onto CPU and never aggregated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
torch.Tensor or sequence of torch.Tensor
|
Scores exactly as the attribution method produced them, in
whatever units it works in; nothing is scaled, summed, or
made absolute. The node axis must be as long as the named
units — |
required |
|
LayeredSpec or AdjacencySpec
|
Parsed edgelist supplying the |
required |
|
int
|
0-based depth into |
None
|
|
Hop
|
A |
None
|
|
Hop
|
A |
None
|
|
inputs
|
|
"inputs"
|
|
sequence of str
|
One name per axis of the tensor after any stacking,
containing |
None
|
|
mapping of str to sequence
|
Labels for axes other than |
None
|
Returns:
| Type | Description |
|---|---|
DataArray
|
The values and shape of the (stacked) tensor, carrying the
spec's node names as the |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
See Also
aggregate_node_attributions : Folds named scores to one
value per node; call this after mapping.
align_inputs : Shared input_nodes order on the way
in; a column index into the caller's feature names,
not the full state axis of an AdjacencySpec.
gather_hop_inputs : Builds the tensor whose columns
hop_input names.
Hop : One hop; pass hop_output=spec.hops[i] or
hop_input=spec.hops[i] to name either side of it.
LayeredSpec : Holds layer_nodes and hops, the names
used when layer, hop_input, or hop_output is
given.
AdjacencySpec : Holds nodes, the state-vector names used
when all three are omitted.
Notes
Captum is not imported anywhere in this package; mapping is name alignment only, and any attribution method will do.
Name a hop module's scores with the same index i you use
for the module. For what PackedLinear (or MaskedLinear)
on spec.hops[i] returns, which is what Captum layer methods
such as LayerConductance score by default, pass
hop_output=spec.hops[i]. For what it reads (the
gather_hop_inputs tensor, or Captum with
attribute_to_layer_input=True), pass
hop_input=spec.hops[i]. The side is always stated, never
inferred: a hop's input and output often have the same width,
so a width check alone cannot catch the wrong side. Only map
units that are spec nodes; BatchNorm and other unnamed modules
have no node axis to name.
DeepLift, DeepLiftShap, and LRP rescale nn.Module
nonlinearities. The activation after a hop is an
nn.ReLU held in an nn.ModuleList, one entry per
hop, and called from forward. The hop module returns
the pre-activation tensor: hop_output names that
linear map. Post-activation node states are the output
of the nn.ReLU that follows the hop.
LayerActivation and LayerGradientXActivation
hook that module.
A recurrent net on an AdjacencySpec has no layer to index,
and the natural extra axis there is step: pass one tensor
per unrolled step as a sequence and they are stacked for you.
Examples:
Name a two-observation tensor at the output layer of a
LayeredSpec:
>>> import pandas as pd
>>> import torch
>>> import kpnn2
>>> edgelist = pd.DataFrame(
... {
... "source": ["A", "H"],
... "target": ["H", "C"],
... }
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> spec.layer_nodes
(('A',), ('H',), ('C',))
>>> scores = torch.tensor([[0.5], [1.0]])
>>> da = kpnn2.map_node_attributions(
... attributions=scores,
... spec=spec,
... layer=2,
... )
>>> da["node"].values.tolist()
['C']
>>> da.sel(node="C").values.tolist()
[0.5, 1.0]
>>> int(da.coords["layer"])
2
Both sides of a hop can have the same width. Here hops[0]
reads A, B and writes H1, H2, so one
(1, 2) tensor fits either side; the keyword decides the
names:
>>> square = pd.DataFrame(
... {
... "source": ["A", "B", "A", "B", "H1", "H2"],
... "target": ["H1", "H1", "H2", "H2", "C", "C"],
... }
... )
>>> square_spec = kpnn2.parse_layered(square)
>>> hop = square_spec.hops[0]
>>> hop.in_features, hop.out_features
(2, 2)
>>> hop_scores = torch.tensor([[0.1, 0.2]])
>>> out_da = kpnn2.map_node_attributions(
... attributions=hop_scores,
... spec=square_spec,
... hop_output=hop,
... )
>>> out_da["node"].values.tolist()
['H1', 'H2']
>>> int(out_da.coords["layer"])
1
>>> in_da = kpnn2.map_node_attributions(
... attributions=hop_scores,
... spec=square_spec,
... hop_input=hop,
... )
>>> in_da["node"].values.tolist()
['A', 'B']
>>> "layer" in in_da.coords
False
A skip hop reads several layers, so its input axis is those
layers concatenated (width hop.in_features):
>>> skip_edges = pd.DataFrame(
... {
... "source": ["A", "H", "A"],
... "target": ["H", "C", "C"],
... }
... )
>>> skip_spec = kpnn2.parse_layered(skip_edges)
>>> skip_hop = skip_spec.hops[1]
>>> skip_hop.source_layers
(0, 1)
>>> skip_da = kpnn2.map_node_attributions(
... attributions=torch.tensor([[0.1, 0.2]]),
... spec=skip_spec,
... hop_input=skip_hop,
... )
>>> skip_da["node"].values.tolist()
['A', 'H']
On an AdjacencySpec there are no layers: omit layer,
hop_input, and hop_output and the whole state vector
is named. One tensor per unrolled step stacks onto a step
axis:
>>> cyclic = pd.DataFrame(
... {
... "source": ["x", "a", "b", "a"],
... "target": ["a", "b", "a", "y"],
... }
... )
>>> state_spec = kpnn2.parse_adjacency(cyclic)
>>> per_step = kpnn2.map_node_attributions(
... attributions=[
... torch.zeros(2, 4),
... torch.ones(2, 4),
... ],
... spec=state_spec,
... )
>>> per_step.dims
('step', 'observation', 'node')
>>> per_step["node"].values.tolist()
['a', 'b', 'x', 'y']
>>> "layer" in per_step.coords
False
>>> input_scores = kpnn2.map_node_attributions(
... attributions=torch.tensor([[0.5], [1.0]]),
... spec=state_spec,
... axis="inputs",
... )
>>> input_scores["node"].values.tolist()
['x']
>>> "layer" in input_scores.coords
False