Документ Transformer Внимание - это все, что вам нужно на момент написания этой статьи (14 августа 2019 г.) является неизменным документом №1 по Arxiv Sanity Preserver. Этот документ показал, что, используя только механизмы внимания, можно достичь самых современных результатов языкового перевода. Последующие модели, построенные на Transformer (например, BERT), достигли отличной производительности в широком спектре задач обработки естественного языка.

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

Бумага

Васвани А., Шазир Н., Пармар Н., Ушкорейт Дж., Джонс Л., Гомес А. Н., Кайзер Ł, Полосухин И. Все, что вам нужно - это внимание. NeurIPS 2017 (стр. 5998–6008).

Все цитаты в этом посте взяты из газеты.

Код

Для реализации модели Transformer в Pytorch см. The Annotated Transformer, который представляет собой записную книжку iPython, содержащую текст статьи Transformer с вкраплениями рабочего кода Pytorch. В этом посте я буду ссылаться на код из аннотированного трансформатора и подробно объяснять некоторые разделы кода.

Обратите внимание, что порядок, в котором мы будем обсуждать части преобразователя здесь, отличается от порядка либо в исходной статье, либо в преобразователе с аннотациями. Здесь я организовал все в соответствии с потоком данных через модель, например начиная с английского предложения «Я люблю деревья» и прорабатывая Трансформатор до испанского перевода «Me gustan los arboles».

Здесь используются гиперпараметры базовой модели Transformer, как показано в этом отрывке из Таблицы 3 статьи Transformer:

Это те же гиперпараметры, которые используются в коде функции make_model (src_vocab, tgt_vocab, N = 6, d_model = 512, d_ff = 2048, h = 8, dropout = 0.1).

Представление входов и выходов

При переводе с английского на испанский входом в Transformer является английское предложение («Я люблю деревья»), а на выходе - испанский перевод этого предложения («Me gustan los arboles»).

Представление ввода

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

Длина каждого горячего вектора заранее определяется размером словаря. Если мы хотим представить 10 000 различных слов, нам нужно использовать горячие векторы длиной 10 000 (чтобы у нас был уникальный слот для единицы для каждого слова.) Для получения дополнительной информации о горячих векторах см. Сообщение Подготовка табличных данных для нейронных сетей (включая код!), Раздел Представление категориальных переменных: горячие векторы.

Вложения слов

Мы не хотим скармливать простым однообразным векторам Transformer, потому что они редкие, огромные и ничего не говорят нам о характеристиках этого слова. Поэтому мы изучаем вложение слова, которое представляет собой меньшее вещественное векторное представление слова, которое несет некоторую информацию о слове. Мы можем сделать это, используя nn.Embedding в Pytorch, или, в более общем смысле, умножив наш горячий вектор на выученную матрицу весов W.

nn. Вложение состоит из весовой матрицы W, которая преобразует простой вектор в вектор с действительными значениями. Матрица весов имеет форму (num_embeddings, embedding_dim). num_embeddings - это просто размер словаря - вам нужно одно вложение для каждого слова в словаре. embedding_dim - это размер вашего реального представления; вы можете выбрать, что хотите - 3, 64, 256, 512 и т. д. В документе Transformers они выбирают 512 (гиперпараметр d_model = 512).

Люди называют nn.Embedding «таблицей поиска», потому что вы можете представить себе весовую матрицу просто как стек вещественных векторных представлений слов:

Есть два варианта работы с весовой матрицей Pytorch nn. Один из вариантов - инициализировать его с помощью предварительно обученных встраиваний и оставить фиксированным, и в этом случае это действительно просто таблица поиска. Другой вариант - инициализировать его случайным образом или с помощью предварительно обученных встраиваний, но сохранить его обучаемым. В этом случае представления слов будут уточняться и изменяться во время тренировки, потому что матрица весов будет уточняться и изменяться во время тренировки.

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

В преобразователе с аннотациями вложения слов создаются с использованием класса «Embeddings», который, в свою очередь, использует nn.Embedding:

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

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

  • «Мне нравятся деревья» и «Деревья выросли» содержат слово «деревья».
  • Слово «деревья» имеет одно и то же встраивание слова независимо от того, третье это слово или второе слово в предложении.

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

