CNN visualization of intermediate steps using Saliency maps¶
Using TensorFlow 2.0 and Keras
Based on the CIFAR-10 deep neural network prepared in the CnnCifar10 notebook (HTML / Jupyter).
Part 1's visualizations (HTML / Jupyter) based on activation maps have shown there limits. Let's try some more advanced techniques.
Learning goals:
- saliency maps
- attribution maps
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras import datasets, layers, losses, models
import tensorflow as tf
import seaborn as sns
if True:
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
Data : CIFAR-10¶
Images are normalized to $[0, 1]$
(xTrain, yTrain),(xTest, yTest) = datasets.cifar10.load_data()
xTrain = xTrain / 255.
xTest = xTest / 255.
classNames = ['plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck']
xTrain.shape, xTest.shape
Model¶
model0 = models.load_model('models/CIFAR-10_CNN5.h5')
model0.summary()
Helpers¶
def predictUntilLayer(model, layerIndex, data):
""" Execute prediction on a portion of the model """
intermediateModel = models.Model(inputs=model.input,
outputs=model.layers[layerIndex].output)
return intermediateModel.predict(data)
def plotSaliencyMap(original, saliencies, activations, activationsAfter, layerNames, title="Input"):
""" Plot saliency map and comparison to input """
n = len(saliencies)
fig, axes = plt.subplots(n, 5, figsize=(14, 4*n), sharey=True)
axes[0][0].imshow(original)
axes[0][0].set_title(title)
for i in range(n):
axes[i][1].imshow(normalizeImage(saliencies[i]))
axes[i][1].set_title("%s Saliency" % layerNames[i])
axes[i][2].imshow(normalizeImage((saliencies[i] - original)))
axes[i][2].set_title("Saliency - Input")
act = activations[i]
if len(act.shape) == 2:
axes[i][3].imshow(np.clip(act, 0, 1), cmap='gray')
else:
axes[i][3].text(0, 0.5, "%.3g" % act)
axes[i][3].set_title("Activation on origin")
act = activationsAfter[i]
if len(act.shape) == 2:
axes[i][4].imshow(np.clip(act, 0, 1), cmap='gray')
else:
axes[i][4].text(0, 0.5, "%.3g" % act)
axes[i][4].set_title("Activation on saliency")
plt.setp(axes, xticks=[], yticks=[], frame_on=False)
def normalizeImage(img):
""" Normalize image to have pixel values in [0, 1] """
mi = img.min()
ma = img.max()
return (img - mi) / (ma - mi)
Network end to end saliency maps¶
Original saliency map paper [2, section 3] called it "Activation maximization". Activation is reinforced through gradient ascent in order to maximize : $$ x^* = \arg \max_{x s.t. \Vert x \Vert = \rho} h_{ij}(θ,x)$$
In which :
- $h_{ij}$ is the unit #j of the layer #i
- $x$ is an element of the image space ($\in \mathbb{R}^p \times \mathbb{R}^q$, p and q being the width and height)
- $\theta$ are the coefficients of the unit
This problem is very difficult to tackle with a numerical optimizer.
A sub-optimal solution is sought through gradient ascent.
In [2], gradient ascent is performed 9 times on random picked test samples.
In [3] the saliency maps is computed only on a modified softmax layer in order to compute the best activation for a given class.
We transform this problem back to the classic minimization of the deep neural network :
- select a target class $c$ to detect
- compute the loss using cross-entropy versus the selected class, add regularization terms
- compute corresponding gradients
- substract gradient on input image
In which :
- $\mathcal{I}$ is the input image to transform as a saliency map. Initialized as a neutral black or grey
- $c$ is the target class on which we want to figure out saliency maps
- $\epsilon$ is the learning rate of the gradient descent
It is a DNN training in which the network coefficients are frozen, and the weights to optimize are the pixels of the input.
@tf.function
def gradientMinLossStep(model, img, learningRate, lambdaReg, trueActivation, loss):
""" Minimize error using backpropagation """
with tf.GradientTape() as tape:
tape.watch(img)
tape.watch(trueActivation)
activation = model(img)
imgNorm = tf.norm(img, 'euclidean')
# Loss + regularization
objective = loss(trueActivation, activation) + tf.cast(lambdaReg * imgNorm, tf.float32)
grads = tape.gradient(objective, img)
# Gradient descent to minimize objective (cross entropy)
img = img - learningRate * grads
return objective, grads, img, imgNorm
def endToEndSaliency(numEpochs, model, img, learningRate, lambdaReg, trueActivation):
imgTf = tf.expand_dims(img, axis=0)
trueActivationTf = tf.constant(trueActivation.reshape(1, -1))
loss = tf.keras.losses.SparseCategoricalCrossentropy(reduction=tf.keras.losses.Reduction.NONE)
intensityHist, gradHist, imgNorms = [], [], []
for epoch in range(numEpochs):
intensity, grads, imgTf, imgNorm = gradientMinLossStep(model, imgTf, learningRate,
lambdaReg, trueActivationTf, loss)
intensityHist.append(intensity)
gradHist.append(grads)
imgNorms.append(imgNorm)
# Only clip the returned saliency image
return (imgTf[0], intensityHist, gradHist, imgNorms)
Apply on neutral grey image
mediumGray = np.ones((32, 32, 3)) * 0.2
e2eSaliencies = []
for c in range(10):
e2eSaliencies.append(endToEndSaliency(100, model0, mediumGray, 0.05, 1e-3, np.array([c * 1.])))
fig, axes = plt.subplots(2, 5, figsize=(15, 7))
for c, sal, ax in zip(classNames, e2eSaliencies, axes.ravel()):
ax.imshow(normalizeImage(sal[0].numpy()))
ax.set_title(c)
pred = model0.predict(sal[0].numpy().reshape(1, 32, 32, 3))[0]
predMaxIndex = np.argmax(pred)
ax.set_xlabel("predicted : %s %.2f%%" % (classNames[predMaxIndex], pred[predMaxIndex] * 100))
plt.setp(axes, xticks=[], yticks=[], frame_on=False);
fig, axes = plt.subplots(1, 2, figsize=(16, 4))
for i, saliency in enumerate(e2eSaliencies):
axes[0].plot(saliency[3])
axes[1].semilogy(saliency[1], label=classNames[i])
axes[0].set_xlabel("Epoch")
axes[0].set_title('Image norm (L2)')
axes[0].grid()
axes[1].set_xlabel("Epoch")
axes[1].set_title('Loss (sparse cross entropy)')
axes[1].legend()
axes[1].grid()
Apply saliency map to a test image¶
Salient images are multiplied to a test image and used as input to the model for predictions.
testImgIndex = 3
sampleInput = xTest[testImgIndex]
sampleClass = classNames[yTest[testImgIndex][0]]
fig, axes = plt.subplots(2, 5, figsize=(15, 7))
for c, sal, ax in zip(classNames, e2eSaliencies, axes.ravel()):
saliencyVal = sal[0].numpy()
gradTimesImage = np.multiply(saliencyVal, sampleInput)
gradTimesImage = gradTimesImage #/ gradTimesImage.max()
ax.imshow(normalizeImage(gradTimesImage))
ax.set_title("%s x saliency(%s)" % (sampleClass, c))
pred = model0.predict(gradTimesImage.reshape(1, 32, 32, 3))[0]
predMaxIndex = np.argmax(pred)
ax.set_xlabel("predicted : %s %.2f%%" % (classNames[predMaxIndex], pred[predMaxIndex] * 100))
plt.setp(axes, xticks=[], yticks=[], frame_on=False);
Almost all modified images are classified as the target class for which the saliency map has been made.
Selected image saliency¶
Another way to use saliency maps is to inspect a given layer (or a given unit within a layer) in order to find which input image will lead to the highest activation. In this problem there is no clearly defined goal as there was previously the probability of the target class. The goal is not a minimum but a maximum of intensity on the layer/unit output. Thus this is a gradient ascent as presented in [2] and [3] (with regularization).
$$ \mathcal{I} = \mathcal{I} + \nabla_{\mathcal{I}}^{(l)}(z) - \lambda \Vert \mathcal{I} \Vert_F $$In which :
- $\mathcal{I}$ is the input image to transform as a saliency map
- $z$ is either the full layer $l$ output or a given unit of this layer
- $\lambda$ is the regularization hyper-parameter
- $\Vert . \Vert_F$ if the Frobenius norm
It can be performed on a selected test image. The main interest is to visualize not only the parts of the images that activate the full network but also the ones that activate part of the network.
Following code is inspired by [4] but using Keras and TensorFlow 2.0, and from the Deepdream demo of TensorFlow [5] but without the strange combination of two layers, without the clip on the image pixel values that is inserting some high frequency noise (ripples). Regularization is also added in order to speedup and enhance saliency maps.
We apply this technique on the successive layers of the neural network.
@tf.function
def gradientMaxStep(model, img, learningRate, lambdaReg, unit=None):
""" Gradient ascent step in order to maximize the output (on the specified unit if provided)"""
with tf.GradientTape() as tape:
tape.watch(img)
activation = model(img)
# Select unit if any
if unit is not None:
if len(activation.shape) == 4:
activationTarget = activation[:,:,:,unit]
else:
activationTarget = activation[:,unit]
else:
activationTarget = activation
imgNorm = tf.cast(tf.norm(img, 'euclidean'), tf.float32)
# Objective to maximize, with regularization
# tf.math.abs(tf.math.reduce_mean(activationTarget))
objective = tf.norm(activationTarget) - lambdaReg * imgNorm
grads = tape.gradient(objective, img)
# Gradient ascent
img = img + learningRate * grads
return img, objective, grads, imgNorm
def getPartialSalient(numEpochs, model, img, learningRate, lambdaReg, unit=None):
""" Saliency computation on part or full network in "unsupervised mode"
i.e. no target class provided.
Output is maximized through gradient ascent with regularization (soft constraint on the image norm)
"""
imgTf = tf.expand_dims(img, axis=0)
trueActivationTf = None
loss = None
intensityHist, gradHist, imgNorms = [], [], []
for epoch in range(numEpochs):
imgTf, intensity, grads, imgNorm = gradientMaxStep(model, imgTf, learningRate, lambdaReg, unit)
intensityHist.append(intensity)
gradHist.append(grads)
imgNorms.append(imgNorm)
return (imgTf[0], intensityHist, gradHist, imgNorms)
layerTitles = ['Conv layer #0', 'Conv layer #1', 'Conv layer #2', 'Dense layer #0']
partialModels = [models.Model(inputs=model0.input,
outputs=model0.layers[i].output) for i in [0, 2, 4, 7]]
learningRates = [0.05, 0.05, 0.01, 0.005]
lambdaRegs = [1, 5, 0.1, 1]
testImgIndex = 17
sampleInput = xTest[testImgIndex]
saliencyRes, activations, activationsAfter = [], [], []
for m, learningRate, lambdaReg in zip(partialModels, learningRates, lambdaRegs):
saliencyRes.append(getPartialSalient(200, m, sampleInput, learningRate, lambdaReg))
act = m.predict(np.array([sampleInput])).squeeze(axis=0)
if len(act.shape) == 3:
activations.append(np.mean(act, axis=2))
else:
activations.append(np.mean(act))
actAfter = m.predict(np.array([saliencyRes[-1][0]])).squeeze(axis=0)
if len(act.shape) == 3:
activationsAfter.append(np.mean(actAfter, axis=2))
else:
activationsAfter.append(np.mean(actAfter))
m.predict(np.array([saliencyRes[-1][0]])).shape
plotSaliencyMap(sampleInput,
[saliencyRes[i][0].numpy() for i in range(len(saliencyRes))],
activations, activationsAfter,
layerNames=layerTitles)
fig, axes = plt.subplots(1, 2, figsize=(16, 4))
for i in range(len(saliencyRes)):
axes[0].plot(saliencyRes[i][3], label=layerTitles[i])
axes[1].plot(saliencyRes[i][1], label=layerTitles[i])
axes[0].set_title('Image norm during gradient ascent')
axes[0].set_xlabel('epoch')
axes[0].legend()
axes[0].grid()
axes[1].set_title('Intensity (objective function)')
axes[1].set_xlabel('epoch')
axes[1].grid()
The pixel values are clipped at the end of the saliency processing to $[0,1]$
Gradient display¶
saliencyIndex = 2
fig, axes = plt.subplots(2, 5, figsize=(16, 6))
for i, ax in enumerate(axes.ravel()):
epoch = 20 * i
gradImg = (saliencyRes[saliencyIndex][2][epoch].numpy() * 0.5 + 0.5).reshape(32, 32, 3)
ax.imshow(np.clip(gradImg, 0, 1))
ax.set_title("epoch %d" % epoch)
plt.setp(axes, xticks=[], yticks=[], frame_on=False);
print(np.min(saliencyRes[saliencyIndex][2][100].numpy()), np.max(saliencyRes[saliencyIndex][2][90].numpy()))
Saliency of a given unit¶
If starting from a grey image, since the full hidden layer activation is maximized, we have no idea what the maximization will drive as the layer is supposed to handle all classes.
However we may apply such processing with focus on a given unit within the layer.
selectedUnits = [0, 4, 10, 60]
partialModels2 = [models.Model(inputs=model0.input,
outputs=model0.layers[i].output) for i in [2, 2, 7, 7]]
layerTitles2 = ['Layer #1, Conv #%d' % selectedUnits[0], 'Layer #1, Conv #%d' % selectedUnits[1],\
'Layer #4, Unit #%d' % selectedUnits[2], 'Layer #4, Unit #%d' % selectedUnits[3]]
learningRates = [0.05, 0.05, 0.01, 0.01]
testImgIndex = 17 # 2 = boat, 3 = Concord, 17 = horse
sampleInputLocal = mediumGray
# xTest[testImgIndex]
# np.random.normal(0.4, 0.1, (32, 32, 3))
saliencyResLocal, activationsLocal, activationsAfterLocal = [], [], []
for m, learningRate, unit in zip(partialModels2, learningRates, selectedUnits):
saliencyResLocal.append(getPartialSalient(200, m, sampleInputLocal, learningRate, 1, unit))
act = m.predict(np.array([sampleInputLocal])).squeeze(axis=0)
if len(act.shape) == 3:
activationsLocal.append(act[:,:,unit])
else:
activationsLocal.append(act[unit])
actAfter = m.predict(np.array([saliencyRes[-1][0]])).squeeze(axis=0)
if len(act.shape) == 3:
activationsAfterLocal.append(actAfter[:,:,unit])
else:
activationsAfterLocal.append(actAfter[unit])
plotSaliencyMap(sampleInputLocal,
[saliencyResLocal[i][0].numpy() for i in range(len(saliencyResLocal))],
activationsLocal, activationsAfterLocal,
layerNames=layerTitles2)
References¶
- Tensorflow tutorial for CNN
- Visualizing Higher-Layer Features of a Deep Network, D. Erhan, Y. Bengio, A. Courville, 2009
- Deep Inside Convolutional Networks: Visualising Image Classification Models and Saliency Maps
- Saliency Maps for Deep Learning Part 1: Vanilla Gradient, Andrew Schreiber
- DeepDream, Tensorflow documentation