Source code for darkon.gradcam.gradcam

# Copyright 2017 Neosapience, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========================================================================
"""
References
----------
.. [1] Ramprasaath R. Selvaraju, Michael Cogswell, Abhishek Das, Ramakrishna Vedantam, Devi Parikh, Dhruv Batra \
"Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization" ICCV2017

"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
from __future__ import unicode_literals

import numpy as np
import tensorflow as tf
import cv2
from skimage.transform import resize as skimage_resize

from .guided_grad import replace_grad_to_guided_grad
from .candidate_ops import candidate_featuremap_op_names, candidate_predict_op_names
from .candidate_ops import _unusable_ops


def _deprocess_image(x):
    # Same normalization as in:
    # https://github.com/fchollet/keras/blob/master/examples/conv_filter_visualization.py
    if np.ndim(x) > 3:
        x = np.squeeze(x)
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    x *= 255
    x = np.clip(x, 0, 255).astype('uint8')
    return x


[docs]class Gradcam: """ Gradcam Class Parameters ---------- x_placeholder : tf.Tensor Data place holder Tensor from tf.placeholder() num_classes: int number of classes featuremap_op_name : str Operation name of CNN feature map layer To get the list of candidate names, use ``Gradcam.candidate_featuremap_op_names()`` predict_op_name : str Operation name of prediction layer (decision output) To get the list of candidate names, use ``Gradcam.candidate_predict_op_names()`` graph : tf.Graph Tensorflow graph """ def __init__(self, x_placeholder, num_classes, featuremap_op_name, predict_op_name=None, graph=None): self._x_placeholder = x_placeholder graph = graph if graph is not None else tf.get_default_graph() self.graph = graph predict_op_name = self._find_prob_layer(predict_op_name, graph) self._prob_ts = graph.get_operation_by_name(predict_op_name).outputs[0] self._target_ts = graph.get_operation_by_name(featuremap_op_name).outputs[0] self._class_idx = tf.placeholder(tf.int32) top1 = tf.argmax(tf.reshape(self._prob_ts, [-1])) loss_by_idx = tf.reduce_sum(tf.multiply(self._prob_ts, tf.one_hot(self._class_idx, num_classes)), axis=1) loss_by_top1 = tf.reduce_sum(tf.multiply(self._prob_ts, tf.one_hot(top1, num_classes)), axis=1) self._grad_by_idx = self._normalize(tf.gradients(loss_by_idx, self._target_ts)[0]) self._grad_by_top1 = self._normalize(tf.gradients(loss_by_top1, self._target_ts)[0]) replace_grad_to_guided_grad(graph) max_output = tf.reduce_max(self._target_ts, axis=2) self._saliency_map = tf.gradients(tf.reduce_sum(max_output), x_placeholder)[0]
[docs] def gradcam(self, sess, input_data, target_index=None, feed_options=dict()): """ Calculate Grad-CAM (class activation map) and Guided Grad-CAM for given input on target class Parameters ---------- sess: tf.Session Tensorflow session input_data : numpy.ndarray A single input instance target_index : int Target class index If None, predicted class index is used feed_options : dict Optional parameters to graph Returns ------- dict Note ---- Keys in return: * gradcam_img: Heatmap overlayed on input * guided_gradcam_img: Guided Grad-CAM result * heatmap: Heatmap of input on the target class * guided_backprop: Guided backprop result """ input_feed = np.expand_dims(input_data, axis=0) if input_data.ndim == 3: is_image = True image_height, image_width = input_data.shape[:2] if input_data.ndim == 1: is_image = False input_length = input_data.shape[0] if target_index is not None: feed_dict = {self._x_placeholder: input_feed, self._class_idx: target_index} feed_dict.update(feed_options) conv_out_eval, grad_eval = sess.run([self._target_ts, self._grad_by_idx], feed_dict=feed_dict) else: feed_dict = {self._x_placeholder: input_feed} feed_dict.update(feed_options) conv_out_eval, grad_eval = sess.run([self._target_ts, self._grad_by_top1], feed_dict=feed_dict) weights = np.mean(grad_eval, axis=(0, 1, 2)) conv_out_eval = np.squeeze(conv_out_eval, axis=0) cam = np.zeros(conv_out_eval.shape[:2], dtype=np.float32) for i, w in enumerate(weights): cam += w * conv_out_eval[:, :, i] if is_image: cam += 1 cam = cv2.resize(cam, (image_height, image_width)) saliency_val = sess.run(self._saliency_map, feed_dict={self._x_placeholder: input_feed}) saliency_val = np.squeeze(saliency_val, axis=0) else: cam = skimage_resize(cam, (input_length, 1), preserve_range=True, mode='reflect') cam = np.transpose(cam) cam = np.maximum(cam, 0) heatmap = cam / np.max(cam) ret = {'heatmap': heatmap} if is_image: ret.update({ 'gradcam_img': self.overlay_gradcam(input_data, heatmap), 'guided_gradcam_img': _deprocess_image(saliency_val * heatmap[..., None]), 'guided_backprop': saliency_val }) return ret
[docs] @staticmethod def candidate_featuremap_op_names(sess, graph=None, feed_options=None): """ Returns the list of candidates for operation names of CNN feature map layer Parameters ---------- sess: tf.Session Tensorflow session graph: tf.Graph Tensorflow graph feed_options: dict Optional parameters to graph Returns ------- list String list of candidates """ graph = graph if graph is not None else tf.get_default_graph() feed_options = feed_options if feed_options is not None else {} return candidate_featuremap_op_names(sess, graph, feed_options)
[docs] @staticmethod def candidate_predict_op_names(sess, num_classes, graph=None, feed_options=None): """ Returns the list of candidate for operation names of prediction layer Parameters ---------- sess: tf.Session Tensorflow session num_classes: int Number of prediction classes graph: tf.Graph Tensorflow graph feed_options: dict Optional parameters to graph Returns ------- list String list of candidates """ graph = graph if graph is not None else tf.get_default_graph() feed_options = feed_options if feed_options is not None else {} return candidate_predict_op_names(sess, num_classes, graph, feed_options)
[docs] @staticmethod def overlay_gradcam(image, heatmap): """ Overlay heatmap on input data """ output_image = np.array(image) output_image -= np.min(output_image) output_image = np.minimum(output_image, 255) cam = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET) output_image = np.float32(cam) + np.float32(output_image) output_image = 255 * output_image / np.max(output_image) output_image = np.uint8(output_image) return output_image
@staticmethod def _find_prob_layer(output_name, graph): if output_name is not None: return output_name for op in graph.get_operations(): if _unusable_ops(op): continue output_name = op.name return output_name @staticmethod def _normalize(x): return tf.div(x, (tf.sqrt(tf.reduce_mean(tf.square(x), axis=1)) + 1e-5))