Search…

CV Explainability: Grad-CAM, Failures, Bias Checks

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. 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 ycy^c with respect to each feature map AkA^k.

Step 2. Average each gradient map to a single weight:

αkc=1ZijycAijk\alpha_k^c = \frac{1}{Z}\sum_i \sum_j \frac{\partial y^c}{\partial A^k_{ij}}

Step 3. Weighted sum of the feature maps, clipped at zero:

Lc=ReLU ⁣(kαkcAk)L^c = \text{ReLU}\!\left(\sum_k \alpha_k^c A^k\right)

Worked, with four feature maps

Take a toy 2×2 spatial size and four channels. Here are the activations:

A¹ activations (2×2)
2
8
1
1
A² activations (2×2)
9
1
2
2
A³ activations (2×2)
1
7
0
2
A⁴ activations (2×2)
4
4
4
4

And here are the gradients of the pneumonia score with respect to each:

# Channel Gradient values Sum αk = mean target
1 0.4, 0.2, 0.1, 0.1 0.8 0.200
2 −0.1, −0.3, −0.1, −0.1 −0.6 −0.150
3 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 (α3=0.6\alpha_3 = 0.6). Channel 2 has a negative weight, meaning it pushes the score down. Channel 4 is nearly irrelevant.

Now the weighted sum, cell by cell:

L00=0.2(2)+(0.15)(9)+0.6(1)+0.025(4)L_{00} = 0.2(2) + (-0.15)(9) + 0.6(1) + 0.025(4) =0.41.35+0.6+0.1=0.25  ReLU  0= 0.4 - 1.35 + 0.6 + 0.1 = -0.25 \;\xrightarrow{\text{ReLU}}\; 0

L01=0.2(8)+(0.15)(1)+0.6(7)+0.025(4)L_{01} = 0.2(8) + (-0.15)(1) + 0.6(7) + 0.025(4) =1.60.15+4.2+0.1=5.75= 1.6 - 0.15 + 4.2 + 0.1 = 5.75

L10=0.2(1)+(0.15)(2)+0.6(0)+0.025(4)=0.20.3+0+0.1=0.00L_{10} = 0.2(1) + (-0.15)(2) + 0.6(0) + 0.025(4) = 0.2 - 0.3 + 0 + 0.1 = 0.00

L11=0.2(1)+(0.15)(2)+0.6(2)+0.025(4)=0.20.3+1.2+0.1=1.20L_{11} = 0.2(1) + (-0.15)(2) + 0.6(2) + 0.025(4) = 0.2 - 0.3 + 1.2 + 0.1 = 1.20

Weighted sum (2×2)
-0.25
5.75
0
1.2
ReLU
After ReLU (2×2)
0
5.75
0
1.2
÷ max
Normalised heatmap (2×2)
0
1
0
0.21

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

MethodQuestion it answersCostMain weakness
Grad-CAMWhich regions raised this class score?1 forward + 1 backwardCoarse — 7×7 upsampled to 224×224
OcclusionWhat happens if I hide this patch?~50–200 forwardsSlow, patch size changes the answer
Integrated GradientsPer-pixel attribution vs a baseline~50 forwards + backwardsNoisy, baseline choice matters a lot
Attention rollout (ViT)Which tokens did attention flow through?1 forwardAttention weight is not causal importance
Blackout testDoes accuracy survive without this region?1 full evalOnly tests regions you thought to try
Explanation methods. The blackout test is the least sophisticated and the most conclusive.

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.

Alignment 0%
Source features (blue) and target features (red) begin misaligned. Adaptation learns a shared feature space so one decision boundary works for both.

Failure analysis

Take 60 to 100 errors and read them. Not a sample of the whole set — the errors.

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:

ECE=bnbNaccbconfb\text{ECE} = \sum_b \frac{n_b}{N}\left|\text{acc}_b - \text{conf}_b\right|

=120(0.037)+180(0.057)+250(0.092)+300(0.114)+150(0.114)1000= \frac{120(0.037) + 180(0.057) + 250(0.092) + 300(0.114) + 150(0.114)}{1000}

=4.44+10.26+23.00+34.20+17.101000=89.001000=0.0890= \frac{4.44 + 10.26 + 23.00 + 34.20 + 17.10}{1000} = \frac{89.00}{1000} = 0.0890

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 TT before the softmax, and fit TT on the validation set.

Take logits [3.0,1.0,0.5][3.0, 1.0, 0.5]. At T=1T = 1:

e3.0=20.086,e1.0=2.718,e0.5=1.649,sum=24.453e^{3.0} = 20.086,\quad e^{1.0} = 2.718,\quad e^{0.5} = 1.649,\quad \text{sum} = 24.453 probs=[0.8214,  0.1112,  0.0674]\text{probs} = [0.8214,\; 0.1112,\; 0.0674]

At T=1.5T = 1.5, logits become [2.000,0.667,0.333][2.000, 0.667, 0.333]:

e2.000=7.389,e0.667=1.948,e0.333=1.395,sum=10.732e^{2.000} = 7.389,\quad e^{0.667} = 1.948,\quad e^{0.333} = 1.395,\quad \text{sum} = 10.732 probs=[0.6885,  0.1815,  0.1300]\text{probs} = [0.6885,\; 0.1815,\; 0.1300]

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.

Practice task

Use any classifier you have trained.

  1. Implement Grad-CAM with hooks. Verify your αk\alpha_k are the channel means of the gradients.
  2. Run it on 20 errors and 20 correct predictions. Look for differences in where the model attends.
  3. Pick a region the model attends to that should not matter. Black it out and re-evaluate.
  4. Slice accuracy by at least three metadata fields. Flag anything more than 5 points below overall.
  5. Bucket 60 errors by cause, then rank fixes by errors-removed per day.
  6. Build a reliability table with 5 bins and compute ECE by hand.
  7. Fit a temperature on validation. Recompute ECE, accuracy, and AUC. Confirm only ECE moved.
  8. 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.

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.

Test your understanding
Your defect classifier scores 94% on the test set. Grad-CAM shows it consistently attending to the conveyor belt background, not the part. What should you do first?
Test your understanding
You route any prediction below 0.90 confidence to a human reviewer. Your ECE is 0.089 and the model is overconfident. What is happening?

Frequently asked questions

Which layer should I hook for Grad-CAM?
The last convolutional block — layer4 in a ResNet. Earlier layers have finer spatial resolution, which makes the heatmap look sharper, but their features represent edges and textures rather than concepts, so the map shows you where the image has detail rather than where the evidence is. If you need finer localisation, use Grad-CAM++ or combine the coarse map with a guided backpropagation image.
Does Grad-CAM work on vision transformers?
Not directly, because ViT has no final conv feature map with spatial structure in the same way. You can reshape the patch tokens of the last block back into a grid and apply the same weighting, which works reasonably. Attention rollout is the more native alternative, composing attention across all layers, but remember attention weight is correlation with what the model looked at, not proof of what caused the output.
How many errors do I need to look at?
Sixty is usually enough to see the pattern, and a hundred is comfortable. Past that you are mostly re-confirming buckets you already found. What matters more than the count is that they are actual errors rather than a random sample of all predictions, and that you assign each to exactly one bucket so the counts add up and the per-day ranking is meaningful.
Why are neural networks overconfident?
Cross-entropy keeps rewarding larger logit margins even after the prediction is already correct, so training past the point of fitting the data pushes confidences toward 1.0 without improving the underlying accuracy. Modern networks train long past that point. Label smoothing and mixup reduce the effect during training; temperature scaling fixes it afterwards with one parameter and no accuracy cost.
Can I fix a shortcut with augmentation alone?
Sometimes, and it is always worth trying first because it is cheap. Random cropping removes border artefacts, colour jitter breaks reliance on a specific scanner's tone curve, and blacking out the offending region during training forces the model elsewhere. But if the shortcut is genuinely predictive in all your training data — one hospital, one machine — no augmentation creates the variation that is missing. That needs data from a second source.
Start typing to search across all content
navigate Enter open Esc close