Архитектурные принципы трансформера
Трансформер — модель, построенная полностью на механизме внимания, без рекуррентных или сверточных слоёв. Основные блоки включают эмбеддинги входных токенов, позиционное кодирование, многоголовое внимание (Multi‑Head Attention), позиционно‑зависимый слой обратного распространения (Feed‑Forward) и слой нормализации. В типичном варианте трансформер состоит из стека идентичных энкодер‑ и декодер‑слоёв, каждый из которых повторяет вышеописанные операции. При работе с небольшими задачами можно упростить структуру до одного энкодера и одного декодера, сохранив при этом все ключевые элементы.
Эмбеддинги и позиционное кодирование
Токенизатор преобразует входную последовательность в индексы, после чего каждый индекс отображается в вектор фиксированной размерности — эмбеддинг. В NumPy эмбеддинги реализуются как матрица E ∈ ℝ^{V×d}, где V — размер словаря, а d — размерность скрытого пространства. Для обучения эмбеддинги обычно инициализируются случайными значениями и оптимизируются вместе с остальными параметрами.
Позиционное кодирование добавляет информацию о порядке токенов. Классический вариант использует синусоиды разных частот:
PE[pos, 2i] = sin(pos / 10000^{2i/d})
PE[pos, 2i+1] = cos(pos / 10000^{2i/d})
В NumPy эту матрицу удобно построить один раз и складывать с эмбеддингом перед подачей в слой внимания.
Многоголовое внимание
Многоголовое внимание разбивает скрытое пространство на h независимых подпространств, позволяя модели одновременно учитывать различные типы взаимосвязей. Для каждой головы вычисляются запросы Q, ключи K и значения V через линейные проекции:
Q = X @ W_Q # X – вход, W_Q ∈ ℝ^{d×d_k}
K = X @ W_K
V = X @ W_V
Затем применяется скалярное произведение запросов и ключей, масштабируется и проходит через софтмакс:
scores = (Q @ K.T) / sqrt(d_k)
weights = softmax(scores, axis=-1)
head = weights @ V
Все головы конкатенируются и пропускаются через финальную линейную проекцию W_O. Реализация в NumPy требует аккуратного управления формами массивов; обычно используют reshape и transpose для разделения и объединения голов.
Feed‑Forward слой и нормализация
После внимания каждый токен проходит через позиционно‑независимый двухслойный полносвязный блок:
FFN(x) = max(0, x @ W_1 + b_1) @ W_2 + b_2
В качестве активации часто выбирают ReLU. Для стабилизации обучения к каждому блоку добавляют слой слой‑нормализации (LayerNorm) и остаточное соединение (Residual):
x = x + Sublayer(x)
x = LayerNorm(x)
LayerNorm рассчитывается как:
μ = mean(x, axis=-1, keepdims=True)
σ = std(x, axis=-1, keepdims=True)
x_norm = (x - μ) / (σ + ε) * γ + β
Параметры γ и β обучаются совместно с другими весами.
Процедурная реализация на NumPy
Код реализуется в виде набора функций, каждая из которых принимает массивы и возвращает результат без использования объектно‑ориентированных конструкций. Основные функции:
embed(tokens, E): возвращает эмбеддинги токенов.add_positional_encoding(x, PE): складывает позиционное кодирование.multi_head_attention(x, W_Q, W_K, W_V, W_O, h): реализует многоголовое внимание.feed_forward(x, W_1, b_1, W_2, b_2).layer_norm(x, gamma, beta, eps=1e-6).transformer_block(x, params): объединяет все шаги в один блок.
Все параметры собираются в словарь params, что упрощает их передачу в функции обучения и позволяет легко менять размерность, количество голов и глубину модели.
Цикл обучения и пакетная обработка
Для обучения трансформера используется кросс‑энтропийный loss между предсказанными токенами и целевыми. Пакетный ввод реализуется через массивы формы (batch_size, seq_len). Внутри цикла:
- Вычисляются эмбеддинги и позиционное кодирование.
- Последовательно применяются блоки энкодера и декодера.
- На выходе получаем логиты
logits ∈ ℝ^{batch_size×seq_len×V}. - Вычисляется
loss = -sum(logits[range, target]) / (batch_size * seq_len). - Градиенты рассчитываются вручную через обратное распространение (или с помощью
autograd‑библиотеки, если требуется ускорение). - Параметры обновляются с помощью оптимизатора SGD или Adam.
Для небольших экспериментов достаточно 2‑3 эпох обучения на синтетических данных, что позволяет быстро увидеть, как модель начинает генерировать осмысленные последовательности.
Практические рекомендации и расширения
- Инициализация весов: применяйте метод Кайнана (He) для линейных слоёв и стандартную инициализацию для внимания (
W_Q,W_K,W_V). - Маскирование: в декодере добавьте нижнюю треугольную маску, чтобы токен не «видел» будущие позиции.
- Отладка: выводите формы промежуточных тензоров после каждого блока, чтобы убедиться в корректности
reshape‑операций. - Ускорение: при необходимости замените NumPy на CuPy или JAX, сохранив ту же структуру кода.
- Эксперименты: меняйте количество голов, размер скрытого пространства и глубину стека блоков, наблюдая влияние на качество генерации.
Создание трансформера с нуля на чистом NumPy демонстрирует, как фундаментальные математические операции превращаются в мощную модель естественного языка. Такой подход позволяет полностью контролировать каждую часть процесса, от построения эмбеддингов до обучения, и служит отличной базой для дальнейших исследований и кастомных решений.