Входы

Первая часть архитектуры называется входами. Какие входы? это зависит от того, что мы пытаемся сделать. В нашем примере мы создаем языковую модель, программное обеспечение, которое умеет генерировать соответствующий текст, но архитектура Transformers также полезна и в других случаях использования. Поскольку генерация текста является нашей конечной целью, нам нужно обучить (научить) модель, как это делать, поэтому входными данными является текст. Первая проблема заключается в том, что компьютер не может обрабатывать текст непосредственно в CPU/GPU (где происходят вычисления компьютера). Поэтому нам нужно представлять слова в виде чисел. Как вы скоро увидите, способ представления этих слов в виде чисел является решающим шагом.

Токенизатор

Токенизация — это процесс разделения вашего полного текста на более мелкие части. Скажем, у нас есть набор данных из 10 000 статей Википедии. Мы берем каждый символ и расшифровываем, как с ним обращаться (токенизировать). Существует множество способов токенизации текста, давайте посмотрим, как это делает токенизатор OpenAi.
Вот текст:
«Многие слова сопоставляются с одним токеном, а некоторые — нет: неделимые.

Символы Юникода, такие как эмодзи, могут быть разделены на множество токенов, содержащих базовые байты: 🤚🏾

Последовательности символов, которые обычно встречаются рядом друг с другом, могут быть сгруппированы вместе: 1234567890

Это токенизация:

