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

  1. Qué es TensorFlow y su ecosistema
  2. Tensores: constantes, variables, shapes y dtypes
  3. Broadcasting: operar con formas distintas
  4. Diferenciación automática con tf.GradientTape
  5. Las tres APIs para construir modelos
  6. Pipelines de datos eficientes con tf.data
  7. 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 tensorflow en 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 enteros int32. No se pueden mezclar dtypes en una operación sin convertir antes con tf.cast.
  • Un tf.constant es 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 numpy
tf.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 datos

Recuerda 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 descuento

Reglas prácticas (se comparan las shapes de derecha a izquierda):

  1. Dos dimensiones son compatibles si son iguales o si una de ellas es 1.
  2. Las dimensiones que faltan se tratan como 1.
  3. 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) ** 2 no 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 cambia loss si muevo w un 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-03

Comparació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:

  1. 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"),
])

  1. 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)

  1. 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 call conectas 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. Idealmente buffer_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. AUTOTUNE deja 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)
  • ModelCheckpoint con save_best_only=True es 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 el model.save() manual de 02-05: aquel guardaba al final; este guarda lo mejor, sobre la marcha.
  • TensorBoard escribe 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.gradient devolverá None. Los parámetros entrenables deben ser tf.Variable.
  • Usar la cinta dos veces: por defecto GradientTape se consume al llamar a gradient(). Si necesitas varios gradientes de la misma grabación, crea la cinta con persistent=True (y bórrala después con del tape).
  • Broadcasting silencioso: una operación entre (n, 1) y (n,) produce (n, n) sin avisar. Imprime resultado.shape cuando dudes; es gratis y evita horas de depuración.
  • shuffle con 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

Módulo 2: Fundamentos de Redes Neuronales

Módulo 3: Redes Neuronales Convolucionales (CNN)

Módulo 4: Redes Neuronales Recurrentes (RNN)

Módulo 5: Técnicas Avanzadas en Deep Learning

Módulo 6: Herramientas y Frameworks

Módulo 7: Proyectos Prácticos

Módulo 8: Consideraciones Éticas y Futuro del Deep Learning

© Copyright 2026. Todos los derechos reservados