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

    На этом шаге мы закончим изучение этого вопроса.

    Давайте реализуем функцию градиентного восхождения с помощью GradientTape. Обратите внимание, что для ускорения мы будем использовать декоратор @tf.function.

    Иногда для ускорения процесса градиентного спуска используется неочевидный трюк - нормализация градиентного тензора делением на его L2-норму (квадратный корень из усредненных квадратов значений в тензоре). Это гарантирует, что величина обновлений во входном изображении всегда будет находиться в одном диапазоне.


Пример 9.16. Максимизация потерь методом стохастического градиентного восхождения
@tf.function
def gradient_ascent_step(image, filter_index, learning_rate): 
  with tf.GradientTape() as tape: 
    # Явно передать для наблюдения тензор с изображением, потому что это 
    # не объект Variable (автоматически под наблюдение попадают только 
    # объекты Variable)
    tape.watch(image) 
    # Вычислить скаляр потерь, показывающий, насколько текущее 
    # изображение активирует фильтр
    loss = compute_loss(image, filter_index) 
  # Вычислить градиенты потерь по отношению к изображению
  grads = tape.gradient(loss, image) 
  # Применить "трюк нормализации градиента"
  grads = tf.math.l2_normalize(grads) 
  # Немного сдвинуть изображение в направлении наибольшей 
  # активации целевого фильтра
  image += learning_rate * grads 
  # Вернуть обновленное изображение, чтобы дать возможность 
  # вызывать эту функцию в цикле
  return image

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


Пример 9.17. Функция, которая генерирует изображение, представляющее фильтр
img_width = 200
img_height = 200

def generate_filter_pattern(filter_index): 
  # Количество шагов градиентного восхождения
  iterations = 30 
  # Амплитуда одного шага
  learning_rate = 10. 
  image = tf.random.uniform( 
      minval=0.4, 
      maxval=0.6, 
      # Инициализировать тензор изображения случайными значениями 
      # (модель Xception принимает входные значения в диапазоне [0, 1], 
      # поэтому здесь мы выбираем диапазон с центром в точке 
      # со значением 0,5)
      shape=(1, img_width, img_height, 3)) 
  for i in range(iterations): 
    # В цикле обновлять значения тензора с изображением, 
    # чтобы максимизировать функцию потерь
    image = gradient_ascent_step(image, filter_index, learning_rate) 
  return image[0].numpy()

    Полученный тензор с изображением - это массив с формой (200, 200, 3) и вещественными значениями, которые могут быть нецелочисленными, в диапазоне [0, 255]. Поэтому нужно дополнительно его обработать, чтобы превратить в изображение, пригодное для показа. Сделаем это с помощью простой вспомогательной функции.


Пример 9.18. Вспомогательная функция для преобразования тензора в изображение
import numpy as np

def deprocess_image(image): 
  # Нормализовать значения в тензоре приведением их в диапазон [0, 255]
  image -= image.mean() 
  image /= image.std() 
  image *= 64
  image += 128 
  image = np.clip(image, 0, 255).astype("uint8") 
  # Центрировать результат, чтобы избежать артефактов на границах
  image = image[25:-25, 25:-25, :] 
  return image

    Взглянем на получившееся изображение (рисунок 1):


import matplotlib.pyplot as plt

plt.axis("off")
plt.imshow(deprocess_image(generate_filter_pattern(filter_index=2)))


Рис.1. Шаблон, на который второй канал в слое block3_sepconv1 дает максимальный отклик

    Похоже, что фильтр с индексом 0 в слое block3_sepconv1 отвечает за узор из горизонтальных линий, немного похожий на водную гладь или на мех.

    А теперь самое интересное: мы можем визуализировать все фильтры в слое или даже все фильтры во всех слоях модели.


Пример 9.19. Создание сетки со всеми шаблонами откликов фильтров в слое
all_images = []
# Сгенерировать и сохранить изображения для первых 64 фильтров в слое
for filter_index in range(64): 
  print(f"Processing filter {filter_index}") 
  image = deprocess_image( 
      generate_filter_pattern(filter_index)) 
  all_images.append(image)
  
# Подготовить чистый холст для добавления изображений фильтров
margin = 5
n = 8
cropped_width = img_width - 25 * 2
cropped_height = img_height - 25 * 2
width = n * cropped_width + (n - 1) * margin
height = n * cropped_height + (n - 1) * margin
stitched_filters = np.zeros((width, height, 3))

# Заполнить изображение сохраненными фильтрами
for i in range(n): 
  for j in range(n): 
    image = all_images[i * n + j] 
    row_start = (cropped_width + margin) * i 
    row_end = (cropped_width + margin) * i + cropped_width 
    column_start = (cropped_height + margin) * j 
    column_end = (cropped_height + margin) * j + cropped_height 
        
    stitched_filters[
        row_start: row_end, 
        column_start: column_end, :] = image

# Сохранить холст на диск
keras.utils.save_img( 
    f"filters_for_layer_{layer_name}.png", stitched_filters)

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

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




Рис.2. Некоторые шаблоны фильтров из слоев block2_sepconv1, block4_sepconv1 и block8_sepconv1

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




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