Чем регулируется размер вектора выхода сети 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 им не нравился).

Говорят, BN в рекуррентных сетях не работает, так как статистика нормализации для разных предложений одного батча только портит всё, а не помогает, так как предложения все разной длины и все такое. В этой архитектуре тоже такой эффект ожидается и BN вреден?
Почему они не взяли BN — интересный вопрос, в статье особо не комментируется. Вроде бы были удачные попытки использовать с RNN например в speech recognition. Deep Speech 2 от Baidu (arxiv), AFAIR

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

  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. до размерности вектора одного эмбединга. Получается некое представление первого слова предложения, составленное из взвешенных векторов всех остальных слов предложения.
  8. Такой же процесс производится для всех других слов в предложении.
  9. Так как размерность выхода та же, то можно проделать все тоже самое еще раз (п.2-8), но вместо оригинальных эмбеддингов слов взять то, что получается после прохода через этот Multi-head attention, а нейросети аттеншенов внутри взять с другими весами (веса между слоями не общие). И таких слоев можно сделать много (у гугла 6). Однако между первым и вторым слоем добавляется еще полносвязный слой и residual соединения, чтобы добавить сети выразительности.

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

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

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

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

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

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

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

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

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

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

Результаты

image

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

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

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

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

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

ML: Трансформер

В этом документе мы продолжаем обсуждать механизм внимания. Многоголовое внимание было введено в статье «Attention is All You Need» (2017) для архитектуры трансформера (transformer). Это был вариант энкодер-декодера для задачи перевода, который не использовал рекуррентных слоёв. Вместо этого последовательности векторов слов пропускались через несколько слоёв с маскированным вниманием. В результате появилась возможность распараллеливания, что существенно ускорило обучение (по сравнению с рекуррентными сетями).

В дальнейшем различные части трансформера были использованы в таких моделях как GPT (2018) и BERT (2018), которым посвящён следующий документ.

Общая архитектура

С точки зрения структуры и методов обучения трансформер выглядит аналогично энкодер-декодеру на основе рекуррентных сетей. На вход энкодера подаётся последовательность слов на исходном языке. Векторы этих слов, пройдя через последовательность слоёв самовнимания, меняются с учётом контекста всего предложения. На рисунке ниже они обозначены как memory («память об исходном предложении»):

Декодер в режиме принудительного обучения (teacher forcing) на вход получает эту память и слова предложения-перевода. На выходе учится предсказывать этот же перевод сдвинутый влево на одно слово. В режиме тестирования (или «честного» обучения) на декодер сначала подают служебное слово » » (begin of sentence) и ожидают на выходе слово «кот«. Затем на вход подаётся « кот«, а на выходе получают «кот сидит» и т.д., пока декодер не выдаст » » (end of sentence).

Энкодер и декодер состоят из стопки однотипных блоков, которые осуществляют «глубокое» преобразование входных тензоров.

Энкодер Трансформера

Рассмотрим подробнее энкодер трансформера (рисунок справа). На его вход подаётся тензор формы (N,B,E), где N — число слов во входной последовательности, B — число одновременно обрабатываемых примеров (батч) и E — размерность их векторов эмбединга. Этот тензор пропускается через функцию многоголового само-внимания (Multi-Head Attention): три стрелки на рисунке — это совпадающие запросы, ключи и значения. Результат складывается с исходным тензором и нормируется (см. ниже). Получившийся тензор поступает в полносвязный слой (Feed Forward) c двумя линейными преобразованиями (после первого — активационная функция ReLU): $$ \text(\mathbf) = \max(0,~\mathbf\cdot\mathbf_1+\mathbf_1)\cdot\mathbf_2+\mathbf_2. $$ Выход этого слоя снова суммируется с его входом и нормируется. Подобные вычисления повторяются несколько раз (на рисунке множитель означает L таких слоёв-блоков с различными параметрами). На выходе последнего блока получается тензор исходной формы (N,B,E), который описывает слова последовательности с учётом контекста всего текста.

☝ Сложение входа и выхода слоя — это распространённая практика в глубоком обучении. Благодаря этому, градиент при обратном распространении легче добирается до на начала стопки слоёв. Действительно, в узле сложения происходит копирование градиента. Одна его версия проходит через слой и затухает на нелинейных функциях активации. Вторая — обходит слой без изменения и усиливает свою затухшую (и изменённую) копию.

