1. Redes neuronales artificiales: de la neurona biológica a la función matemática
1.1 La unidad mínima de una red neuronal
Una red neuronal artificial se inspira en cómo funcionan las neuronas biológicas. Una sola neurona es, en esencia, una función matemática:
output = activation(w · x + b) Aquí x es el vector de entrada, w es el peso, b es el sesgo, y activation es una función no lineal (comúnmente Sigmoid, ReLU o GELU). Apila miles de estas «neuronas» en capas y conéctalas y tendrás la base del deep learning moderno.
1.2 Los tres elementos del entrenamiento
- Propagación hacia adelante (Forward propagation): los datos fluyen de la capa de entrada a la capa de salida, produciendo una predicción.
- Función de pérdida (Loss function): mide la brecha entre la predicción y la verdad de referencia. La clasificación usa entropía cruzada; la regresión usa el error cuadrático medio.
- Propagación hacia atrás + descenso de gradiente (Backpropagation + gradient descent): partiendo de la salida, se calcula la «contribución» (gradiente) de cada parámetro a la pérdida, y luego se empuja el peso en la dirección opuesta para que la siguiente predicción sea más precisa.
💡 Intuición clave: entrenar una red neuronal es esencialmente «buscar en un espacio de parámetros de miles de millones de dimensiones una configuración de pesos que minimice la pérdida». Por eso los LLM necesitan enormes cantidades de datos y cómputo — el espacio de parámetros es enorme.
2. Transformer: la piedra angular de los LLM modernos
En 2017, Google publicó Attention Is All You Need, introduciendo la arquitectura Transformer y cambiando la trayectoria de la IA. Su innovación central: eliminar por completo la recurrencia y depender puramente del mecanismo de atención para procesar secuencias.
2.1 Por qué el Transformer arrasó en NLP
| Propiedad | RNN / LSTM | Transformer |
|---|---|---|
| Dependencias de largo alcance | Tiende a olvidar el contexto temprano | Conexión directa entre dos posiciones cualesquiera |
| Cómputo en paralelo | Estrictamente secuencial | Procesa todas las posiciones a la vez |
| Velocidad de entrenamiento | Lenta | Rápida (uso completo de GPU) |
| Escalabilidad | Difícil escalar más allá de 10B parámetros | Soporte nativo para 100B+ parámetros |
2.2 Self-attention
La idea central: para cada palabra de una secuencia, «mira» a cada otra palabra y decide cuánto importa cada una para ella.
Toma esta frase de ejemplo — «Xiaoming fue ayer de viaje de negocios a Pekín, y él volvió hoy»:
- Cada palabra produce tres vectores: Query (Q), Key (K), Value (V).
- Para «él», calcula el producto punto de su Q con el K de cada otra palabra para obtener una puntuación de relevancia.
- Aplica Softmax para obtener los pesos de atención (p. ej., 0,8 para «Xiaoming», 0,1 para «Pekín»).
- Usa esos pesos para tomar una suma ponderada del V de cada palabra, produciendo una representación nueva, rica en contexto, de «él».
En forma matemática:
Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V donde d_k es la dimensionalidad del vector Key; dividir por su raíz cuadrada evita que el producto punto haga explotar el gradiente del Softmax.
2.3 Atención multi-cabezal (Multi-head attention)
La atención multi-cabezal es self-attention con esteroides. Ejecuta múltiples operaciones de self-attention en paralelo a través de diferentes «subespacios de representación» y concatena los resultados. Esto permite al modelo capturar diferentes tipos de relaciones a la vez:
- Una cabeza puede centrarse en relaciones sintácticas (sujeto-verbo-objeto).
- Otra en relaciones semánticas (sinónimos, antónimos).
- Una tercera en correferencia de largo alcance (a qué sustantivo se refiere un pronombre).
2.4 Codificación posicional (Positional encoding)
El Transformer procesa cada palabra en paralelo, por lo que pierde por defecto la información de «orden». «El perro muerde al hombre» y «El hombre muerde al perro» tienen tokens idénticos — solo difiere el orden. La codificación posicional añade un «vector de posición» único al embedding de cada palabra para que el modelo sepa dónde se sitúa en la secuencia.
El Transformer original usó codificaciones posicionales sinusoidales/coseno fijas. Modelos posteriores (LLaMA, Qwen) usan RoPE (Rotary Position Embeddings), que maneja secuencias mucho más largas.
3. La arquitectura completa del Transformer
Un bloque Transformer estándar tiene dos mitades:
input → [LayerNorm → Multi-Head Attention → Residual]
→ [LayerNorm → Feed-Forward Network (FFN) → Residual]
→ output - Multi-Head Attention: encuentra «relaciones» — permite que cada palabra vea a cada otra palabra.
- Feed-Forward Network (FFN): hace el «procesamiento» — aplica una transformación no lineal a cada palabra individualmente para extraer características de nivel superior.
- Conexión residual: añade la entrada directamente a la salida, evitando gradientes que se desvanecen y permitiendo redes profundas.
- LayerNorm: estabiliza el entrenamiento y previene explosiones numéricas.
La familia GPT (ChatGPT, DeepSeek, Qwen) son simplemente decenas o cientos de tales bloques apilados en un Transformer Decoder-only.
4. Pre-entrenamiento, SFT, RLHF: la receta de entrenamiento en tres pasos
4.1 Pre-entrenamiento
Entrena en un vasto corpus de texto de internet (billones de tokens) para que el modelo aprenda a «predecir el siguiente token». Este es el paso más largo y costoso — DeepSeek-V3 usó 14,8T tokens de datos de entrenamiento y costó aproximadamente 5,57M USD (alquiler de GPU).
4.2 Ajuste fino supervisado (SFT)
Entrena en un pequeño conjunto de pares «instrucción-respuesta» curados por humanos y de alta calidad (decenas de miles) para que el modelo aprenda a «entender la solicitud y responder en consecuencia». El volumen es muy inferior al del pre-entrenamiento, pero los requisitos de calidad son extremos.
4.3 Aprendizaje por refuerzo a partir de feedback humano (RLHF)
Los humanos puntúan las diferentes respuestas del modelo; se entrena un modelo de recompensa con esas clasificaciones, y luego se usa RL (típicamente PPO o DPO) para empujar al modelo hacia respuestas mejor clasificadas.
5. ¿Por qué necesitamos clusters de GPU?
Entrenar un modelo de 100B parámetros — un solo pase de forward + backward requiere:
- Embeddings del token de entrada (params × longitud de secuencia)
- Matrices de atención (longitud de secuencia² × número de cabezas)
- Gradientes (params)
- Estado del optimizador (momentum, varianza — 3-4× params)
Nada de esto cabe en una sola GPU (80GB), por lo que necesitamos múltiples estrategias paralelas:
| Paralelismo | Idea | Framework representativo |
|---|---|---|
| Datos paralelo | Divide los datos entre GPUs; cada una ejecuta el modelo completo | PyTorch DDP |
| Modelo paralelo | Ubica diferentes capas en diferentes GPUs | Megatron-LM |
| Pipeline paralelo | Divide el modelo en etapas; cada GPU procesa un micro-batch diferente | GPipe, PipeDream |
| Tensor paralelo | Divide una sola operación matricial entre GPUs | Megatron-LM |
6. De los LLM a los Agents: el salto en capacidad
Una vez que los LLM se volvieron lo suficientemente fuertes, la gente empezó a añadir «extras»: el modelo no solo puede «hablar» sino también «hacer». Este es el punto de partida del AI Agent:
- ReAct (razonar + actuar): el modelo primero piensa qué hacer y luego decide qué herramienta llamar.
- Llamada a funciones (Function calling): el modelo emite «solicitudes de llamada a herramientas» estructuradas ejecutadas por un sistema externo.
- Colaboración multi-Agent: múltiples Agents juegan diferentes roles y se coordinan para completar una tarea.
7. Resumen del capítulo
- ✅ El Transformer reemplazó la recurrencia por el mecanismo de atención, permitiendo entrenamiento paralelo y modelado de largo alcance.
- ✅ El self-attention permite que cada palabra «vea» a cada otra; la atención multi-cabezal captura múltiples relaciones a la vez.
- ✅ Pre-entrenamiento + SFT + RLHF es la receta de tres pasos para entrenar un LLM.
- ✅ Los modelos de 100B parámetros exigen clusters de GPU y una combinación de estrategias de paralelismo.
8. Práctica: construye un Mini Transformer desde cero
Ahora que la teoría está cubierta, esta sección te guía en la construcción de un Transformer Decoder-only de 6 capas en PyTorch desde cero, entrenado en un pequeño corpus de texto. El código completo tiene alrededor de 250 líneas y se ejecuta en CPU o GPU — el objetivo es hacer que cada engranaje del Transformer sea visible para ti.
8.1 Objetivos y entorno
- Implementar un Transformer Decoder-only de 6 capas con ~1,2M parámetros.
- Entrenar un modelo de lenguaje a nivel de carácter en un pequeño corpus de ~500 líneas y observar cómo cae la pérdida.
- Proporcionar dos scripts ejecutables:
train.pyysample.py.
Configuración del entorno (2 líneas):
pip install torch numpy
python -c "import torch; print('torch', torch.__version__)" 8.2 Preparación de datos
Usaremos un pequeño fragmento de datos pseudo estilo poesía Tang (puedes reemplazarlo con tu propio input.txt):
cat > data/input.txt << 'EOF'
Quiet night thoughts
Before my bed a pool of light,
Can it be frost upon the ground?
Eyes up, I find the moon so bright,
Head down, I think of home I've found.
Spring dawn
Spring slumber knows no dawn so soon,
The birds' song fills the air with tune.
Last night the wind and rain did blow,
How many flowers have let go?
EOF
wc -c data/input.txt Un modelo a nivel de carácter simplemente lee el archivo completo como una cadena larga — no se necesita tokenizador.
8.3 Código completo: train.py
"""Mini Transformer: 6 capas Decoder-only, modelo de lenguaje a nivel de carácter.
Uso:
python train.py
"""
import math
import time
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------- 1. Data loader ----------
class CharDataset:
"""Corta un fichero de texto en muestras de entrenamiento de longitud (block_size)."""
def __init__(self, path: str, block_size: int = 64):
self.text = Path(path).read_text(encoding="utf-8")
self.chars = sorted(set(self.text))
self.vocab_size = len(self.chars)
self.stoi = {c: i for i, c in enumerate(self.chars)}
self.itos = {i: c for i, c in enumerate(self.chars)}
self.block_size = block_size
self.data = torch.tensor([self.stoi[c] for c in self.text], dtype=torch.long)
print(f"[data] chars={self.vocab_size} len={len(self.data)}")
def get_batch(self, batch_size: int = 32):
ix = torch.randint(0, len(self.data) - self.block_size - 1, (batch_size,))
x = torch.stack([self.data[i : i + self.block_size] for i in ix])
y = torch.stack([self.data[i + 1 : i + 1 + self.block_size] for i in ix])
return x, y
# ---------- 2. Componentes del modelo ----------
class PositionalEncoding(nn.Module):
"""Codificación posicional sinusoidal, congelada."""
def __init__(self, d_model: int, max_len: int = 512):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
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, max_len, d_model)
def forward(self, x):
return x + self.pe[:, : x.size(1)]
class CausalSelfAttention(nn.Module):
"""Atención multi-cabezal causal (usada en el Decoder)."""
def __init__(self, d_model: int, n_heads: int, max_len: int = 512, dropout: float = 0.1):
super().__init__()
assert d_model % n_heads == 0
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model)
self.proj = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
# máscara causal: triángulo superior (excluyendo diagonal) es -inf
mask = torch.triu(torch.ones(max_len, max_len), diagonal=1).bool()
self.register_buffer("mask", mask)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # (B, h, T, head_dim)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
att = att.masked_fill(self.mask[:T, :T], float("-inf"))
att = F.softmax(att, dim=-1)
att = self.dropout(att)
out = (att @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class FeedForward(nn.Module):
"""FFN del Transformer: GELU + Linear + Dropout."""
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Block(nn.Module):
"""Bloque Decoder Pre-LN: Attention -> FFN, cada uno con residual."""
def __init__(self, d_model: int, n_heads: int, d_ff: int, max_len: int, dropout: float):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads, max_len, dropout)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = FeedForward(d_model, d_ff, dropout)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ffn(self.ln2(x))
return x
class MiniTransformer(nn.Module):
"""Transformer Decoder-only de 6 capas, modelo de lenguaje a nivel de carácter."""
def __init__(
self,
vocab_size: int,
d_model: int = 128,
n_heads: int = 4,
d_ff: int = 512,
n_layers: int = 6,
max_len: int = 256,
dropout: float = 0.1,
):
super().__init__()
self.tok_emb = nn.Embedding(vocab_size, d_model)
self.pos_enc = PositionalEncoding(d_model, max_len)
self.blocks = nn.ModuleList(
[Block(d_model, n_heads, d_ff, max_len, dropout) for _ in range(n_layers)]
)
self.ln_f = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, vocab_size, bias=False)
# Weight tying: el embedding de entrada comparte pesos con la cabeza de salida
self.head.weight = self.tok_emb.weight
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Embedding):
nn.init.normal_(m.weight, std=0.02)
def forward(self, idx, targets=None):
x = self.tok_emb(idx)
x = self.pos_enc(x)
for blk in self.blocks:
x = blk(x)
x = self.ln_f(x)
logits = self.head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens: int = 100, temperature: float = 1.0, top_k: int = 20):
"""Generación autorregresiva con temperatura + muestreo top-k."""
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.pos_enc.pe.size(1) :]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
if top_k:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1)
nxt = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, nxt], dim=1)
return idx
# ---------- 3. Bucle de entrenamiento ----------
def main():
torch.manual_seed(0)
ds = CharDataset("data/input.txt", block_size=64)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = MiniTransformer(vocab_size=ds.vocab_size).to(device)
print(f"[model] params={sum(p.numel() for p in model.parameters()) / 1e6:.2f}M device={device}")
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
steps = 1000
batch_size = 32
t0 = time.time()
for step in range(steps):
x, y = ds.get_batch(batch_size)
x, y = x.to(device), y.to(device)
_, loss = model(x, y)
opt.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if step % 100 == 0 or step == steps - 1:
dt = time.time() - t0
print(f"step {step:4d} loss={loss.item():.4f} elapsed={dt:.1f}s")
torch.save({"model": model.state_dict(), "stoi": ds.stoi, "itos": ds.itos}, "ckpt.pt")
print("[done] saved ckpt.pt")
if __name__ == "__main__":
main() 8.4 Script de inferencia: sample.py
"""Genera texto a partir de un ckpt.pt entrenado."""
import torch
from train import MiniTransformer
ckpt = torch.load("ckpt.pt", map_location="cpu")
stoi, itos = ckpt["stoi"], ckpt["itos"]
model = MiniTransformer(vocab_size=len(itos))
model.load_state_dict(ckpt["model"])
model.eval()
prompt = "Quiet night thoughts\n"
idx = torch.tensor([[stoi[c] for c in prompt]], dtype=torch.long)
out = model.generate(idx, max_new_tokens=120, temperature=0.8, top_k=10)[0].tolist()
print("=== generated ===")
print("".join(itos[i] for i in out)) 8.5 Ejecútalo y salida esperada
mkdir -p data
# (escribe el input.txt de la sección 8.2)
python train.py Salida esperada (CPU ~30-60s, MPS/GPU unos pocos segundos):
[data] chars=28 len=87
[model] params=1.21M device=cpu
step 0 loss=3.3321 elapsed=0.0s
step 100 loss=2.7102 elapsed=2.1s
step 200 loss=2.3504 elapsed=4.0s
...
step 900 loss=1.8421 elapsed=32.5s
step 999 loss=1.7903 elapsed=36.0s
[done] saved ckpt.pt
python sample.py
=== generated ===
Quiet night thoughts
Before my bed a pool of light,
Can it be frost upon the ground?
Eyes up, I find the moon so bright,
Head down, I think of home I've found. 8.6 Consejos de ajuste
- La pérdida no baja: datos demasiado cortos → usa un
input.txtmás largo (p. ej. un corpus completo de poesía Tang); ajusta LR a 1e-4 / 5e-4. - La generación colapsa a un carácter: temperatura demasiado baja — súbela a 0,8-1,2; sube top_k a 20-50.
- Sin memoria: baja
batch_sizea 8, o ejecuta en media precisión contorch.amp.autocast. - Necesitas más velocidad: pon
d_model=96,n_layers=4para reducir a ~400K parámetros.
8.7 Siguientes pasos
- Un GPT completo: añade muestreo
top-p (nucleus), KV-cache para inferencia más rápida. - Arquitectura LLaMA: reemplaza LayerNorm con
RMSNorm, PE sinusoidal conRoPE, FFN GELU conSwiGLU, y añadeGQApara reducir memoria. - Entrenamiento multi-GPU:
torchrun --nproc_per_node=2 train.pyconDistributedDataParallel. - Usa un tokenizador real: sustituye
CharDatasetportiktoken(GPT-2 BPE) osentencepiece.