TensorFlow y Keras: Entrenamiento y Serving
TensorFlow en el Ecosistema de IA
TensorFlow, desarrollado por Google, ofrece un ecosistema completo desde entrenamiento hasta producción. Mientras PyTorch domina en investigación, TensorFlow tiene fortalezas en:
- TF Serving: Serving de modelos en producción con bajo overhead
- TF Lite: Deployment en dispositivos móviles y edge
- TF.js: Modelos en el navegador
- Ecosistema Keras: API de alto nivel, rápida experimentación
Keras 3: API Unificada
import keras # Keras 3 — backend agnóstico (TF, PyTorch, JAX)
from keras import layers, Model
class TransformerBlock(layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1):
super().__init__()
self.attention = layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_dim
)
self.ffn = keras.Sequential([
layers.Dense(ff_dim, activation="gelu"),
layers.Dense(embed_dim),
])
self.norm1 = layers.LayerNormalization(epsilon=1e-6)
self.norm2 = layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = layers.Dropout(dropout)
self.dropout2 = layers.Dropout(dropout)
def call(self, inputs, training=False):
# Self-attention: inputs se usa como Query, Key y Value simultáneamente
attn_output = self.attention(inputs, inputs)
# Dropout solo se aplica durante entrenamiento (training=True)
attn_output = self.dropout1(attn_output, training=training)
# Conexión residual (inputs + ...) + Layer Norm: patrón fundamental de transformers
# La conexión residual permite que los gradientes fluyan sin degradarse
out1 = self.norm1(inputs + attn_output)
# Feed-forward: expande a ff_dim con GELU (activación estándar en transformers),
# luego comprime de vuelta a embed_dim
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
# Segunda conexión residual + normalización
return self.norm2(out1 + ffn_output)
Entrenamiento de un Clasificador de Texto
# TextVectorization: tokenizador integrado de Keras (texto → secuencia de enteros)
from keras.layers import TextVectorization
max_tokens = 20000 # Vocabulario máximo: las 20K palabras más frecuentes
max_length = 256 # Largo fijo de secuencias (padding/truncado automático)
vectorizer = TextVectorization(
max_tokens=max_tokens,
output_sequence_length=max_length,
)
# adapt() construye el vocabulario analizando los textos de entrenamiento
vectorizer.adapt(train_texts)
# Modelo funcional de Keras: Input acepta strings directamente
inputs = keras.Input(shape=(1,), dtype="string")
x = vectorizer(inputs) # String → secuencia de enteros
x = layers.Embedding(max_tokens, 128)(x) # Enteros → vectores densos de 128 dims
x = TransformerBlock(128, num_heads=4, ff_dim=256)(x) # ff_dim=256 (2x embed para expandir/comprimir)
x = layers.GlobalAveragePooling1D()(x) # (batch, seq, 128) → (batch, 128): promedia secuencia
x = layers.Dropout(0.3)(x)
x = layers.Dense(64, activation="relu")(x)
# softmax: convierte logits a probabilidades que suman 1
outputs = layers.Dense(num_classes, activation="softmax")(x)
model = Model(inputs, outputs)
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-4),
# sparse_categorical_crossentropy: labels son enteros (0,1,2...)
# (vs categorical_crossentropy que requiere one-hot encoding)
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
# Callbacks para controlar el entrenamiento automáticamente
callbacks = [
# Para entrenamiento si val_loss no mejora en 3 épocas consecutivas
keras.callbacks.EarlyStopping(
monitor="val_loss", patience=3, restore_best_weights=True
),
# Reduce learning rate a la mitad si val_loss estanca por 2 épocas
keras.callbacks.ReduceLROnPlateau(
monitor="val_loss", factor=0.5, patience=2
),
# Guarda logs para visualizar curvas de entrenamiento en TensorBoard
keras.callbacks.TensorBoard(log_dir="./logs"),
# Guarda solo el modelo con mejor val_loss (no el último)
keras.callbacks.ModelCheckpoint(
"best_model.keras", save_best_only=True
),
]
history = model.fit(
train_ds, validation_data=val_ds,
epochs=20, callbacks=callbacks
)
Transfer Learning con Modelos Pre-Entrenados
from transformers import TFAutoModelForSequenceClassification
# Cargar modelo pre-entrenado de Hugging Face con backend TF
model = TFAutoModelForSequenceClassification.from_pretrained(
"bert-base-multilingual-cased",
num_labels=5, # Número de clases a clasificar
)
# Fine-tuning con tf.keras
# learning_rate=2e-5: LR bajo estándar para fine-tuning de BERT
# (muy alto destruiría los pesos pre-entrenados)
optimizer = keras.optimizers.Adam(learning_rate=2e-5)
# model.compute_loss: función de pérdida interna de HuggingFace
# que maneja correctamente la estructura de salida del modelo
model.compile(optimizer=optimizer, loss=model.compute_loss, metrics=["accuracy"])
model.fit(
# shuffle(1000): buffer de 1000 ejemplos para aleatorizar (mayor = mejor mezcla, más RAM)
# batch(16): lotes de 16 ejemplos en train (limitado por VRAM con gradientes)
tf_train_dataset.shuffle(1000).batch(16),
# batch(32): en eval se puede usar batch más grande (sin gradientes)
validation_data=tf_val_dataset.batch(32),
epochs=3,
)
TensorFlow Serving: Producción
Exportar SavedModel
# Guardar para serving
model.save("saved_model/intent_classifier/1") # Versión 1
# Estructura:
# saved_model/intent_classifier/1/
# ├── saved_model.pb
# ├── variables/
# │ ├── variables.data-00000-of-00001
# │ └── variables.index
# └── assets/
Deploy con TF Serving (Docker)
# TF Serving expone: puerto 8501 (REST API) y 8500 (gRPC)
docker run -p 8501:8501 \
# Monta el directorio de modelos local en /models (ruta por defecto de TF Serving)
--mount type=bind,source=$(pwd)/saved_model,target=/models \
# TF Serving buscará el modelo en /models/intent_classifier/{versión}/
-e MODEL_NAME=intent_classifier \
tensorflow/serving:latest
# POST al endpoint :predict con formato "instances" (cada elemento = 1 ejemplo)
# Tokens BERT: 101=[CLS], 7592=token de texto, 102=[SEP]
curl -X POST http://localhost:8501/v1/models/intent_classifier:predict \
-H "Content-Type: application/json" \
-d '{"instances": [{"input_ids": [101, 7592, 102]}]}'
gRPC para Alta Performance
# gRPC (Google Remote Procedure Call): usa Protocol Buffers (serialización binaria)
# ~2-5x más rápido que REST-JSON para serving de modelos
import grpc
from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc
# insecure_channel: conexión sin TLS (solo para desarrollo)
# Puerto 8500 = gRPC (vs 8501 = REST)
channel = grpc.insecure_channel("localhost:8500")
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
request = predict_pb2.PredictRequest()
request.model_spec.name = "intent_classifier"
# signature_name: define las entradas/salidas del modelo
# "serving_default" es la firma exportada por defecto
request.model_spec.signature_name = "serving_default"
TensorFlow Lite: Edge Deployment
# Convertir modelo para mobile/edge
converter = tf.lite.TFLiteConverter.from_saved_model("saved_model/1")
# Optimize.DEFAULT: cuantiza pesos de float32 → int8 automáticamente
# Resultado: ~4x menos tamaño, ~2x más rápido, con pérdida mínima (<1%)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
with open("model.tflite", "wb") as f:
f.write(tflite_model)
# TF Lite Interpreter: runtime minimalista para dispositivos edge (móvil, IoT)
# No usa el grafo TF completo — optimizado para baja latencia y poca memoria
interpreter = tf.lite.Interpreter(model_path="model.tflite")
# allocate_tensors: reserva memoria para todos los tensores (obligatorio antes de inferir)
interpreter.allocate_tensors()
# Obtiene metadatos de entrada/salida: shape, dtype, e "index" (ID del tensor)
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# Asigna el input usando el index del primer tensor de entrada
interpreter.set_tensor(input_details[0]["index"], input_data)
# invoke() ejecuta el forward pass completo del modelo
interpreter.invoke()
# Lee el resultado del primer tensor de salida
output = interpreter.get_tensor(output_details[0]["index"])
Comparativa: PyTorch vs TensorFlow
| Aspecto | PyTorch | TensorFlow |
|---|---|---|
| Investigación | Dominante (>80% papers) | Menos usado |
| Producción | TorchServe, Triton | TF Serving (más maduro) |
| Mobile/Edge | ExecuTorch | TF Lite (más maduro) |
| Debugging | Eager mode nativo | Eager + Graph mode |
| Ecosistema | HuggingFace, PEFT | TFX, TF Extended |
| Curva de aprendizaje | Más pythonico | Más verboso |
¿Cuál Usar?
- Fine-tuning / RAG / Agentes: PyTorch + HuggingFace
- Serving en producción con bajo overhead: TF Serving
- Mobile / Edge: TF Lite
- Prototipado rápido: Keras (cualquier backend)
- La mayoría de proyectos de AI Engineering: PyTorch
TensorBoard: Visualización de Entrenamiento
# En el código de entrenamiento
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/experiment_1")
for epoch in range(num_epochs):
writer.add_scalar("Loss/train", train_loss, epoch)
writer.add_scalar("Loss/val", val_loss, epoch)
writer.add_scalar("Accuracy/val", val_acc, epoch)
writer.add_scalar("LR", current_lr, epoch)
writer.close()
# Visualizar
tensorboard --logdir runs/ --port 6006
TensorBoard es compatible tanto con PyTorch como con TensorFlow, y es la herramienta estándar para visualizar métricas de entrenamiento.
Resumen
TensorFlow y Keras complementan a PyTorch en el toolkit del AI Engineer:
- Keras 3: Prototipado rápido con backend agnóstico
- TF Serving: Serving de modelos en producción con autoscaling
- TF Lite: Deployment en dispositivos edge
- Transfer Learning: Fine-tuning eficiente de modelos pre-entrenados
- TensorBoard: Visualización y debugging de entrenamiento
🧠 Preguntas de Repaso
1. ¿Cuál es la principal ventaja de TF Serving sobre TorchServe para producción?
- A) TF Serving es más rápido en investigación
- B) TF Serving es más maduro y tiene mejor soporte para autoscaling en producción
- C) TF Serving soporta más lenguajes de programación
- D) TF Serving es open-source y TorchServe no
Respuesta: B) — TF Serving es considerado más maduro para producción, con mejor soporte para autoscaling, versionado de modelos y canary deployments. En contraste, PyTorch domina en investigación (>80% de papers).
2. En TF Serving con Docker, ¿qué protocolo es ~2-5x más rápido que REST-JSON y en qué puerto escucha?
- A) WebSocket en el puerto 8080
- B) gRPC en el puerto 8500, usando Protocol Buffers (serialización binaria)
- C) GraphQL en el puerto 9090
- D) HTTP/2 en el puerto 443
Respuesta: B) — gRPC escucha en el puerto 8500 (REST en 8501) y usa Protocol Buffers para serialización binaria, lo que lo hace ~2-5x más rápido que REST-JSON.
3. ¿Qué logra TF Lite Optimize.DEFAULT al convertir un modelo?
- A) Aumenta la precisión del modelo a float64
- B) Cuantiza de float32 a int8 logrando ~4x menos tamaño y ~2x más velocidad con menos de 1% de pérdida
- C) Comprime el modelo con gzip
- D) Elimina capas no utilizadas del grafo
Respuesta: B) — TF Lite con Optimize.DEFAULT aplica cuantización de float32 a int8, resultando en modelos ~4x más pequeños y ~2x más rápidos, con pérdida mínima de precisión (<1%).
4. ¿Cuál es la recomendación principal para la mayoría de proyectos de AI Engineering?
- A) Usar TensorFlow exclusivamente
- B) Usar PyTorch para la mayoría de proyectos, TF Serving para producción y TF Lite para edge
- C) Usar JAX para todo
- D) Usar Keras sin backend específico
Respuesta: B) — La recomendación es usar PyTorch para fine-tuning, RAG y agentes (mayoría de proyectos), TF Serving para serving en producción por su madurez, y TF Lite para deployment en dispositivos edge/mobile.