← Назад к вопросам

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

1.6 Junior🔥 142 комментариев
#NLP и обработка текста#Глубокое обучение

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

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура энкодера BERT

BERT (Bidirectional Encoder Representations from Transformers) — это модель-энкодер на основе Transformers. Расскажу про её архитектуру слой за слоем.

Общая архитектура

Текст → Токенизация → Эмбеддинги → 12-24 блока Transformer Encoder → Output

Фаза 1: Токенизация

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "Hello world"
tokens = tokenizer.encode(text)
token_strings = tokenizer.convert_ids_to_tokens(tokens)

print(tokens)  # [101, 7592, 2088, 102]
print(token_strings)  # ['[CLS]', 'hello', 'world', '[SEP]']

BERT использует WordPiece токенизацию:

  • [CLS] — специальный токен для начала (используется для классификации)
  • [SEP] — разделитель между предложениями
  • указывает продолжение слова (subword)

Фаза 2: Эмбеддинги

Token Embeddings

Каждому токену соответствует плотный вектор размером 768 (для BERT-base).

# Таблица эмбеддингов: 30,522 токена × 768 размер
token_embeddings = nn.Embedding(30522, 768)

Positional Embeddings

Трансформеры не знают о порядке токенов. Добавляют позиционные эмбеддинги:

# Каждой позиции своё представление
position_embeddings = nn.Embedding(512, 768)  # max 512 позиций

Segment Embeddings (для двух предложений)

# [CLS] Предложение A [SEP] Предложение B [SEP]
# Segment ID: [0, 0, 0, ..., 1, 1, 1]
segment_embeddings = nn.Embedding(2, 768)  # 0 или 1

Комбинация эмбеддингов

embeddings = token_embeddings + position_embeddings + segment_embeddings
embeddings = LayerNorm(embeddings)  # Нормализация
embeddings = Dropout(embeddings)    # Dropout 0.1

Фаза 3: Transformer Encoder Block (основной компонент)

BERT содержит 12 (base) или 24 (large) стекованных блоков.

Multi-Head Self-Attention

# Формула:
# Attention(Q, K, V) = softmax(Q @ K.T / sqrt(d_k)) @ V

# Где:
Q = X @ W_Q  # Query
K = X @ W_K  # Key  
V = X @ W_V  # Value

# Multi-head: 12 голов параллельно
# Размер: 768 / 12 = 64 per head

class MultiHeadAttention(nn.Module):
    def __init__(self, hidden_size=768, num_heads=12):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads
        
        self.W_q = nn.Linear(hidden_size, hidden_size)
        self.W_k = nn.Linear(hidden_size, hidden_size)
        self.W_v = nn.Linear(hidden_size, hidden_size)
        self.W_o = nn.Linear(hidden_size, hidden_size)  # Output projection
    
    def forward(self, x):
        batch_size = x.shape[0]
        
        # Linear projections
        Q = self.W_q(x)  # (batch, seq, 768)
        K = self.W_k(x)
        V = self.W_v(x)
        
        # Reshape для multi-head (batch, seq, heads, head_dim)
        Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        
        # Attention scores
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.head_dim)
        # scores: (batch, heads, seq, seq)
        
        attn_weights = F.softmax(scores, dim=-1)
        attn_output = attn_weights @ V
        
        # Объединить головы
        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.view(batch_size, -1, 768)
        
        # Финальная проекция
        output = self.W_o(attn_output)
        return output, attn_weights

Feed-Forward Network

# Простая двухслойная сеть
# 768 → 3072 → 768 (промежуточный слой в 4x больше)

class FeedForward(nn.Module):
    def __init__(self, hidden_size=768, ffn_size=3072):
        super().__init__()
        self.linear1 = nn.Linear(hidden_size, ffn_size)
        self.gelu = nn.GELU()  # BERT использует GELU, не ReLU
        self.linear2 = nn.Linear(ffn_size, hidden_size)
    
    def forward(self, x):
        return self.linear2(self.gelu(self.linear1(x)))

Полный блок с Residual Connections и LayerNorm

class TransformerEncoderBlock(nn.Module):
    def __init__(self):
        super().__init__()
        self.attention = MultiHeadAttention()
        self.norm1 = nn.LayerNorm(768)
        self.ffn = FeedForward()
        self.norm2 = nn.LayerNorm(768)
        self.dropout = nn.Dropout(0.1)
    
    def forward(self, x):
        # Multi-head attention с residual connection
        # attn_output = attention(x)
        # x = LayerNorm(x + dropout(attn_output))
        attn_output, _ = self.attention(x)
        x = self.norm1(x + self.dropout(attn_output))
        
        # Feed-forward с residual connection
        ffn_output = self.ffn(x)
        x = self.norm2(x + self.dropout(ffn_output))
        
        return x

Фаза 4: Полная архитектура

class BERT(nn.Module):
    def __init__(self):
        super().__init__()
        
        # Embeddings
        self.word_embeddings = nn.Embedding(30522, 768)
        self.position_embeddings = nn.Embedding(512, 768)
        self.segment_embeddings = nn.Embedding(2, 768)
        self.norm = nn.LayerNorm(768)
        self.dropout = nn.Dropout(0.1)
        
        # 12 Transformer encoder blocks
        self.encoder = nn.ModuleList([
            TransformerEncoderBlock() for _ in range(12)
        ])
    
    def forward(self, input_ids):
        batch_size, seq_length = input_ids.shape
        
        # Embedding слой
        x = self.word_embeddings(input_ids)
        x += self.position_embeddings(torch.arange(seq_length, device=input_ids.device))
        x += self.segment_embeddings(torch.zeros_like(input_ids))
        x = self.norm(x)
        x = self.dropout(x)
        
        # Проходим через encoder блоки
        for encoder_block in self.encoder:
            x = encoder_block(x)
        
        return x  # (batch_size, seq_length, 768)

Фаза 5: Использование в задачах

Классификация (используем [CLS] токен)

# [CLS] токен — специальный, его берут для классификации
output = model(input_ids)  # (batch, seq_len, 768)
cls_token = output[:, 0, :]  # Берём только первый токен (batch, 768)

classifier = nn.Linear(768, num_classes)
logits = classifier(cls_token)

Token Classification (NER)

# Используем выход для каждого токена
output = model(input_ids)  # (batch, seq_len, 768)
token_classifier = nn.Linear(768, num_classes)
token_logits = token_classifier(output)  # (batch, seq_len, num_classes)

Ключевые характеристики

BERT-baseBERT-large
Hidden Size: 768Hidden Size: 1024
Layers: 12Layers: 24
Heads: 12Heads: 16
Parameters: 110MParameters: 340M

Практический пример с Hugging Face

from transformers import BertModel, BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

inputs = tokenizer("Hello, how are you?", return_tensors='pt')
outputs = model(**inputs)

token_embeddings = outputs[0]  # (1, seq_len, 768)
pooled_output = outputs[1]      # (1, 768) — для классификации

print(token_embeddings.shape)   # torch.Size([1, 7, 768])
print(pooled_output.shape)      # torch.Size([1, 768])

Итог

Архитектура BERT:

  1. Embedding слой — token + position + segment embeddings
  2. 12/24 Encoder Blocks — каждый: Multi-Head Attention → FFN
  3. Residual Connections — в каждом подслое
  4. LayerNorm — после каждого подслоя
  5. [CLS] токен — для задач классификации
  6. Output — контекстуальные представления всех токенов

Эта архитектура стала стандартом и основой для большинства современных NLP моделей.