Фон

За последние несколько месяцев до написания этого поста, кажется, произошел своего рода прорыв в переносе Трансформеров в мир Компьютерного Зрения.

Перечислим несколько известных работ по этому поводу:

Если я могу сделать прогноз на 2021 год — в следующем году мы увидим МНОГО статей об использовании трансформеров в задачах зрения (не стесняйтесь комментировать здесь через год, если я ошибаюсь) .

Но что происходит внутри Vision Transformers? Как они вообще работают? Можем ли мы потрогать их и разобрать на части, чтобы лучше понять?

«Объяснимость» может быть амбициозным и перегруженным термином, который означает разные вещи для разных людей, но когда я говорю «Объяснимость», я имею в виду следующее:

  • (полезно для разработчика) Что происходит внутри, когда мы запускаем Transformer на этом изображении? Возможность просмотра промежуточных слоев активации. В компьютерном зрении — это обычно изображения! Их можно интерпретировать, поскольку вы можете отображать активацию различных каналов в виде 2D-изображений.
  • (полезно для разработчика) Чему она научилась? Возможность исследовать, какие закономерности (если таковые имеются) изучила модель. Обычно это форма вопроса «Какое входное изображение максимизирует отклик от этой активации?» , и для этого можно использовать варианты «Максимизации активации».
  • (полезно как для разработчика, так и для пользователя) Что он увидел на этом изображении? Возможность ответить «Какая часть изображения отвечает за предсказание сети», иногда называется «Атрибуция пикселей».

Значит, нам это понадобится и для Vision Transformers!

В этом посте мы рассмотрим мою попытку сделать это для Vision Transformers.

Все здесь будет сделано с недавно выпущенной моделью Deit Tiny от Facebook, т.е.:

model = torch.hub.load('facebookresearch/deit:main', 'deit_tiny_patch16_224', pretrained=True)

И мы собираемся предположить входные изображения 224xx224, чтобы было легче следовать формам, хотя это не обязательно.

В оставшейся части этого поста предполагается, что вы понимаете, как работают Vision Transformers.

По сути, это ванильные трансформеры, но изображения разбиты на 14x14 разных токенов, где каждый токен представляет собой патч 16x16 из изображения.

Прежде чем продолжить, вы можете прочитать две статьи, приведенные выше, и эти сообщения в блоге о них:

Q, K, V и внимание.

Vision Transformer состоит из нескольких блоков кодирования, каждый из которых имеет:

  • Несколько головок внимания, которые отвечают за представление каждого патча, за объединение информации из других патчей в изображении.
  • MLP, который преобразует каждое представление патча в представление функций более высокого уровня.
  • Оба имеют остаточные связи. И мы увидим их в действии!

Это так же просто, как это, взято из Удивительной реализации пакета Pytorch Image Models для преобразователей зрения Росса Вайтмана:

def forward(self, x):
        x = x + self.drop_path(self.attn(self.norm1(x)))
        x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x

Внутри каждой головы внимания (в модели «Deit Tiny» по 3 головы внимания на каждом уровне) игроки Q,k и V.

Форма каждого из них — 3x197x64

  • Есть 3 головы внимания.
  • Каждая голова внимания видит 197 жетонов.
  • Каждый токен имеет представление функции длиной 64.
  • Из этих 197 токенов 196 представляют исходное изображение 14x14=196 патчей изображения, а первый токен представляет собой токен класса, который проходит через преобразователь и будет использоваться в конце для предсказания.

Если для каждой головы внимания в отдельности мы заглянем во второе измерение со 197 токенами, мы сможем просмотреть последние 14 x 14 = 196 токенов.

Это дает нам изображение размером 14x14x64, которое мы затем можем визуализировать.

Строки Q и K представляют собой элемент длиной 64, который представляет местоположение на изображении.

Затем мы можем думать о Q, K и V следующим образом: для каждого фрагмента изображения с qiqi информация будет поступать из мест на изображении, которые имеют ключи kjkj, похожие на этот qiqi.

