Часть 2: Vision Transformer (ViT) — Когда трансформеры научились видеть

от автора

Обо мне

Привет, меня зовут Василий Техин, и последние 6 лет я живу в мире машинного обучения — от первых шагов с линейной регрессией до экспериментов с современными VLm.
Когда я только начинал, мне не хватало материалов, где сложные концепции объяснялись бы без формул на трех страницах и обязательного PhD по математике. Я верил (и верю до сих пор), что любую идею можно разложить на понятные кирпичики — так, чтобы после прочтения у вас в голове складывалась цельная картина, а не россыпь терминов. Поэтому я начал эту серию статей, которая имеет целью объяснить «на пальцах», как устроены архитектуры сыгравшие большую роль в развитие машинного обучения.

Часть 1: ResNet-18 — Архитектура, покорившая глубину

Пролог: Революция в компьютерном зрении

Представьте, что лингвист внезапно стал экспертом по живописи. Именно это произошло в 2020 году, когда архитектура для обработки текста — трансформеры — научилась «видеть» изображения (оригинальная статья An Image is Worth 16×16 Words). Vision Transformer (ViT) доказал: для понимания картинок не обязательны свёртки!

Ключевая идея: Разрежьте изображение на кусочки-патчи, обработайте их как слова в предложении — и запустите через классический трансформер. Гениально? Да! Универсально? Не всегда.


Vit из орининальной статьи

Vit из орининальной статьи

Разберём на простом примере

Как ViT из картинки делает предсказание?
Возьмём задачу: определить «человек» (класс 0) или «машина» (класс 1) на изображении 4×4 пикселя.
Возьмем тоже простое крошечное изображение 4×4, что и в предыдущей статье про Resnet:

1. Разбиение на патчи: «Пазл из пикселей»

Канал R:      Канал G:      Канал B: [[2, 4, 1, 3]  [[2, 4, 1, 3]  [[2, 4, 1, 3]  [0, 5, 8, 2]   [0, 5, 8, 2]   [0, 5, 8, 2]  [4, 2, 7, 1]   [4, 2, 7, 1]   [4, 2, 7, 1]  [3, 6, 0, 4]]  [3, 6, 0, 4]]  [3, 6, 0, 4]] 

Разрежем на 4 патча 2×2:

Патч 1 (верх-лево): R[[2,4],[0,5]] G[[2,4],[0,5]] B[[2,4],[0,5]] → Вектор [2,4,0,5,2,4,0,5,2,4,0,5] Патч 2 (верх-право): R[[1,3],[8,2]] G[[1,3],[8,2]] B[[1,3],[8,2]] → [1,3,8,2,1,3,8,2,1,3,8,2] Патч 3 (низ-лево): R[[4,2],[3,6]] G[[4,2],[3,6]] B[[4,2],[3,6]] → [4,2,3,6,4,2,3,6,4,2,3,6] Патч 4 (низ-право): R[[7,1],[0,4]] G[[7,1],[0,4]] B[[7,1],[0,4]] → [7,1,0,4,7,1,0,4,7,1,0,4] 

Аналогия: Как слова в предложении несут смысл, так патчи несут визуальную информацию. Патч 1 = небо, Патч 4 = колесо.

2. Линейное проецирование: «Перевод на язык модели»

Каждый 12-мерный вектор патча сжимаем до 4-мерного эмбеддинга:

# Веса W (12×4): W = [[0.1, 0.2, -0.3, 0.4],      [0.5, -0.6, 0.7, 0.8],      ...       [0.9, -1.0, 0.2, 0.3]]  # Для Патча 1: z1 = [2*0.1 + 4*0.5 + 0*0.2 + ... + 5*0.9] = [3.8, -2.1, 1.4, 0.7] 

Пояснение: Как переводчик переводит слова между языками, так W переводит пиксели в «язык трансформера».

3. Позиционные эмбеддинги: «Где находится патч»

Без информации о позиции модель не отличит небо (верх) от колеса (низ). 2 подхода:

🔢 Фиксированные синусоидальные эмбеддинги (как в оригинальных трансформерах):

# Формула: PE(pos, 2i)   = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))  # Для Патча 1 (pos=0) при d_model=4: z1 = [3.8, -2.1, 1.4, 0.7] + [sin(0), cos(0), sin(0), cos(0)]      = [3.8 + 0, -2.1 + 1, 1.4 + 0, 0.7 + 1]      = [3.8, -1.1, 1.4, 1.7] 

Почему синусы/косинусы? Они кодируют позиции волнами разной частоты — модель легко понимает «расстояние» между позициями.

🎓 Обучаемые эмбеддинги (как в оригинальном ViT):

pos_embeddings = nn.Parameter(torch.randn(4, 4))  # 4 позиции × 4 размерности z1 = [3.8, -2.1, 1.4, 0.7] + [0.1, 0.3, -0.2, 0.5] = [3.9, -1.8, 1.2, 1.2] 

Плюсы/минусы:

Тип

Преимущества

Недостатки

Синусоидальные

Работает для любых длин последовательностей

Менее гибкие

Обучаемые

Лучше адаптируются к данным

Требуют фиксированной длины

Примеры из практики:

  • ALBERT: Использует синусоидальные эмбеддинги для экономии параметров

  • DeBERTa: Комбинирует абсолютные и относительные позиционные эмбеддинги

  • Swin Transformer: Вводит «сдвигаемые окна» для эффективного учёта позиций


4. [КЛАСС]-токен: «Учимся обобщать»

Добавляем специальный вектор, который собирает информацию обо всех патчах:

z0 = [0.9, -0.3, 1.1, 0.4]  # Обучаемый параметр! Вход трансформера: [z0, z1, z2, z3, z4]  # z0 всегда на первом месте 

Аналогия: Этот токен — как директор на совещании: слушает доклады отделов (патчей) и формирует общую картину.


Трансформерный блок: «Мозг ViT»

Принцип работы

Принцип работы

Шаг 1: Self-Attention — «Поиск связей»

Ключевые компоненты:

  • Query (Q): «Вопрос» от текущего токена («Что вокруг меня?»)

  • Key (K): «Описание» других токенов («Я — небо»)

  • Value (V): Фактическое содержание токена

Как работает для z1 (небо):

# 1. Создаём Q, K, V для всех токенов: Q_z1 = z1 * W_Q = [3.9*0.4, -1.8*(-0.1), ...] = [1.56, 0.18, ...] K_z3 = z3 * W_K = [0.2, -1.1, 0.5, 0.7]  # Для z3 (дерево)  # 2. Считаем "сходство" между z1 и z3: score = Q_z1 • K_z3 / sqrt(4) = (1.56*0.2 + 0.18*(-1.1) + ...) / 2 = 0.85  # 3. Взвешенная сумма Values: attention_z1 = 0.85*V_z3 + 0.1*V_z0 + ...  # V_z3 = [0.4, 0.1, -0.2, 0.3] 

Итог: Патч «небо» (z1) сильнее всего взаимодействует с «деревом» (z3), слабее — с «колесом» (z4).

Шаг 2: Multi-Head Attention — «Команда экспертов»

Вместо одного «взгляда» используем несколько параллельных attention-блоков:

# Пример для 2 heads: head1 = attention(Q1, K1, V1)  # Специализируется на цветах head2 = attention(Q2, K2, V2)  # Специализируется на формах combined = concat(head1, head2) * W_out  # Объединяем результаты 

Зачем? Так модель одновременно анализирует разные аспекты изображения.

Шаг 3: MLP — «Углубляем понимание»

После внимания каждый вектор проходит через «мини-мозг»:

h1 = [1.2, -0.3, 0.8, 1.1] →  GeLU(h1 * W1 + b1) →   # W1: расширяем 4→8 размерностей h1 * W2 + b2 →          # W2: сжимаем 8→4 → [0.9, -0.2, 1.4, 0.3] 

Пояснение: Как ассистент, который углубляет заметки после совещания: выделяет главное, отбрасывает шум.


Предсказание: Итоговая картина

После 12 трансформерных блоков [КЛАСС]-токен содержит сжатое представление всего изображения:

z0_final = [0.2, 1.8, -0.4, 0.9]  # После всех слоёв  # Линейный классификатор: Веса_класса0 = [1.1, 0.3, -0.7, 0.5]   Веса_класса1 = [0.4, -0.9, 0.2, 1.3]    Логиты = [   0.2*1.1 + 1.8*0.3 + (-0.4)*(-0.7) + 0.9*0.5 = 1.43,  # "машина"    0.2*0.4 + 1.8*(-0.9) + (-0.4)*0.2 + 0.9*1.3 = -0.25   # "человек" ]  Softmax: [e¹.⁴³, e⁻⁰.²⁵] / сумма = [0.84, 0.16] → 84% "машина" 

Полный путь данных:
Пиксели → Патчи → Эмбеддинги → 12×[Attention+MLP] → [CLS]-токен → Классификатор


ViT в коде: Главные компоненты

1. Разбиение на патчи

class PatchEmbed(nn.Module):     def __init__(self, img_size=224, patch_size=16, embed_dim=768):         super().__init__()         self.proj = nn.Conv2d(3, embed_dim, kernel_size=patch_size, stride=patch_size)      def forward(self, x):         # (B, 3, 224, 224) → (B, 768, 14, 14) → (B, 196, 768)          x = self.proj(x).flatten(2).transpose(1, 2)           return x 

2. Синусоидальные позиционные эмбеддинги + [CLS]

class ViT(nn.Module):     def __init__(self, num_patches, embed_dim):         super().__init__()         # [CLS]-токен         self.cls_token = nn.Parameter(torch.randn(1, 1, embed_dim))                  # Синусоидальные позиционные эмбеддинги         position = torch.arange(0, num_patches+1).unsqueeze(1)         div_term = torch.exp(torch.arange(0, embed_dim, 2) * (-math.log(10000.0) / embed_dim))         pe = torch.zeros(num_patches+1, embed_dim)         pe[:, 0::2] = torch.sin(position * div_term)         pe[:, 1::2] = torch.cos(position * div_term)         self.register_buffer('pe', pe.unsqueeze(0))  # (1, num_patches+1, embed_dim)      def forward(self, x):         # Добавляем [CLS]-токен         cls_tokens = self.cls_token.expand(x.shape[0], -1, -1)         x = torch.cat([cls_tokens, x], dim=1)  # (B, num_patches+1, embed_dim)                  # Добавляем позиционные эмбеддинги         x = x + self.pe         return x 

3. Трансформерный блок

class TransformerBlock(nn.Module):     def __init__(self, dim, num_heads=4):         super().__init__()         # Multi-head Attention         self.norm1 = nn.LayerNorm(dim)         self.attn = nn.MultiheadAttention(dim, num_heads)                  # MLP         self.norm2 = nn.LayerNorm(dim)         self.mlp = nn.Sequential(             nn.Linear(dim, 4*dim),               nn.GELU(),             nn.Linear(4*dim, dim)         )      def forward(self, x):         # 1. Self-Attention + skip-connection         residual = x         x = self.norm1(x)         attn_out, _ = self.attn(x, x, x)           x = residual + attn_out                  # 2. MLP + skip-connection         residual = x         x = self.norm2(x)         x = residual + self.mlp(x)           return x 

Как ViT учится?

4 ключевых этапа:

  1. Прямой проход:

    • 5 токенов (4 патча + [CLS]) проходят через 12 блоков

    • На каждом шаге: Attention → MLP → LayerNorm

  2. Классификация:

    • Только [CLS]-токен после последнего слоя → линейный классификатор

  3. Расчёт ошибки:

    • Кросс-энтропия между предсказанием и меткой:

    loss = -log(0.84)  # Если метка "машина" 
  4. Обратное распространение:

    • Градиенты обновляют:

      • Веса проецирования патчей (W из шага 2)

      • Параметры Q/K/V в каждом блоке

      • Веса MLP

      • Позиционные эмбеддинги (если обучаемые)


Почему ViT — это не всегда лучше ResNet?

✅ Сильные стороны ViT:

  1. Глобальный контекст: Видит всю картинку сразу (свёртки видят только локальные области).

  2. Масштабируемость: Чем больше данных — тем лучше качество (JFT-300M: 89.3% accuracy).

  3. Унификация: Одна архитектура для текста, аудио и изображений.

❌ Слабые стороны ViT:

  1. Данные: Требует в 10-100 раз больше обучающих изображений, чем ResNet.

  2. Индуктивные ограничения: Плохо переносит изменения размера изображения (в отличие от свёрток).

  3. Вычислительная сложность: Квадратичный рост времени работы относительно числа патчей.

Когда выбирать:

  • 🤖 ViT: Если у вас >1 млн изображений и нужна state-of-the-art точность.

  • 🚀 ResNet: Если данных мало (<100K) или нужна реальная скорость (мобильные приложения).


Философский итог

ViT не «убил» свёрточные сети — он показал, что внимание может быть универсальным механизмом обучения. Но как и в жизни, универсальных решений нет: ResNet остаётся рабочим инструментом, а ViT — мощным, но требовательным прорывом.

В следующей части: «Сначала мы учим модели видеть, потом — воображать. DiT — следующий шаг к искусственному воображению.»

P.S. Ошибки в статье? Хотите глубже разобрать attention? Пишите в комментариях!


Проверь себя

  1. Почему патч 4×4 пикселей нельзя подавать напрямую в трансформер?

  2. Зачем ViT [CLS]-токен, если можно усреднить все патчи?


ссылка на оригинал статьи https://habr.com/ru/articles/922868/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *