Трансформеры находятся на подъеме как в области изображений, так и в области НЛП. В этом блоге мы разберемся, откуда взялась идея внимания, как она работает и, наконец, увидим реализации.

Это часть серии блогов, которые я пишу, чтобы понять, что внимание — это все, что вам нужно, бумага, почему трансформер работает в разных областях, как обучение меняется с вниманием (архитектура трансформера) от CNN. Я обновлю этот раздел ссылками на другие блоги:

  1. Демистификация логики внимания трансформеров (этот блог)
  2. Трансформаторные позиционные заложения

Внимание Пожалуйста! 🎤

В течение всего процесса внимания мы изучаем три показателя: запрос, ключ и значение. Идея запроса, ключа и значения пришла из информационно-поисковой системы. Итак, давайте разберемся с идеей запроса к базе данных.

Допустим, у меня есть база данных со всеми бенгальскими авторами и информацией об их книгах. Теперь я хочу прочитать какую-нибудь книгу, написанную Рабиндранатом. Я сделаю запрос к базе данных следующим образом: select books written by Rabindranath. Предположим, что наша база данных выглядит так, как показано ниже.

В базе данных авторы аналогичны ключам, а книги аналогичны ценностям. Rabindranath — это ключ из запроса. Итак, нам сначала нужно вычислить сходство между запросом и ключами (всеми авторами в базе данных) базы данных. Затем возвращаем значения (книги) наиболее похожего автора (в этом случае мы вернем все книги из Rabindranath Tagore).

Точно так же внимание имеет три матрицы: запрос(Q), ключ(K) и ценность(V). Каждый из них имеет те же размеры, что и входное встраивание. Мы узнаем значения этих метрик во время обучения.

Мы обсудим, как вводимые вложения и вычисляются в отдельном блоге. Но здесь вы можете предположить, что из каждого слова мы создаем вектор, чтобы мы могли обрабатывать информацию. Для каждого слова мы генерируем вектор размером 512.

Итак, в нашем случае все 3 матрицы имеют размер 512x512 (поскольку размерность вложений слов 512). Для каждого вложения токена мы умножаем его на все три матрицы (Q, K, V). Итак, у нас будет 3 промежуточных вектора длины 512 для каждого токена.

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

Затем мы делим скалярное произведение на квадратный корень из размерностей ключевого вектора. Это масштабирование выполняется для предотвращения того, чтобы скалярное произведение становилось слишком большим или слишком маленьким (в зависимости от положительных или отрицательных значений), что может вызвать числовую нестабильность во время обучения. Коэффициент масштабирования выбирается таким образом, чтобы дисперсия скалярного произведения была примерно равна 1.

Затем мы передаем результат через операцию softmax. Это нормализует оценки, поэтому все они положительные и в сумме дают 1. Вывод softmax определяет, сколько информации или характеристик (значений) мы должны взять из разных слов. По сути, мы вычисляем веса.

Здесь важно отметить, зачем нам нужна информация/функции из других слов. В тексте, подобном этому the dog did not attack the old man because it was sleepy. Теперь, если мы просто посмотрим на одно слово it, у модели нет никакой информации о том, что itозначает собака или старик.

Наконец, мы вычисляем произведение softmax и значений и суммируем их вместе.

Матрица здесь 🚙

Вся логика, которой я поделился выше, хороша, но если бы мы реализовали ее таким образом, она не была бы оптимизирована, поэтому давайте посмотрим на векторную реализацию этого.

Ключ запроса и вычисление матрицы можно выполнить, как показано ниже.

Точно так же мы можем вычислить векторы ключа и значения.

Наконец, мы вычисляем баллы и результат внимания.

Давайте код 📝

import torch
import torch.nn as nn
from typing import List

def get_input_embeddings(words: List[str], embeddings_dim: int):
    # we are creating random vector of embeddings_dim size for each words
    # normally we train a tokenizer to get the embeddings.
    # check the blog on tokenizer to learn about this part
    embeddings = [torch.randn(embeddings_dim) for word in words]
    return embeddings


text = "I should sleep now"
words = text.split(" ")
len(words) # 4


