Эта статья является второй в серии «Реализованный трансформер». Он вводит позиционное кодирование с нуля. Затем объясняется, как PyTorch реализует позиционное кодирование. Затем следует реализация трансформаторов.

Фон

Позиционное кодирование используется для обеспечения относительного положения каждого токена или слова в последовательности. При чтении предложения каждое слово зависит от окружающих его слов. Например, некоторые слова имеют разные значения в разных контекстах, поэтому модель должна иметь возможность понимать эти варианты и слова, на которые каждое из них опирается в контексте. Примером может служить слово «багажник». В одном случае его можно использовать для обозначения слона, использующего хобот для питья воды. В другом случае это может относиться к стволу дерева, пораженному молнией.

Поскольку модель использует векторы встраивания длиной d_model для представления каждого слова, любое позиционное кодирование должно быть совместимым. Может показаться естественным использовать целые числа, когда первая лексема получает 0, вторая лексема получает 1 и так далее. Однако это число быстро растет и не может быть легко добавлено в матрицу вложения. Вместо этого для каждой позиции создается вектор позиционного кодирования, что означает, что может быть создана матрица позиционного кодирования для представления всех возможных позиций, которые может занимать слово.

Чтобы обеспечить уникальное представление каждой позиции, авторы Внимание — это все, что вам нужно использовали функции синуса и косинуса для создания уникального вектора для каждой позиции в последовательности. Хотя это может показаться странным, есть несколько причин, почему это полезно. Во-первых, вывод синуса и косинуса находится в формате [-1, 1], который нормализован. Он не вырастет до неуправляемого размера, как целые числа. Во-вторых, не нужно проводить дополнительное обучение, поскольку для каждой позиции генерируются уникальные представления.

Уравнения, используемые для создания уникальных кодировок, кажутся пугающими, но это всего лишь модифицированные версии синуса и косинуса. Максимальное количество позиций или векторов во встраивании будет представлено L:

По сути, это означает, что для каждого вектора позиционного кодирования для каждых двух элементов установите четный элемент равным PE(k,2i), а нечетный элемент установите равным PE(k, 2i+1). Затем повторяйте до тех пор, пока в векторе не появятся элементы d_model.

Каждый вектор кодирования имеет ту же размерность, d_model, что и вектор внедрения. Это позволяет их суммировать. kпредставляет позицию, начиная с 0 и заканчивая L-1. Наибольшее число, которое может быть задано для i, равно d_modelделенному на 2, поскольку уравнения чередуются для каждого элемента в вложение. Для nможно задать любое значение, но в исходной статье рекомендуется 10 000. На изображении ниже следующие параметры используются для расчета векторов позиционного кодирования для последовательности из 6 маркеров:

  • n = 10,000
  • L = 6
  • d_model = 4

На этом изображении показано, как максимальное значение i равно 1, которое используется как для синуса, так и для косинуса. k изменяется с каждой строкой в ​​матрице внедрения, начиная с 0 и заканчивая 5, что соответствует максимальной длине 6. Каждый вектор имеетd_model = 4 элемента.

Базовая реализация

Чтобы увидеть, как эти уникальные векторы позиционного кодирования работают с векторами встраивания, лучше всего использовать примеры из статьи «Слой встраивания».

Эта реализация будет построена непосредственно на той, что была в предыдущем посте. Выходные данные ниже включают три последовательности с d_model, равным 4.

# set the output to 2 decimal places without scientific notation
torch.set_printoptions(precision=2, sci_mode=False)

# tokenize the sequences
tokenized_sequences = [tokenize(seq) for seq in sequences]

# index the sequences 
indexed_sequences = [[stoi[word] for word in seq] for seq in tokenized_sequences]

# convert the sequences to a tensor
tensor_sequences = torch.tensor(indexed_sequences).long()

# vocab size
vocab_size = len(stoi)

# embedding dimensions
d_model = 4

# create the embeddings
lut = nn.Embedding(vocab_size, d_model) # look-up table (lut)

# embed the sequence
embeddings = lut(tensor_sequences)