Авторы Transformer предлагают добавить «позиционное кодирование» для решения этой проблемы. Позиционное кодирование позволяет преобразователю использовать информацию о порядке слов. Позиционное кодирование использует последовательность векторов с действительными значениями, которые фиксируют информацию об упорядочивании. Каждое слово в предложении суммируется с определенным вектором позиционного кодирования на основе его положения в предложении:

Как именно «вектор позиции» несет информацию о позиции? Авторы исследовали два варианта создания векторов позиционного кодирования:

  • вариант 1: изучение векторов позиционного кодирования (требуются обучаемые параметры),
  • вариант 2: вычисление векторов позиционного кодирования с использованием уравнения (не требует обучаемых параметров)

Они обнаружили, что производительность обоих вариантов была схожей, поэтому они выбрали последний вариант (расчет), потому что он не требует параметров и также может позволить модели хорошо работать даже с длинами предложений, которые не были замечены во время обучения.

Вот формула, которую они используют для вычисления позиционной кодировки:

В этом уравнении

  • pos - это позиция слова в предложении (например, «2» для второго слова в предложении)
  • i индексируется в измерении встраивания, т. е. это позиция вдоль измерения вектора позиционного кодирования. Для вектора позиционного кодирования длиной d_model = 512 у нас будет диапазон i от 1 до 512.

Зачем использовать синус и косинус? По словам авторов, «каждое измерение позиционного кодирования соответствует синусоиде. […] Мы выбрали эту функцию, потому что предположили, что она позволит модели легко научиться посещать занятия по относительным позициям ».

В Аннотированном преобразователе позиционная кодировка создается и добавляется к вложениям слов с помощью класса PositionalEncoding:

Сводка ввода

Теперь у нас есть входное представление: английское предложение «Мне нравятся деревья», преобразованное в три вектора (по одному на каждое слово):

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

Мы проделаем тот же процесс (встраивание слов + позиционное кодирование) для представления вывода, которым в данном случае является испанское предложение «Me gustan los arboles».

Теперь мы рассмотрели нижнюю часть рисунка 1 статьи Transformers: как входные и выходные предложения обрабатываются перед подачей в остальную часть модели (не путать с «выходными вероятностями» вверху, которые что-то другое):

Форма входного тензора (и выходного тензора) после встраивания и позиционного кодирования равна [nbatches, L, 512], где «nbatches» - это размер пакета (в соответствии с именем переменной аннотированного преобразователя), L - длина последовательность (например, L = 3 для «Мне нравятся деревья»), а 512 - измерение встраивания / измерение позиционного кодирования. Обратите внимание, что пакеты создаются тщательно, чтобы один пакет содержал последовательности одинаковой длины.

Кодировщик

Пришло время кодировщику обработать наше предложение. Вот как выглядит кодировщик:

Как видно из рисунка, кодировщик состоит из N = 6 одинаковых слоев, уложенных друг на друга.

При переводе с английского на испанский слово «in» - это английское предложение, например «Мне нравятся деревья», представленные в формате «вложения слов + позиционные кодировки», о котором мы только что говорили. То, что выходит «наружу», - это другое представление этого предложения.

Каждый из шести уровней кодировщика содержит два подуровня:

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

Мы поговорим о том, что делает каждый из этих подуровней. Но сначала вот код из Аннотированного преобразователя, показывающий, как построен кодировщик:

Класс «Кодировщик» берет ‹layer› и складывает его ‹N› раз. ‹Layer›, который он принимает, является экземпляром класса EncoderLayer.

Класс EncoderLayer инициализируется с помощью ‹size›, ‹self_attn›, ‹feed_forward› и ‹dropout›:

  • ‹Size› - это d_model, который равен 512 в базовой модели.
  • ‹Self_attn› является экземпляром класса MultiHeadedAttention. Это соответствует подуровню 1.
  • ‹Feed_forward› является экземпляром класса PositionwiseFeedForward. Это соответствует подуровню 2.
  • ‹Dropout› - это показатель отсева, например 0,1

