CBM Concept Intervention Demo¶
This notebook demonstrates concept intervention on test cases, replicating demo_intervention.py.
Key Concept: When the model makes wrong concept predictions, we can correct them manually and see how it changes the final diagnosis. This is the main advantage of Concept Bottleneck Models!
Test Cases:
- Case 7: Non-Melanoma (BCC, 7-point score: 0)
- Case 596: Melanoma in situ (7-point score: 3)
- Case 578: Melanoma in situ (7-point score: 7)
- Case 657: Melanoma <0.76mm (7-point score: 8)
1. Setup and Imports¶
In [1]:
import os
import sys
import torch
import numpy as np
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
# Add parent directory to path
sys.path.insert(0, '..')
from src.models.basic_cbm import ConceptBottleneckModel
from src.utils.visualization import plot_intervention_analysis
print("✓ Imports successful")
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
/home/csc29/.conda/envs/CBM-env/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
✓ Imports successful PyTorch version: 2.5.1 CUDA available: True
2. Load Trained Model¶
In [2]:
# Load trained model
model_path = '../trained_models/derm7pt_best/best_model.pth'
if not os.path.exists(model_path):
print(f"Error: Model not found at {model_path}")
else:
print(f"Loading model from: {model_path}")
model = ConceptBottleneckModel.load(model_path)
model.eval()
print("✓ Model loaded successfully")
Loading model from: ../trained_models/derm7pt_best/best_model.pth
/home/csc29/projects/SynergyCBM/SkinCBM/notebooks/../src/models/basic_cbm.py:324: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature. checkpoint = torch.load(path, map_location=device)
✓ Model loaded successfully
3. Define Test Cases and Helper Functions¶
In [3]:
# Test cases with ground truth concepts
cases = [
{'case_num': 7, 'image': 'sample_data_derm7pt/case_007_dermoscopic.jpg', 'diagnosis': 'Non-Melanoma'},
{'case_num': 596, 'image': 'sample_data_derm7pt/case_596_dermoscopic.jpg', 'diagnosis': 'Melanoma'},
{'case_num': 578, 'image': 'sample_data_derm7pt/case_578_dermoscopic.jpg', 'diagnosis': 'Melanoma'},
{'case_num': 657, 'image': 'sample_data_derm7pt/case_657_dermoscopic.jpg', 'diagnosis': 'Melanoma'}
]
# Ground truth concepts (from test set)
gt_concepts = {
7: [0, 0, 1, 0, 0, 0, 0], # Case 7: BCC, 7pt=0
596: [2, 0, 0, 0, 1, 2, 0], # Case 596: Melanoma in situ, 7pt=3
578: [2, 1, 0, 2, 2, 2, 0], # Case 578: Melanoma in situ, 7pt=7
657: [2, 1, 0, 2, 2, 2, 1] # Case 657: Melanoma < 0.76mm, 7pt=8
}
concept_names = [
"Pigment Network", "Blue-Whitish Veil", "Vascular Structures",
"Streaks", "Pigmentation", "Dots & Globules", "Regression"
]
class_labels = ['Absent', 'Regular', 'Irregular']
def preprocess_image(image_path):
"""Load and preprocess image."""
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
image = Image.open(image_path).convert('RGB')
image_tensor = transform(image).unsqueeze(0)
viz_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
image_viz = viz_transform(Image.open(image_path).convert('RGB'))
image_viz = image_viz.permute(1, 2, 0).numpy()
return image_tensor, image_viz
def intervene_on_concept(model, image_tensor, concept_idx, new_class):
"""Perform intervention on a specific concept."""
with torch.no_grad():
original_concepts, original_logits = model(image_tensor)
concepts_reshaped = original_concepts.reshape(1, 7, 3)
intervened_concepts = concepts_reshaped.clone()
intervened_concepts[0, concept_idx, :] = 0.0
intervened_concepts[0, concept_idx, new_class] = 1.0
intervened_concepts_flat = intervened_concepts.reshape(1, -1)
intervened_logits = model.task_predictor(intervened_concepts_flat)
original_prob = torch.softmax(original_logits, dim=1)[0, 1].item()
intervened_prob = torch.softmax(intervened_logits, dim=1)[0, 1].item()
original_classes = concepts_reshaped.argmax(dim=2).squeeze().cpu().numpy()
return original_classes, original_prob, intervened_prob
print(f"✓ Loaded {len(cases)} test cases")
print(f"✓ Helper functions defined")
✓ Loaded 4 test cases ✓ Helper functions defined
4. Run Intervention Analysis on All Cases¶
For each case, we:
- Get original predictions
- Test individual interventions (correct one wrong concept at a time to ground truth)
- Test cumulative intervention (correct all wrong concepts simultaneously)
- Create visualization showing impact of each intervention
In [4]:
# Create output directory
output_dir = 'outputs'
os.makedirs(output_dir, exist_ok=True)
print("="*70)
print("Concept Intervention Analysis")
print("="*70)
# Process each case
for case in cases:
case_num = case['case_num']
image_path = case['image']
ground_truth = case['diagnosis']
print(f"\n{'='*70}")
print(f"Case {case_num}: {ground_truth}")
print(f"{'='*70}")
if not os.path.exists(image_path):
print(f"Warning: Image not found: {image_path}")
continue
# Load image
image_tensor, image_viz = preprocess_image(image_path)
# Get original prediction
with torch.no_grad():
original_concepts_tensor, original_logits = model(image_tensor)
original_concepts = original_concepts_tensor.reshape(1, 7, 3).argmax(dim=2).squeeze().cpu().numpy()
original_prob = torch.softmax(original_logits, dim=1)[0, 1].item()
print(f"Original prediction: {original_prob:.1%} melanoma")
# Get ground truth for this case
case_gt_concepts = gt_concepts.get(case_num, [0]*7)
# Find wrong concepts
interventions_to_test = []
for idx, (pred_class, gt_class, name) in enumerate(zip(original_concepts, case_gt_concepts, concept_names)):
if pred_class != gt_class:
interventions_to_test.append({
'idx': idx,
'name': name,
'new_class': gt_class
})
print(f"Found {len(interventions_to_test)} wrong concept(s)")
# Individual interventions
interventions_data = []
for interv in interventions_to_test:
concept_idx = interv['idx']
concept_name = interv['name']
new_class = interv['new_class']
orig_classes, orig_prob, interv_prob = intervene_on_concept(
model, image_tensor, concept_idx, new_class
)
original_class = int(orig_classes[concept_idx])
change = interv_prob - orig_prob
crosses = (orig_prob < 0.5) != (interv_prob < 0.5)
marker = "🔥" if crosses else ""
interventions_data.append({
'concept_name': concept_name,
'concept_idx': concept_idx,
'original_class': original_class,
'new_class': new_class,
'original_prob': orig_prob,
'intervened_prob': interv_prob,
'has_change': True
})
print(f" {marker} {concept_name:.<25} {class_labels[original_class]:>10} → {class_labels[new_class]:<10} (GT) "
f"| {orig_prob:.1%} → {interv_prob:.1%} ({change:+.1%})")
# Cumulative intervention (all wrong concepts)
if len(interventions_to_test) > 0:
print(f"\n Correcting ALL {len(interventions_to_test)} wrong concepts simultaneously:")
with torch.no_grad():
original_concepts_tensor, original_logits = model(image_tensor)
concepts_reshaped = original_concepts_tensor.reshape(1, 7, 3)
intervened_concepts = concepts_reshaped.clone()
for interv in interventions_to_test:
concept_idx = interv['idx']
new_class = interv['new_class']
intervened_concepts[0, concept_idx, :] = 0.0
intervened_concepts[0, concept_idx, new_class] = 1.0
intervened_concepts_flat = intervened_concepts.reshape(1, -1)
intervened_logits = model.task_predictor(intervened_concepts_flat)
cumulative_prob = torch.softmax(intervened_logits, dim=1)[0, 1].item()
change = cumulative_prob - original_prob
crosses = (original_prob < 0.5) != (cumulative_prob < 0.5)
marker = "🔥" if crosses else ""
print(f" {marker} ALL CONCEPTS CORRECTED........ "
f"| {original_prob:.1%} → {cumulative_prob:.1%} ({change:+.1%})")
# Add cumulative intervention to data
interventions_data.append({
'concept_name': f'ALL {len(interventions_to_test)} CORRECTED',
'concept_idx': -1,
'original_class': -1,
'new_class': -1,
'original_prob': original_prob,
'intervened_prob': cumulative_prob,
'has_change': True
})
# Create visualization
fig = plot_intervention_analysis(
image=image_viz,
case_num=case_num,
ground_truth=ground_truth,
interventions_data=interventions_data,
concept_names=concept_names,
original_prob=original_prob,
original_concepts=original_concepts
)
if fig is not None:
output_path = os.path.join(output_dir, f'intervention_case_{case_num}.png')
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
plt.close(fig)
print(f"\n✓ Saved: {output_path}")
print("\n" + "="*70)
print("✅ All intervention analyses complete!")
print("="*70)
====================================================================== Concept Intervention Analysis ====================================================================== ====================================================================== Case 7: Non-Melanoma ====================================================================== Original prediction: 37.8% melanoma Found 2 wrong concept(s) Vascular Structures...... Absent → Regular (GT) | 37.8% → 37.6% (-0.2%) 🔥 Dots & Globules.......... Irregular → Absent (GT) | 37.8% → 54.2% (+16.4%) Correcting ALL 2 wrong concepts simultaneously: 🔥 ALL CONCEPTS CORRECTED........ | 37.8% → 54.0% (+16.2%)
/tmp/ipykernel_2958063/2228877741.py:130: UserWarning: Glyph 11088 (\N{WHITE MEDIUM STAR}) missing from font(s) DejaVu Sans.
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
✓ Saved: outputs/intervention_case_7.png ====================================================================== Case 596: Melanoma ====================================================================== Original prediction: 37.8% melanoma Found 2 wrong concept(s) 🔥 Pigment Network.......... Absent → Irregular (GT) | 37.8% → 52.2% (+14.4%) Pigmentation............. Absent → Regular (GT) | 37.8% → 36.1% (-1.7%) Correcting ALL 2 wrong concepts simultaneously: 🔥 ALL CONCEPTS CORRECTED........ | 37.8% → 50.4% (+12.6%)
/tmp/ipykernel_2958063/2228877741.py:130: UserWarning: Glyph 11088 (\N{WHITE MEDIUM STAR}) missing from font(s) DejaVu Sans.
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
✓ Saved: outputs/intervention_case_596.png ====================================================================== Case 578: Melanoma ====================================================================== Original prediction: 34.5% melanoma Found 3 wrong concept(s) Pigment Network.......... Absent → Irregular (GT) | 34.5% → 48.7% (+14.2%) Blue-Whitish Veil........ Absent → Regular (GT) | 34.5% → 43.9% (+9.4%) Streaks.................. Absent → Irregular (GT) | 34.5% → 44.4% (+9.9%) Correcting ALL 3 wrong concepts simultaneously: 🔥 ALL CONCEPTS CORRECTED........ | 34.5% → 68.1% (+33.6%)
/tmp/ipykernel_2958063/2228877741.py:130: UserWarning: Glyph 11088 (\N{WHITE MEDIUM STAR}) missing from font(s) DejaVu Sans.
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
✓ Saved: outputs/intervention_case_578.png ====================================================================== Case 657: Melanoma ====================================================================== Original prediction: 34.5% melanoma Found 4 wrong concept(s) Pigment Network.......... Absent → Irregular (GT) | 34.5% → 48.7% (+14.2%) Blue-Whitish Veil........ Absent → Regular (GT) | 34.5% → 43.9% (+9.4%) Streaks.................. Absent → Irregular (GT) | 34.5% → 44.4% (+9.9%) Regression............... Absent → Regular (GT) | 34.5% → 31.0% (-3.5%) Correcting ALL 4 wrong concepts simultaneously: 🔥 ALL CONCEPTS CORRECTED........ | 34.5% → 64.5% (+30.0%)
/tmp/ipykernel_2958063/2228877741.py:130: UserWarning: Glyph 11088 (\N{WHITE MEDIUM STAR}) missing from font(s) DejaVu Sans.
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
✓ Saved: outputs/intervention_case_657.png ====================================================================== ✅ All intervention analyses complete! ======================================================================
5. Display Intervention Visualizations¶
View all generated intervention analysis plots.
In [5]:
# Display all intervention visualizations
fig, axes = plt.subplots(2, 2, figsize=(20, 16))
axes = axes.flatten()
for idx, case in enumerate(cases):
output_path = os.path.join(output_dir, f'intervention_case_{case["case_num"]}.png')
if os.path.exists(output_path):
img = plt.imread(output_path)
axes[idx].imshow(img)
axes[idx].axis('off')
axes[idx].set_title(f'Case {case["case_num"]}: {case["diagnosis"]}', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
print("✓ Intervention visualizations displayed")
✓ Intervention visualizations displayed
6. Understanding the Results¶
What the visualizations show:
- Original Image: Dermoscopic photograph of the lesion
- Original Prediction: Model's initial diagnosis probability
- Intervention Impact Chart: Bar chart showing how correcting each wrong concept changes melanoma probability
- Sorted by absolute impact (most influential interventions first)
- Green bars = interventions that change the diagnosis classification
- Includes "ALL CORRECTED" showing cumulative effect
- Diagnosis Confidence Panel: Shows the best individual intervention's impact
Key Observations:
- Individual interventions correct one concept at a time to ground truth
- Cumulative intervention corrects all wrong concepts simultaneously
- Some concepts have large impact on diagnosis (move probability significantly)
- Some concepts have minimal impact (probability barely changes)
- When intervention crosses 0.5 threshold, it changes the diagnosis (Non-Melanoma ↔ Melanoma)
Why this matters:
- Clinician collaboration: Doctors can correct wrong concepts they disagree with
- Trust and transparency: See exactly how each feature affects the diagnosis
- Model debugging: Identify which concepts the model relies on most
- Concept quality assessment: Concepts with large intervention impact are more important
7. Summary¶
What we demonstrated:
- ✅ Concept Intervention: Correcting wrong concepts to ground truth values
- ✅ Individual Interventions: One concept at a time (shows per-concept impact)
- ✅ Cumulative Intervention: All wrong concepts corrected simultaneously
- ✅ Impact Quantification: Exact change in melanoma probability for each intervention
- ✅ Enhanced Visualizations: Publication-quality figures showing intervention effects
Key Findings:
- Different concepts have different levels of influence on the final diagnosis
- Some interventions can flip the diagnosis (cross 0.5 threshold) 🔥
- Cumulative intervention shows combined effect of correcting all concepts
- Intervention reveals which concepts the model relies on most
Comparison with Prediction Demo:
- Prediction demo (
02_demo_with_sample_data.ipynb): Shows what the model predicts - Intervention demo (this notebook): Shows how predictions change when concepts are corrected
Next Steps:
- Full test set analysis: Run
examples/intervention_analysis.pyfor comprehensive metrics - Quantitative evaluation: See
outputs/intervention_analysis/*.csvfor statistical summaries - Research paper: Use these visualizations and metrics to demonstrate CBM interpretability
In [6]:
print("✅ Intervention demo complete!")
print("\nGenerated files:")
for case in cases:
output_path = f"outputs/intervention_case_{case['case_num']}.png"
if os.path.exists(output_path):
print(f" - {output_path}")
print("\nNext:")
print(" - Compare with prediction demo: outputs/demo_case_*.png")
print(" - Run full test set analysis: python examples/intervention_analysis.py")
print(" - Check comprehensive metrics: outputs/intervention_analysis/*.csv")
✅ Intervention demo complete! Generated files: - outputs/intervention_case_7.png - outputs/intervention_case_596.png - outputs/intervention_case_578.png - outputs/intervention_case_657.png Next: - Compare with prediction demo: outputs/demo_case_*.png - Run full test set analysis: python examples/intervention_analysis.py - Check comprehensive metrics: outputs/intervention_analysis/*.csv