Шаг 234.
Глубокое обучение на Python. ... . Интерпретация знаний, заключенных в сверточной нейронной сети. Визуализация тепловых карт активации класса

    На этом шаге мы рассмотрим использование тепловой карты в процессе принятия решений сверточной нейронной сети.

    В этом шаге описывается еще один прием визуализации, позволяющий понять, какие части данного изображения помогли сверточной нейронной сети принять окончательное решение о его классификации. Это полезно для отладки процесса принятия решений в сверточной нейронной сети, особенно в случае ошибок классификации (данная предметная область называется интерпретируемостью модели). Он также помогает определить местоположение конкретных объектов на изображении.

    Категория методов, описываемых здесь, называется визуализацией карты активации класса (Class Activation Map, CAM). Их суть заключается в создании тепловых карт активации класса для входных изображений. Тепловая карта активации класса - это двумерная сетка оценок, связанных с конкретным выходным классом и вычисляемых для каждого местоположения в любом входном изображении. Эти оценки определяют, насколько важно каждое местоположение для рассматриваемого класса. Например, для изображения, передаваемого в сверточную нейронную сеть, которая осуществляет классификацию кошек и собак, визуализация CAM позволяет сгенерировать тепловые карты для классов "кошка" и "собака", показывающие, насколько важными являются разные части изображения для этих классов.

    Далее мы будем использовать реализацию, описанную в статье Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization.


Selvaraju R. R. et al. arXiv, 2017, https://arxiv.org/abs/1610.02391.
Она очень проста: отобразить карту признаков для входного изображения, полученную на выходе сверточного слоя, и взвесить каждый канал в ней по градиенту класса для данного канала. Проще говоря, этот трюк заключается во взвешивании признаков в пространственной карте "как интенсивно входное изображение активирует разные каналы" по признаку "насколько важен каждый канал для данного класса". В результате получается пространственная карта признаков "как интенсивно входное изображение активирует класс".

    Продемонстрируем этот прием с использованием предварительно обученной сети Xception.


Пример 9.20. Загрузка предварительно обученной сети Xception
from tensorflow import keras

# Обратите внимание на то, что мы добавили сверху полносвязный классификатор; 
# во всех предыдущих случаях мы отбрасывали его
model = keras.applications.xception.Xception(weights="imagenet")

    Рассмотрим фотографию двух африканских слонов на рисунке 1, на которой изображены самка и ее слоненок, прогуливающиеся по саванне.


Рис.1. Контрольная фотография африканских слонов

    Преобразуем эту фотографию в форму, которую сможет прочитать модель Xception. Модель обучена на изображениях размерами 299 × 299, предварительно обработанных в соответствии с правилами, реализованными в функции keras.applications.xception.preprocess_input(), а потому мы также должны привести фотографию к размерам 299 × 299, преобразовать ее в тензор NumPy с числами типа float32 и применить правила предварительной обработки.


Пример 9.21. Предварительная обработка входного изображения для передачи в модель Xception
import numpy as np

img_path = "elephant.jpg"

def get_img_array(img_path, target_size): 
  # Изображение 299 × 299 в формате Python Imaging Library (PIL)
  img = keras.utils.load_img(img_path, target_size=target_size) 
  # Массив NumPy с числами типа float32, имеющий форму (299, 299, 3)
  array = keras.utils.img_to_array(img) 
  # обавление размерности для преобразования массива в 
  # пакет с формой (1, 299, 299, 3)
  array = np.expand_dims(array, axis=0) 
  # Предварительная обработка пакета (нормализация каналов цвета)
  array = keras.applications.xception.preprocess_input(array) 
  return array

img_array = get_img_array(img_path, target_size=(299, 299))

    Теперь можно передать изображение в предварительно обученную сеть и декодировать полученный вектор в удобочитаемый формат:


preds = model.predict(img_array)
print(keras.applications.xception.decode_predictions(preds, top=3)[0])

[('n02504458', 'African_elephant', np.float32(0.80115944)), 
 ('n01871265', 'tusker', np.float32(0.12182931)), 
 ('n02504013', 'Indian_elephant', np.float32(0.0253264))]

    Вот первые три прогнозируемых класса для данного изображения:

    Сеть распознала на изображении неопределенное количество африканских слонов. Элемент в векторе прогнозов с максимальной активацией соответствует классу African elephant (африканский слон) с индексом 386:


print(np.argmax(preds[0]))

386

    Для визуализации части изображения, наиболее соответствующей классу "африканский слон", выполним процедуру Grad-CAM.

    Прежде всего подготовим модель, отображающую входное изображение в активации последнего сверточного слоя.


