Ответ
Энкодер BERT — это стек из L идентичных слоев (L=12 или 24). Каждый слой трансформер-энкодера обрабатывает последовательность векторов, обогащая их контекстуальной информацией со всех позиций.
Структура одного слоя энкодера:
- 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. Это позволяет модели фокусироваться на разных типах зависимостей.
- Входные эмбеддинги
- Добавление и нормализация (Add & Norm): К исходному входу слоя
Xприбавляется выход механизма внимания (остаточная связь), и результат нормализуется через LayerNorm. - Position-wise Feed-Forward Network (FFN): Это два линейных слоя с активацией GELU между ними. Применяется независимо к каждому токену в последовательности. Формула:
FFN(x) = GELU(xW1 + b1)W2 + b2. - Второе 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
Стек таких слоев последовательно преобразует входные эмбеддинги, на каждом шаге интегрируя информацию со всей последовательности.
Ответ 18+ 🔞
Давай разберем, как эта штука работает, а то звучит страшно, а на деле — просто слоёный пирог из математики, только безвкусный.
Представь, что у тебя есть текст, разбитый на кусочки (токены). BERT берёт каждый кусочек, превращает в цифровой вектор (эмбеддинг) и начинает его прогонять через свою мясорубку. А мясорубка эта — просто L одинаковых слоёв, поставленных друг на друга. L — это обычно 12 или 24, то есть овердохуища вычислений. Каждый слой — это один и тот же конвейер, который немного меняет вектора, добавляя в них понимание контекста.
Из чего состоит один такой слой, этот самый «трансформер-энкодер»?
-
Multi-Head Self-Attention (Многоголовая Само-Внимательность):
- Берём наши вектора
Xи делаем из них три копии, но каждая — немного разная. Называются они Query (Вопрос), Key (Ключ) и Value (Значение). Чисто технически — умножаем на три разные матрицы. - Self-Attention — это хитрая жопа. Он смотрит, насколько каждый «вопрос» от одного слова похож на «ключи» от ВСЕХ слов в предложении, включая его самого. Получаются веса — кто на кого влияет. Потом этими весами взвешиваются «значения» от всех слов и суммируются. В итоге каждый вектор теперь знает про всех соседей. Формула для одной головы:
Attention(Q,K,V) = softmax(QK^T / sqrt(d_k))V. - Multi-Head — это когда такая операция делается не один раз, а
hраз параллельно, но с разными проекциями. Каждая «голова» учится смотреть на разные типы связей: одна — на соседние слова, другая — на грамматическое согласование, третья — хуй знает на что. Потом результаты всех голов склеиваются и проецируются обратно. Это как смотреть на ситуацию с разных углов, чтобы не быть, прости господи, полупидором с однобоким мнением.
- Берём наши вектора
-
Добавление и нормализация (Add & Norm):
- К исходному вектору, который пришёл на вход слоя (
X), прибавляется тот вектор, который получился после внимания. Это остаточная связь — гениальная штука, которая не даёт градиентам исчезнуть и позволяет сети учиться просто «поправками» к уже имеющемуся сигналу. - Потом всё это дело прогоняется через LayerNorm, чтобы значения не улетали в космос и обучение было стабильным. Без этого всё накрывается медным тазом на первых же итерациях.
- К исходному вектору, который пришёл на вход слоя (
-
Position-wise Feed-Forward Network (FFN):
- Это просто маленькая полносвязная нейросеть, которая применяется КАЖДОМУ токену в последовательности ОТДЕЛЬНО (поэтому position-wise). Состоит из двух линейных слоёв и активации GELU между ними:
FFN(x) = GELU(xW1 + b1)W2 + b2. - Её задача — нелинейно преобразовать информацию, которую собрал механизм внимания. Можно думать об этом как о «переваривании» контекста.
- Это просто маленькая полносвязная нейросеть, которая применяется КАЖДОМУ токену в последовательности ОТДЕЛЬНО (поэтому position-wise). Состоит из двух линейных слоёв и активации GELU между ними:
-
И ещё раз Add & Norm:
- Выход этой маленькой сети снова прибавляется к тому, что было на входе блока FFN, и снова нормализуется. Остаточные связи везде, ёпта!
Чем этот энкодер BERT отличается от декодера в классическом трансформере?
- Внимание у него полное (не маскированное). Декодеру, который генерирует текст по слову, нельзя подсматривать в будущее. А BERT в задачах предобучения (типа Masked Language Model — «угадай пропущенное слово») должен видеть ВЕСЬ контекст, и слева, и справа. Поэтому он смотрит на все слова сразу — доверия ебать ноль к нему, он всё про тебя знает.
- Нет блока 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
И вот этот слой, как кирпичик, ставится друг на друга L раз. Каждый следующий слой получает на вход обогащённые вектора от предыдущего и добавляет в них ещё немного понимания. В итоге, пройдя всю эту башню, изначальные «тупые» эмбеддинги превращаются в умные вектора, которые знают про каждое слово всё: и что оно значит, и как связано с другими. Вот и вся магия, а остальное — матричные умножения, да похуй.