embeddings_dim = 512 # 512 dim because the original paper uses it. we can use other dim also
embeddings = get_input_embeddings(words, embeddings_dim=embeddings_dim)
embeddings[0].shape # torch.Size([512])


# initialize the query, key and value metrices 
query_matrix = nn.Linear(embeddings_dim, embeddings_dim)
key_matrix = nn.Linear(embeddings_dim, embeddings_dim)
value_matrix = nn.Linear(embeddings_dim, embeddings_dim)
query_matrix.weight.shape, key_matrix.weight.shape, value_matrix.weight.shape # torch.Size([512, 512]), torch.Size([512, 512]), torch.Size([512, 512])


# query, key and value vectors computation for each words embeddings
query_vectors = torch.stack([query_matrix(embedding) for embedding in embeddings])
key_vectors = torch.stack([key_matrix(embedding) for embedding in embeddings])
value_vectors = torch.stack([value_matrix(embedding) for embedding in embeddings])
query_vectors.shape, key_vectors.shape, value_vectors.shape # torch.Size([4, 512]), torch.Size([4, 512]), torch.Size([4, 512])


# compute the score
scores = torch.matmul(query_vectors, key_vectors.transpose(-2, -1)) / torch.sqrt(torch.tensor(embeddings_dim, dtype=torch.float32))
scores.shape # torch.Size([4, 4])


# compute the attention weights for each of the words with the other words
softmax = nn.Softmax(dim=-1)
attention_weights = softmax(scores)
attention_weights.shape # torch.Size([4, 4])


# attention output
output = torch.matmul(attention_weights, value_vectors)
output.shape # torch.Size([4, 512])

Многоголовое внимание 🙉

Потому что никогда не бывает слишком много внимания. 😛 — я

Внимание, о котором я упоминал выше, — это внимание одной головы. В многоглавом внимании у нас больше одной головы, 8 голов в исходной бумаге.

Вычисление внимания как для нескольких головок, так и для одной головы одинаково до запроса (q0-q3), ключа (k0-k3), значения (v0-v3) промежуточного вектора.

После этого мы делим вектор запроса на равные части по количеству головок, которые у нас есть. На изображении выше у нас есть 8 головок, а векторы запроса, ключа и значения имеют размерность 512. Таким образом, мы создаем 8 векторов размерности 64.

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

После того, как у нас есть мини-запросы, ключи и значения (те, что с 64 размерами) в голове, мы вычисляем оставшуюся логику так же, как и внимание одной головы. Наконец, у нас есть 4 вектора размерности 64 от каждой головы.

Мы объединяем первые 64 выхода каждой головки, чтобы получить окончательный выходной вектор из 512 тусклых изображений. То же самое для оставшихся 3 результатов векторов.

Преобразователи с несколькими головками обладают большей способностью представлять сложные взаимосвязи в данных. Каждая голова способна запоминать разные узоры. Несколько головок также обеспечивают возможность одновременного обслуживания различных подпространств (64 затемненных вектора из 512 исходных векторов) входного представления.

Реализация многоголового внимания

num_heads = 8
# batch dim is 1 since we are processing one text.
batch_size = 1

text = "I should sleep now"
words = text.split(" ")
len(words) # 4


embeddings_dim = 512
embeddings = get_input_embeddings(words, embeddings_dim=embeddings_dim)
embeddings[0].shape # torch.Size([512])


# initialize the query, key and value metrices 
query_matrix = nn.Linear(embeddings_dim, embeddings_dim)
key_matrix = nn.Linear(embeddings_dim, embeddings_dim)
value_matrix = nn.Linear(embeddings_dim, embeddings_dim)
query_matrix.weight.shape, key_matrix.weight.shape, value_matrix.weight.shape # torch.Size([512, 512]), torch.Size([512, 512]), torch.Size([512, 512])


# query, key and value vectors computation for each words embeddings
query_vectors = torch.stack([query_matrix(embedding) for embedding in embeddings])
key_vectors = torch.stack([key_matrix(embedding) for embedding in embeddings])
value_vectors = torch.stack([value_matrix(embedding) for embedding in embeddings])
query_vectors.shape, key_vectors.shape, value_vectors.shape # torch.Size([4, 512]), torch.Size([4, 512]), torch.Size([4, 512])


