Чем регулируется размер вектора выхода сети transformer
Перейти к содержимому

Чем регулируется размер вектора выхода сети transformer

  • автор:

Transformer — новая архитектура нейросетей для работы с последовательностями

Необходимое предисловие: я решил попробовать современный формат несения света в массы и пробую стримить на YouTube про deep learning.

В частности, в какой-то момент меня попросили рассказать про attention, а для этого нужно рассказать и про машинный перевод, и про sequence to sequence, и про применение к картинкам, итд итп. В итоге получился вот такой стрим на час:

Я так понял по другим постам, что c видео принято постить его транскрипт. Давайте я лучше вместо этого расскажу про то, чего в видео нет — про новую архитектуру нейросетей для работы с последовательностями, основанную на attention. А если нужен будет дополнительный бэкграунд про машинный перевод, текущие подходы, откуда вообще взялся attention, итд итп, вы посмотрите видео, хорошо?

Новая архитектура называется Transformer, была разработана в Гугле, описана в статье Attention Is All You Need (arxiv) и про нее есть пост на Google Research Blog (не очень детальный, зато с картинками).

Сверх-краткое содержание предыдущих серий

Задача машинного перевода в deep learning сводится к работе с последовательностями (как и много других задач): мы тренируем модель, которая может получить на вход предложение как последовательность слов и выдать последовательность слов на другом языке. В текущих подходах внутри модели обычно есть энкодер и декодер — энкодер преобразует слова входного предложения в один или больше векторов в неком пространстве, декодер — генерирует из этих векторов последовательность слов на другом языке.
Стандартные архитектуры для энкодера — RNN или CNN, для декодера — чаще всего RNN. Дальнейшее развитие навернуло на эту схему механизм attention и про это уже лучше посмотреть стрим.

И вот предлагается новая архитектура для решения этой задачи, которая не является ни RNN, ни CNN.


Вот основная картинка. Что в ней что!

Энкодер и Multi-head attention layer

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

Вот он конкретно:

Идея в том, что каждое слово параллельно проходит через слои, изображенные на картинке.
Некоторые из них — это стандартные fully-connected layers, некоторые — shortcut connections как в ResNet (там, где на картинке Add).

Но новая интересная вещь в этих слоях — это Multi-head attention. Это специальный новый слой, который дает возможность каждому входному вектору взаимодействовать с другими словами через attention mechanism, вместо передачи hidden state как в RNN или соседних слов как в CNN.


Ему на вход даются вектора Query, и несколько пар Key и Value (на практике, Key и Value это всегда один и тот же вектор). Каждый из них преобразуется обучаемым линейным преобразованием, а потом вычисляется скалярное произведение Q со всеми K по очереди, прогоняется результат этих скалярных произведений через softmax, и с полученными весами все вектора V суммируются в единый вектор. Эта формулировка attention очень близка к предыдущим работам, где используется attention.

Единственное, что они к нему дополняют — что таких attention’ов параллельно тренируется несколько (их количество на картинке обозначено через h), т.е. несколько линейных преобразований и параллельных скалярных произведений/взвешенных сумм. И потом результат всех этих параллельных attention’ов конкатенируется, еще раз прогоняется через обучаемое линейное преобразование и идет на выход.
Но в целом, каждый такой модуль получает на вход вектор Query и набор векторов для Key и Value, и выдает один вектор того же размера, что и каждый из входов.

Непонятно, что это дает. В стандартном attention «интуиция» ясна — сеть attention пытается выдать соответствие одного слова другому в предложении, если они близки чем-то. И это одна сеть. Здесь тоже самое, но куча сетей параллельно? И делают они тоже самое, а выход конкантенируется? Но в чем тогда смысл, не обучатся ли они в точности одному и тому же?
Нет. Если есть необходимость обращать внимание на несколько аспектов слов, то это дает сети возможность это сделать.
Такой трюк используется довольно часто — оказывается, что тупо разных начальных случайных весов хватает, чтобы толкать разные слои в разные стороны.

Что такое несколько аспектов слов?.
Например, у слова есть фичи про его смысловое значение и про грамматическое.
Хочется получить вектора, соответствующие соседям с точки зрения смысловой составляющей и с грамматической.

Так как на выход такой блок выдает вектор того же размера, что и был на входе, то этот блок можно вставлять в сеть несколько раз, добавляя сети глубину. На практике, они используют комбинацию из Multi-head attention, residual layer и fully-connected layer 6 раз, то это есть это такая достаточно глубокая сеть.

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