Наглядные примеры K и Q — разные модели потока информации

Давайте посмотрим на пример.

Входом в сеть является это изображение самолета:

Теперь мы можем посмотреть на изображения Q и K в разных слоях и визуализировать их для одного из 64 каналов c.

Этот вектор активации будет изображением 14x14 с положительными и отрицательными значениями, которые, по-видимому, находятся в диапазоне [-5, 5].

  • qicqic — это значение вектора признаков запроса для одного из местоположений i на изображении в канале c.
  • kjckjc — значение вектора ключевых признаков для одного из местоположений j на изображении в канале c.

Сложная часть:

Для каждого местоположения j в K (помните, что оно происходит от одного из фрагментов 14x14 в исходном изображении) мы можем спросить: «Как это местоположение будет распространять информацию в другие части изображения?»

Поскольку мы берем скалярное произведение между векторами токенов (каждый qiqi и kjkj), есть два сценария:

  • Две фишки в одном и том же канале c, qicqic и kjckjc, имеют одинаковый знак (оба положительные или отрицательные) — их умножение положительно.
  • Это означает, что местоположение изображения j и канал c — kjckjc — будут способствовать передаче информации в это местоположение изображения qiqi.
  • Два токена в одном и том же канале c, qicqic и kjckjc, имеют разные знаки (один положительный, а другой отрицательный) — их умножение отрицательное.
  • Это означает, что местоположение изображения j и канал c — kjckjc — НЕ будут способствовать передаче информации в это местоположение изображения qiqi.

Чтобы противопоставить отрицательные и положительные пиксели, мы пропустим каждое изображение через слой torch.nn.Sigmoid() (яркие значения положительные, темные значения отрицательные).

Глядя на визуализацию Q,K для разных каналов, я думаю, что можно выделить два паттерна.

Паттерн 1 — Информация течет в одном направлении

Слой 8, канал 26, заголовок первого внимания:

Запрос изображенияКлюч изображенияОригинал

  • На ключевом изображении выделяется Самолет.
  • Изображение запроса выделяет все изображение.

Для большинства местоположений в образе Запроса, поскольку они являются положительными, информация будет поступать к ним только из положительных мест в образе Ключа, которые исходят от Самолета.

Q, K здесь говорят нам -

We found an airplane, and we want all the locations in the image to know about this!

Паттерн 2 — Информация течет в двух направлениях

Слой 11, канал 59, заголовок первого внимания:

Запрос изображенияКлюч изображенияОригинал

  • Изображение Query выделяет в основном нижнюю часть самолета.
  • Ключевой образ негативен в верхней части Самолета.

Здесь информация идет в двух направлениях:

  • Верхняя часть плоскости (отрицательные значения в ключе) будет распространяться на все изображение (отрицательные значения в запросе).
  • Hey we found this plane, lets tell the rest of the image about it.
  • Информация из «неплоскостных» частей изображения (положительные значения в ключе) будет поступать в нижнюю часть плоскости (положительные значения в запросе).
  • Lets tell the plane more about what's around it.

Как выглядят активации внимания для токена класса в сети?

Еще одна вещь, которую мы можем сделать, — визуализировать потоки внимания к токену класса на разных уровнях сети.

Поскольку у нас есть несколько головок внимания, для простоты мы рассмотрим только первую.

Матрица внимания (Q∗KTQ∗KT) имеет форму 197x197.

Если мы посмотрим на первую строку (форма 197) и отбросим первое значение (левое с формой 196 = 14x14), то именно так информация перетекает из разных мест на изображении в токен класса.

Вот как выглядит активация внимания класса через слои:

Похоже, что на уровне 7 сеть смогла довольно хорошо сегментировать плоскость.

Однако — если мы посмотрим на последовательные слои, некоторые части плоскости теряются, а затем появляются снова:

И мы можем поблагодарить оставшиеся связи за это!

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

