Positional Encoding в Python: от классики до современных подходов
Основное решение: синусоидальное позиционное кодирование
В архитектуре Transformer, предложенной в статье "Attention is All You Need", позиционная информация добавляется к входным эмбеддингам с помощью фиксированных синусоидальных функций. Этот метод не требует обучения, масштабируется на произвольную длину последовательности и позволяет модели различать порядок элементов.
Реализация на PyTorch
import torch
import math
def sinusoidal_positional_encoding(max_len: int, d_model: int) -> torch.Tensor:
"""
Возвращает тензор позиционных кодировок размером (max_len, d_model).
"""
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
return pe
# Пример для последовательности из 10 токенов и размерности 8
pe = sinusoidal_positional_encoding(10, 8)
print(pe.shape) # torch.Size([10, 8])
print(pe[:3, :4])Python position encoding (positional encoding в python)
torch.Size([10, 8])
tensor([[ 0.0000, 1.0000, 0.0000, 1.0000],
[ 0.8415, 0.5403, 0.8219, 0.5697],
[ 0.9093, -0.4161, 0.9360, 0.3520]])
Каждая строка представляет собой вектор позиции, который линейно комбинируется с эмбеддингом токена. Частоты синусов и косинусов уменьшаются вдоль оси размерности, что позволяет сети легко улавливать как короткие, так и длинные зависимости.
Типичные проблемы и их решения
- Проблема: Кодировка фиксирована и не адаптируется к данным.
Решение: Использовать обучаемые позиционные эмбеддинги, если требуется гибкость. - Проблема: При очень большой длине последовательности (например, > 10 000) значения sin/cos становятся слишком похожими для соседних позиций.
Решение: Увеличить базу (max_len) или использовать относительное позиционное кодирование. - Проблема: Размерность d_model должна быть чётной, иначе последний столбец будет не определён.
Решение: Проверять чётность d_model перед расчётами.
Как реализовать позиционное кодирование, которое будет обучаться совместно с моделью?
Обучаемые позиционные эмбеддинги хранят матрицу весов размером (max_len, d_model), которая обновляется в процессе обучения. Такой подход используется в BERT, GPT и многих современных моделях.
import torch.nn as nn
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_len: int, d_model: int, dropout: float = 0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
self.pe = nn.Embedding(max_len, d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: (batch, seq_len, d_model)
"""
positions = torch.arange(0, x.size(1), device=x.device).unsqueeze(0)
return self.dropout(x + self.pe(positions))
# Пример использования
model = LearnedPositionalEncoding(max_len=128, d_model=512)
x = torch.randn(2, 10, 512) # batch=2, seq_len=10
out = model(x)
print(out.shape) # torch.Size([2, 10, 512])
Проблемы и ограничения
- Ограничение: max_len фиксирован на этапе создания модели. Если на вход поступит более длинная последовательность, произойдет ошибка индексации.
Решение: Заранее определить достаточный max_len или интерполировать веса при большей длине. - Проблема: При малом размере датасета модель может выучить шумовые паттерны, не связанные с позицией.
Решение: Использовать предобученные позиционные эмбеддинги или комбинировать с синусоидальным кодированием.
Цель:
подстроить кодировку под специфику данных, особенно когда порядок токенов имеет нетривиальное влияние (например, в задачах генерации текста).Как обеспечить учёт относительных расстояний между токенами в Transformer?
Относительное позиционное кодирование (Relative Positional Encoding, RPE) модифицирует механизм внимания, добавляя смещение, зависящее от разности позиций ключа и запроса. Классическая реализация предложена в Transformer-XL.
import torch
import torch.nn.functional as F
def relative_attention_scores(query: torch.Tensor, key: torch.Tensor,
pos_embed: torch.Tensor) -> torch.Tensor:
"""
query, key: (batch, heads, seq_len, d_k)
pos_embed: (seq_len, d_k) - позиционные кодировки ключей
"""
# (batch, heads, seq_len, seq_len) - стандартные скоры
scores = torch.matmul(query, key.transpose(-2, -1))
# Добавляем вклад от относительных позиций
# pos_embed должен содержать кодировки для каждой позиции,
# здесь упрощённо: используем синусоидальные и вычитаем.
rel_scores = torch.matmul(query, pos_embed.T)
return scores + rel_scores
# Пример (упрощённый, не полная реализация Transformer-XL)
q = torch.randn(1, 2, 4, 8) # batch=1, heads=2, seq=4, d_k=8
k = torch.randn(1, 2, 4, 8)
pos = sinusoidal_positional_encoding(4, 8) # синусоидальные кодировки для ключей
scores = relative_attention_scores(q, k, pos)
print(scores.shape) # torch.Size([1, 2, 4, 4])
Основные сложности
- Проблема: Вычисление векторов для каждой пары позиций приводит к квадратичной сложности по длине последовательности.
Решение: Использовать предвычисленные матрицы относительных смещений (см. оригинальную работу Transformer-XL). - Проблема: Реализация RPE требует изменения архитектуры многоголового внимания, что не всегда удобно при использовании стандартных библиотек.
Решение: Использовать готовые реализации (например, в библиотеке x-transformers).
Цель:
улучшить обобщение на последовательности разной длины, особенно в задачах с длинным контекстом (языковое моделирование, генерация музыки).Как объединить синусоидальное кодирование с обучаемыми параметрами для повышения гибкости?
Можно добавить линейную проекцию к синусоидальным кодировкам или использовать синусоидальную инициализацию для обучаемых эмбеддингов. Это даёт начальное смещение, которое затем дообучается.
class HybridPositionalEncoding(nn.Module):
def __init__(self, max_len: int, d_model: int, trainable: bool = True):
super().__init__()
self.d_model = d_model
# создаём фиксированную часть
pe_fixed = sinusoidal_positional_encoding(max_len, d_model)
# обучаемая часть
if trainable:
self.pe_train = nn.Parameter(torch.randn(max_len, d_model) * 0.1)
else:
self.register_buffer('pe_train', torch.zeros(max_len, d_model))
self.register_buffer('pe_fixed', pe_fixed)
def forward(self, x: torch.Tensor) -> torch.Tensor:
seq_len = x.size(1)
pe = self.pe_fixed[:seq_len] + self.pe_train[:seq_len]
return x + pe.unsqueeze(0)
model = HybridPositionalEncoding(max_len=512, d_model=256, trainable=True)
x = torch.randn(3, 20, 256)
out = model(x)
print(out.shape)
Риски и замечания
- Проблема: Сумма двух кодировок может усилить шум, если обучаемая часть становится большой.
Решение: Нормировать итоговый вектор перед сложением или добавить регуляризацию. - Проблема: Необходимость хранить две матрицы, что увеличивает число параметров.
Решение: Использовать только обучаемую часть с синусоидальной инициализацией.
Цель:
получить начальное разумное поведение (синусоидальные волны) и возможность адаптироваться к доменным особенностям.Расширенные примеры использования позиционного кодирования
Визуализация синусоидальных кодировок
Следующий код строит тепловую карту позиционных кодировок для последовательности длиной 32 и размерности 16. Визуализация помогает увидеть, как меняются паттерны вдоль позиций и каналов.
import matplotlib.pyplot as plt
import seaborn as sns
pe = sinusoidal_positional_encoding(32, 16)
plt.figure(figsize=(10, 6))
sns.heatmap(pe.numpy(), cmap='coolwarm', cbar=True)
plt.title('Синусоидальное позиционное кодирование (32x16)')
plt.xlabel('Размерность')
plt.ylabel('Позиция')
plt.show()
[График: тепловая карта с полосами, показывающими чередование синусов и косинусов по столбцам]
Сравнение синусоидального и обучаемого кодирования на задаче классификации позиций
Создаётся синтетический набор данных, где метка класса зависит от позиции одного токена в последовательности. Сравниваются модели с разными типами позиционного кодирования.
import torch.optim as optim
from sklearn.datasets import make_classification
# Синтетические данные: 1000 последовательностей длины 10, 3 класса
X = torch.randn(1000, 10, 8)
y = (X[:, 0, 0] > 0).long() # класс зависит от первого токена
# Модель: простой Transformer без маски
class SimpleTransformer(nn.Module):
def __init__(self, pe_layer):
super().__init__()
self.pe = pe_layer
self.attention = nn.MultiheadAttention(embed_dim=8, num_heads=2, batch_first=True)
self.classifier = nn.Linear(8, 2)
def forward(self, x):
x = self.pe(x)
attn_out, _ = self.attention(x, x, x)
pooled = attn_out.mean(dim=1)
return self.classifier(pooled)
# Обучаем с синусоидальным PE
pe_sin = sinusoidal_positional_encoding(10, 8)
pe_sin_layer = lambda x: x + pe_sin[:x.size(1)].unsqueeze(0)
model_sin = SimpleTransformer(pe_sin_layer)
optimizer = optim.Adam(model_sin.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
for epoch in range(20):
optimizer.zero_grad()
logits = model_sin(X)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
print(f'Epoch {epoch+1}, Loss: {loss.item():.4f}')
# Аналогично для обучаемого PE
pe_learned = LearnedPositionalEncoding(max_len=10, d_model=8, dropout=0.0)
model_learned = SimpleTransformer(pe_learned)
# ... (код обучения аналогичен)
print('Обучение завершено')
Epoch 1, Loss: 0.6931 ... Epoch 20, Loss: 0.3204 Обучение завершено
Использование Rotary Position Encoding (RoPE) в Python
RoPE - современный метод, применяемый в LLaMA и других моделях. Он поворачивает пары элементов эмбеддинга в зависимости от позиции. Реализация на PyTorch требует аккуратной работы с комплексными числами.
import torch
import math
def apply_rotary_emb(x: torch.Tensor, freqs: torch.Tensor):
# x: (batch, seq, heads, d_k), freqs: (seq, d_k//2)
seq_len, _ = freqs.shape
x_rot = x.float().reshape(*x.shape[:-1], -1, 2) # (batch, seq, heads, d_k//2, 2)
x_complex = torch.view_as_complex(x_rot)
freqs_complex = torch.view_as_complex(torch.stack([freqs.cos(), freqs.sin()], dim=-1).unsqueeze(1)) # (seq, 1, d//2)
x_rotated = x_complex * freqs_complex
x_out = torch.view_as_real(x_rotated).flatten(-2)
return x_out.type_as(x)
# Пример
x = torch.randn(2, 5, 4, 8) # batch, seq, heads, d_k
freqs = torch.linspace(0, math.pi, 4) # 4 = d_k//2
x_rot = apply_rotary_emb(x, freqs)
print(x_rot.shape) # torch.Size([2, 5, 4, 8])
torch.Size([2, 5, 4, 8])
RoPE не требует дополнительных параметров, сочетает преимущества относительного кодирования и эффективность реализации.