← Назад к вопросам
Как устроена архитектура энкодера на примере 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-base | BERT-large |
|---|---|
| Hidden Size: 768 | Hidden Size: 1024 |
| Layers: 12 | Layers: 24 |
| Heads: 12 | Heads: 16 |
| Parameters: 110M | Parameters: 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:
- Embedding слой — token + position + segment embeddings
- 12/24 Encoder Blocks — каждый: Multi-Head Attention → FFN
- Residual Connections — в каждом подслое
- LayerNorm — после каждого подслоя
- [CLS] токен — для задач классификации
- Output — контекстуальные представления всех токенов
Эта архитектура стала стандартом и основой для большинства современных NLP моделей.