Подуровень кодировщика 1: механизм внимания нескольких головок

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

Устное резюме внимания: ключи, запросы и значения

Назовем наш вклад в механизм внимания «х». В начале кодировщика x - это наше начальное представление предложения. В середине кодировщика «x» - это результат предыдущего EncoderLayer. Например, EncoderLayer3 получает свой вход «x» из выхода EncoderLayer2.

Мы используем x для вычисления ключей, запросов и значений. Ключи, запросы и значения вычисляются из x с использованием различных линейных слоев:

где для конкретного уровня кодировщика linear_k, linear_q и linear_v - это отдельные уровни нейронной сети с прямой связью, которые идут от размерности 512 к размерности 512 (от d_model до d_model). Linear_k, linear_q и linear_v имеют разные веса, которые изучаются отдельно. Если бы мы использовали одинаковые веса слоев для вычисления ключей, запросов и значений, то все они были бы идентичны друг другу, и нам не потребовались бы разные имена для них.

Когда у нас есть ключи (K), запросы (Q) и значения (V), мы рассчитываем внимание следующим образом:

Это уравнение (1) в статье Transformer. Давайте разберемся, что происходит:

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

Здесь Q - стек запросов q, а K - стек ключей k.

После того, как мы возьмем скалярное произведение, мы разделим его на квадратный корень из d_k:

Какой смысл делить на sqrt (d_k)? Авторы объясняют, что они масштабируют скалярные произведения на sqrt (d_k), чтобы предотвратить увеличение скалярных произведений по мере увеличения d_k (длины вектора).

Пример: скалярное произведение векторов [2,2] и [2,2] равно 8, но скалярное произведение векторов [2,2,2,2,2] и [2,2,2,2, 2] равно 20. Мы не хотим, чтобы скалярное произведение было огромным, если мы выбираем большую длину вектора, поэтому мы делим его на квадратный корень из длины вектора, чтобы смягчить этот эффект. Огромное значение скалярного произведения - это плохо, потому что оно «подтолкнет функцию softmax к областям, где у нее очень маленькие градиенты».

Это подводит нас к следующему шагу - применению softmax, который сжимает числа в диапазоне (0,1):

Обсуждение функции softmax см. В этом посте.

Что у нас есть на данный момент? На данный момент у нас есть набор чисел от 0 до 1, которые мы можем рассматривать как веса нашего внимания. Мы рассчитали эти веса внимания как softmax (QK ^ T / sqrt (d_k)).

Последний шаг - произвести взвешенную сумму значений V, используя только что вычисленные веса внимания:

И это все уравнение!

Более подробное описание многоголового внимания с кодом

Этот раздел относится к коду из Аннотированного преобразователя, поскольку я думаю, что просмотр кода - хороший способ понять, что происходит.

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

В Аннотированном преобразователе многоголовое внимание реализовано с помощью класса MultiHeadedAttention:

Экземпляр этого класса инициализируется:

  • ‹H› = 8, количество «голов». В базовой модели Transformer 8 головок.
  • ‹D_model› = 512
  • ‹Dropout› = процент отсева = 0,1

Размер ключей d_k рассчитывается как d_model / h. Итак, в этом случае d_k = 512/8 = 64.

Давайте подробнее рассмотрим функцию forward () из MultiHeadedAttention:

Мы видим, что входными данными для forward () являются запрос, ключ, значение и маска. Пока не обращайте внимания на маску. Откуда берутся запрос, ключ и значение? Фактически, они происходят от символа «x», который трижды повторяется в EncoderLayer (см. Желтое выделение):

X был получен из предыдущего EncoderLayer, или, если мы находимся на EncoderLayer1, x - это наше начальное представление предложения. (Обратите внимание, что self.self_attn в классе EncoderLayer является экземпляром MultiHeadedAttention.)

В классе MultiHeadedAttention мы возьмем старые запросы (старый x), старые ключи (также старый x) и старые значения (также старый x) и создадим новые запросы, ключи и значения. которые отличаются друг от друга.

Обратите внимание, что форма ввода «запрос» - [nbatches, L, 512], где nbatches - это размер пакета, L - длина последовательности, а 512 - это d_model. Входы «ключ» и «значение» также имеют форму [nbatches, L, 512].

Шаг 1) в функции MultiHeadedAttention forward () гласит: «Выполняйте все линейные проекции в пакетном режиме из d_model =› h x d_k ».

  • Мы сделаем три разных линейных проекции на один и тот же тензор формы [nbatches, L, 512], чтобы получить новые запросы, ключи и значения, каждое из формы [nbatches, L, 512]. (Форма не изменилась, так как линейный слой 512 - ›512).
  • Затем мы изменим этот результат на 8 разных голов. Например, запросы имеют форму [nbatches, L, 512] и преобразуются с помощью view () в [nbatches, L, 8, 64], где h = 8 - количество головок, а d_k = 64 - размер ключа.
  • Наконец, мы поменяем местами размеры 1 и 2, используя транспонирование, чтобы получить форму [nbatches, 8, L, 64]

Шаг 2) в коде гласит: «Обратите внимание на все проецируемые векторы в пакете».

  • Конкретная строка - x, self.attn = Внимание (запрос, ключ, значение, маска = маска, dropout = self.dropout)
  • Вот уравнение, которое мы реализуем с помощью функции Внимание ():

Чтобы подсчитать оценки, мы сначала выполняем матричное умножение между запросом [nbatches, 8, L, 64] и транспонированным ключом [nbatches, 8, 64, L]. Это QK ^ T из уравнения. Итоговая форма оценок: [nbatches, 8, L, L].

Затем мы вычисляем веса внимания p_attn, применяя softmax к оценкам. Если возможно, мы также применяем выпадение к весам внимания. Таким образом, p_attn соответствует softmax (QK ^ T / sqrt (d_k)) в приведенном выше уравнении. Форма p_attn - [nbatches, 8, L, L], потому что применение softmax и dropout к оценкам не меняет форму.

Наконец, мы выполняем матричное умножение между весами внимания p_attn [nbatches, 8, L, L] и значениями [nbatches, 8, L, 64]. Результатом является окончательный результат нашей функции внимания с shape [nbatches, 8, L, 64]. Мы возвращаем это из функции вместе с самими весами внимания p_attn.

Обратите внимание, что на входе в функцию внимания и на выходе функции внимания у нас есть 8 голов (размерность 1 нашего Тензора, например [nbatches, 8, L, 64].). различное матричное умножение для каждой из восьми голов. Вот что подразумевается под «многоглавым» вниманием: дополнительное измерение «голов» позволяет нам иметь несколько «подпространств представления». Это дает нам восемь различных способов рассмотрения одного и того же предложения.

Шаг 3) (в классе MultiHeadedAttention, поскольку мы теперь вернулись из функции Внимание ()) - это конкатенация с использованием view () с последующим применением последнего линейного слоя.

Конкретные строки шага 3:

x = x.transpose (1, 2) .contiguous (). view (nbatches, -1, self.h * self.d_k)

вернуть self.linears [-1] (x)

  • x - это то, что возвращается функцией внимания: наше восьмиглавое представление [nbatches, 8, L, 64].
  • Мы транспонируем его, чтобы получить [nbatches, L, 8, 64], а затем изменяли его с помощью представления, чтобы получить [nbatches, L, 8 x 64] = [nbatches, L, 512]. Эта операция изменения формы с использованием view () в основном представляет собой объединение 8 голов.
  • Наконец, мы применяем наш последний линейный слой из self.linears. Этот линейный слой идет от 512 до 512. Обратите внимание, что в Pytorch, если многомерный тензор задан линейному слою, линейный слой применяется только к последнему измерению. Таким образом, результат self.linears [-1] (x) по-прежнему имеет форму [nbatches, L, 512].
  • Обратите внимание, что [nbatches, L, 512] - это именно та фигура, которую нам нужно передать другому слою MultiHeadedAttention….
  • … Но прежде чем мы это сделаем, у нас есть последний шаг, подуровень кодировщика 2, о котором мы поговорим сразу после того, как рассмотрим рисунок 2 из статьи о трансформаторе.

Вот рисунок 2 из статьи о трансформаторе:

Слева, в разделе «Масштабируемое внимание к скалярному произведению» у нас есть визуальное изображение того, что вычисляет функция Внимание (): softmax (QK ^ T / sqrt (d_k)) V. Он «масштабируется» из-за деления на sqrt (d_k) и является «скалярным произведением», потому что QK ^ T представляет скалярное произведение между набором сложенных запросов и набором сложенных ключей.

Справа, в разделе «Многоголовое внимание», у нас есть визуальное изображение того, что делает класс MultiHeadedAttention. Теперь вы должны узнать части этой фигуры:

  • Внизу идут старые V, K и Q, которые являются нашим выходом «x» из предыдущего EncoderLayer (или нашим x из представления входного предложения для EncoderLayer1).
  • Затем мы применяем линейный слой (поля «Linear») для вычисления обработанных V, K и Q (которые явно не показаны).
  • Мы вводим обработанные V, K и Q в наше масштабируемое скалярное внимание с 8 головами. Это функция внимания ().
  • Наконец, мы объединяем результат функции Внимание () по 8 головам и применяем последний линейный слой для получения нашего многоголового вывода внимания.

Подуровень кодировщика 2: полностью подключенная сеть прямого распространения с функцией позиционирования

Мы почти закончили понимание всего EncoderLayer! Напомним, что это базовая структура одного EncoderLayer:

Мы рассмотрели подуровень 1, внимание нескольких голов. Теперь мы рассмотрим подуровень 2, сеть прямого распространения.

Подуровень 2 легче понять, чем подуровень 1, потому что подуровень 2 - это просто нейронная сеть прямого распространения. Вот выражение для подслоя 2:

Другими словами, мы применяем полносвязный слой с весами W1 и смещениями b1, выполняем нелинейность ReLU (максимум с нулем), а затем применяем второй полносвязный слой с весами W2 и смещениями b2.

Вот соответствующий фрагмент кода из Annotated Transformer:

Таким образом, кодировщик состоит из 6 слоев кодировщика. Каждый EncoderLayer имеет 2 подуровня: подуровень 1 для многогранного внимания и подуровень 2, который представляет собой просто нейронную сеть прямого распространения.

Декодер

Теперь, когда мы понимаем кодировщик, декодер будет легче понять, потому что он похож на кодировщик. Вот снова рисунок 1 с несколькими дополнительными аннотациями:

Вот три основных различия между декодером и кодировщиком:

  • Подуровень декодера 1 использует «замаскированное» внимание нескольких голов, чтобы предотвратить незаконное «заглядывание в будущее».
  • Декодер имеет дополнительный подуровень, обозначенный на рисунке выше «подуровень 2». Этот подуровень представляет собой «внимание нескольких головок кодировщика-декодера».
  • К выходным данным декодера применяются линейный слой и softmax для получения выходных вероятностей, которые указывают следующее предсказанное слово.

Поговорим о каждой из этих частей.

Подуровень декодера 1: замаскированное внимание нескольких головок

Смысл маскировки в многоголовом слое внимания состоит в том, чтобы не дать декодеру «заглянуть в будущее», т. Е. Мы не хотим, чтобы в нашу модель были встроены хрустальные шары.

Маска состоит из единиц и нулей:

Строки кода в функции Внимание (), которые используют маску, находятся здесь:

если маска отлична от None:

scores = scores.masked_fill (маска == 0, -1e9)

Функция masked_fill (маска, значение) заполняет элементы тензора [self] значением [value], где [mask] - True. Форма [маски] должна быть «транслируемой с формой лежащего в основе тензора ». Итак, в основном, мы используем маску, чтобы обнулить те части тензорной оценки, которые соответствуют будущим словам, которые мы не должны видеть.

Процитируем авторов: «Мы […] модифицируем подуровень самовнимания в стеке декодера, чтобы позиции не переходили на последующие позиции. Эта маскировка в сочетании с тем фактом, что выходные вложения смещены на одну позицию, гарантирует, что прогнозы для позиции i могут зависеть только от известных выходных данных в позициях меньше i ».

Подуровень декодера 2: внимание нескольких головок кодировщика-декодера

Вот код для DecoderLayer:

Линия, подчеркнутая желтым цветом, обозначает «внимание кодировщика-декодера»:

x = self.sublayer [1] (x, лямбда x: self.src_attn (x, m, m, src_mask))

self.src_attn - это экземпляр MultiHeadedAttention. Входные данные: query = x, key = m, value = m и mask = src_mask. Здесь x берется из предыдущего DecoderLayer, а m или «память» берется из вывода Encoder (то есть вывода EncoderLayer6).

(Обратите внимание, что линия над линией желтого цвета, x = self.sublayer [0] (x, lambda x: self.self_attn (x, x, x, tgt_mask)), определяет самовнимание декодера подслоя декодера. 1, о котором мы только что говорили. Он работает точно так же, как и самовнимание кодировщика, за исключением дополнительного шага маскирования.)

В сторону: полный обзор внимания в трансформаторе

Мы рассмотрели три вида внимания в Transformer. Вот резюме автора из статьи Transformer о трех способах использования внимания в их модели:

Конечный результат декодера: линейный и Softmax для получения вероятностей вывода

В конце стека декодера мы передаем окончательный вывод декодера в линейный слой, за которым следует softmax, чтобы предсказать следующий токен.

Мы запустили кодировщик один раз, чтобы получить выходные данные стека кодировщика, который представляет собой входное предложение на английском языке «Мне нравятся деревья»; Теперь мы собираемся запустить декодер несколько раз, чтобы он мог предсказать несколько слов в испанском переводе «Me gustan los arboles».

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

После обучения сети (т.е. когда мы выполняем вывод) мы выполним следующие шаги (обратите внимание, что выходные данные кодировщика рассчитываются один раз, а затем используются несколько раз):

  1. Подайте в декодер выход нашего кодировщика (который представляет собой полное предложение на английском языке «I like tree») и специальный токен начала предложения, ‹/s›, в слоте «выходное предложение» в нижней части декодера. Декодер выдаст предсказанное слово, которым в нашем примере должно быть «Я» (первое слово в нашем испанском переводе).
  2. Подайте в декодер выход нашего кодировщика, начало предложения и слово, которое только что создал декодер, т. Е. Подайте декодеру выход кодера и «‹/s› Me». На этом этапе декодер должен выдать предсказанное слово «густан».
  3. Подайте на декодер выход нашего кодировщика и «‹/s› Me gustan.» На этом этапе декодер должен выдать предсказанное слово «лось».
  4. Подайте на декодер выход нашего кодировщика и «‹/s› Me gustan los.» На этом этапе декодер должен выдать предсказанное слово «arboles».
  5. Подайте на декодер выход нашего кодировщика и «‹/s› Me gustan los arboles». На этом этапе декодер должен создать токен конца предложения, например «‹/eos›.»
  6. Поскольку декодер создал токен конца предложения, мы знаем, что перевод этого предложения завершен.

А во время тренировки? Во время обучения декодер может быть не очень хорошим - поэтому он может давать неверные предсказания следующего слова. Если декодер производит мусор, мы не хотим возвращать этот мусор обратно в декодер для следующего шага. Итак, во время обучения мы используем процесс, называемый принуждение учителя (ref1, ref2, ref3).

При принудительном использовании учителя мы используем тот факт, что знаем, каким должен быть правильный перевод, и передаем декодеру символы, которые он должен предсказать. Обратите внимание, что мы не хотим, чтобы декодер просто изучал задачу копирования, поэтому мы подадим ему «‹/s› Me gustan los» только на том этапе, где он должен предсказывать слово «arboles». Это реализуется посредством:

  • маскирование, о котором мы говорили ранее, при котором будущие слова обнуляются (то есть не подавать декодеру «los arboles», когда он должен предсказывать «густан»), и
  • сдвиг вправо, чтобы слово «настоящее» тоже не вводилось (т.е. не передавать декодеру «густан», когда он должен предсказывать «густан»).

Затем рассчитываются потери с использованием распределения вероятностей по возможным следующим словам, которые фактически создал декодер (например, [0,01,0.01,0.02,0.7,0.20,0.01,0.05]), в сравнении с распределением вероятностей, которое он должен был произвести (которое равно [ 0,0,0,0,1,0,0] с «1» в слоте «arboles», если мы используем горячие векторы в качестве основной истины.)

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

Вот класс Pytorch Generator, используемый для последнего линейного слоя и softmax:

Веселее

Поздравляем - вы только что проработали ключевые части модели Transformer! В Transformer встроено несколько дополнительных концепций, которые я кратко рассмотрю здесь:

Выпадение: выпадение используется в нескольких разных местах трансформатора. В этом методе случайное подмножество нейронов игнорируется во время каждого прямого / обратного прохода, чтобы предотвратить переобучение. Подробнее о выбывании читайте в этом посте.

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

  • Остаточная связь: если мы вычисляем некоторую функцию f (x), остаточная связь дает результат f (x) + x. Другими словами, мы добавляем исходный ввод обратно к только что рассчитанному выводу. Подробнее см. Эту статью.
  • Слойная нормализация: это метод, который нормализует входные данные по функциям (в отличие от пакетной нормализации, которая нормализует функции по пакету). Подробнее см. Эту статью.

На следующей диаграмме EncoderLayer я закрасил красным цветом соответствующие части: стрелку и поле «Добавить и норма», которые вместе представляют остаточное соединение и нормализацию слоя:

Цитата: «Результатом каждого подслоя является LayerNorm (x + Sublayer (x)), где Sublayer (x) - это функция, реализованная самим подслоем. Чтобы облегчить эти остаточные связи, все подслои в модели, а также слои встраивания производят выходные данные с размером dmodel = 512 ».

Для реализации Pytorch см. Класс «LayerNorm» преобразователя с аннотациями, а также класс «SublayerConnection», который применяет LayerNorm, Dropout и остаточное соединение.

Оптимизатор Ноама: Трансформер обучается с помощью Оптимизатора Адама. Авторы сообщают о специальной формуле для изменения скорости обучения на протяжении всего обучения. Сначала скорость обучения увеличивается линейно для определенного количества шагов обучения. После этого скорость обучения уменьшается пропорционально обратному квадратному корню из номера шага. Этот график скорости обучения реализован в классе Annotated Transformer NoamOpt.

Сглаживание этикеток. Наконец, авторы применяют технику сглаживания этикеток. По сути, сглаживание меток берет правильные ответы с горячим кодированием и сглаживает их, так что большая часть вероятностной массы идет туда, где была 1, а остаток распределяется по всем слотам, которые были 0. Подробнее см. Эту статью. Для реализации см. Класс аннотированного преобразователя LabelSmoothing.

Большое резюме!

  • Трансформатор состоит из кодировщика и декодера.
  • Входное предложение (например, «Мне нравятся деревья») и выходное предложение (например, «Me gustan los arboles») представлены с использованием встраивания слова и вектора позиционного кодирования для каждого слова.
  • Кодировщик состоит из 6 слоев кодировщика. Декодер состоит из 6 слоев декодера.
  • Каждый EncoderLayer имеет два подуровня: многоголовое самовнимание и уровень прямой связи.
  • Каждый DecoderLayer имеет три подуровня: многоголовое самовнимание, многоголовое внимание кодера-декодера и уровень прямой связи.
  • В конце декодера линейный слой и softmax применяются к выходным данным декодера для предсказания следующего слова.
  • Кодировщик запускается один раз. Декодер запускается несколько раз, чтобы на каждом шаге выдавать предсказываемое слово.

Ссылки

Об избранном изображении

Представленное изображение является кадром из картины Хрустальный шар Джона Уильяма Уотерхауса ».

Первоначально опубликовано на http://glassboxmedicine.com 15 августа 2019 г.