Обо мне
Привет, меня зовут Василий Техин, и последние 6 лет я живу в мире машинного обучения — от первых шагов с линейной регрессией до экспериментов с современными VLm.
Когда я только начинал, мне не хватало материалов, где сложные концепции объяснялись бы без формул на трех страницах и обязательного PhD по математике. Я верил (и верю до сих пор), что любую идею можно разложить на понятные кирпичики — так, чтобы после прочтения у вас в голове складывалась цельная картина, а не россыпь терминов. Поэтому я начал эту серию статей, которая имеет целью объяснить «на пальцах», как устроены архитектуры сыгравшие большую роль в развитие машинного обучения.
Часть 1: ResNet-18 — Архитектура, покорившая глубину
Пролог: Революция в компьютерном зрении
Представьте, что лингвист внезапно стал экспертом по живописи. Именно это произошло в 2020 году, когда архитектура для обработки текста — трансформеры — научилась «видеть» изображения (оригинальная статья An Image is Worth 16×16 Words). Vision Transformer (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 ключевых этапа:
-
Прямой проход:
-
5 токенов (4 патча + [CLS]) проходят через 12 блоков
-
На каждом шаге: Attention → MLP → LayerNorm
-
-
Классификация:
-
Только [CLS]-токен после последнего слоя → линейный классификатор
-
-
Расчёт ошибки:
-
Кросс-энтропия между предсказанием и меткой:
loss = -log(0.84) # Если метка "машина" -
-
Обратное распространение:
-
Градиенты обновляют:
-
Веса проецирования патчей (
Wиз шага 2) -
Параметры Q/K/V в каждом блоке
-
Веса MLP
-
Позиционные эмбеддинги (если обучаемые)
-
-
Почему ViT — это не всегда лучше ResNet?
✅ Сильные стороны ViT:
-
Глобальный контекст: Видит всю картинку сразу (свёртки видят только локальные области).
-
Масштабируемость: Чем больше данных — тем лучше качество (JFT-300M: 89.3% accuracy).
-
Унификация: Одна архитектура для текста, аудио и изображений.
❌ Слабые стороны ViT:
-
Данные: Требует в 10-100 раз больше обучающих изображений, чем ResNet.
-
Индуктивные ограничения: Плохо переносит изменения размера изображения (в отличие от свёрток).
-
Вычислительная сложность: Квадратичный рост времени работы относительно числа патчей.
Когда выбирать:
-
🤖 ViT: Если у вас >1 млн изображений и нужна state-of-the-art точность.
-
🚀 ResNet: Если данных мало (<100K) или нужна реальная скорость (мобильные приложения).
Философский итог
ViT не «убил» свёрточные сети — он показал, что внимание может быть универсальным механизмом обучения. Но как и в жизни, универсальных решений нет: ResNet остаётся рабочим инструментом, а ViT — мощным, но требовательным прорывом.
В следующей части: «Сначала мы учим модели видеть, потом — воображать. DiT — следующий шаг к искусственному воображению.»
P.S. Ошибки в статье? Хотите глубже разобрать attention? Пишите в комментариях!
Проверь себя
-
Почему патч 4×4 пикселей нельзя подавать напрямую в трансформер?
-
Зачем ViT [CLS]-токен, если можно усреднить все патчи?
ссылка на оригинал статьи https://habr.com/ru/articles/922868/
Добавить комментарий