Ответ
Поскольку механизм самовнимания в Transformer по своей природе инвариантен к порядку (permutation-invariant), позиционная информация добавляется явно через позиционные энкодинки (Positional Encodings). Я реализовывал это в PyTorch.
Суть: К вектору эмбеддинга каждого токена (который несет смысловую информацию) прибавляется специальный вектор, кодирующий его позицию в последовательности.
1. Синусоидальные позиционные энкодинки (Sinusoidal Positional Encodings) Используются в оригинальной статье "Attention Is All You Need". Их ключевое преимущество — модель может экстраполировать на последовательности длиннее, чем те, на которых обучалась.
import torch
import math
def sinusoidal_positional_encoding(max_seq_len, d_model):
"""
Генерирует матрицу позиционных энкодировок размером (max_seq_len, d_model).
"""
position = torch.arange(max_seq_len).unsqueeze(1) # (max_seq_len, 1)
div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
pe = torch.zeros(max_seq_len, d_model)
pe[:, 0::2] = torch.sin(position * div_term) # четные индексы
pe[:, 1::2] = torch.cos(position * div_term) # нечетные индексы
return pe # (max_seq_len, d_model)
# Пример использования:
# d_model = 512
# token_embedding = ... # (batch_size, seq_len, d_model)
# pos_encoding = sinusoidal_positional_encoding(seq_len, d_model).to(device)
# embedded = token_embedding + pos_encoding # Позиционная информация добавляется
2. Обучаемые позиционные энкодинки (Learned Positional Embeddings)
Более простой и распространенный на практике подход, который я чаще использую. Позиционные векторы — это просто еще один слой эмбеддинга (nn.Embedding), где индексом является позиция токена.
import torch.nn as nn
class TransformerEmbedding(nn.Module):
def __init__(self, vocab_size, d_model, max_seq_len, dropout=0.1):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, d_model)
self.position_embedding = nn.Embedding(max_seq_len, d_model) # Обучаемый!
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# x: (batch_size, seq_len)
seq_len = x.size(1)
positions = torch.arange(seq_len, device=x.device).unsqueeze(0) # (1, seq_len)
token_emb = self.token_embedding(x) # (batch_size, seq_len, d_model)
pos_emb = self.position_embedding(positions) # (1, seq_len, d_model)
return self.dropout(token_emb + pos_emb)
Почему это работает? Модель учится не только значению слов, но и их типичному положению относительно друг друга (например, что подлежащее часто стоит перед сказуемым). Без этой информации фраза "собака укусила человека" и "человек укусил собаку" имела бы одинаковое представление.