На этом шаге мы рассмотрим использование таких фильтров.
Другой простой способ исследовать фильтры, полученные сетью, - отобразить визуальный шаблон, за который отвечает каждый фильтр. Это можно сделать методом градиентного восхождения в пространстве входов (gradient ascent in input space): выполняя градиентный спуск до значения входного изображения сверточной нейронной сети, максимизируя отклик конкретного фильтра, начав с пустого изображения. В результате получится версия входного изображения, для которого отклик данного фильтра был бы максимальным.
Попробуем проделать это с фильтрами модели Xception, обученной на наборе данных ImageNet. Задача решается просто: нужно сконструировать функцию потерь, максимизирующую значение данного фильтра данного сверточного слоя, и затем использовать стохастический градиентный спуск для настройки значений входного изображения, чтобы максимизировать значение активации. Это будет наш второй пример реализации цикла низкоуровневого градиентного спуска с использованием объекта GradientTape (первый был показан на 54 шаге).
Для начала создадим экземпляр модели Xception, загрузив веса, полученные при обучении на наборе данных ImageNet
from tensorflow import keras model = keras.applications.xception.Xception( weights="imagenet", # Слои классификации в этом варианте использования модели # не нужны, поэтому отключим их include_top=False)
Нас интересуют сверточные слои модели - Conv2D и SeparableConv2D. Но, чтобы получить их результаты, нужно знать имена слоев. Давайте выведем эти имена в порядке увеличения глубины.
for layer in model.layers: if isinstance(layer, (keras.layers.Conv2D, keras.layers.SeparableConv2D)): print(layer.name) block1_conv1 block1_conv2 block2_sepconv1 block2_sepconv2 conv2d block3_sepconv1 block3_sepconv2 conv2d_1 block4_sepconv1 block4_sepconv2 conv2d_2 block5_sepconv1 block5_sepconv2 block5_sepconv3 block6_sepconv1 block6_sepconv2 block6_sepconv3 block7_sepconv1 block7_sepconv2 block7_sepconv3 block8_sepconv1 block8_sepconv2 block8_sepconv3 block9_sepconv1 block9_sepconv2 block9_sepconv3 block10_sepconv1 block10_sepconv2 block10_sepconv3 block11_sepconv1 block11_sepconv2 block11_sepconv3 block12_sepconv1 block12_sepconv2 block12_sepconv3 block13_sepconv1 block13_sepconv2 conv2d_3 block14_sepconv1 block14_sepconv2
Обратите внимание, что все слои SeparableConv2D получили имена вида block6_sepconv1, block7_sepconv2 и т. д. Модель Xception организована в блоки, каждый из которых содержит несколько сверточных слоев.
Теперь создадим вторую модель, которая вернет выходные данные определенного слоя, - модель экстрактора признаков. Поскольку наша модель создается с применением функционального API, ее можно проверить: запросить output одного из слоев и повторно использовать его в новой модели. Нет необходимости копировать весь код Xception.
# Эту строку можно заменить именем любого слоя в сверточной основе Xception layer_name = "block3_sepconv1" # Объект слоя, который нас интересует layer = model.get_layer(name=layer_name) # Мы используем model.input и layer.output для создания модели, # которая возвращает выход целевого слоя feature_extractor = keras.Model(inputs=model.input, outputs=layer.output)
Чтобы использовать эту модель, просто передайте ей некоторые входные данные (обратите внимание, что модель Xception требует предварительной обработки входных данных с помощью функции keras.applications.xception.preprocess_input()).
activation = feature_extractor( keras.applications.xception.preprocess_input(img_tensor) )
Воспользуемся нашей моделью экстрактора признаков, чтобы определить функцию, возвращающую скалярное значение, которое количественно определяет, насколько данное входное изображение активирует данный фильтр в слое. Это функция потерь, которую мы максимизируем в процессе градиентного восхождения:
import tensorflow as tf # Функция потерь принимает тензор с изображением и индекс фильтра (целое число) def compute_loss(image, filter_index): activation = feature_extractor(image) # Обратите внимание: исключая из вычисления потерь пиксели, лежащие на # границах, мы избегаем пограничных артефактов; в данном случае мы # отбрасываем первые два пикселя по сторонам активации filter_activation = activation[:, 2:-2, 2:-2, filter_index] # Вернуть среднее значений активации для фильтра return tf.reduce_mean(filter_activation)
В предыдущих шагах для извлечения признаков мы использовали predict(x). Здесь мы берем model(x). Почему?
Оба вызова, y = model.predict(x) и y = model(x), где x - массив входных данных, подразумевают "запуск модели с исходными данными x и получение результата y". Но в обоих случаях данная формулировка обозначает не совсем одно и то же.
Метод predict() перебирает данные (при желании можно указать размер пакета, выполнив вызов predict(x, batch_size=64)) и извлекает массив NumPy с выходными данными. Схематично его реализацию можно представить так:
def predict(x): y_batches = [] for x_batch in get_batches(x): y_batch = model(x).numpy() y_batches.append(y_batch) return np.concatenate(y_batches)
Таким образом, вызовы predict() могут обрабатывать очень большие массивы. Между тем model(x) выполняет обработку в памяти и не масштабируется. В то же время predict() не дифференцируется: нельзя получить его градиент, вызывая в контексте GradientTape.
Если нужно получить градиенты вызовов модели, используйте model(x); если нужен только результат применения модели - берите predict(). Иными словами, predict() будет полезен во всех случаях, кроме реализации цикла низкоуровневого градиентного спуска (как сейчас).
На следующем шаге мы закончим изучение этого вопроса.