Deep Learning · NLP

Attention &
Архитектура Трансформера

От нейрона до механизма внимания: подробная лекция с математикой, кодом и интуицией. Для тех, кто хочет по-настоящему понять, как работает GPT, BERT и всё, что после.

Матрицы & Градиенты Attention Mechanism Transformer Architecture Transfer Learning

🔍 Живой пример: Attention матрица

Каждая ячейка показывает, насколько сильно токен строки «смотрит» на токен столбца при обработке фразы. Чем ярче — тем больше внимания.

01

Основы искусственных нейронных сетей

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

💡
Ключевая идея ИНС — это универсальный аппроксиматор. Согласно теореме Хорника (1989), сеть с одним скрытым слоем и нелинейной функцией активации способна приблизить любую непрерывную функцию с произвольной точностью при достаточном количестве нейронов.
x₁ x₂ x₃ w₁ w₂ w₃ Σ +bias f(z) активация y z = Σ wᵢxᵢ + b y = f(z) Входы Нейрон Активация Выход

Конкретный числовой пример. Пусть нейрон получает x₁=1, x₂=0, x₃=−1, веса w₁=0.5, w₂=0.3, w₃=−0.8, смещение b=0.1. Тогда: z = 0.5·1 + 0.3·0 + (−0.8)·(−1) + 0.1 = 1.4. После ReLU: y = max(0, 1.4) = 1.4. Нейрон «активировался». Если бы z = −0.3, то y = 0 — нейрон «молчит».

Рассмотрим один полносвязный слой. Входной вектор x ∈ ℝⁿ преобразуется в выходной вектор y ∈ ℝᵐ:

z = W · x + b
y = f(z)
# W ∈ ℝᵐˣⁿ — матрица весов; b ∈ ℝᵐ — вектор смещений; f — функция активации

Для батча из B примеров: X ∈ ℝB×n, тогда Z = X @ W.T + b. Это единственная операция во всём слое — и именно поэтому современные ускорители (GPU/TPU) так эффективны: они заточены под умножение матриц (GEMM).

# PyTorch: вручную vs nn.Linear
import torch
import torch.nn as nn

B, n, m = 32, 128, 64
X = torch.randn(B, n)

# Вручную
W = torch.randn(m, n, requires_grad=True)
b = torch.zeros(m, requires_grad=True)
Z = X @ W.T + b          # (B, m)

# Через nn.Linear (то же самое внутри)
layer = nn.Linear(n, m)
Z = layer(X)             # (B, m)
Вход (n=4) x₁ x₂ x₃ x₄ Скрытый 1 (m=5) h₁ h₂ h₃ h₄ h₅ Скрытый 2 h₁ h₂ h₃ h₄ Выход (k=2) y₁ y₂ Матрица весов W¹: (5×4) = 20 параметров Матрица весов W²: (4×5) = 20 параметров Матрица весов W³: (2×4) = 8 параметров Итого: 48 + 14 (bias)

Что происходит при увеличении глубины: слой 1 учит простые комбинации признаков (например, «цена > 1M И площадь > 80м²»); слой 2 — комбинации этих комбинаций («новостройка В дорогом районе»); слой 3 — высокоуровневое решение («элитная недвижимость»). Именно иерархия представлений делает глубокие сети мощнее мелких.

Без нелинейности стек линейных слоёв остаётся линейным преобразованием. Функция активации ломает линейность, позволяя сети учить сложные зависимости.

