Redes Neuronales desde Cero: Guía Práctica con Python y TensorFlow
Aprende qué son las redes neuronales, cómo funcionan por dentro y construye tu primera red en Python con TensorFlow para clasificar dígitos escritos a mano.
Respuesta rápida: Una red neuronal es un bucle de prueba-error muy bien organizado: metes datos, la red predice, le dices cuánto se ha equivocado, y ella ajusta sus parámetros internos sola. No es magia. En este tutorial construyes una desde cero en Python con TensorFlow y la entrenas para reconocer dígitos escritos a mano con más del 96% de precisión.
¿Qué es el machine learning y por qué importa?
Si llevas un tiempo aprendiendo a programar, seguramente ya te has topado con frases como "inteligencia artificial", "deep learning" o "redes neuronales". Suenan a ciencia ficción. Suenan a que necesitas un doctorado para entenderlas. No es así.
Machine learning es una forma de programar donde no le dices al ordenador exactamente qué hacer paso a paso. En lugar de eso, le das ejemplos y él solito encuentra las reglas. Es como cuando aprendiste a distinguir un gato de un perro: nadie te dio una lista de instrucciones, solo te enseñaron muchos animales y tu cerebro fue encontrando patrones.
Una red neuronal es una manera concreta de hacer machine learning. Está inspirada en cómo funciona el cerebro (de ahí lo de "neural"), pero no construimos un cerebro real. Construimos una máquina de sumar y multiplicar números que, si la organizas bien, aprende cosas sorprendentes.
Red neuronal artificial inspirada en el cerebro: nodos interconectados organizados en capas que procesan y transforman información
Piensa en una red neuronal como una máquina transformadora: metes datos por un lado (por ejemplo, píxeles de una imagen de 28×28), la máquina los transforma capa por capa, y por el otro lado sale una predicción ("esto es un 4"). Si la predicción es incorrecta, la máquina se reajusta sola. Eso es todo.
Lo que necesitas instalar
No hace falta un superordenador. Con cualquier portátil de menos de cinco años te vale.
| Herramienta | Para qué sirve |
|---|---|
| Python 3.8+ | El lenguaje. Fácil de leer, universal en machine learning. |
| TensorFlow | La librería de Google con todo el código de redes neuronales ya hecho. |
| NumPy | Operaciones con matrices. Detrás de cada red neuronal hay mucha multiplicación de matrices. |
| Matplotlib | Para dibujar gráficos y visualizar las imágenes. |
| Jupyter Notebook | Entorno interactivo donde escribes código y ves resultados al lado. |
Instala todo de una vez:
pip install tensorflow numpy matplotlib jupyter
Verifica que funciona:
python -c "import tensorflow as tf; print(tf.__version__)"
Si ves un número como 2.15.0, estás listo. Si ves un error, búscalo en Google — el 99% de las veces es una versión vieja de Python o un problema de permisos.
Cómo piensa una neurona artificial
La neurona natural vs. la artificial: una explicación más detallada
Vamos a profundizar en esto. Porque si entiendes bien la neurona artificial, el resto de la red neuronal solo es juntar muchas de estas neuronas. Es como aprender a hacer una pieza de Lego: una vez la dominas, puedes construir castillos enteros.
La neurona natural (la del cerebro, la biológica)
Antes de hablar de la artificial, conviene saber de dónde viene la inspiración. Tu cerebro tiene unos 86 mil millones de neuronas. Cada una es una célula diminuta con una estructura más o menos así:
- Dendritas: son como antenas. Reciben señales de otras neuronas.
- Soma (cuerpo celular): aquí se acumulan las señales. Si la suma supera un cierto umbral, la neurona "decide" activarse.
- Axón: es el cable que transmite la señal hacia otras neuronas si la neurona se ha activado.
- Sinapsis: es el espacio entre el axón de una neurona y la dendrita de otra. Aquí es donde ocurre la "magia": la señal puede ser más fuerte o más débil según la conexión.
La idea clave es esta: una neurona recibe muchas señales, las suma, y si el total es suficientemente grande, se activa y envía su propia señal.
Esto es importantísimo. El cerebro no funciona con instrucciones escritas. Funciona con suma de influencias. Un montón de pequeñas decisiones que se juntan y producen una salida.
Estructura de una neurona biológica: dendritas que reciben señales, soma que las integra, axón que transmite la salida y sinapsis que conecta con otras neuronas
La neurona artificial (la que construimos en el ordenador)
Una neurona artificial es una imitación muy simplificada de la biológica. No tiene nada de vida, pero imita la lógica de "sumar entradas, aplicar un umbral, y producir una salida".
Una neurona artificial tiene cuatro componentes:
- Entradas (inputs): son números. Pueden ser cualquier cosa: píxeles de una imagen, valores de sensores, resultados de otras neuronas.
- Pesos (weights): son números que multiplican a cada entrada. Cada entrada tiene su propio peso.
- Sesgo (bias): es un número que se suma después de multiplicar todas las entradas por sus pesos.
- Función de activación: es una regla que decide, a partir del resultado de la suma, qué número sale finalmente de la neurona.
Matemáticamente, la neurona calcula primero una suma ponderada:
Después aplica una función de activación que determina la salida:
Esta función es el equivalente al umbral biológico: decide si la neurona "se enciende" y con qué intensidad. No te asustes por la notación. Es solo multiplicar cada entrada por su peso , sumar todo, añadir el sesgo , y aplicar una regla .
De la neurona real a la neurona artificial:
| Neurona biológica | Neurona artificial |
|---|---|
| Dendritas (entradas) | Entradas |
| Soma (integración) | Suma ponderada |
| Umbral de activación | Función de activación |
| Axón (señal de salida) | Salida |
| Fuerza sináptica | Pesos |
| Excitabilidad basal | Sesgo |
Diagrama de una neurona artificial: entradas con pesos, suma ponderada con sesgo y función de activación que produce la salida
Desglose de cada componente (con ejemplos de verdad)
Vamos a ver cada una de las cuatro partes con más calma. Vamos a usar el ejemplo de la chaqueta, pero lo vamos a hacer bien, con números reales, para que veas cómo se aplica cada parámetro.
Las entradas
Las entradas son los datos que le llegan a la neurona. En nuestro ejemplo de la chaqueta, la neurona recibe dos entradas:
- Entrada 1: la temperatura. Es un número. Podemos usar grados Celsius: 15 grados, -2 grados, 30 grados...
- Entrada 2: está lloviendo. Esto no es un número, pero lo convertimos. Digamos que "1" significa "sí, llueve" y "0" significa "no, no llueve".
Ya tenemos nuestras dos entradas. Podríamos tener más: "está nevando", "hay viento fuerte", "voy a salir de noche", etc. Cada nueva característica que quieras considerar es una nueva entrada.
Lo importante: las entradas tienen que ser números. Si no lo son (como "lloviendo" o "soleado"), los transformas en números. Eso se llama "codificar".
Los pesos
Cada entrada tiene un peso asociado. El peso es un número que multiplica a la entrada. ¿Qué significa ese número? Importancia. Si un peso es grande (por ejemplo 10), esa entrada influye muchísimo en el resultado. Si un peso es pequeño (por ejemplo 0.01), esa entrada casi no afecta. Si el peso es cero, esa entrada no afecta nada (la neurona la ignora por completo). Y si el peso es negativo, esa entrada influye en dirección contraria.
Ejemplo con números en nuestra neurona de la chaqueta:
Supongamos que la neurona empieza con pesos aleatorios. Al principio no sabe nada, así que los asigna al azar:
- Peso para la temperatura:
w_temp = 0.2 - Peso para "está lloviendo":
w_lluvia = 0.5
¿Qué significa esto? Que de momento, la neurona cree que la lluvia es más importante que la temperatura para decidir si ponerse chaqueta. Está equivocada, pero es un comienzo.
Si después de aprender, los pesos se ajustan. La neurona podría terminar con:
w_temp = -0.8(temperatura fría necesita chaqueta, pero con signo negativo cuidado)w_lluvia = 0.3
Realmente, lo que aprenderá es que cuando la temperatura es baja (número pequeño), el producto temp * w_temp será grande si el peso es negativo. No te lies con el signo. Lo importante: el peso decide cuánto influye cada entrada.
¿Cómo se ajustan los pesos? Aquí está la magia. Cada vez que la neurona se equivoca, calcula qué pesos han contribuido al error. Si un peso hizo que el error fuera grande, lo reduce un poco. Si otro peso ayudó a acertar, lo aumenta un poco. Esto es el "descenso del gradiente", pero no necesitas entenderlo del todo ahora. Solo recuerda: los pesos no los pone un humano. La neurona los aprende sola.
El sesgo (bias)
El sesgo es otro número que se suma después de multiplicar todas las entradas por sus pesos. Pero a diferencia de los pesos, el sesgo no está multiplicado por ninguna entrada. Es un número independiente que la neurona también aprende.
¿Para qué sirve el sesgo? Para ajustar el umbral de activación. Sin sesgo, la neurona solo se activaría si la suma de (entrada * peso) supera un cierto valor. Con el sesgo, puedes mover ese umbral hacia arriba o hacia abajo.
Ejemplo con la chaqueta:
Imagina un día de primavera: temperatura 18 grados, no llueve. Es un día agradable. La suma ponderada podría ser baja. Pero el sesgo puede ser negativo (por ejemplo -2) para que la neurona no se active a la mínima. O puede ser positivo si quieres que la neurona se active más fácilmente.
El sesgo es como la predisposición de la neurona. Si el sesgo es muy positivo, la neurona se activará casi siempre. Si es muy negativo, casi nunca.
En la práctica: En el código que escribimos antes, cada capa Dense tiene su propio sesgo. No lo vimos explícitamente, pero está ahí. Cuando ponemos layers.Dense(128, activation='relu'), esa capa tiene 128 neuronas, cada una con su propio sesgo. La red aprende esos 128 números.
La función de activación
Aquí es donde ocurre la "decisión". Después de sumar las entradas ponderadas y añadir el sesgo, tenemos un número. Ese número puede ser cualquier cosa: -10, 3.5, 0.001, 1000... La función de activación toma ese número y lo transforma en otro número, normalmente entre 0 y 1 o entre -1 y 1.
¿Por qué necesitamos una función de activación? Por dos razones fundamentales:
- Introducir no linealidad. Si solo sumaras y multiplicaras, la red entera sería una función lineal. Una función lineal solo puede aprender relaciones lineales (como "a más X, más Y"). El mundo real no es lineal. Las funciones de activación "rompen" la linealidad y permiten que la red aprenda patrones complicados.
- Limitar la salida. Queremos que la salida de las neuronas esté en un rango manejable. No queremos que una neurona devuelva 10.000 porque desajustaría todo.
Las funciones de activación más comunes (con ejemplos):
ReLU (Rectified Linear Unit)
Es la más usada hoy en día. Su regla es sencillísima:
- Si la entrada es negativa → salida 0
- Si la entrada es positiva → salida el mismo número
Ejemplo: Si el resultado bruto es -3.2, ReLU devuelve 0. Si es 2.5, devuelve 2.5.
Parece demasiado simple, pero funciona increíblemente bien. Su principal ventaja: es muy rápida de calcular y evita problemas de "gradientes que desaparecen".
En nuestra red de ejemplo, usamos ReLU en la capa oculta: activation='relu'.
Sigmoid (o logística)
Esta es la clásica. Su salida está siempre entre 0 y 1. Es perfecta para cuando quieres interpretar la salida como una probabilidad.
Ejemplo: Entrada -5 → salida cerca de 0. Entrada 0 → salida 0.5. Entrada 5 → salida cerca de 1.
Tiene un problema: para números muy grandes o muy pequeños, la pendiente es casi cero, y la neurona deja de aprender. Por eso hoy se usa menos en capas ocultas, pero sigue siendo común en la capa de salida para clasificación binaria.
Tanh (tangente hiperbólica)
Es muy parecida a sigmoid, pero su salida está entre -1 y 1. Es simétrica, lo que a veces ayuda a que el aprendizaje sea más estable.
Ejemplo: Entrada -5 → salida cerca de -1. Entrada 0 → salida 0. Entrada 5 → salida cerca de 1.
Softmax (caso especial)
Esta no se aplica a una neurona sola. Se aplica a un grupo de neuronas de salida. Lo que hace es convertir un vector de números en probabilidades que suman 1.
Ejemplo: Entrada [2, 1, 0.5] → salida [0.59, 0.24, 0.17] (aproximadamente). La neurona con el valor más alto tiene la probabilidad más alta, pero todas las neuronas tienen un valor.
En nuestra red del MNIST, usamos softmax en la última capa: activation='softmax'. Así podemos decir "la red cree que esto es un 5 con un 85% de probabilidad".
El ejemplo de la chaqueta paso a paso (con números reales)
Vamos a hacer el ejemplo completo de la neurona que decide si ponerse chaqueta. Vamos a usar números de verdad para que veas cómo se aplica cada parámetro.
Definimos la neurona:
- Dos entradas: temperatura (grados Celsius) y lluvia (1 = sí, 0 = no)
- Pesos iniciales (aleatorios):
w_temp = 0.2,w_lluvia = 0.5 - Sesgo inicial (aleatorio):
bias = -0.3 - Función de activación: sigmoid (devuelve entre 0 y 1). Interpretamos >0.5 como "ponerse chaqueta".
Caso 1: Hace frío y llueve
- Temperatura: 5 grados
- Llueve: 1
- Cálculo bruto:
(5 * 0.2) + (1 * 0.5) + (-0.3)=1 + 0.5 - 0.3=1.2 - Aplicamos sigmoid a 1.2: sigmoid(1.2) ≈ 0.77
- 0.77 > 0.5 → La neurona dice "sí, chaqueta".
- Acierto. Con frío y lluvia, chaqueta. Por ahora bien.
Caso 2: Hace calor y no llueve
- Temperatura: 28 grados
- Llueve: 0
- Cálculo bruto:
(28 * 0.2) + (0 * 0.5) + (-0.3)=5.6 + 0 - 0.3=5.3 - sigmoid(5.3) ≈ 0.995
- 0.995 > 0.5 → La neurona dice "sí, chaqueta".
- ERROR. Con 28 grados y sol, no te pones chaqueta. La neurona se ha equivocado estrepitosamente.
¿Por qué ha fallado? Porque el peso de la temperatura es positivo (0.2), así que cuando la temperatura sube, el resultado bruto sube, y la neurona se activa más. Eso es justo lo contrario de lo que queremos. Queremos que la neurona se active con temperaturas bajas, no con altas.
El aprendizaje: Cuando la neurona se equivoca, ajusta los pesos para que la próxima vez falle menos. En este caso, debería hacer que w_temp sea negativo. Así, cuando la temperatura suba (números grandes), el producto será muy negativo y reducirá el resultado bruto.
Después de muchos ejemplos (días fríos, días calurosos, con lluvia y sin ella), los pesos convergerán a algo como:
w_temp = -0.8(temperatura alta reduce la activación)w_lluvia = 0.6(lluvia aumenta la activación)bias = 0.2(sesgo ligeramente positivo)
Con esos pesos, el caso de calor y sol daría: (28 * -0.8) + (0 * 0.6) + 0.2 = -22.4 + 0 + 0.2 = -22.2, sigmoid ≈ 0. La neurona dice "no chaqueta". Acierta. El caso de frío y lluvia: (5 * -0.8) + (1 * 0.6) + 0.2 = -4 + 0.6 + 0.2 = -3.2, sigmoid ≈ 0.04. Espera, eso también da "no chaqueta". Algo falla. Un momento.
Aquí hay una lección importante: con una sola neurona solo puedes aprender relaciones lineales simples. La relación entre temperatura y chaqueta no es lineal del todo. Porque tanto el frío extremo como el calor extremo requieren chaqueta (en el calor extremo quizás no, pero bueno, el ejemplo es limitado). Una neurona sola tiene límites. Por eso se usan redes con muchas neuronas y varias capas. Las capas ocultas permiten aprender relaciones más complejas.
Pero el principio es el mismo: cada neurona recibe entradas, las multiplica por pesos, añade un sesgo, y pasa el resultado por una función de activación.
Una neurona artificial en código (para que lo veas funcionar)
Vamos a construir una neurona a mano, solo con NumPy, sin usar Keras. Así ves que por dentro no hay magia, solo multiplicaciones y sumas.
import numpy as np
def sigmoid(x):
"""Función de activación sigmoid"""
return 1 / (1 + np.exp(-x))
class Neurona:
def __init__(self, num_entradas):
# Inicializamos pesos y sesgo con números pequeños aleatorios
self.pesos = np.random.randn(num_entradas) * 0.5
self.sesgo = np.random.randn(1) * 0.5
def forward(self, entradas):
"""Propagación hacia adelante: calcular salida"""
# Suma ponderada de entradas por pesos, más sesgo
resultado_bruto = np.dot(entradas, self.pesos) + self.sesgo
# Aplicar función de activación
salida = sigmoid(resultado_bruto)
return salida
def ver_parametros(self):
print(f"Pesos: {self.pesos}")
print(f"Sesgo: {self.sesgo}")
# Creamos una neurona con 2 entradas
neurona = Neurona(2)
# Caso: hace frío y llueve
entradas = np.array([5.0, 1.0]) # temperatura 5, llueve sí
salida = neurona.forward(entradas)
print(f"Entrada: {entradas}")
print(f"Salida de la neurona: {salida:.4f}")
neurona.ver_parametros()
Si ejecutas esto varias veces, verás que cada vez los pesos y el sesgo son diferentes (porque son aleatorios). Y la salida será diferente. Eso es una neurona sin entrenar. No sabe nada.
Para entrenarla, necesitaríamos un montón de ejemplos y un algoritmo que ajuste los pesos. Eso es exactamente lo que hace TensorFlow cuando llamamos a model.fit(). Solo que con millones de operaciones por segundo.
¿Y por qué juntamos muchas neuronas?
Una neurona sola es muy limitada. Solo puede aprender una relación lineal (o una curva muy simple). En cambio, si juntas muchas neuronas en capas, cada neurona aprende un patrón diferente. La primera capa puede aprender bordes, la segunda formas simples, la tercera partes de objetos, etc.
Eso es lo que ocurre en la red del MNIST:
- La capa de entrada recibe los 784 píxeles.
- La capa oculta de 128 neuronas aprende 128 patrones diferentes (posiblemente formas de trazos, curvas, líneas diagonales...)
- La capa de salida de 10 neuronas combina esos patrones para decidir qué dígito es.
Cada neurona de la capa oculta hace exactamente lo que hemos descrito: multiplica sus 784 entradas por sus 784 pesos, añade su sesgo, y pasa el resultado por ReLU. 128 veces. Y luego la capa de salida hace lo mismo con sus 128 entradas. En total, más de 100.000 parámetros. Pero todos hacen la misma operación básica.
Esa es la belleza de las redes neuronales: un concepto simple (la neurona artificial) repetido miles o millones de veces, organizado en capas, y ajustado con datos, puede resolver problemas increíblemente complejos.
Resumen para que no te pierdas
| Componente | Qué es | En el ejemplo de la chaqueta | En la red del MNIST |
|---|---|---|---|
| Entradas | Los datos que le llegan | Temperatura, lluvia | 784 píxeles (cada uno un número entre 0 y 1) |
| Pesos | Importancia de cada entrada | Cuánto influye la temperatura | Cada neurona tiene 784 pesos, uno por píxel |
| Sesgo | Ajuste independiente | Predisposición a poner chaqueta | Cada neurona tiene su propio sesgo |
| Función activación | Regla de salida | Sigmoid (decide si >0.5) | ReLU en capas ocultas, softmax en salida |
| Aprendizaje | Cómo se ajustan pesos y sesgo | Probando y corrigiendo errores | Descenso del gradiente (lo hace TensorFlow) |
Si te queda una sola idea de esta sección, que sea esta: una neurona es una máquina de multiplicar y sumar números, con un par de ajustes. Lo que parece inteligencia es solo muchas de estas máquinas trabajando juntas y aprendiendo de sus errores.
Manos al código: clasificamos dígitos escritos a mano
Vamos a construir una red neuronal que reconoce dígitos escritos a mano desde cero. Usaremos el dataset MNIST: 70.000 imágenes de números del 0 al 9. Es el "hola mundo" de las redes neuronales. Si solo has hecho algún tutorial de Python y tienes las ideas básicas, este es tu sitio.
Antes de empezar, aclaremos algo: No necesitas ser un experto en Python. Con saber qué es una variable, una lista y cómo ejecutar un script, vas sobrado. El resto lo aprendes sobre la marcha.
Crear un proyecto desde cero (para el que sabe poco Python)
Vamos a hacerlo bien desde el principio. Así aprendes la estructura que se usa en el mundo real.
Paso 0: La estructura de carpetas
Abre tu explorador de archivos y crea esta estructura en algún lugar fácil de encontrar (por ejemplo, dentro de Documentos o en tu Escritorio):
proyecto_mnist/
│
├── datos/ # aquí se descargarán las imágenes
├── modelos/ # aquí guardaremos la red cuando esté entrenada
├── notebooks/ # aquí pondremos nuestros cuadernos de Jupyter
└── scripts/ # aquí pondremos archivos .py sueltos
No es obligatorio, pero te acostumbrarás a trabajar así. Los proyectos de verdad tienen esta pinta.
Si usas Jupyter Notebook (recomendado para empezar), trabaja dentro de la carpeta notebooks/. Si usas un editor normal como VS Code o incluso el IDLE de Python, usa la carpeta scripts/.
Paso 1: Abrir la terminal y activar el entorno (si usas uno)
Si no sabes lo que es un entorno virtual, no te preocupes. Por ahora, simplemente abre una terminal (en Windows: cmd o PowerShell; en Mac: Terminal; en Linux: la que uses).
Navega hasta la carpeta que creaste:
cd ruta/donde/esta/proyecto_mnist
Por ejemplo, si lo pusiste en el Escritorio:
cd Escritorio/proyecto_mnist
Opcional pero muy recomendable: crear un entorno virtual. Es como una burbuja aislada donde instalas cosas sin ensuciar tu sistema. Si eres muy principiante, sáltatelo. Si quieres aprender la forma correcta:
python -m venv venv
Y luego lo activas:
- En Windows:
venv\Scripts\activate - En Mac/Linux:
source venv/bin/activate
Verás que tu terminal ahora empieza con (venv). Eso significa que estás dentro.
Paso 2: Instalar lo que necesitas (solo una vez)
Con la terminal abierta en la carpeta del proyecto, escribe:
pip install tensorflow numpy matplotlib jupyter
Esto puede tardar un par de minutos. Verás un montón de texto bajando. Es normal. Cuando termine, ya tienes todo.
Si te da error en Windows: prueba a abrir la terminal como administrador. O prueba con:
python -m pip install tensorflow numpy matplotlib jupyter
Si te da error en Mac/Linux: prueba con:
pip3 install tensorflow numpy matplotlib jupyter
Paso 3: Crear un cuaderno de Jupyter (la forma más cómoda)
Estando en la terminal, dentro de la carpeta proyecto_mnist, escribe:
jupyter notebook
Se te abrirá una pestaña en el navegador. Se ve raro al principio, pero es normal.
Haz clic en "New" (arriba a la derecha) y luego en "Python 3". Se creará un nuevo cuaderno. Ponle nombre: 01_clasificador_mnist.ipynb (puedes hacer clic en el título para cambiarlo).
Ya estás dentro. Lo que escribas aquí se ejecuta en celdas. Una celda es ese cuadro gris donde puedes escribir código. Para ejecutar lo que hay dentro, pulsas Shift + Enter.
Construir la red paso a paso (con explicaciones para principiantes)
Voy a darte el código completo, pero no lo copies todo de golpe. Escríbelo celda por celda, ejecuta, mira qué pasa. Así aprendes.
Celda 1: Importar las herramientas
# Esto es un comentario. Python lo ignora.
# Aquí le decimos a Python qué librerías vamos a usar.
import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
import matplotlib.pyplot as plt
# Esto es solo para ver que todo ha ido bien
print("Librerías importadas correctamente")
Explicación para quien empieza:
importes la palabra mágica que trae código que otros escribieron.tensorflow as tfsignifica "importa TensorFlow y llámalo tf para escribir menos".from ... import ...es otra forma de importar, solo una parte concreta.print()muestra un mensaje. Si ves el mensaje, todo bien. Si ves un error rojo enorme, vuelve al paso de instalación.
Ejecuta: Shift + Enter.
Celda 2: Cargar el dataset MNIST
# Cargar los datos
mnist = tf.keras.datasets.mnist
# Esto devuelve dos tuplas: entrenamiento y prueba
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# Veamos qué hemos cargado
print("Forma de x_train:", x_train.shape)
print("Forma de y_train:", y_train.shape)
print("Forma de x_test:", x_test.shape)
print("Forma de y_test:", y_test.shape)
Explicación:
x_trainson las imágenes de entrenamiento.x_train.shapete dirá algo como(60000, 28, 28): 60.000 imágenes de 28x28 píxeles.y_trainson las etiquetas (qué número es cada imagen).(60000,)significa 60.000 números sueltos.x_testyy_testson los datos de prueba: 10.000 imágenes.
Si ves errores aquí, probablemente sea un problema de internet. El dataset se descarga automáticamente la primera vez.
Ejecuta.
Celda 3: Ver una imagen con nuestras propias retinas
# Elegimos la primera imagen del conjunto de entrenamiento
imagen = x_train[0]
etiqueta = y_train[0]
# La mostramos
plt.imshow(imagen, cmap='gray')
plt.title(f"Esta imagen es un {etiqueta}")
plt.show()
# También podemos ver los números (píxeles) de la imagen
print("Los primeros píxeles de la primera fila:")
print(imagen[0, :20]) # primeros 20 píxeles de la primera fila
print("Los valores van del 0 al 255 (negro a blanco)")
Explicación:
imshowmuestra una imagen.cmap='gray'la pone en escala de grises.imagen[0, :20]es una pequeña porción de la matriz. Verás números entre 0 y 255.
Verás el dibujo de un número. Si es la primera imagen de MNIST, es un 5. Lo sabrás por el título.
Ejecuta.
Celda 4: Normalizar los datos (paso crítico)
# Normalizar: pasar de 0-255 a 0-1
x_train = x_train / 255.0
x_test = x_test / 255.0
# Verificamos que el primer píxel ahora está entre 0 y 1
print("Primer píxel de la primera imagen después de normalizar:")
print(x_train[0, 0, 0])
# También podemos re-ver la imagen (se ve igual, pero los números son pequeños)
plt.imshow(x_train[0], cmap='gray')
plt.title("Sigue siendo la misma imagen, solo que los números cambiaron")
plt.show()
Explicación:
- Las redes neuronales funcionan mejor con números pequeños (entre -1 y 1, o entre 0 y 1). Si metes números grandes, los pesos se disparan y la red no aprende.
x_train / 255.0divide cada píxel por 255. El máximo era 255, el mínimo 0. Ahora el máximo es 1, el mínimo 0.- Verás que la imagen se ve igual. Los números han cambiado, pero la relación entre ellos sigue siendo la misma.
Ejecuta.
Celda 5: Reestructurar (aplanar) las imágenes
# Ahora mismo cada imagen es 28x28. La red espera una lista plana de 784 números.
# Vamos a aplanar: convertir cada imagen 28x28 en un solo vector de 784 elementos.
x_train = x_train.reshape(-1, 784)
x_test = x_test.reshape(-1, 784)
print("Nueva forma de x_train:", x_train.shape)
print("Nueva forma de x_test:", x_test.shape)
# Ver los primeros 20 números de la primera imagen (ahora es una lista plana)
print("\nPrimeros 20 píxeles de la primera imagen (aplanada):")
print(x_train[0, :20])
Explicación:
reshape(-1, 784)significa: mantén las filas que hagan falta (-1se calcula solo), pero cada fila debe tener 784 columnas.- Antes:
(60000, 28, 28)→ Ahora:(60000, 784). Hemos perdido la estructura de dos dimensiones, pero para nuestra red simple no importa. - Si te da error aquí, es porque olvidaste la celda anterior o porque los datos no están cargados.
Ejecuta.
Celda 6: Construir la red neuronal
# Creamos un modelo secuencial (capas una detrás de otra)
modelo = models.Sequential()
# Añadimos la primera capa (y única capa oculta)
# 128 neuronas, función de activación 'relu', entrada de 784 números
modelo.add(layers.Dense(128, activation='relu', input_shape=(784,)))
# Añadimos la capa de salida
# 10 neuronas (una por dígito), función 'softmax' (da probabilidades)
modelo.add(layers.Dense(10, activation='softmax'))
# Mostramos un resumen del modelo
modelo.summary()
Explicación para principiantes:
Sequential()es un contenedor vacío. Luego le añades capas conadd().Dense(128, activation='relu', input_shape=(784,)):128es el número de neuronas en esta capa.activation='relu'es la función de activación.relues muy común: si la entrada es negativa, la convierte en 0; si es positiva, la deja igual.input_shape=(784,)solo se pone en la primera capa. Le dice a la red: "espérate vectores de 784 números".
Dense(10, activation='softmax'):10neuronas de salida.softmaxconvierte los 10 números en probabilidades que suman 1. Así podemos decir "la red tiene un 85% de confianza en que es un 3".
modelo.summary()te enseña cuántos parámetros (pesos) tiene la red. En este caso, capa oculta: 784128 + 128 (los sesgos) = 100.480 parámetros. Capa de salida: 12810 + 10 = 1.290. En total, más de 100.000 números que la red va a aprender.
Ejecuta y mira el resumen.
Celda 7: Compilar el modelo
# Compilamos: preparamos el modelo para entrenar
modelo.compile(
optimizer='adam', # el algoritmo que ajusta los pesos
loss='sparse_categorical_crossentropy', # cómo medimos el error
metrics=['accuracy'] # qué métrica queremos ver
)
print("Modelo compilado. Listo para entrenar.")
Explicación:
optimizer='adam'es el piloto automático que decide cómo mover los pesos para reducir el error. Es uno de los mejores para empezar.loss='sparse_categorical_crossentropy'es la función que calcula el error. El nombre suena a chino, pero solo recuerda: para clasificar números del 0 al 9, usa esta.metrics=['accuracy']significa "mientras entrenas, dime también el porcentaje de aciertos". Es lo que realmente te interesa.
Ejecuta.
Celda 8: Entrenar la red (aquí ocurre la magia)
# Entrenamos durante 5 épocas
# Cada época, la red ve todas las imágenes una vez
historial = modelo.fit(
x_train, y_train, # datos de entrenamiento
epochs=5, # número de veces que ve TODO el conjunto
batch_size=32, # imágenes por lote (actualiza pesos cada 32)
validation_split=0.2 # reserva el 20% para validación
)
Explicación detallada de cada parámetro:
epochs=5: la red va a recorrer las 60.000 imágenes de entrenamiento 5 veces completas. En cada época, los pesos se ajustan un poquito más.batch_size=32: no actualiza los pesos tras cada imagen, sino tras cada lote de 32 imágenes. ¿Por qué? Si actualizaras tras cada imagen, el proceso sería muy inestable (como un borracho caminando). Si actualizaras tras todas las imágenes, sería muy lento. 32 es un buen punto medio.validation_split=0.2: aparta el 20% de los datos de entrenamiento (12.000 imágenes) para validar. Esas imágenes la red NO las usa para aprender, solo para comprobar cómo va. Así puedes ver si está aprendiendo de verdad o solo memorizando.
Qué verás mientras entrena:
Epoch 1/5
1500/1500 [==============================] - 5s - loss: 0.4321 - accuracy: 0.8745 - val_loss: 0.2109 - val_accuracy: 0.9211
Epoch 2/5
1500/1500 [==============================] - 5s - loss: 0.1823 - accuracy: 0.9456 - val_loss: 0.1654 - val_accuracy: 0.9487
...
losses el error en entrenamiento. Baja si la red aprende.accuracyes el acierto en entrenamiento. Sube si la red aprende.val_lossyval_accuracyson los mismos pero con datos que la red no ha visto. Sival_accuracyes mucho más baja queaccuracy, la red está memorizando (sobreajuste).
Ejecuta. Tardará unos segundos o un par de minutos según tu ordenador.
Celda 9: Evaluar con datos de prueba (los que nunca ha visto)
# Evaluamos con el conjunto de prueba (10.000 imágenes que la red no ha visto nunca)
test_loss, test_acc = modelo.evaluate(x_test, y_test, verbose=0)
print(f"Pérdida en prueba: {test_loss:.4f}")
print(f"Precisión en prueba: {test_acc:.4f}")
print(f"Has acertado el {test_acc * 100:.2f}% de las imágenes de prueba")
Explicación:
evaluatehace pasar todas las imágenes de prueba por la red y calcula el error y la precisión.verbose=0significa "no me des tantos detalles". Si ponesverbose=1verás una barra de progreso.- Una precisión típica con esta red simple es 96-97%. No está mal para cuatro líneas de código.
Ejecuta.
Celda 10: Hacer predicciones manuales (lo más divertido)
# Elegimos una imagen del conjunto de prueba
indice = 0 # cámbialo por 1, 2, 3, ... para ver diferentes imágenes
# Cogemos la imagen (está aplanada)
imagen_prueba = x_test[indice].reshape(1, 784)
# La red predice
prediccion = modelo.predict(imagen_prueba)
# Nos quedamos con el dígito de mayor probabilidad
numero_predicho = np.argmax(prediccion)
print(f"La red ha predicho: {numero_predicho}")
print(f"La etiqueta real es: {y_test[indice]}")
print("\nProbabilidades para cada dígito:")
for i, prob in enumerate(prediccion[0]):
print(f"Dígito {i}: {prob:.4f}")
# Mostramos la imagen original (re-construimos la forma 28x28)
plt.imshow(x_test[indice].reshape(28, 28), cmap='gray')
plt.title(f"Predicción: {numero_predicho} | Real: {y_test[indice]}")
plt.show()
Explicación:
reshape(1, 784)convierte la imagen en un "lote" de 1 imagen. La red siempre espera lotes.model.predictdevuelve un array con 10 números (las probabilidades). Por ejemplo:[0.01, 0.02, 0.03, 0.85, 0.02, ...].np.argmaxencuentra la posición del número más grande. Si la posición 3 tiene 0.85,argmaxdevuelve 3.- El bucle
formuestra todas las probabilidades para que veas cuánto dudaba la red.
Prueba cambiando indice a varios números (0, 100, 1000, 5000...). Busca uno que la red falle. Cuando encuentres un fallo, analiza las probabilidades. Verás que a veces la red está dudando entre un 4 y un 9, o entre un 3 y un 8. Igual que un humano.
Ejecuta varias veces con diferentes índices.
Celda 11: Guardar el modelo (para usarlo luego sin volver a entrenar)
# Guardamos el modelo entrenado en un archivo
modelo.save('../modelos/mnist_modelo.h5')
print("Modelo guardado en la carpeta 'modelos/'")
Explicación:
- El archivo
.h5contiene la arquitectura de la red y todos los pesos aprendidos. - Si cierras Jupyter y vuelves a abrir, puedes cargar el modelo con
tf.keras.models.load_model()y usarlo sin tener que entrenar otra vez. - Entrenar lleva tiempo. Guardar el modelo te ahorra repetir el trabajo.
Ejecuta. Ahora mira en tu carpeta modelos/ y verás el archivo.
Celda 12: (Opcional) Visualizar la evolución del entrenamiento
# El objeto 'historial' guarda el loss y accuracy por época
plt.figure(figsize=(12, 4))
# Gráfico del loss
plt.subplot(1, 2, 1)
plt.plot(historial.history['loss'], label='Entrenamiento')
plt.plot(historial.history['val_loss'], label='Validación')
plt.title('Evolución del error (loss)')
plt.xlabel('Época')
plt.ylabel('Loss')
plt.legend()
# Gráfico de la precisión
plt.subplot(1, 2, 2)
plt.plot(historial.history['accuracy'], label='Entrenamiento')
plt.plot(historial.history['val_accuracy'], label='Validación')
plt.title('Evolución de la precisión (accuracy)')
plt.xlabel('Época')
plt.ylabel('Accuracy')
plt.legend()
plt.tight_layout()
plt.show()
Explicación:
historial.historyes un diccionario con los valores de loss y accuracy por época.- En el gráfico de la izquierda, ves cómo baja el error. En el de la derecha, cómo sube la precisión.
- Si las líneas de validación se separan mucho de las de entrenamiento (validación empeora mientras entrenamiento mejora), tienes sobreajuste.
Ejecuta y mira los gráficos. Es muy satisfactorio ver las líneas subiendo.
Cómo ejecutar todo esto si eres muy principiante (resumen para no perderse)
- Abre la terminal (en Windows busca "cmd", en Mac "Terminal").
- Navega a tu carpeta con
cd ruta/de/tu/proyecto_mnist. - Ejecuta
jupyter notebook. - Crea un nuevo cuaderno (New → Python 3).
- Copia cada celda de código, una por una, y ejecuta con
Shift + Enter. - Si te da error en alguna celda, no pases a la siguiente. Lee el error. El 90% de las veces es que has escrito mal algo o has olvidado ejecutar una celda anterior.
- Cuando termines, cierra Jupyter desde el navegador y en la terminal pulsa
Ctrl + Cdos veces.
Si no quieres usar Jupyter (y prefieres un archivo .py normal)
Crea un archivo llamado clasificador.py dentro de la carpeta scripts/. Escribe todo el código dentro (sin separar en celdas). Luego, en la terminal, dentro de proyecto_mnist, ejecuta:
python scripts/clasificador.py
La diferencia es que no verás las imágenes en medio. Para ver los gráficos, tendrías que usar plt.savefig() en lugar de plt.show().
Un par de consejos para el que empieza (de alguien que ya ha roto cien veces el código)
Consejo 1: No copies y pegues todo de golpe. Escribe cada línea. Cuando escribes a mano, tu cerebro procesa lo que estás haciendo. Cuando pegas, solo estás leyendo. La diferencia es abismal.
Consejo 2: Cuando te dé un error (y te dará), lee el mensaje. No te asustes. Los mensajes de error en Python son largos pero dicen cosas útiles. Busca la última línea que empiece por Error:. Ahí suele estar la respuesta.
Consejo 3: Cambia números. Prueba con 64 neuronas en lugar de 128. Con 2 épocas en lugar de 5. Con batch_size=64. Rompe el código a propósito para ver qué errores salen. Así aprendes.
Consejo 4: Si ves que la precisión en prueba es del 99% (con esta red tan simple puede pasar), no te emociones demasiado. MNIST es un dataset fácil. Los problemas del mundo real son mucho más difíciles. Pero has dado el primer paso. Eso ya es más de lo que hace la mayoría.
En la siguiente sección veremos los 5 errores más comunes que vas a cometer (y cómo salir de ellos sin tirar el portátil por la ventana). Pero eso ya es para otro día. Por ahora, dedícate a ejecutar este código y a entender qué hace cada línea.
Errores comunes
Las dimensiones no cuadran
Mensaje: ValueError: Shapes (None, 784) and (None, 10) are incompatible
Causa: Olvidaste el reshape antes de entrenar.
Solución: Asegúrate de ejecutar x_train = x_train.reshape(-1, 784) y x_test = x_test.reshape(-1, 784) antes de llamar a fit.
La precisión no mejora del 10%
Causa típica: a) no normalizaste diviendo entre 255, b) la última capa no tiene softmax, c) función de pérdida incorrecta.
Solución: Comprueba que x_train = x_train / 255.0 está antes de entrenar, que la última capa tiene activation='softmax', y que usas loss='sparse_categorical_crossentropy' con etiquetas numéricas.
El loss se vuelve NaN
Causa: Los gradientes se dispararon. Ocurre cuando la tasa de aprendizaje es muy alta o los datos tienen valores grandes.
Solución: Reinicia el kernel y ejecuta de nuevo. Si persiste, añade clipnorm=1.0 al optimizador.
Sobreajuste (memoriza pero no generaliza)
Síntoma: accuracy en entrenamiento = 0.99, en prueba = 0.80.
Solución: Añade una capa Dropout:
modelo = models.Sequential([
layers.Dense(128, activation='relu', input_shape=(784,)),
layers.Dropout(0.5),
layers.Dense(10, activation='softmax')
])
El entrenamiento tarda demasiado
Con MNIST y 5 épocas deberías tardar menos de dos minutos en CPU. Si tarda más, reduce la capa oculta a 64 o 32 neuronas. Más neuronas no siempre es mejor.
Preguntas frecuentes
¿Cuántas capas ocultas necesito?
Para MNIST, una capa oculta es suficiente. Para problemas complejos como reconocer objetos en fotos reales se usan decenas de capas. Empieza con una; si no mejora, añade otra.
¿Qué es el descenso del gradiente?
Es cómo la red ajusta sus pesos para minimizar el error. Imagina que estás en una montaña con los ojos vendados y quieres bajar al valle. El gradiente es la pendiente del suelo bajo tus pies. El descenso del gradiente dice: "da un paso hacia donde el suelo baja más". La red repite esto miles de veces hasta llegar al mínimo.
¿Cuántas épocas son las correctas?
No hay número fijo. Entrenas hasta que el error en validación deje de bajar. Si sigues entrenando después, la red empieza a sobreajustarse. En la práctica se usa "early stopping": parar automáticamente cuando la validación no mejora durante X épocas.
¿Cuál es más importante, accuracy o loss?
Las dos, pero miden cosas distintas. loss es lo que la red minimiza internamente. accuracy es el porcentaje de aciertos. Mira siempre accuracy en validación — esa es tu verdadera métrica.
¿Puedo usar esta red para clasificar fotos a color de animales?
Podrías, pero no deberías. Para fotos reales necesitas redes convolucionales (CNN). Esta red trata cada píxel de forma independiente y no entiende relaciones espaciales. Una CNN aprende a detectar bordes, formas y patrones. El bucle de entrenamiento es el mismo, la arquitectura cambia.
Por dónde seguir
Lo siguiente, en orden recomendado:
- Redes convolucionales (CNN) — para clasificar imágenes reales. Aprenderás qué son los filtros, el pooling y por qué las CNN entienden la estructura espacial de una imagen.
- Aumento de datos (data augmentation) — cuando tienes pocas imágenes, puedes rotarlas o añadir ruido para que la red generalice mejor.
- Transfer learning — coger una red ya entrenada por Google o Meta y ajustarla para tu problema. Con cien imágenes propias puedes tener un clasificador serio.
- Redes recurrentes (RNN) — para series temporales, texto o audio, donde el orden de los datos importa.
- Despliegue — una red que solo corre en tu portátil no sirve de nada. Aprende a meterla en una API con FastAPI.
Proyectos para practicar:
- Clasificar si una reseña es positiva o negativa (análisis de sentimiento).
- Reconocer si una fruta es manzana o naranja con fotos propias.
- Hacer un detector de escritura ilegible: ¿número real o garabato?
- Clasificar canciones por género a partir de sus características (duración, ritmo, BPM).
Recursos gratuitos:
- TensorFlow Playground — juega con una red neuronal visualmente, sin código.
- Kaggle "Digit Recognizer" — compite con el mismo problema de este tutorial.
- Fast.ai — curso gratuito y práctico que construye primero y explica las matemáticas después.
Un consejo: No intentes entender todas las matemáticas desde el primer día. Aprende a construir cosas que funcionen. Cuando algo explote o se comporte de forma rara, entonces profundiza. Al principio necesitas curiosidad y ganas de probar cosas. El resto se aprende sobre la marcha.
Artículos relacionados
De programador a experto en IA: hoja de ruta de 10 meses
El camino completo desde Python hasta producción, sin distracciones ni hype.
RelacionadoIA en producción: deja de rezar y empieza a auditar
Por qué el 84% de los desarrolladores usa IA pero solo el 29% confía en ella.
RelacionadoCNN: redes convolucionales explicadas
El siguiente paso natural tras las redes densas: aprende a clasificar imágenes reales.
Sobre el autor
Apasionado por la tecnología y la innovación, con más de 20 años de experiencia en desarrollo de software y consultoría tecnológica. Su trayectoria profesional comenzó en 2001 como programador, evolucionando desde entonces combinando su amor por el código con una sólida visión de negocio.
Ha trabajado tanto en España como en el extranjero, en sectores diversos como telecomunicaciones, banca, seguros y marketing digital. Esta experiencia multidisciplinar le permite entender los retos técnicos desde una perspectiva de negocio real.
Hoy aporta su experiencia asesorando en la modernización de procesos y la implementación de herramientas tecnológicas que optimizan la gestión y las relaciones con clientes. Se especializa en ayudar a equipos a integrar inteligencia artificial de forma práctica y responsable.
Cree firmemente en el aprendizaje continuo y que el verdadero progreso solo se logra creciendo juntos.