☝ Нормализация борется с ситуацией, кода веса нейрона «загоняют» его выход в очень большие или очень маленькие значения, что замедляет процесс обучения. Для устранения этого эффекта, из выходов нейронов (до или после активационной функции) вычитается среднее значение и результат делят на стандартное отклонение (корень из дисперсии). Существует два метода нормализции нейронов скрытых слоёв: batch (2015) и layer (2016) normalization. В первом методе усреднения проиводятся по примерам батча, а во втором — по всем нейронам данного слоя. Оба метода ускоряют обучение, однако второй проще, т.к. одинаковым образом работает при обучении и тестировании и не зависит от размера батча. В Трансформере усреднение проводится во всем компонентам вектора эмбединга независимо для каждого входа (слова).

Энкодер в PyTorch

В PyTorch энкодер трансформера строится в два этапа. Сперва определяется TransformerEncoderLayer, а затем с его помощью создаётся собственно энкодер TransformerEncoder:

nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1,activation=’relu’)

Параметры: d_model = E – размерность входа (вектора одного токена), nhead = H – число голов (должно быть делителем параметра d_model), dim_feedforward – размерность полносвязной сети. Функция активации activation используется в полносвязном слое Feed Forward, а dropout задаёт долю элементов матрицы которые случайным образом делаются нулевыми (борьба с переобучением на этапе тренировки). Слой дропаут стоит сразу после функции softmax в nn.MultiheadAttention.

nn.TransformerEncoder(encoder_layer, num_layers, norm=None)

Параметр encoder_layer является экземпляром класса TransformerEncoderLayer, а num_layers = L задаёт число последовательных блоков, подобных тому, что приведен на рисунке выше.

Приведём пример создания энкодера трансформера:

N, B, E, H = 10, 32, 512, 8 # число слов, размер батча, размерность эмбединга, голов encoder_layer = nn.TransformerEncoderLayer(d_model=E, nhead=H) transformer_encoder = nn.TransformerEncoder (encoder_layer, num_layers=1) src = torch.rand(N, B, E) out = transformer_encoder(src) # out.shape == src.shape

Список параметров для одного слоя имеет вид (в именах опущен префикс layers.0.):

self_attn.in_proj_weight : 786432 (1536, 512) # (3*E, E) Wq, Wk, Wv self_attn.in_proj_bias : 1536 (1536,) # (3*E,) Bq, Bk, Bv self_attn.out_proj.weight : 262144 (512, 512) # (E,E) Wo self_attn.out_proj.bias : 512 (512,) # (E,) Bo linear1.weight :1048576 (2048, 512) # (dim_feedforward, E) W1 linear1.bias : 2048 (2048,) # (dim_feedforward,) B1 norm1.weight : 512 (512,) # (E,) norm1.bias : 512 (512,) # (E,) linear2.weight :1048576 (512, 2048) # (E, dim_feedforward) W2 linear2.bias : 512 (512,) # (E,) B2 norm2.weight : 512 (512,) # (E,) norm2.bias : 512 (512,) # (E,) total :3152384

PyTorch хранит матрицы линеных преобразований в транспонированном виде: $\text(\mathbf)=\mathbf\cdot \mathbf^T+\mathbf$. Поэтому в полносвязном слое происходят умножения: (*,E) @ (E, FF) @ (FF, E) = (*,E), где FF = dim_feedforward.
В Vaswani A., et al., (2017), как и выше, были использованы значения E = 512, FF = 2048, поэтому в модуле Feed Forward размерности векторов эмбединга сначала увеличиваются в 4 раза, а потом возвращаются к первоначальному значению.

Кодирование номеров слов

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

В исходной статье (2017) выбирались достаточно специфические периодические функции следующего вида: $$ \text(\text,~2i) = \sin(\text/10000^),~~~~~~~ \text(\text,~2i+1) = \cos(\text/10000^), $$ где pos — номер слова в предложении, а $i$ — номер компоненты вектора эмбединга. Получившиеся $E$-мерные векторы эмбединга складывались с векторами эмбединга слов.

В дальнейшем (GPT, BERT) использововались обучаемые векторы кодирования положения слов. Для этого, по-мимо эмбединга слов словаря, вводится отдельный (также $E$-мерный) эмбединг положения (для каждого положения pos слова в предложении свой вектор). Векторы слова и положения, как и выше, складываются, а затем поступают в энкодинг трансформера.

Декодер

Добавим теперь к энкодеру декодер, получив полную архитектуру Трансформера. На вход декодера подаются слова целевого предложения перевода. Эти слова векторизуются (с отличным от энкодера эмбедингом) и к ним добавляются векторы номера позиции слова (positional encoding).

Затем векторы проходят блок само-внимания (как в энкодере), для уточнения контекстного смысла векторов. В отличии от энкодера, это самовнимание с маской (Masked Multi-Head Attention), для того, чтобы декодер не заглядывал в «ответ» (подробнее см. ниже). Выход блока самовнимания суммируется с его входом и нормируется.

