Как устроен энкодер в архитектуре BERT?

«Как устроен энкодер в архитектуре BERT?» — вопрос из категории NLP и трансформеры, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Энкодер BERT — это стек из L идентичных слоев (L=12 или 24). Каждый слой трансформер-энкодера обрабатывает последовательность векторов, обогащая их контекстуальной информацией со всех позиций.

Структура одного слоя энкодера:

  1. Multi-Head Self-Attention:
    • Входные эмбеддинги X (размерность d_model) проецируются в три матрицы: Query (Q), Key (K), Value (V).
    • Self-Attention вычисляет взвешенную сумму значений V, где веса определяются совместимостью Q и K. Формула для одной "головы": Attention(Q,K,V) = softmax(QK^T / sqrt(d_k))V.
    • Multi-Head означает, что это вычисление параллельно выполняется h раз с разными проекционными матрицами, а результаты конкатенируются и проецируются обратно в размерность d_model. Это позволяет модели фокусироваться на разных типах зависимостей.
  2. Добавление и нормализация (Add & Norm): К исходному входу слоя X прибавляется выход механизма внимания (остаточная связь), и результат нормализуется через LayerNorm.
  3. Position-wise Feed-Forward Network (FFN): Это два линейных слоя с активацией GELU между ними. Применяется независимо к каждому токену в последовательности. Формула: FFN(x) = GELU(xW1 + b1)W2 + b2.
  4. Второе Add & Norm: Выход FFN снова складывается с входом этого блока и нормализуется.

Ключевые отличия энкодера BERT от декодера в оригинальном трансформере:

  • Используется полное (не маскированное) Self-Attention, так как задача предобучения (MLM) требует доступа ко всему контексту.
  • Отсутствует механизм Encoder-Decoder Attention, так как BERT — чисто энкодерная модель.

Упрощенная реализация на PyTorch для понимания:

import torch
import torch.nn as nn
import torch.nn.functional as F

class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model=768, n_heads=12, ff_dim=3072):
        super().__init__()
        self.self_attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
        self.linear1 = nn.Linear(d_model, ff_dim)
        self.linear2 = nn.Linear(ff_dim, d_model)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(0.1)

    def forward(self, src):
        # 1. Multi-Head Self-Attention с остаточной связью
        attn_output, _ = self.self_attn(src, src, src)
        src = src + self.dropout(attn_output)
        src = self.norm1(src)

        # 2. Position-wise Feed-Forward Network с остаточной связью
        ff_output = self.linear2(F.gelu(self.linear1(src)))
        src = src + self.dropout(ff_output)
        src = self.norm2(src)
        return src

Стек таких слоев последовательно преобразует входные эмбеддинги, на каждом шаге интегрируя информацию со всей последовательности.