Недавно я наткнулся на главу в книге Франсуа Шоле Глубокое обучение с помощью Python, в которой описывается реализация сопоставления активации классов для сети VGG16. Он реализовал алгоритм с помощью Кераса, поскольку он является создателем библиотеки. Следовательно, моим инстинктом было повторно реализовать алгоритм CAM с использованием PyTorch.

Grad-CAM

Сам алгоритм взят из этой бумаги. Это было отличным дополнением к инструментам анализа компьютерного зрения по одной основной причине. Это дает нам возможность посмотреть, какие именно части изображения повлияли на решение всей модели для конкретного присвоенного ярлыка. Это особенно полезно при анализе неправильно классифицированных образцов. Алгоритм Grad-CAM очень интуитивно понятен и достаточно прост в реализации.

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

В этом посте я собираюсь повторно реализовать алгоритм Grad-CAM, используя PyTorch, и, чтобы сделать его немного интереснее, я собираюсь использовать его с разными архитектурами.

VGG19

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

Стратегия определяется следующим образом:

  • Загрузите модель VGG19
  • Найдите его последний сверточный слой
  • Вычислить наиболее вероятный класс
  • Возьмем градиент логита класса относительно карт активации, которые мы только что получили.
  • Объедините градиенты
  • Оцените каналы карты по соответствующим объединенным градиентам
  • Интерполировать тепловую карту

Я отложил несколько изображений (включая изображения слонов, которые Чолле использовал в своей книге) из набора данных ImageNet, чтобы исследовать алгоритм. Я также применил Grad-CAM к некоторым фотографиям из моего Facebook, чтобы увидеть, как алгоритм работает в «полевых» условиях. Вот оригинальные изображения, с которыми мы будем работать:

Хорошо, давайте загрузим модель VGG19 из модуля torchvision и подготовим преобразования и загрузчик данных:

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

А вот и самая сложная часть (самая сложная во всем, но не слишком сложная). Мы можем вычислить градиенты в PyTorch, используя метод .backward(), вызываемый для torch.Tensor. Это именно то, что я собираюсь сделать: я собираюсь вызвать backward() в наиболее вероятном логите, который я получаю, выполняя прямой проход изображения по сети. Однако PyTorch кэширует только градиенты конечных узлов в вычислительном графе, такие как веса, смещения и другие параметры. Градиенты выходных данных относительно активаций являются просто промежуточными значениями и отбрасываются как как только градиент распространяется по ним на обратном пути. Итак, какие у нас есть варианты?

Hook ‘Em

Если вы закончили Техасский университет в Остине, как и я, вам понравится эта часть. В PyTorch есть инструмент обратного вызова: хуки. Хуки можно использовать в разных сценариях, наш - один из них. Эта часть документации PyTorch говорит нам, как именно прикрепить крючок к нашим промежуточным значениям, чтобы вытащить градиенты из модели, прежде чем они будут отброшены. В документации говорится:

Хук будет вызываться каждый раз, когда вычисляется градиент относительно Tensor.

Теперь мы знаем, что нам нужно зарегистрировать обратный хук для карты активации последнего сверточного слоя в нашей модели VGG19. Найдем, где зацепить.

Мы можем легко наблюдать архитектуру VGG19, вызвав vgg19(pretrained=True):

Предварительно обученные модели в PyTorch интенсивно используют Sequential() модули, что в большинстве случаев затрудняет их анализ, мы увидим этот пример позже.

На изображении мы видим всю архитектуру VGG19. Я выделил последний сверточный слой в функциональном блоке (включая функцию активации). Что ж, теперь мы знаем, что хотим зарегистрировать обратную ловушку на 35-м уровне функционального блока нашей сети. Это именно то, что я собираюсь сделать. Также стоит упомянуть, что необходимо зарегистрировать ловушку внутри метода forward(), чтобы избежать проблемы регистрации ловушки на дублирующий тензор и, следовательно, потери градиента.

Как видите, в функциональном блоке остался оставшийся максимальный уровень объединения, не беспокойтесь, я добавлю этот слой в метод forward().

Пока это выглядит великолепно, мы, наконец, можем получить наши градиенты и активации из модели.

Рисование CAM

Во-первых, давайте проведем прямой проход через сеть с изображением слонов и посмотрим, что предсказывает VGG19. Не забудьте перевести свою модель в режим оценки, иначе вы можете получить очень случайные результаты:

Как и ожидалось, мы получаем те же результаты, что и Чолле в своей книге:

Predicted: [('n02504458', 'African_elephant', 20.891441), ('n01871265', 'tusker', 18.035757), ('n02504013', 'Indian_elephant', 15.153353)]

Теперь мы собираемся выполнить обратное распространение с помощью логита 386-го класса, который представляет «африканского слона» в наборе данных ImageNet.

Наконец, мы получили тепловую карту для изображения слона. Это одноканальное изображение 14x14. Размер определяется пространственными размерами карт активации в последнем сверточном слое сети.

Теперь мы можем использовать OpenCV для интерполяции тепловой карты и проецирования ее на исходное изображение, здесь я использовал код из книги Шолле:

На изображении ниже мы видим области изображения, которые наша сеть VGG19 восприняла наиболее серьезно при принятии решения, какой класс («Африканский слон») назначить изображению. Можно предположить, что сеть приняла форму головы и ушей слонов, что является верным признаком присутствия слона на изображении. Что еще более интересно, сеть также провела различие между африканским слоном, слоном клыкастым и индийским слоном. Я не специалист по слонам, но полагаю, что форма ушей и клыков - довольно хороший критерий различия. В общем, именно так человек подошел бы к такой задаче. Эксперт изучит форму ушей и клыков, возможно, некоторые другие тонкие особенности, которые могут пролить свет на то, что это за слон.