Внимание

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

Для количественной оценки этого мы можем использовать метод под названием Распределение внимания из Количественного потока внимания в трансформерах, Самиры Абнар и Виллема Зуидема.

Это также то, что предлагают авторы Изображение стоит 16x16 слов: трансформеры для распознавания изображений в масштабе.

В каждом блоке Transformer мы получаем матрицу внимания AijAij, которая определяет, сколько внимания будет направлено от токена j на предыдущем уровне к токену i на следующем уровне.

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

Однако у нас также есть остаточные соединения (как мы видели в предыдущем разделе).

Мы можем смоделировать их, добавив матрицу идентичности I к матрицам внимания слоя: Aij+IAij+I.

У нас есть несколько головок внимания. Что нам с ними делать?

В информационном документе «Внимание» предлагается брать среднее количество голов. Как мы увидим, может иметь смысл использование других вариантов: например, минимум, максимум или использование разных весов.

Наконец, мы получаем способ рекурсивно вычислить матрицу Attention Rollout на уровне L:

AttentionRolloutL=(AL+I)AttentionRolloutL-1AttentionRolloutL=(AL+I)AttentionRolloutL-1

Мы также должны нормализовать строки, чтобы общий поток внимания оставался равным 1.

Модификации, чтобы Attention Rollout работал с Vision Transformers

Я реализовал это и запустил это на недавних моделях Эффективных данных от Facebook, но результаты были не такими хорошими, как в статье Изображение стоит 16x16 слов: преобразователи для распознавания изображений в масштабе.

Результаты были очень зашумлены, и кажется, что внимание сосредоточено не только на интересной части изображения.

Пытаясь заставить это работать, я заметил две вещи:

То, как мы объединяем внимание, имеет значение

Например, вот как выглядит результат, если мы возьмем минимальное значение среди заголовков внимания, а не среднее значение, как предлагается в документе Attention Rollout:

ImageMean FusionMin Fusion

Разные головы внимания смотрят на разные вещи, поэтому я полагаю, что взятие минимума устраняет шум, находя их общий знаменатель.

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

Мы можем сосредоточиться только на главных внимания и отбросить остальные

Отбрасывание самых низких значений внимания оказывает огромное влияние на то, как выглядят результаты.

Вот как это выглядит, когда мы увеличиваем долю отбрасываемых пикселей внимания:

Как видите, чем больше пикселей мы удаляем, тем лучше изолируем выделяющийся объект на изображении.

Наконец, вот как это выглядит для нескольких разных изображений:

ImageVanilla Attention RolloutС отбрасыванием самых низких пикселей + максимальное слияние

Внедрение Gradient Attention для пояснения к конкретному классу

Редактировать — оказывается, для этого есть другой метод!

Помимо того, что я реализовал ниже, я отсылаю вас к Hila Chefer Transformer Interpretability Beyond Attention Visualization и их github repo.

Другой вопрос, который мы можем задать: «Что на изображении способствует более высокому результату в категории 42?»

Или, другими словами, классовая объяснимость.

При объединении головок внимания в каждом слое мы могли бы просто взвесить все внимания (в текущей реализации это внимания после softmax, но, возможно, имеет смысл изменить это) по градиенту целевого класса, а затем взять среднее значение среди головы внимание.

Aij∗gradijAij∗gradij

Где Трансформер видит Собаку (категория 243) и Кошку (категория 282)?

Где Трансформер видит мушкетную собаку (категория 161) и попугая (категория 87)?

Что говорит нам максимизация активации

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

В Vision Transformers изображения разбиты на 14x14 независимых фрагментов (которые представляют собой 16x16 пикселей).

Мы также видим это в результате максимизации активации ниже — вместо непрерывного изображения мы получаем патчи 14x14.

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

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

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

Исходный код проекта. Полностью пример кода написан на Python.

import argparse
import sys
import torch
from PIL import Image
from torchvision import transforms
import numpy as np
import cv2

из vit_rollout импортировать VITAttentionRollout
из vit_grad_rollout импортировать VITAttentionGradRollout

def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(' — use_cuda', action='store_true', default=False,
help='Использовать ускорение графического процессора NVIDIA ')
parser.add_argument(' — image_path', type=str, default='./examples/both.png',
help='Входной путь к изображению')
parser.add_argument (' — head_fusion', type=str, default='max',
help='Как соединить головки внимания для развертывания внимания. \
Может быть средним/максимальным/минимальным')
parser.add_argument(' — discard_ratio', type=float, default=0.9,
help='Сколько младших путей внимания 14x14 следует отбросить')
parser.add_argument(' — category_index' , type=int, default=None,
help='Индекс категории для развертывания градиента')
args = parser.parse_args()
args.use_cuda = args.use_cuda и torch.cuda .is_available()
if args.use_cuda:
print("Использование GPU")
else:
print("Использование CPU")

возвращаемые аргументы

def show_mask_on_image(img, mask):
img = np.float32(img) / 255
heatmap = cv2.applyColorMap(np.uint8(255 * маска), cv2.COLORMAP_JET)
heatmap = np.float32(тепловая карта) / 255
cam = тепловая карта + np.float32(img)
cam = cam / np.max(cam)
return np.uint8(255 * cam)

if __name__ == '__main__':
args = get_args()
model = torch.hub.load('facebookresearch/deit:main',
'deit_tiny_patch16_224', pretrained=True) модель.eval()

если args.use_cuda:
модель = model.cuda()

transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0,5, 0,5, 0,5], std=[0.5, 0.5, 0.5]),
])
img = Image.open(args.image_path)
img = img.resize((224, 224))
input_tensor = transform(img).unsqueeze(0)
if args.use_cuda:
input_tensor = input_tensor.cuda()

если args.category_index имеет значение None:
print("Выполнение Attention Rollout")
Attention_rollout = VITAttentionRollout(model, head_fusion=args.head_fusion,
discard_ratio=args.discard_ratio)
mask = Attention_rollout(input_tensor)
name = “attention_rollout_{:.3f}_{}.png”.format(args.discard_ratio, args.head_fusion)
else:
print(“Выполнение градиента Attention Rollout")
grad_rollout = VITAttentionGradRollout(model, discard_ratio=args.discard_ratio)
mask = grad_rollout(input_tensor, args.category_index)
name = "grad_rollout_{}_{:.3f} _{}.png”.format(args.category_index,
args.discard_ratio, args.head_fusion)

np_img = np.array(img)[:, :, ::-1]
mask = cv2.resize(mask, (np_img.shape[1], np_img.shape[0]))
маска = show_mask_on_image(np_img, маска)
cv2.imshow("Входное изображение", np_img)
cv2.imshow(имя, маска)
cv2.imwrite("input.png", np_img )
cv2.imwrite(имя, маска)
cv2.waitKey(-1)

Краткое содержание

В этом посте мы применили методы объяснимости для трансформеров зрения.

Это была моя попытка лучше понять, как они работают и что происходит внутри них.

Этот Урок был отредактирован, собрана информация и хорошо сформулирована для структурированного эксперта в предметной области,

Л.П.Хариша Лакшан Варнакуласурия

Кто является опытным инженером-программистом в области программного обеспечения / баз данных / сети / бизнес-аналитики (BI) / бизнес-аналитика / разработки сетей и безопасности / систем / мобильных устройств с предметным опытом в области развития бизнеса и практики лидерства.

Следуйте за мной LinkedIn @ Хариша Лакшан Варнакуласурия | LinkedIn

Подпишитесь на мой сайт @ http://www.unicornprofessional.ml

Пишите мне на @[email protected]

Просмотрите мой веб-сайт портфолио @ https://www.srilankancodingchamp.ml/

Вы можете узнать больше информации о моем реализованном проекте в деталях.