ФункцияФормулаДиапазонПлюсы / Минусы
Sigmoid σ(x) = 1/(1+e⁻ˣ) (0, 1) Интерпретируема как вероятность; страдает от vanishing gradient на насыщении
Tanh tanh(x) = (eˣ−e⁻ˣ)/(eˣ+e⁻ˣ) (−1, 1) Центрирована в 0 — лучше sigmoid; но vanishing gradient остаётся
ReLU max(0, x) [0, +∞) Быстрая, нет насыщения при x>0; «умирающие нейроны» при x≤0
GELU x·Φ(x), Φ — CDF Гаусса ℝ Стандарт в трансформерах (BERT, GPT); дифференцируема везде
SiLU/Swish x·σ(x) ℝ Используется в LLaMA, PaLM; гладкая, немного лучше GELU на практике
⚠️
Проблема vanishing gradient Sigmoid и Tanh насыщаются: при |x| ≫ 0 производная стремится к 0. При глубоком стеке слоёв градиент, умноженный на множество таких малых чисел, экспоненциально уменьшается — сеть перестаёт учиться в нижних слоях. ReLU решает это частично; трансформеры решают это остаточными соединениями (см. ниже).
Формы функций активации (x от −3 до +3) 0 +1 −1 Sigmoid (0,1) насыщение Tanh (−1,+1) центрирована ReLU max(0,x) GELU гладкая, ≈ReLU

Обучение ИНС — это минимизация функции потерь L. Алгоритм обратного распространения вычисляет частные производные L по каждому параметру, применяя цепное правило дифференцирования от выхода к входу.

# Прямой проход (forward pass)
z¹ = W¹x + b¹
a¹ = f(z¹)
z² = W²a¹ + b²
ŷ = softmax(z²)
L = CrossEntropy(ŷ, y)

# Обратный проход (backward pass): цепное правило
∂L/∂W² = ∂L/∂z² · ∂z²/∂W²
∂L/∂W¹ = ∂L/∂z² · W² · f'(z¹) · x

# Обновление весов (SGD)
W ← W − η · ∂L/∂W
1
Forward pass: вычисляем предсказание ŷ, считаем потери L
2
Backward pass: идём от L назад по графу, применяя цепное правило: ∂L/∂wᵢ = ∂L/∂aᵢ · ∂aᵢ/∂wᵢ
3
Обновление: градиентный спуск (SGD, Adam, AdamW) корректирует параметры
4
Повторяем по батчам и эпохам, пока L не сходится
# PyTorch делает это автоматически (autograd)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

for x, y in dataloader:
    optimizer.zero_grad()        # Очищаем градиенты
    logits = model(x)            # Forward pass
    loss = criterion(logits, y)  # Считаем L
    loss.backward()             # Backward pass (autograd)
    optimizer.step()            # Обновляем параметры
Вычислительный граф: forward (→) и backward (←) x, W, b z=Wx+b linear a=f(z) activation ŷ=σ(a) softmax L(ŷ,y) loss → Forward pass ← Backward pass: ∂L/∂W = chain rule ∂L/∂ŷ ∂L/∂a ∂L/∂z ∂L/∂W W ← W − η·∂L/∂W gradient descent Цепное правило: ∂L/∂W = ∂L/∂ŷ · ∂ŷ/∂a · ∂a/∂z · ∂z/∂W
⚡
Оптимизаторы: от SGD до AdamW SGD: W ← W − η·∇W. Простой, медленно сходится. Momentum: добавляет «инерцию» — накапливает градиент, меньше осциллирует. Adam (2015): адаптивный LR для каждого параметра — усредняет первый момент (m̂) и второй (v̂) градиента, делит на √v̂. AdamW: Adam + weight decay без смешивания с градиентом — стандарт для трансформеров. При обучении LLM используют также gradient clipping (ограничение нормы градиента ≤1.0) для стабильности.
02

Основные архитектуры ИНС

Полносвязные сети (MLP / Dense)

Каждый нейрон слоя L соединён с каждым нейроном слоя L+1. Хорошо работают на табличных данных. Не используют пространственную или временную структуру входа — в этом их слабость.

Пример применения: классификация транзакций по набору числовых признаков, предсказание цены квартиры.

Свёрточные сети (CNN)

Применяют разделяемые по пространству фильтры (kernels). Фильтр 3×3 скользит по изображению и вычисляет свёртку — локальную взвешенную сумму. Ключевые свойства: локальная связность, разделение весов, инвариантность к трансляции.

