💻 Tutorial paso a paso

Primeros pasos con TensorFlow

Construiremos una red neuronal desde cero con TensorFlow y Keras: cargar datos, diseñar un MLP, entrenarlo, guardarlo y usarlo para predecir. Cada línea de código está explicada.

⏱️ ~25 min 📊 Nivel: principiante 🧠 Framework: TensorFlow 2.x + Keras

Requisitos previos

  • Python 3.8 o superior instalado
  • Conocimientos básicos de Python (funciones, listas, bucles)
  • Saber qué es una red neuronal (consulta la teoría del perceptrón y el MLP)
1

Instalación e imports

Lo primero es instalar TensorFlow. Desde tu terminal:

Terminal
pip install tensorflow

Ahora importamos las librerías que vamos a usar. TensorFlow incluye Keras como su API de alto nivel — es con la que trabajaremos:

Python train_model.py
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

print(f"TensorFlow version: {tf.__version__}")
print(f"GPU disponible: {len(tf.config.list_physical_devices('GPU')) > 0}")
L1tensorflow — el framework completo. Se importa como tf por convención.
L2keras — la API de alto nivel de TensorFlow. Nos permite definir modelos y capas de forma simple.
L3layers — contiene las capas predefinidas: Dense, Dropout, Conv2D, etc.
L4numpy — para manipular arrays. TensorFlow trabaja con tensores, pero NumPy es útil para preparar datos.
Salida esperada TensorFlow version: 2.17.0 GPU disponible: True
💡 Nota: Si la GPU aparece como False, TensorFlow usará la CPU. Funciona igualmente, pero será más lento para modelos grandes. Para este tutorial no importa.
2

Cargar y preparar los datos

Usaremos el dataset MNIST: 70,000 imágenes de dígitos manuscritos (0-9), cada una de 28×28 píxeles en escala de grises. Es el dataset clásico para empezar con redes neuronales.

Python cargar datos
# Cargar MNIST (viene incluido en Keras)
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

# Ver las dimensiones
print(f"Train: {x_train.shape}, Labels: {y_train.shape}")
print(f"Test:  {x_test.shape}, Labels: {y_test.shape}")
print(f"Rango de píxeles: [{x_train.min()}, {x_train.max()}]")
print(f"Ejemplo de label: {y_train[0]}")
Salida Train: (60000, 28, 28), Labels: (60000,) Test: (10000, 28, 28), Labels: (10000,) Rango de píxeles: [0, 255] Ejemplo de label: 5

Ahora necesitamos preprocesar los datos. Para un MLP necesitamos dos cosas:

  1. Aplanar las imágenes: de (28, 28) a (784,) — un MLP espera vectores, no matrices
  2. Normalizar los píxeles: de [0, 255] a [0, 1] — dividiendo entre 255
Python preprocesar
# Aplanar: (60000, 28, 28) → (60000, 784)
x_train = x_train.reshape(-1, 784).astype("float32")
x_test  = x_test.reshape(-1, 784).astype("float32")

# Normalizar a [0, 1]
x_train /= 255.0
x_test  /= 255.0

print(f"Shape tras preprocesado: {x_train.shape}")
print(f"Nuevo rango: [{x_train.min():.1f}, {x_train.max():.1f}]")
L2reshape(-1, 784) — el -1 deja que NumPy calcule esa dimensión automáticamente (será 60000). 28×28 = 784.
L2astype("float32") — convierte de enteros (uint8) a decimales. Las redes necesitan números decimales.
L6Dividir entre 255 escala los píxeles a [0, 1]. Esto ayuda a que la red aprenda más rápido y de forma estable.
Salida Shape tras preprocesado: (60000, 784) Nuevo rango: [0.0, 1.0]

La normalización es fundamental en deep learning por varias razones:

  • Los pesos de la red se inicializan como valores pequeños (~0.01). Si los inputs son grandes (0-255), las multiplicaciones producen valores enormes → gradientes inestables.
  • Con datos en [0, 1], el optimizador puede usar learning rates estándar (como 0.001) sin problemas.
  • Diferentes features con diferentes rangos harían que la red preste más atención a las de mayor magnitud, sesgando el aprendizaje.

Regla: normaliza siempre tus datos antes de entrenar. Las técnicas más comunes son dividir entre el máximo (Min-Max) o estandarizar (media 0, std 1).

3

Diseñar el modelo (MLP)

Ahora viene lo más interesante: definir la arquitectura de nuestra red neuronal. Usaremos un MLP (Multi-Layer Perceptron) sencillo con dos capas ocultas.

En Keras, la forma más fácil de construir un modelo es con Sequential: defines las capas en orden, como una lista.

Python definir modelo
model = keras.Sequential([
    layers.Dense(128, activation="relu", input_shape=(784,)),
    layers.Dropout(0.2),
    layers.Dense(64, activation="relu"),
    layers.Dropout(0.2),
    layers.Dense(10, activation="softmax"),
])

model.summary()
L1Sequential — modelo secuencial: la salida de cada capa es la entrada de la siguiente.
L2Dense(128, activation="relu") — capa densa (fully connected) con 128 neuronas y activación ReLU. input_shape=(784,) indica que la entrada tiene 784 features.
L3Dropout(0.2) — durante el entrenamiento, «apaga» aleatoriamente el 20% de las neuronas en cada paso. Es una técnica de regularización que previene el overfitting.
L4Segunda capa oculta con 64 neuronas. Ir reduciendo el número de neuronas es un patrón habitual (embudo).
L6Dense(10, activation="softmax") — capa de salida. 10 neuronas (una por dígito 0-9). softmax convierte las salidas en probabilidades que suman 1.
Salida de model.summary() Model: "sequential" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ │ dense (Dense) │ (None, 128) │ 100,480 │ │ dropout (Dropout) │ (None, 128) │ 0 │ │ dense_1 (Dense) │ (None, 64) │ 8,256 │ │ dropout_1 (Dropout) │ (None, 64) │ 0 │ │ dense_2 (Dense) │ (None, 10) │ 650 │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┛ Total params: 109,386 (427.29 KB)

Cada capa Dense tiene pesos + biases:

  • Dense(128): 784 × 128 pesos + 128 biases = 100,480
  • Dense(64): 128 × 64 + 64 = 8,256
  • Dense(10): 64 × 10 + 10 = 650

Dropout no tiene parámetros: solo «apaga» neuronas al azar.

Total: 100,480 + 8,256 + 650 = 109,386 parámetros. Esto es un modelo pequeño — GPT-4 tiene cientos de miles de millones.

4

Compilar el modelo

Antes de entrenar, necesitamos compilar el modelo — esto significa decirle cómo aprender. Tres decisiones:

  1. Optimizador: el algoritmo que ajusta los pesos (Adam es lo estándar)
  2. Función de pérdida: cómo medir el error del modelo
  3. Métricas: qué queremos monitorizar durante el entrenamiento
Python compilar
model.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)
L2"adam" — Adam optimizer. Combina momentum y learning rates adaptativos. Es el optimizador por defecto en la mayoría de proyectos.
L3"sparse_categorical_crossentropy" — pérdida para clasificación multiclase cuando las labels son enteros (0, 1, ..., 9). Si las labels fueran one-hot, usaríamos "categorical_crossentropy".
L4"accuracy" — porcentaje de predicciones correctas. Se calcula en cada epoch para train y validación.
💡 Nota: model.compile() no entrena nada todavía — solo configura el optimizador y la loss. Es como «preparar la cocina antes de cocinar». El entrenamiento real ocurre en el siguiente paso con model.fit().
5

Entrenar el modelo

¡Hora de entrenar! Con model.fit(), Keras se encarga de todo el loop de entrenamiento: forward pass, cálculo de la loss, backward pass, actualización de pesos. Todo automático.