Как видите, слов около 40 (в зависимости от того, как считать (знаки препинания). Из этих 40 слов было сгенерировано 64 лексемы. Иногда лексема представляет собой целое слово, например, «Много, слова, карта», а иногда это часть слова, как в случае с Unicode.
Зачем использовать токенизацию? Потому что она помогает модели обучаться. Существует множество различных аспектов причин, по которым токенизация полезна, но в основном, поскольку текст — это наши данные, токены — это функции данных и различные способы разработки этих функций приведут к различиям в производительности.

Теперь, когда у нас есть токены, мы можем создать словарь поиска, который позволит нам избавиться от слов и вместо этого использовать индексы. Например, если весь мой набор данных состоит из этого предложения: Где бог. Я мог бы создать такой словарь, который представляет собой просто пару слов ключ: значение и одно число, представляющее их. Мне не нужно будет каждый раз использовать все слово целиком, я просто использую номер. Например:
{Где: 0, это: 1, бог: 2}. Всякий раз, когда я встречаю слово является, я заменяю его на 1. Чтобы увидеть больше примеров токенизаторов, вы можете проверить один, разработанный Google, или TikToken OpenAI.

Слово в вектор

Мы делаем большие успехи в нашем путешествии по представлению слов в виде чисел. Следующим шагом будет создание числовых, семантических представлений из этих токенов. Для этого мы можем использовать алгоритм под названием Word2Vec. Детали в данный момент не очень важны, но основная идея заключается в том, что вы берете вектор (пока упростим, представьте себе обычный список) чисел любого размера (авторы статьи использовали 512 ) и этот список чисел должен представлять семантическое значение слова. Представьте себе список чисел вроде [-2, 4, -3,7, 41…-0,98], который на самом деле содержит семантическое представление слова. Это должно быть сделано так, что если я положу эти векторы на двумерный график, похожие термины будут ближе, чем разные термины.

Как Вы можете видеть на картинке (взято отсюда), Бэби находится рядом с Авв и Спит, тогда как Гражданин/Штат/Америка также несколько сгруппированы вместе.
*2D-векторы слов (также известные как список с двумя числами) не смогут содержать никакого точного значения даже для одного слова, как уже упоминалось, авторы использовали 512 чисел. Поскольку мы не можем нарисовать что-то с 512 измерениями, мы используем метод, называемый PCA, чтобы уменьшить количество измерений до двух, надеюсь, сохранив большую часть исходного значения.

Оно работает! На самом деле вы можете обучить модель, которая сможет создавать списки чисел, которые имеют семантическое значение. Компьютер не знает, что ребенок — это маленький человечек, но он знает, что обычно видит это детское слово вокруг aww и (надеюсь) спит чаще, чем государство и правительство. Я напишу еще немного о том, как именно это происходит, но до тех пор, если вам интересно, это может быть хорошим местом для проверки.

Эти «списки чисел» очень важны, поэтому в терминологии машинного обучения они получили собственное название — Embeddings. Почему вложения? потому что мы выполняем вложение (такое творческое), которое представляет собой процесс сопоставления (перевода) термина из одной формы (слова) в другую (список чисел). Это много ().
С этого момента мы будем называть слова вложениями, которые, как объяснялось, представляют собой списки чисел, которые содержат семантическое значение любого слова, которое оно обучено представлять.

Создание вложений с помощью Pytorch

Сначала мы посчитаем количество уникальных токенов, которые у нас есть, для простоты скажем, 1000. Создание слоя встраивания, которое является первой частью архитектуры Transformer, будет таким же простым, как написание этого кода:

*Общее замечание по коду — не воспринимайте этот код и его соглашения как хороший стиль кодирования, он написан специально для того, чтобы его было легко понять.

import torch.nn as nn

vocabulary_size = 2
num_dimensions_per_word = 2

embds = nn.Embedding(vocabulary_size, num_dimensions_per_word)

print(embds.weight)
---------------------
output:
Parameter containing:
tensor([[-1.5218, -2.5683],
        [-0.6769, -0.7848]], requires_grad=True)

Теперь у нас есть матрица вложения, которая в данном случае представляет собой матрицу 2 на 2, сгенерированную случайными числами, полученными из нормального распределения N(0,1) (например, распределение со средним значением 0 и дисперсией 1).
Обратите внимание на require_grad=Верно, это язык Pytorch для того, чтобы сказать, что эти 4 числа являются изучаемыми весами. Их можно и нужно настраивать в процессе обучения, чтобы лучше представлять данные, получаемые моделью.

В более реалистичном сценарии мы можем ожидать что-то близкое к матрице 10k на 512, которая представляет весь наш набор данных в числах.

vocabulary_size = 10_000
num_dimensions_per_word = 512

embds = nn.Embedding(vocabulary_size, num_dimensions_per_word)

print(embds)
---------------------
output:
Embedding(10000, 512)

* Забавный факт (я могу придумать вещи повеселее): иногда можно услышать, что языковые модели используют миллиарды параметров. Этот начальный, не слишком сумасшедший слой содержит 10_000 на 512 параметров, что само по себе составляет 5 миллионов. Эта LLM (Large Language Model) сложная штука, она требует много вычислений.
Параметры здесь — это красивое слово для тех чисел (-1,525 и т. д.), просто они подвержены изменениям и будут меняться в процессе обучения.

Зачем использовать целых 512, а не 5? потому что большее количество чисел означает, что мы, вероятно, можем получить более точное значение. Отлично, тогда давайте использовать миллион! почему нет? потому что больше чисел означает больше вычислений, больше вычислительной мощности, дороже обучение и т. д. Было обнаружено, что 512 — хорошее место посередине.

Длина последовательности

При обучении модели мы собираемся собрать целую кучу слов. Это более эффективно с точки зрения вычислений и помогает модели обучаться, поскольку она получает больше контекста. Как уже упоминалось, каждое слово будет представлено 512-мерным вектором (список с 512 числами), и каждый раз, когда мы передаем входные данные в модель (так называемый прямой проход), мы будем отправлять кучу предложений, а не только одно. Например, мы решили поддерживать последовательность из 50 слов. Это означает, что мы возьмем x слов в предложении, если x > 50, мы разделим его и возьмем только первые 50, если x ‹ 50, нам все еще нужно, чтобы размер был точно таким же (я скоро объясните, почему) и мы добавляем отступы, которые представляют собой специальные фиктивные строки, к остальной части предложения. Например, если мы поддерживаем предложение из 7 слов, и у нас есть предложение «Где бог». Мы добавляем 4 отступа, поэтому на вход модели будет «Где бог ‹PAD› ‹PAD› ‹PAD› ‹PAD›». На самом деле, мы обычно добавляем по крайней мере еще 2 специальных отступа, чтобы модель знала, где начинается предложение и где оно заканчивается, поэтому на самом деле это будет что-то вроде «‹StartOfSentence› Where is god ‹PAD› ‹PAD› ‹EndOfSentence›».

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

Позиционные кодировки

Теперь у нас есть способ представлять (и учить) слова в нашем словаре. Давайте сделаем это еще лучше, закодировав положение слов. Почему это важно? потому что если я возьму эти два предложения:

1. Мужчина играл с моим котом
2. Кот играл с моим мужчиной

Мы можем представить два предложения, используя одни и те же вложения, но предложения имеют разные значения. Мы можем думать о таких данных, порядок которых не имеет значения. Если я вычисляю сумму чего-либо, не имеет значения, с чего я начну. в языке — порядок имеет значение. Вложение содержит семантические значения, но не имеет точного значения порядка. В каком-то смысле они сохраняют порядок, потому что эти вложения были изначально созданы в соответствии с некоторой лингвистической логикой (ребенок кажется ближе ко сну, а не к состоянию), но одно и то же слово может иметь более одного значения само по себе, и, что более важно, когда оно находится в другой контекст. Мы можем улучшить это много. Авторы предлагают добавить позиционное кодирование к встраиваниям. Мы делаем это, вычисляя вектор положения для каждого слова и добавляя его (суммируя два) вектора. Векторы позиционного кодирования должны иметь одинаковый размер, чтобы их можно было добавить. Формула позиционного кодирования использует две функции: синус для четных позиций (например, 0-е слово, 2-е слово, 4-е, 6-е и т. д.) и косинус для нечетных позиций (например, 1-е, 3-е, 5-е и т. д.).

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

В приведенной выше формуле верхняя строка представляет четные числа, начиная с 0 (i = 0), и продолжает оставаться четной (2*1, 2*2, 2*3). Вторая строка представляет нечетные числа таким же образом.

Каждый позиционный вектор представляет собой вектор number_of_dimensions (512 в нашем случае) с номерами от 0 до 1.

from math import sin, cos
max_seq_len = 50 
number_of_model_dimensions = 512


positions_vector = np.zeros((max_seq_len, number_of_model_dimensions))

for position in range(max_seq_len):
    for index in range(number_of_model_dimensions//2):
        theta = pos / (10000 ** ((2*i)/number_of_model_dimensions))
        positions_vector[position, 2*index ] = sin(theta)
        positions_vector[position, 2*index + 1] = cos(theta)

print(positions_vector)
---------------------
output:
(50, 512)

Если мы напечатаем первое слово, то увидим, что получаем только 0 и 1 взаимозаменяемо.

print(positions_vector[0][:10])
---------------------
output:
array([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])

Второй номер уже намного разнообразнее.

print(positions_vector[1][:10])
---------------------
output:
array([0.84147098, 0.54030231, 0.82185619, 0.56969501, 0.8019618 ,
       0.59737533, 0.78188711, 0.62342004, 0.76172041, 0.64790587])

*Вдохновение для кода взято здесь.

Как видим, разные позиции приводят к разным представлениям. Чтобы завершить первый ввод целиком (в квадрате красного цвета на картинке ниже), мы добавляем числа в матрице позиций к нашей матрице вложений ввода. В итоге мы получаем матрицу того же размера, что и вложение, только на этот раз числа содержат семантическое значение + порядок.

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