CV Explainability: Grad-CAM, Failures, Bias Checks
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- Responsible CV: Privacy, Fairness, Security, Governance
A hospital’s pneumonia classifier scored 96% on the held-out test set. It was deployed, and accuracy collapsed to 71%. The Grad-CAM maps showed why in about ten seconds: the model was looking at the corner of the image, where the portable scanner burned in a small marker. Portable scanners get wheeled to patients too sick to walk, so that marker predicted pneumonia beautifully — until the model met a different hospital.
Prerequisites: Vision transformers and computer vision metrics.
The setup
A binary chest X-ray classifier: pneumonia or normal. Trained on 12,000 images from one hospital, tested on a held-out split from the same hospital.
| # | Evaluation | Accuracy target | AUC | Verdict |
|---|---|---|---|---|
| 1 | Held-out split, same hospital | 96.2% | 0.981 | Ship it |
| 2 | Second hospital, different scanners | 71.4% | 0.744 | Something is very wrong |
| 3 | Second hospital, corner 40 px blacked out | 70.9% | 0.739 | Corner was not the whole story |
| 4 | Same hospital, corner blacked out | 82.1% | 0.868 | 14 points came from the marker |
Four evaluations of one model. Only the last two explain the gap.
That fourth row is the diagnostic. Blacking out a 40-pixel corner cost 14 points of accuracy on the original test set. No genuine medical signal lives in that corner.
shortcut learning When a model achieves high accuracy using a feature that correlates with the label in the training data but has nothing to do with the real cause.Shortcuts are not bugs in training. Gradient descent did exactly what you asked: it minimised loss. The scanner marker was the easiest available route.
Grad-CAM
Grad-CAM answers: which spatial regions of the last conv layer increased the score for this class?
The recipe has three steps.
Step 1. Get the gradient of the class score with respect to each feature map .
Step 2. Average each gradient map to a single weight:
Step 3. Weighted sum of the feature maps, clipped at zero:
Worked, with four feature maps
Take a toy 2×2 spatial size and four channels. Here are the activations:
And here are the gradients of the pneumonia score with respect to each:
| # | Channel | Gradient values | Sum | αk = mean target |
|---|---|---|---|---|
| 1 | A¹ | 0.4, 0.2, 0.1, 0.1 | 0.8 | 0.200 |
| 2 | A² | −0.1, −0.3, −0.1, −0.1 | −0.6 | −0.150 |
| 3 | A³ | 0.6, 0.8, 0.5, 0.5 | 2.4 | 0.600 |
| 4 | A⁴ | 0.05, 0.05, 0.0, 0.0 | 0.1 | 0.025 |
Step 2: global average pool of the gradients gives one weight per channel.
Channel 3 matters most (). Channel 2 has a negative weight, meaning it pushes the score down. Channel 4 is nearly irrelevant.
Now the weighted sum, cell by cell:
Top-right is the hot region. That 2×2 map gets bilinearly upsampled to the input size and overlaid.
Why the ReLU. Without it, the negative cell would show as a strong signal in the opposite direction, which is not what you asked. Grad-CAM’s question is “what supports this class”, so evidence against it is discarded.
feats, grads = {}, {}
layer = model.layer4[-1]
layer.register_forward_hook(lambda m,i,o: feats.setdefault('a', o))
layer.register_full_backward_hook(lambda m,gi,go: grads.setdefault('g', go[0]))
logits = model(x)
model.zero_grad()
logits[0, target_class].backward()
alpha = grads['g'].mean(dim=(2,3), keepdim=True) # step 2
cam = torch.relu((alpha * feats['a']).sum(1)) # step 3
cam = cam / (cam.amax() + 1e-8)
cam = F.interpolate(cam[None], size=x.shape[-2:], mode='bilinear')[0,0]
Two details that trip people up. Hook layer4[-1], the last conv block — earlier layers have finer spatial resolution but their features are edges and textures, not concepts, so the maps look pretty and mean little. And backprop from the logit, not the softmax probability, because the softmax mixes in gradients from the other classes.
What Grad-CAM cannot tell you
| Method | Question it answers | Cost | Main weakness |
|---|---|---|---|
| Grad-CAM | Which regions raised this class score? | 1 forward + 1 backward | Coarse — 7×7 upsampled to 224×224 |
| Occlusion | What happens if I hide this patch? | ~50–200 forwards | Slow, patch size changes the answer |
| Integrated Gradients | Per-pixel attribution vs a baseline | ~50 forwards + backwards | Noisy, baseline choice matters a lot |
| Attention rollout (ViT) | Which tokens did attention flow through? | 1 forward | Attention weight is not causal importance |
| Blackout test | Does accuracy survive without this region? | 1 full eval | Only tests regions you thought to try |
Grad-CAM’s spatial resolution at layer4 of a ResNet is 7×7 for a 224×224 input. Each cell covers a 32×32 region. That is enough to say “the corner” and nowhere near enough to say “this specific opacity in the lower left lobe”.
The blackout test is the one that settles arguments. A heatmap is suggestive; re-running the full evaluation with a region masked out gives you a number. Fourteen points of accuracy is not a matter of interpretation.
Slicing metrics
One aggregate number is the enemy. Here is the same 96.2% model, sliced:
| # | Slice | N | Accuracy target | vs overall |
|---|---|---|---|---|
| 1 | Overall | 2000 | 96.2% | — |
| 2 | Fixed scanner, Siemens | 890 | 97.8% | +1.6 |
| 3 | Fixed scanner, GE | 640 | 96.9% | +0.7 |
| 4 | Portable scanner | 310 | 98.7% | +2.5 |
| 5 | Paediatric (under 16) | 160 | 84.4% | −11.8 |
| 6 | Adult 16–65 | 1290 | 97.1% | +0.9 |
| 7 | Over 65 | 550 | 95.8% | −0.4 |
| 8 | Underexposed images | 95 | 79.0% | −17.2 |
Slice-level accuracy. Two slices are far below the headline and together are only 12% of the data.
The portable-scanner slice at 98.7% is the tell. Portable scans are technically harder — worse positioning, more motion blur, lower dose. A model doing real medicine should score worse there, not better. Scoring better means it found something in those images other than the anatomy.
Failure analysis
Take 60 to 100 errors and read them. Not a sample of the whole set — the errors.
flowchart TD
A["Collect 60-100 errors"] --> B["Look at each one"]
B --> C{"What went wrong?"}
C -->|"label is wrong"| D["Annotation error"]
C -->|"image is unusable"| E["Data quality"]
C -->|"never seen this case"| F["Coverage gap"]
C -->|"looks learnable, model missed it"| G["Model capacity"]
C -->|"model looked elsewhere"| H["Shortcut"]
D --> I["Count each bucket"]
E --> I
F --> I
G --> I
H --> I
I --> J["Fix the largest bucket first"]
J --> K["Re-measure"]
K --> A
The 76 errors from the second hospital, bucketed:
| # | Bucket | Count target | Share | Fix | Effort |
|---|---|---|---|---|---|
| 1 | Shortcut — attended to corner/edges | 31 | 40.8% | Crop borders, add multi-site data | medium |
| 2 | Underexposed images | 17 | 22.4% | Add exposure normalisation + augmentation | low |
| 3 | Paediatric anatomy | 12 | 15.8% | Collect paediatric data or exclude from scope | high |
| 4 | Wrong ground-truth label | 9 | 11.8% | Re-adjudicate, fix labels | low |
| 5 | Genuinely subtle case | 7 | 9.2% | Nothing cheap — accept or route to human | very high |
76 errors bucketed. Two thirds are in the two cheapest buckets.
The ranking that matters is errors-removed per day of work, not errors-removed:
| # | Fix | Errors addressed | Days | Errors per day target |
|---|---|---|---|---|
| 1 | Fix wrong labels | 9 | 0.5 | 18.0 |
| 2 | Exposure normalisation | 17 | 1 | 17.0 |
| 3 | Crop borders + multi-site data | 31 | 6 | 5.2 |
| 4 | Collect paediatric data | 12 | 20 | 0.6 |
| 5 | Improve subtle-case detection | 7 | 30+ | 0.2 |
Two days of work removes 26 of 76 errors. The paediatric fix is 40 times less efficient.
Do the top two first. They cost two days, and the border crop is the third — more expensive but it addresses the failure that actually broke deployment.
Calibration
The model says 0.85. Should you believe it?
calibration A model is calibrated when its confidence matches its accuracy: among all predictions made at 0.85 confidence, 85% should be correct.Bin the test predictions by confidence and check:
| # | Confidence bin | N | Mean confidence | Actual accuracy | |gap| target |
|---|---|---|---|---|---|
| 1 | 0.5 – 0.6 | 120 | 0.554 | 0.517 | 0.037 |
| 2 | 0.6 – 0.7 | 180 | 0.651 | 0.594 | 0.057 |
| 3 | 0.7 – 0.8 | 250 | 0.752 | 0.660 | 0.092 |
| 4 | 0.8 – 0.9 | 300 | 0.851 | 0.737 | 0.114 |
| 5 | 0.9 – 1.0 | 150 | 0.961 | 0.847 | 0.114 |
A reliability table. Every bin is overconfident, and the gap widens with confidence.
Expected calibration error is the size-weighted average gap:
An ECE of 0.089 means the confidence is off by about 9 points on average.
Temperature scaling
One parameter fixes most of it. Divide the logits by before the softmax, and fit on the validation set.
Take logits . At :
At , logits become :
Confidence drops from 0.821 to 0.689. Crucially, the ranking is unchanged, so accuracy, AUC, and mAP are all identical. Only the numbers on the confidence become honest.
class TemperatureScaler(nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
self.log_T = nn.Parameter(torch.zeros(1)) # T = exp(0) = 1
def forward(self, x):
return self.model(x) / self.log_T.exp()
# Fit T on the VALIDATION set only, with the model frozen
opt = torch.optim.LBFGS([scaler.log_T], lr=0.01, max_iter=50)
Fitting on the test set would be cheating in a subtle way: you would be reporting a calibration you tuned on the data you are measuring.
| # | Model state | Accuracy | AUC | ECE target |
|---|---|---|---|---|
| 1 | Before temperature scaling | 96.2% | 0.981 | 0.0890 |
| 2 | After temperature scaling (T = 1.62) | 96.2% | 0.981 | 0.0143 |
One fitted parameter cuts ECE by 84% and changes nothing else.
- Test accuracy is much higher than you expected
- Accuracy drops when you move to new data, sites, or hardware
- A confidence threshold controls a real decision
- The domain is regulated, medical, financial, or safety-critical
- You have not yet sliced your metrics — do that first, it is cheaper
- You are still iterating on a baseline and nothing is deployed
- The heatmap would replace, rather than accompany, a blackout test
Practice task
Use any classifier you have trained.
- Implement Grad-CAM with hooks. Verify your are the channel means of the gradients.
- Run it on 20 errors and 20 correct predictions. Look for differences in where the model attends.
- Pick a region the model attends to that should not matter. Black it out and re-evaluate.
- Slice accuracy by at least three metadata fields. Flag anything more than 5 points below overall.
- Bucket 60 errors by cause, then rank fixes by errors-removed per day.
- Build a reliability table with 5 bins and compute ECE by hand.
- Fit a temperature on validation. Recompute ECE, accuracy, and AUC. Confirm only ECE moved.
- Deliberately add a shortcut — a small coloured square in every positive training image — and retrain. Watch Grad-CAM find it.
Step 8 is the fastest way to trust the tool. You know the ground truth about what the model learned, so you can see whether Grad-CAM reports it.
Summary
A 96.2% model failed at 71.4% elsewhere because it read a scanner marker. Blacking out a 40-pixel corner cost 14 points on the original test set, which no genuine signal could explain.
Grad-CAM averages the gradients per channel to get weights — 0.200, −0.150, 0.600, 0.025 — then takes the weighted sum of activations and applies ReLU. Cell (0,0) came out at −0.25 and became 0; cell (0,1) at 5.75 became the hot spot.
Slicing exposed paediatric at 84.4% and underexposed at 79.0%, both invisible in the headline. The portable-scanner slice scoring above average was the giveaway, since those images are harder.
Bucketing 76 errors ranked the fixes: labels and exposure at 18 and 17 errors per day, paediatric data at 0.6.
Calibration showed the model claiming 0.851 while being right 73.7% of the time. ECE was 0.0890. A single fitted temperature of 1.62 dropped it to 0.0143 with accuracy and AUC unchanged.
- A high test score means the model is right, not that it is right for the right reason.
- Grad-CAM: average gradients per channel to get αk, weight the activations, sum, ReLU, upsample.
- Hook the last conv block and backprop from the logit, not the softmax probability.
- Grad-CAM at layer4 is 7×7 — enough to say 'the corner', not 'that specific lesion'.
- The blackout test is cruder than a heatmap and far more conclusive: it produces a number.
- Slice every metric by scanner, site, exposure, and demographics. Flag anything 5+ points low.
- A slice performing unexpectedly well is as suspicious as one performing badly.
- Bucket 60-100 errors by cause and rank fixes by errors removed per day of work.
- Neural networks are systematically overconfident: 0.851 claimed against 73.7% actual.
- Temperature scaling fits one parameter, cut ECE from 0.089 to 0.014, and left accuracy untouched.
What comes next
Everything so far has treated an image as a flat grid. 3D vision reconstructs the scene itself — recovering camera positions and 3D structure from a set of ordinary photographs, and estimating where an object sits in space.