Обо мне

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

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

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

Представьте, что лингвист внезапно стал экспертом по живописи. Именно это произошло в 2020 году, когда архитектура для обработки текста — трансформеры — научилась "видеть" изображения (оригинальная статья An Image is Worth 16x16 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]-токен, если можно усреднить все патчи?

Комментарии (2)


  1. S_A
    28.06.2025 13:48

    спасибо! доступно.

    но задача классификации сейчас не самая полезная в CV. есть ли трансформеры для сегментации? ещё бы желательно претренированные


  1. Flokis_guy
    28.06.2025 13:48

    Vision Transformer (ViT) доказал: для понимания картинок не обязательны свёртки!

    Я больше скажу, не нужны ни свертки, ни трансформеры. Можно вернуться к истокам используя MLP и получить отличные результаты.