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 не требует дополнительных параметров, сочетает преимущества относительного кодирования и эффективность реализации.

Positional encoding в Python - comments

En
Python position encoding (python)