Еще у них воткнут LayerNormalization (arxiv). Это процедура нормализации, которая нормализует выходы от всех нейронов в леере внутри каждого сэмпла (в отличие от каждого нейрона отдельно внутри батча, как в Batch Normalization, видимо потому что BN им не нравился).

Попробуем резюмировать работу энкодера по пунктам.

  1. Делаются эмбеддинги для всех слов предложения (вектора одинаковой размерности). Для примера пусть это будет предложение I am stupid . В эмбеддинг добавляется еще позиция слова в предложении.
  2. Берется вектор первого слова и вектор второго слова ( I , am ), подаются на однослойную сеть с одним выходом, которая выдает степень их похожести (скалярная величина). Эта скалярная величина умножается на вектор второго слова, получая его некоторую «ослабленную» на величину похожести копию.
  3. Вместо второго слова подается третье слово и делается тоже самое что в п.2. с той же самой сетью с теми же весами (для векторов I , stupid ).
  4. Делая тоже самое для всех оставшихся слов предложения получаются их «ослабленные» (взвешенные) копии, которые выражают степень их похожести на первое слово. Далее эти все взвешенные вектора складываются друг с другом, получая один результирующий вектор размерности одного эмбединга:
    output=am * weight(I, am) + stupid * weight(I, stupid)
    Это механизм «обычного» attention.
  5. Так как оценка похожести слов всего одним способом (по одному критерию) считается недостаточной, тоже самое (п.2-4) повторяется несколько раз с другими весами. Типа одна один attention может определять похожесть слов по смысловой нагрузке, другой по грамматической, остальные еще как-то и т.п.
  6. На выходе п.5. получается несколько векторов, каждый из которых является взвешенной суммой всех остальных слов предложения относительно их похожести на первое слово ( I ). Конкантенируем этот вректор в один.
  7. Дальше ставится еще один слой линейного преобразования, уменьшающий размерность результата п.6. до размерности вектора одного эмбединга. Получается некое представление первого слова предложения, составленное из взвешенных векторов всех остальных слов предложения.

Такой же процесс производится для всех других слов в предложении.

В блоге у них про этот процесс визуализируется красивой гифкой — пока смотреть только на часть про encoding:

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

Переходим к декодеру


Декодер тоже запускается по слову за раз, получает на вход прошлое слово и должен выдать следующее (на первой итерации получает специальный токен <start> ).

В декодере есть два разных типа использования Multi-head attention:

  • Первый — это возможность обратиться к векторам прошлых декодированных слов, также как и было в процессе encoding (но можно обращаться не ко всем, а только к уже декодированным).
  • Второй — возможность обратиться к выходу энкодера. B этом случае Query — это вектор входа в декодере, а пары Key/Value — это финальные эмбеддинги энкодера, где опять же один и тот же вектор идет и как key, и как value (но линейные преобразования внутри attention module для них разные)
    В середине еще просто FC layer, опять те же residual connections и layer normalization.

И все это снова повторяется 6 раз, где выход предыдущего блока идет на вход следующему.

Наконец, в конце сети стоит обычный softmax, который выдает вероятности слов. Сэмплирование из него и есть результат, то есть следующее слово в предложении. Мы его даем на вход следующему запуску декодера и процесс повторяется, пока декодер не выдаст токен <end of sentence> .

Разумеется, это все end-to-end дифференцируемо, как обычно.
Теперь на гифку можно посмотреть целиком.

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

Результаты

И вот это добро прилично улучшает state of the art на machine translation.
image

2 пункта BLEU — это достаточно серьезно, тем более, что на этих значениях BLEU все хуже коррелирует с тем, насколько перевод нравится человеку.

В целом, основное нововведение — это использование механизма self-attention чтобы взаимодействовать с другими словами в предложении вместо механизмов RNN или CNN.
Они теоретизируют, что это помогает, потому что сеть может с одинаковой легкостью обратиться к любой информации вне зависимости от длины контекста — обратиться к прошлому слову или к слову за 10 шагов назад одинаково просто.
От этого и проще обучаться, и можно проводить вычисления параллельно, в отличие от RNN, где нужно каждый шаг делать по очереди.
Еще они попробовали ту же архитектуру для Constituency Parsing, то есть грамматического разбора, и тоже все получилось.

Я пока не видел подтверждения, что Transformer уже используется в продакшене Google Translate (хотя надо думать используется), а вот использование в Яндекс.Переводчике было упомянуто в интервью с Антоном Фроловым из Яндекса (на всякий случай, таймстемп 32:40).

