Каким образом в архитектуре Transformer учитывается позиция слов (токенов) в последовательности?

«Каким образом в архитектуре Transformer учитывается позиция слов (токенов) в последовательности?» — вопрос из категории NLP и трансформеры, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Поскольку механизм самовнимания в 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)

Почему это работает? Модель учится не только значению слов, но и их типичному положению относительно друг друга (например, что подлежащее часто стоит перед сказуемым). Без этой информации фраза "собака укусила человека" и "человек укусил собаку" имела бы одинаковое представление.