Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ethen8181
GitHub Repository: ethen8181/machine-learning
Path: blob/master/keras/resnet_cam/explainer.py
2595 views
1
import numpy as np
2
from scipy.ndimage import zoom
3
from keras.models import Model
4
from keras.applications.resnet50 import preprocess_input
5
from keras.preprocessing.image import load_img, img_to_array
6
7
8
class CAMExplainer:
9
"""CAM (Class Activation Map) Explainer"""
10
11
def __init__(self, model, target_size=(224, 224)):
12
self.model = model
13
self.target_size = target_size
14
15
self.class_weights_ = None
16
self.resnet50_cam_layers_ = None
17
18
def fit(self, img_path):
19
if self.class_weights_ is None:
20
self.resnet50_cam_layers_, self.class_weights_ = self._get_resnet50_cam_info()
21
22
self.img_, keras_img = self._read_and_process_img(img_path)
23
self.cam_, self.predicted_class_ = self._create_cam(keras_img)
24
return self
25
26
def plot(self):
27
import matplotlib.pyplot as plt
28
fig, ax = plt.subplots()
29
30
ax.imshow(self.img_, alpha=0.5)
31
ax.imshow(self.cam_, cmap='jet', alpha=0.5)
32
plt.show()
33
34
def _create_cam(self, img):
35
# before_gap_output will be of shape [1, 7, 7, 2048] for resnet50
36
before_gap_output, prediction = self.resnet50_cam_layers_.predict(img)
37
img_width = before_gap_output.shape[1]
38
img_height = before_gap_output.shape[2]
39
n_activation = before_gap_output.shape[3]
40
41
predicted_class = np.argmax(prediction)
42
dominate_class_weight = self.class_weights_[:, predicted_class]
43
44
# we resize the shape of the activation so we can perform a dot product with
45
# the dominated class weight
46
before_gap_output = np.squeeze(before_gap_output).reshape((-1, n_activation))
47
cam = np.dot(before_gap_output, dominate_class_weight).reshape((img_width, img_height))
48
49
# we reshape it back to the target image size
50
# so we can overlay the class activation map on top of our image later
51
# order 1 = bi-linear interpolation was fast enough for this use-case
52
width_scale_ratio = self.target_size[0] // img_width
53
height_scale_ratio = self.target_size[1] // img_height
54
cam = zoom(cam, (width_scale_ratio, height_scale_ratio), order=1)
55
return cam, predicted_class
56
57
def _read_and_process_img(self, img_path):
58
"""
59
Reads in a single image, resize it to the specified target size
60
and performs the same preprocessing on the image as the original
61
pre-trained model.
62
"""
63
img = load_img(img_path, target_size=self.target_size)
64
img_arr = img_to_array(img)
65
66
# keras works with batches of images, since we only have 1 image
67
# here, we need to add an additional dimension to turn it into
68
# shape [samples, size1, size2, channels]
69
keras_img = np.expand_dims(img_arr, axis=0)
70
71
# different pre-trained model preprocess the images differently
72
# we also preprocess our images the same way to be consistent
73
keras_img = preprocess_input(keras_img)
74
return img, keras_img
75
76
def _get_resnet50_cam_info(self):
77
# we need the output of the activation layer right before the
78
# global average pooling (gap) layer and the last dense/softmax
79
# layer that generates the class prediction
80
before_gap_layer = self.model.layers[-4]
81
class_pred_layer = self.model.layers[-1]
82
83
outputs = before_gap_layer.output, class_pred_layer.output
84
resnet50_cam_layers = Model(inputs=self.model.input, outputs=outputs)
85
86
# only access the first element of weights, we won't be needing the bias term here
87
class_weights = class_pred_layer.get_weights()[0]
88
return resnet50_cam_layers, class_weights
89
90