Пример 9.22. Подготовка модели, возвращающей вывод последнего сверточного слоя
last_conv_layer_name = "block14_sepconv2_act"
classifier_layer_names = [
    "avg_pool",
    "predictions",
]
last_conv_layer = model.get_layer(last_conv_layer_name)
last_conv_layer_model = keras.Model(model.inputs, last_conv_layer.output)

    Затем создадим модель, отображающую активации последнего сверточного слоя, чтобы получить прогнозы классов.


Пример 9.23. Повторное применение классификатора к результату последнего сверточного слоя
classifier_input = keras.Input(shape=last_conv_layer.output.shape[1:])
x = classifier_input
for layer_name in classifier_layer_names:
  x = model.get_layer(layer_name)(x)
classifier_model = keras.Model(classifier_input, x)

    Вычислим градиент для наиболее вероятного класса входного изображения с учетом активаций последнего сверточного слоя.


Пример 9.24. Получение градиентов для наиболее вероятного класса
import tensorflow as tf

with tf.GradientTape() as tape:
  # Вычисление активаций последнего сверточного слоя и передача их 
  # для наблюдения объекту GradientTape
  last_conv_layer_output = last_conv_layer_model(img_array) 
  tape.watch(last_conv_layer_output) 
  # Канал активации, соответствующий наиболее вероятному классу
  preds = classifier_model(last_conv_layer_output) 
  top_pred_index = tf.argmax(preds[0]) 
  top_class_channel = preds[:, top_pred_index]

# Градиент наиболее вероятного класса согласно выходной карте 
# признаков последнего сверточного слоя
grads = tape.gradient(top_class_channel, last_conv_layer_output)

    Теперь применим объединение и взвесим по важности тензор градиентов, чтобы получить тепловую карту активации класса.


Пример 9.25. Объединение и взвешивание по важности тензора градиентов
# Вектор, каждый элемент которого представляет среднюю интенсивность 
# градиента для данного канала. Он количественно оценивает важность каждого 
# канала для наиболее вероятного класса
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)).numpy()
last_conv_layer_output = last_conv_layer_output.numpy()[0]
for i in range(pooled_grads.shape[-1]): 
  # Умножить каждый канал в выводе последнего сверточного слоя на 
  # оценку "важность этого канала"
  last_conv_layer_output[:, :, i] *= pooled_grads[i]
# Среднее для каналов в полученной карте признаков - это тепловая 
# карта активации класса
heatmap = np.mean(last_conv_layer_output, axis=-1)

    Для нужд визуализации нормализуем тепловую карту, приведя значения в ней к диапазону от 0 до 1. Результат показан на рисунке 2.


Пример 9.26. Заключительная обработка тепловой карты
import matplotlib.pyplot as plt

heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)


Рис.2. Тепловая карта активации для отдельного класса

    В заключение сгенерируем изображение, наложив тепловую карту на фотографию слонов (рисунок 3).


Пример 9.27. Наложение тепловой карты на исходное изображение
import matplotlib.cm as cm

# Загрузка исходного изображения
img = keras.utils.load_img(img_path)
img = keras.utils.img_to_array(img)

# Масштабирование тепловой карты в диапазон 0-255
heatmap = np.uint8(255 * heatmap)

# Использование "струйной" (jet) цветовой карты для раскрашивания 
# тепловой карты
jet = cm.get_cmap("jet")
jet_colors = jet(np.arange(256))[:, :3]

# Создание изображения с раскрашенной тепловой картой
jet_heatmap = jet_colors[heatmap]
jet_heatmap = keras.utils.array_to_img(jet_heatmap)
jet_heatmap = jet_heatmap.resize((img.shape[1], img.shape[0]))
jet_heatmap = keras.utils.img_to_array(jet_heatmap)

# Наложение тепловой карты на оригинальную фотографию с уровнем 
# прозрачности тепловой карты 40%
superimposed_img = jet_heatmap * 0.4 + img
superimposed_img = keras.utils.array_to_img(superimposed_img)

# Сохранение полученного изображения
save_path = "elephant_cam.jpg"
superimposed_img.save(save_path)

Блокнот с этим примером и файлами можно взять здесь.


Рис.3. Оригинальная фотография с наложенной тепловой картой активации класса "африканский слон"

    Этот прием визуализации помогает ответить на два важных вопроса.

    Интересно отметить, что уши слоненка оказались сильно активированы: вероятно, именно по этому признаку сеть отличает африканских слонов от индийских.

    На следующем шаге мы подведем итоги по изученному материалу.




Предыдущий шаг Содержание Следующий шаг