Хорошо, давайте повторим ту же процедуру с другими изображениями.

Акулы в основном идентифицируются по области рта / зубов на верхнем изображении и по форме тела и окружающей воде на нижнем изображении. Довольно круто!

Выходя за рамки VGG

VGG - отличная архитектура, однако с тех пор исследователи разработали новые и более эффективные архитектуры для классификации изображений. В этой части мы исследуем одну из таких архитектур: DenseNet.

Есть некоторые проблемы, с которыми я столкнулся при попытке реализовать Grad-CAM для плотно подключенной сети. Во-первых, как я уже упоминал, предварительно обученные модели из зоопарка моделей PyTorch в основном построены из вложенных блоков. Это отличный выбор для удобочитаемости и эффективности; однако возникает проблема с разделением таких вложенных сетей. Обратите внимание, что VGG состоит из 2 блоков: функционального блока и полностью подключенного классификатора. DenseNet состоит из нескольких вложенных блоков, и пытаться добраться до карт активации последнего сверточного слоя нецелесообразно. Есть два способа обойти эту проблему: мы можем взять последнюю карту активации с соответствующим слоем пакетной нормализации. Как мы вскоре увидим, это дает довольно хорошие результаты. Второе, что мы могли бы сделать, - это построить DenseNet с нуля и заново указать веса блоков / слоев, чтобы мы могли напрямую обращаться к слоям. Второй подход кажется слишком сложным и трудоемким, поэтому я его избегал.

Код для DenseNet CAM почти идентичен тому, который я использовал для сети VGG, единственная разница заключается в индексе слоя (блок в случае DenseNet), из которого мы собираемся получать наши активации:

Важно следовать архитектуре DenseNet, поэтому я добавил глобальный средний пул в сеть перед классификатором (вы всегда можете найти эти руководства в исходных документах).

Я собираюсь передать оба изображения игуаны через нашу плотно связанную сеть, чтобы найти класс, который был назначен изображениям:

Predicted: [('n01698640', 'American_alligator', 14.080595), ('n03000684', 'chain_saw', 13.87465), ('n01440764', 'tench', 13.023708)]

Здесь сеть предсказала, что это изображение «американского аллигатора». Хм, давайте запустим наш алгоритм Grad-CAM против класса «Американский аллигатор». На изображениях ниже я показываю тепловую карту и проекцию тепловой карты на изображение. Мы видим, что сеть в основном смотрела на «тварь». Очевидно, что аллигаторы могут выглядеть как игуаны, поскольку у них обоих одинаковая форма тела и общая структура.

Однако обратите внимание, что есть еще одна часть изображения, которая повлияла на оценки в классе. Фотограф на снимке может сбить с толку своим положением и позой. При выборе модель учитывала и игуану, и человека. Посмотрим, что будет, если мы вырежем фотографа из изображения. Вот 3 лучших прогноза для обрезанного изображения:

Predicted: [('n01677366', 'common_iguana', 13.84251), ('n01644900', 'tailed_frog', 11.90448), ('n01675722', 'banded_gecko', 10.639269)]

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

Вторая игуана была классифицирована правильно, и вот соответствующие тепловая карта и проекция.

Выходя за рамки ImageNet

Давайте попробуем некоторые изображения, которые я скачал со своей страницы в Facebook. Я собираюсь использовать для этого наш DenseNet201.

Изображение, на котором я держу свою кошку, классифицируется следующим образом:

Predicted: [('n02104365', 'schipperke', 12.584991), ('n02445715', 'skunk', 9.826308), ('n02093256', 'Staffordshire_bullterrier', 8.28862)]

Давайте посмотрим на карту активации классов для этого изображения.

На изображениях ниже мы видим, что модель смотрит в нужное место.

Посмотрим, поможет ли исключение себя с классификацией.

Резко помогло обрезание себя:

Predicted: [('n02123597', 'Siamese_cat', 6.8055286), ('n02124075', 'Egyptian_cat', 6.7294292), ('n07836838', 'chocolate_sauce', 6.4594917)]

Теперь Луна предсказывается, по крайней мере, как кошка, что намного ближе к настоящему ярлыку (чего я не знаю, потому что не знаю, что это за кошка).

Последнее изображение, которое мы собираемся рассмотреть, - это изображение меня, моей жены и моего друга, которые едут на сверхскоростном экспрессе из Москвы в Санкт-Петербург.

Изображение правильно классифицировано:

Predicted: [('n02917067', 'bullet_train', 10.605988), ('n04037443', 'racer', 9.134802), ('n04228054', 'ski', 9.074459)]

Мы действительно находимся перед сверхскоростным поездом. Тогда давайте просто для удовольствия взглянем на карту активации классов.

Важно отметить, что последний сверточный слой DenseNet дает карты пространственной активации 7x7 (в отличие от 14x14 в сети VGG), поэтому разрешение тепловой карты может быть немного преувеличено при проецировании обратно в пространство изображения (соответствует красный цвет внимания на наших лицах).

Другой потенциальный вопрос, который может возникнуть, заключается в том, почему бы нам просто не вычислить градиент логита класса по отношению к входному изображению. Помните, что сверточная нейронная сеть работает как экстрактор признаков, а более глубокие слои сети работают во все более абстрактных пространствах. Мы хотим увидеть, какие функции на самом деле повлияли на выбор класса моделью, а не только отдельные пиксели изображения. Вот почему так важно брать карты активации более глубоких сверточных слоев.

Надеюсь, вам понравилась эта статья, спасибо, что прочитали.