embeddings
tensor([[[-0.27, -0.82,  0.33,  1.39],
         [ 1.72, -0.63, -1.13,  0.10],
         [-0.23, -0.07, -0.28,  1.17],
         [ 0.61,  1.46,  1.21,  0.84],
         [-2.05,  1.77,  1.51, -0.21],
         [ 0.86, -1.81,  0.55,  0.98]],

        [[ 0.06, -0.34,  2.08, -1.24],
         [ 1.44, -0.64,  0.78, -1.10],
         [ 1.78,  1.22,  1.12, -2.35],
         [-0.48, -0.40,  1.73,  0.54],
         [ 1.28, -0.18,  0.52,  2.10],
         [ 0.34,  0.62, -0.45, -0.64]],

        [[-0.22, -0.66, -1.00, -0.04],
         [-0.23, -0.07, -0.28,  1.17],
         [ 1.44, -0.64,  0.78, -1.10],
         [ 1.78,  1.22,  1.12, -2.35],
         [-0.48, -0.40,  1.73,  0.54],
         [ 0.70, -1.35,  0.15, -1.44]]], grad_fn=<EmbeddingBackward0>)

Следующим шагом является кодирование положения каждого слова в каждой последовательности с помощью позиционного кодирования. Приведенная ниже функция соответствует приведенному выше определению. Единственное изменение, о котором стоит упомянуть, заключается в том, что L обозначается как max_length. Часто устанавливается чрезвычайно большое значение в несколько тысяч, чтобы гарантировать, что почти каждая последовательность может быть закодирована надлежащим образом. Это гарантирует, что одна и та же матрица позиционного кодирования может использоваться для последовательностей различной длины. Перед добавлением его можно нарезать до нужной длины.

def gen_pe(max_length, d_model, n):

  # generate an empty matrix for the positional encodings (pe)
  pe = np.zeros(max_length*d_model).reshape(max_length, d_model) 

  # for each position
  for k in np.arange(max_length):

    # for each dimension
    for i in np.arange(d_model//2):

      # calculate the internal value for sin and cos
      theta = k / (n ** ((2*i)/d_model))       

      # even dims: sin   
      pe[k, 2*i] = math.sin(theta) 

      # odd dims: cos               
      pe[k, 2*i+1] = math.cos(theta)

  return pe

# maximum sequence length
max_length = 10
n = 100
encodings = gen_pe(max_length, d_model, n)

Выходные данные кодировок содержат 10 векторов кодирования позиций.

array([[ 0.    ,  1.    ,  0.    ,  1.    ],
       [ 0.8415,  0.5403,  0.0998,  0.995 ],
       [ 0.9093, -0.4161,  0.1987,  0.9801],
       [ 0.1411, -0.99  ,  0.2955,  0.9553],
       [-0.7568, -0.6536,  0.3894,  0.9211],
       [-0.9589,  0.2837,  0.4794,  0.8776],
       [-0.2794,  0.9602,  0.5646,  0.8253],
       [ 0.657 ,  0.7539,  0.6442,  0.7648],
       [ 0.9894, -0.1455,  0.7174,  0.6967],
       [ 0.4121, -0.9111,  0.7833,  0.6216]])

Как уже упоминалось, для max_length установлено значение 10. Хотя это больше, чем требуется, оно гарантирует, что если другая последовательность будет иметь длину 7, 8, 9 или 10, можно использовать ту же матрицу позиционного кодирования. Осталось только нарезать до нужной длины. Ниже вложения имеют seq_length, равное шести, поэтому кодировки могут быть разделены соответствующим образом.

# select the first six tokens
seq_length = embeddings.shape[1]
encodings[:seq_length]
tensor([[ 0.00,  1.00,  0.00,  1.00],
        [ 0.84,  0.54,  0.10,  1.00],
        [ 0.91, -0.42,  0.20,  0.98],
        [ 0.14, -0.99,  0.30,  0.96],
        [-0.76, -0.65,  0.39,  0.92],
        [-0.96,  0.28,  0.48,  0.88]])

Поскольку длина последовательности одинакова для всех трех последовательностей, требуется только одна матрица позиционного кодирования, и ее можно транслировать по всем трем с помощью PyTorch. Встроенная партия в этом примере имеет форму (3, 6, 4), а позиционное кодирование имеет форму (10, 4 )прежде чем он будет разделен на (6, 4). Эта матрица затем транслируется для создания матрицы кодирования (3, 6, 4), видимой на изображении. Для получения дополнительной информации о вещании прочитайте Простое введение в вещание.

Это позволяет без проблем добавлять две матрицы.

embedded_sequence + encodings[:seq_length] # encodings[:6]

Когда позиционные кодировки добавляются к встраиваниям, вывод будет таким же, как изображение в начале раздела.

tensor([[[-0.27,  0.18,  0.33,  2.39],
         [ 2.57, -0.09, -1.03,  1.09],
         [ 0.68, -0.49, -0.08,  2.15],
         [ 0.75,  0.47,  1.50,  1.80],
         [-2.80,  1.12,  1.90,  0.71],
         [-0.10, -1.53,  1.03,  1.86]],

        [[ 0.06,  0.66,  2.08, -0.24],
         [ 2.28, -0.10,  0.88, -0.10],
         [ 2.69,  0.80,  1.32, -1.37],
         [-0.34, -1.39,  2.03,  1.50],
         [ 0.52, -0.83,  0.91,  3.02],
         [-0.62,  0.90,  0.03,  0.23]],

        [[-0.22,  0.34, -1.00,  0.96],
         [ 0.61,  0.47, -0.18,  2.16],
         [ 2.35, -1.06,  0.98, -0.12],
         [ 1.92,  0.23,  1.41, -1.40],
         [-1.24, -1.06,  2.12,  1.46],
         [-0.26, -1.06,  0.63, -0.56]]], grad_fn=<AddBackward0>)

Эти выходные данные будут переданы на следующий уровень модели, то есть внимание с несколькими головками, которое будет рассмотрено в следующей статье.

Однако эта базовая реализация неэффективна из-за использования вложенных циклов, особенно при больших значениях d_model и max_lengthиспользуются. Вместо этого можно использовать более ориентированный на PyTorch подход.

Изменение формулы позиционного кодирования для PyTorch

Чтобы использовать возможности PyTorch, исходные уравнения, в частности делитель, необходимо изменить с использованием логарифмических правил.

Делитель это:

Чтобы изменить делитель, nвводится в числитель путем отрицания его показателя степени. Затем правило 7 используется для возведения всего уравнения в показатель степени e. Затем правило 3 используется для извлечения экспоненты за пределы журнала. Затем это упрощается, чтобы получить результат.

Это важно, потому что его можно использовать для одновременной генерации всех делителей для позиционных кодировок. Ниже видно, что для четырехмерного вложения необходимы только два делителя, поскольку делитель меняется только через каждые 2i, где i это измерение. Это повторяется для каждой позиции:

Поскольку только максимальное число, которое может быть задано для i, равно d_modelделенному на 2, условия могут быть рассчитаны один раз:

d_model = 4
n = 100

div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(n) / d_model))

