Gradient CAM

The method we just saw only lets us compute a heatmap with the last activations, since once we have our features, we have to multiply them by the last weight matrix. This won’t work for inner layers in the network. A variant introduced in the paper “Grad-CAM: Why Did You Say That? Visual Explanations from Deep Networks via Gradient-based Localization” in 2016 uses the gradients of the final activation for the desired class. If you remember a little bit about the backward pass, the gradients of the output of the last layer with respect to the input of that layer are equal to the layer weights, since it is a linear layer.

With deeper layers, we still want the gradients, but they won’t just be equal to the weights anymore. We have to calculate them. The gradients of every layer are calculated for us by PyTorch during the backward pass, but they’re not stored (except for tensors where requires_grad is True). We can, however, register a hook on the backward pass, which PyTorch will give the gradients to as a parameter, so we can store them there. For this we will use a HookBwd class that works like Hook, but intercepts and stores gradients instead of activations:

In [ ]:

  1. class HookBwd():
  2. def __init__(self, m):
  3. self.hook = m.register_backward_hook(self.hook_func)
  4. def hook_func(self, m, gi, go): self.stored = go[0].detach().clone()
  5. def __enter__(self, *args): return self
  6. def __exit__(self, *args): self.hook.remove()

Then for the class index 1 (for True, which is “cat”) we intercept the features of the last convolutional layer as before, and compute the gradients of the output activations of our class. We can’t just call output.backward(), because gradients only make sense with respect to a scalar (which is normally our loss) and output is a rank-2 tensor. But if we pick a single image (we’ll use 0) and a single class (we’ll use 1), then we can calculate the gradients of any weight or activation we like, with respect to that single value, using output[0,cls].backward(). Our hook intercepts the gradients that we’ll use as weights:

In [ ]:

  1. cls = 1
  2. with HookBwd(learn.model[0]) as hookg:
  3. with Hook(learn.model[0]) as hook:
  4. output = learn.model.eval()(x.cuda())
  5. act = hook.stored
  6. output[0,cls].backward()
  7. grad = hookg.stored

The weights for our Grad-CAM are given by the average of our gradients across the feature map. Then it’s exactly the same as before:

In [ ]:

  1. w = grad[0].mean(dim=[1,2], keepdim=True)
  2. cam_map = (w * act[0]).sum(0)

In [ ]:

  1. _,ax = plt.subplots()
  2. x_dec.show(ctx=ax)
  3. ax.imshow(cam_map.detach().cpu(), alpha=0.6, extent=(0,224,224,0),
  4. interpolation='bilinear', cmap='magma');

Gradient CAM - 图1

The novelty with Grad-CAM is that we can use it on any layer. For example, here we use it on the output of the second-to-last ResNet group:

In [ ]:

  1. with HookBwd(learn.model[0][-2]) as hookg:
  2. with Hook(learn.model[0][-2]) as hook:
  3. output = learn.model.eval()(x.cuda())
  4. act = hook.stored
  5. output[0,cls].backward()
  6. grad = hookg.stored

In [ ]:

  1. w = grad[0].mean(dim=[1,2], keepdim=True)
  2. cam_map = (w * act[0]).sum(0)

And we can now view the activation map for this layer:

In [ ]:

  1. _,ax = plt.subplots()
  2. x_dec.show(ctx=ax)
  3. ax.imshow(cam_map.detach().cpu(), alpha=0.6, extent=(0,224,224,0),
  4. interpolation='bilinear', cmap='magma');

Gradient CAM - 图2