Al cerrar el módulo anterior dijimos que ya conoces las técnicas —CNN, RNN, GAN, autoencoders, transfer learning, atención— y que había llegado el momento de consolidar las herramientas. Empezamos por la que llevas usando desde la lección 02-05 sin haberla mirado de frente: TensorFlow. Cada vez que escribiste keras.Sequential, model.fit() o model.save(), debajo estaba TensorFlow haciendo el trabajo pesado: representar los datos como tensores, calcular gradientes automáticamente y ejecutar las operaciones en CPU o GPU. En esta lección abrimos la caja: entenderás qué es realmente TensorFlow, cómo funcionan sus tensores, cómo calcula gradientes con GradientTape (reproduciremos el descenso de gradiente manual de 02-03), qué tres formas hay de construir modelos y cómo alimentar tus redes con pipelines de datos eficientes. Para el equipo de TecnoMarket, esto marca la diferencia entre "usar recetas" y "dominar la herramienta".
Contenido
- Qué es TensorFlow y su ecosistema
- Tensores: constantes, variables, shapes y dtypes
- Broadcasting: operar con formas distintas
- Diferenciación automática con tf.GradientTape
- Las tres APIs para construir modelos
- Pipelines de datos eficientes con tf.data
- Callbacks útiles durante el entrenamiento
Qué es TensorFlow y su ecosistema
TensorFlow es una plataforma de código abierto para machine learning creada por Google y publicada en 2015. Su nombre lo describe: hace fluir (flow) tensores (arrays multidimensionales) a través de grafos de operaciones matemáticas.
Conviene distinguir las capas del ecosistema:
| Capa | Qué es | Ejemplo de uso |
|---|---|---|
| TensorFlow (núcleo) | Motor de tensores, operaciones y diferenciación automática | tf.matmul, tf.GradientTape |
| Keras | API de alto nivel para definir y entrenar modelos | keras.Sequential, model.fit() |
| tf.data | Construcción de pipelines de datos | Dataset.from_tensor_slices(...) |
| TensorBoard | Visualización de entrenamientos | Curvas de pérdida, histogramas |
| TFLite / TF.js / TF Serving | Despliegue en móvil, navegador y servidores | Lo veremos en 06-05 |
El punto clave para ti: Keras ES la API oficial de alto nivel de TensorFlow. No has estado usando "otra cosa" durante el curso; has estado usando TensorFlow a través de su interfaz más cómoda. La relación es como la de un coche automático (Keras) y su motor (TensorFlow): hasta ahora conducías; hoy abrimos el capó.
import tensorflow as tf
from tensorflow import keras
print(tf.__version__) # p. ej. 2.16.x
print(keras.__name__) # keras vive dentro del ecosistema TF- Si ejecutaste
pip install tensorflowen 01-05, ya tienes todo lo de esta lección. - TensorFlow detecta la GPU automáticamente si hay una disponible (en Colab: Entorno de ejecución → Cambiar tipo → GPU).
# ¿Qué dispositivos ve TensorFlow?
print(tf.config.list_physical_devices())
# [PhysicalDevice(name='/physical_device:CPU:0', ...), quizá una GPU]Tensores: constantes, variables, shapes y dtypes
Un tensor es un array multidimensional con un tipo de dato (dtype) uniforme. Ya los conoces conceptualmente del curso: una imagen de producto de TecnoMarket es un tensor (alto, ancho, 3), un lote de reseñas vectorizadas es un tensor (batch, longitud).
tf.constant: tensores inmutables
import tensorflow as tf
escalar = tf.constant(4.99) # precio de un cable USB
vector = tf.constant([120.0, 15.5, 899.0]) # precios de 3 productos
matriz = tf.constant([[1, 2], [3, 4]]) # tensor 2D
print(escalar.shape) # () -> 0 dimensiones
print(vector.shape) # (3,) -> 1 dimensión con 3 elementos
print(matriz.shape) # (2, 2)
print(matriz.dtype) # <dtype: 'int32'>
print(vector.dtype) # <dtype: 'float32'>Puntos que debes interiorizar:
- shape: la forma del tensor. Los errores de shape son, con diferencia, los más frecuentes en deep learning; leer bien un mensaje
Incompatible shapes: (32, 10) vs (32, 1)te ahorrará horas. - dtype: el tipo de dato. Por defecto los flotantes son
float32(equilibrio entre precisión y velocidad) y los enterosint32. No se pueden mezclar dtypes en una operación sin convertir antes contf.cast. - Un
tf.constantes inmutable: no puedes cambiar sus valores una vez creado.
tf.Variable: tensores que aprenden
Los pesos de una red son variables: tensores cuyo valor debe poder actualizarse en cada paso de entrenamiento. Cuando en 02-03 hablamos del "reparto de culpa" que ajusta cada peso, esos pesos son internamente objetos tf.Variable.
w = tf.Variable(3.0) # un peso inicializado a 3.0
b = tf.Variable(0.0) # un sesgo
w.assign(2.5) # cambiar su valor (¡esto un constant no puede!)
w.assign_add(0.1) # w = w + 0.1
print(w.numpy()) # 2.6 -> .numpy() convierte a numpytf.constant |
tf.Variable |
|
|---|---|---|
| Mutable | No | Sí (assign, assign_add) |
| Uso típico | Datos de entrada, hiperparámetros | Pesos y sesgos del modelo |
| Rastreado por GradientTape | Solo si se pide explícitamente | Sí, automáticamente |
Operaciones básicas
a = tf.constant([[1.0, 2.0], [3.0, 4.0]])
b = tf.constant([[10.0, 20.0], [30.0, 40.0]])
print(a + b) # suma elemento a elemento
print(a * b) # producto elemento a elemento (¡NO matricial!)
print(tf.matmul(a, b)) # producto matricial (el de las capas densas)
print(tf.reduce_mean(a)) # media de todos los elementos -> 2.5
print(tf.reshape(a, (4, 1))) # cambiar la forma sin cambiar los datosRecuerda de 02-01: una capa densa es exactamente tf.matmul(entradas, pesos) + sesgo seguido de una activación. Ahora puedes escribirla a mano.
Broadcasting: operar con formas distintas
El broadcasting permite operar tensores de formas diferentes: TensorFlow "estira" virtualmente el tensor pequeño para que encaje con el grande, sin copiar datos. Es la misma regla que usa numpy.
precios = tf.constant([[120.0], [15.5], [899.0]]) # shape (3, 1): 3 productos
iva = tf.constant(1.21) # escalar, shape ()
con_iva = precios * iva # el escalar se aplica a los 3 -> shape (3, 1)
descuentos = tf.constant([0.95, 0.90, 0.80]) # shape (3,): 3 campañas
tabla = precios * descuentos # (3,1) x (3,) -> broadcasting -> (3, 3)
print(tabla.shape) # (3, 3): cada producto con cada descuentoReglas prácticas (se comparan las shapes de derecha a izquierda):
- Dos dimensiones son compatibles si son iguales o si una de ellas es 1.
- Las dimensiones que faltan se tratan como 1.
- Si ninguna regla aplica, obtendrás un error
Incompatible shapes.
El broadcasting es potentísimo, pero también fuente de errores silenciosos: en el ejemplo anterior, quizá querías 3 precios con 3 descuentos (resultado (3,)) y obtuviste una tabla (3, 3) sin ningún error. Comprueba siempre la shape del resultado.
Diferenciación automática con tf.GradientTape
Aquí está la joya de la corona. En 02-03 calculamos gradientes a mano con numpy, aplicando la regla de la cadena paso a paso para repartir la culpa del error. TensorFlow hace exactamente eso, pero automáticamente: tf.GradientTape es una "cinta grabadora" que registra cada operación que haces dentro de su contexto, y luego la reproduce hacia atrás para calcular derivadas.
Reproduzcamos el ejemplo clásico: minimizar una función de pérdida simple, primero el gradiente y luego el descenso completo.
import tensorflow as tf
# Queremos encontrar el w que minimiza la "pérdida" L(w) = (w - 4)^2
# Sabemos por cálculo que el mínimo está en w = 4 y que dL/dw = 2*(w - 4)
w = tf.Variable(0.0) # empezamos lejos del mínimo
with tf.GradientTape() as tape: # 1. la cinta empieza a grabar
loss = (w - 4.0) ** 2 # 2. operaciones grabadas
grad = tape.gradient(loss, w) # 3. reproducir hacia atrás
print(grad.numpy()) # -8.0 (= 2*(0-4), la derivada exacta)Explicación línea a línea:
with tf.GradientTape() as tape:abre el contexto de grabación. Todo lo que ocurra dentro y que involucre variables se registra.loss = (w - 4.0) ** 2no solo calcula el valor (16.0): la cinta anota "resta, luego cuadrado", que es lo que necesita para aplicar la regla de la cadena.tape.gradient(loss, w)pregunta: "¿cuánto cambialosssi muevowun poquito?". Es la misma pregunta del backpropagation de 02-03.
Ahora el descenso de gradiente completo, el mismo bucle que escribimos con numpy:
w = tf.Variable(0.0)
learning_rate = 0.1
for paso in range(30):
with tf.GradientTape() as tape:
loss = (w - 4.0) ** 2
grad = tape.gradient(loss, w)
w.assign_sub(learning_rate * grad) # w = w - lr * gradiente
print(w.numpy()) # ~3.995: ha convergido a 4, igual que en 02-03Comparación con la versión numpy de 02-03:
| numpy (02-03) | TensorFlow (GradientTape) | |
|---|---|---|
| Derivada | La escribes tú a mano: 2*(w-4) |
La calcula la cinta automáticamente |
| Riesgo de error | Alto (una derivada mal y todo falla) | Prácticamente nulo |
| Redes profundas | Inviable a mano (miles de derivadas) | Idéntico esfuerzo: una línea |
| Valor didáctico | Entiendes el mecanismo | Escalas el mecanismo |
Cuando llamas a model.fit() en Keras, internamente ocurre exactamente esto: un GradientTape graba el forward pass, calcula los gradientes de la pérdida respecto a todas las variables del modelo, y el optimizador (SGD, Adam...) las actualiza. fit() es este bucle, empaquetado.
Las tres APIs para construir modelos
TensorFlow/Keras ofrece tres maneras de definir un modelo. Dos ya las dominas:
- Sequential (la conoces desde 02-05)
Para pilas lineales de capas: entrada → capa → capa → salida.
model = keras.Sequential([
keras.layers.Dense(128, activation="relu"),
keras.layers.Dense(64, activation="relu"),
keras.layers.Dense(10, activation="softmax"),
])
- API funcional (la usaste en 03-03)
Para grafos con ramas, múltiples entradas/salidas o conexiones de salto, como el bloque residual que construimos:
inputs = keras.Input(shape=(784,))
x = keras.layers.Dense(128, activation="relu")(inputs)
x = keras.layers.Dense(64, activation="relu")(x)
outputs = keras.layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs, outputs)
- Subclassing (la nueva)
Defines una clase heredando de keras.Model y escribes tú mismo el forward pass en call(). Máxima flexibilidad: puedes usar bucles, condicionales, lógica arbitraria.
class ClasificadorTecnoMarket(keras.Model):
def __init__(self):
super().__init__()
self.densa1 = keras.layers.Dense(128, activation="relu")
self.densa2 = keras.layers.Dense(64, activation="relu")
self.salida = keras.layers.Dense(10, activation="softmax")
def call(self, x):
x = self.densa1(x) # tú decides el flujo de datos
x = self.densa2(x)
return self.salida(x)
model = ClasificadorTecnoMarket()- En
__init__declaras las capas (las piezas). - En
callconectas las piezas (el flujo). Se ejecuta en cada forward pass.
| API | Cuándo usarla | Flexibilidad | Ya vista en |
|---|---|---|---|
| Sequential | Pila lineal simple | Baja | 02-05 |
| Funcional | Grafos con ramas y saltos | Media-alta | 03-03 |
| Subclassing | Lógica arbitraria, investigación | Máxima | Esta lección |
Consejo: usa siempre la API más simple que resuelva tu problema. El subclassing te resultará familiar cuando veas PyTorch en la próxima lección: allí es la forma estándar de trabajar.
Pipelines de datos eficientes con tf.data
Hasta ahora pasábamos arrays numpy directamente a fit(). Funciona con MNIST, pero el catálogo real de TecnoMarket tiene cientos de miles de fotos y reseñas que no caben en memoria. tf.data construye pipelines: tuberías que cargan, transforman y sirven los datos por lotes, en paralelo con el entrenamiento.
import tensorflow as tf
# Simulamos reseñas de TecnoMarket ya vectorizadas (como en 04-03) y sus etiquetas
import numpy as np
resenas = np.random.rand(10000, 200).astype("float32") # 10k reseñas
etiquetas = np.random.randint(0, 2, size=(10000,)) # 0=negativa, 1=positiva
dataset = tf.data.Dataset.from_tensor_slices((resenas, etiquetas))
dataset = (dataset
.shuffle(buffer_size=10000) # 1. barajar (evita que el modelo memorice el orden)
.batch(32) # 2. agrupar en lotes de 32
.prefetch(tf.data.AUTOTUNE)) # 3. preparar el siguiente lote mientras se entrena
# Se usa directamente en fit, sin batch_size (ya lo lleva el dataset):
# model.fit(dataset, epochs=5)Qué hace cada paso y por qué importa:
shuffle(buffer_size): mantiene un buffer de elementos y extrae al azar. Si el CSV de reseñas está ordenado por producto, sin shuffle cada lote sería monotemático y el entrenamiento oscilaría. Idealmentebuffer_size≥ tamaño del dataset (si cabe).batch(32): el descenso de gradiente por mini-lotes de 02-04 necesita lotes; aquí se forman.prefetch(AUTOTUNE): mientras la GPU entrena con el lote N, la CPU prepara el lote N+1. Sin prefetch, la GPU espera parada a que lleguen los datos; con él, la tubería nunca se vacía.AUTOTUNEdeja que TensorFlow elija cuántos lotes anticipar.
Para las fotos de producto, el patrón habitual añade map para decodificar y transformar en paralelo:
def cargar_foto(ruta, etiqueta):
img = tf.io.read_file(ruta) # leer bytes del disco
img = tf.image.decode_jpeg(img, channels=3) # bytes -> tensor (h, w, 3)
img = tf.image.resize(img, [224, 224]) / 255.0 # tamaño fijo y normalizar
return img, etiqueta
ds_fotos = (tf.data.Dataset.from_tensor_slices((rutas, etiquetas_fotos))
.map(cargar_foto, num_parallel_calls=tf.data.AUTOTUNE) # en paralelo
.shuffle(1000)
.batch(32)
.prefetch(tf.data.AUTOTUNE))Así es como el equipo de TecnoMarket alimentaría la MobileNetV2 de 05-03 con su catálogo completo sin agotar la memoria: las imágenes se leen del disco justo cuando se necesitan.
Orden recomendado: map (transformar) → shuffle (barajar) → batch (agrupar) → prefetch (anticipar). Barajar antes de agrupar garantiza lotes distintos en cada época.
Callbacks útiles durante el entrenamiento
Los callbacks son objetos que Keras invoca en momentos clave del entrenamiento (fin de cada época, de cada lote...). Ya usaste EarlyStopping en 05-04; completemos el kit básico:
callbacks = [
# Ya lo conoces de 05-04: parar cuando la validación deja de mejorar
keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
# NUEVO: guardar automáticamente el mejor modelo visto hasta el momento
keras.callbacks.ModelCheckpoint(
"tecnomarket-dl/models/mejor_modelo.keras",
monitor="val_loss", # qué métrica vigilar
save_best_only=True), # solo sobrescribir si mejora
# NUEVO: registrar métricas para visualizarlas con TensorBoard
keras.callbacks.TensorBoard(log_dir="tecnomarket-dl/logs"),
]
# model.fit(dataset, validation_data=ds_val, epochs=50, callbacks=callbacks)ModelCheckpointconsave_best_only=Truees tu red de seguridad: aunque el entrenamiento se corte a mitad (Colab desconecta, se va la luz), el mejor modelo está a salvo en disco. Compara con elmodel.save()manual de 02-05: aquel guardaba al final; este guarda lo mejor, sobre la marcha.TensorBoardescribe logs que luego se visualizan como curvas interactivas; veremos cómo usarlo en la práctica en 06-04.- Los tres callbacks conviven sin problema en la misma lista: vigilan, guardan y registran a la vez.
Errores Comunes y Consejos
- Mezclar dtypes:
tf.constant(1) + tf.constant(1.0)falla (int32+float32). Solución:tf.cast(x, tf.float32). Consejo: escribe tus números con decimal (1.0) desde el principio. - Olvidar que las constantes no se rastrean: si defines un peso como
tf.constant,tape.gradientdevolveráNone. Los parámetros entrenables deben sertf.Variable. - Usar la cinta dos veces: por defecto
GradientTapese consume al llamar agradient(). Si necesitas varios gradientes de la misma grabación, crea la cinta conpersistent=True(y bórrala después condel tape). - Broadcasting silencioso: una operación entre
(n, 1)y(n,)produce(n, n)sin avisar. Imprimeresultado.shapecuando dudes; es gratis y evita horas de depuración. shufflecon buffer minúsculo:shuffle(10)sobre 100.000 reseñas apenas baraja. Usa un buffer del tamaño del dataset o, si no cabe, lo más grande posible.- Consejo:
tensor.numpy()convierte cualquier tensor a numpy para inspeccionarlo o graficarlo. Es tu puente de vuelta al mundo conocido.
Ejercicios
Ejercicio 1: tensores y broadcasting
TecnoMarket tiene 4 productos con precios [120.0, 15.5, 899.0, 45.0] y quiere aplicar tres escenarios de descuento: 5 %, 15 % y 30 %. Usando broadcasting (sin bucles), construye una tabla de shape (4, 3) donde cada fila sea un producto y cada columna un escenario con el precio final. Comprueba la shape del resultado.
Ejercicio 2: gradiente con GradientTape
Usa tf.GradientTape para calcular el gradiente de L(w, b) = (3*w + b - 10)**2 respecto a w y b, partiendo de w=1.0, b=0.0. Después escribe un bucle de descenso de gradiente (learning rate 0.01, 200 pasos) y comprueba que la pérdida final es casi cero. ¿Hay una única solución (w, b) posible?
Ejercicio 3: pipeline tf.data
Crea un tf.data.Dataset a partir de 1.000 muestras sintéticas (np.random.rand(1000, 20) como características y etiquetas binarias aleatorias) con shuffle, lotes de 64 y prefetch. Itera sobre él e imprime la shape de los dos primeros lotes. ¿Por qué el último lote de una época puede tener menos de 64 elementos y cómo lo evitarías?
Soluciones
Solución 1:
import tensorflow as tf
precios = tf.constant([[120.0], [15.5], [899.0], [45.0]]) # shape (4, 1)
factores = tf.constant([0.95, 0.85, 0.70]) # shape (3,)
tabla = precios * factores # broadcasting: (4,1) x (3,) -> (4,3)
print(tabla.shape) # (4, 3)
print(tabla.numpy().round(2))La clave es dar a los precios shape (4, 1) (columna): así cada dimensión de tamaño 1 se "estira" contra la otra.
Solución 2:
w = tf.Variable(1.0)
b = tf.Variable(0.0)
with tf.GradientTape() as tape:
loss = (3.0 * w + b - 10.0) ** 2
grads = tape.gradient(loss, [w, b])
print([g.numpy() for g in grads]) # [-42.0, -14.0]
# dL/dw = 2*(3w+b-10)*3 = 2*(-7)*3 = -42 ; dL/db = 2*(-7)*1 = -14
lr = 0.01
for _ in range(200):
with tf.GradientTape() as tape:
loss = (3.0 * w + b - 10.0) ** 2
gw, gb = tape.gradient(loss, [w, b])
w.assign_sub(lr * gw)
b.assign_sub(lr * gb)
print(loss.numpy()) # ~0.0
print(w.numpy(), b.numpy())No hay solución única: cualquier par con 3w + b = 10 anula la pérdida (una recta entera de soluciones). El descenso de gradiente encuentra una de ellas, la más cercana al punto de partida. Esto anticipa por qué en redes reales distintas inicializaciones dan distintos pesos finales con rendimiento similar.
Solución 3:
import numpy as np, tensorflow as tf
X = np.random.rand(1000, 20).astype("float32")
y = np.random.randint(0, 2, size=(1000,))
ds = (tf.data.Dataset.from_tensor_slices((X, y))
.shuffle(1000)
.batch(64)
.prefetch(tf.data.AUTOTUNE))
for i, (xb, yb) in enumerate(ds.take(2)):
print(f"Lote {i}: X {xb.shape}, y {yb.shape}") # (64, 20) y (64,)1000 / 64 = 15 lotes completos y un resto de 40: el último lote tiene 40 elementos. Si tu código exige lotes de tamaño fijo, usa batch(64, drop_remainder=True) (a cambio de descartar esas 40 muestras en cada época).
Conclusión
Has abierto el capó de la herramienta que llevabas usando todo el curso: TensorFlow representa los datos como tensores (constantes para datos, variables para pesos), calcula gradientes con GradientTape —el mismo backpropagation de 02-03, automatizado—, ofrece tres APIs de construcción (Sequential, funcional y subclassing, de menos a más flexible), alimenta los modelos con pipelines tf.data (shuffle → batch → prefetch) y vigila el entrenamiento con callbacks como ModelCheckpoint. Con esto, model.fit() ha dejado de ser magia: es un bucle de GradientTape con extras.
En la siguiente lección conocerás al otro gigante: PyTorch, el framework donde ese bucle de entrenamiento no está empaquetado sino que lo escribes tú, línea a línea. Verás que todo lo aprendido hoy —tensores, autograd, subclassing— tiene su gemelo directo allí, y reconstruiremos la red MNIST de 02-05 para comprobarlo.
Curso de Deep Learning
Módulo 1: Introducción a Deep Learning
- ¿Qué es Deep Learning?
- Historia y evolución del Deep Learning
- Aplicaciones de Deep Learning
- Conceptos básicos de redes neuronales
- Preparación del entorno de trabajo
Módulo 2: Fundamentos de Redes Neuronales
- Perceptrón y Perceptrón Multicapa
- Función de activación
- Propagación hacia adelante y hacia atrás
- Optimización y función de pérdida
- Tu primera red neuronal completa
Módulo 3: Redes Neuronales Convolucionales (CNN)
- Introducción a las CNN
- Capas convolucionales y de pooling
- Arquitecturas populares de CNN
- Aplicaciones de CNN en reconocimiento de imágenes
Módulo 4: Redes Neuronales Recurrentes (RNN)
- Introducción a las RNN
- LSTM y GRU
- Aplicaciones de RNN en procesamiento del lenguaje natural
- Secuencias y series temporales
Módulo 5: Técnicas Avanzadas en Deep Learning
- Redes Generativas Adversariales (GAN)
- Autoencoders
- Transfer Learning
- Regularización y técnicas de mejora
- Mecanismos de atención y Transformers
Módulo 6: Herramientas y Frameworks
- Introducción a TensorFlow
- Introducción a PyTorch
- Comparación de frameworks
- Entornos de desarrollo y recursos adicionales
- Guardar, cargar y desplegar modelos
Módulo 7: Proyectos Prácticos
- Clasificación de imágenes con CNN
- Generación de texto con RNN
- Detección de anomalías con Autoencoders
- Creación de una GAN para generación de imágenes
- Fine-tuning de un modelo preentrenado