Этот короткий фрагмент кода можно использовать для генерации всех необходимых делителей. В этом примере для d_model задано значение 4, а для nустановлено значение 100. В результате получается два делителя. :

tensor([1.0000, 0.1000])

Отсюда можно воспользоваться возможностями индексации PyTorch для создания всей матрицы позиционного кодирования с помощью нескольких строк кода. Следующим шагом является создание каждой позиции от kдо L-1.

max_length = 10

# generate the positions into a column matrix
k = torch.arange(0, max_length).unsqueeze(1)        
tensor([[0],
        [1],
        [2],
        [3],
        [4],
        [5],
        [6],
        [7],
        [8],
        [9]])

С позициями и делителями можно легко вычислить внутреннюю часть функций синуса и косинуса.

Умножая kи div_term, входные данные можно рассчитать для каждой позиции. PyTorch будет автоматически транслировать матрицы, чтобы их можно было умножить. Обратите внимание, что это произведение Адамара, а не умножение матриц, потому что соответствующие элементы будут умножены друг на друга:

k*div_term

Результат этого расчета можно увидеть на изображении выше. Все, что осталось сделать, это подключить входные данные к функциям cos и sin и соответствующим образом сохранить их в матрице.

Это можно начать с создания пустой матрицы соответствующего размера:

# generate an empty tensor
pe = torch.zeros(max_length, d_model)
tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]])

Теперь четные столбцы, которые являются sin, можно выбрать с помощью pe[:, 0::2]. Это говорит PyTorch выбирать каждую строку и каждый четный столбец. То же самое можно сделать для нечетных столбцов, таких как cos: pe[:, 1::2]. Еще раз, это говорит PyTorch выбрать каждую строку и каждый нечетный столбец. Поскольку результат k*div_term содержит все необходимые входные данные, его можно использовать для вычисления каждого нечетного и четного столбца.

# set the odd values (columns 1 and 3)
pe[:, 0::2] = torch.sin(k * div_term)

# set the even values (columns 2 and 4)
pe[:, 1::2] = torch.cos(k * div_term)
     
# add a dimension for broadcasting across sequences: optional       
pe = pe.unsqueeze(0)  
tensor([[[ 0.00,  1.00,  0.00,  1.00],
         [ 0.84,  0.54,  0.10,  1.00],
         [ 0.91, -0.42,  0.20,  0.98],
         [ 0.14, -0.99,  0.30,  0.96],
         [-0.76, -0.65,  0.39,  0.92],
         [-0.96,  0.28,  0.48,  0.88],
         [-0.28,  0.96,  0.56,  0.83],
         [ 0.66,  0.75,  0.64,  0.76],
         [ 0.99, -0.15,  0.72,  0.70],
         [ 0.41, -0.91,  0.78,  0.62]]])