После этого включается механизм внимания на словах предложения исходного языка (после их обработки энкодером). При этом запросами являются слова декодера, а в качестве ключей и значений выступают векторы энкодера (см. буквы Q,K,V на картинке). Выход снова суммируется с входом и нормируется.

Завершает блок декодера полносвязная сеть (Feed Forward) из двух слоёв (как в энкодере). Таких последовательных блоков декодер имеет несколько (их число обычно совпадает с числом блоков энкодера). Естественно, параметры для обучения у блоков отличаются.

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

Трансформер в PyTorch

Трансформер можно собрать из энкодера и декодера (для которого есть свой класс nn.TransformerDecoder, аналогичный nn.TransformerEncoder). Впрочем, можно сразу воспользоваться классом nn.Transformer:

nn.Transformer
(d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=2048, dropout=0.1, activation=’relu’, custom_encoder=None, custom_decoder=None)

Смысл параметров понятен из их названий. Функция прямого распространения:

nn.Transformer.forward
(src, tgt, src_mask=None, tgt_mask=None, memory_mask=None, src_key_padding_mask=None, tgt_key_padding_mask=None, memory_key_padding_mask=None)

кроме исходного (src) и целевого (tgt) тензоров содержит маски, играющие важную роль в процессе обучения.

Маски

Для удобства работы с последовательностями переменной длинны (при формировании батчей), гиперпараметры $N$ и $M$ полагаются достаточно большими и более короткие предложения «добиваются» (padding) специальным токеном с выделенным индексом (обычно 0). Например, пусть $B, N=1, 10$ (один пример и максимум десять слов в исходном предложении). Тогда для примера из начала документа, последовательность, поступающая в энкодер, имеет вид:

The cat sits on the mat .

Так как слова необходимо игнорировать, в энкодер (и далее в функцию само-внимания) передаётся не только тензор $(N,B,E)$, но и логическая маска src_key_padding_mask: $(B,N)$, в которой значениями True отмечаются «забитые» слова (для каждого примера B). Например, для предложения про кота эта маска имеет вид:

torch.tensor([[False, False, False, False, False, False, False, True, True, True]])

Маска используется в функции само-внимания для исключения «забитых» слов. Технически это делается замещением элементов матрицы $\mathbf\cdot\mathbf: ~(N,M)$ большими отрицательными числами -inf в колонках ключей для слов . После прохождения через софтмакс соответствующие этим ключам веса будут равны нулю (см предыдущий документ).

  • tgt_key_padding_mask: $(B,M)$ — маска блокирования слов в целевом предложении (её построение полностью аналогично энкодеру).
  • memory_key_padding_mask: $(B,N)$ — маска блокирования -слов в блоке внимания на исходной последовательности. Обычно: memory_key_padding_mask = src_key_padding_mask.clone()

$$ \begin <|c|c|c|c|>\hline ~~0~~ & -\infty & -\infty & -\infty \\ \hline 0 & 0 & -\infty & -\infty \\ \hline 0 & 0 & 0 & -\infty \\ \hline 0 & 0 & 0 & 0 \\ \hline \end $$

def get_tgt_mask(size): m = torch.from_numpy(np.triu(np.ones( (size, size) ), k=1).astype('uint8')) m = m.float().masked_fill(m == 1, float('-inf')).masked_fill(m == 0, float(0.0)) return m

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

Литература

Статьи

  • 2017: Vaswani A., et al. «Attention is All You Need«
    — отказ от рекуррентных сетей, архитектура Transformer (Google Brain)

Исходники

  • The Annotated Encoder-Decoder with Attention
  • NLP From Scratch: Translation with a Sequence to Sequence Network and Attention
  • The Annotated Transformer — разбор кода трасформера.
  • Transformer [1/2]- Pytorch’s nn.Transformer — использование класса nn.Transformer в PyTorch для создания трансформера.
  • «Attention and Augmented Recurrent Neural Networks»
  • The Illustrated Transformer

Знакомство с трансформерами. Часть 1

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

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

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

Здесь можно найти видеолекции о трансформерах. А в этом репозитории имеется реализация простого трансформера с использованием PyTorch.

Механизм внутреннего внимания

Фундаментальная операция, выполняемая в реализации любой архитектуры трансформера, представлена механизмом внутреннего внимания (self-attention). О происхождении этого термина мы поговорим позже. Пока постарайтесь не вкладывать в это понятие какого-то особого смысла.

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

Для того чтобы получить выходной вектор механизм внутреннего внимания просто вычисляет среднее взвешенное значение по всем входным векторам:

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

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

Скалярные произведения дают нам некое значение, находящееся между отрицательной и положительной бесконечностью, поэтому мы, чтобы привести такие значения к интервалу [0, 1], применяем функцию Softmax, а так же обеспечиваем то, что их сумма по всей последовательности равна 1:

Именно так и работает механизм внутреннего внимания.

Базовая реализация механизма внутреннего внимания. Обратите внимание на то, что здесь не показано применение функции Softmax к весам

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

Почему работает механизм внутреннего внимания?

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

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

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

Скалярное произведение векторов, отражающих характеристики фильма и предпочтения пользователя

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

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

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

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

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

Первые два изученных признака из базовой модели матричной факторизации. У модели нет доступа к какой-либо информации о содержимом фильмов. Ей известно только то, каким пользователям эти фильмы нравятся. Обратите внимание на то, фильмы размещены, по горизонтали, от низкопробных до высокоинтеллектуальных. А по вертикали — от рассчитанных на массового потребителя до достаточно своеобразных.

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

Именно так работает базовый принцип механизма внутреннего внимания. Предположим, перед нами — последовательность слов. Для того чтобы обработать её с помощью механизма внутреннего внимания мы назначаем каждому слову в нашем словаре числовые векторы (эмбеддинги) (значения которых будет изучать модель). Это — то, что в моделировании последовательностей известно как эмбеддинг-слой. Последовательность слов, вроде the, cat, walks, on, the, street превращается в последовательность векторов .

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

Здесь — это сумма по всем эмбеддингам первой последовательности, взвешенная по их (нормализованным) скалярным произведениям с

Модель пытается, путём обучения, выяснить то, какими должны быть значения . В результате, то, насколько «связаны» слова, полностью определяется решаемой задачей. В большинстве случаев определённый артикль the не очень сильно связан с интерпретацией других слов в предложении. В результате мы получим эмбеддинг имеющий малое или отрицательное значение скалярного произведения с характеристиками всех остальных слов. С другой стороны — для того, чтобы интерпретировать значение слова walks в этом предложении, весьма полезно будет разобраться в том, кто именно гуляет. То, что нам нужно, вероятно, выражается именем существительным, поэтому для существительных, вроде cat, и для глаголов, вроде walks, модель, скорее всего, изучит эмбеддинги и скалярное произведение которых даст высокий положительный результат.

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

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

  • Тут (пока) нет параметров (позже мы добавим в модель несколько параметров). Истинный смысл работы простого механизма внутреннего внимания заключается в том, что он полностью определяется той системой, которая создаёт входную последовательность. Дальнейшие механизмы, вроде эмбеддинг-слоя, обеспечивают работу механизма внутреннего внимания, изучая представления с конкретными скалярными произведениями.
  • Механизм внутреннего внимания видит входные материалы в виде набора, а не последовательности данных. Если переставить элементы входной последовательности, выходная последовательность будет в точности той же самой, её элементы будут переставлены точно таким же образом (то есть — механизм внутреннего внимания эквивариантен к перестановкам). Мы несколько смягчим это, когда полностью реализуем трансформер, но механизм внутреннего внимания, сам по себе, на самом деле, игнорирует «последовательную» природу входных данных.

Реализация базового механизма внутреннего внимания в PyTorch

Ричард Фейнман сказал: «Чего не могу воссоздать, того не понимаю». Поэтому мы, двигаясь дальше, создадим простой трансформер. Начнём мы с реализации базового механизма внутреннего внимания в PyTorch.

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

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

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

import torch import torch.nn.functional as F # представим, что имеется тензор x размера (b, t, k) x = . raw_weights = torch.bmm(x, x.transpose(1, 2)) # - torch.bmm - это команда пакетного умножения матриц. Она # выполняет операции умножения над пакетами # матриц.

Затем, чтобы превратить необработанные веса в положительные значения, в сумме дающие единицу, мы, построчно, применяем функцию Softmax:

weights = F.softmax(raw_weights, dim=2)

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

y = torch.bmm(weights, x)

Это всё. Две операции умножения матриц и одно обращение к функции Softmax позволили нам построить базовый механизм внутреннего внимания.

Дополнительные приёмы

Настоящие механизмы внутреннего внимания, используемые в современных трансформерах, задействуют три дополнительных приёма.

1. Запросы, ключи и значения

В реализации механизма внутреннего внимания каждый входной вектор используется тремя способами:

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

Эти роли часто называют запрос (query), ключ (key) и значение (value) — о том, откуда взялись эти названия, мы поговорим позже. В базовой реализации механизма внутреннего внимания, о которой мы до сих пор говорили, каждый входной вектор должен играть все три эти роли. Мы немного упрощаем жизнь модели, получая новые векторы для каждой роли. Для этого мы применяем линейную трансформацию к исходному входному вектору. Другими словами, мы добавляем три матрицы весов размером — , и и производим три линейных трансформации для каждого xi, делая это для трёх разных частей механизма внутреннего внимания:

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

Механизм внутреннего внимания с трансформациями, соответствующими ключу (синий), запросу (красный) и значению (зелёный)

2. Масштабирование скалярного произведения векторов

Функция Softmax может испытывать сложности при обработке очень больших входных значений. Это сильно мешает алгоритму градиентного спуска, замедляет обучение модели, а может и совсем его остановить. Так как среднее значение скалярного произведения векторов растёт с ростом размерности k эмбеддинга, полезно масштабировать результаты скалярного произведения, немного уменьшая их, предотвращая тем самым чрезмерный рост входных данных функции Softmax:

Почему тут используется ? Представьте себе вектор в пространстве ℝ k , все значения которого равняются c. Его евклидова длина — . В результате получается, что мы осуществляем деление на значение, на которое увеличение размерности увеличивает длину средних векторов.

3. Множественный механизм внутреннего внимания

И наконец — мы должны учитывать тот факт, что слово может иметь разные значения по отношению к различным соседним словам. Рассмотрим пример: mary, gave, roses, to, susan (Мэри дала розы Сьюзен). Видно, что слово gave имеет разные взаимоотношения с разными частями предложения. Слово mary обозначает того, кто что-то кому-то даёт. Слово roses — это то, что дают. А слово susan указывает на того, кто что-то принимает.

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

Реализацию механизма внутреннего внимания можно оснастить мощной возможностью выявления различий во входных данных, скомбинировав несколько таких механизмов (индексировать их мы будем с помощью r), у каждого из которых будет собственная матрица:
. Это — блоки механизма внутреннего внимания (attention heads).

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

Эффективная реализация множественного механизма внутреннего внимания

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

Но оказывается, что мы вполне можем, так сказать, усидеть на двух стульях: есть способ реализации множественного механизма внутреннего внимания, работающего примерно с той же скоростью, что и такой же механизм, представленный единственным блоком. При этом в нашем распоряжении остаются сильные стороны наличия различных матриц механизма внутреннего внимания, работающих параллельно. Для того чтобы этого достичь, мы разбиваем каждый входной вектор на фрагменты. Например, если речь идёт о входном векторе размерности 256 и о 8 блоках внутреннего внимания, мы разбиваем вектор на 8 фрагментов размерности 32. Для каждого фрагмента мы генерируем матрицы ключей, значений и запросов, тоже имеющие размерность 32. Это значит, что мы получим матрицы размером .

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

Для того чтобы как следует разобраться с тем, как работает более эффективная «фрагментарная» версия механизма внутреннего внимания, обратитесь к лекциям, ссылки на которые даны в начале материала.

Реализация полной версии механизма внутреннего внимания в PyTorch

Теперь создадим модуль механизма внутреннего внимания, оснащённый всяческими интересными дополнениями. Мы упакуем его в модуль PyTorch, что позволит, когда будет нужно, использовать его в каком-нибудь другом проекте.

Комбинирование трёх блоков внутреннего внимания в одной операции умножения матриц (для запросов)

import torch from torch import nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, k, heads=8): super().__init__() self.k, self.heads = k, heads

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

 # Здесь вычисляются запросы, ключи и значения для всех # блоков (в виде единого конкатенированного вектора) self.tokeys = nn.Linear(k, k * heads, bias=False) self.toqueries = nn.Linear(k, k * heads, bias=False) self.tovalues = nn.Linear(k, k * heads, bias=False) # Здесь выходы различных блоков приводятся к # единому k-вектору self.unifyheads = nn.Linear(heads * k, k)

Теперь можно реализовать вычисления, имеющие отношение к механизму внутреннего внимания (функция модуля forward ). Сначала мы вычисляем запросы, ключи и значения:

 def forward(self, x): b, t, k = x.size() h = self.heads queries = self.toqueries(x).view(b, t, h, k) keys = self.tokeys(x) .view(b, t, h, k) values = self.tovalues(x) .view(b, t, h, k)

Выход каждого линейного модуля имеет размер (b, t, h*k) . Мы его перестраиваем, приводя к виду (b, t, h, k) , что позволяет дать каждому блоку собственное измерение.

Далее — нужно вычислить скалярное произведение. Речь идёт об одной и той же операции, выполняемой для каждого блока, поэтому мы преобразуем данные в пакет матриц. Это позволяет нам, как и прежде, воспользоваться функцией torch.bmm() , а весь набор ключей, запросов и значений будет представлен в виде немного более крупного пакета.

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

# - преобразование данных блоков в пакет матриц keys = keys.transpose(1, 2).contiguous().view(b * h, t, k) queries = queries.transpose(1, 2).contiguous().view(b * h, t, k) values = values.transpose(1, 2).contiguous().view(b * h, t, k)

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

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

queries = queries / (k ** (1/4)) keys = keys / (k ** (1/4)) # - получить скалярное произведение запросов и ключей, масштабировать данные dot = torch.bmm(queries, keys.transpose(1, 2)) # - размер произведения, содержащего необработанные веса - (b*h, t, t) dot = F.softmax(dot, dim=2) # - произведение теперь содержит веса, нормализованные построчно

Мы применяем механизм внутреннего внимания к значениям, что даёт нам выходные данные для каждого из блоков внутреннего внимания.

 # применить механизм внутреннего внимания к значениям out = torch.bmm(dot, values).view(b, h, t, k)

Для унификации блоков внутреннего внимания мы снова выполняем транспонирование, в результате размерности блоков и эмбеддинга соответствуют друг другу, и перестраиваем данные для получения конкатенированных векторов размерности kh. Затем мы пропускаем это всё через слой unifyheads для того чтобы снова свести их к размерности k.

 # вернуть h, t, унифицировать блоки внутреннего внимания out = out.transpose(1, 2).contiguous().view(b, t, h * k) return self.unifyheads(out)

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

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

О, а приходите к нам работать? ��

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

ML: Трансформер

В этом документе мы продолжаем обсуждать механизм внимания. Многоголовое внимание было введено в статье «Attention is All You Need» (2017) для архитектуры трансформера (transformer). Это был вариант энкодер-декодера для задачи перевода, который не использовал рекуррентных слоёв. Вместо этого последовательности векторов слов пропускались через несколько слоёв с маскированным вниманием. В результате появилась возможность распараллеливания, что существенно ускорило обучение (по сравнению с рекуррентными сетями).

В дальнейшем различные части трансформера были использованы в таких моделях как GPT (2018) и BERT (2018), которым посвящён следующий документ.

Общая архитектура

С точки зрения структуры и методов обучения трансформер выглядит аналогично энкодер-декодеру на основе рекуррентных сетей. На вход энкодера подаётся последовательность слов на исходном языке. Векторы этих слов, пройдя через последовательность слоёв самовнимания, меняются с учётом контекста всего предложения. На рисунке ниже они обозначены как memory («память об исходном предложении»):

Декодер в режиме принудительного обучения (teacher forcing) на вход получает эту память и слова предложения-перевода. На выходе учится предсказывать этот же перевод сдвинутый влево на одно слово. В режиме тестирования (или «честного» обучения) на декодер сначала подают служебное слово » » (begin of sentence) и ожидают на выходе слово «кот«. Затем на вход подаётся « кот«, а на выходе получают «кот сидит» и т.д., пока декодер не выдаст » » (end of sentence).

Энкодер и декодер состоят из стопки однотипных блоков, которые осуществляют «глубокое» преобразование входных тензоров.

Энкодер Трансформера

Рассмотрим подробнее энкодер трансформера (рисунок справа). На его вход подаётся тензор формы (N,B,E), где N — число слов во входной последовательности, B — число одновременно обрабатываемых примеров (батч) и E — размерность их векторов эмбединга. Этот тензор пропускается через функцию многоголового само-внимания (Multi-Head Attention): три стрелки на рисунке — это совпадающие запросы, ключи и значения. Результат складывается с исходным тензором и нормируется (см. ниже). Получившийся тензор поступает в полносвязный слой (Feed Forward) c двумя линейными преобразованиями (после первого — активационная функция ReLU): $$ \text(\mathbf) = \max(0,~\mathbf\cdot\mathbf_1+\mathbf_1)\cdot\mathbf_2+\mathbf_2. $$ Выход этого слоя снова суммируется с его входом и нормируется. Подобные вычисления повторяются несколько раз (на рисунке множитель означает L таких слоёв-блоков с различными параметрами). На выходе последнего блока получается тензор исходной формы (N,B,E), который описывает слова последовательности с учётом контекста всего текста.

☝ Сложение входа и выхода слоя — это распространённая практика в глубоком обучении. Благодаря этому, градиент при обратном распространении легче добирается до на начала стопки слоёв. Действительно, в узле сложения происходит копирование градиента. Одна его версия проходит через слой и затухает на нелинейных функциях активации. Вторая — обходит слой без изменения и усиливает свою затухшую (и изменённую) копию.

☝ Нормализация борется с ситуацией, кода веса нейрона «загоняют» его выход в очень большие или очень маленькие значения, что замедляет процесс обучения. Для устранения этого эффекта, из выходов нейронов (до или после активационной функции) вычитается среднее значение и результат делят на стандартное отклонение (корень из дисперсии). Существует два метода нормализции нейронов скрытых слоёв: batch (2015) и layer (2016) normalization. В первом методе усреднения проиводятся по примерам батча, а во втором — по всем нейронам данного слоя. Оба метода ускоряют обучение, однако второй проще, т.к. одинаковым образом работает при обучении и тестировании и не зависит от размера батча. В Трансформере усреднение проводится во всем компонентам вектора эмбединга независимо для каждого входа (слова).

Энкодер в PyTorch

В PyTorch энкодер трансформера строится в два этапа. Сперва определяется TransformerEncoderLayer, а затем с его помощью создаётся собственно энкодер TransformerEncoder:

nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1,activation=’relu’)

Параметры: d_model = E – размерность входа (вектора одного токена), nhead = H – число голов (должно быть делителем параметра d_model), dim_feedforward – размерность полносвязной сети. Функция активации activation используется в полносвязном слое Feed Forward, а dropout задаёт долю элементов матрицы которые случайным образом делаются нулевыми (борьба с переобучением на этапе тренировки). Слой дропаут стоит сразу после функции softmax в nn.MultiheadAttention.

nn.TransformerEncoder(encoder_layer, num_layers, norm=None)

Параметр encoder_layer является экземпляром класса TransformerEncoderLayer, а num_layers = L задаёт число последовательных блоков, подобных тому, что приведен на рисунке выше.

Приведём пример создания энкодера трансформера:

N, B, E, H = 10, 32, 512, 8 # число слов, размер батча, размерность эмбединга, голов encoder_layer = nn.TransformerEncoderLayer(d_model=E, nhead=H) transformer_encoder = nn.TransformerEncoder (encoder_layer, num_layers=1) src = torch.rand(N, B, E) out = transformer_encoder(src) # out.shape == src.shape

Список параметров для одного слоя имеет вид (в именах опущен префикс layers.0.):

self_attn.in_proj_weight : 786432 (1536, 512) # (3*E, E) Wq, Wk, Wv self_attn.in_proj_bias : 1536 (1536,) # (3*E,) Bq, Bk, Bv self_attn.out_proj.weight : 262144 (512, 512) # (E,E) Wo self_attn.out_proj.bias : 512 (512,) # (E,) Bo linear1.weight :1048576 (2048, 512) # (dim_feedforward, E) W1 linear1.bias : 2048 (2048,) # (dim_feedforward,) B1 norm1.weight : 512 (512,) # (E,) norm1.bias : 512 (512,) # (E,) linear2.weight :1048576 (512, 2048) # (E, dim_feedforward) W2 linear2.bias : 512 (512,) # (E,) B2 norm2.weight : 512 (512,) # (E,) norm2.bias : 512 (512,) # (E,) total :3152384

PyTorch хранит матрицы линеных преобразований в транспонированном виде: $\text(\mathbf)=\mathbf\cdot \mathbf^T+\mathbf$. Поэтому в полносвязном слое происходят умножения: (*,E) @ (E, FF) @ (FF, E) = (*,E), где FF = dim_feedforward.
В Vaswani A., et al., (2017), как и выше, были использованы значения E = 512, FF = 2048, поэтому в модуле Feed Forward размерности векторов эмбединга сначала увеличиваются в 4 раза, а потом возвращаются к первоначальному значению.

Кодирование номеров слов

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

В исходной статье (2017) выбирались достаточно специфические периодические функции следующего вида: $$ \text(\text,~2i) = \sin(\text/10000^),~~~~~~~ \text(\text,~2i+1) = \cos(\text/10000^), $$ где pos — номер слова в предложении, а $i$ — номер компоненты вектора эмбединга. Получившиеся $E$-мерные векторы эмбединга складывались с векторами эмбединга слов.

В дальнейшем (GPT, BERT) использововались обучаемые векторы кодирования положения слов. Для этого, по-мимо эмбединга слов словаря, вводится отдельный (также $E$-мерный) эмбединг положения (для каждого положения pos слова в предложении свой вектор). Векторы слова и положения, как и выше, складываются, а затем поступают в энкодинг трансформера.

Декодер

Добавим теперь к энкодеру декодер, получив полную архитектуру Трансформера. На вход декодера подаются слова целевого предложения перевода. Эти слова векторизуются (с отличным от энкодера эмбедингом) и к ним добавляются векторы номера позиции слова (positional encoding).

Затем векторы проходят блок само-внимания (как в энкодере), для уточнения контекстного смысла векторов. В отличии от энкодера, это самовнимание с маской (Masked Multi-Head Attention), для того, чтобы декодер не заглядывал в «ответ» (подробнее см. ниже). Выход блока самовнимания суммируется с его входом и нормируется.

После этого включается механизм внимания на словах предложения исходного языка (после их обработки энкодером). При этом запросами являются слова декодера, а в качестве ключей и значений выступают векторы энкодера (см. буквы Q,K,V на картинке). Выход снова суммируется с входом и нормируется.

Завершает блок декодера полносвязная сеть (Feed Forward) из двух слоёв (как в энкодере). Таких последовательных блоков декодер имеет несколько (их число обычно совпадает с числом блоков энкодера). Естественно, параметры для обучения у блоков отличаются.

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

Трансформер в PyTorch

Трансформер можно собрать из энкодера и декодера (для которого есть свой класс nn.TransformerDecoder, аналогичный nn.TransformerEncoder). Впрочем, можно сразу воспользоваться классом nn.Transformer:

nn.Transformer
(d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=2048, dropout=0.1, activation=’relu’, custom_encoder=None, custom_decoder=None)

Смысл параметров понятен из их названий. Функция прямого распространения:

nn.Transformer.forward
(src, tgt, src_mask=None, tgt_mask=None, memory_mask=None, src_key_padding_mask=None, tgt_key_padding_mask=None, memory_key_padding_mask=None)

кроме исходного (src) и целевого (tgt) тензоров содержит маски, играющие важную роль в процессе обучения.

Маски

Для удобства работы с последовательностями переменной длинны (при формировании батчей), гиперпараметры $N$ и $M$ полагаются достаточно большими и более короткие предложения «добиваются» (padding) специальным токеном с выделенным индексом (обычно 0). Например, пусть $B, N=1, 10$ (один пример и максимум десять слов в исходном предложении). Тогда для примера из начала документа, последовательность, поступающая в энкодер, имеет вид:

The cat sits on the mat .

Так как слова необходимо игнорировать, в энкодер (и далее в функцию само-внимания) передаётся не только тензор $(N,B,E)$, но и логическая маска src_key_padding_mask: $(B,N)$, в которой значениями True отмечаются «забитые» слова (для каждого примера B). Например, для предложения про кота эта маска имеет вид:

torch.tensor([[False, False, False, False, False, False, False, True, True, True]])

Маска используется в функции само-внимания для исключения «забитых» слов. Технически это делается замещением элементов матрицы $\mathbf\cdot\mathbf: ~(N,M)$ большими отрицательными числами -inf в колонках ключей для слов . После прохождения через софтмакс соответствующие этим ключам веса будут равны нулю (см предыдущий документ).

  • tgt_key_padding_mask: $(B,M)$ — маска блокирования слов в целевом предложении (её построение полностью аналогично энкодеру).
  • memory_key_padding_mask: $(B,N)$ — маска блокирования -слов в блоке внимания на исходной последовательности. Обычно: memory_key_padding_mask = src_key_padding_mask.clone()

$$ \begin <|c|c|c|c|>\hline ~~0~~ & -\infty & -\infty & -\infty \\ \hline 0 & 0 & -\infty & -\infty \\ \hline 0 & 0 & 0 & -\infty \\ \hline 0 & 0 & 0 & 0 \\ \hline \end $$

def get_tgt_mask(size): m = torch.from_numpy(np.triu(np.ones( (size, size) ), k=1).astype('uint8')) m = m.float().masked_fill(m == 1, float('-inf')).masked_fill(m == 0, float(0.0)) return m

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

Литература

Статьи

  • 2017: Vaswani A., et al. «Attention is All You Need«
    — отказ от рекуррентных сетей, архитектура Transformer (Google Brain)

Исходники

  • The Annotated Encoder-Decoder with Attention
  • NLP From Scratch: Translation with a Sequence to Sequence Network and Attention
  • The Annotated Transformer — разбор кода трасформера.
  • Transformer [1/2]- Pytorch’s nn.Transformer — использование класса nn.Transformer в PyTorch для создания трансформера.
  • «Attention and Augmented Recurrent Neural Networks»
  • The Illustrated Transformer

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

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