Análisis de arquitectura de transformadores

1. Diagrama de arquitectura del transformador

Este artículo proviene principalmente de: http://nlp.seas.harvard.edu/annotated-transformer/#hardware-and-schedule

Dirección en papel: https://arxiv.org/pdf/1810.04805.pdf

1.1 El papel del modelo Transformer

El modelo de transformador basado en la arquitectura seq2seq puede completar tareas típicas en el campo de la investigación de NLP, como la traducción automática, la generación de texto, etc. Al mismo tiempo, puede construir un modelo de lenguaje preentrenado para transferir el aprendizaje de diferentes tareas.

1.2 Arquitectura general del transformador

(1) Sección de entrada

  • Capa de incrustación de texto de origen y su codificador posicional
  • Capa de incrustación de texto de destino y su codificador posicional

inserte la descripción de la imagen aquí

(2) Sección de salida

  • capa lineal
  • capa softmax

inserte la descripción de la imagen aquí

(3) codificador

  • Apilado por N capas de codificador
  • Cada capa de codificador consta de dos estructuras de conexión de subcapa
  • La estructura de conexión de la primera subcapa consta de una subcapa de autoatención de varios cabezales y una capa de normalización y una conexión residual
  • La estructura de conexión de la segunda subcapa consiste en una subcapa totalmente conectada feedforward y una capa de normalización y una conexión residual

inserte la descripción de la imagen aquí

(4) Parte del decodificador:

  • Está apilado por N capas de decodificador
  • Cada capa de decodificador consta de tres estructuras de conexión de subcapa
  • La estructura de conexión de la primera subcapa consta de una subcapa de autoatención de varios cabezales y una capa de normalización y una conexión residual
  • La estructura de conexión de la segunda subcapa incluye una subcapa de atención de múltiples cabezas y una capa de normalización y una conexión residual
  • La estructura de conexión de la tercera subcapa incluye una subcapa totalmente conectada de realimentación y una capa de normalización y una conexión residual

inserte la descripción de la imagen aquí

2. Incrustaciones de piezas de entrada

2.1 Capa de incrustación de texto

Ya sea incrustación de texto de origen o incrustación de texto de destino, se trata de transformar la representación digital de palabras en el texto en una representación vectorial, con la esperanza de capturar la relación entre palabras en un espacio de dimensiones tan altas.

(1) darse cuenta

  • La función de inicialización toma d_modella dimensión de incrustación de palabras y vocabel número total de palabras como parámetros, y usa internamente la capa predeterminada Incrustación en nn para la incrustación de palabras.
  • En la función directa, la entrada x se pasa al objeto instanciado de Incrustación y luego se multiplica por un signo de raíz para d_modelescalar y controlar el valor.
  • Su salida es el resultado de incrustación de texto.
# torch中变量封装函数Variable.
from torch.autograd import Variable
# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
# 该类继承nn.Module, 这样就有标准层的一些功能, 这里我们也可以理解为一种模式, 我们自己实现的所有层都会这样去写.
class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """类的初始化函数, 
         d_model: 指词嵌入的维度, 
         vocab: 指词表的大小.
        """
        # 接着就是使用super的方式指明继承nn.Module的初始化函数, 我们自己实现的所有层都会这样去写.
        super(Embeddings, self).__init__()
        # 之后就是调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lut
        self.lut = nn.Embedding(vocab, d_model)
        # 最后就是将d_model传入类中
        self.d_model = d_model

    def forward(self, x):
        """ x: 因为Embedding层是首层, 所以代表输入给模型的文本通过词汇映射后的张量
        """

        # 将x传给self.lut并与根号下self.d_model相乘作为结果返回
        return self.lut(x) * math.sqrt(self.d_model)

(2) prueba

# 词嵌入维度是512维
d_model = 512
# 词表大小是1000
vocab = 1000
# 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4
x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
emb = Embeddings(d_model, vocab)
embr = emb(x)
print("embr:", embr)

embr: Variable containing:
( 0 ,.,.) = 
  35.9321   3.2582 -17.7301  ...    3.4109  13.8832  39.0272
   8.5410  -3.5790 -12.0460  ...   40.1880  36.6009  34.7141
 -17.0650  -1.8705 -20.1807  ...  -12.5556 -34.0739  35.6536
  20.6105   4.4314  14.9912  ...   -0.1342  -9.9270  28.6771

( 1 ,.,.) = 
  27.7016  16.7183  46.6900  ...   17.9840  17.2525  -3.9709
   3.0645  -5.5105  10.8802  ...  -13.0069  30.8834 -38.3209
  33.1378 -32.1435  -3.9369  ...   15.6094 -29.7063  40.1361
 -31.5056   3.3648   1.4726  ...    2.8047  -9.6514 -23.4909
[torch.FloatTensor of size 2x4x512]

2.2 Codificador de posición PositionalEncoding

Debido a que en la estructura del codificador de Transformer, no hay procesamiento de la información de posición de palabra, por lo que es necesario agregar un codificador de posición después de la capa de incrustación y agregar información que puede producir una semántica diferente debido a las diferentes posiciones de palabra en el tensor de incrustación de palabra para compensar para la información de posición de desaparecidos .

Utilice las funciones seno y coseno a diferentes frecuencias:

PE ( pos , 2 i ) = sin ( pos / 1000 0 2 i / dmodelo ) PE_{(pos,2i)}=sin(pos/10000^{2i/d_{modelo}})educación física( pos , 2 i ) _=sin(pos/100002 i / dm o d e l)

PE ( pos , 2 i + 1 ) = cos ( pos / 1000 0 2 i / dmodelo ) PE_{(pos,2i+1)}=cos(pos/10000^{2i/d_{modelo}})educación física( pos , 2 i + 1 ) _=cos ( pos / 1000 02 i / dm o d e l)

donde pos posp os es posición,iii es la dimensión. Es decir, cada dimensión del código de posición corresponde a una sinusoide. Estas longitudes de onda forman a de2π 2\pi2 π10000 ⋅ 2 π 10000 \cdot 2\pi10000La serie de conjuntos de 2 π . Elegimos esta función porque planteamos la hipótesis de que facilitaría que el modelo aprendiera a prestar atención a las posiciones relativas, ya que para cualquier desplazamiento k dado,PE pos + k PE_{pos+k}educación físicapos + k _Se puede expresar como PE pos PE_{pos}educación físicapos _función lineal de .

(1) darse cuenta

  • La función de inicialización toma d_model, dropout, max_lencomo parámetros, que representan respectivamente d_model: la dimensión de incrustación de palabras, dropout: relación de puesta a cero, max_len: la longitud máxima de cada oración.
  • El parámetro de entrada en la función directa es x, que es la salida de la capa de incrustación.
  • Finalmente, se genera un tensor de incrustación de palabras con información de codificación de posición.
# 定义位置编码器类, 我们同样把它看做一个层, 因此会继承nn.Module    
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        """位置编码器类的初始化函数, 共有三个参数, 分别是
          d_model: 词嵌入维度, 
          dropout: 置0比率, 
          max_len: 每个句子的最大长度
        """
        super(PositionalEncoding, self).__init__()

        # 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout
        self.dropout = nn.Dropout(p=dropout)

        # 初始化一个位置编码矩阵, 它是一个0阵,矩阵的大小是max_len x d_model.
        pe = torch.zeros(max_len, d_model)

        # 初始化一个绝对位置矩阵, 在我们这里,词汇的绝对位置就是用它的索引去表示. 
        # 所以我们首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵, 
        # 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个max_len x 1 的矩阵, 
        position = torch.arange(0, max_len).unsqueeze(1)

        # 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
        # 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可, 
        # 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
        # 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛.  这样我们就可以开始初始化这个变换矩阵了.
        # 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵, 
        # 而是有了一个跳跃,只初始化了一半即1xd_model/2 的矩阵。 为什么是一半呢,其实这里并不是真正意义上的初始化了一半的矩阵,
        # 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上, 
        # 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵.
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)

        # 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵,要想和embedding的输出(一个三维张量)相加,
        # 就必须拓展一个维度,所以这里使用unsqueeze拓展维度.
        pe = pe.unsqueeze(0)

        # 最后把pe位置编码矩阵注册成模型的buffer,什么是buffer呢,
        # 我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象. 
        # 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
        self.register_buffer('pe', pe)

    def forward(self, x):
        """ x, 表示文本序列的词嵌入表示 """
        # 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的x的第二维相同即x.size(1),
        # 因为我们默认max_len为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要进行与输入张量的适配. 
        # 最后使用Variable进行封装,使其与x的样式相同,但是它是不需要进行梯度求解的,因此把requires_grad设置成false.
        x = x + Variable(self.pe[:, :x.size(1)], 
                         requires_grad=False)
        # 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
        return self.dropout(x)

(2) prueba

# 词嵌入维度是512维
d_model = 512
# 置0比率为0.1
dropout = 0.1
# 句子最大长度
max_len=60

x = embr
pe = PositionalEncoding(d_model, dropout, max_len)
pe_result = pe(x)
print("pe_result:", pe_result)

pe_result: Variable containing:
( 0 ,.,.) = 
 -19.7050   0.0000   0.0000  ...  -11.7557  -0.0000  23.4553
  -1.4668 -62.2510  -2.4012  ...   66.5860 -24.4578 -37.7469
   9.8642 -41.6497 -11.4968  ...  -21.1293 -42.0945  50.7943
   0.0000  34.1785 -33.0712  ...   48.5520   3.2540  54.1348

( 1 ,.,.) = 
   7.7598 -21.0359  15.0595  ...  -35.6061  -0.0000   4.1772
 -38.7230   8.6578  34.2935  ...  -43.3556  26.6052   4.3084
  24.6962  37.3626 -26.9271  ...   49.8989   0.0000  44.9158
 -28.8435 -48.5963  -0.9892  ...  -52.5447  -4.1475  -3.0450
[torch.FloatTensor of size 2x4x512]

(3) Dibuje la curva de distribución de las características en el vector de vocabulario

import matplotlib.pyplot as plt

# 创建一张15 x 5大小的画布
plt.figure(figsize=(15, 5))
# 实例化PositionalEncoding类得到pe对象, 输入参数是20和0
pe = PositionalEncoding(20, 0)
# 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数, 
# 且这个tensor里的数值都是0, 被处理后相当于位置编码张量
y = pe(Variable(torch.zeros(1, 100, 20)))
# 然后定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# 因为总共有20维之多, 我们这里只查看4,5,6,7维的值.
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
# 在画布上填写维度提示信息
plt.legend(["dim %d"%p for p in [4,5,6,7]])

inserte la descripción de la imagen aquí

Análisis de efectividad:

  • Cada curva de color representa el significado de una característica en un determinado vocabulario en diferentes posiciones.
  • Se garantiza que el vector de incrustación de la posición correspondiente de la misma palabra cambiará con diferentes posiciones.
  • El rango de valores de la onda sinusoidal y la onda coseno es de 1 a -1, lo que controla muy bien el tamaño del valor incrustado, lo que es útil para el cálculo rápido del gradiente.

3. Codificador

  • Apilado por N capas de codificador
  • Cada capa de codificador consta de dos estructuras de conexión de subcapa
  • La estructura de conexión de la primera subcapa consta de una subcapa de autoatención de varios cabezales y una capa de normalización y una conexión residual
  • La estructura de conexión de la segunda subcapa consiste en una subcapa totalmente conectada feedforward y una capa de normalización y una conexión residual

inserte la descripción de la imagen aquí

3.1 tensor de máscara subsiguiente_mask

Máscara significa enmascarar. El código es el valor en nuestro tensor. Su tamaño es variable. Generalmente, solo hay 1 y 0 elementos en él, lo que significa que la posición está cubierta o no. En cuanto a si la posición 0 está cubierta o la Se cubre 1 posición, se puede personalizar, por lo tanto, su función es tapar unos valores en otro tensor, o reemplazarlos, y su expresión es un tensor

El papel del tensor de máscara : En el transformador, el papel principal del tensor de máscara está en la aplicación de la atención.Algunos cálculos de valor en el tensor de atención generado se pueden obtener conociendo la información futura.La información futura se ve porque Durante el entrenamiento , todo el resultado de salida se incrusta a la vez, pero teóricamente, la salida del decodificador no produce el resultado final a la vez, sino que se obtiene sintetizando los resultados anteriores una y otra vez. Por lo tanto, la información futura puede aprovecharse con anticipación. .

(1) darse cuenta

def subsequent_mask(size):
    """生成向后遮掩的掩码张量, 
      参数size是掩码张量最后两个维度的大小, 它的最后两维形成一个方阵
    """
    # 在函数中, 首先定义掩码张量的形状
    attn_shape = (1, size, size)

    # 然后使用np.ones方法向这个形状中添加1元素,形成上三角阵, 最后为了节约空间, 
    # 再使其中的数据类型变为无符号8位整形unit8 
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')

    # 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作, 
    # 在这个其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减, 
    # 如果是0, subsequent_mask中的该位置由0变成1
    # 如果是1, subsequent_mask中的该位置由1变成0 
    return torch.from_numpy(1 - subsequent_mask)

(2) prueba

# 生成的掩码张量的最后两维的大小
size = 5
sm = subsequent_mask(size)
print("sm:", sm)

tensor([[[1, 0, 0, 0, 0],
         [1, 1, 0, 0, 0],
         [1, 1, 1, 0, 0],
         [1, 1, 1, 1, 0],
         [1, 1, 1, 1, 1]]], dtype=torch.uint8)

(3) Visualización de tensores de máscara

plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])

inserte la descripción de la imagen aquí

Análisis de efectividad:

  • Al observar la matriz cuadrada visualizada, el amarillo es la parte de 1, aquí representa lo cubierto, el púrpura representa la información que no está cubierta, las abscisas representan la posición del vocabulario objetivo y las ordenadas representan la posición visible;
  • Podemos ver que la posición 0 es amarilla cuando la miramos, y está tapada. La posición 1 sigue siendo amarilla a simple vista, lo que indica que la palabra no se ha generado por primera vez. Mirándola desde la segunda posición. , se pueden ver las palabras en la posición 1, pero no en otras posiciones, y así sucesivamente.

3.2 Mecanismo de atención Atención

(1) ¿Qué es la atención?

Cuando observamos cosas, la razón por la que podemos juzgar rápidamente una cosa (por supuesto, permitir que el juicio sea incorrecto) es porque nuestro cerebro puede concentrarse rápidamente en la parte más reconocible de la cosa para emitir un juicio, en lugar de comenzar desde el principio. Sólo después de observar las cosas al final puede haber un resultado de juicio. Es en base a esta teoría que se produce el mecanismo de atención.

La función Atención se puede describir como la asignación de una consulta y un conjunto de pares clave-valor a una salida, donde la consulta, la clave, el valor y la salida son todos vectores. La salida es la suma ponderada de valores, donde el peso de cada valor se calcula mediante la función compatible de la consulta y la clave correspondiente.

(2) Reglas de cálculo de atención

Se requieren tres entradas especificadas Q(consulta), K(clave), V(valor), y luego el resultado del cálculo de atención se obtiene a través de la fórmula, que representa la expresión de consulta bajo la acción de clave y valor . Hay muchas reglas de cálculo específicas, y solo presentaré la que usamos aquí.

Atención ( Q , K , V ) = softmax ( QKT dk ) V Atención(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})VA t e n c i ó n ( Q ,K ,V )=entonces f t máx ( _dk Q KT) V

(3) Interpretación metafórica de Q, K, V

Supongamos que tenemos un problema: dado un fragmento de texto, ¡descríbalo usando algunas palabras clave!

Para facilitar la unificación de la respuesta correcta, esta pregunta puede haber escrito algunas palabras clave de antemano como sugerencias. Estas sugerencias pueden considerarse claves, y toda la información del texto es equivalente a consulta, y el significado del valor es más abstracto, puede compararse con la información de respuesta que le viene a la mente después de leer este texto. Aquí asumimos que no todos son muy inteligentes al principio. Después de ver este texto por primera vez, básicamente lo que le viene a la mente La información solo solicita esta información, Entonces, la clave y el valor son básicamente los mismos, pero a medida que comprendemos este problema en profundidad, más cosas vienen a la mente a través de nuestro pensamiento y podemos comenzar a responder nuestra consulta. Un párrafo de texto, extrayendo información clave para la representación. Este es el proceso de atención. A través de este proceso, el valor en nuestra mente finalmente ha cambiado, y el método de consulta de representación de palabras clave se genera de acuerdo con la clave de solicitud, que es otra representación de características. Método. Justo ahora dijimos esa
clave y el valor son generalmente los mismos por defecto, que es diferente de la consulta. Este es nuestro formulario de entrada de atención general, pero hay un caso especial, es decir, nuestra consulta es la misma que la clave y el valor. La situación que llamamos auto- mecanismo de atención , al igual que nuestro ejemplo de ahora, usar el mecanismo de atención general es usar palabras clave diferentes del texto dado para representarlo. El mecanismo de autoatención necesita ser expresado por el propio texto dado, es decir, tú necesitan extraer palabras clave de un texto dado para expresarlo, lo que equivale a una extracción de características del propio texto.

(4) Mecanismo de atención

El mecanismo de atención es el portador de la red de aprendizaje profundo a la que se pueden aplicar las reglas de cálculo de atención. Además de las reglas de cálculo de atención, también incluye algunas capas necesarias totalmente conectadas y el procesamiento de tensor relacionado para integrarlo con la red de aplicaciones. Un mecanismo de atención que utiliza reglas de cálculo de autoatención se denomina mecanismo de autoatención.

La atención particular se denomina "Atención de producto punto escalado" (Scaled Dot-Product Attention"). Su entrada es consulta, clave (dimensión es dk d_kdk) y valores (dimensión es dv d_vdv). Calculamos el producto punto de consulta y todas las claves, y luego dividimos cada uno por dk \sqrt{d_k}dk , y finalmente use la función softmax para obtener el peso del valor.

inserte la descripción de la imagen aquí

Las dos funciones de atención más utilizadas son la atención aditiva (citar) y la atención de producto escalar (multiplicación). Además del factor de escala 1 dk \frac{1}{\sqrt{d_k}}dk 1, el producto escalar Atención es el mismo que nuestro algoritmo habitual. La atención aditiva calcula funciones compatibles utilizando una red de avance con una sola capa oculta. Aunque teóricamente la atención del producto punto y la atención aditiva tienen una complejidad similar, en la práctica, la atención del producto punto se puede implementar utilizando una multiplicación de matrices altamente optimizada, por lo que el cálculo de la atención del producto punto es más rápido y más eficiente en cuanto al espacio.

cuando dk d_kdkCuando el valor de es relativamente pequeño, el rendimiento de los dos mecanismos es similar. cuando dk d_kdkCuando es más grande, la atención aditiva funciona mejor que la atención de producto escalar sin escalar (citar). Sospechamos que para dk d_k muy grandedkvalor, el producto escalar crece sustancialmente, empujando la función softmax a regiones con gradientes extremadamente pequeños. (Para ilustrar por qué el producto escalar se vuelve más grande, suponga que qqq ykk es una variable aleatoria independiente con media 0 y varianza 1. Entonces su producto escalarq ⋅ k = ∑ i = 1 dkqikiq \cdot k = \sum_{i=1}^{d_k} q_ik_iqk=yo = 1dkqyokyo, la media es 0 y la varianza es dk d_kdk). Para contrarrestar este efecto, reducimos el producto escalar en 1 dk \frac{1}{\sqrt{d_k}}dk 1veces.

¿Por qué dividir por ∗ ∗ d ∗ ∗ **\sqrt{d}** en Atenciónd ** tan importante? **

El cálculo de Atención es realizar softmax después del producto interno, y la operación principal involucrada es eq ⋅ ke^{q \cdot k}miq k , se puede considerar aproximadamente que el valor después del producto interno y antes del softmax es− 3 d -3\sqrt{d}3d a 3 d 3\sqrt{d}3d En este rango, dado que d suele ser al menos 64, entonces e 3 de^{3\sqrt{d}}mi3d Relativamente grande y e − 3 de^{-3\sqrt{d}}mi3d Es relativamente pequeño, por lo que después de softmax, la distribución de Atención es muy cercana a una distribución caliente, lo que genera un problema grave de desaparición de gradiente, lo que resulta en un efecto de entrenamiento deficiente. (Por ejemplo, y=softmax(x) ingresa a la zona de saturación cuando |x| es grande, y x continúa cambiando y el valor de y casi no cambia, es decir, el gradiente en la zona de saturación desaparece)

En consecuencia, hay dos soluciones:

  1. Divida por d \sqrt{d} después del producto interno como la parametrización NTKd , de modo que la varianza de q⋅k se convierte en 1, correspondiente a e 3 , e − 3 e^3,e^{−3}mi3 ,mi3 no será demasiado grande ni demasiado pequeño, por lo que después de softmax, no se convertirá en uno caliente y el gradiente desaparecerá Esta es también la práctica de los transformadores convencionales como Self Attention en BERT

  2. La otra es no dividir por d \sqrt{d}d , pero al inicializar la capa completamente conectada de q y k, la varianza de inicialización debe dividirse por una d más, lo que también puede hacer que la varianza inicial de q⋅k se convierta en 1. T5 adopta este enfoque.

(5) Implementación del código

  • La entrada es Q, K, V, máscara y caída, la máscara se usa para enmascarar y la caída se usa para la configuración aleatoria en 0.
  • Hay dos salidas, la representación de atención de la consulta y el tensor de atención.
def attention(query, key, value, mask=None, dropout=None):
    """注意力机制的实现, 
        输入分别是query, key, value, mask: 掩码张量, 
       dropout是nn.Dropout层的实例化对象, 默认为None
    """

    # 将query的最后一个维度提取出来,代表的是词嵌入的维度
    d_k = query.size(-1)

    # 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 
    # 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
    # 得到注意力得分张量scores
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

    # 接着判断是否使用掩码张量
    if mask is not None:
        # 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 
        # 如果掩码张量处为0,则对应的scores张量用-1e9这个值来替换, 如下演示
        scores = scores.masked_fill(mask == 0, -1e9)

    # 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度.
    # 这样获得最终的注意力张量
    p_attn = scores.softmax(dim=-1)

    # 之后判断是否使用dropout进行随机置0
    if dropout is not None:
        # 将p_attn传入dropout对象中进行'丢弃'处理
        p_attn = dropout(p_attn)

    # 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力张量
    return torch.matmul(p_attn, value), p_attn

(6) Prueba

from embedding import Embeddings
from positional_encoding import PositionalEncoding

  d_model = 512
  dropout = 0.1
  max_len = 60

  vocab = 1000
  emb = Embeddings(d_model, vocab)
  input = Variable(torch.LongTensor([[1,2,4,5], [4,3,2,9]]))
  embr = emb(input)
  print("embr: ", embr)
  print(embr.shape)

  pe = PositionalEncoding(d_model, dropout, max_len)
  pe_res = pe(embr)

  query = key = value = pe_res
  attn, p_attn = attention(query, key, value)

  print("attn:", attn)
  print("attn shape:", attn.shape)
  print("p_attn:", p_attn)
  print("p_attn shape:", p_attn.shape)
attn: tensor([[[ -6.9986, -22.1325,  44.3268,  ...,   0.0000,  -2.9953,  -3.8844],
         [ 57.2318,   5.9384,   0.0000,  ...,  38.5518,   0.5860,  12.5283],
         [-56.2970,   0.0000,  -4.3592,  ...,  26.0355,  -2.3129,   0.0000],
         [ -2.1557,   3.4803, -36.6878,  ...,  15.8174, -21.3978, -30.9041]],

        [[-57.3073, -25.4691,  -5.3997,  ...,  26.0355,  -2.3131,  14.3969],
         [  6.9673, -32.6722,  39.3464,  ...,  -5.4699, -10.4042,  -4.4331],
         [ 57.3072,   4.8757,  -5.6530,  ...,  38.5518,   0.5862,   0.0000],
         [ 18.3028,  -9.1978,  59.9258,  ...,  -9.5552, -45.4553,   0.0000]]],
       grad_fn=<UnsafeViewBackward>)
attn shape: torch.Size([2, 4, 512])
p_attn: tensor([[[1., 0., 0., 0.],
         [0., 1., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 1.]],

        [[1., 0., 0., 0.],
         [0., 1., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward>)
p_attn shape: torch.Size([2, 4, 4])

El efecto de salida de la máscara que se utilizará.

query = key = value = pe_result

# 令mask为一个2x4x4的零张量
mask = Variable(torch.zeros(2, 4, 4))
attn, p_attn = attention(query, key, value, mask=mask)
print("attn:", attn)
print("p_attn:", p_attn)

# query的注意力表示:
attn: Variable containing:
( 0 ,.,.) = 
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770
   0.4284  -7.4741   8.8839  ...    1.5618   0.5063   0.5770

( 1 ,.,.) = 
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
  -2.8890   9.9972 -12.9505  ...    9.1657  -4.6164  -0.5491
[torch.FloatTensor of size 2x4x512]

# 注意力张量:
p_attn: Variable containing:
(0 ,.,.) = 
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500

(1 ,.,.) = 
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500
  0.2500  0.2500  0.2500  0.2500
[torch.FloatTensor of size 2x4x4]

3.3 Atención multicabezal Atención multicabezal

(1) ¿Qué es el mecanismo de atención de múltiples cabezas?

Del diagrama de estructura de la atención de cabezas múltiples, parece que las llamadas cabezas múltiples se refieren a múltiples conjuntos de capas de transformación lineal. De hecho, no lo es. Solo uso un conjunto de capas de transformación lineal, es decir, tres capas de transformación. pares de tensores Q, K, V respectivamente Realiza transformaciones lineales. Estas transformaciones no cambiarán el tamaño del tensor original , por lo que cada matriz de transformación es una matriz cuadrada. Después de obtener el resultado de salida, el papel de múltiples cabezas comienza a aparecer Cada cabeza comienza a desdoblar el tensor de salida del nivel semántico, es decir, cada cabeza quiere obtener un conjunto de Q, K y V para el cálculo del mecanismo de atención, pero solo una parte de la representación de cada palabra en se obtiene la oración, es decir, solo se divide el vector de incrustación de palabras de la última dimensión. Este es el llamado multicabezal, y la entrada obtenida por cada cabezal se envía al mecanismo de atención para formar un mecanismo de atención multicabezal.

La atención de múltiples cabezas permite que el modelo se enfoque conjuntamente en la información de diferentes subespacios de representación en diferentes ubicaciones, y si solo hay una cabeza de atención, su promedio debilitará esta información.

M ulti H ead ( Q , K , V ) = C oncat ( head 1 , . . . , headh ​​) WO donde headi = Atención ( QW i Q , KW i K , VW i V ) MultiHead(Q,K,V )=Concat(cabeza_1,...,cabeza_h)W^O \\ donde ~ cabeza_i = Atención(QW_i^Q, KW_i^K, VW_i^V)M u lt i Head a d ( Q ,K ,V )=C o n c a t ( cabeza _ _ _1,... ,cabeza _ _ _h) WOa dónde se dirige _ _ _ yo=A t e n c i ó n ( Q Wiq,KW _ik,VW _iV)

El mapeo se realiza mediante la matriz de peso: $W^Q_i \in \mathbb{R}^{d_{ { modelo}} \times d_k}
$, W i K ∈ R d modelo × dk W^K_i \in \mathbb {R }^{d_{\text{modelo}} \times d_k}WikRdmodelo× rek, W i V ∈ R d modelo × dv W^V_i \in \mathbb{R}^{d_{\text{modelo}} \times d_v}WiVRdmodelo× revW i O ∈ R hdv × d modelo W^O_i \in \mathbb{R}^{hd_v \times d_{\text{modelo}} }WiORhd _v× remodelo

inserte la descripción de la imagen aquí

(2) Atención de múltiples cabezas

Este diseño estructural permite que cada mecanismo de atención optimice diferentes partes características de cada vocabulario , equilibrando así las posibles desviaciones del mismo mecanismo de atención y permitiendo que los significados de las palabras tengan expresiones más diversas. Los experimentos muestran que puede mejorar el efecto del modelo.

(3) darse cuenta

  • Debido a que es necesario utilizar varias capas lineales idénticas en el mecanismo de atención de varios cabezales, la función de clonación clones se implementa primero.
  • La entrada de la función de clones es el módulo, N, que representan respectivamente la capa de destino del clon y el número de clones.
  • La salida de la función de clones es una lista de Módulos con N capas clonadas.
  • Luego implemente la clase MultiHeadedAttention, su entrada de función de inicialización es h, d_model, dropout representa el número de cabezas, la dimensión de incrustación de palabras y la relación de reducción a cero, respectivamente.
  • Sus entradas de objetos de instanciación son Q, K, V y la máscara del tensor de máscara.
  • Su salida de objeto instanciado es una representación de atención de Q procesada a través de un mecanismo de atención de múltiples cabezas.
import copy
def clones(model, N):
    """
    用于生成相同网络层的克隆函数, 
        - module表示要克隆的目标网络层, 
        - N代表需要克隆的数量
    """
    # 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层,
    # 然后将其放在nn.ModuleList类型的列表中存放.
    return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])
    
class MultiHeadAttention(nn.Module):
    """
    """
    def __init__(self, head, embedding_dim, dropout=0.1):
        super(MultiHeadAttention, self).__init__()
        """ 多头注意力机制
            - head代表头数
            - embedding_dim代表词嵌入的维度, 
           - dropout代表进行dropout操作时置0比率,默认是0.1
        """
        
        # 在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,
        # 这是因为我们之后要给每个头分配等量的词特征.也就是embedding_dim/head个.
        assert embedding_dim % head == 0

        # 得到每个头获得的分割词向量维度d_k
        self.d_k = embedding_dim // head
        # 传入头数head
        self.head = head

         # 然后获得线性层对象,通过nn的Linear实例化,
         # 它的内部变换矩阵是embedding_dim x embedding_dim,
         # 然后使用clones函数克隆四个,为什么是四个呢,
         # 这是因为在多头注意力中,Q,K,V各需要一个,
         # 最后拼接的矩阵还需要一个,因此一共是四个.
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
        # self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None.
        self.attn = None

        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        """ 输入参数有四个,前三个就是注意力机制需要的Q, K, V,
            最后一个是注意力机制中可能需要的mask掩码张量,默认是None.
        """
        # 如果存在掩码张量mask
        if mask is not None:
            # 使用unsqueeze拓展维度
            mask = mask.unsqueeze(0)

        # 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
        batch_size = query.size(0)

        # 之后就进入多头处理环节
        # 首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,
        # 将输入QKV分别传到线性层中,做完线性变换后,
        # 开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑,多加了一个维度h,代表头数,
        # 这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
        # 计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
        # 为了让代表句子长度维度和词向量维度能够相邻,
        # 这样注意力机制才能找到词义与句子位置的关系,
        # 从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.
        # 这样我们就得到了每个头的输入.
        query, key, value = [
            model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
            for model, x in zip(self.linears, (query, key, value))
        ]

        # 得到每个头的输入后,接下来就是将他们传入到attention中,
        # 这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

        # 通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,
        # 我们需要将其转换为输入的形状以方便后续的计算,
        # 因此这里开始进行第一步处理环节的逆操作,
        # 先对第二和第三维进行转置,然后使用contiguous方法,
        # 这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用,
        # 所以,下一步就是使用view重塑形状,变成和输入形状相同.
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
        
        # 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出.
        return self.linears[-1](x)

(4) prueba

d_model = 512
    dropout = 0.1
    max_len = 60
    vocab = 1000
    emb = Embeddings(d_model, vocab)
    input = Variable(torch.LongTensor([[1,2,4,5], [4,3,2,9]]))
    embr = emb(input)
    
    pe = PositionalEncoding(d_model, dropout, max_len)
    pe_res = pe(embr)

    head = 8
    embedding_dim = 512
    dropout = 0.2
    query = key = value = pe_res

    mask = Variable(torch.zeros(8, 4, 4))
    mha = MultiHeadAttention(head, embedding_dim, dropout)
    mha_res = mha(query, key, value, mask)
tensor([[[-2.3411, -0.8430, -4.1038,  ...,  1.4731, -0.7992,  0.9026],
         [-5.1657, -2.4703, -7.4543,  ...,  0.8810,  0.3061,  0.6387],
         [-4.2553, -0.1940, -6.1963,  ..., -3.4095,  0.6791, -0.6660],
         [-4.8889, -4.0475, -5.9836,  ..., -3.4044,  0.5312,  0.7642]],

        [[-4.8633,  2.5490, -6.3160,  ..., -1.7124, -2.2730,  0.7630],
         [-5.1141,  2.4704, -4.4557,  ...,  2.4667, -0.3286,  0.8127],
         [-8.8165,  1.9820, -6.3692,  ..., -1.9055,  2.4552, -6.4086],
         [-6.2969,  2.9008, -1.2483,  ...,  0.1594, -4.0804,  0.0228]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

3.4 Capa Feedforward completamente conectada PositionwiseFeedForward

La capa feedforward totalmente conectada en Transformer es una red totalmente conectada con dos capas lineales.

(1) Función de capa totalmente conectada feedforward

Teniendo en cuenta que el mecanismo de atención puede no encajar lo suficientemente bien en el proceso complejo, la capacidad del modelo se mejora al agregar una red de dos capas.

FFN ( x ) = máx ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x)=max(0, ~ xW_1+b_1)W_2+b_2FFN ( x )=máx ( 0 , _ x ancho1+b1) W2+b2

La posición es cada token en la secuencia, Position-wisees decir, el MLP se aplica a cada token una vez y se aplica el mismo MLP.

(2) Implementación del código

  • Los parámetros de creación de instancias son d_model, d_ff, dropout, que representan la dimensión de incrustación de palabras, la dimensión de transformación lineal y la relación de puesta a cero, respectivamente.
  • El parámetro de entrada x representa la salida de la capa superior.
  • El resultado es una representación de características transformada por una red lineal de 2 capas.
class PositionwiseFeedForward(nn.Module):
    """ 实现前馈全连接层
    """
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        """
            - d_model: 线性层的输入维度也是第二个线性层的输出维度,
                    因为我们希望输入通过前馈全连接层后输入和输出的维度不变. 
           - d_ff: 第二个线性层的输入维度和第一个线性层的输出维度. 
           - dropout: 置0比率
        """
        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
        # 首先经过第一个线性层,然后使用Funtional中relu函数进行激活,
        # 之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果.
        return self.w2(self.dropout(self.w1(x).relu()))

(3) prueba

d_model = 512
# 线性变化的维度
d_ff = 64
dropout = 0.2

ff = PositionwiseFeedForward(d_model, d_ff, dropout)

print(mha_res.shape)
ff_result = ff(mha_res)
print(ff_result)

torch.Size([2, 4, 512])
tensor([[[-2.8747, -0.1289, -0.4966,  ..., -1.2763, -2.0888,  0.3344],
         [-1.2404,  0.3891,  1.3854,  ..., -1.2675, -1.8324,  0.2271],
         [-1.6913,  1.2393,  0.1528,  ..., -0.7420, -2.5605,  0.9924],
         [-3.4989,  1.4898, -0.7094,  ..., -1.1352, -1.9817,  0.4473]],

        [[-2.0806,  0.1014,  1.4044,  ...,  0.1496, -2.4822, -1.5388],
         [-3.5828,  0.3326,  1.2598,  ...,  0.8470, -2.5095,  0.1296],
         [-2.7594, -0.2307,  1.4870,  ...,  1.1056, -2.3847, -1.6484],
         [-1.1717, -1.2086,  0.6444,  ..., -0.5858, -3.3344, -0.9535]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

3.5 Capa de normalización LayerNorm

(1) Función de la capa de normalización

Es una capa de red estándar requerida por todos los modelos de redes profundas, porque a medida que aumenta la cantidad de capas de red, los parámetros pueden comenzar a parecer demasiado grandes o demasiado pequeños después del cálculo de múltiples capas , lo que puede causar anomalías en el proceso de aprendizaje y la convergencia del modelo puede ser muy lenta. Por lo tanto, la capa de normalización será seguida por un cierto número de capas para normalizar el valor , de modo que el valor característico esté dentro de un rango razonable.

(2) Implementación del código

  • Hay dos parámetros de creación de instancias, featuresy eps, que representan respectivamente el tamaño de la característica de incrustación de palabras y un número lo suficientemente pequeño.
  • El parámetro de entrada x representa la salida de la capa anterior.
  • La salida es la representación de características normalizadas.
class LayerNorm(nn.Module):
    """ 实现规范化层的类
    """
    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        """ 初始化参数
            - features: 表示词嵌入的维度,
            - eps: 一个足够小的数, 在规范化公式的分母中出现,防止分母为0.默认是1e-6.
        """

        # 根据features的形状初始化两个参数张量a2,和b2,第一个初始化为1张量,
        # 也就是里面的元素都是1,第二个初始化为0张量,也就是里面的元素都是0,
        # 这两个张量就是规范化层的参数,因为直接对上一层得到的结果做规范化公式计算,
        # 将改变结果的正常表征,因此就需要有参数作为调节因子,使其即能满足规范化要求,
        # 又能不改变针对目标的表征.最后使用nn.parameter封装,代表他们是模型的参数。
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))

        self.eps = eps

    def forward(self, x):
        """
        """
        # 在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.
        # 接着再求最后一个维度的标准差,然后就是根据规范化公式,
        # 用x减去均值除以标准差获得规范化的结果,
        # 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,
        # 加上位移参数b2.返回即可.
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

(3) prueba

features = d_model = 512
eps = 1e-6

x = ff_result
ln = LayerNorm(features, eps)

print(mha_res.shape)
ln_result = ln(x)
print(ln_result)
print(ln_result.shape)
torch.Size([2, 4, 512])
tensor([[[-0.6481,  0.6222, -0.2731,  ...,  0.2868,  1.0175,  0.6720],
         [-0.3056,  0.6657,  0.1862,  ...,  0.3972,  0.9435,  0.5340],
         [-0.1928,  1.0572,  0.3111,  ..., -0.0410,  0.5555,  0.5671],
         [-0.4334, -0.2361, -0.1477,  ...,  0.0923,  2.0700,  0.7032]],

        [[ 1.0477, -0.7183,  0.0449,  ...,  1.6828,  0.3927,  0.5616],
         [ 1.5125, -1.1870,  0.5266,  ...,  1.4665,  1.8670,  0.2973],
         [ 0.8196, -2.3064, -0.2661,  ...,  1.0591,  1.1476, -0.2259],
         [ 0.3523, -0.5912,  0.5318,  ...,  1.0312,  0.6859, -0.6222]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

3.6 Estructura de conexión de subcapa SublayerConnection

Como se muestra en la figura, el enlace residual (conexión de salto) también se usa en el proceso de entrada a cada subcapa y la capa de normalización , por lo que llamamos a esta parte de la estructura conexión de subcapa (que representa la subcapa y su estructura de enlace) , en En cada capa de codificador, hay dos subcapas, y estas dos subcapas más la estructura de enlace circundante forman una estructura de conexión de dos subcapas.

inserte la descripción de la imagen aquí

(1) Implementación del código

  • Los parámetros de entrada de la función de inicialización de la clase son size, dropout, que representan el tamaño de incrustación de palabras y la relación de reducción a cero, respectivamente.
  • Sus parámetros de entrada de objetos instanciados son x, sublayer, que representan la salida de la capa anterior y la representación de la función de la subcapa, respectivamente.
  • Su salida es la salida procesada a través de la estructura de conexión de la subcapa.
class SublayerConnection(nn.Module):
    """ 子层连接结构的类
    """
    def __init__(self, size, dropout=0.1):
        """ 初始化
            - size: 词嵌入维度的大小, 
            - dropout: 是对模型结构中的节点数进行随机抑制的比率, 
        """
        super(SublayerConnection, self).__init__()
        # 实例化了规范化对象self.norm
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        """ 前向逻辑函数中, 接收上一个层或者子层的输入作为第一个参数,
           将该子层连接中的子层函数作为第二个参数
        """

        # 我们首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作,
        # 随机停止一些网络中神经元的作用,来防止过拟合. 最后还有一个add操作, 
        # 因为存在跳跃连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出.
        return x + self.dropout(sublayer(self.norm(x)))

(2) prueba

x = pe_result
mask = Variable(torch.zeros(8, 4, 4))

mha = MultiHeadAttention(head, embedding_dim, dropout)

sublayer = lambda x : mha(x, x, x, mask)
size = 512

sc = SublayerConnection(size, dropout)
print(x.shape)
sc_result = sc(x, sublayer)
print(sc_result)
print(sc_result.shape)
torch.Size([2, 4, 512])
tensor([[[ -8.1109,   0.2217,   1.0194,  ...,  22.8768,  40.2057,  14.8928],
         [ 31.5267,  33.8321,  29.1483,  ...,  17.2134, -16.1430,  -4.6771],
         [  7.9553, -20.3707, -41.5156,  ..., -10.6798, -23.8106,   4.3206],
         [-59.5128,  31.6802,   1.1462,  ..., -15.4225,  -2.6904,  45.0427]],

        [[  6.9450, -18.4468, -42.5561,  ..., -10.3473, -23.6293,   4.3888],
         [-38.2155,  46.7522, -22.8546,  ...,  28.8744,  -0.0767,  13.4702],
         [ 31.4326,  33.3512,  29.2566,  ...,  17.6481, -16.0407,  -4.4314],
         [-16.7070, -15.7774,  16.0646,  ..., -65.1326,   0.0000,   0.1910]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

3.7 Capa de codificador EncoderLayer

Como unidad componente del codificador, **Cada capa del codificador completa un proceso de extracción de características** para la entrada, es decir, el proceso de codificación.

inserte la descripción de la imagen aquí

(1) Implementación del código

  • Hay 4 funciones de inicialización de la clase. La primera es size, de hecho, el tamaño de nuestra dimensión de incrustación de palabras. La segunda self_attn, la pasaremos en la subcapa de autoatención de varios cabezales para instanciar el objeto, y es la mecanismo de autoatención. El tercero es feed_froward, y luego pasaremos el objeto de instanciación de capa totalmente conectado de alimentación hacia adelante. El último es la relación cero dropout.
  • Hay 2 parámetros de entrada para el objeto instanciado, x representa la salida de la capa anterior y mask representa el tensor de máscara.
  • Su salida representa la representación de características de toda la capa de codificación.
class EncoderLayer(nn.Module):
    """ 编码器层
    """
    def __init__(self, size, self_attn, feed_forward, dropout):
        """ 初始化
            - size: 词嵌入维度的大小,它也将作为我们编码器层的大小, 
            - self_attn: 传入多头自注意力子层实例化对象, 并且是自注意力机制, 
            - eed_froward: 传入前馈全连接层实例化对象,
            - dropout: 置0比率
        """
        super(EncoderLayer, self).__init__()

        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.size = size
        # 如图所示, 编码器层中有两个子层连接结构, 所以使用clones函数进行克隆
        self.sublayer = clones(SublayerConnection(size, dropout), 2)

    def forward(self, x, mask):
        """ x和mask,分别代表上一层的输出,和掩码张量mask
        """
        #  首先通过第一个子层连接结构,其中包含多头自注意力子层,
        # 然后通过第二个子层连接结构,其中包含前馈全连接子层. 最后返回结果.
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, mask))

        return self.sublayer[1](x, self.feed_forward)

(2) prueba

size = d_model = 512
head = 8
d_ff = 64
x = pe_result
dropout = 0.2

self_attn = MultiHeadAttention(head, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
mask = Variable(torch.zeros(8, 4, 4))

el = EncoderLayer(size, self_attn, ff, dropout)
print(x.shape)
el_result = el(x, mask)
print(el_result)
print(el_result.shape)
torch.Size([2, 4, 512])
tensor([[[-29.5621,  -2.0977,   2.6601,  ...,  33.7814, -41.1742, -10.7692],
         [ 36.1439, -27.8296, -23.2643,  ...,  21.5115,  -4.6657,  14.0641],
         [ -8.1926, -29.1385,  -2.2535,  ...,  -5.4215,   2.7747,  -0.4909],
         [-25.4288, -14.2624, -22.5432,  ...,  -5.3338,   9.2610,   4.8978]],

        [[ -9.4071, -27.6196,  -2.8486,  ...,  -5.1319,   2.3754,  14.1391],
         [-15.6512,  -1.9466, -36.3869,  ...,  19.8941,  24.4394,  40.9649],
         [ 36.5416, -28.4673, -22.8311,  ...,  21.5283,  -4.6554,  14.3312],
         [-30.7237,  38.2961,  -8.4991,  ...,  57.8437,  11.2464,  -5.4290]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

3.8 Codificador Codificador

El codificador se utiliza para especificar el proceso de extracción de características de la entrada, también conocido como codificación, que se compone de N capas de codificador apiladas.

inserte la descripción de la imagen aquí

(1) darse cuenta

  • Hay dos parámetros para la función de inicialización de la clase, a saber layery N, que representan la capa del codificador y el número de capas del codificador.
  • La función de avance también tiene dos parámetros de entrada, que son los mismos que el avance de la capa del codificador, x representa la salida de la capa anterior y el tensor de máscara del código de máscara.
  • La salida de la clase de codificador es la representación de extracción de características del codificador en Transformer, que pasará a formar parte de la entrada del decodificador.
class Encoder(nn.Module):
    """ 实现编码器
    """
    def __init__(self, layer, N):
        """ 初始化
            - layer: 编码器层
            - N: 编码器层的个数
        """
        super(Encoder, self).__init__()
        # 使用clones函数克隆N个编码器层放在self.layers中
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        """forward函数的输入和编码器层相同, 
            - x: 上一层的输出, 
            - mask: 掩码张量
        """
        # 首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x,
        # 这个循环的过程,就相当于输出的x经过了N个编码器层的处理. 
        # 最后再通过规范化层的对象self.norm进行处理,最后返回结果. 
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

(2) prueba

size = d_model = 512
head = 8
d_ff = 64
x = pe_result
dropout = 0.2
N = 8

self_attn = MultiHeadAttention(head, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
mask = Variable(torch.zeros(8, 4, 4))

layer = EncoderLayer(size, copy.deepcopy(self_attn), copy.deepcopy(ff), dropout)

encoder = Encoder(layer, N)

print(x.shape)
en_result = encoder(x, mask)
print(en_result)
print(en_result.shape)
torch.Size([2, 4, 512])
tensor([[[-0.7567, -1.2521, -0.2055,  ...,  0.8205, -1.2941, -2.0247],
         [-0.0359,  0.9469,  0.0691,  ..., -0.6150,  0.4005, -0.1147],
         [-1.3874,  0.9941,  0.1449,  ..., -0.3395,  1.3993, -2.0148],
         [-0.5812, -0.6430,  2.1250,  ...,  1.8703, -0.1342,  0.6250]],

        [[-1.4746,  1.0971, -0.0154,  ..., -0.3533,  1.4110, -1.8592],
         [-0.5287, -1.6246,  0.7500,  ...,  0.4196,  0.8892,  0.2809],
         [-0.1306,  0.8462,  0.0411,  ..., -0.5721,  0.4040, -0.1732],
         [-0.8179, -1.3323, -0.7204,  ..., -0.4005,  0.5500, -0.0986]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

4. Decodificador

  • Está apilado por N capas de decodificador
  • Cada capa de decodificador consta de tres estructuras de conexión de subcapa
  • La estructura de conexión de la primera subcapa consta de una subcapa de autoatención de varios cabezales y una capa de normalización y una conexión residual
  • La estructura de conexión de la segunda subcapa incluye una subcapa de atención de múltiples cabezas y una capa de normalización y una conexión residual
  • La estructura de conexión de la tercera subcapa incluye una subcapa totalmente conectada de realimentación y una capa de normalización y una conexión residual

inserte la descripción de la imagen aquí

4.1 Capa del decodificador Capa del decodificador

Como unidad constitutiva del decodificador, cada capa del decodificador realiza operaciones de extracción de características en la dirección del objetivo de acuerdo con la entrada dada , es decir, el proceso de decodificación.

(1) darse cuenta

  • Hay 5 parámetros para la función de inicialización de la clase, respectivamente size, que representan el tamaño de la dimensión de la incrustación de palabras y también representan el tamaño de la capa del decodificador, el segundo es el self_attnobjeto de autoatención de varios cabezales, es decir, el mecanismo de atención requiere Q=K=V, los tres primeros son src_attnobjetos de atención de múltiples cabezas, aquí Q!=K=V, el cuarto es el objeto de capa totalmente conectado de avance, y el último es droupoutla relación cero.
  • La función directa tiene 4 parámetros, que son la entrada x de la capa anterior, la memoria variable de almacenamiento semántico de la capa del codificador y el tensor de máscara de datos de origen y el tensor de máscara de datos de destino.
  • Finalmente, se emite el resultado de extracción de características de la entrada del codificador y los datos de destino.
class DecoderLayer(nn.Module):
    """ 解码器层
    """
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        """ 初始化
            - size:代表词嵌入的维度大小, 同时也代表解码器层的尺寸,
            - self_attn: 多头自注意力对象,也就是说这个注意力机制需要Q=K=V, 
            - src_attn:多头注意力对象,这里Q!=K=V, 
            - feed_forward: 前馈全连接层对象,
            - droupout:置0比率.
        """
        super(DecoderLayer, self).__init__()

        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.dropout = dropout
        # 按照结构图使用clones函数克隆三个子层连接对象.
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        """forward函数中的参数有4个,
            - x: 来自上一层的输入x,
            - mermory: 来自编码器层的语义存储变量 
            - src_mask: 源数据掩码张量
            - tgt_mask: 目标数据掩码张量.
        """
        # 将memory表示成m方便之后使用
        m = memory

        # 将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,
        # 因为是自注意力机制,所以Q,K,V都是x,最后一个参数是目标数据掩码张量,
        # 这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据,
        # 比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,
        # 但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,
        # 同样生成第二个字符或词汇时,
        # 模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用.
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, tgt_mask))

        # 接着进入第二个子层,这个子层中常规的注意力机制,q是输入x; k,v是编码层输出memory, 
        # 同样也传入source_mask,但是进行源数据遮掩的原因并非是抑制信息泄漏,
        # 而是遮蔽掉对结果没有意义的字符而产生的注意力值,
        # 以此提升模型效果和训练速度. 这样就完成了第二个子层的处理.
        x = self.sublayer[1](x, lambda x : self.src_attn(x, m, m, src_mask))

        # 最后一个子层就是前馈全连接子层,经过它的处理后就可以返回结果
        return self.sublayer[2](x, self.feed_forward)

(2) prueba

size = d_model = 512
head = 8
d_ff = 64
dropout = 0.2

self_attn = src_attn = MultiHeadAttention(head, d_model, dropout)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
# x是来自目标数据的词嵌入表示, 但形式和源数据的词嵌入表示相同, 这里使用per充当.
x = pe_result
# memory是来自编码器的输出
memory = en_result
# 实际中source_mask和target_mask并不相同, 这里为了方便计算使他们都为mask
mask = Variable(torch.zeros(8, 4, 4))
src_mask = tgt_mask = mask

dl = DecoderLayer(size, self_attn, src_attn, ff, dropout)
print(x.shape)
dl_result = dl(x, memory, src_mask, tgt_mask)
print(dl_result)
print(dl_result.shape)
torch.Size([2, 4, 512])
tensor([[[  0.3128, -28.9028,  12.6505,  ..., -25.0090,   0.0671,  43.7430],
         [-18.6592, -20.2816,  -0.2946,  ...,  30.0085, -15.1012, -14.5942],
         [-19.6110,   0.1298,  -0.3384,  ..., -14.7287, -22.1352,  12.7321],
         [-33.9689,   0.9680, -61.3009,  ..., -51.5810,  -8.8205,  -6.2392]],

        [[-21.1268,  10.4200,   3.9523,  ..., -15.5449,  -0.2314,  12.6887],
         [  6.3277,  27.3815,  43.6648,  ..., -21.2202, -48.8453, -20.5100],
         [-18.5797, -21.5331, -38.0592,  ...,  29.7248, -15.0411,   0.4119],
         [ 65.4318,  15.5895, -23.6869,  ..., -25.7464,  42.8896,  14.5587]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

4.2 Decodificador Decodificador

Según el resultado del codificador y el resultado de la predicción anterior, se caracteriza el siguiente 'valor' posible .

(1) darse cuenta

  • La función de inicialización de la clase tiene dos parámetros, el primero es la capa del decodificador layery el segundo es el número de capas del decodificador N.
  • Hay 4 parámetros en la función directa, xque representan la representación incrustada de los datos de destino, memoryque es la salida de la capa del codificador, src_maskque tgt_maskrepresenta el tensor de máscara de los datos de origen y de destino.
  • Da salida a la representación de características final del proceso de decodificación.
class Decoder(nn.Module):
    """ 实现解码器
    """
    def __init__(self, layer, N):
        """ 初始化
            - layer: 解码器层
            - N:解码器层个数
        """
        super(Decoder, self).__init__()
        # 首先使用clones方法克隆了N个layer,然后实例化了一个规范化层. 
        # 因为数据走过了所有的解码器层后最后要做规范化处理. 
        self.layers = clones(layer, N)

        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, src_mask, tgt_mask):
        """
            - x: 来自上一层的输入x,
            - mermory: 来自编码器层的语义存储变量 
            - src_mask: 源数据掩码张量
            - tgt_mask: 目标数据掩码张量.
        """
        # 然后就是对每个层进行循环,当然这个循环就是变量x通过每一个层的处理,
        # 得出最后的结果,再进行一次规范化返回即可.
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)

(2) prueba

size = d_model = 512
head = 8
d_ff = 64
dropout = 0.2
attn = MultiHeadAttention(head, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
layer = DecoderLayer(d_model, copy.deepcopy(attn), copy.deepcopy(attn), copy.deepcopy(ff), dropout)

N = 8
# 输入参数与解码器层的输入参数相同
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8, 4, 4))
src_mask = tgt_mask = mask

de = Decoder(layer, N)

print(x.shape)
de_result = de(x, memory, src_mask, tgt_mask)
print(de_result)
print(de_result.shape)
torch.Size([2, 4, 512])
tensor([[[ 6.2605e-01, -1.6188e+00, -2.0886e+00,  ...,  1.0329e-01,
          -9.3746e-01, -2.6656e-01],
         [ 1.0500e-01, -2.6750e+00,  2.6044e+00,  ..., -5.4699e-01,
          -7.5199e-02, -2.8667e-01],
         [-1.6483e-02, -3.6539e-01, -3.1693e-01,  ..., -2.1838e-01,
           5.6952e-01,  1.4017e+00],
         [-4.9258e-01, -9.2657e-01, -1.3348e-01,  ..., -2.1710e-01,
           1.3200e+00,  1.3176e+00]],

        [[-2.6396e-03, -2.1476e-01, -2.9699e-01,  ..., -2.0103e-02,
           5.1760e-01,  1.5096e+00],
         [-4.1995e-01, -2.5207e+00, -1.1587e-01,  ..., -4.2679e-01,
           7.7861e-01,  1.7993e-02],
         [ 2.2358e-02, -2.7497e+00,  2.5735e+00,  ..., -4.2572e-01,
          -4.1822e-01, -1.9397e-01],
         [ 2.8288e-01,  4.5199e-01,  3.6352e-01,  ...,  2.1069e+00,
          -8.3942e-01,  3.1137e-02]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

5. Sección de salida

  • capa lineal
  • capa softmax

inserte la descripción de la imagen aquí

5.1 Capa lineal

La salida de la dimensión especificada se obtiene a través del cambio lineal del paso anterior, es decir, la función de transformar la dimensión .

5.2 capa softmax

Haga que los números en la última dimensión de la escala vectorial tengan un rango de probabilidad de 0-1 y satisfaga que su suma sea 1.

5.3 Clase de generador de capa lineal y capa softmax

(1) darse cuenta

  • Hay dos parámetros de entrada para la función de inicialización, d_modelque representan la dimensión de incrustación de palabras y vocab_sizerepresentan el tamaño del vocabulario.
  • La función de avance acepta la salida de la capa anterior.
  • Finalmente se obtienen los resultados procesados ​​por la capa lineal y la capa softmax.
class Generator(nn.Module):
    """ 生成器类
        将线性层和softmax计算层一起实现, 因为二者的共同目标是生成最后的结构
        因此把类的名字叫做Generator, 生成器类
    """

    def __init__(self, d_model, vocab_size):
        """
            - d_model: 词嵌入维度
            - vocab_size: 词表的总大小
        """
        super(Generator, self).__init__()

        self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        """
            - x: 上一层的输出张量
        """
        # 在函数中, 首先使用上一步得到的self.project对x进行线性变化, 
        # 然后使用F中已经实现的log_softmax进行的softmax处理.
        # 在这里之所以使用log_softmax是因为和我们这个pytorch版本的损失函数实现有关, 在其他版本中将修复.
        # log_softmax就是对softmax的结果又取了对数, 因为对数函数是单调递增函数, 
        # 因此对最终我们取最大的概率值没有影响. 最后返回结果即可.
        return log_softmax(self.project(x), dim=-1)

(2) prueba

d_model = 512
vocab_size = 1000
# 输入x是上一层网络的输出, 我们使用来自解码器层的输出
x = de_result

gen = Generator(d_model, vocab_size)
print(x)
gen_result = gen(x)
print(gen_result)
print(gen_result.shape)
torch.Size([2, 4, 512])
tensor([[[-7.6141, -7.1042, -6.0669,  ..., -6.8734, -7.0652, -7.3009],
         [-7.1238, -6.8357, -7.2459,  ..., -7.1225, -8.0530, -6.9380],
         [-7.5266, -7.6293, -8.1937,  ..., -7.6398, -7.8350, -7.6071],
         [-7.4972, -7.2781, -7.3025,  ..., -6.6653, -6.4995, -6.9529]],

        [[-7.5171, -7.0331, -8.0956,  ..., -7.4018, -7.6130, -7.9539],
         [-6.4713, -7.4932, -6.8351,  ..., -6.6046, -8.0713, -7.4401],
         [-7.3482, -6.6409, -7.5268,  ..., -7.1031, -8.2056, -7.2852],
         [-8.1393, -7.1066, -7.4460,  ..., -6.9347, -6.3511, -6.9577]]],
       grad_fn=<LogSoftmaxBackward>)
torch.Size([2, 4, 1000])

6. Construcción de modelos

6.1 Codificador-Decodificador CodificadorDecodificador

La mayoría de los modelos de traducción de secuencias neuronales tienen una estructura de codificador-decodificador. El codificador toma una secuencia de entrada ( x 1 , . . . , xn ) (x_1, ..., x_n)( X1,... ,Xn) se asigna a una representación continuaz = ( z 1 , . . , zn ) z=(z_1, .., z_n)z=( z1,.. ,zn) en. Para cada elemento en z, el decodificador genera una secuencia de salida( y 1 , . . . , ym ) (y_1, ..., y_m)( y1,... ,ym) , se genera un elemento por paso de tiempo. En cada paso, el modelo es autorregresivo y agrega estructuras generadas previamente a la secuencia de entrada para predecir juntos cuándo generar el siguiente resultado. (Características de los modelos autorregresivos)

(1) darse cuenta

  • La función de inicialización de la clase pasa en 5 parámetros, que son el objeto codificador, el objeto decodificador, la función de incrustación de datos de origen, la función de incrustación de datos de destino y el objeto generador de categorías de la parte de salida.
  • La clase implementa tres funciones en total, forward, encode,decode
  • forward es la función lógica principal, con cuatro parámetros, source representa los datos de origen, target representa los datos de destino, source_mask y target_mask representan el tensor de máscara correspondiente.
  • encode es una función de codificación que toma source y source_mask como parámetros.
  • decode es una función de decodificación, con memoria como salida del codificador, source_mask, target, target_mask como parámetros
class EncoderDecoder(nn.Module):
    """ 实现编码器-解码器结构
    """
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        """ 初始化
            - encoder: 编码器对象
            - decoder: 解码器对象
            - src_embed:源数据嵌入函数
            - tgt_embed: 目标数据嵌入函数
            - generator:输出部分类别生成器
        """
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator

    def forward(self, src, tgt, src_mask, tgt_mask):
        """
            - src:源数据
            - tgt:目标数据
            - src_mask:源数据的掩码张量
            - tgt_mask:目标数据的掩码张量
        """
        # 在函数中, 将source, source_mask传入编码函数, 得到结果后,
        # 与source_mask,target,和target_mask一同传给解码函数.
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
    
    def encode(self, src, src_mask):
        """ 编码函数
            - src:源数据
            - src_mask:源数据的掩码张量
        """
        return self.encoder(self.src_embed(src), src_mask)
    
    def decode(self, memory, src_mask, tgt, tgt_mask):
        """ 解码函数
            - memory:经历编码器编码后的输出张量
            - src_mask:源数据的掩码张量
            - tgt:目标数据
            - tgt_mask:目标数据的掩码张量
        """
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

(2) prueba

vocab_size = 1000
d_model = 512
encoder = en
decoder = de
src_embed = nn.Embedding(vocab_size, d_model)
tgt_embed = nn.Embedding(vocab_size, d_model)
generator = Generator(d_model, vocab_size)

src = tgt = Variable(torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 221]]))
src_mask = tgt_mask = Variable(torch.zeros(8, 4, 4))

ed = EncoderDecoder(encoder, decoder, src_embed, tgt_embed, generator)

ed_result = ed(src, tgt, src_mask, tgt_mask)
print(ed_result)
print(ed_result.shape)
tensor([[[ 0.3879,  0.1344, -0.5700,  ..., -0.2206, -0.7505, -0.3314],
         [-0.7957,  0.8759, -0.5033,  ..., -1.3409, -1.4451, -0.3243],
         [ 0.3513,  0.8083,  0.1246,  ..., -0.4443, -1.3551, -1.3547],
         [ 0.0050,  0.6573, -1.1390,  ...,  0.1529, -1.5487, -0.8990]],

        [[-0.1515,  1.9247, -0.0315,  ..., -0.5945, -2.7363, -1.2481],
         [-0.6422,  1.5250,  0.7561,  ..., -1.4778, -1.2162, -2.2946],
         [ 0.0163,  1.8034,  0.1408,  ..., -0.4170, -1.7017, -1.6474],
         [ 0.2880, -0.0269, -0.1636,  ..., -0.7687, -1.3453, -0.8909]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

6.2 Modelo de transformador make_model

(1) darse cuenta

  • Hay 7 parámetros, que son el número total de características de datos de origen (vocabularios), el número total de características de datos de destino (vocabularios), el número de pilas de codificadores y decodificadores, la dimensión del mapeo de vectores de palabras, la dimensión de la matriz de transformación en la red totalmente conectada feedforward, y atención de múltiples cabezas El número de largos en la estructura y la caída de la relación de reducción a cero.
  • La función finalmente devuelve un objeto modelo construido.
def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
    """ 构建transformer模型
        - src_vocab: 源数据特征(词汇)总数
        - tgt_vocab: 目标数据特征(词汇)总数
        - N: 编码器和解码器堆叠数
        - d_model: 词向量映射维度
        - d_ff: 前馈全连接网络中变换矩阵的维度
        - h : 多头注意力结构中的多头数
        - dropout: 置零比率
    """
    # 首先得到一个深度拷贝命令,接下来很多结构都需要进行深度拷贝,
    # 来保证他们彼此之间相互独立,不受干扰.
    c = copy.deepcopy
    # 实例化了多头注意力类,得到对象attn
    attn = MultiHeadAttention(h, d_model)
    # 然后实例化前馈全连接类,得到对象ff 
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    # 实例化位置编码类,得到对象position
    position = PositionalEncoding(d_model, dropout)
    # 根据结构图, 最外层是EncoderDecoder,在EncoderDecoder中,
    # 分别是编码器层,解码器层,源数据Embedding层和位置编码组成的有序结构,
    # 目标数据Embedding层和位置编码组成的有序结构,以及类别生成器层. 
    # 在编码器层中有attention子层以及前馈全连接子层,
    # 在解码器层中有两个attention子层以及前馈全连接层.
    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
        Generator(d_model, tgt_vocab)
    )
    # 模型结构完成后,接下来就是初始化模型中的参数,比如线性层中的变换矩阵
    # 这里一但判断参数的维度大于1,则会将其初始化成一个服从均匀分布的矩阵,
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)

    return model

(2) prueba

src_vocab = 11
tgt_vocab = 11

model= make_model(src_vocab, tgt_vocab)
print(model)
# 太长了。。。

6.3 Inferencia

Aquí, usamos las predicciones del modelo generativo. Intentamos usar nuestro transformador para recordar la entrada. Como verá, dado que el modelo no ha sido entrenado, la salida se genera aleatoriamente.

def inference_test():
    test_model = make_model(11, 11, 2)
    test_model.eval()
    src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
    src_mask = torch.ones(1, 1, 10)

    memory = test_model.encode(src, src_mask)
    ys = torch.zeros(1, 1).type_as(src)

    for i in range(9):
        out = test_model.decode(
            memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
        )
        prob = test_model.generator(out[:, -1])
        _, next_word = torch.max(prob, dim=1)
        next_word = next_word.data[0]
        ys = torch.cat(
            [ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
        )

    print("Example Untrained Model Prediction:", ys)


def run_tests():
    for _ in range(10):
        inference_test()
Example Untrained Model Prediction: tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
Example Untrained Model Prediction: tensor([[0, 3, 4, 4, 4, 4, 4, 4, 4, 4]])
Example Untrained Model Prediction: tensor([[ 0, 10, 10, 10,  3,  2,  5,  7,  9,  6]])
Example Untrained Model Prediction: tensor([[ 0,  4,  3,  6, 10, 10,  2,  6,  2,  2]])
Example Untrained Model Prediction: tensor([[ 0,  9,  0,  1,  5, 10,  1,  5, 10,  6]])
Example Untrained Model Prediction: tensor([[ 0,  1,  5,  1, 10,  1, 10, 10, 10, 10]])
Example Untrained Model Prediction: tensor([[ 0,  1, 10,  9,  9,  9,  9,  9,  1,  5]])
Example Untrained Model Prediction: tensor([[ 0,  3,  1,  5, 10, 10, 10, 10, 10, 10]])
Example Untrained Model Prediction: tensor([[ 0,  3,  5, 10,  5, 10,  4,  2,  4,  2]])
Example Untrained Model Prediction: tensor([[0, 5, 6, 2, 5, 6, 2, 6, 2, 2]])

7. Prueba de funcionamiento

7.1 Introducción a la tarea de copiar

Descripción de la tarea : aprendizaje de secuencias digitales, el objetivo final del aprendizaje es hacer que la salida sea igual a la secuencia de entrada. Por ejemplo, si la entrada es [1, 5, 8, 9, 3], la salida también es [1 , 5, 8, 9, 3].

Importancia de la tarea: la tarea de copia es de gran importancia en la prueba básica del modelo, porque la operación de copia es una regla obvia para el modelo, por lo que si el modelo puede aprenderla en un corto período de tiempo y un pequeño conjunto de datos puede ayudar. nosotros determinamos si todos los procesos del modelo son normales, si ya tienen la capacidad de aprendizaje básica.

7.2 Pruebas básicas del modelo

(1) Cree un generador de conjuntos de datos

lograr

def data_generator(V, batch_size, num_batch):
    """ 该函数用于随机生成copy任务的数据
        - V: 随机生成数字的最大值+1
        - batch: 每次输送给模型更新一次参数的数据量
        - num_batch: 一共输送num_batch次完成一轮
    """
    for i in range(num_batch):
        # 在循环中使用randint方法随机生成[1, V)的整数, 
        # 分布在(batch, 10)形状的矩阵中, 
        data = torch.randint(1, V, size=(batch_size, 10))
        # 接着使数据矩阵中的第一列数字都为1, 这一列也就成为了起始标志列, 
        # 当解码器进行第一次解码的时候, 会使用起始标志列作为输入.
        data[:, 0] = 1
        # 因为是copy任务, 所有source与target是完全相同的, 且数据样本作用变量不需要求梯度
        # 因此requires_grad设置为False
        src = data.requires_grad_(False).clone().detach()
        tgt = data.requires_grad_(False).clone().detach()
        # 使用Batch对source和target进行对应批次的掩码张量生成, 最后使用yield返回
        yield Batch(src, tgt, 0)

prueba

V = 11
batch_size = 20
num_batch = 30
res = data_generator(V, batch_size, num_batch)
<generator object data_generator at 0x00000245A1AD4BA0>

(2) Obtenga el modelo de transformador, el optimizador y la función de pérdida

lograr

Cálculo de la función de pérdida

class SimpleLossCompute:
    "损失函数计算"

    def __init__(self, generator, criterion):
        self.generator = generator
        self.criterion = criterion

    def __call__(self, x, y, norm):
        x = self.generator(x)
        sloss = self.criterion(
                x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
            ) / norm
        return sloss.data * norm, sloss

suavizado de etiquetas

Durante el proceso de entrenamiento, el valor de suavizado de etiquetas que usamos es \epsilon_{ls}=0.1 (cite). Esto hace que el modelo sea menos comprensible ya que aprende a ser más incierto, pero mejora la precisión y las puntuaciones BLEU.

class LabelSmoothing(nn.Module):
    "Implement label smoothing."

    def __init__(self, size, padding_idx, smoothing=0.0):
        super(LabelSmoothing, self).__init__()
        self.criterion = nn.KLDivLoss(reduction="sum")
        self.padding_idx = padding_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.size = size
        self.true_dist = None

    def forward(self, x, target):
        assert x.size(1) == self.size
        true_dist = x.data.clone()
        true_dist.fill_(self.smoothing / (self.size - 2))
        true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
        true_dist[:, self.padding_idx] = 0
        mask = torch.nonzero(target.data == self.padding_idx)
        if mask.dim() > 0:
            true_dist.index_fill_(0, mask.squeeze(), 0.0)
        self.true_dist = true_dist
        return self.criterion(x, true_dist.clone().detach())
V = 11
batch_size = 20
num_batch = 30
model = make_model(V, V, N=2)
# 获得模型优化器
optimizer = torch.optim.Adam(
    model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9
)
# 使用LabelSmoothing获得标签平滑对象
criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
# 使用SimpleLossCompute获得利用标签平滑结果的损失计算方法
loss = SimpleLossCompute(model.generator, criterion)

Ejemplo de suavizado de etiquetas

# 使用LabelSmoothing实例化一个crit对象.
# 第一个参数size代表目标数据的词汇总数, 也是模型最后一层得到张量的最后一维大小
# 这里是5说明目标词汇总数是5个. 第二个参数padding_idx表示要将那些tensor中的数字
# 替换成0, 一般padding_idx=0表示不进行替换. 第三个参数smoothing, 表示标签的平滑程度
# 如原来标签的表示值为1, 则平滑后它的值域变为[1-smoothing, 1+smoothing].
crit = LabelSmoothing(size=5, padding_idx=0, smoothing=0.5)

# 假定一个任意的模型最后输出预测结果和真实结果
predict = Variable(torch.FloatTensor([[0, 0.2, 0.7, 0.1, 0],
                            [0, 0.2, 0.7, 0.1, 0], 
                            [0, 0.2, 0.7, 0.1, 0]]))

# 标签的表示值是0,1,2
target = Variable(torch.LongTensor([2, 1, 0]))

# 将predict, target传入到对象中
crit(predict, target)

# 绘制标签平滑图像
plt.imshow(crit.true_dist)
plt.waitforbuttonpress()

inserte la descripción de la imagen aquí

Análisis de imagen de suavizado de etiquetas:

  • Nos enfocamos en el pequeño cuadrado amarillo, el rango de valores que abarca en relación con la abscisa es el rango de valores de suavizado directo después del suavizado de etiquetas, podemos ver que es aproximadamente de 0.5 a 2.5.
  • El rango de valores que abarca en relación con la ordenada es el rango de valores de suavizado negativo después del suavizado de etiquetas. Podemos ver que es aproximadamente de -0.5 a 1.5, y el espacio del rango de valores total cambia del original [0, 2] a [- 0,5, 2,5].

(3) Ejecutar el modelo para entrenamiento y evaluación

V = 11
batch_size = 80
model = make_model(V, V, N=2)
# 获得模型优化器
optimizer = torch.optim.Adam(model.parameters(), 
                             lr=0.5, betas=(0.9, 0.98), eps=1e-9)
# 使用LabelSmoothing获得标签平滑对象
criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
# 使用SimpleLossCompute获得利用标签平滑结果的损失计算方法
loss = SimpleLossCompute(model.generator, criterion)
lr_scheduler = LambdaLR(optimizer=optimizer,
    lr_lambda=lambda step: rate(step, model_size=model.src_embed[0].d_model, 
                                factor=1.0, warmup=400))

for epoch in range(epochs):
     # 模型使用训练模式, 所有参数将被更新
    model.train()
    run_epoch(data_generator(V, batch_size, 20), model, loss,
        optimizer, lr_scheduler, mode="train")
    
    # 模型使用评估模式, 参数将不会变化 
    model.eval()
    run_epoch(data_generator(V, batch_size, 5), model, loss,
        DummyOptimizer(), DummyScheduler(), mode="eval")[0]

(4) Utilice el modelo para la decodificación codiciosa

Para simplificar, este código predice las traducciones usando decodificación codiciosa.

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    memory = model.encode(src, src_mask)
    ys = torch.zeros(1, 1).fill_(start_symbol).type_as(src.data)
    for i in range(max_len - 1):
        out = model.decode(
            memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
        )
        prob = model.generator(out[:, -1])
        _, next_word = torch.max(prob, dim=1)
        next_word = next_word.data[0]
        ys = torch.cat(
            [ys, torch.zeros(1, 1).type_as(src.data).fill_(next_word)], dim=1
        )
    return ys
V = 11
batch_size = 80
model = make_model(V, V, N=2)
# 获得模型优化器
optimizer = torch.optim.Adam(model.parameters(), 
                             lr=0.5, betas=(0.9, 0.98), eps=1e-9)
# 使用LabelSmoothing获得标签平滑对象
criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
# 使用SimpleLossCompute获得利用标签平滑结果的损失计算方法
loss = SimpleLossCompute(model.generator, criterion)
lr_scheduler = LambdaLR(optimizer=optimizer,
    lr_lambda=lambda step: rate(step, model_size=model.src_embed[0].d_model, 
                                factor=1.0, warmup=400))

for epoch in range(epochs):
     # 模型使用训练模式, 所有参数将被更新
    model.train()
    run_epoch(data_generator(V, batch_size, 20), model, loss,
        optimizer, lr_scheduler, mode="train")
    
    # 模型使用评估模式, 参数将不会变化 
    model.eval()
    run_epoch(data_generator(V, batch_size, 5), model, loss,
        DummyOptimizer(), DummyScheduler(), mode="eval")[0]

model.eval()
src = torch.LongTensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
max_len = src.shape[1]
src_mask = torch.ones(1, 1, max_len)
result = greedy_decode(model, src, src_mask, max_len=max_len, start_symbol=0)
print(result)
tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])

Supongo que te gusta

Origin blog.csdn.net/wdnshadow/article/details/130795440
Recomendado
Clasificación