Skip to content

Commit 820e5ee

Browse files
authored
Add attention rollout interpretability method (Abnar & Zuidema 2020) (#1158)
* Attention Rollout skeleton * attention_rollout.py done * tests/core/test_attention_rollout.py done * attention rollout integrated into example scripts * attention rollout docs * attention rollout docs/interpret/pyhealth.interpret.methods.attention_rollout.rst added * Stip trailing whitespace and rename example keys to rollout * attention rollout: doc and style changes * attention rollout: module docstring header
1 parent 17338b4 commit 820e5ee

12 files changed

Lines changed: 691 additions & 1 deletion

docs/api/interpret.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ New to interpretability in PyHealth? Check out these complete examples:
5757
- Train a ViT model on COVID-19 chest X-ray classification
5858
- Use CheferRelevance for gradient-weighted attention attribution
5959
- Visualize which image patches contribute to predictions
60+
6061
**LIME Example:**
6162

6263
- ``examples/lime_stagenet_mimic4.py`` - Demonstrates LIME (Local Interpretable Model-agnostic Explanations) for StageNet mortality prediction. Shows how to:
@@ -78,6 +79,7 @@ Attribution Methods
7879
interpret/pyhealth.interpret.methods.gim
7980
interpret/pyhealth.interpret.methods.basic_gradient
8081
interpret/pyhealth.interpret.methods.chefer
82+
interpret/pyhealth.interpret.methods.attention_rollout
8183
interpret/pyhealth.interpret.methods.deeplift
8284
interpret/pyhealth.interpret.methods.integrated_gradients
8385
interpret/pyhealth.interpret.methods.shap
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
pyhealth.interpret.methods.attention_rollout
2+
=============================================
3+
4+
Overview
5+
--------
6+
7+
Attention Rollout provides token-level relevance scores for Transformer models
8+
in PyHealth. It quantifies how attention propagates information across layers by
9+
composing the per-layer attention matrices (with a residual-connection
10+
correction), yielding a single importance score per input token (e.g. diagnosis
11+
codes, procedure codes, medications) for a given patient sample.
12+
13+
Unlike :class:`~pyhealth.interpret.methods.CheferRelevance`, which is
14+
gradient-weighted and **class-specific**, attention rollout is **forward-pass
15+
only**, **gradient-free**, and **class-agnostic**: it explains how information
16+
flows through the attention mechanism independent of any target class. It serves
17+
as the standard baseline that gradient-based attention methods are compared
18+
against, and complements Chefer rather than replacing it.
19+
20+
This method is particularly useful for:
21+
22+
- **Clinical decision support**: Understanding which medical codes drove a particular prediction
23+
- **Model debugging**: Identifying whether the model attends to clinically meaningful features
24+
- **Feature importance**: Ranking tokens by how much attention flows to them
25+
- **Trust and transparency**: Providing interpretable, class-agnostic explanations for model predictions
26+
27+
The implementation follows the paper by Abnar & Zuidema (2020): "Quantifying
28+
Attention Flow in Transformers" (https://arxiv.org/abs/2005.00928).
29+
30+
Key Features
31+
------------
32+
33+
- **Multi-modal support**: Works with multiple feature types (conditions, procedures, drugs, labs, etc.)
34+
- **Gradient-free**: Computed from a single forward pass; no backward pass is used in the attribution math
35+
- **Class-agnostic**: Independent of the predicted/target class (``target_class_idx`` is accepted but ignored)
36+
- **Layer-wise composition**: Composes per-layer attention as ``rollout = Â_L @ ... @ Â_1`` with the residual correction ``Â = 0.5 * (A + I)``
37+
- **Distribution over tokens**: Because each ``Â`` is row-stochastic, so is their product; per-token relevance sums to 1 (before the input-shape expansion)
38+
- **Model-agnostic by duck-typing**: Works with any model exposing the attention-readout methods ``set_attention_hooks``, ``get_attention_layers`` and ``get_relevance_tensor`` (currently :class:`~pyhealth.models.Transformer` and :class:`~pyhealth.models.StageAttentionNet`), not just one named model
39+
40+
Usage Notes
41+
-----------
42+
43+
1. **Batch size**: For interpretability, use ``batch_size=1`` to get per-sample explanations.
44+
2. **Do not wrap in** ``torch.no_grad()``: Although rollout is gradient-free in its math, the shared attention-readout plumbing registers a gradient hook on the attention tensors during the forward pass, so calling ``attribute(**batch)`` inside ``torch.no_grad()`` raises a ``RuntimeError``. Call it under the default (grad-enabled) context; no backward pass is performed.
45+
3. **Model compatibility**: Works with any model that exposes ``set_attention_hooks``, ``get_attention_layers`` and ``get_relevance_tensor`` — not restricted to the Transformer. Incompatible models raise ``TypeError`` at construction.
46+
4. **Class specification**: ``target_class_idx`` is accepted for API compatibility but ignored, since rollout is class-agnostic.
47+
48+
Quick Start
49+
-----------
50+
51+
.. code-block:: python
52+
53+
from pyhealth.models import Transformer
54+
from pyhealth.interpret.methods import AttentionRollout
55+
from pyhealth.datasets import get_dataloader
56+
57+
# Assume you have a trained transformer model and dataset
58+
model = Transformer(dataset=sample_dataset, ...)
59+
# ... train the model ...
60+
61+
# Create interpretability object
62+
rollout = AttentionRollout(model)
63+
64+
# Get a test sample (batch_size=1)
65+
test_loader = get_dataloader(test_dataset, batch_size=1, shuffle=False)
66+
batch = next(iter(test_loader))
67+
68+
# Compute attributions (target_class_idx is accepted but ignored)
69+
scores = rollout.attribute(**batch)
70+
71+
# Analyze results
72+
for feature_key, attribution in scores.items():
73+
print(f"{feature_key}: {attribution.shape}")
74+
top_tokens = attribution[0].topk(5).indices
75+
print(f" Top 5 most relevant tokens: {top_tokens}")
76+
77+
API Reference
78+
-------------
79+
80+
.. autoclass:: pyhealth.interpret.methods.AttentionRollout
81+
:members:
82+
:undoc-members:
83+
:show-inheritance:
84+
:member-order: bysource

docs/why_pyhealth.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Go beyond standard metrics with comprehensive model assessment:
175175

176176
- Gradient-based: Integrated Gradients, DeepLift, Saliency Maps, GIM
177177
- Perturbation-based: LIME, SHAP (with healthcare-optimized implementations)
178-
- Attention-based: Chefer relevance propagation for transformers
178+
- Attention-based: Chefer relevance propagation and attention rollout for transformers
179179
- Visualization tools for clinical decision support
180180

181181
**Uncertainty quantification:**

examples/interpretability/dka_stageattn_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def count_labels(ds):
135135
"deeplift": DeepLift(model, use_embeddings=True),
136136
"gim": GIM(model),
137137
"chefer": CheferRelevance(model),
138+
"rollout": AttentionRollout(model),
138139
"shap": ShapExplainer(model, use_embeddings=True),
139140
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
140141
}

examples/interpretability/dka_transformer_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def count_labels(ds):
135135
"deeplift": DeepLift(model, use_embeddings=True),
136136
"gim": GIM(model),
137137
"chefer": CheferRelevance(model),
138+
"rollout": AttentionRollout(model),
138139
"shap": ShapExplainer(model, use_embeddings=True),
139140
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
140141
}

examples/interpretability/los_stageattn_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def main():
121121
"deeplift": DeepLift(model, use_embeddings=True),
122122
"gim": GIM(model),
123123
"chefer": CheferRelevance(model),
124+
"rollout": AttentionRollout(model),
124125
"shap": ShapExplainer(model, use_embeddings=True),
125126
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
126127
}

examples/interpretability/los_transformer_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def main():
121121
"deeplift": DeepLift(model, use_embeddings=True),
122122
"gim": GIM(model),
123123
"chefer": CheferRelevance(model),
124+
"rollout": AttentionRollout(model),
124125
"shap": ShapExplainer(model, use_embeddings=True),
125126
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
126127
}

examples/interpretability/mp_stageattn_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def main():
121121
"deeplift": DeepLift(model, use_embeddings=True),
122122
"gim": GIM(model),
123123
"chefer": CheferRelevance(model),
124+
"rollout": AttentionRollout(model),
124125
"shap": ShapExplainer(model, use_embeddings=True),
125126
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
126127
}

examples/interpretability/mp_transformer_mimic4_interpret.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def main():
121121
"deeplift": DeepLift(model, use_embeddings=True),
122122
"gim": GIM(model),
123123
"chefer": CheferRelevance(model),
124+
"rollout": AttentionRollout(model),
124125
"shap": ShapExplainer(model, use_embeddings=True),
125126
"lime": LimeExplainer(model, use_embeddings=True, n_samples=200),
126127
}

pyhealth/interpret/methods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pyhealth.interpret.methods.base_interpreter import BaseInterpreter
22
from pyhealth.interpret.methods.baseline import RandomBaseline
33
from pyhealth.interpret.methods.chefer import CheferRelevance
4+
from pyhealth.interpret.methods.attention_rollout import AttentionRollout
45
from pyhealth.interpret.methods.basic_gradient import BasicGradientSaliencyMaps
56
from pyhealth.interpret.methods.deeplift import DeepLift
67
from pyhealth.interpret.methods.gim import GIM
@@ -15,6 +16,7 @@
1516
__all__ = [
1617
"BaseInterpreter",
1718
"CheferRelevance",
19+
"AttentionRollout",
1820
"DeepLift",
1921
"GIM",
2022
"IntegratedGradientGIM",

0 commit comments

Comments
 (0)