Что сказать — молодцы и Гугл, и Яндекс! Очень клево, что появляются новые архитектуры, и что attention — уже не просто стандартная часть для улучшения RNN, а дает возможность по новому взглянуть на проблему. Так глядишь и до памяти как стандартного куска доберемся.

Трансформер: все, что вам нужно

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

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

Бумага

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

Код

Для реализации модели 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 применяются к выходным данным декодера для предсказания следующего слова.
  • Кодировщик запускается один раз. Декодер запускается несколько раз, чтобы на каждом шаге выдавать предсказываемое слово.
Ссылки
    . NeurIPS 2017 (стр. 5998–6008) ». Гарвардского НЛП. Блокнот iPython, который шаг за шагом просматривает весь документ с рабочим кодом Pytorch. Мэтью Барнетта. Обзор основных концепций Трансформера без лишних подробностей. Мэтью Барнетта. Подробно о том, как работает Трансформатор. Отличный пост для объяснения того, что на самом деле представляют собой запросы, ключи и значения. Джея Аламмара . Включает интересную графическую иллюстрацию декодера / кодировщика Ричарда Сочера от Tensorflow Core. Включает пример кода Tensorflow.
Об избранном изображении

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

11.7. The Transformer Architecture¶

We have compared CNNs, RNNs, and self-attention in Section 11.6.2 . Notably, self-attention enjoys both parallel computation and the shortest maximum path length. Therefore, it is appealing to design deep architectures by using self-attention. Unlike earlier self-attention models that still rely on RNNs for input representations (Cheng et al., 2016, Lin et al., 2017, Paulus et al., 2017) , the Transformer model is solely based on attention mechanisms without any convolutional or recurrent layer (Vaswani et al., 2017) . Though originally proposed for sequence-to-sequence learning on text data, Transformers have been pervasive in a wide range of modern deep learning applications, such as in areas to do with language, vision, speech, and reinforcement learning.

11.7.1. Model¶

As an instance of the encoder–decoder architecture, the overall architecture of the Transformer is presented in Fig. 11.7.1 . As we can see, the Transformer is composed of an encoder and a decoder. In contrast to Bahdanau attention for sequence-to-sequence learning in Fig. 11.4.2 , the input (source) and output (target) sequence embeddings are added with positional encoding before being fed into the encoder and the decoder that stack modules based on self-attention.

Fig. 11.7.1 The Transformer architecture. ¶

Now we provide an overview of the Transformer architecture in Fig. 11.7.1 . At a high level, the Transformer encoder is a stack of multiple identical layers, where each layer has two sublayers (either is denoted as \(\textrm\) ). The first is a multi-head self-attention pooling and the second is a positionwise feed-forward network. Specifically, in the encoder self-attention, queries, keys, and values are all from the outputs of the previous encoder layer. Inspired by the ResNet design of Section 8.6 , a residual connection is employed around both sublayers. In the Transformer, for any input \(\mathbf \in \mathbb^d\) at any position of the sequence, we require that \(\textrm(\mathbf) \in \mathbb^d\) so that the residual connection \(\mathbf + \textrm(\mathbf) \in \mathbb^d\) is feasible. This addition from the residual connection is immediately followed by layer normalization (Ba et al., 2016) . As a result, the Transformer encoder outputs a \(d\) -dimensional vector representation for each position of the input sequence.

The Transformer decoder is also a stack of multiple identical layers with residual connections and layer normalizations. As well as the two sublayers described in the encoder, the decoder inserts a third sublayer, known as the encoder–decoder attention, between these two. In the encoder–decoder attention, queries are from the outputs of the decoder’s self-attention sublayer, and the keys and values are from the Transformer encoder outputs. In the decoder self-attention, queries, keys, and values are all from the outputs of the previous decoder layer. However, each position in the decoder is allowed only to attend to all positions in the decoder up to that position. This masked attention preserves the autoregressive property, ensuring that the prediction only depends on those output tokens that have been generated.

We have already described and implemented multi-head attention based on scaled dot products in Section 11.5 and positional encoding in Section 11.6.3 . In the following, we will implement the rest of the Transformer model.

11.7.2. Positionwise Feed-Forward Networks¶

The positionwise feed-forward network transforms the representation at all the sequence positions using the same MLP. This is why we call it positionwise. In the implementation below, the input X with shape (batch size, number of time steps or sequence length in tokens, number of hidden units or feature dimension) will be transformed by a two-layer MLP into an output tensor of shape (batch size, number of time steps, ffn_num_outputs ).

Трансформер

Трансформер (англ. transformer) — архитектура глубоких нейронных сетей, основанная на механизме внимания без использования рекуррентных нейронных сетей (сокр. RNN). Самое большое преимущество трансформеров по сравнению с RNN заключается в их высокой эффективности в условиях параллелизации. Впервые модель трансформера была предложена в статье Attention is All You Need [1] от разработчиков Google в 2017 году.

Содержание

Архитектура трансформера

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

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

Ниже рассмотрим архитектуру кодировщика и декодировщика подробнее.

Архитектура трансформера-кодировщика

Рассмотрим последовательно шаг за шагом этапы работы кодировщика:

1. На вход поступает последовательность элементов [math]w_i[/math] , по ней создается последовательность эмбедингов, где каждый [math]x_i[/math] это векторное представление элемента [math]w_i[/math] .

2. Добавляются позиционные векторы [math]p_i[/math] : [math]h_i = x_i + p_i[/math] , [math]H = (h_1. h_n)[/math] . Это необходимо для того, чтобы отобразить информацию о позиции элемента в исходной последовательности. Основное свойство позиционного кодирования — чем дальше два вектора будут стоять друг от друга в последовательности, тем больше между ними будет расстояние. Более подробное устройство позиционного кодирования будет рассмотрено ниже.

3. Полученный вектор [math]h_i[/math] подается на вход в блок многомерного самовнимания (англ. multi-headed self-attention). [math]h^j_i = \mathrm(Q^j h_i, K^j H, V^j H)[/math] , где обучаемые матрицы: [math]Q[/math] для запроса, [math]K[/math] для ключа, [math]V[/math] для значения. Подробное объяснения работы механизма self-attention будет разобрано ниже.

4. Затем необходима конкатенация, чтобы вернуться в исходную размерность: [math] h’_i = M H_j (h^j_i) = [h^1_i. h^J_i] [/math]

5. Добавим сквозные связи (англ. skip connection) — по факту просто добавление из входного вектора к выходному ( [math]h’_i + h_i[/math] ). После делаем нормализацию слоя (англ. layer normalization): [math]h»_i = \mathrm(h’_i + h_i; \mu_1, \sigma_1)[/math] . У нее два обучаемых параметра, для каждой размерности вектора вычисляется среднее и дисперсия.

6. Теперь добавим преобразование, которое будет обучаемым — полносвязную двухслойную нейронную сеть: [math] h»’_i = W_2 \mathrm (W_1 h»_i + b_1) + b_2 [/math]

7. Повторим пункт 5 еще раз: добавим сквозную связь и нормализацию слоя: [math]z_i = \mathrm(h»’_i + h»_i; \mu_2, \sigma_2)[/math]

После, в кодирующем компоненте пункты кодировщика 3—7 повторяются еще несколько раз, преобразовывая друг за другом из контекста контекст. Тем самым мы обогащаем модель и увеличиваем в ней количество параметров.

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

Так как в архитектуре трансформер обработка последовательности заменяется на обработку множества мы теряем информацию о порядке элементов последовательности. Чтобы отобразить информацию о позиции элемента в исходной последовательности мы используем позиционное кодирование.

Позиционное кодирование (англ. positional encoding) — позволяет модели получить информацию о порядке элементов в последовательности путем прибавления специальных меток к вектору входных элементов. Позиции элементов [math]i[/math] кодируются векторами [math]p_i[/math] , [math]i = 1, 2, . n[/math] , так, что чем больше [math]|i — j|[/math] , тем больше [math]||p_i — p_j||[/math] , и [math]n[/math] не ограничено. Пример такого кодирования:

[math] p_ <(i, s)>= \begin \sin \left(i \cdot 10000^<\frac<-2k>>>\right) & \quad \text <если >s=2k\\ \cos \left(i \cdot 10000^<\frac<-2k>>>\right) & \quad \text <если >s=2k+1 \end [/math]

Self-attention

Self-Attention — разновидность механизма внимания, задачей которой является выявление закономерности между входными данными.

Будем для каждого элемента [math]x_i[/math] получать обучаемым преобразованием три вектора:

  • Запрос (query) [math]q_i = Q x_i[/math]
  • Ключ (key) [math]k_i = K x_i[/math]
  • Значение (value) [math]v_i = V x_i[/math]

Векторы [math]q_i[/math] и [math]k_i[/math] будем использовать, чтобы посчитать важность элемента [math]x_j[/math] для элемента [math]x_i[/math] . Чтобы понять, насколько для пересчета вектора элемента [math]x_i[/math] важен элемент [math]x_j[/math] мы берем [math]k_j[/math] (вектор ключа элемента [math]x_j[/math] ) и умножаем на [math]q_i[/math] (вектор запроса элемента [math]x_i[/math] ). Так мы скалярно перемножаем вектор запроса на все векторы ключей, тем самым понимаем, насколько каждый входной элемент нам нужен, чтобы пересчитать вектор элемента [math]x_i[/math] .

Далее считаем важность элемента [math]x_j[/math] для кодирования элемента [math]x_i[/math] : [math]w_=\frac< \exp \left(\frac<\langle q_i, k_j \rangle><\sqrt> \right) >< \sum_^n \exp \left(\frac<\langle q_i, k_p \rangle><\sqrt> \right) >[/math] , где [math]d[/math] — размерность векторов [math]q_i[/math] и [math]k_j[/math] , а [math]n[/math] — число элементов во входной последовательности.

Таким образом, новое представление элемента [math]x_i[/math] считаем как взвешенную сумму векторов значения: [math]z_i = \mathrm(Q x_i, K X, V X) = \sum_^n w_

v_p[/math] , где [math]X = (x_1, x_2, . x_n)[/math] — входные векторы. По факту self-attention — это soft-arg-max с температурой [math]\sqrt[/math] . Мы перемешиваем все входные векторы, чтобы получить новые векторы всех элементов, где каждый элемент зависит от всех входных элементов.

Multi-headed self-attention

Multi-headed self-attention — улучшенная модификация self-attention.

Слой внимания снабжается множеством «подпространств представлений» (англ. representation subspaces). Теперь у нас есть не один, а множество наборов матриц запроса/ключа/значения. Каждый из этих наборов создается случайным образом. Далее после обучения каждый набор используется для отображения входящих векторов в разных подпространствах представлений. Также появляется способность модели фокусироваться на разных аспектах входной информации.

То есть параллельно независимо несколько раз делаем attention. Потом результат каждого attention по элементам конкатенируем, затем сжимаем получившуюся матрицу и получаем для каждого элемента свой вектор той же размерности.

[math]с^j = \mathrm(Q^j q, K^j X, V^j X)[/math] , где [math]j = 1. J[/math] , [math]J[/math] — число разных моделей внимания, [math]X = (x_1, x_2, . x_n)[/math] — входные векторы, а [math]W[/math] — обучаемые матрицы.

Архитектура трансформера-декодировщика

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

1. Для того, чтобы распараллелить декодировщик и уйти от рекуррентности, но тем не менее генерировать элементы друг за другом, используется прием маскирования данных из будущего. Идея в том, что мы запрещаем себе подглядывать в те элементы, которые еще не сгенерированы с учетом порядка. Когда генерируем элемент под номером [math]t[/math] , имеем право смотреть только первые [math]t-1[/math] элементов: [math]h_t = y_ + p_t[/math] ; [math]H_t=(h_1, . h_t)[/math]

2. Далее идет этап многомерного самовнимания: линейная нормализация и multi-headed self-attention. Особенность в том, что в attention ключи и значения применяются не ко всем векторам, а только к тем, значения которых уже синтезировали ( [math]H_t[/math] ): [math] h’_t = \mathrm \circ M H_j \circ \mathrm(Q^j h_t, K^j H_t, V^j H_t) [/math] , где [math]\circ[/math] — композиция.

3. На следующем этапе мы делаем многомерное внимание на кодировку [math]Z[/math] , результат работы компонента кодировщика: [math] h»_t = \mathrm \circ M H_j \circ \mathrm(Q^j h_t, K^j Z, V^j Z) [/math]

4. Линейная полносвязная сеть (по аналогии с кодировщиком): [math] y_t = \mathrm \circ FNN(h»_t) [/math]

5. В самом конце мы хотим получить вероятностную порождающую модель для элементов. Результат (индекс слова с наибольшей вероятностью): [math]\mathrm(W_y y_t + b_y) [/math] , где [math] W_y [/math] , [math] b_y [/math] — обучаемые параметры линейного преобразования. Для каждой позиции [math]t[/math] выходной последовательности мы строим вероятностную модель языка, то есть все элементы из выходного словаря получают значение вероятности. Эти значения как раз получаются из векторов [math]y_t[/math] из предыдущего пункта, которые мы берем с последнего блока трансформера-декодировщика.

Последний этап выполняется только после того, когда повторились пункты 1—4 для всех декодировщиков. На выходе получаем вероятности классов, по факту для каждой позиции решаем задачу многоклассовой классификации, для того, чтобы понять какие элементы лучше поставить на каждые позиции.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *