Блок трансформера: attention, feed-forward, residual, нормализация
Трансформер собран из повторяющихся блоков. Разберём один такой блок по кирпичикам — это шаблон, который стопкой даёт всю модель.
Блок трансформера — повторяемый модуль из слоя внимания и полносвязной сети, обёрнутых остаточными связями и нормализацией.
Четыре составные части
Один блок содержит:
- Multi-head attention — токены обмениваются информацией (это мы уже разобрали).
- Feed-forward сеть (FFN) — небольшая полносвязная сеть, применяемая к каждому токену отдельно.
- Остаточные связи (residual) — выход каждого подслоя прибавляется к его входу.
- Нормализация слоя (LayerNorm) — приводит активации к стабильному масштабу.
Feed-forward: «подумать» над каждым токеном
После того как внимание собрало контекст, к каждому токену по отдельности применяется одна и та же маленькая сеть из двух линейных слоёв с нелинейностью между ними. Если внимание отвечает за «обмен информацией между токенами», то FFN — за «обработку» собранной информации внутри токена. Внутренний слой обычно в 4 раза шире — там у модели «пространство для размышлений». Любопытно, что именно в FFN-слоях, как считают, хранится значительная часть фактических знаний модели.
Остаточные связи и нормализация
Два приёма делают глубокие стопки блоков обучаемыми. Residual: вместо того чтобы заменять вход, подслой добавляет к нему поправку (x + f(x)) — так исходный сигнал свободно проходит сквозь десятки слоёв, а градиенты не затухают. LayerNorm: нормализует вектор к нулевому среднему и единичному разбросу, чтобы числа не «разлетались». Посмотрим оба приёма в действии.
import math
# Остаточная связь (residual): выход слоя ПРИБАВЛЯЕТСЯ ко входу,
# а не заменяет его. Это сохраняет исходную информацию и помогает обучению.
x = [1.0, 2.0, 3.0] # вход в подслой
sublayer_out = [0.1, -0.2, 0.05] # что насчитал подслой (например, attention)
residual = [a + b for a, b in zip(x, sublayer_out)]
print("вход x: ", x)
print("выход подслоя: ", sublayer_out)
print("после residual x+f:", [round(v, 2) for v in residual])
# Нормализация слоя (LayerNorm): приводим вектор к нулевому среднему и единичной дисперсии.
def layer_norm(vec, eps=1e-5):
mean = sum(vec) / len(vec)
var = sum((v - mean) ** 2 for v in vec) / len(vec)
return [(v - mean) / math.sqrt(var + eps) for v in vec]
normed = layer_norm(residual)
print("после LayerNorm: ", [round(v, 2) for v in normed])
print()
print("Среднее после нормализации ~", round(sum(normed) / len(normed), 5))
print("Residual бережёт сигнал сквозь десятки слоёв, LayerNorm держит числа в узде.")
Вывод:
вход x: [1.0, 2.0, 3.0] выход подслоя: [0.1, -0.2, 0.05] после residual x+f: [1.1, 1.8, 3.05] после LayerNorm: [-1.1, -0.23, 1.32] Среднее после нормализации ~ -0.0 Residual бережёт сигнал сквозь десятки слоёв, LayerNorm держит числа в узде.
Видно: residual лишь чуть скорректировал вход (а не стёр его), а LayerNorm привёл числа к среднему около нуля и аккуратному масштабу. Без этих двух приёмов обучать сети из десятков слоёв было бы почти невозможно.
Как это собрано в блоке
Концептуально один блок выглядит так (детали порядка нормализации у разных моделей отличаются):
x = x + attention(layernorm(x)) # подслой внимания + residual
x = x + feed_forward(layernorm(x)) # подслой FFN + residual
Каждая строка — это «подслой + нормализация + остаточная связь». Два таких подслоя и образуют один блок трансформера.
Итог
- Блок = multi-head attention + feed-forward сеть, оба обёрнуты residual и LayerNorm.
- Внимание обменивает информацию между токенами, FFN обрабатывает её внутри каждого токена.
- Residual (x + f(x)) сохраняет сигнал и градиенты сквозь глубокую стопку слоёв.
- LayerNorm держит активации в стабильном масштабе, делая обучение возможным.