# Свёрточный слой: обнаруживает локальные паттерны
conv = nn.Conv2d(
    in_channels=3,
    out_channels=64,
    kernel_size=3,
    padding=1
)
# Каждый из 64 фильтров 3×3 обнаруживает свой паттерн
# (края, текстуры, формы...)
Свёртка: фильтр 3×3 скользит по изображению 5×5 Вход (5×5) 1 2 0 3 1 Фильтр (3×3) -1 0 1 -2 0 2 -1 0 1 Sobel (вертикаль) Feature map (3×3) 8 8 обнаружен край! Свёртка = поэлементное умножение + сумма Σ(patch ⊙ filter) = вертикальный детектор края 9 весов на весь размер входа! ← weight sharing
🔍
Почему CNN работают для изображений? Кошка — это кошка, где бы она ни стояла на фото. CNN использует один и тот же фильтр для всего изображения (weight sharing), что резко снижает число параметров и даёт инвариантность к позиции.

Рекуррентные сети (RNN)

Для последовательных данных (текст, временные ряды) нужна память о прошлом. RNN хранит скрытое состояние hₜ, которое обновляется на каждом шаге:

hₜ = tanh(Wₕ · hₜ₋₁ + Wₓ · xₜ + b)
yₜ = Wᵧ · hₜ
# hₜ — «память» о всех предыдущих токенах

Проблема: при длинных последовательностях градиент при обратном проходе перемножается много раз на одну и ту же матрицу Wₕ. Если её спектральный радиус <1 — градиент исчезает (vanishing); >1 — взрывается (exploding). Это делает стандартный RNN практически непригодным для последовательностей длиннее ~20 шагов.

RNN развёрнутый во времени: h₀ → h₁ → h₂ → h₃ RNN t=0 RNN t=1 RNN t=2 RNN t=3 h₀=0 h₁ h₂ h₃ h₄… x₀ x₁ x₂ x₃ y₀ y₁ y₂ y₃ ∂L/∂h₀ = ∂L/∂h₄ · Wₕ⁴ — произведение 4 матриц → vanishing!

Разработан Хохрайтером и Шмидхубером (1997) специально для решения vanishing gradient. LSTM добавляет ячейку памяти cₜ и три гейта (вентиля), управляющих потоком информации:

fₜ = σ(Wf · [hₜ₋₁, xₜ] + bf) ← forget gate: что забыть
iₜ = σ(Wi · [hₜ₋₁, xₜ] + bi) ← input gate: что запомнить
g̃ₜ = tanh(Wg · [hₜ₋₁, xₜ] + bg) ← candidate
oₜ = σ(Wo · [hₜ₋₁, xₜ] + bo) ← output gate: что выдать

cₜ = fₜ ⊙ cₜ₋₁ + iₜ ⊙ g̃ₜ ← обновление памяти
hₜ = oₜ ⊙ tanh(cₜ) ← скрытое состояние

Символ ⊙ — поэлементное умножение. Ключевое: ячейка памяти cₜ распространяется через сложение, а не умножение — это создаёт «шоссе» для градиентов, решая vanishing gradient.

GRU (Gated Recurrent Unit)

Упрощённая версия LSTM (Чо и др., 2014): только два гейта вместо трёх, нет отдельной ячейки памяти. Быстрее в обучении, на практике часто не уступает LSTM.

zₜ = σ(Wz · [hₜ₋₁, xₜ]) ← update gate
rₜ = σ(Wr · [hₜ₋₁, xₜ]) ← reset gate
h̃ₜ = tanh(W · [rₜ ⊙ hₜ₋₁, xₜ])
hₜ = (1 − zₜ) ⊙ hₜ₋₁ + zₜ ⊙ h̃ₜ
🌀

RNN

Простая рекуррентность. Плохо масштабируется на длинные последовательности из-за vanishing gradient.

🧠

LSTM

Три гейта + ячейка памяти. Долгий SOTA в NLP до 2018 года. Сложнее GRU, чуть мощнее.

⚡

GRU

Два гейта, нет cell state. Быстрее LSTM, часто сопоставим по качеству. Популярен в production.

🔄

Трансформер

Без рекуррентности вообще. Параллелен, масштабируется. Вытеснил RNN/LSTM почти везде.

