🤖 Unidad 4. Arquitectura Transformer
Los Transformers (Vaswani et al., 2017) revolucionaron el campo del Deep Learning al introducir el mecanismo de atención como único componente para modelar secuencias, eliminando la necesidad de recurrencia.
4.1. Motivación: Limitaciones de las RNN
| Limitación RNN | Solución Transformer |
|---|---|
| Procesamiento secuencial (no paralelizable) | Procesamiento paralelo completo |
| Dificultad con dependencias largas | Atención directa entre cualquier par de posiciones |
| Gradientes que se desvanecen | Conexiones directas mediante atención |
4.2. Mecanismo de Atención
¿Qué es la Atención?
La atención permite que cada elemento de una secuencia "preste atención" a todos los demás elementos, ponderando su importancia.
Intuición:
Frase: "El gato negro que vive en mi casa está durmiendo en el sofá"
↑ ↑
"está" presta más atención a "gato" que a "casa"
Scaled Dot-Product Attention
La atención se calcula usando tres vectores para cada elemento:
- Query (Q): Lo que estamos buscando.
- Key (K): Etiqueta de cada elemento.
- Value (V): El contenido real.
Donde \(d_k\) es la dimensión de las keys (para estabilidad numérica).
import numpy as np
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Calcula la atención escalada por producto punto.
Q: queries (batch, seq_len, d_k)
K: keys (batch, seq_len, d_k)
V: values (batch, seq_len, d_v)
"""
d_k = Q.shape[-1]
# Calcular scores
scores = np.matmul(Q, K.transpose(-2, -1)) / np.sqrt(d_k)
# Aplicar máscara si existe
if mask is not None:
scores = scores + (mask * -1e9)
# Softmax para obtener pesos de atención
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# Multiplicar por valores
output = np.matmul(attention_weights, V)
return output, attention_weights
Multi-Head Attention
En lugar de una sola atención, usamos múltiples "cabezas" que aprenden diferentes tipos de relaciones:
Donde cada cabeza es: $\(\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)\)$
import tensorflow as tf
from tensorflow.keras.layers import Layer
class MultiHeadAttention(Layer):
def __init__(self, d_model, num_heads):
super().__init__()
self.num_heads = num_heads
self.d_model = d_model
self.depth = d_model // num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
"""Divide la última dimensión en (num_heads, depth)."""
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q, mask=None):
batch_size = tf.shape(q)[0]
q = self.wq(q)
k = self.wk(k)
v = self.wv(v)
q = self.split_heads(q, batch_size)
k = self.split_heads(k, batch_size)
v = self.split_heads(v, batch_size)
# Scaled dot-product attention
scaled_attention = tf.matmul(q, k, transpose_b=True)
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention = scaled_attention / tf.math.sqrt(dk)
if mask is not None:
scaled_attention += (mask * -1e9)
attention_weights = tf.nn.softmax(scaled_attention, axis=-1)
output = tf.matmul(attention_weights, v)
output = tf.transpose(output, perm=[0, 2, 1, 3])
output = tf.reshape(output, (batch_size, -1, self.d_model))
return self.dense(output)
4.3. Arquitectura Completa del Transformer
Diagrama General
┌─────────────────────────────────────┐
│ DECODER │
│ ┌───────────────────────────────┐ │
│ │ Linear + Softmax → Output │ │
│ └───────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────┐ │
│ │ Multi-Head Attention │ │ ← Cross-attention
│ │ (queries del decoder, │ │ (conecta encoder
│ │ keys/values del encoder) │ │ y decoder)
│ └───────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────┐ │
│ │ Masked Multi-Head │ │
│ │ Self-Attention │ │
│ └───────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────┐ │
│ │ Output Embedding + │ │
│ │ Positional Encoding │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ ENCODER │
│ ┌───────────────────────────────┐ │
│ │ Feed Forward Neural Net │ │
│ └───────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────┐ │
│ │ Multi-Head Self-Attention │ │
│ └───────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────┐ │
│ │ Input Embedding + │ │
│ │ Positional Encoding │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↑
Input Tokens
4.4. Componentes del Transformer
Positional Encoding
Como no hay recurrencia, necesitamos inyectar información de posición:
def positional_encoding(position, d_model):
"""Genera positional encodings."""
angle_rads = get_angles(
np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model
)
# Seno a índices pares
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
# Coseno a índices impares
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
pos_encoding = angle_rads[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
def get_angles(pos, i, d_model):
angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))
return pos * angle_rates
Feed-Forward Network
Después de la atención, cada posición pasa por una red feed-forward:
def point_wise_feed_forward_network(d_model, dff):
return tf.keras.Sequential([
tf.keras.layers.Dense(dff, activation='relu'),
tf.keras.layers.Dense(d_model)
])
Layer Normalization y Residual Connections
Cada sublayer tiene:
- Residual Connection: \(\text{output} = x + \text{Sublayer}(x)\)
- Layer Normalization: Normaliza las activaciones.
class EncoderLayer(Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
def call(self, x, training, mask):
# Multi-head attention + residual + norm
attn_output = self.mha(x, x, x, mask)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(x + attn_output)
# Feed-forward + residual + norm
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
out2 = self.layernorm2(out1 + ffn_output)
return out2
4.5. Self-Attention vs Cross-Attention
Self-Attention
Q, K, V vienen de la misma secuencia:
Cross-Attention
Q viene del decoder, K y V del encoder:
# En el decoder (después del self-attention)
output = cross_attention(
q=decoder_output,
k=encoder_output,
v=encoder_output
)
Masked Self-Attention
En el decoder, cada posición solo puede atender a posiciones anteriores (para generación autoregresiva):
def create_look_ahead_mask(size):
"""Máscara triangular para evitar mirar el futuro."""
mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
return mask # (size, size)
4.6. Implementación Completa de un Transformer
import tensorflow as tf
from tensorflow.keras.layers import Layer, Embedding, Dense, Dropout, LayerNormalization
class Transformer(tf.keras.Model):
def __init__(self, num_layers, d_model, num_heads, dff,
input_vocab_size, target_vocab_size,
pe_input, pe_target, rate=0.1):
super().__init__()
self.encoder = Encoder(num_layers, d_model, num_heads, dff,
input_vocab_size, pe_input, rate)
self.decoder = Decoder(num_layers, d_model, num_heads, dff,
target_vocab_size, pe_target, rate)
self.final_layer = Dense(target_vocab_size)
def call(self, inputs, training):
inp, tar = inputs
enc_padding_mask = create_padding_mask(inp)
dec_padding_mask = create_padding_mask(inp)
look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
dec_target_padding_mask = create_padding_mask(tar)
combined_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)
enc_output = self.encoder(inp, training, enc_padding_mask)
dec_output = self.decoder(tar, enc_output, training,
combined_mask, dec_padding_mask)
final_output = self.final_layer(dec_output)
return final_output
class Encoder(Layer):
def __init__(self, num_layers, d_model, num_heads, dff,
vocab_size, maximum_position_encoding, rate=0.1):
super().__init__()
self.d_model = d_model
self.embedding = Embedding(vocab_size, d_model)
self.pos_encoding = positional_encoding(maximum_position_encoding, d_model)
self.enc_layers = [EncoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]
self.dropout = Dropout(rate)
def call(self, x, training, mask):
seq_len = tf.shape(x)[1]
x = self.embedding(x)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
for enc_layer in self.enc_layers:
x = enc_layer(x, training, mask)
return x
class Decoder(Layer):
def __init__(self, num_layers, d_model, num_heads, dff,
vocab_size, maximum_position_encoding, rate=0.1):
super().__init__()
self.d_model = d_model
self.embedding = Embedding(vocab_size, d_model)
self.pos_encoding = positional_encoding(maximum_position_encoding, d_model)
self.dec_layers = [DecoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]
self.dropout = Dropout(rate)
def call(self, x, enc_output, training, look_ahead_mask, padding_mask):
seq_len = tf.shape(x)[1]
x = self.embedding(x)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
for dec_layer in self.dec_layers:
x = dec_layer(x, enc_output, training, look_ahead_mask, padding_mask)
return x
4.7. Variantes de Transformers
Encoder-Only (BERT)
Solo usa el encoder. Ideal para:
- Clasificación de texto.
- NER.
- Question Answering extractivo.
# Usando Hugging Face
from transformers import BertModel, BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
inputs = tokenizer("Hello, how are you?", return_tensors="pt")
outputs = model(**inputs)
# outputs.last_hidden_state: (batch, seq_len, hidden_size)
Decoder-Only (GPT)
Solo usa el decoder con masked self-attention. Ideal para:
- Generación de texto.
- Completar oraciones.
- Modelos de lenguaje.
from transformers import GPT2LMHeadModel, GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')
input_ids = tokenizer.encode("Once upon a time", return_tensors='pt')
outputs = model.generate(input_ids, max_length=50, num_return_sequences=1)
print(tokenizer.decode(outputs[0]))
Encoder-Decoder (T5, BART)
Arquitectura completa. Ideal para:
- Traducción.
- Resumen.
- Question Answering generativo.
from transformers import T5Tokenizer, T5ForConditionalGeneration
tokenizer = T5Tokenizer.from_pretrained('t5-small')
model = T5ForConditionalGeneration.from_pretrained('t5-small')
# Traducción
input_text = "translate English to German: Hello, how are you?"
input_ids = tokenizer(input_text, return_tensors="pt").input_ids
outputs = model.generate(input_ids)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
4.8. Ejemplo Práctico: Fine-tuning de BERT
from transformers import (
BertTokenizer,
TFBertForSequenceClassification,
create_optimizer
)
import tensorflow as tf
# Cargar modelo y tokenizer
model_name = 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = TFBertForSequenceClassification.from_pretrained(model_name, num_labels=2)
# Datos de ejemplo
textos = [
"I love this movie!",
"This film was terrible.",
"Great acting and plot.",
"Waste of time."
]
etiquetas = [1, 0, 1, 0] # 1=positivo, 0=negativo
# Tokenizar
encodings = tokenizer(
textos,
truncation=True,
padding=True,
max_length=128,
return_tensors='tf'
)
# Crear dataset
dataset = tf.data.Dataset.from_tensor_slices((
dict(encodings),
etiquetas
)).batch(2)
# Configurar optimizador
batch_size = 2
num_train_steps = len(textos) // batch_size * 3 # 3 épocas
optimizer, schedule = create_optimizer(
init_lr=2e-5,
num_train_steps=num_train_steps,
num_warmup_steps=0
)
# Compilar
model.compile(
optimizer=optimizer,
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
# Entrenar
model.fit(dataset, epochs=3)
# Predecir
nuevo_texto = "Amazing film, highly recommended!"
inputs = tokenizer(nuevo_texto, return_tensors='tf', truncation=True, padding=True)
outputs = model(inputs)
prediccion = tf.nn.softmax(outputs.logits, axis=-1)
print(f"Probabilidades: {prediccion.numpy()}")
4.9. Comparación de Arquitecturas
| Característica | RNN/LSTM | Transformer |
|---|---|---|
| Paralelización | Secuencial | Total |
| Dependencias largas | Difícil | Fácil (atención directa) |
| Complejidad computacional | \(O(n)\) | \(O(n^2)\) |
| Memoria | \(O(1)\) por paso | \(O(n^2)\) para atención |
| Preentrenamiento | Limitado | Muy efectivo |
4.10. Tendencias Actuales
Efficient Transformers
Para reducir la complejidad \(O(n^2)\):
- Sparse Attention: Solo atiende a un subconjunto de posiciones.
- Linear Attention: Aproximaciones lineales.
- Longformer, BigBird: Para secuencias muy largas.
Large Language Models (LLMs)
- GPT-4, Claude, Llama: Miles de millones de parámetros.
- Instruction Tuning: Afinados para seguir instrucciones.
- RLHF: Entrenamiento con feedback humano.
📅 Fecha de creación: Enero 2026
✍️ Autor: Fran García