Они идентичны значениям, полученным с помощью вложенного цикла for. Напомним, вот весь код вместе:

max_length = 10
d_model = 4
n = 100

def gen_pe(max_length, d_model, n):
  # calculate the div_term
  div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(n) / d_model))

  # generate the positions into a column matrix
  k = torch.arange(0, max_length).unsqueeze(1)

  # generate an empty tensor
  pe = torch.zeros(max_length, d_model)

  # set the even values
  pe[:, 0::2] = torch.sin(k * div_term)

  # set the odd values
  pe[:, 1::2] = torch.cos(k * div_term)

  # add a dimension       
  pe = pe.unsqueeze(0)        

  # the output has a shape of (1, max_length, d_model)
  return pe                           

gen_pe(max_length, d_model, n)  

Несмотря на более сложную реализацию, PyTorch использует эту реализацию из-за ее повышенной производительности для машинного обучения.

Позиционное кодирование в трансформаторах

Теперь, когда вся тяжелая работа убрана, реализация проста. Он является производным от Аннотированный трансформатор и PyTorch. Обратите внимание, что значение по умолчанию для n равно 10 000, а значение по умолчанию max_length равно 5 000.

Эта реализация также включает отсев, который случайным образом обнуляет некоторые элементы своего ввода с заданной вероятностью, p. Это помогает с регуляризацией и предотвращает коадаптацию нейронов (чрезмерную зависимость друг от друга). Выходные данные также масштабируются с коэффициентом ¹⁄₍₁_ₚ₎. Вместо того, чтобы углубляться в это в этой статье. Дополнительную информацию см. в статье Слой отсева. Было бы лучше освоиться с ним сейчас, прежде чем переходить к остальной части модели трансформера, потому что он есть почти в каждом втором слое.

class PositionalEncoding(nn.Module):
  def __init__(self, d_model: int, dropout: float = 0.1, max_length: int = 5000):
    """
    Args:
      d_model:      dimension of embeddings
      dropout:      randomly zeroes-out some of the input
      max_length:   max sequence length
    """
    # inherit from Module
    super().__init__()     

    # initialize dropout                  
    self.dropout = nn.Dropout(p=dropout)      

    # create tensor of 0s
    pe = torch.zeros(max_length, d_model)    

    # create position column   
    k = torch.arange(0, max_length).unsqueeze(1)  

    # calc divisor for positional encoding 
    div_term = torch.exp(                                 
            torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
    )

    # calc sine on even indices
    pe[:, 0::2] = torch.sin(k * div_term)    

    # calc cosine on odd indices   
    pe[:, 1::2] = torch.cos(k * div_term)  

    # add dimension     
    pe = pe.unsqueeze(0)          

    # buffers are saved in state_dict but not trained by the optimizer                        
    self.register_buffer("pe", pe)                        

  def forward(self, x: Tensor):
    """
    Args:
      x:        embeddings (batch_size, seq_length, d_model)
    
    Returns:
                embeddings + positional encodings (batch_size, seq_length, d_model)
    """
    # add positional encoding to the embeddings
    x = x + self.pe[:, : x.size(1)].requires_grad_(False) 

    # perform dropout
    return self.dropout(x)

Переход вперед

Для выполнения прямого прохода могут использоваться те же встроенные последовательности, что и ранее.

embeddings
tensor([[[-0.27, -0.82,  0.33,  1.39],
         [ 1.72, -0.63, -1.13,  0.10],
         [-0.23, -0.07, -0.28,  1.17],
         [ 0.61,  1.46,  1.21,  0.84],
         [-2.05,  1.77,  1.51, -0.21],
         [ 0.86, -1.81,  0.55,  0.98]],

        [[ 0.06, -0.34,  2.08, -1.24],
         [ 1.44, -0.64,  0.78, -1.10],
         [ 1.78,  1.22,  1.12, -2.35],
         [-0.48, -0.40,  1.73,  0.54],
         [ 1.28, -0.18,  0.52,  2.10],
         [ 0.34,  0.62, -0.45, -0.64]],

        [[-0.22, -0.66, -1.00, -0.04],
         [-0.23, -0.07, -0.28,  1.17],
         [ 1.44, -0.64,  0.78, -1.10],
         [ 1.78,  1.22,  1.12, -2.35],
         [-0.48, -0.40,  1.73,  0.54],
         [ 0.70, -1.35,  0.15, -1.44]]], grad_fn=<EmbeddingBackward0>)