🎯
Ахиллесова пята RNN-семейства Все рекуррентные сети обрабатывают последовательность последовательно — шаг за шагом. Это не параллелизуется. При обучении на длинном тексте GPU простаивает, ожидая результата предыдущего шага. Трансформер обрабатывает всю последовательность одновременно — именно это открыло эру масштабирования LLM.
03

Архитектура трансформера

Трансформер предложен в статье «Attention Is All You Need» (Vaswani et al., Google, 2017). Основная идея: полностью отказаться от рекуррентности и свёрток, заменив их механизмом self-attention, который позволяет каждому токену напрямую «смотреть» на любой другой токен последовательности.

ENCODER Input Embeddings Positional Encoding ENCODER LAYER (×N) Multi-Head Self-Attention Add & LayerNorm Feed-Forward (FFN) Encoder Output (K, V) «The cat sat on...» DECODER Output Embeddings (shifted) Positional Encoding DECODER LAYER (×N) Masked Self-Attention Add & LayerNorm Cross-Attention (Q←dec, K,V←enc) Add & LayerNorm Feed-Forward (FFN) Linear + Softmax → Вероятности токенов «Кот сидел на...» (target) K, V

Self-Attention: механизм внимания

Self-attention — сердце трансформера. Для каждого токена в последовательности он вычисляет взвешенную сумму значений всех остальных токенов, где веса отражают «релевантность» каждого токена для текущего.

Из входного вектора каждого токена x создаются три вектора через три разные линейные проекции:

Q = X · Wq ← Query: «что я ищу?»
K = X · Wk ← Key: «что я предлагаю?»
V = X · Wv ← Value: «что я передам, если буду выбран?»

