Реализация PyTorch моделей Transformer

искусственный интеллект PyTorch

Эта статья написанаЛо ЧжоуянОригинал, при перепечатке указывать автора и источник. Несанкционированное коммерческое использование запрещено.

Документ Google за 2017 годAttention is all you needОбъясните, что называется путем к простоте! В документе представленыTransformerмодель, полностью основанная наAttention mechanism, отказываясь от традиционногоRNNиCNN.

В соответствии со структурной схемой статьи, мы используем ее шаг за шагомPyTorchреализовать этоTransformerМодель.

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

Сначала посмотрите на структурную схему трансформатора:

transformer_architecture

Объясните эту структурную схему. Во-первых,TransformerМодель также использует классическийencoer-decoderАрхитектура состоит из двух частей: кодера и декодера.

В левой половине рисунка выше используетсяNxФрейм — это слой нашего энкодера. Кодировщик имеет всего 6 слоев такой структуры.

В правой половине рисунка выше используетсяNxФрейм — это слой нашего декодера. Декодер имеет в общей сложности 6 слоев этой структуры.

Входная последовательность проходит черезword embeddingиpositional encodingПосле добавления введите кодировщик.

Выходная последовательность проходит черезword embeddingиpositional encodingПосле добавления введите в декодер.

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

word embeddingиpositional encodingЯ объясню позже. Давайте сначала подробно разберем, как устроен каждый слой кодера и декодера.

Encoder

Кодер состоит из 6 одинаковых слоев, и каждый слой состоит из двух частей:

  • Первая часть представляет собойmulti-head self-attention mechanism
  • Вторая часть представляет собойposition-wise feed-forward network, является полносвязным слоем

Две части, обе по одной Остаточное соединение, затем следуетLayer Normalization.

Если вы новичок, вы можете спросить:

  • Что такое многоголовое самовнимание?
  • Что такое ступенчатая структура?
  • Что такое нормализация слоя?

Мы ответим на эти вопросы один за другим позже.

Decoder

Как и энкодер, декодер состоит из 6 одинаковых слоев, и каждый слой включает в себя следующие 3 части:

  • Первая частьmulti-head self-attention mechanism
  • Вторая частьmulti-head context-attention mechanism
  • Третья часть представляет собойposition-wise feed-forward network

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

Однако в декодере появилась обновкаmulti-head context-attention mechanism. Это не так сложно, я понимаюmulti-head self-attentionты можешь понятьmulti-head context-attention. Мы объясним это позже.

Механизм внимания

Прежде чем прояснить различные виды внимания, мы должны сначала прояснить механизм внимания.

С точки зрения непрофессионала,attentionзначит, для выхода в определенное времяy, он входитxвнимание вышеуказанных разделов. Это внимание на самом деле может быть понято какВеса.

Механизмы внимания также можно разделить на множество типов.Attention? Attention!Есть относительно полная таблица:

attention_mechanism
Figure 2. a summary table of several popular attention mechanisms.

первый вышеadditive attentionВозможно, вы слышали. Раньше в нашей модели seq2seq использовался механизм внимания, и этот вид **аддитивного внимания** использовался часто. проект Googletensorflow/nmtВнимание, используемое в нем, таково.

Почему это внимание называетсяadditive attentionШерстяная ткань? Очень просто, для скрытого состояния входной последовательностиh_iи скрытое состояние выходной последовательностиs_t, его обработка очень проста, прямаясливаться,стать[s_t;h_i]

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

тогда этоМультипликативный механизм вниманияНа что это похоже? Это также видно из формулы в таблице выше:Скалярный продукт двух скрытых состояний!

Что такое Самовнимание?

Здесь вы можете объяснить, что такоеself-attention.

Когда мы говорим о механизме внимания выше, мы будем говорить о двух скрытых состояниях, а именноh_iиs_t, первое — это скрытое состояние, сгенерированное в i-й позиции входной последовательности, а второе — скрытое состояние, сгенерированное в t-й позиции выходной последовательности.

