На этом шаге мы продолжим изучение этого вопроса.
Для извлечения карт признаков, подлежащих визуализации, создадим модель Keras, которая принимает пакеты изображений и выводит активации всех сверточных и объединяющих слоев.
from tensorflow.keras import layers layer_outputs = [] layer_names = [] for layer in model.layers: # Извлечь выход всех слоев Conv2D и MaxPooling2D if isinstance(layer, (layers.Conv2D, layers.MaxPooling2D)): # Сохранить имена слоев для последующего использования layer_outputs.append(layer.output) layer_names.append(layer.name) # Создать модель, возвращающую выходы с учетом заданного входа activation_model = keras.Model(inputs=model.input, outputs=layer_outputs)
Если передать этой модели изображение, она вернет значения активации слоев в исходной модели. Это первый пример модели с несколькими выходами в нашем изложении, который встречается вам на практике: все представленные ранее модели имели ровно один вход и один выход. В частности, данная модель имеет один вход и девять выходов: по одному на каждую активацию слоя.
# Вернет список с девятью массивами NumPy: # по одному массиву на каждую активацию слоя activations = activation_model.predict(img_tensor)
Возьмем для примера активацию первого сверточного слоя для входного изображения кошки:
first_layer_activation = activations[0]
print(first_layer_activation.shape)
(1, 178, 178, 32)
Это карта признаков 178 × 178 с 32 каналами. Попробуем отобразить четвертый канал активации первого слоя оригинальной модели (рисунок 1).
import matplotlib.pyplot as plt plt.matshow(first_layer_activation[0, :, :, 4], cmap="viridis")
Рис.1. Четвертый канал активации первого слоя для контрольного изображения кошки
Похоже, что этот канал представляет собой диагональный детектор контуров - но имейте в виду, что у вас каналы могут отличаться, потому что обучение конкретных фильтров не является детерминированной операцией.
Теперь построим полную визуализацию всех активаций в сети (рисунок 2). Для этого извлечем и отобразим каналы активации всех слоев, поместив результаты в одну большую сетку с изображениями.
images_per_row = 16 # Цикл по активациям (и именам слоев) for layer_name, layer_activation in zip(layer_names, activations): # Активация слоя имеет форму (1, size, size, n_features) n_features = layer_activation.shape[-1] size = layer_activation.shape[1] n_cols = n_features // images_per_row # Подготовить пустую сетку для отображения всех каналов этой активации display_grid = np.zeros(((size + 1) * n_cols - 1, images_per_row * (size + 1) - 1)) for col in range(n_cols): for row in range(images_per_row): channel_index = col * images_per_row + row # Это единственный канал (признак) channel_image = layer_activation[0, :, :, channel_index].copy() # Нормализовать значения канала, приведя их к диапазону [0, 255]. # Все нулевые каналы остаются нулевыми if channel_image.sum() != 0: channel_image -= channel_image.mean() channel_image /= channel_image.std() channel_image *= 64 channel_image += 128 channel_image = np.clip(channel_image, 0, 255).astype("uint8") display_grid[ # Поместить матрицу канала в подготовленную пустую сетку col * (size + 1):(col + 1) * size + col, row * (size + 1):(row + 1) * size + row] = channel_image # Отобразить сетку для слоя scale = 1. / size plt.figure(figsize=(scale * display_grid.shape[1], scale * display_grid.shape[0])) plt.title(layer_name) plt.grid(False) plt.axis("off") plt.imshow(display_grid, aspect="auto", cmap="viridis")
Рис.2. Все каналы всех активаций слоев для контрольного изображения кошки (изображение кликабельно)
Вот несколько замечаний к полученным результатам.
Мы только что рассмотрели важную универсальную характеристику представлений, создаваемых глубокими нейронными сетями: признаки, извлекаемые слоями, становятся все более абстрактными с глубиной слоя. Активации на верхних слоях содержат все меньше и меньше информации о конкретном входном изображении и все больше и больше - о цели (в данном случае о классе изображения - кошка или собака). Глубокая нейронная сеть фактически действует как конвейер очистки информации, который получает неочищенные исходные данные (в данном случае изображения в формате RGB) и подвергает их многократным преобразованиям, фильтруя ненужную информацию (например, конкретный внешний вид изображения) и оставляя и очищая нужную (например, класс изображения).
Примерно так же люди и животные воспринимают окружающий мир: понаблюдав сцену в течение нескольких секунд, человек запоминает, какие абстрактные объекты присутствуют в ней (велосипед, дерево), но не запоминает всех деталей внешнего вида этих объектов. Фактически при попытке нарисовать велосипед по памяти, скорее всего, вам не удастся получить более или менее правильное изображение даже притом, что вы могли видеть велосипеды тысячи раз (см. примеры на рисунке 3).
Рис.3. Слева: попытки нарисовать велосипед по памяти. Справа: так должен бы выглядеть схематичный рисунок велосипеда
Попробуйте сделать это прямо сейчас, и вы убедитесь в справедливости сказанного. Ваш мозг научился полностью абстрагировать видимую картинку, получаемую на входе, и преобразовывать ее в высокоуровневые визуальные понятия, фильтруя при этом неважные визуальные детали и затрудняя тем самым их запоминание.
На следующем шаге мы рассмотрим визуализацию фильтров сверточных нейронных сетей.