# (batch_size, num_heads, seq_len, embeddings_dim)
query_vectors_view = query_vectors.view(batch_size, -1, num_heads, embeddings_dim//num_heads).transpose(1, 2) 
key_vectors_view = key_vectors.view(batch_size, -1, num_heads, embeddings_dim//num_heads).transpose(1, 2) 
value_vectors_view = value_vectors.view(batch_size, -1, num_heads, embeddings_dim//num_heads).transpose(1, 2) 
query_vectors_view.shape, key_vectors_view.shape, value_vectors_view.shape
# torch.Size([1, 8, 4, 64]),
#  torch.Size([1, 8, 4, 64]),
#  torch.Size([1, 8, 4, 64])


# We are splitting the each vectors into 8 heads. 
# Assuming we have one text (batch size of 1), So we split 
# the embedding vectors also into 8 parts. Each head will 
# take these parts. If we do this one head at a time.
head1_query_vector = query_vectors_view[0, 0, ...]
head1_key_vector = key_vectors_view[0, 0, ...]
head1_value_vector = value_vectors_view[0, 0, ...]
head1_query_vector.shape, head1_key_vector.shape, head1_value_vector.shape


# The above vectors are of same size as before only the feature dim is changed from 512 to 64
# compute the score
scores_head1 = torch.matmul(head1_query_vector, head1_key_vector.permute(1, 0)) / torch.sqrt(torch.tensor(embeddings_dim//num_heads, dtype=torch.float32))
scores_head1.shape # torch.Size([4, 4])


# compute the attention weights for each of the words with the other words
softmax = nn.Softmax(dim=-1)
attention_weights_head1 = softmax(scores_head1)
attention_weights_head1.shape # torch.Size([4, 4])

output_head1 = torch.matmul(attention_weights_head1, head1_value_vector)
output_head1.shape # torch.Size([4, 512])


# we can compute the output for all the heads
outputs = []
for head_idx in range(num_heads):
    head_idx_query_vector = query_vectors_view[0, head_idx, ...]
    head_idx_key_vector = key_vectors_view[0, head_idx, ...]
    head_idx_value_vector = value_vectors_view[0, head_idx, ...]
    scores_head_idx = torch.matmul(head_idx_query_vector, head_idx_key_vector.permute(1, 0)) / torch.sqrt(torch.tensor(embeddings_dim//num_heads, dtype=torch.float32))

    softmax = nn.Softmax(dim=-1)
    attention_weights_idx = softmax(scores_head_idx)
    output = torch.matmul(attention_weights_idx, head_idx_value_vector)
    outputs.append(output)

[out.shape for out in outputs]
# [torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64]),
#  torch.Size([4, 64])]

# stack the result from each heads for the corresponding words
word0_outputs = torch.cat([out[0] for out in outputs])
word0_outputs.shape

# lets do it for all the words
attn_outputs = []
for i in range(len(words)):
    attn_output = torch.cat([out[i] for out in outputs])
    attn_outputs.append(attn_output)
[attn_output.shape for attn_output in attn_outputs] # [torch.Size([512]), torch.Size([512]), torch.Size([512]), torch.Size([512])]


# Now lets do it in vectorize way. 
# We can not permute the last two dimension of the key vector.
key_vectors_view.permute(0, 1, 3, 2).shape # torch.Size([1, 8, 64, 4])


# Transpose the key vector on the last dim
score = torch.matmul(query_vectors_view, key_vectors_view.permute(0, 1, 3, 2)) # Q*k
score = torch.softmax(score, dim=-1)


# reshape the results 
attention_results = torch.matmul(score, value_vectors_view)
attention_results.shape # [1, 8, 4, 64]

# merge the results
attention_results = attention_results.permute(0, 2, 1, 3).contiguous().view(batch_size, -1, embeddings_dim)
attention_results.shape # torch.Size([1, 4, 512])

Весь код, реализованный в этом блоге, собран в этой записной книжке. Не стесняйтесь редактировать и пробовать вещи.

Надеюсь, вам понравился этот блог. 🤗 Если вам интересно почитать о преобразователе зрения, загляните в этот блог:



Ресурсы