так называемыйself-attentionПо факту,выходная последовательностьэтовходная последовательность! Таким образом, подсчет вашего собственного показателя внимания называетсяself-attention!

Что такое контекстное внимание?

понялself-attention, то вы, должно быть, догадалисьcontext-attentionКак дела:Это внимание между кодировщиком и декодером! Таким образом, вы также можете назвать этоencoder-decoder attention!

context-attentionТермин не является авторским, некоторые статьи или кодексы будут описывать его таким образом, я думаю, что это достаточно ярко, поэтому я буду продолжать использовать это имя здесь. Другие статьи могут иметь другие названия, но это не имеет значения, мы поняли суть, а именновнимание между двумя разными последовательностямиself-attentionразные.

не важно какself-attentionвсе ещеcontext-attention, когда они вычисляют показатель внимания, они могут выбрать множество способов, таких как упомянутые в таблице выше:

  • additive attention
  • local-base
  • general
  • dot-product
  • scaled dot-product

Так что же представляет собой наша модель Трансформера? ответ:scaled dot-product attention.

Что такое масштабированное скалярное произведение внимания?

бумагаAttention is all you needОписание механизма внимания внутри выглядит следующим образом:

An attention function can be described as a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility of the query with the corresponding key.

Это предложение описывает это очень ясно. Это переводится как:V выбирается путем определения степени подобия между Q и K!

Нагляднее описать его формулой:

\text{Attention}(Q,K,V)=softmax(\frac{QK^T}{\sqrt d_k})V

scaled dot-product attentionиdot-product attentionЕдинственная разница в том,scaled dot-product attentionимеет масштабный коэффициент\frac{1}{\sqrt d_k}.

в приведенной выше формулеd_kПредставляет размерность K. В документе значение по умолчанию равно64.

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

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

Зачем\frac{1}{\sqrt d_k}Шерстяная ткань? Документ не распространяется дальше. Лично я думаю, что вы можете использовать другие коэффициенты масштабирования, чтобы увидеть, улучшится ли эффект модели.

В документе также представлена ​​очень четкая структурная схема для справки:

scaled_dot_product_attention_arch
Figure 3. Scaled dot-product attention architecture.

Сначала объясните, что такое наши K, Q, V:

  • С точки зрения кодировщика, все Q, K и V происходят из одного и того же места (равны), и они являются выходом предыдущего кодировщика. Для первого уровня кодировщика они являются входными данными, полученными путем добавления встраивания слов и позиционного кодирования.
  • По мнению декодера, все Q, K и V происходят из одного и того же места (равны), и они являются выходными данными предыдущего уровня декодера. Для первого уровня декодера они являются входными данными, полученными путем добавления встраивания слов и позиционного кодирования. Но для декодера мы не хотим, чтобы он получал следующий временной шаг (приходящая информация), поэтому нам нужно сделатьsequence masking.
  • В кодировщике-декодере Q поступает с выхода предыдущего слоя декодера, K и V приходят с выхода кодера, а K и V совпадают.
  • Размеры Q, K и V одинаковы, т. е.d_q=d_k=d_v.

Выше показаны масштабированное скалярное произведение внимания и собственное внимание декодера.maskingтакая вещь. Так что же это за маска? Являются ли две операции с маской одинаковыми? Этот вопрос будет подробно объяснен позже.

Внедрение масштабируемого точечного произведения внимания

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

import torch
import torch.nn as nn