Python entrenar
history = model.fit(
    x_train, y_train,
    epochs=10,
    batch_size=32,
    validation_split=0.1,
    verbose=1,
)
L2Datos de entrenamiento: las 60,000 imágenes y sus labels.
L3epochs=10 — el modelo verá el dataset completo 10 veces. Más epochs = más aprendizaje (hasta cierto punto).
L4batch_size=32 — en cada paso, procesa 32 imágenes a la vez. Más grande = más rápido pero necesita más memoria.
L5validation_split=0.1 — reserva el 10% de los datos de train para validación. Así vemos si el modelo generaliza bien o hace overfitting.
L1history — guarda las métricas de cada epoch (loss, accuracy) para poder graficarlas después.
Salida (ejemplo) Epoch 1/10 1688/1688 ━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8613 - loss: 0.4762 - val_accuracy: 0.9607 - val_loss: 0.1355 Epoch 2/10 1688/1688 ━━━━━━━━━━━━━━━━ 2s 1ms/step - accuracy: 0.9548 - loss: 0.1524 - val_accuracy: 0.9700 - val_loss: 0.1003 ... Epoch 10/10 1688/1688 ━━━━━━━━━━━━━━━━ 2s 1ms/step - accuracy: 0.9812 - loss: 0.0598 - val_accuracy: 0.9778 - val_loss: 0.0742
  • accuracy: 0.9812 — el modelo acierta el 98.12% de los datos de entrenamiento.
  • loss: 0.0598 — la función de pérdida (cross-entropy). Más bajo = mejor.
  • val_accuracy: 0.9778 — accuracy en los datos de validación (datos que el modelo NO ha visto durante el entrenamiento).
  • val_loss: 0.0742 — loss de validación. Si esta sube mientras train baja → overfitting.

En este caso, val_accuracy ≈ train_accuracy, así que no hay overfitting significativo. ¡Buen modelo!

6

Evaluar el modelo

Evaluamos el modelo con los datos de test — datos completamente nuevos que el modelo nunca ha visto, ni siquiera durante la validación. Esto nos da la métrica real de rendimiento:

Python evaluar
test_loss, test_accuracy = model.evaluate(x_test, y_test, verbose=0)

print(f"Test loss:     {test_loss:.4f}")
print(f"Test accuracy: {test_accuracy:.4f}")
print(f"Acierta {test_accuracy*100:.1f}% de los dígitos que nunca ha visto")
Salida Test loss: 0.0734 Test accuracy: 0.9778 Acierta 97.8% de los dígitos que nunca ha visto

¡97.8% de accuracy con un MLP tan sencillo! No está mal para un modelo con apenas 109K parámetros. Con una CNN podríamos superar el 99%.

7

Guardar el modelo

El modelo entrenado vive en memoria RAM. Si cierras Python, se pierde todo. Necesitamos guardarlo en disco para poder reutilizarlo después:

Python guardar
# Opción 1: Formato Keras nativo (.keras) — RECOMENDADO
model.save("mi_modelo_mnist.keras")
print("✅ Modelo guardado como mi_modelo_mnist.keras")

# Opción 2: Solo los pesos (sin la arquitectura)
model.save_weights("pesos_mnist.weights.h5")
print("✅ Pesos guardados como pesos_mnist.weights.h5")
L2.save("archivo.keras") — guarda todo: arquitectura + pesos + configuración del optimizador. Puedes cargar y seguir entrenando.
L6.save_weights() — solo guarda los valores numéricos de los pesos. Necesitarás volver a definir la arquitectura para cargarlos.
💡 Nota: El formato .keras es el recomendado desde TensorFlow 2.16+. Anteriormente se usaba .h5 (HDF5) o SavedModel (directorio). Los tres siguen funcionando, pero .keras es el estándar actual.
8

Cargar y usar el modelo guardado

En un escenario real, guardarías el modelo al final del entrenamiento y lo cargarías en un script (o API) separado para hacer predicciones. Así se carga:

Python cargar modelo (nuevo script)
# En un nuevo script o sesión de Python:
import tensorflow as tf
from tensorflow import keras

# Cargar el modelo completo
loaded_model = keras.models.load_model("mi_modelo_mnist.keras")

# Verificar que funciona
loaded_model.summary()