Со встроенными последовательностями можно создать матрицу позиционного кодирования. Для отсева установлено значение 0,0, чтобы легко увидеть дополнение между вложениями и позиционными кодировками. Значения отличаются от реализованных с нуля, поскольку n имеет значение по умолчанию 10 000 вместо 100.

d_model = 4
max_length = 10
dropout = 0.0

# create the positional encoding matrix
pe = PositionalEncoding(d_model, dropout, max_length)

# preview the values
pe.state_dict()
OrderedDict([('pe',
              tensor([[[ 0.00,  1.00,  0.00,  1.00],
                       [ 0.84,  0.54,  0.01,  1.00],
                       [ 0.91, -0.42,  0.02,  1.00],
                       [ 0.14, -0.99,  0.03,  1.00],
                       [-0.76, -0.65,  0.04,  1.00],
                       [-0.96,  0.28,  0.05,  1.00],
                       [-0.28,  0.96,  0.06,  1.00],
                       [ 0.66,  0.75,  0.07,  1.00],
                       [ 0.99, -0.15,  0.08,  1.00],
                       [ 0.41, -0.91,  0.09,  1.00]]]))])

Перед их добавлением последовательности имеют форму (batch_size, seq_length, d_model), т.е. (3, 6, 4). Позиционные кодировки имеют одинаковый размер после того, как они нарезаны и транслированы, поэтому выходные данные прямого прохода имеют размер (batch_size, seq_length, d_model), т. е. еще (3, 6, 4). Это представляет собой 3 последовательности из 6 токенов, встроенных в 4-мерное пространство с позиционными кодами, указывающими их местоположение в последовательности.

pe(embeddings)
 tensor([[[-0.27,  0.18,  0.33,  2.39],
         [ 2.57, -0.09, -1.12,  1.10],
         [ 0.68, -0.49, -0.26,  2.17],
         [ 0.75,  0.47,  1.24,  1.84],
         [-2.80,  1.12,  1.55,  0.79],
         [-0.10, -1.53,  0.60,  1.98]],

        [[ 0.06,  0.66,  2.08, -0.24],
         [ 2.28, -0.10,  0.79, -0.10],
         [ 2.69,  0.80,  1.14, -1.35],
         [-0.34, -1.39,  1.76,  1.54],
         [ 0.52, -0.83,  0.56,  3.10],
         [-0.62,  0.90, -0.40,  0.35]],

        [[-0.22,  0.34, -1.00,  0.96],
         [ 0.61,  0.47, -0.27,  2.17],
         [ 2.35, -1.06,  0.80, -0.10],
         [ 1.92,  0.23,  1.15, -1.35],
         [-1.24, -1.06,  1.77,  1.54],
         [-0.26, -1.06,  0.20, -0.44]]], grad_fn=<AddBackward0>)

Следующая статья в серии — Уровень многоголового внимания.

Рекомендации

  1. Выпадающий слой
  2. Обзор позиционного кодирования
  3. Реализация позиционного кодирования PyTorch
  4. Аннотированный трансформер
  5. Трансформаторное позиционное кодирование

Приложение

Визуализация уникальности позиционных кодировок

Прежде чем сделать вывод, было бы полезно проверить уникальность позиционных кодировок и посмотреть, как они работают с большими последовательностями.

Используя matplotlib, векторы можно легко сравнивать друг с другом.

def visualize_pe(max_length, d_model, n):
  plt.imshow(gen_pe(max_length, d_model, n), aspect="auto")
  plt.title("Positional Encoding")
  plt.xlabel("Encoding Dimension")
  plt.ylabel("Position Index")

  # set the tick marks for the axes
  if d_model < 10:
    plt.xticks(torch.arange(0, d_model))
  if max_length < 20:
    plt.yticks(torch.arange(max_length-1, -1, -1))
    
  plt.colorbar()
  plt.show()

# plot the encodings
max_length = 10
d_model = 4
n = 100

visualize_pe(max_length, d_model, n)

Каждая строка представляет вектор позиционного кодирования, а каждый столбец представляет соответствующее измерение, к которому он будет добавлен при объединении с вложениями.

Это также можно увидеть при больших значениях n, d_model и max_length<. /em>:

# plot the encodings
max_length = 1000
d_model = 512
n = 10000

visualize_pe(max_length, d_model, n)