# Скалярное произведение Q·Kᵀ измеряет похожесть запроса и ключа
scores = Q · Kᵀ / √dₖ
weights = softmax(scores) ← нормировка в вероятности (строки суммируются в 1)
output = weights · V ← взвешенная сумма значений
🔢
Зачем делить на √dₖ? При большой размерности dₖ скалярные произведения Q·Kᵀ растут по величине, что приводит к насыщению softmax (веса стремятся к 0 или 1, а градиенты исчезают). Деление на √dₖ нормирует дисперсию и стабилизирует обучение. Это эквивалентно тому, что если компоненты Q и K — независимые случайные переменные с μ=0, σ=1, то Q·K имеет дисперсию dₖ, а после деления — дисперсию 1.
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q: (batch, heads, seq_len, d_k)
    K: (batch, heads, seq_len, d_k)
    V: (batch, heads, seq_len, d_v)
    """
    d_k = Q.shape[-1]
    
    # Матрица оценок: насколько каждый токен внимателен к другим
    scores = Q @ K.transpose(-2, -1) / d_k ** 0.5  # (B, H, T, T)
    
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)  # Маскируем будущее
    
    weights = F.softmax(scores, dim=-1)
    
    return weights @ V, weights  # Взвешенная сумма + сами веса

Multi-Head Attention

Одна пара Q, K, V захватывает только один тип зависимостей. Multi-Head Attention запускает h независимых «голов» внимания параллельно, каждая с проекциями меньшей размерности dₖ = d_model / h. Результаты конкатенируются и проецируются обратно.

headᵢ = Attention(Q·Wqᵢ, K·Wkᵢ, V·Wvᵢ)

MultiHead(Q, K, V) = Concat(head₁, ..., headₕ) · Wₒ

# GPT-3: d_model=12288, h=96, dₖ=128
# BERT-base: d_model=768, h=12, dₖ=64

Зачем несколько голов? Разные головы специализируются на разных типах отношений:

📍

Позиционная голова

Обращает внимание на соседние токены — локальный контекст.

🔗

Синтаксическая голова

Учит зависимости между подлежащим и сказуемым, независимо от расстояния.

🌐

Семантическая голова

Связывает кореференции: «она» → «Мария» через весь абзац.

❓

Задача-специфичная

Fine-tuning развивает головы под конкретный тип задачи.

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.d_k = d_model // n_heads
        self.n_heads = n_heads
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, Q, K, V, mask=None):
        B = Q.shape[0]
        # Проецируем и разбиваем по головам
        Q = self.W_q(Q).view(B, -1, self.n_heads, self.d_k).transpose(1,2)
        K = self.W_k(K).view(B, -1, self.n_heads, self.d_k).transpose(1,2)
        V = self.W_v(V).view(B, -1, self.n_heads, self.d_k).transpose(1,2)

        attn_out, _ = scaled_dot_product_attention(Q, K, V, mask)
        # Конкатенируем головы и проецируем обратно
        attn_out = attn_out.transpose(1,2).contiguous().view(B, -1, self.n_heads * self.d_k)
        return self.W_o(attn_out)

Positional Encoding

Self-attention не знает, в каком порядке стоят токены — операция симметрична. Чтобы передать позиционную информацию, к эмбеддингам прибавляют позиционное кодирование.

В оригинальной статье — детерминированные синусоиды разных частот:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

# pos — позиция токена; i — индекс измерения
# Низкие частоты кодируют грубую позицию, высокие — тонкую

Почему синусоиды? Модель может обобщаться на длины, которых не видела при обучении (например, обучалась на 512 токенах, а инференс на 1024). Современные модели (RoPE в LLaMA, ALiBi в MPT) используют обучаемые позиционные кодирования или относительные смещения — они лучше экстраполируют.

Residual-соединения и Layer Normalization

Каждый подблок трансформера (Attention и FFN) обёрнут в residual connection (остаточное соединение):

output = LayerNorm(x + SubLayer(x))

# SubLayer — это Attention или FFN
# x прибавляется «напрямую» — создаёт градиентное шоссе

Зачем residual? Идея из ResNet (He et al., 2015): при обратном распространении градиент идёт по двум путям — через SubLayer (сложный) и напрямую через сложение (+). Прямой путь гарантирует, что градиент дойдёт до нижних слоёв без затухания, даже если SubLayer выдаёт малый градиент.

Layer Normalization нормирует активации по признакам (не по батчу как Batch Norm), что стабилизирует обучение независимо от размера батча:

LN(x) = γ · (x − μ) / (σ + ε) + β
# μ, σ — среднее и стандартное отклонение по d_model измерениям
# γ, β — обучаемые параметры масштаба и сдвига
📐
Pre-LN vs Post-LN В оригинальном трансформере LayerNorm стоит после сложения (Post-LN). В GPT-2 и далее — перед SubLayer (Pre-LN): x + SubLayer(LN(x)). Pre-LN обучается стабильнее без warmup и стал стандартом в LLM.

Feed-Forward Sublayer и полная архитектура

После attention каждый токен проходит через позиционно-независимую FFN — одинаковую для всех позиций, но с разными активациями:

FFN(x) = GELU(x · W₁ + b₁) · W₂ + b₂
# W₁: d_model → 4·d_model (расширение)
# W₂: 4·d_model → d_model (сжатие)
# В GPT-3: d_model=12288, inner=49152 (4×) — 49K нейронов на позицию!

FFN работает как ассоциативная память: каждый нейрон первого слоя — это «паттерн», активирующийся при определённом контексте. Исследования показывают, что фактические знания (факты) во многом хранятся именно в весах FFN, а не в attention.

04

Transfer Learning

Transfer learning — обучение модели на одной задаче (обычно большой и дешёвой в получении данных), а затем перенос выученных представлений для решения другой задачи (часто маленькой и дорогостоящей в разметке).

🚀
Почему это работает? Нижние слои сети учат универсальные признаки: края и текстуры в CNN; синтаксис и морфологию в LLM. Верхние слои — задача-специфичные признаки. Перенос позволяет «бесплатно» получить нижние слои, обученные на гигантских данных, и дообучить только верхние.

Парадигма в NLP: Pretrain → Fine-tune

1
Предобучение (Pretraining): модель обучается на огромном корпусе (Common Crawl, Wikipedia, GitHub) на self-supervised задаче — предсказать следующий токен (GPT) или восстановить замаскированный токен (BERT). Данных — триллионы токенов, вычислений — тысячи GPU-дней.
2
Fine-tuning (тонкая настройка): берём предобученные веса и дообучаем на размеченных данных целевой задачи. Данных может быть всего несколько тысяч примеров. Обучение — часы на одном GPU.
3
RLHF / Instruction Tuning (опционально): после fine-tuning — дополнительное выравнивание на предпочтения человека (как в ChatGPT, Claude).

BERT vs GPT: два стиля предобучения

ПараметрBERT (2018, Google)GPT (2018–2023, OpenAI)
АрхитектураEncoder-onlyDecoder-only
Задача pretrainingMasked LM (MLM): восстановить 15% замаскированных токеновCausal LM: предсказать следующий токен
Направление вниманияДвустороннее (bidirectional)Одностороннее (left-to-right)
Сильные стороныКлассификация, NER, QA с ответомГенерация текста, reasoning, few-shot
Типичный fine-tune+ classification head, 3 эпохиPrompt-based, RLHF
Современные потомкиRoBERTa, DeBERTa, ModernBERTGPT-4, LLaMA 3, Claude, Gemini

Практические стратегии Fine-tuning

❄️

Feature Extraction

Замораживаем всю предобученную модель. Используем выходные эмбеддинги как признаки для отдельного классификатора. Быстро, мало памяти.

🔥

Full Fine-tuning

Обновляем все веса. Максимальное качество, но дорого и опасно — можно перетренировать или «забыть» предобученные знания.

🎯

LoRA / QLoRA

Добавляем low-rank матрицы ΔW = A·B к весам. Обучаем только их (~0.1% параметров). SOTA эффективность. Стандарт для LLM.

💬

Prompt Tuning

Обучаем только «мягкие токены» (soft prompts), добавленные к входу. Экстремально эффективно по памяти.

# LoRA: добавляем обучаемые матрицы к заморозенным весам
from peft import get_peft_model, LoraConfig, TaskType

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,           # ранг матриц A, B
    lora_alpha=32, # масштабирующий коэффициент
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"]  # только Q и V attention
)
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 6,742,609,920 || 0.06%
⚠️
Catastrophic Forgetting При агрессивном fine-tuning модель может «забыть» знания из pretraining. Лечится: низкий learning rate (1e-5 – 5e-5), warmup scheduler, LoRA, или добавление данных pretraining в смесь для fine-tuning.

Zero-shot и Few-shot Learning

Мощные языковые модели (GPT-4, Claude, LLaMA 3) демонстрируют emergent-способности:

0
Zero-shot: описываем задачу в prompt, модель решает без примеров. «Переведи на французский: Hello world»
k
Few-shot: даём 2–10 примеров в контексте (in-context learning). Модель угадывает паттерн без обновления весов — только чтение контекста.
05

Сводная таблица и дорожная карта

КонцепцияЗачемБез чего не обойтись
Матрицы весов WЛинейное преобразование признаковОснова любого слоя
Функция активацииНелинейность → сложные функцииReLU/GELU в каждом слое
BackpropВычислить градиенты для обновленияAutograd в PyTorch/JAX
CNNЛокальные паттерны в изображенияхWeight sharing + pooling
LSTM/GRUДолгосрочная память в последовательностяхГейты управляют потоком
Self-AttentionГлобальные зависимости, параллелизмQ·Kᵀ/√dₖ → softmax → V
Multi-HeadНесколько типов зависимостей одновременноh голов → concat → проекция
Positional EncodingПорядок токенов в attentionSinusoidal или RoPE
Residual + LNСтабильность обучения глубоких сетейx + SubLayer(LN(x))
Transfer LearningНе учить с нуля, переиспользовать знанияPretraining → fine-tuning
LoRAДешёвый fine-tuning больших моделейΔW = A·B, обучаем A и B
🗺️
Что читать дальше Оригинальные статьи: «Attention Is All You Need» (2017), «BERT» (2018), «Language Models are Few-Shot Learners» GPT-3 (2020), «LoRA» (2021), «LLaMA 2» (2023). Практика: реализуй мини-трансформер с нуля — Andrej Karpathy's «nanoGPT» (GitHub) — 300 строк, полная архитектура.