TensorFlow Fundamentals · lesson 15/27
Visual Analysis
A trained network is a stack of numeric feature maps, and nothing about that stack is inherently visible. Visual analysis makes it inspectable: it renders what each filter responds to and how strongly each input region drives a prediction. The payoff is debugging — you can see dead filters, saturated activations, and shortcuts the model learned.
The idea
A convolution layer returns a tensor of shape (batch, height, width, filters). Each slice [:, :, :, c] is the activation map of filter c, and its values say how strongly that learned pattern fired at each spatial location.
Two complementary techniques:
- Activation maximization — freeze the weights and treat the input image as the trainable variable. Use gradient ascent to maximize a chosen activation, then visualize the resulting image. This reveals the "preferred stimulus" of a filter.
- Saliency and occlusion — measure how much each input pixel or patch changes a chosen output logit, either with gradients or by zeroing regions and re-running.
Pure gradient ascent produces adversarial-looking high-frequency noise, so the objective adds regularizers: total variation encourages smoothness, an L2 penalty limits pixel magnitude, and a periodic Gaussian blur stabilizes the result.
Worked example
Take a small MNIST CNN: Conv2D(32, 3, activation="relu") on a (28, 28, 1) input gives a (26, 26, 32) map.
- Feed one digit; filter
c = 7has mean activation0.42over its26×26cells. - To see filter 7's preferred pattern, start from a random
(1, 28, 28, 1)image and run 100 ascent steps on the pixels with learning rate1.0, blurring after each step. - Drop the blur and the same filter converges to pixel-level speckle. The image still maximizes the activation, but it tells you nothing about the pattern.
In code
import tensorflow as tf
model = tf.keras.models.load_model("mnist_cnn.keras")
conv = tf.keras.Model(model.inputs, model.get_layer("conv2d").output)
img = tf.Variable(tf.random.uniform((1, 28, 28, 1)))
opt = tf.keras.optimizers.Adam(1.0)
for step in range(100):
with tf.GradientTape() as tape:
acts = conv(img)
loss = tf.reduce_mean(acts[..., 7]) # filter 7
loss -= 0.1 * tf.reduce_mean(tf.image.total_variation(img))
opt.apply_gradients([(tape.gradient(loss, img), img)])
img.assign(tf.nn.avg_pool2d(img, 3, 1, "SAME")) # keep it smooth
tf.keras.utils.save_img("filter7.png", img[0])Check yourself
- Why are the weights frozen and the image updated during activation maximization?
- What artifact appears when you remove total-variation and blur regularization, and why?
- How would occlusion differ from gradient-based saliency on a model that uses a background shortcut?
Key takeaways
- Feature-map slices are per-filter activations indexed by spatial location.
- Activation maximization learns an input, not a parameter; regularization is what makes it readable.
- Saliency and occlusion answer "where did this prediction come from", not "what does this filter want".