Saltar al contenido principal
InicioTutorialesInteligencia Artificial (IA)

Construir un transformador con PyTorch

Aprende a construir un modelo Transformer utilizando PyTorch, una potente herramienta del aprendizaje automático moderno.
may 2024  · 26 min leer

El objetivo de este tutorial es proporcionar una comprensión completa de cómo construir un modelo Transformer utilizando PyTorch. El Transformer es uno de los modelos más potentes del aprendizaje automático moderno. Han revolucionado este campo, sobre todo en tareas de Procesamiento del Lenguaje Natural (PLN) como la traducción de idiomas y el resumen de textos. Las redes de memoria a largo plazo (LSTM) han sido sustituidas por transformadores en estas tareas debido a su capacidad para manejar dependencias de largo alcance y cálculos paralelos.

La herramienta utilizada en esta guía para construir el Transformer es PyTorch, una popular librería de aprendizaje automático de código abierto conocida por su simplicidad, versatilidad y eficiencia. Con un gráfico de cálculo dinámico y amplias bibliotecas, PyTorch se ha convertido en la herramienta de referencia para investigadores y desarrolladores en el ámbito del aprendizaje automático y la inteligencia artificial.

Para aquellos que no estén familiarizados con PyTorch, se recomienda una visita al curso de DataCamp, Introducción al aprendizaje profundo con PyTorch, para obtener una base sólida.

Antecedentes y teoría

Introducidos por primera vez en el artículo Attention is All You Need de Vaswani et al., los transformadores se han convertido desde entonces en la piedra angular de muchas tareas de PNL gracias a su diseño único y a su eficacia.

En el corazón de Transformers está el mecanismo de atención, concretamente el concepto de "autoatención", que permite al modelo sopesar y priorizar distintas partes de los datos de entrada. Este mecanismo es el que permite a los Transformadores gestionar las dependencias de largo alcance en los datos. Se trata fundamentalmente de un esquema de ponderación que permite a un modelo centrarse en diferentes partes de la entrada al producir una salida.

Este mecanismo permite al modelo tener en cuenta distintas palabras o características en la secuencia de entrada, asignando a cada una un "peso" que significa su importancia para producir un resultado determinado.

Por ejemplo, en una tarea de traducción de frases, al traducir una palabra concreta, el modelo podría asignar mayor peso de atención a las palabras que están relacionadas gramatical o semánticamente con la palabra de destino. Este proceso permite al Transformer captar las dependencias entre palabras o rasgos, independientemente de su distancia entre sí en la secuencia.

No se puede exagerar el impacto de Transformers en el campo de la PNL. Han superado a los modelos tradicionales en muchas tareas, demostrando una capacidad superior para comprender y generar lenguaje humano de forma más matizada.

Para una comprensión más profunda de la PNL, el curso Introducción al Procesamiento del Lenguaje Natural en Python de DataCamp es un recurso recomendado.

Configuración de PyTorch

Antes de lanzarse a la construcción de un transformador, es esencial configurar correctamente el entorno de trabajo. Lo primero y más importante es instalar PyTorch. PyTorch (versión estable actual - 2.0.1) puede instalarse fácilmente a través de los gestores de paquetes pip o conda.

Para pip, utilice el comando:

pip3 install torch torchvision torchaudio

Para conda, utilice el comando:

conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia

Para utilizar pytorch con una cpu, visite la documentación de pytorch.

Además, es beneficioso tener una comprensión básica de los conceptos de aprendizaje profundo, ya que serán fundamentales para entender el funcionamiento de Transformers. Para quienes necesiten un repaso, el curso de DataCamp Aprendizaje profundo en Python es un recurso valioso que cubre conceptos clave del aprendizaje profundo.

Construcción del modelo de transformador con PyTorch

Para construir el modelo de Transformador son necesarios los siguientes pasos:

  1. Importación de bibliotecas y módulos
  2. Definición de los componentes básicos: atención multicabezal, redes de alimentación en función de la posición, codificación posicional...
  3. Construcción del bloque codificador
  4. Construcción del bloque decodificador
  5. Combinación de las capas de codificación y descodificación para crear la red de transformadores completa

1. Importación de las bibliotecas y módulos necesarios

Empezaremos importando la biblioteca PyTorch para la funcionalidad básica, el módulo de redes neuronales para crear redes neuronales, el módulo de optimización para entrenar redes y las funciones de utilidad de datos para manejar datos. Además, importaremos el módulo math estándar de Python para operaciones matemáticas y el módulo copy para crear copias de objetos complejos.

Estas herramientas sientan las bases para definir la arquitectura del modelo, gestionar los datos y establecer el proceso de formación.

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import math
import copy

2. Definir los componentes básicos: Atención multicabeza, redes de alimentación en función de la posición, codificación posicional

Atención multicabezal

El mecanismo de atención multicabeza calcula la atención entre cada par de posiciones de una secuencia. Consta de múltiples "cabezas de atención" que captan diferentes aspectos de la secuencia de entrada.

Para saber más sobre la atención multicabezal, consulte la sección Mecanismos de atención del curso Conceptos de grandes modelos lingüísticos (LLM).

Figura 1. Atención multicabeza (fuente: imagen creada por el autor)

Figura 1. Atención multicabeza (fuente: imagen creada por el autor)

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        # Ensure that the model dimension (d_model) is divisible by the number of heads
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
        
        # Initialize dimensions
        self.d_model = d_model # Model's dimension
        self.num_heads = num_heads # Number of attention heads
        self.d_k = d_model // num_heads # Dimension of each head's key, query, and value
        
        # Linear layers for transforming inputs
        self.W_q = nn.Linear(d_model, d_model) # Query transformation
        self.W_k = nn.Linear(d_model, d_model) # Key transformation
        self.W_v = nn.Linear(d_model, d_model) # Value transformation
        self.W_o = nn.Linear(d_model, d_model) # Output transformation
        
    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        # Calculate attention scores
        attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        # Apply mask if provided (useful for preventing attention to certain parts like padding)
        if mask is not None:
            attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
        
        # Softmax is applied to obtain attention probabilities
        attn_probs = torch.softmax(attn_scores, dim=-1)
        
        # Multiply by values to obtain the final output
        output = torch.matmul(attn_probs, V)
        return output
        
    def split_heads(self, x):
        # Reshape the input to have num_heads for multi-head attention
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
        
    def combine_heads(self, x):
        # Combine the multiple heads back to original shape
        batch_size, _, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
        
    def forward(self, Q, K, V, mask=None):
        # Apply linear transformations and split heads
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))
        
        # Perform scaled dot-product attention
        attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
        
        # Combine heads and apply output transformation
        output = self.W_o(self.combine_heads(attn_output))
        return output

Definición e inicialización de clases:

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):

La clase se define como una subclase de nn.Module de PyTorch.

  1. d_model: Dimensionalidad de la entrada.
  2. num_heads: El número de cabezas de atención en las que dividir la entrada.

La inicialización comprueba si d_model es divisible por num_heads y, a continuación, define los pesos de transformación para la consulta, la clave, el valor y la salida.

Atención al producto de punto escalado:

def scaled_dot_product_attention(self, Q, K, V, mask=None):
  1. Cálculo de las puntuaciones de atención: attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k). Aquí, las puntuaciones de atención se calculan tomando el producto punto de las consultas (Q) y las claves (K), y luego escalando por la raíz cuadrada de la dimensión de la clave (d_k).
  2. Aplicar máscara: Si se proporciona una máscara, se aplica a las puntuaciones de atención para enmascarar valores específicos.
  3. Cálculo de los pesos de atención: Las puntuaciones de atención se pasan por una función softmax para convertirlas en probabilidades que sumen 1.
  4. Cálculo de la producción: El resultado final de la atención se calcula multiplicando los pesos de la atención por los valores (V).

Dividiendo cabezas:

def split_heads(self, x):

Este método remodela la entrada x en la forma (batch_size, num_heads, seq_length, d_k). Permite al modelo procesar varias cabezas de atención al mismo tiempo, lo que posibilita el cálculo en paralelo.

Combinando cabezas:

def combine_heads(self, x):

Tras aplicar la atención a cada cabeza por separado, este método vuelve a combinar los resultados en un único tensor de forma (batch_size, seq_length, d_model). Esto prepara el resultado para su posterior procesamiento.

Método Forward:

def forward(self, Q, K, V, mask=None):

El método de avance es donde se realiza el cálculo real:

  1. Aplicar transformaciones lineales: Las consultas (Q), las claves (K) y los valores (V) se someten primero a transformaciones lineales utilizando los pesos definidos en la inicialización.
  2. Cabezas partidas: Las transformadas Q, K, V se dividen en varias cabezas mediante el método split_heads.
  3. Aplique el producto de punto escalado Atención: Se llama al método scaled_dot_product_attention en los cabezales divididos.
  4. Combina cabezas: Los resultados de cada cabeza se combinan de nuevo en un único tensor utilizando el método combine_heads.
  5. Aplicar transformación de salida: Por último, el tensor combinado se pasa por una transformación lineal de salida.

En resumen, la clase MultiHeadAttention encapsula el mecanismo de atención multicabezal utilizado habitualmente en los modelos de transformador. Se encarga de dividir la entrada en varias cabezas de atención, aplicar la atención a cada cabeza y, a continuación, combinar los resultados. De este modo, el modelo puede captar diversas relaciones en los datos de entrada a diferentes escalas, lo que mejora la capacidad expresiva del modelo.

Redes Feed-Forward en función de la posición

class PositionWiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(PositionWiseFeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

Definición de la clase:

class PositionWiseFeedForward(nn.Module):

La clase es una subclase de nn.Module de PyTorch, lo que significa que heredará todas las funcionalidades necesarias para trabajar con capas de redes neuronales.

Inicialización:

def __init__(self, d_model, d_ff):
    super(PositionWiseFeedForward, self).__init__()
    self.fc1 = nn.Linear(d_model, d_ff)
    self.fc2 = nn.Linear(d_ff, d_model)
    self.relu = nn.ReLU()
  1. d_model: Dimensionalidad de la entrada y la salida del modelo.
  2. d_ff: Dimensionalidad de la capa interna en la red feed-forward.
  3. self.fc1 y self.fc2: Dos capas totalmente conectadas (lineales) con dimensiones de entrada y salida definidas por d_model y d_ff.
  4. self.relu: Función de activación ReLU (Rectified Linear Unit), que introduce no linealidad entre las dos capas lineales.

Método Forward:

def forward(self, x):
    return self.fc2(self.relu(self.fc1(x)))
  1. x: La entrada a la red feed-forward.
  2. self.fc1(x): La entrada pasa primero por la primera capa lineal (fc1).
  3. self.relu(...): A continuación, la salida de fc1 se pasa por una función de activación ReLU. ReLU sustituye todos los valores negativos por ceros, introduciendo la no linealidad en el modelo.
  4. self.fc2(...): A continuación, la salida activada pasa por la segunda capa lineal (fc2), que produce la salida final.

En resumen, la clase PositionWiseFeedForward define una red neuronal feed-forward que consiste en dos capas lineales con una función de activación ReLU entre ellas. En el contexto de los modelos de transformadores, esta red feed-forward se aplica a cada posición por separado y de forma idéntica. Ayuda a transformar los rasgos aprendidos por los mecanismos de atención dentro del transformador, actuando como un paso de procesamiento adicional para los resultados de la atención.

Codificación posicional

La codificación posicional se utiliza para inyectar la información de posición de cada token en la secuencia de entrada. Utiliza funciones seno y coseno de distintas frecuencias para generar la codificación posicional.

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_seq_length):
        super(PositionalEncoding, self).__init__()
        
        pe = torch.zeros(max_seq_length, d_model)
        position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
        
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        self.register_buffer('pe', pe.unsqueeze(0))
        
    def forward(self, x):
        return x + self.pe[:, :x.size(1)]

Definición de la clase:

class PositionalEncoding(nn.Module):

La clase se define como una subclase de nn.Module de PyTorch, lo que permite utilizarla como una capa estándar de PyTorch.

Inicialización:

def __init__(self, d_model, max_seq_length):
    super(PositionalEncoding, self).__init__()
    
    pe = torch.zeros(max_seq_length, d_model)
    position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
    div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
    
    pe[:, 0::2] = torch.sin(position * div_term)
    pe[:, 1::2] = torch.cos(position * div_term)
    
    self.register_buffer('pe', pe.unsqueeze(0))
  1. d_model: La dimensión de la entrada del modelo.
  2. max_seq_length: Longitud máxima de la secuencia para la que se precalculan las codificaciones posicionales.
  3. pe: Un tensor lleno de ceros, que se rellenará con codificaciones posicionales.
  4. posición: Un tensor que contiene los índices de posición para cada posición en la secuencia.
  5. div_term: Término utilizado para escalar los índices de posición de una manera específica.
  6. La función seno se aplica a los índices pares y la función coseno a los impares de pe.
  7. Por último, pe se registra como un buffer, lo que significa que formará parte del estado del módulo pero no se considerará un parámetro entrenable.

Método Forward:

def forward(self, x):
    return x + self.pe[:, :x.size(1)]

El método forward simplemente añade las codificaciones posicionales a la entrada x.

Utiliza los primeros elementos x.size(1) de pe para garantizar que las codificaciones posicionales coinciden con la longitud de secuencia real de x.

Resumen

La clase PositionalEncoding añade información sobre la posición de los tokens dentro de la secuencia. Dado que el modelo de transformador carece de conocimiento inherente del orden de las fichas (debido a su mecanismo de autoatención), esta clase ayuda al modelo a considerar la posición de las fichas en la secuencia. Las funciones sinusoidales utilizadas se eligen para que el modelo aprenda fácilmente a atender a posiciones relativas, ya que producen una codificación única y suave para cada posición de la secuencia.

3. Construcción de los bloques codificadores

Figura 2. Parte codificadora de la red de transformadores (Fuente: imagen del artículo original)

Figura 2. La parte codificadora de la red de transformadores (Fuente: imagen del artículo original)

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, mask):
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        return x

Definición de la clase:

class EncoderLayer(nn.Module):

La clase se define como una subclase de nn.Module de PyTorch, lo que significa que se puede utilizar como un bloque de construcción para redes neuronales en PyTorch.

Inicialización:

def __init__(self, d_model, num_heads, d_ff, dropout):
    super(EncoderLayer, self).__init__()
    self.self_attn = MultiHeadAttention(d_model, num_heads)
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.dropout = nn.Dropout(dropout)

Parámetros:

  1. d_model: La dimensionalidad de la entrada.
  2. num_heads: El número de cabezas de atención en la atención multicabeza.
  3. d_ff: La dimensionalidad de la capa interna en la red feed-forward de posición.
  4. abandono: La tasa de abandono utilizada para la regularización.

Componentes:

  1. self.self_attn: Mecanismo de atención multicabezal.
  2. self.feed_forward: Red neuronal feed-forward en función de la posición.
  3. auto.norma1 y auto.norma2: Normalización de la capa, aplicada para suavizar la entrada de la capa.
  4. self.dropout: Capa de abandono, utilizada para evitar el sobreajuste mediante el ajuste aleatorio de algunas activaciones a cero durante el entrenamiento.

Método Forward:

def forward(self, x, mask):
    attn_output = self.self_attn(x, x, x, mask)
    x = self.norm1(x + self.dropout(attn_output))
    ff_output = self.feed_forward(x)
    x = self.norm2(x + self.dropout(ff_output))
    return x

Entrada:

  1. x: La entrada a la capa codificadora.
  2. máscara: Máscara opcional para ignorar ciertas partes de la entrada.

Pasos del proceso:

  1. Autoatención: La entrada x pasa por el mecanismo de autoatención multicabezal.
  2. Añadir y Normalizar (después de Atención): La salida de la atención se añade a la entrada original (conexión residual), seguida de un abandono y normalización mediante norm1.
  3. Red Feed-Forward: La salida del paso anterior se pasa a través de la red feed-forward en función de la posición.
  4. Añadir y Normalizar (después de Feed-Forward): Al igual que en la etapa 2, la salida feed-forward se añade a la entrada de esta etapa (conexión residual), seguida de un dropout y una normalización mediante norm2.
  5. Salida: El tensor procesado se devuelve como salida de la capa codificadora.

Resumen:

La clase EncoderLayer define una única capa del codificador del transformador. Encierra un mecanismo de autoatención multicabezal seguido de una red neuronal de avance en función de la posición, con conexiones residuales, normalización de capas y abandono aplicados según proceda. El conjunto de estos componentes permite al codificador captar relaciones complejas en los datos de entrada y transformarlas en una representación útil para las tareas posteriores. Normalmente, se apilan varias capas codificadoras de este tipo para formar la parte codificadora completa de un modelo de transformador.

4. Construcción de los bloques decodificadores

Figura 3 La parte decodificadora de la red Transformer (Fuente: Imagen del artículo original)

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.cross_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, enc_output, src_mask, tgt_mask):
        attn_output = self.self_attn(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout(attn_output))
        attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
        x = self.norm2(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))
        return x

Definición de la clase:

class DecoderLayer(nn.Module):

Inicialización:

def __init__(self, d_model, num_heads, d_ff, dropout):
    super(DecoderLayer, self).__init__()
    self.self_attn = MultiHeadAttention(d_model, num_heads)
    self.cross_attn = MultiHeadAttention(d_model, num_heads)
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.norm3 = nn.LayerNorm(d_model)
    self.dropout = nn.Dropout(dropout)

Parámetros:

  1. d_model: La dimensionalidad de la entrada.
  2. num_heads: El número de cabezas de atención en la atención multicabeza.
  3. d_ff: La dimensionalidad de la capa interna en la red feed-forward.
  4. abandono: La tasa de abandono de la regularización.

Componentes:

  1. self.self_attn: Mecanismo de autoatención multicabezal para la secuencia objetivo.
  2. self.cross_attn: Mecanismo de atención multicabezal que atiende a la salida del codificador.
  3. self.feed_forward: Red neuronal feed-forward en función de la posición.
  4. self.norm1, self.norm2, self.norm3: Componentes de normalización de capas.
  5. self.dropout: Capa de abandono para la regularización.

MétodoForward:

ef forward(self, x, enc_output, src_mask, tgt_mask):
    attn_output = self.self_attn(x, x, x, tgt_mask)
    x = self.norm1(x + self.dropout(attn_output))
    attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
    x = self.norm2(x + self.dropout(attn_output))
    ff_output = self.feed_forward(x)
    x = self.norm3(x + self.dropout(ff_output))
    return x

Entrada:

  1. x: La entrada a la capa decodificadora.
  2. enc_output: La salida del codificador correspondiente (utilizada en el paso de atención cruzada).
  3. src_mask: Máscara de fuente para ignorar ciertas partes de la salida del codificador.
  4. tgt_mask: Máscara de destino para ignorar ciertas partes de la entrada del decodificador.

Pasos del proceso:

  1. Autoatención en la secuencia objetivo: La entrada x se procesa mediante un mecanismo de autoatención.
  2. Añadir y normalizar (después de la autoatención): El resultado de la autoatención se añade a la x original, seguido de la eliminación y la normalización mediante norm1.
  3. Atención cruzada con salida de codificador: La salida normalizada del paso anterior se procesa a través de un mecanismo de atención cruzada que atiende a la salida enc_output del codificador.
  4. Añadir y normalizar (después de la atención cruzada): La salida de la atención cruzada se añade a la entrada de esta etapa, seguida de la eliminación y normalización mediante norm2.
  5. Red Feed-Forward: La salida del paso anterior se pasa a través de la red feed-forward.
  6. Añadir y Normalizar (después de Feed-Forward): La salida feed-forward se añade a la entrada de esta etapa, seguida de dropout y normalización mediante norm3.
  7. Salida: El tensor procesado se devuelve como salida de la capa decodificadora.

Resumen:

La clase DecoderLayer define una única capa del decodificador del transformador. Consta de un mecanismo de autoatención multicabezal, un mecanismo de atención cruzada multicabezal (que atiende a la salida del codificador), una red neuronal feed-forward en función de la posición y las correspondientes conexiones residuales, normalización de capas y capas de abandono. Esta combinación permite al descodificador generar salidas significativas basadas en las representaciones del codificador, teniendo en cuenta tanto la secuencia de destino como la secuencia de origen. Al igual que ocurre con el codificador, se suelen apilar varias capas de decodificador para formar la parte decodificadora completa de un modelo de transformador.

A continuación, los bloques Codificador y Decodificador se unen para construir el modelo completo del Transformador.

5. Combinación de las capas de codificación y descodificación para crear la red de transformadores completa

Figura 4. Red de transformadores La red de transformadores (Fuente: Imagen del documento original)

Figura 4. La red de transformadores (Fuente: Imagen del documento original)

class Transformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
        super(Transformer, self).__init__()
        self.encoder_embedding = nn.Embedding(src_vocab_size, d_model)
        self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model)
        self.positional_encoding = PositionalEncoding(d_model, max_seq_length)

        self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
        self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])

        self.fc = nn.Linear(d_model, tgt_vocab_size)
        self.dropout = nn.Dropout(dropout)

    def generate_mask(self, src, tgt):
        src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
        tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(3)
        seq_length = tgt.size(1)
        nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
        tgt_mask = tgt_mask & nopeak_mask
        return src_mask, tgt_mask

    def forward(self, src, tgt):
        src_mask, tgt_mask = self.generate_mask(src, tgt)
        src_embedded = self.dropout(self.positional_encoding(self.encoder_embedding(src)))
        tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))

        enc_output = src_embedded
        for enc_layer in self.encoder_layers:
            enc_output = enc_layer(enc_output, src_mask)

        dec_output = tgt_embedded
        for dec_layer in self.decoder_layers:
            dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)

        output = self.fc(dec_output)
        return output

Definición de la clase:

class Transformer(nn.Module):

Inicialización:

def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):

El constructor recibe los siguientes parámetros:

  1. src_vocab_size: Tamaño del vocabulario fuente.
  2. tgt_vocab_size: Tamaño del vocabulario objetivo.
  3. d_model: La dimensionalidad de las incrustaciones del modelo.
  4. num_heads: Número de cabezas de atención en el mecanismo de atención multicabeza.
  5. num_layers: Número de capas tanto para el codificador como para el descodificador.
  6. d_ff: Dimensionalidad de la capa interna en la red feed-forward.
  7. max_seq_length: Longitud máxima de la secuencia para la codificación posicional.
  8. abandono: Tasa de abandono por regularización.

Y define los siguientes componentes:

  1. self.encoder_embedding: Capa de incrustación de la secuencia de origen.
  2. self.decoder_embedding: Capa de incrustación de la secuencia de destino.
  3. self.positional_encoding: Componente de codificación posicional.
  4. self.encoder_layers: Una lista de capas de codificación.
  5. self.decoder_layers: Una lista de capas de descodificación.
  6. self.fc: Asignación de la capa final totalmente conectada (lineal) al tamaño del vocabulario objetivo.
  7. self.dropout: Capa de abandono.

Generar método de máscara:

def generate_mask(self, src, tgt):

Este método se utiliza para crear máscaras para las secuencias de origen y destino, garantizando que se ignoren las fichas de relleno y que las fichas futuras no sean visibles durante el entrenamiento para la secuencia de destino.

Método Forward:

def forward(self, src, tgt):

Este método define el paso hacia adelante para el Transformador, tomando las secuencias de origen y destino y produciendo las predicciones de salida.

  1. Incrustación de entrada y codificación posicional: Las secuencias de origen y destino se incrustan primero utilizando sus respectivas capas de incrustación y luego se añaden a sus codificaciones posicionales.
  2. Capas del codificador: La secuencia fuente pasa por las capas del codificador, y la salida final del codificador representa la secuencia fuente procesada.
  3. Capas decodificadoras: La secuencia de destino y la salida del codificador pasan por las capas del descodificador, lo que da como resultado la salida del descodificador.
  4. Capa lineal final: La salida del descodificador se asigna al tamaño del vocabulario objetivo mediante una capa totalmente conectada (lineal).

Salida:

El resultado final es un tensor que representa las predicciones del modelo para la secuencia objetivo.

Resumen:

La clase Transformer reúne los distintos componentes de un modelo Transformer, incluyendo las incrustaciones, la codificación posicional, las capas codificadoras y las capas decodificadoras. Proporciona una interfaz cómoda para el entrenamiento y la inferencia, encapsulando las complejidades de la atención multicabezal, las redes feed-forward y la normalización de capas.

Esta implementación sigue la arquitectura estándar de Transformer, lo que la hace adecuada para tareas de secuencia a secuencia como la traducción automática, el resumen de textos, etc. La inclusión del enmascaramiento garantiza que el modelo se adhiere a las dependencias causales dentro de las secuencias, ignorando las fichas de relleno y evitando la fuga de información de las fichas futuras.

Estos pasos secuenciales permiten al modelo Transformer procesar eficazmente las secuencias de entrada y producir las secuencias de salida correspondientes.

Entrenamiento del modelo de transformador PyTorch

Preparación de los datos de las muestras

A efectos ilustrativos, en este ejemplo se creará un conjunto de datos ficticio. Sin embargo, en un escenario práctico, se emplearía un conjunto de datos más sustancial, y el proceso implicaría el preprocesamiento del texto junto con la creación de mapas de vocabulario tanto para la lengua de origen como para la de destino.

src_vocab_size = 5000
tgt_vocab_size = 5000
d_model = 512
num_heads = 8
num_layers = 6
d_ff = 2048
max_seq_length = 100
dropout = 0.1

transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

# Generate random sample data
src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)
tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)

Hiperparámetros:

Estos valores definen la arquitectura y el comportamiento del modelo de transformador:

  1. src_vocab_size, tgt_vocab_size: Tamaños de vocabulario para las secuencias de origen y destino, ambas fijadas en 5000.
  2. d_model: Dimensionalidad de las incrustaciones del modelo, establecida en 512.
  3. num_heads: Número de cabezales de atención en el mecanismo de atención multicabezal, fijado en 8.
  4. num_layers: Número de capas tanto para el codificador como para el descodificador, establecido en 6.
  5. d_ff: Dimensionalidad de la capa interna de la red feed-forward, fijada en 2048.
  6. max_seq_length: Longitud máxima de la secuencia para la codificación posicional, fijada en 100.
  7. abandono: Tasa de abandono para la regularización, fijada en 0,1.

Creación de una instancia de transformador:

transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

Esta línea crea una instancia de la clase Transformer, inicializándola con los hiperparámetros dados. La instancia tendrá la arquitectura y el comportamiento definidos por estos hiperparámetros.

Generación de datos de muestras aleatorias:

Las siguientes líneas generan secuencias aleatorias de origen y destino:

  1. src_data: Enteros aleatorios entre 1 y src_vocab_size, que representan un lote de secuencias fuente con forma (64, max_seq_length).
  2. tgt_data: Enteros aleatorios entre 1 y tgt_vocab_size, que representan un lote de secuencias objetivo con forma (64, max_seq_length).
  3. Estas secuencias aleatorias pueden utilizarse como entradas del modelo de transformador, simulando un lote de datos con 64 ejemplos y secuencias de longitud 100.

Resumen:

El fragmento de código muestra cómo inicializar un modelo de transformador y generar secuencias aleatorias de origen y destino que pueden introducirse en el modelo. Los hiperparámetros elegidos determinan la estructura y las propiedades específicas del transformador. Esta configuración podría formar parte de un guión más amplio en el que el modelo se entrenara y evaluara en tareas reales de secuencia a secuencia, como la traducción automática o el resumen de textos.

Entrenamiento del modelo

A continuación, se entrenará el modelo utilizando los datos de muestra antes mencionados. Sin embargo, en un escenario real, se emplearía un conjunto de datos significativamente mayor, que normalmente se dividiría en conjuntos distintos con fines de entrenamiento y validación.

criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

transformer.train()

for epoch in range(100):
    optimizer.zero_grad()
    output = transformer(src_data, tgt_data[:, :-1])
    loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, 1:].contiguous().view(-1))
    loss.backward()
    optimizer.step()
    print(f"Epoch: {epoch+1}, Loss: {loss.item()}")

Función de pérdida y optimizador:

  1. criterion = nn.CrossEntropyLoss(ignore_index=0): Define la función de pérdida como pérdida de entropía cruzada. El argumento ignore_index se establece en 0, lo que significa que la pérdida no considerará objetivos con un índice de 0 (normalmente reservado para tokens de relleno).
  2. optimizer = optim.Adam(...): Define el optimizador como Adam con una tasa de aprendizaje de 0,0001 y valores beta específicos.

Modo de entrenamiento de modelos:

  1. transformer.train(): Establece el modelo de transformador en modo de entrenamiento, lo que permite comportamientos como el abandono que sólo se aplican durante el entrenamiento.

Bucle de formación:

El fragmento de código entrena el modelo durante 100 épocas utilizando un bucle de entrenamiento típico:

  1. para epoch en range(100): Itera más de 100 épocas de entrenamiento.
  2. optimizer.zero_grad(): Borra los gradientes de la iteración anterior.
  3. output = transformer(src_data, tgt_data[:, :-1]): Pasa los datos de origen y los datos de destino (excluyendo el último token de cada secuencia) a través del transformador. Esto es habitual en tareas de secuencia a secuencia en las que el objetivo se desplaza un token.
  4. pérdida = criterio(...): Calcula la pérdida entre las predicciones del modelo y los datos objetivo (excluyendo el primer token de cada secuencia). La pérdida se calcula transformando los datos en tensores unidimensionales y utilizando la función de pérdida de entropía cruzada.
  5. loss.backward(): Calcula los gradientes de la pérdida con respecto a los parámetros del modelo.
  6. optimizer.step(): Actualiza los parámetros del modelo utilizando los gradientes calculados.
  7. print(f "Época: {época+1}, Pérdida: {pérdida.elemento()}"): Imprime el número de época actual y el valor de pérdida de esa época.

Resumen:

Este fragmento de código entrena el modelo transformador en secuencias de origen y destino generadas aleatoriamente durante 100 épocas. Utiliza el optimizador Adam y la función de pérdida de entropía cruzada. La pérdida se imprime para cada época, lo que le permite controlar el progreso del entrenamiento. En un escenario real, sustituiría las secuencias aleatorias de origen y destino por datos reales de su tarea, como la traducción automática.

Evaluación del rendimiento del modelo de transformador

Una vez entrenado el modelo, su rendimiento puede evaluarse en un conjunto de datos de validación o de prueba. A continuación se ofrece un ejemplo de cómo podría hacerse:

transformer.eval()

# Generate random sample validation data
val_src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)
val_tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)

with torch.no_grad():

    val_output = transformer(val_src_data, val_tgt_data[:, :-1])
    val_loss = criterion(val_output.contiguous().view(-1, tgt_vocab_size), val_tgt_data[:, 1:].contiguous().view(-1))
    print(f"Validation Loss: {val_loss.item()}")

Modo de evaluación:

  1. transformer.eval(): Pone el modelo de transformador en modo de evaluación. Esto es importante porque desactiva ciertos comportamientos como el abandono que sólo se utilizan durante el entrenamiento.

Generar datos de validación aleatorios:

  1. val_src_data: Enteros aleatorios entre 1 y src_vocab_size, que representan un lote de secuencias fuente de validación con forma (64, max_seq_length).
  2. val_tgt_data: Enteros aleatorios entre 1 y tgt_vocab_size, que representan un lote de secuencias objetivo de validación con forma (64, max_seq_length).

Bucle de validación:

  1. with torch.no_grad(): Desactiva el cálculo de gradientes, ya que no necesitamos calcular gradientes durante la validación. Esto puede reducir el consumo de memoria y acelerar los cálculos.
  2. val_output = transformer(val_src_data, val_tgt_data[:, :-1]): Pasa los datos de origen de validación y los datos de destino de validación (excluyendo el último token de cada secuencia) a través del transformador.
  3. val_loss = criterion(...): Calcula la pérdida entre las predicciones del modelo y los datos objetivo de validación (excluyendo el primer token de cada secuencia). La pérdida se calcula remodelando los datos en tensores unidimensionales y utilizando la función de pérdida de entropía cruzada previamente definida.
  4. print(f "Pérdida de validación: {val_loss.item()}"): Imprime el valor de la pérdida de validación.

Resumen:

Este fragmento de código evalúa el modelo transformador en un conjunto de datos de validación generado aleatoriamente, calcula la pérdida de validación y la imprime. En un escenario real, los datos de validación aleatorios deberían sustituirse por datos de validación reales de la tarea en la que se está trabajando. La pérdida de validación puede darle una indicación del rendimiento de su modelo en datos no vistos, que es una medida crítica de la capacidad de generalización del modelo.

Para más detalles sobre Transformers y Cara de abrazo, es útil nuestro tutorial Introducción al uso de Transformers y Cara de abrazo.

Conclusión y otros recursos

En conclusión, este tutorial demostró cómo construir un modelo Transformer utilizando PyTorch, una de las herramientas más versátiles para el aprendizaje profundo. Gracias a su capacidad de paralelización y a la posibilidad de capturar dependencias a largo plazo en los datos, los transformadores tienen un inmenso potencial en diversos campos, especialmente en tareas de PLN como la traducción, el resumen y el análisis de sentimientos.

Aquellos que deseen profundizar en los conceptos y técnicas del aprendizaje profundo avanzado pueden explorar el curso Advanced Deep Learning with Keras en DataCamp. También puedes leer sobre la construcción de una red neuronal sencilla con PyTorch en otro tutorial.

Temas
Relacionado

blog

Todo lo que sabemos sobre GPT-5

Predecir cómo podría ser la próxima evolución de la tecnología de IA de OpenAI y qué avances podría tener el modelo GPT-5.
Josep Ferrer's photo

Josep Ferrer

10 min

blog

Los 16 mejores marcos y bibliotecas de IA: Guía para principiantes

Explore los mejores marcos y bibliotecas de IA y sus fundamentos en esta guía definitiva para profesionales de datos noveles que comienzan su carrera profesional.
Yuliya Melnik's photo

Yuliya Melnik

15 min

blog

¿Qué es la tokenización?

La tokenización divide el texto en partes más pequeñas para facilitar el análisis mecánico y ayudar a las máquinas a comprender el lenguaje humano.
Abid Ali Awan's photo

Abid Ali Awan

9 min

tutorial

Primeros pasos con Claude 3 y la API de Claude 3

Conozca los modelos Claude 3, las pruebas de rendimiento detalladas y cómo acceder a ellas. Además, descubra la nueva API Python de Claude 3 para generar texto, acceder a funciones de visión y streaming.
Abid Ali Awan's photo

Abid Ali Awan

tutorial

Guía introductoria para perfeccionar los LLM

La puesta a punto de grandes modelos lingüísticos (LLM) ha revolucionado el Procesamiento del Lenguaje Natural (PLN), ofreciendo capacidades sin precedentes en tareas como la traducción de idiomas, el análisis de sentimientos y la generación de textos. Este enfoque transformador aprovecha modelos preentrenados como GPT-2, mejorando su rendimiento en dominios específicos mediante el proceso de ajuste fino.
Josep Ferrer's photo

Josep Ferrer

12 min

tutorial

Cómo recortar una cadena en Python: Tres métodos diferentes

Aprenda los fundamentos del recorte de caracteres iniciales y finales de una cadena en Python.
Adel Nehme's photo

Adel Nehme

5 min

See MoreSee More