PyTorch básico: el primer paso para trasladar los principios al código
Al finalizar este tema
Serás capaz de implementar los principios de las redes neuronales aprendidos en los temas 2 a 4 (el dial, el descenso del gradiente y la retropropagación) en código real de PyTorch. Inicio de la sección de herramientas de la Fase 3. Este tema sienta las bases para los ejercicios del tema 13 (HuggingFace·API) y del tema 14 (Claude Code·Cursor).
El objetivo no es recorrer fragmentos cortos de código, sino escribir un bucle de entrenamiento completo desde el principio hasta el final. Tomaremos como ejemplo un clasificador de tipos celulares (el escenario del tema 3).
¿Qué es PyTorch?
PyTorch es un framework de aprendizaje profundo desarrollado por Meta AI. Proporciona los componentes necesarios para el entrenamiento e inferencia de redes neuronales a través de una API de Python.
Cuatro componentes clave.
- Tensor: Arreglos multidimensionales. Similares a NumPy, pero con soporte para aceleración en GPU y diferenciación automática.
- Autograd: Automatiza la retropropagación del tema 4. Crea un grafo de cálculo en segundo plano y calcula todas las derivadas parciales con una sola llamada a
backward(). - nn.Module: Interfaz de clase para definir capas y modelos de redes neuronales.
- Optimizer: Implementa optimizadores como Adam y AdamW del tema 3.
Al ensamblar estos componentes, se obtiene el bucle de entrenamiento.
Instalación. Ya está incluido en Colab. Para entornos locales, consulta pip install torch. Al usar GPU, especifica la versión de wheel correspondiente a la versión de CUDA.
Alternativas. TensorFlow, JAX, MLX (Apple). El estándar actual en investigación y práctica profesional es PyTorch. La mayoría de los modelos de código abierto (LLaMA, Mistral, Qwen, etc.) se distribuyen en PyTorch.
Tensor: el recipiente que contiene el dial y los píxeles del tema 2
Tensor es un arreglo multidimensional de números reales. Todos los valores de activación de las capas, pesos y sesgos mencionados en el tema 2 son tensores.
import torch
# Escalar (0 dimensiones)x = torch.tensor(3.14)
# Vector (1 dimensión)v = torch.tensor([1.0, 2.0, 3.0])
# Matriz (2 dimensiones)m = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
# 3 dimensiones (por ejemplo: batch 32, secuencia 100, embedding 512)t = torch.randn(32, 100, 512)print(t.shape) # torch.Size([32, 100, 512])dtype y dispositivo. Los tensores tienen un dtype (tipo de dato) y se ejecutan en un dispositivo (CPU/GPU).
# CPU fp32 (predeterminado)x = torch.randn(1000, 1000)
# fp16 GPUy = torch.randn(1000, 1000, dtype=torch.float16, device="cuda")
# Traslado de dispositivox_gpu = x.to("cuda")x_cpu = y.to("cpu")Es mucho más rápido con la GPU. La mayoría de los entrenamientos prácticos se realizan en GPU.
Operaciones con tensores. API casi idéntica a la de NumPy.
a = torch.randn(3, 4)b = torch.randn(4, 5)
c = a @ b # Producto matricial, shape (3, 5)d = a + 1 # Broadcastinge = a.sum(dim=1) # Suma por filas, shape (3,)f = a.mean() # Escalarg = a.relu() # ReLU elemento a elementoEn el episodio #2·5, se mencionó que todas las funciones de activación (ReLU, GELU, Sigmoid, etc.) están disponibles como métodos de tensor.
Autograd: la retropropagación del episodio #4 en una sola línea
Si se adjunta requires_grad=True a un tensor, PyTorch rastrea ese tensor como el objetivo para el cálculo de derivadas parciales. El grafo de cálculo del episodio #4 se crea automáticamente.
# Parámetros respecto a los que se derivaw = torch.randn(3, requires_grad=True)b = torch.randn(1, requires_grad=True)
# Datosx = torch.tensor([1.0, 2.0, 3.0])y = torch.tensor(10.0)
# Propagación hacia delantey_pred = w @ x + bloss = (y_pred - y) ** 2
# Retropropagación: esta línea calcula todas las derivadas parcialesloss.backward()
# Gradientes calculadosprint(w.grad) # ∂loss/∂wprint(b.grad) # ∂loss/∂bAquí se incluyen los cuatro pasos del episodio #4.
- Propagación directa (
y_pred = w @ x + b,loss = ...): construcción automática del grafo de cálculo. - Cálculo de la pérdida (
(y_pred - y) ** 2). - Retropropagación (
loss.backward()): recorrido inverso del grafo, cálculo de todas las derivadas parciales. - Actualización de los parámetros: se tratará más adelante.
Convención importante. Después de llamar a .backward(), los valores de .grad de los parámetros se acumulan. Es necesario inicializar .grad a cero antes del siguiente paso (véase más adelante en optimizer.zero_grad()).
nn.Module: definición del modelo en el episodio #5
Los modelos grandes se definen heredando de la clase nn.Module. Implementemos el clasificador de tipos celulares del episodio #3.
import torch.nn as nn
class CellTypeClassifier(nn.Module): def __init__(self, n_genes, n_types, hidden=512): super().__init__() self.encoder = nn.Sequential( nn.Linear(n_genes, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, hidden // 2), nn.ReLU(), nn.Linear(hidden // 2, n_types) )
def forward(self, x): return self.encoder(x)
model = CellTypeClassifier(n_genes=20000, n_types=25)print(sum(p.numel() for p in model.parameters()))# Muestra el número de parámetrosExplicación.
__init__: Definición de la capa.nn.Linear(in, out)esy = W·x + b. Capa totalmente conectada del bloque #2.nn.Sequential: Conexión secuencial de las capas.nn.ReLU(),nn.Dropout(0.3): Funciones de activación y normalización.forward: Lógica de propagación hacia adelante. PyTorch llama automáticamente a esta función.
Seguimiento automático de parámetros. Los pesos y sesgos dentro de nn.Linear se convierten automáticamente en requires_grad=True y se incluyen en model.parameters(). No es necesario gestionarlos directamente mediante torch.tensor(..., requires_grad=True).
Llamada. El modelo se utiliza como una función.
x = torch.randn(32, 20000) # Batch 32, 20.000 geneslogits = model(x) # Llama automáticamente a forwardprint(logits.shape) # torch.Size([32, 25])Pérdida y Optimizador: Descenso de Gradiente en el Episodio #3
La función de pérdida y el optimizador que vimos en el Episodio #3.
Pérdida.
# Problema de clasificación: parte 3 A.1, CEloss_fn = nn.CrossEntropyLoss()
# Problema de regresión: parte 3 A.1, MSEloss_fn = nn.MSELoss()Optimizer.
# SGD: parte 3 A.3optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# Adam: parte 3 A.7optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# AdamW: parte 3 A.8 (estándar para entrenar LLM)optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)Programador de la tasa de aprendizaje (sección #3 A.9).
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)Reducción automática de la tasa de aprendizaje mediante llamadas a scheduler.step() en cada época.
Bucle de entrenamiento: uniendo todas las piezas
Ahora, combinamos todos los componentes anteriores en un único bucle de entrenamiento completo.
import torchimport torch.nn as nnfrom torch.utils.data import DataLoader, TensorDataset
# Preparación de datos (ejemplo simulado)n_samples = 10000n_genes = 20000n_types = 25
X = torch.randn(n_samples, n_genes)y = torch.randint(0, n_types, (n_samples,))
dataset = TensorDataset(X, y)train_loader = DataLoader(dataset, batch_size=128, shuffle=True)
# Modelomodel = CellTypeClassifier(n_genes, n_types)model = model.to("cuda")
# Pérdida y optimizadorloss_fn = nn.CrossEntropyLoss()optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
# Bucle de entrenamienton_epochs = 10for epoch in range(n_epochs): model.train() total_loss = 0.0 for batch_x, batch_y in train_loader: batch_x = batch_x.to("cuda") batch_y = batch_y.to("cuda")
# Propagación hacia delante (parte 2) logits = model(batch_x) loss = loss_fn(logits, batch_y)
# Retropropagación (parte 4) optimizer.zero_grad() # Reinicia el gradiente del paso anterior loss.backward() # autograd
# Actualización de parámetros (parte 3) optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch}: loss = {total_loss / len(train_loader):.4f}")Este bucle se corresponde exactamente con la figura de la sección 3.
Inicialización aleatoria de parámetros ← automática al crear el modelo
Repetir (de cientos de miles a millones de veces):
Seleccionar un minibatch al azar ← DataLoader
Propagación hacia delante del minibatch ← logits = model(batch_x)
Calcular el coste con predicción y etiqueta← loss = loss_fn(logits, batch_y)
Retropropagar y calcular gradientes ← loss.backward()
Actualizar parámetros con el optimizador ← optimizer.step()PyTorch expone directamente los principios de las secciones 2 a 4 a través de una API.
Dataset y DataLoader: mini-lotes de la sección 3
En la sección 3, explicamos por qué son necesarios los mini-lotes en el SGD. PyTorch lo implementa mediante Dataset + DataLoader.
Dataset. Clase que define __len__ y __getitem__.
from torch.utils.data import Dataset
class ScRNADataset(Dataset): def __init__(self, expression_matrix, labels): self.expr = expression_matrix self.labels = labels
def __len__(self): return len(self.labels)
def __getitem__(self, idx): return self.expr[idx], self.labels[idx]DataLoader. Gestión de la carga por lotes, el orden aleatorio y el procesamiento paralelo.
train_loader = DataLoader( dataset, batch_size=128, shuffle=True, # Baraja el orden en cada época num_workers=4, # Carga paralela mediante núcleos de CPU pin_memory=True, # Acelera la transferencia a la GPU drop_last=True # Excluye el último batch incompleto)Aplicaciones prácticas en biología. Implementación de conjuntos de datos personalizados por dominio, como scRNA-seq, imágenes y datos de secuenciación. Scanpy, AnnData y PyTorch Geometric ofrecen wrappers específicos para cada dominio.
Uso de GPU: entrenamiento rápido
La GPU es entre diez y cien veces más rápida que la CPU. La mayoría de los entrenamientos en entornos profesionales se realizan con GPU.
# Comprueba las GPU disponiblesprint(torch.cuda.is_available())print(torch.cuda.device_count())
# Traslada tensores y modelodevice = "cuda" if torch.cuda.is_available() else "cpu"model = model.to(device)batch_x = batch_x.to(device)Multi-GPU. Los modelos y conjuntos de datos grandes se benefician del uso de múltiples GPU.
- DataParallel (DP): Múltiples GPU en un solo nodo. Es sencillo, pero presenta cuellos de botella.
- DistributedDataParallel (DDP): Es el estándar. Cada GPU ejecuta su propio proceso; reduce los gradientes mediante all-reduce.
- Fully Sharded Data Parallel (FSDP): Distribuye los parámetros y los estados del optimizador. Esencial para entrenar modelos de 70B+.
Apple Silicon. Compatible con dispositivos mps. device = "mps". Permite realizar experimentos pequeños en portátiles personales.
Precisión mixta: mejora de la velocidad y ahorro de memoria
Realizar cálculos en FP16 o BF16 en lugar de FP32 reduce el tiempo de ejecución y el uso de memoria.
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for batch_x, batch_y in train_loader: optimizer.zero_grad()
with autocast(dtype=torch.bfloat16): logits = model(batch_x) loss = loss_fn(logits, batch_y)
scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()BF16 frente a FP16.
- FP16: precisión semicompleta. Presenta riesgo de desbordamiento, por lo que requiere GradScaler.
- BF16 (bfloat16): tiene el mismo rango de exponente que FP32. Presenta un riesgo menor de desbordamiento. Compatible con las GPU de las series A100 y H100. Se está convirtiendo en el estándar de la industria.
Efecto. El entrenamiento es de 2 a 4 veces más rápido y requiere la mitad de memoria. La pérdida de precisión es mínima.
Guardar y cargar — Checkpoint
Guarda el estado intermedio y final del entrenamiento.
# Guardadotorch.save({ "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "loss": total_loss,}, "checkpoint.pt")
# Cargacheckpoint = torch.load("checkpoint.pt")model.load_state_dict(checkpoint["model_state_dict"])optimizer.load_state_dict(checkpoint["optimizer_state_dict"])start_epoch = checkpoint["epoch"] + 1Si el entrenamiento dura varios días, asegúrate de realizar puntos de control periódicos para prepararte ante fallos del sistema o cortes de energía.
Inferencia — Uso después de completar el entrenamiento
Realiza predicciones con el modelo entrenado.
model.eval() # Pone dropout y batchnorm en modo de inferenciawith torch.no_grad(): # Desactiva las derivadas para ahorrar memoria y tiempo for batch_x, _ in test_loader: batch_x = batch_x.to(device) logits = model(batch_x) preds = logits.argmax(dim=-1) # ... usar predsno_grad no utiliza autograd para crear un gráfico de cálculo, lo que permite ahorrar memoria y mejorar la velocidad.
Implementación. Existen varios métodos para implementar modelos entrenados en un entorno de producción.
- TorchScript: Compila los modelos de PyTorch mediante JIT. Permite la implementación en C++ y en dispositivos móviles.
- ONNX: Formato independiente de frameworks. Permite la optimización con TensorRT y OpenVINO.
- HuggingFace Hub: Permite compartir y servir modelos.
- vLLM, TGI, SGLang: Frameworks especializados en la implementación de LLM.
Consejos prácticos para el trabajo con datos biológicos
Carga de datos
scRNA-seq: scanpy + AnnData → se crea un objeto Dataset de PyTorch.
Secuencias de proteínas: se analizan los datos de biopython y se crea un objeto Dataset.
Imágenes (patología, microscopía): se utiliza torchvision.datasets.ImageFolder y se aplican técnicas de aumento de datos.
Optimización del entrenamiento
- Acumulación de gradientes: permite simular lotes grandes cuando la memoria de la GPU es limitada.
- Checkpointing de gradientes (sección #4 A.7): ahorra memoria al recalcular las activaciones.
- DeepSpeed, FSDP: para entrenar modelos grandes.
Gestión de experimentos
- Weights & Biases (wandb): permite registrar y visualizar los experimentos.
- MLflow: herramienta de seguimiento de experimentos de código abierto.
- Lightning: reduce la cantidad de código repetitivo en el bucle de entrenamiento.
PyTorch Lightning. Es un wrapper que oculta el código repetitivo del bucle de entrenamiento.
import pytorch_lightning as pl
class CellClassifierLightning(pl.LightningModule): def __init__(self, model, lr=1e-3): super().__init__() self.model = model self.lr = lr self.loss_fn = nn.CrossEntropyLoss()
def training_step(self, batch, batch_idx): x, y = batch logits = self.model(x) loss = self.loss_fn(logits, y) self.log("train_loss", loss) return loss
def configure_optimizers(self): return torch.optim.AdamW(self.parameters(), lr=self.lr)
trainer = pl.Trainer(max_epochs=10, accelerator="gpu", devices=1)trainer.fit(model_lightning, train_loader)Multi-GPU, precisión mixta y puntos de control automáticos. Facilita el entrenamiento en producción.
Escenarios de aplicación en biología
Escenario 1: Clasificador de tipos celulares en la práctica
Implementación completa del escenario 1 del episodio 3. AnnData → Conjunto de datos de PyTorch → Entrenamiento. Consulte el tutorial de Scanpy.
Escenario 2: Predicción de contactos de proteínas (mini AlphaFold)
Secuencia de proteínas → Red neuronal basada en atención → Probabilidad de contacto entre pares de aminoácidos. Práctica conceptual de los episodios 5 y 6. Se puede entrenar con conjuntos de datos pequeños (por ejemplo, CATH).
Escenario 3: Clasificación de imágenes de patología tisular
Implementación práctica del escenario de diapositivas de patología de los episodios 2 y 6. Ajuste fino de ResNet-50. Utilice Torchvision.datasets.
Resumen clave
- Los 4 componentes de PyTorch: Tensor, Autograd, nn.Module, Optimizer. La API refleja directamente los principios de los episodios 2 a 4.
- 5 pasos del ciclo de entrenamiento: forward, loss, backward, zero_grad, step. Tal como se muestra en la figura del episodio 3.
- Procesamiento por lotes con DataLoader, aprovechamiento de la GPU con
to(device). - Acelere el entrenamiento de 2 a 4 veces con precisión mixta (BF16).
- Es esencial guardar puntos de control periódicamente.
- Entrene modelos grandes con Multi-GPU (DDP, FSDP).
- Facilite el trabajo con herramientas como Lightning y wandb.
Próximos conceptos
- Episodio 13
huggingface-and-openai— Utilice el centro de modelos de Hugging Face y las API comerciales. - Episodio 14
claude-code-and-cursor— Práctica con agentes de codificación de IA. - Episodio 15
bio-ai-integration— Integre todo esto en un flujo de trabajo biológico.
📐 Apéndice: Consejos prácticos y fórmulas para expertos
Dificultad: Muy difícil Dirigido a: Lectores con experiencia en la optimización del entrenamiento práctico con PyTorch y sistemas.
A.1 Funcionamiento interno de Autograd
Cuando se utiliza un tensor requires_grad=True en una operación, cada operación se registra como un nodo del gráfico.
Clase Function. Cada operación (add, mul, matmul, etc.) es una subclase de torch.autograd.Function. Se definen los métodos estáticos forward y backward.
Autograd personalizado:
class MyReLU(torch.autograd.Function): @staticmethod def forward(ctx, x): ctx.save_for_backward(x) return x.clamp(min=0)
@staticmethod def backward(ctx, grad_output): x, = ctx.saved_tensors grad_input = grad_output.clone() grad_input[x < 0] = 0 return grad_inputctx.save_for_backward: para la propagación hacia atrás, se almacenan los tensores necesarios. El almacenamiento de los valores de activación en el fragmento #4 se realiza mediante este mecanismo.
A.2 Gradient Checkpointing
Fragmento #4, A.7. torch.utils.checkpoint.
from torch.utils.checkpoint import checkpoint
class MyBlock(nn.Module): def forward(self, x): # No guarda las activaciones de esta función; las recalcula durante backward return checkpoint(self._forward, x, use_reentrant=False)
def _forward(self, x): # Cálculo real return self.layers(x)Memoria nivel O(√L). Tiempo 1.5x. Modelos grandes esenciales.
A.3 Valores de Precisión Mixta
FP32: exponente 8, mantisa 23. Intervalo ~1e-38 ~ 1e+38, precisión de unas 7 cifras.
FP16: exponente 5, mantisa 10. Intervalo ~6e-5 ~ 6,5e+4, precisión de unas 3 cifras. Riesgo de desbordamiento.
BF16: exponente 8, mantisa 7. Intervalo igual al de FP32, precisión de unas 2 cifras. Resistente al desbordamiento.
Loss Scaling (FP16 esencial):
scaled_loss = loss * scale_factor
scaled_loss.backward() # El gradiente está multiplicado por scale_factor
# Desescala el gradiente y ejecuta el paso del optimizadorBF16 no requiere escalado de pérdida.
A.4 Sincronización DDP
Sincronización de gradientes All-Reduce. Después de calcular los gradientes para su minibatch, cada GPU calcula el promedio de los gradientes en todas las GPU.
Complejidad temporal: O(P / bandwidth). P = número de parámetros.
Backend NCCL. Biblioteca optimizada por NVIDIA. Utiliza InfiniBand y NVLink para la comunicación entre GPU.
Bucketing. Agrupa los gradientes en bloques para superponer la comunicación y el cálculo. torch.nn.parallel.DistributedDataParallel automático.
A.5 FSDP (Data Parallel Totalmente Fragmentada)
Fragmenta los parámetros y el estado del optimizador entre las GPU.
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
model = FSDP(model, sharding_strategy=ShardingStrategy.FULL_SHARD)Ahorro de memoria. Cada GPU almacena solo 1/N de los parámetros. Un modelo de 70 mil millones de parámetros puede utilizarse en 8 GPU de 40 GB.
Sobrecarga de comunicación. Recopilación y liberación de parámetros durante las fases de propagación hacia adelante y hacia atrás. Es esencial la optimización de NCCL.
A.6 Kernel CUDA personalizado
Algunas operaciones no están optimizadas con las implementaciones integradas de PyTorch. Se crean kernels personalizados con Triton y CUDA C.
Flash Attention 2. Implementación del softmax en línea descrito en la sección #6 A.8 utilizando Triton y CUDA.
from flash_attn import flash_attn_func
output = flash_attn_func(q, k, v, causal=True)nn.functional.scaled_dot_product_attention es de 2 a 4 veces más rápido.
Tendencia reciente. torch.compile (PyTorch 2.0) optimiza automáticamente los kernels. model = torch.compile(model). En la mayoría de los casos, la mejora es de 2 veces o más.
A.7 Perfilado
Profiler de PyTorch:
with torch.profiler.profile( activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA], schedule=torch.profiler.schedule(wait=1, warmup=1, active=3), on_trace_ready=torch.profiler.tensorboard_trace_handler("./log")) as prof: for step, batch in enumerate(train_loader): # ... paso de entrenamiento prof.step()Visualización con TensorBoard. Identificación de cuellos de botella (kernel lento, transferencia de CPU a GPU, carga de datos).
A.8 Reproducibilidad
Obtener resultados reproducibles al repetir el mismo experimento.
import torchimport randomimport numpy as np
def set_seed(seed): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) random.seed(seed) np.random.seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = FalseLa reproducibilidad total es difícil (debido a la no determinismo en algunas operaciones de CUDA). En la práctica, se fija la semilla y se registra el hardware y la versión de PyTorch.
A.9 Consejos para la depuración
Detección de NaN:
torch.autograd.set_detect_anomaly(True)Si se produce un NaN durante la retropropagación, determinar en qué operación se originó.
Recorte de gradientes (Episodio n.º 4, A.8):
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)loss.backward() después, optimizer.step() antes.
Batch size finder (Lightning): Herramienta para encontrar automáticamente el tamaño de lote máximo posible.
A.10 Tendencias recientes
torch.compile (PyTorch 2.0+): Optimización automática mediante la compilación JIT.
torch.export: Extracción del modelo como un grafo (para su despliegue en ONNX, TensorRT y dispositivos móviles).
AOT autograd: Generación del grafo de retropropagación en tiempo de compilación.
Nested Tensor: Procesamiento de secuencias de longitud variable (sin necesidad de rellenar).
FunctorchModule → torch.func: API funcional de estilo JAX.
Estas herramientas se están convirtiendo en el estándar para el entrenamiento y la implementación de los modelos grandes más recientes.
Referencias
El contenido, los escenarios, las analogías y los datos numéricos de este artículo son desarrollos propios de BioPlayground. A continuación, se presentan referencias externas útiles para el aprendizaje conceptual:
- Documentación oficial de PyTorch: pytorch.org/docs
- Tutorial de 60 minutos de PyTorch: pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
- PyTorch Lightning: lightning.ai
- HuggingFace Transformers: huggingface.co/docs/transformers
- Flash Attention: github.com/Dao-AILab/flash-attention
- DeepSpeed: deepspeed.ai
- PyTorch Profiler: pytorch.org/tutorials/recipes/recipes/profiler_recipe.html
- Scanpy: scanpy.readthedocs.io — biblioteca estándar para scRNA-seq
- PyTorch Geometric: pyg.org — redes neuronales gráficas (moléculas y proteínas)
- TorchVision: pytorch.org/vision — para el dominio de las imágenes
En el artículo #12 se ha completado la presentación de los fundamentos de PyTorch. En el artículo #13 se tratará el uso del Hub de modelos de HuggingFace y las API de OpenAI y Anthropic.