class ScaledDotProductAttention(nn.Module):
    """Scaled dot-product attention mechanism."""

    def __init__(self, attention_dropout=0.0):
        super(ScaledDotProductAttention, self).__init__()
        self.dropout = nn.Dropout(attention_dropout)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, q, k, v, scale=None, attn_mask=None):
        """前向传播.

        Args:
        	q: Queries张量,形状为[B, L_q, D_q]
        	k: Keys张量,形状为[B, L_k, D_k]
        	v: Values张量,形状为[B, L_v, D_v],一般来说就是k
        	scale: 缩放因子,一个浮点标量
        	attn_mask: Masking张量,形状为[B, L_q, L_k]

        Returns:
        	上下文张量和attetention张量
        """
        attention = torch.bmm(q, k.transpose(1, 2))
        if scale:
        	attention = attention * scale
        if attn_mask:
        	# 给需要mask的地方设置一个负无穷
        	attention = attention.masked_fill_(attn_mask, -np.inf)
		# 计算softmax
        attention = self.softmax(attention)
		# 添加dropout
        attention = self.dropout(attention)
		# 和V做点积
        context = torch.bmm(attention, v)
        return context, attention

Что такое многоголовое внимание?

После понимания масштабируемого скалярного произведения внимание с несколькими головками также очень просто. В документе упоминалось, что они обнаружили, что после прохождения Q, K и V через линейное отображение они были разделены наhпорция, за каждую порциюscaled dot-product attentionлучший результат. Затем объедините результаты каждой части и снова выполните линейное отображение, чтобы получить окончательный результат. Это называетсяmulti-head attention. Гиперпараметры вышеhэтоheadsколичество. Бумага по умолчанию8.

Ниже представлена ​​структурная схема многоголового внимания:

multi-head attention_architecture
Figure 4: Multi-head attention architecture.

Стоит отметить, что упомянутое вышеразделен наhПоделитьсявd_k、d_q、d_vРазделенный над измерением. Следовательно, в масштабированное скалярное произведение вниманияd_kНа самом деле он равен тому, что был до входаD_K/h.

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

Формула многоголового внимания выглядит следующим образом:

\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_ 1,\dots,\text{head}_ h)W^O

в,

\text{head}_ i = \text{Attention}(QW_i^Q,KW_i^K,VW_i^V)

В газетеd_{model}=512,h=8. Таким образом, в масштабированном скалярном произведении внимания

d_q = d_k = d_v = d_{model}/h = 512/8 = 64

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

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

import torch
import torch.nn as nn


class MultiHeadAttention(nn.Module):

    def __init__(self, model_dim=512, num_heads=8, dropout=0.0):
        super(MultiHeadAttention, self).__init__()

        self.dim_per_head = model_dim // num_heads
        self.num_heads = num_heads
        self.linear_k = nn.Linear(model_dim, self.dim_per_head * num_heads)
        self.linear_v = nn.Linear(model_dim, self.dim_per_head * num_heads)
        self.linear_q = nn.Linear(model_dim, self.dim_per_head * num_heads)

        self.dot_product_attention = ScaledDotProductAttention(dropout)
        self.linear_final = nn.Linear(model_dim, model_dim)
        self.dropout = nn.Dropout(dropout)
		# multi-head attention之后需要做layer norm
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, key, value, query, attn_mask=None):
		# 残差连接
        residual = query

        dim_per_head = self.dim_per_head
        num_heads = self.num_heads
        batch_size = key.size(0)

        # linear projection
        key = self.linear_k(key)
        value = self.linear_v(value)
        query = self.linear_q(query)

        # split by heads
        key = key.view(batch_size * num_heads, -1, dim_per_head)
        value = value.view(batch_size * num_heads, -1, dim_per_head)
        query = query.view(batch_size * num_heads, -1, dim_per_head)

        if attn_mask:
            attn_mask = attn_mask.repeat(num_heads, 1, 1)
        # scaled dot product attention
        scale = (key.size(-1) // num_heads) ** -0.5
        context, attention = self.dot_product_attention(
          query, key, value, scale, attn_mask)

        # concat heads
        context = context.view(batch_size, -1, dim_per_head * num_heads)

        # final linear projection
        output = self.linear_final(context)

        # dropout
        output = self.dropout(output)

        # add residual and norm layer
        output = self.layer_norm(residual + output)

        return output, attention

Приведенный выше код наконец появилсяResidual connectionиLayer normalization. Давайте объясним их сейчас.

Что такое остаточное соединение?

Остаточные соединения на самом деле довольно просты! Давайте я покажу вам схему, и вы поймете:

residual_conn
Figure 5. Residual connection.

Предположим, что слой в сети имеет входxВывод после действияF(x), затем увеличитьresidual connectionПосле этого становится:

F(x)+x

это+xоперация - этоshortcut.

Такостаточная структураКаковы преимущества? Очевидно: потому что дополнительныйx, то когда этот слой сети ищет частную производную от x, появляется дополнительный постоянный член1! Поэтому в процессе обратного распространения даже умножение градиентов не вызоветГрадиент исчезает!

Поэтому код для реализации остаточного соединения очень прост:

def residual(sublayer_fn,x):
	return sublayer_fn(x)+x

На схеме архитектуры трансформатора в начале статьиAdd & NormсерединаAddя имею в виду этоshortcut.

Уже,residual connectionпроблема ясна. Дополнительные сведения об остаточных сетях см. в ссылках в конце статьи.

Что такое нормализация слоя?

GRADIENTS, BATCH NORMALIZATION AND LAYER NORMALIZATIONВ статье есть хорошее объяснение нормализации:

Существует много видов нормализации, но все они имеют общую цель — преобразовать входные данные в данные со средним значением 0 и дисперсией 1. Мы нормализуем данные перед тем, как передать их в функцию активации, потому что мы не хотим, чтобы входные данные попадали в область насыщения функции активации.

Говоря о нормализации, следует упомянутьBatch Normalization. BN часто используется в таких местах, как CNN.

Основная идея BN — выполнять нормализацию каждого пакета данных в каждом слое.

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

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

batch_normalization
Figure 6. Batch normalization example.(From theneuralperspective.com)

Видно, что среднее значение правой половины равновдоль направления пакета данных N!

Формула расчета пакетной нормализации выглядит следующим образом:

BN(x_i)=\alpha\times\frac{x_i-u_B}{\sqrt{\sigma_B^2+\epsilon}}+\beta

Конкретную реализацию можно найти в связанной статье выше.

После разговора о пакетной нормализации пришло время поговорить о нашем сегодняшнем герое.Layer normalization.

Так что же такое нормализация слоя? : это также способ нормализации данных, но LNРассчитывайте среднее значение и дисперсию для каждой выборки вместо вычисления среднего значения и дисперсии в направлении партии, как это делает BN.!

Ниже приведена принципиальная схема LN:

layer_normalization
Figure 7. Layer normalization example.

Вы можете увидеть разницу между ними, сравнив ее с диаграммой BN выше!

Давайте посмотрим на формулу LN, которая очень похожа на BN:

LN(x_i)=\alpha\times\frac{x_i-u_L}{\sqrt{\sigma_L^2+\epsilon}}+\beta

Реализация нормализации слоя

Два вышеуказанных параметра\alphaи\betaвсе изучаемые параметры. Давайте сами реализуем нормализацию слоя (в PyTorch это уже реализовано!). код показывает, как показано ниже:

import torch
import torch.nn as nn


class LayerNorm(nn.Module):
    """实现LayerNorm。其实PyTorch已经实现啦,见nn.LayerNorm。"""

    def __init__(self, features, epsilon=1e-6):
        """Init.

        Args:
            features: 就是模型的维度。论文默认512
            epsilon: 一个很小的数,防止数值计算的除0错误
        """
        super(LayerNorm, self).__init__()
        # alpha
        self.gamma = nn.Parameter(torch.ones(features))
        # beta
        self.beta = nn.Parameter(torch.zeros(features))
        self.epsilon = epsilon

    def forward(self, x):
        """前向传播.

        Args:
            x: 输入序列张量,形状为[B, L, D]
        """
        # 根据公式进行归一化
        # 在X的最后一个维度求均值,最后一个维度就是模型的维度
        mean = x.mean(-1, keepdim=True)
        # 在X的最后一个维度求方差,最后一个维度就是模型的维度
        std = x.std(-1, keepdim=True)
        return self.gamma * (x - mean) / (std + self.epsilon) + self.beta

Кстати,Layer normalizationОн в основном используется в структуре RNN.

Что такое Маска?

Теперь пришло время объяснить маску! Маска, как следует из названия,маска, что мы имеем в виду здесь, вероятно,замаскировать некоторые значения, чтобы они не имели никакого эффекта.

Следует отметить, что в нашей модели Трансформера задействованы два типа масок. соответственноpadding maskиsequence mask. Последнее мы уже видели в самостоятельном внимании декодера!

в,padding maskЕго необходимо использовать во всех масштабируемых скалярных произведениях внимания, иsequence maskОн используется только при самостоятельном внимании декодера.

Итак, прежде чем мыScaledDotProductAttentionизforwardпараметры в методеattn_maskВ разных местах оно имеет разное значение. Мы объясним это позже.

Padding mask

чтоpadding maskШерстяная ткань? Напомним, что длина нашей входной последовательности не одинакова для каждой партии! То есть мы хотим выполнитьвыровнять! В частности, заполнение после более короткой последовательности0. Поскольку эти заполненные позиции на самом деле бессмысленны, наш механизм вниманияне следует заострять внимание на этих позициях, поэтому нам нужно выполнить некоторую обработку.

Конкретно,Добавьте к значению этих позиций очень большое отрицательное число (может быть и минус бесконечность), чтобы после softmax вероятность этих позиций была близка к 0!

И наша маска заполнения на самом деле является тензором, каждое значение представляет собойBoolen, значениеFalseМесто, где мы собираемся делать обработку.

Вот реализация:


def padding_mask(seq_k, seq_q):
	# seq_k和seq_q的形状都是[B,L]
    len_q = seq_q.size(1)
    # `PAD` is 0
    pad_mask = seq_k.eq(0)
    pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1)  # shape [B, L_q, L_k]
    return pad_mask

Sequence mask

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

Так как это сделать конкретно? Тоже очень просто:Создайте верхнюю треугольную матрицу, значение верхнего треугольника равно 1, значение нижнего треугольника равно 0, а диагональ также равна 0.. Применяя эту матрицу к каждой последовательности, мы можем достичь нашей цели.

Конкретная реализация кода выглядит следующим образом:

def sequence_mask(seq):
    batch_size, seq_len = seq.size()
    mask = torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),
                    diagonal=1)
    mask = mask.unsqueeze(0).expand(batch_size, -1, -1)  # [B, L, L]
    return mask

Гарвардские статьиThe Annotated TransformerЕсть карта эффектов:

sequence_mask
Figure 8. Sequence mask.

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

Возвращаясь к вопросу, поставленному в начале этого резюме,attn_maskСколько видов параметров существует? Что значит соответственно?

  • Для собственного внимания декодера используемое в нем масштабированное скалярное произведение внимания также требуетpadding maskиsequence maskв видеattn_mask, конкретная реализация заключается в добавлении двух масок в качестве attn_mask.
  • Другие случаи,attn_maskвсегда равноpadding mask.

На данный момент проблемы, связанные с маской, решены.

Что такое позиционное кодирование?

Ну наконец-то объяснитькод местоположения, это то, что упоминает структурная схема в начале текстаPositional encoding.

В нынешнем виде кажется, что в нашей архитектуре Transformer чего-то не хватает. ВерноНе имеет ограничений на порядок последовательности! Мы знаем, что порядок последовательности является очень важной информацией, если эта информация отсутствует, наш результат может быть таким: все слова правильные, но осмысленные предложения не могут быть сформированы!

Для решения этой проблемы. В документе представленыPositional encoding. что это? В двух словах:Кодирует положение слова в последовательности! Если позиция закодирована, то наша модель может фиксировать информацию о заказе!

Так как это сделать конкретно? Интересна реализация статьи с использованием функции синус-косинус. Формула выглядит следующим образом:

PE(pos,2i) = sin(pos/10000^{2i/d_{model}})
PE(pos,2i+1) = cos(pos/10000^{2i/d_{model}})

в,posОтносится к положению слова в последовательности. Видно, что вДля четных позиций используйте синусоидальное кодирование, а для нечетных позиций используйте косинусное кодирование..

в приведенной выше формулеd_{model}размер модели, бумага по умолчанию512.

Эта формула кодирования означает:положение данного слова\text{pos}, мы можем закодировать его какd_{model}размерный вектор! То есть каждое измерение кода положения соответствует синусоиде, а длина волны составляет2\piприбыть10000*2\piпоследовательность эквивалентности.

Приведенное выше позиционное кодированиекодирование абсолютного положения. но словоотносительное положениеТоже очень важно. Вот почему в статье используются тригонометрические функции!

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

sin(\alpha+\beta) = sin\alpha cos\beta + cos\alpha sin\beta
cos(\alpha+\beta) = cos\alpha cos\beta - sin\alpha sin\beta

Приведенная выше формула показывает, что для позиционного смещения между словамиk,PE(pos+k)можно выразить какPE(pos)иPE(k)Комбинированная форма , это способность выражать относительные положения!

ВышеупомянутоеPВсе секреты Е. После разговора о позиционном кодировании у нас есть еще одно в той же позиции, что иword embedding.

Word embeddingС ним знаком каждый, он кодирует слова в последовательности, кодируя каждое слово вd_{model}объемный вектор! Вы видите это,Позиционное кодирование — это позиционное кодирование словаря, а встраивание слов — это кодирование самого словаря.!

Итак, я предпочитаю другое имя для позиционного кодированияPositional embedding!

Реализация позиционного кодирования

Реализация PE не сложна, просто следуйте формуле статьи. код показывает, как показано ниже:

import torch
import torch.nn as nn


class PositionalEncoding(nn.Module):
    
    def __init__(self, d_model, max_seq_len):
        """初始化。
        
        Args:
            d_model: 一个标量。模型的维度,论文默认是512
            max_seq_len: 一个标量。文本序列的最大长度
        """
        super(PositionalEncoding, self).__init__()
        
        # 根据论文给的公式,构造出PE矩阵
        position_encoding = np.array([
          [pos / np.pow(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
          for pos in range(max_seq_len)])
        # 偶数列使用sin,奇数列使用cos
        position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
        position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

        # 在PE矩阵的第一行,加上一行全是0的向量,代表这`PAD`的positional encoding
        # 在word embedding中也经常会加上`UNK`,代表位置单词的word embedding,两者十分类似
        # 那么为什么需要这个额外的PAD的编码呢?很简单,因为文本序列的长度不一,我们需要对齐,
        # 短的序列我们使用0在结尾补全,我们也需要这些补全位置的编码,也就是`PAD`对应的位置编码
        pad_row = torch.zeros([1, d_model])
        position_encoding = torch.cat((pad_row, position_encoding))
        
        # 嵌入操作,+1是因为增加了`PAD`这个补全位置的编码,
        # Word embedding中如果词典增加`UNK`,我们也需要+1。看吧,两者十分相似
        self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
        self.position_encoding.weight = nn.Parameter(position_encoding,
                                                     requires_grad=False)
    def forward(self, input_len):
        """神经网络的前向传播。

        Args:
          input_len: 一个张量,形状为[BATCH_SIZE, 1]。每一个张量的值代表这一批文本序列中对应的长度。

        Returns:
          返回这一批序列的位置编码,进行了对齐。
        """
        
        # 找出这一批序列的最大长度
        max_len = torch.max(input_len)
        tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
        # 对每一个序列的位置进行对齐,在原序列位置的后面补上0
        # 这里range从1开始也是因为要避开PAD(0)的位置
        input_pos = tensor(
          [list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
        return self.position_encoding(input_pos)
    

Реализация встраивания Word

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

Итак, конкретная реализация очень проста:

import torch.nn as nn


embedding = nn.Embedding(vocab_size, embedding_size, padding_idx=0)
# 获得输入的词嵌入编码
seq_embedding = seq_embedding(inputs)*np.sqrt(d_model)

вышеvocab_sizeразмер словаря,embedding_size– размерность вложения слов, равнаяd_{model}=512. Таким образом, матрица встраивания слов представляет собойvocab_size*embedding_sizeдвумерный тензор.

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

Что такое позиционно-ориентированная сеть Feed-Forward?

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

FFN(x)=max(0,xW_1+b_1)W_2+b_2

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

В документе упоминается, что эту формулу также можно объяснить двумя одномерными свертками с размером ядра 1. Вход и выход свертки обаd_{model}=512, размер среднего слояd_{ff}=2048.

Реализация выглядит следующим образом:

import torch
import torch.nn as nn


class PositionalWiseFeedForward(nn.Module):

    def __init__(self, model_dim=512, ffn_dim=2048, dropout=0.0):
        super(PositionalWiseFeedForward, self).__init__()
        self.w1 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.w2 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, x):
        output = x.transpose(1, 2)
        output = self.w2(F.relu(self.w1(output)))
        output = self.dropout(output.transpose(1, 2))

        # add residual and norm layer
        output = self.layer_norm(x + output)
        return output

Реализация трансформатора

На данный момент все подробности разъяснены. Теперь, чтобы завершить код для нашей модели Transformer.

Во-первых, нам нужно реализовать 6 уровней кодировщика и декодера.

Код энкодера реализован следующим образом:

import torch
import torch.nn as nn


class EncoderLayer(nn.Module):
	"""Encoder的一层。"""

    def __init__(self, model_dim=512, num_heads=8, ffn_dim=2018, dropout=0.0):
        super(EncoderLayer, self).__init__()

        self.attention = MultiHeadAttention(model_dim, num_heads, dropout)
        self.feed_forward = PositionalWiseFeedForward(model_dim, ffn_dim, dropout)

    def forward(self, inputs, attn_mask=None):

        # self attention
        context, attention = self.attention(inputs, inputs, inputs, padding_mask)

        # feed forward network
        output = self.feed_forward(context)

        return output, attention


class Encoder(nn.Module):
	"""多层EncoderLayer组成Encoder。"""

    def __init__(self,
               vocab_size,
               max_seq_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.0):
        super(Encoder, self).__init__()

        self.encoder_layers = nn.ModuleList(
          [EncoderLayer(model_dim, num_heads, ffn_dim, dropout) for _ in
           range(num_layers)])

        self.seq_embedding = nn.Embedding(vocab_size + 1, model_dim, padding_idx=0)
        self.pos_embedding = PositionalEncoding(model_dim, max_seq_len)

    def forward(self, inputs, inputs_len):
        output = self.seq_embedding(inputs)
        output += self.pos_embedding(inputs_len)

        self_attention_mask = padding_mask(inputs, inputs)

        attentions = []
        for encoder in self.encoder_layers:
            output, attention = encoder(output, self_attention_mask)
            attentions.append(attention)

        return output, attentions

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

import torch
import torch.nn as nn


class DecoderLayer(nn.Module):

    def __init__(self, model_dim, num_heads=8, ffn_dim=2048, dropout=0.0):
        super(DecoderLayer, self).__init__()

        self.attention = MultiHeadAttention(model_dim, num_heads, dropout)
        self.feed_forward = PositionalWiseFeedForward(model_dim, ffn_dim, dropout)

    def forward(self,
              dec_inputs,
              enc_outputs,
              self_attn_mask=None,
              context_attn_mask=None):
        # self attention, all inputs are decoder inputs
        dec_output, self_attention = self.attention(
          dec_inputs, dec_inputs, dec_inputs, self_attn_mask)

        # context attention
        # query is decoder's outputs, key and value are encoder's inputs
        dec_output, context_attention = self.attention(
          enc_outputs, enc_outputs, dec_output, context_attn_mask)

        # decoder's output, or context
        dec_output = self.feed_forward(dec_output)

        return dec_output, self_attention, context_attention


class Decoder(nn.Module):

    def __init__(self,
               vocab_size,
               max_seq_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.0):
        super(Decoder, self).__init__()

        self.num_layers = num_layers

        self.decoder_layers = nn.ModuleList(
          [DecoderLayer(model_dim, num_heads, ffn_dim, dropout) for _ in
           range(num_layers)])

        self.seq_embedding = nn.Embedding(vocab_size + 1, model_dim, padding_idx=0)
        self.pos_embedding = PositionalEncoding(model_dim, max_seq_len)

    def forward(self, inputs, inputs_len, enc_output, context_attn_mask=None):
        output = self.seq_embedding(inputs)
        output += self.pos_embedding(inputs_len)

        self_attention_padding_mask = padding_mask(inputs, inputs)
        seq_mask = sequence_mask(inputs)
        self_attn_mask = torch.gt((self_attention_padding_mask + seq_mask), 0)

        self_attentions = []
        context_attentions = []
        for decoder in self.decoder_layers:
            output, self_attn, context_attn = decoder(
            output, enc_output, self_attn_mask, context_attn_mask)
            self_attentions.append(self_attn)
            context_attentions.append(context_attn)

        return output, self_attentions, context_attentions

Наконец, мы объединяем энкодер и декодер в модель Transformer!

код показывает, как показано ниже:

import torch
import torch.nn as nn


class Transformer(nn.Module):

    def __init__(self,
               src_vocab_size,
               src_max_len,
               tgt_vocab_size,
               tgt_max_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.2):
        super(Transformer, self).__init__()

        self.encoder = Encoder(src_vocab_size, src_max_len, num_layers, model_dim,
                               num_heads, ffn_dim, dropout)
        self.decoder = Decoder(tgt_vocab_size, tgt_max_len, num_layers, model_dim,
                               num_heads, ffn_dim, dropout)

        self.linear = nn.Linear(model_dim, tgt_vocab_size, bias=False)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, src_seq, src_len, tgt_seq, tgt_len):
        context_attn_mask = padding_mask(tgt_seq, src_seq)

        output, enc_self_attn = self.encoder(src_seq, src_len)

        output, dec_self_attn, ctx_attn = self.decoder(
          tgt_seq, tgt_len, output, context_attn_mask)

        output = self.linear(output)
        output = self.softmax(output)

        return output, enc_self_attn, dec_self_attn, ctx_attn

Пока реализована модель Трансформера!

Справочная статья

1.Почему ResNet и DenseNet могут быть такими глубокими? В этой статье объясняется, почему остаточный блок помогает решить проблему дисперсии градиента.
2.GRADIENTS, BATCH NORMALIZATION AND LAYER NORMALIZATION
3.The Annotated Transformer
4.Building the Mighty Transformer for Sequence Tagging in PyTorch : Part I
5.Building the Mighty Transformer for Sequence Tagging in PyTorch : Part II
6.Attention?Attention!

Код ссылки

1.jadore801120/attention-is-all-you-need-pytorch
2.JayParks/transformer

свяжитесь со мной

  • Email: stupidme.me.lzy@gmail.com

  • WeChat: luozhouyang0528

  • Личный публичный аккаунт, возможно вас заинтересует: