Фаза 04 · урок 14

Визуальные трансформеры (Vision Transformers, ViT)

Цель урока: Десятилетие свёртка была синонимом компьютерного зрения. У CNN были сильные индуктивные смещения — локальность, эквивариантность к сдвигу, — которые, как никто не думал, можно заменить. Затем Dosovitskiy et al. (2020) показали, что…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Компьютерное зрение
Чтение
11 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Конвейер
  5. Встраивание патчей
  6. Токен класса
  7. Позиционное встраивание
  8. Блок encoder трансформера
  9. Почему pre-LN
  10. Компромисс размера патча
  11. Рецепт DeiT для обучения ViT на ImageNet-1k
  12. Swin и ConvNeXt
  13. Предобучение MAE
  14. Соберите
  15. Шаг 1: Встраивание патчей
  16. Шаг 2: Блок трансформера
  17. Шаг 3: ViT
  18. Шаг 4: Проверка работоспособности — инференс одного изображения
  19. Используйте
  20. Поставьте
  21. Упражнения
  22. Ключевые термины
  23. Дополнительные материалы

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

Тип: Соберите Языки: Python Предварительные требования: Фаза 7, урок 02 (Self-Attention), фаза 4, урок 04 (Классификация изображений) Время: ~45 минут

Цели обучения

  • Реализовать с нуля встраивание патчей, обучаемое позиционное встраивание, токен класса и блоки encoder трансформера, чтобы собрать минимальный ViT
  • Объяснить, почему считалось, что ViT нужны огромные объёмы данных для предобучения, пока DeiT и MAE не доказали обратное
  • Сравнить ViT, Swin и ConvNeXt по их архитектурным априорным предпосылкам: отсутствуют, локальное оконное внимание, свёрточный backbone
  • Дообучить предобученный ViT на небольшом наборе данных с помощью timm и стандартного рецепта linear probe / fine-tune

Проблема

Десятилетие свёртка была синонимом компьютерного зрения. У CNN были сильные индуктивные смещения — локальность, эквивариантность к сдвигу, — которые, как никто не думал, можно заменить. Затем Dosovitskiy et al. (2020) показали, что простой transformer, применённый к развёрнутым патчам изображения, вообще без свёрточной механики, может на масштабе сравняться с лучшими CNN или превзойти их.

Подвох заключался в словах «на масштабе». ViT на ImageNet-1k проигрывал ResNet. ViT, предобученный на ImageNet-21k или JFT-300M, а затем дообученный на ImageNet-1k, превосходил его. Вывод был таким: у трансформеров нет полезных априорных предпосылок, но они способны выучить их на достаточном объёме данных. Последующая работа (DeiT, MAE, DINO) показала, что при правильных рецептах обучения — сильной аугментации, self-supervised предобучении, дистилляции — ViT хорошо обучаются и на небольших данных.

К 2026 году чистые CNN всё ещё конкурентоспособны на edge-устройствах (ConvNeXt — самый сильный), но трансформеры доминируют во всём остальном: сегментации (Mask2Former, SegFormer), детекции (DETR, RT-DETR), мультимодальности (CLIP, SigLIP), видео (VideoMAE, VJEPA). Структура блока ViT — та, которую нужно знать.

Концепция

Конвейер

Диаграмма к уроку «Визуальные трансформеры (Vision Transformers, ViT)»

Семь шагов. Патчи -> токены -> внимание -> классификатор. Каждый вариант (DeiT, Swin, ConvNeXt, предобучение MAE) изменяет один или два из этих семи, а остальное оставляет без изменений.

Встраивание патчей

Первая свёртка — секрет. Размер ядра 16, stride 16, поэтому изображение 224x224 становится сеткой 14x14 патчей 16x16, каждый из которых проецируется во встраивание размерности 768. Одна эта свёртка одновременно разбивает на патчи и выполняет линейную проекцию.

Input:  (3, 224, 224)
Conv (3 -> 768, k=16, s=16, no padding):
Output: (768, 14, 14)
Flatten spatial: (196, 768)

196 патчей = 196 токенов. Размерность признаков каждого токена равна 768 (ViT-B), 1024 (ViT-L) или 1280 (ViT-H).

Токен класса

Один обучаемый вектор, добавляемый в начало последовательности:

tokens = [CLS; patch_1; patch_2; ...; patch_196]   shape (197, 768)

После N блоков трансформера выход [CLS] — это глобальное представление изображения. Голова классификации считывает только этот один вектор.

Позиционное встраивание

У трансформеров нет встроенного понятия пространственного положения. Добавьте обучаемый вектор к каждому токену:

tokens = tokens + learned_pos_embedding   (also shape (197, 768))

Встраивание является параметром модели; обучение на основе градиента адаптирует его к двумерной структуре изображения. Синусоидальные двумерные альтернативы существуют, но на практике используются редко.

Блок encoder трансформера

Стандартный. Multi-head self-attention, MLP, residual connections, pre-LayerNorm.

x = x + MSA(LN(x))
x = x + MLP(LN(x))

MLP is two-layer with GELU: Linear(d -> 4d) -> GELU -> Linear(4d -> d)

ViT-B/16 содержит 12 таких блоков, каждый с 12 головами внимания; всего 86M параметров.

Почему pre-LN

Ранние трансформеры использовали post-LN (x = LN(x + sublayer(x))) и с трудом обучались глубже 6–8 слоёв без warmup. Pre-LN (x = x + sublayer(LN(x))) стабильно обучает более глубокие сети без warmup. Каждый ViT и каждая современная LLM использует pre-LN.

Компромисс размера патча

  • Патчи 16x16 -> 196 токенов, стандартный вариант.
  • Патчи 32x32 -> 49 токенов, быстрее, но с меньшим разрешением.
  • Патчи 8x8 -> 784 токена, более детально, но стоимость внимания O(n^2) плохо масштабируется.

Более крупные патчи = меньше токенов = быстрее, но меньше пространственных деталей. SwinV2 использует патчи 4x4 в иерархических окнах.

Рецепт DeiT для обучения ViT на ImageNet-1k

Исходному ViT требовался JFT-300M, чтобы превзойти CNN. DeiT (Touvron et al., 2020) обучил ViT-B до 81.8% top-1 только на ImageNet-1k с четырьмя изменениями:

  1. Тяжёлая аугментация: RandAugment, Mixup, CutMix, Random Erasing.
  2. Stochastic depth (случайно отбрасывать целые блоки при обучении).
  3. Repeated augmentation (одно и то же изображение выбирается 3 раза на batch).
  4. Дистилляция от CNN-учителя (необязательна, дополнительно повышает точность).

Каждый современный рецепт обучения ViT восходит к DeiT.

Swin и ConvNeXt

  • Swin (Liu et al., 2021) — внимание на основе окон. Каждый блок уделяет внимание внутри локального окна; чередующиеся блоки сдвигают окно, чтобы смешивать информацию между окнами. Возвращает CNN-подобную априорную предпосылку локальности, сохраняя оператор внимания.
  • ConvNeXt (Liu et al., 2022) — переработанная CNN, соответствующая архитектурным решениям Swin (depthwise convs, LayerNorm, GELU, inverted bottleneck). Показала, что разрыв заключается не в «внимании против свёртки», а в «современном рецепте обучения + архитектуре».

В 2026 году ConvNeXt-V2 и Swin-V2 оба готовы к продакшену; правильный выбор зависит от вашего стека инференса (ConvNeXt лучше компилируется для edge) и корпуса предобучения.

Предобучение MAE

Masked Autoencoder (He et al., 2022): случайно замаскируйте 75% патчей, обучите encoder обрабатывать только видимые 25%, обучите небольшой decoder восстанавливать замаскированные патчи по выходу encoder. После предобучения отбросьте decoder и дообучите encoder.

MAE делает ViT обучаемым только на ImageNet-1k, достигает SOTA и является текущим рецептом self-supervised по умолчанию.

Соберите

Шаг 1: Встраивание патчей

import torch
import torch.nn as nn

class PatchEmbedding(nn.Module):
    def __init__(self, in_channels=3, patch_size=16, dim=192, image_size=64):
        super().__init__()
        assert image_size % patch_size == 0
        self.proj = nn.Conv2d(in_channels, dim, kernel_size=patch_size, stride=patch_size)
        num_patches = (image_size // patch_size) ** 2
        self.num_patches = num_patches

    def forward(self, x):
        x = self.proj(x)
        return x.flatten(2).transpose(1, 2)

Одна свёртка, один flatten, один transpose. Это весь этап преобразования изображения в токены.

Шаг 2: Блок трансформера

Pre-LN, multi-head self-attention, MLP с GELU, residual connections.

class Block(nn.Module):
    def __init__(self, dim, num_heads, mlp_ratio=4, dropout=0.0):
        super().__init__()
        self.ln1 = nn.LayerNorm(dim)
        self.attn = nn.MultiheadAttention(dim, num_heads, dropout=dropout, batch_first=True)
        self.ln2 = nn.LayerNorm(dim)
        self.mlp = nn.Sequential(
            nn.Linear(dim, dim * mlp_ratio),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(dim * mlp_ratio, dim),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        a, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False)
        x = x + a
        x = x + self.mlp(self.ln2(x))
        return x

nn.MultiheadAttention обрабатывает разбиение на головы, scaled dot-product и выходную проекцию. batch_first=True, поэтому формы имеют вид (N, seq, dim).

Шаг 3: ViT

class ViT(nn.Module):
    def __init__(self, image_size=64, patch_size=16, in_channels=3,
                 num_classes=10, dim=192, depth=6, num_heads=3, mlp_ratio=4):
        super().__init__()
        self.patch = PatchEmbedding(in_channels, patch_size, dim, image_size)
        num_patches = self.patch.num_patches
        self.cls_token = nn.Parameter(torch.zeros(1, 1, dim))
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, dim))
        self.blocks = nn.ModuleList([
            Block(dim, num_heads, mlp_ratio) for _ in range(depth)
        ])
        self.ln = nn.LayerNorm(dim)
        self.head = nn.Linear(dim, num_classes)
        nn.init.trunc_normal_(self.pos_embed, std=0.02)
        nn.init.trunc_normal_(self.cls_token, std=0.02)

    def forward(self, x):
        x = self.patch(x)
        cls = self.cls_token.expand(x.size(0), -1, -1)
        x = torch.cat([cls, x], dim=1)
        x = x + self.pos_embed
        for blk in self.blocks:
            x = blk(x)
        x = self.ln(x[:, 0])
        return self.head(x)

vit = ViT(image_size=64, patch_size=16, num_classes=10, dim=192, depth=6, num_heads=3)
x = torch.randn(2, 3, 64, 64)
print(f"output: {vit(x).shape}")
print(f"params: {sum(p.numel() for p in vit.parameters()):,}")

Около 2.8M параметров — крошечный ViT, посильный для CPU. Реальный ViT-B содержит 86M; это то же определение класса с dim=768, depth=12, num_heads=12.

Шаг 4: Проверка работоспособности — инференс одного изображения

logits = vit(torch.randn(1, 3, 64, 64))
print(f"logits: {logits}")
print(f"probs:  {logits.softmax(-1)}")

Должно выполниться без ошибки. Вероятности в сумме дают 1.

Используйте

timm поставляет каждый вариант ViT с предобученными весами ImageNet. Одна строка:

import timm

model = timm.create_model("vit_base_patch16_224", pretrained=True, num_classes=10)

timm — стандарт для production-задач с визуальными трансформерами в 2026 году. Поддерживает ViT, DeiT, Swin, Swin-V2, ConvNeXt, ConvNeXt-V2, MaxViT, MViT, EfficientFormer и десятки других через тот же API.

Для мультимодальных задач (изображение + текст) transformers поставляет CLIP, SigLIP, BLIP-2, LLaVA. Image encoder во всех них — вариант ViT.

Поставьте

Этот урок создаёт:

  • outputs/prompt-vit-vs-cnn-picker.md — промпт, выбирающий между ViT, ConvNeXt или Swin по размеру набора данных, вычислительным ресурсам и стеку инференса.
  • outputs/skill-vit-patch-and-pos-embed-inspector.md — навык, проверяющий, что формы встраивания патчей и позиционного встраивания ViT соответствуют ожидаемой моделью длине последовательности, и выявляющий самые распространённые ошибки портирования.

Упражнения

  1. (Легко) Выведите формы каждого промежуточного тензора для прямого прохода через приведённый выше маленький ViT. Подтвердите: вход (N, 3, 64, 64) -> патчи (N, 16, 192) -> с CLS (N, 17, 192) -> вход классификатора (N, 192) -> выход (N, num_classes).
  2. (Средне) Дообучите предобученный timm ViT-S/16 на наборе synthetic-CIFAR из урока 4. Сравните с дообучением ResNet-18 на тех же данных. Сообщите время обучения и итоговую точность.
  3. (Сложно) Реализуйте предобучение MAE для маленького ViT: замаскируйте 75% патчей, обучите encoder + небольшой decoder восстанавливать замаскированные патчи. Оцените точность linear probe на синтетических данных до и после предобучения.

Ключевые термины

Термин Как обычно говорят Что это на самом деле означает
Встраивание патчей «Первая свёртка» Свёртка с размером ядра = stride = размеру патча; превращает изображение в сетку встраиваний токенов
Токен класса “[CLS]” Обучаемый вектор, добавленный в начало последовательности токенов; его итоговый выход — глобальное представление изображения
Позиционное встраивание «Обучаемая позиция» Обучаемый вектор, добавляемый к каждому токену, чтобы transformer знал, откуда пришёл каждый патч
Pre-LN «LayerNorm перед подслоем» Стабильный вариант трансформера: x + sublayer(LN(x)) вместо LN(x + sublayer(x))
Multi-head attention «Параллельное внимание» Стандартное внимание трансформера, разделённое на num_heads независимых подпространств и затем конкатенированное
ViT-B/16 «Base, patch 16» Канонический размер: dim=768, depth=12, heads=12, patch_size=16, image=224; ~86M параметров
DeiT «Эффективный по данным ViT» ViT, обученный только на ImageNet-1k с сильной аугментацией; доказал, что большие наборы данных для предобучения не являются строго необходимыми
MAE «Masked autoencoder» Self-supervised предобучение: замаскировать 75% патчей, восстановить их; доминирующий рецепт предобучения ViT

Дополнительные материалы


Источник: Vision Transformers (ViT) 04.13 — 3D-зрение: облака точек и NeRF · Фаза 04 — Компьютерное зрение · 04.15 — Зрение в реальном времени и развёртывание на периферии · Полный каталог