# Evaluar con los datos de test (misma accuracy que antes)
test_loss, test_acc = loaded_model.evaluate(x_test, y_test, verbose=0)
print(f"Modelo cargado → accuracy: {test_acc:.4f}")
Salida Modelo cargado → accuracy: 0.9778

La misma accuracy. El modelo cargado es idéntico al original. Puedes distribuir el archivo .keras a tus compañeros, subirlo a producción, etc.

9

Hacer predicciones

Ya tenemos nuestro modelo entrenado y cargado. Vamos a usarlo para clasificar dígitos nuevos:

Python predecir
# Predecir las primeras 5 imágenes del test set
predictions = loaded_model.predict(x_test[:5], verbose=0)

# 'predictions' tiene shape (5, 10) — 10 probabilidades por imagen
for i in range(5):
    predicted_digit = np.argmax(predictions[i])
    confidence = predictions[i][predicted_digit] * 100
    real_digit  = y_test[i]
    status = "✅" if predicted_digit == real_digit else "❌"
    print(f"{status} Imagen {i}: predicho={predicted_digit} "
          f"(confianza: {confidence:.1f}%) | real={real_digit}")
L2.predict() devuelve las probabilidades para cada clase. Shape: (n_samples, n_classes).
L6np.argmax() — devuelve el índice de la probabilidad más alta → el dígito predicho.
L7La confianza es la probabilidad asignada a la clase ganadora. Más alto = el modelo está más seguro.
Salida ✅ Imagen 0: predicho=7 (confianza: 99.9%) | real=7 ✅ Imagen 1: predicho=2 (confianza: 99.8%) | real=2 ✅ Imagen 2: predicho=1 (confianza: 99.7%) | real=1 ✅ Imagen 3: predicho=0 (confianza: 99.9%) | real=0 ✅ Imagen 4: predicho=4 (confianza: 99.2%) | real=4

¡Todas correctas y con alta confianza! Nuestro MLP reconoce dígitos manuscritos con un 97-98% de precisión general.

10

Script completo

Aquí tienes el script completo que puedes copiar y ejecutar directamente. Incluye todo lo que hemos visto: imports → datos → modelo → entrenamiento → guardado → carga → predicción.

📄 Script: mnist_tensorflow.py

Python mnist_tensorflow.py
"""
Primeros pasos con TensorFlow: MLP para clasificar dígitos MNIST.
"""
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

# ── 1. Cargar datos ──────────────────────────────────────
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

# ── 2. Preprocesar ──────────────────────────────────────
x_train = x_train.reshape(-1, 784).astype("float32") / 255.0
x_test  = x_test.reshape(-1, 784).astype("float32") / 255.0

# ── 3. Definir modelo ───────────────────────────────────
model = keras.Sequential([
    layers.Dense(128, activation="relu", input_shape=(784,)),
    layers.Dropout(0.2),
    layers.Dense(64, activation="relu"),
    layers.Dropout(0.2),
    layers.Dense(10, activation="softmax"),
])

# ── 4. Compilar ─────────────────────────────────────────
model.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

# ── 5. Entrenar ─────────────────────────────────────────
history = model.fit(
    x_train, y_train,
    epochs=10,
    batch_size=32,
    validation_split=0.1,
    verbose=1,
)

# ── 6. Evaluar ──────────────────────────────────────────
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
print(f"\n🎯 Test accuracy: {test_acc:.4f}")

# ── 7. Guardar ──────────────────────────────────────────
model.save("mi_modelo_mnist.keras")
print("💾 Modelo guardado")

# ── 8. Cargar ───────────────────────────────────────────
loaded = keras.models.load_model("mi_modelo_mnist.keras")

# ── 9. Predecir ─────────────────────────────────────────
preds = loaded.predict(x_test[:5], verbose=0)
for i in range(5):
    digit = np.argmax(preds[i])
    conf  = preds[i][digit] * 100
    print(f"{'✅' if digit == y_test[i] else '❌'} "
          f"Predicho: {digit} ({conf:.1f}%) | Real: {y_test[i]}")