Skip to content

Instantly share code, notes, and snippets.

@rhee-elten
Last active September 3, 2024 03:52
Show Gist options
  • Save rhee-elten/ca633edcf039069015e8ccd7faaa099a to your computer and use it in GitHub Desktop.
Save rhee-elten/ca633edcf039069015e8ccd7faaa099a to your computer and use it in GitHub Desktop.
keras gradcam 예제
#!/usr/bin/env python
# coding: utf-8
# In[1]:
## gist: https://gist.github.com/rhee-elten/ca633edcf039069015e8ccd7faaa099a
## keras 모델로 p1 데이터셋을 학습하고, 테스트 데이터셋에 대한 gradcam 을 추출
## 93.87% 94.86% VAL AUC 최고치 였던 저장 파일 사용
_backbone = 'DenseNet169'
_pretrained = 'save/p1-keras-9/checkpoints_/best_model.053-0.114.hdf5'
_image_files_dir = 'data/p1/650x900'
_image_size = 900, 650
# In[2]:
# %env TF_FORCE_GPU_ALLOW_GROWTH=true
# In[3]:
get_ipython().run_line_magic('matplotlib', 'inline')
# In[4]:
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (4.5, 3.25)
plt.rcParams['figure.dpi'] = 100
plt.rcParams['image.cmap'] = 'gray'
# In[5]:
## 미지정 파라메터의 디폴트 값 설정
def def_global(name, default_value, type=None, comment=None):
if name not in globals():
globals()[name] = default_value
print('{:>22s}: {:64.64s}'.format(name, repr(globals()[name])), flush=True)
# _save_dir 또는 checkpoint .hdf5 파일 경로를 지정
def_global('_pretrained', None)
def_global('_image_files_dir', None)
def_global('_image_size', None) # (900, 650))
def_global('_num_classes', 2)
# weight 만 저장된 파일을 읽어오는 경우 아래 규칙으로 모델을 먼저 빌드함.
def_global('_backbone', None)
def_global('_input_layer_name','input_1')
def_global('_inner_model_name', None) # 'densenet169')
def_global('_last_conv_layer_name', None) # 'conv5_block32_concat')
def_global('_gradcam_examples', 20)
def_global('_seed', 4223585)
## _inner_model_name 를 지정하지 않으면, _backbone 으로 부터 추측
if _inner_model_name is None:
_inner_model_name = _backbone.lower()
print('_inner_model_name:',_inner_model_name)
## _inner_model_name 를 지정하지 않으면, _inner_model_name 으로 부터 추측
if _last_conv_layer_name is None:
_last_conv_layer_name = {
'densenet169': 'conv5_block32_concat',
'resnet101v2': 'conv5_block3_out',
'resnet50v2': 'conv5_block3_out',
}[_inner_model_name]
print('_last_conv_layer_name:', _last_conv_layer_name)
# In[ ]:
# In[ ]:
# In[ ]:
# In[6]:
"""
keras 로 학습하고 저장한 모델 파일을 로딩하는 코드
"""
import sys
import numpy as np
import pandas as pd
from glob import glob
import tensorflow as tf
K = tf.compat.v1.keras.backend
try:
from tensorflow.python.framework.ops import disable_eager_execution
disable_eager_execution()
tf.compat.v1.disable_v2_behavior()
except:
_, ex_value, _ = sys.exc_info()
print(repr(ex_value), file=sys.stderr)
tf.compat.v1.disable_eager_execution()
from importlib import import_module
from inspect import getmembers
from tensorflow.keras import Sequential
from tensorflow.keras.initializers import TruncatedNormal
from tensorflow.keras.layers import Activation, Conv2D, Dense, Input, Layer
from tensorflow.keras.models import Model
from tensorflow.keras.regularizers import l2 as l2_regularizer
##################################################
class Scaler(Layer):
@tf.function
def call(self, inp):
return tf.cast(inp, tf.float32) / 255.0 * 2.0 - 1.0
## 구 버젼으로 저장한 weights 파일 지원을 위해 생성
class Resizer(Layer):
@tf.function
def call(self, inp):
resize_area = tf.compat.v1.image.resize_area
return resize_area(inp, (_image_size[1], _image_size[0]))
def build_model(backbone, input_shape, num_classes, _exclude_top=0):
input_ = Input(shape=input_shape)
layer = Scaler()(input_)
if _exclude_top:
keras_apps = import_module("tensorflow.keras.applications")
network_fn = dict(getmembers(keras_apps))[backbone]
mdl_ = network_fn(
input_shape=input_shape,
include_top=False,
pooling="avg",
)
layer = mdl_(layer)
layer = Dense(units=num_classes, activation=None, name="logits")(layer)
output = Activation("softmax")(layer)
else:
keras_apps = import_module("tensorflow.keras.applications")
network_fn = dict(getmembers(keras_apps))[backbone]
mdl_ = network_fn(
input_shape=input_shape,
weights=None,
pooling="avg",
classes=num_classes,
)
output = mdl_(layer)
mdl = Model(input_, output)
mdl.compile(tf.keras.optimizers.SGD(), "sparse_categorical_crossentropy", ["acc"])
return mdl
custom_objects = {
"Scaler": Scaler,
"Resizer": Resizer,
}
def load_model(_pretrained, _image_size=_image_size, custom_objects=custom_objects):
## 1차 시도 - load_model() 을 이용하여 모델과 weights 를 로딩
try:
mdl = tf.keras.models.load_model(_pretrained, custom_objects=custom_objects)
print("\n")
print("============================================================")
print("== model loaded using tf.keras.models.load_model()")
print("============================================================")
print("\n", flush=True)
return mdl
except ValueError: # ValueError: Unknown layer: Functional
_, ex_value, _ = sys.exc_info()
print("***** load_model() failed by:", ex_value)
## 2차 시도 - exclude_top=0 으로 하여 모델을 생성하고, weights 를 로딩
try:
mdl = build_model(_backbone, (_image_size[1], _image_size[0], 3), _num_classes)
mdl.load_weights(_pretrained)
print("\n")
print("============================================================")
print("== model loaded using model.load_weights() (exclude_top=0)")
print("============================================================")
print("\n", flush=True)
return mdl
except (
ValueError
): ## "ValueError: You are trying to load a weight file containing 2 layers into a model with 1 layers."
_, ex_value, _ = sys.exc_info()
print("***** load_model(exclude_top=0) failed by:", ex_value)
## 3차 시도 - exclude_top=1 로 하여 모델을 생성하고, weights 를 로딩
try:
mdl = build_model(
_backbone, (_image_size[1], _image_size[0], 3), _num_classes, _exclude_top=1
)
mdl.load_weights(_pretrained)
print("\n")
print("============================================================")
print("== model loaded using model.load_weights() (exclude_top=1)")
print("============================================================")
print("\n", flush=True)
return mdl
except ValueError:
_, ex_value, _ = sys.exc_info()
print("***** load_model(exclude_top=1) failed by:", ex_value)
assert 0, "model load failed"
return None
# In[7]:
"""
지정된 테스트 데이터셋 이미지를 읽어오고,
파일명 규칙에 따라 레이블을 설정하는 코드
"""
import re
from glob import glob
from os.path import basename, splitext
import cv2
def read_dataset(test_files_dir, image_size=None, label_fn=None):
if label_fn is None:
def label_fn(x):
b = splitext(basename(x))[0]
m = re.match(
r'^(\d{9})_([abcd])_(right|left)_([012])(-(.*))?$', b)
assert m, b
pid, compo, side, mal, aug = m.group(1, 2, 3, 4, 6)
mal = int(mal)
yval = int(mal == 2)
return yval
test_file_names = glob(test_files_dir+'/*.jpg')
test_file_names += glob(test_files_dir+'/*.png')
test_file_names = sorted(test_file_names)
assert(len(test_file_names) > 0)
test_files = []
for x in test_file_names:
if x.strip():
yval = label_fn(x)
test_files.append((x, yval))
print('found:', len(test_files))
num_files = len(test_files)
X = None
if image_size is not None:
X = np.zeros((num_files, image_size[1], image_size[0], 3), 'uint8')
y = np.zeros((num_files,), 'int')
for i, itm in enumerate(test_files):
pth, yval = itm
im = cv2.imread(pth, flags=cv2.IMREAD_COLOR)
h, w = im.shape[:2]
if X is None:
X = np.zeros((num_files, h, w, 3), 'uint8')
image_size = X.shape[2], X.shape[1]
if h != image_size[1] or w != image_size[0]:
im = cv2.resize(im, image_size, interpolation=cv2.INTER_AREA)
X[i, ...] = im
y[i] = yval
print('X:', X.shape, 'y:', y.shape)
return X, y, test_file_names
# In[ ]:
# In[ ]:
# In[ ]:
# In[8]:
"""
로딩된 모델에서,
지정된 레이어 (모델 마다 다름) 의 출력을 추출하여
gradcam heatmap 을 만드는 함수를 생성하는 코드
"""
def build_gradcam_fn(
model,
last_conv_layer_name=_last_conv_layer_name,
inner_model_name=_inner_model_name,
input_layer_name=_input_layer_name,
):
# First, we create a model that maps the input image to the activations
# of the last conv layer as well as the output predictions
try:
## inner_model 이 지정된 경우 inner 모델 내부의 last_conv_layer 선택
inner_model = model.get_layer(inner_model_name)
last_conv_layer = inner_model.get_layer(last_conv_layer_name).get_output_at(1)
except:
_, ex_value, _ = sys.exc_info()
print("***** get innner_model failed:", ex_value)
## inner_model 이 없는 경우 바깥쪽 모델의 last_conv_layer 선택
last_conv_layer = model.get_layer(last_conv_layer_name).output
print("last_conv_layer.shape:", last_conv_layer.get_shape())
## 특정 클래스 (OK,NG 등등) 를 선택할 수 있는
## placeholder 를 네트워크에 추가
print("model.output.shape", model.output.get_shape())
pred_index_ph = tf.compat.v1.placeholder(tf.int64)
class_channel = model.output[:, pred_index_ph]
## last_conv_layer를 구성하는 각각의 값들이
## 최종 출력값에 미치는 영향을
## 계산하기 위해서 grdients 값 추출
print(
"getting tf.gradients...:",
class_channel.get_shape(),
last_conv_layer.get_shape(),
)
grads = tf.gradients(class_channel, last_conv_layer)
if grads is None:
print("tf.gradients: grads is None")
if grads[0] is None:
print("tf.gradients: grads[0] is None")
print("get_shape(grads)", tuple(g.get_shape() for g in grads))
# This is a vector where each entry is the mean intensity of the gradient
# over a specific feature map channel
pooled_grads = tf.reduce_mean(grads[0], axis=(0, 1, 2))
print("reduce_mean(grads[0], (0,1,2)).shape:", pooled_grads.get_shape())
# We multiply each channel in the feature map array
# by "how important this channel is" with regard to the top predicted class
# then sum all the channels to obtain the heatmap class activation
print("last_conv_layer[0].shape:", last_conv_layer[0].get_shape())
print(
"pooled_grads[..., tf.newaxis].shape:",
pooled_grads[..., tf.newaxis].get_shape(),
)
heatmap = last_conv_layer[0] @ pooled_grads[..., tf.newaxis]
print("heatmap.shape:", heatmap.get_shape())
heatmap = tf.squeeze(heatmap)
print("heatmap.shape squeezed:", heatmap.get_shape())
# For visualization purpose, we will also normalize the heatmap between 0 & 1
heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
def gradcam_fn(img_array, pred_index):
return heatmap.eval(
feed_dict={
model.get_layer(input_layer_name).input: img_array,
pred_index_ph: pred_index,
},
session=K.get_session(),
)
return gradcam_fn
# In[9]:
"""
원본 이미지와 생성된 cam heatmap 을
시각화 하여 그리는 함수
"""
def visualize_cam(im, cam, clss, title=None, heatmap_alpha=0.35, figsize=(19.2, 9.6), dpi=100):
print('visualize_cam: im.shape:', im.shape, 'cam.shape:', cam.shape)
h, w = im.shape[:2]
cam_ = cv2.resize(cam, (w, h), interpolation=cv2.INTER_LINEAR_EXACT)
_, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=figsize, dpi=dpi)
ax1.imshow(im)
ax1.axis('off')
if title is not None:
ax1.set_title(title)
ax2.matshow(cam,cmap='jet')
ax2.axis('off')
ax3.imshow(im)
ax3.imshow(cam_, cmap='jet', alpha=heatmap_alpha)
ax3.axis('off')
ax3.set_title('prediction: {}'.format(clss))
plt.tight_layout()
plt.show()
# In[ ]:
# In[ ]:
# In[ ]:
# In[10]:
"""
지정된 모델 로딩
"""
mdl = load_model(_pretrained)
## 적합한 모델인지 검증
layer_names = [lyr.name for lyr in mdl.layers]
if not (_last_conv_layer_name in layer_names or _inner_model_name in layer_names):
mdl.summary()
assert _last_conv_layer_name in layer_names or _inner_model_name in layer_names, (
_last_conv_layer_name,
_inner_model_name,
layer_names,
)
## 모델 구조 출력
mdl.summary()
try:
mdl.layers[-1].summary()
except:
_, ex_value, _ = sys.exc_info()
# for GRADCAM, remove softmax
# tf.identity or tf.keras.activations.linear
try:
print("old activation:", mdl.layers[-1].activation)
mdl.layers[-1].activation = tf.keras.activations.linear
print("patched activation:", mdl.layers[-1].activation)
print("mdl.layers[-1].activation patched")
except:
_, ex_value, _ = sys.exc_info()
print("mdl.layers[-1].activation patch failed:", ex_value)
print("old activation:", mdl.layers[-1].layers[-1].activation)
mdl.layers[-1].layers[-1].activation = tf.keras.activations.linear
print("patched activation:", mdl.layers[-1].layers[-1].activation)
print("mdl.layers[-1].layers[-1].activation patched")
# In[11]:
## gradcam 추출하는 함수 생성
gradcam = build_gradcam_fn(mdl)
# In[ ]:
# In[ ]:
# In[ ]:
# In[12]:
## 데이터셋을 X, y 에 로딩
X, y, _image_files = read_dataset(_image_files_dir, _image_size)
if _image_size is None:
_image_size = X.shape[2], X.shape[1]
## 이상치 데이터만 선택:
## NG (2) 만 선택 (파일명에서 0, 1: OK, 2: MAL)
is_mal = (y == 1)
X_mal = X[is_mal]
y_mal = y[is_mal]
files_mal = np.asarray(_image_files)[is_mal].tolist()
# In[13]:
## X_mal 에서 임의로 몇개만 선택
rng = np.random.default_rng(_seed)
choices = sorted(rng.choice(np.arange(len(y_mal)), size=(_gradcam_examples,), replace=False))
print("choices:", choices)
# In[14]:
from os.path import basename
for n in choices:
img = X_mal[n]
img_clss = y_mal[n]
img_filename = basename(files_mal[n])
title = 'file: {:s}{}, clss: {:d}'.format(
img_filename, img.shape, img_clss)
print(title)
preds = mdl.predict(np.asarray([img]))
preds = preds[0]
pred_index = np.argmax(preds)
with np.printoptions(precision=3, suppress=True):
print('preds:', preds, 'pred:', pred_index)
heatmap = gradcam(np.asarray([img]), pred_index)
visualize_cam(img, heatmap, pred_index, title=title)
# In[ ]:
# In[ ]:
# In[ ]:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment