Трансформеры появляются повсюду, они уже захватили НЛП, а теперь они приходят и к компьютерному зрению! Вы, должно быть, слышали о преобразователях зрения и о том, сколько из них превосходят традиционные CNN. Но как они работают?

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

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

Как работают трансформеры зрения? — Обзор

Итак, первый шаг — разделить изображение на серию лоскутков, которые затем нужно сплющить и пропустить через общий плотный слой. Результат, который мы получаем, представляет собой серию векторов, каждый из которых соответствует одному участку изображения. Затем у нас есть токен класса, который содержит информацию о классе объекта на изображении, например (Кошка или Собака). Этот токен класса объединяется с остальными векторами, полученными ранее.

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

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

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

Наконец, у нас есть голова MLP, которая выводит вектор с размерностью, равной количеству классов. В идеале такой MLP должен иметь функцию SoftMax, однако код, на который я ссылался, не имел такой функции активации. Вместо этого они просто определили плотный слой, возвращаемый в качестве вывода, вектор с размерностью в виде числа классов.

Теперь, когда у нас есть общий обзор, давайте погрузимся в код. Но перед этим нам нужно понять еще одну вещь.

Понимание Эйнопс

Einops расшифровывается как Einstein-Inspired Notation для операций, и это структура, которая может выполнять тензорные операции с матрицами. Есть две операции Einops, которые нам нужно понять, прежде чем мы сможем перейти к коду, это перестановка и повторение.

Первый шаг — импортировать все необходимые пакеты, а именно переставить и повторить, которые затем применяются к input_tensor. Функция перестановки делает то, что следует из ее названия, она перестраивает входной тензор в соответствии со строковым выражением, которое мы определили в скобках функции. Входной тензор изначально имел форму (2,3,5), и он был преобразован в тензор формы (3,5,2).

На рис. 4 мы видим, как используется функция повтора. В основном он используется для расширения размерности тензора путем повторения существующего входного тензора. Как и в предыдущем примере, ключом к пониманию того, как изменяются размеры, является наблюдение за строковым выражением внутри вызова функции. Здесь h=1 и w=7, форма выходного тензора, таким образом, будет (3,1,7).

А теперь давайте, наконец, взглянем на код.

Программирование преобразователя зрения

Позвольте мне сначала поблагодарить создателя этого репозитория, который потратил время на кодирование множества различных типов преобразователей зрения. Код был блестяще написан, и в большинстве случаев каждая строчка говорила сама за себя. Полный код можно найти здесь: https://github.com/lucidrains/vit-pytorch.git

Класс ViT

Теперь давайте посмотрим на класс ViT, определенный в строке 82. В инициализаторе в первую очередь вычисляется количество патчей (строка 90) и сглаженный размер каждого патча изображения (строка 91). Размер патча определяется пользователем при создании экземпляра класса ViT, и в зависимости от размера образа образ разбивается на несколько патчей. Например, если размер изображения 256*256, а размер патча 32, то количество патчей будет (256//32)*(256//32)=64. Диммер патча будет 3*32*32=3072.

Следующим шагом является self.to_patch_embedding, который сначала изменяет размеры входного изображения, а затем пропускает этот перестроенный вектор через общий линейный слой. Перестановка происходит следующим образом —

‘b c (h p1) (w p2) -> b (h w) (p1 p2 c)’

Здесь b = размер пакета, то есть количество изображений в пакете, c = размер канала, который равен 3, поскольку обычно в изображении RGB 3 канала. p1 и p2 — высота и ширина изображения соответственно, которые мы считали равными 32. Выходной тензор имеет размеры (b, h*w, p1*p2*c), где h*w обозначает количество патчей, а (p1*p2 *c) означает затемнение патча (определено в строке 91).

import torch
from vit_pytorch import ViT
v = ViT(
    image_size = 256,
    patch_size = 32,
    num_classes = 1000,
    dim = 1024,
    depth = 6,
    heads = 16,
    mlp_dim = 2048,
    dropout = 0.1,
    emb_dropout = 0.1
)
img = torch.randn(1, 3, 256, 256)
preds = v(img) # (1, 1000)

Линейный слой, определенный в строке 96, соответствует общему плотному слою, через который проходят все сплющенные векторы (см. рис. 1, линейная проекция сглаженных участков). Входной размер этого плотного слоя — patch_dim=3072, а выходной размер — dim=1024 (определено справа выше).

На рис. 1 вы можете видеть прямоугольники в форме капсул розового цвета прямо над слоем «Linear Projection of Flattened Patches». Эти розовые прямоугольники представляют векторные выходные данные из общего линейного слоя. Каждый выходной вектор соответствует одному участку изображения, который был сглажен до размера (p1*p2*c) и затем пропущен через линейный слой.

Далее мы переходим к кодировке позиции и токену класса. Переменная pos_embedding используется для обозначения относительного положения патчей изображения, без этого ViT не сможет понять патчи. Эта конкретная переменная должна иметь тот же размер, что и вектор, выходящий из линейного слоя, следовательно, размер этой переменной равен (1, num_patches+1, dim). Помните, что каждый выходной вектор имеет размер dim=1024, потому что выход линейного слоя равен 1024, кроме того, поскольку есть 64 патча и один дополнительный токен класса, который будет конкатенирован позже, мы создаем 64 + 1 = 65 таких векторов размера 1024. Этот вектор pos_embedding будет добавлен к x позже.

cls_token обозначает класс объекта на изображении, и он также должен иметь ту же форму, что и остальные выходные векторы (обозначаемые как x). Поскольку существует только один класс, мы сохраняем форму (1, 1, тусклый). Обратите внимание, что в строке 118 cls_token объединяется с x, поэтому его форма должна быть похожа на форму x.

Теперь давайте перейдем к прямой функции ViT. После прохождения изображения через переменную self.to_patch_embedding мы получаем ряд выходных векторов, соответствующих каждому патчу изображения, мы обозначаем эту выходную переменную как x (строка 114). В строке 117 мы увеличиваем размер cls_token, чтобы его внешний размер соответствовал размеру пакета (количество изображений в пакете). Поскольку переменная x была сгенерирована путем рассмотрения нескольких объединенных вместе изображений, нам нужно сохранить форму cls_token такой же, как и у x, поэтому мы используем функция повторения.

После этого мы объединяем cls_token с x, поэтому теперь внутри x имеется num_patches + 1 вектор размера dim. Позиционное кодирование идет дальше в строке 119, где переменная self.pos_embedding просто добавляется к x. После этого у нас есть слой отсева, который выполняет регуляризацию по x, чтобы предотвратить переоснащение, а за ним следует кодер-трансформер.

Кодер-трансформер

Эта часть может быть немного сложной, однако я пройдусь по всем важным строкам, объясняющим, как именно работает эта часть ViT. Сначала давайте перейдем к определению класса Transformer в строке 67. Мы начнем с наследования всех связанных функций от nn.Module, после чего мы используем super().__init__() для инициализации базового класса. После этого мы определяем слои преобразователя кодировщика.

Ранее я объяснял, что кодер-трансформер имеет несколько уровней, каждый из которых состоит из одного уровня внимания с несколькими головками, за которым следует уровень прямой связи. Количество слоев определяется переменной depth в вызове экземпляра ViT (см. выше), здесь depth = 6, что означает наличие 6 слоев, каждый из которых содержит внимание и компонент прямой связи.

В строках 71–75 мы определяем структуру кодировщика-преобразователя, где каждому компоненту (вниманию и упреждению) предшествует компонент нормализация слоя. Это означает, что перед выполнением операции внимания и прямой связи над x выполняется нормализация слоя над x, что обеспечивает более быструю сходимость обучения.

Теперь мы рассмотрим прямую функцию в классе Transformer (строки 76–80), здесь, в зависимости от количества слоев (глубина = 6), внимание и прямая связь. операции выполняются над x. Мы также наблюдаем пропущенные соединения в строках 78 и 79 в виде x = attn(x) + x и x = ff(x) + x. Посмотрите на рис. 1, правая часть, где представлена ​​внутренняя структура энкодера-трансформатора. Пропущенные соединения на изображении соответствуют тому, что написано в коде, значение x сохраняется и добавляется к результату после каждой операции.

Итак, мы прошлись по слоям, но как именно работает внимание? Давайте ответим на этот вопрос в нашем следующем сегменте, где мы рассмотрим компоненты на каждом уровне.

Понимание компонентов внимания и прямой связи

Давайте попробуем понять, как здесь реализуется внимание, но сначала давайте рассмотрим основную идею внимания.

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

Из x вычисляются три отдельных вектора, и эти векторы (Q, K, V) вводятся в приведенную ниже формулу. Результирующий вектор затем подается в слой прямой связи, после чего процесс повторяется в зависимости от количества слоев.

Теперь, как именно генерируются три вектора (Query, Key, Value)? В строке 47 у нас есть плотный слой self.to_qkv, который принимает x и выводит вектор размера (b, h*w, inner_dim*3), обратите внимание, что b обозначает размер пакета, а h*w — количество патчей. В строке 55 в прямой функции qkv делится на 3 части, после чего определяются переменные q, k и v после перестановки переменной qkv на основе количества головы.

В строках 58–63 мы воспроизводим формулу, упомянутую на рис. 6, обратите внимание, что self.attend — это функция SoftMax, которую мы определили в строке 44. После этого мы снова перестраиваем выходной вектор обратно к его исходные размеры в строке 64.

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

Выход из Transformer Encoder!

Таким образом, кодер-трансформер был довольно интенсивным! К счастью, следующие несколько строк не будут такими сложными для понимания!

Давайте посмотрим на строку 83, мы видим, что переменная self.pool определена как ‘cls’. Это означает, что x = x[:, 0], что означает, что рассматривается только первый компонент x, соответствующий части класса (см. рис. 1 для лучшего понимания).

Наконец, мы заканчиваем его mlp_head (определено в строке 108), это еще один плотный слой, который выводит вектор с размером, равным количеству классов, он указывает, какой класс выбирается с помощью ВиТ.

Заключение

Мы изучили, как Vision Transformer реализуется с помощью кода и как различные компоненты складываются вместе, чтобы обеспечить конечный результат. Мы увидели, как реализуется внимание и что происходит внутри различных уровней кодировщика-преобразователя.

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

Ссылку можно найти здесь — https://github.com/lucidrains/vit-pytorch.git

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