🧠 Red Neuronal Simple para Reconocimiento de Dígitos Manuscritos¶
Visión por Computador — NRC-8773 | Semana 5¶
| Campo | Detalle |
|---|---|
| Asignatura | Visión por Computador |
| NRC | 8773 |
| Semana | 5 |
| Docente | Félix Julián Gutiérrez Bernal |
| Institución | Corporación Universitaria Minuto de Dios – UNIMINUTO |
| Programa | Especialización en Inteligencia Artificial |
| Fecha | Junio de 2026 |
Integrantes del grupo¶
- Alexander Oviedo Fadul
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
- Yesith Stiven Vizcano Mican
Enlace del notebook en Google Colab:
https://colab.research.google.com/drive/1IUR_XYQeP7xYDOxlYwRLuV21NGZDBfQF
1. Introducción¶
¿Cómo puede una máquina aprender a través de modelos que emulan el cerebro humano?¶
Desde finales de la década de 1940, investigadores comenzaron a plantear modelos matemáticos que buscaban replicar, de forma abstracta, el funcionamiento del cerebro biológico. La unidad fundamental de esos modelos es la neurona artificial: una unidad de procesamiento que recibe múltiples señales de entrada, las pondera mediante coeficientes llamados pesos (ω), y produce una única salida. Esta operación es, en esencia, una combinación lineal:
$$V = \sum_{i=0}^{n} \omega_i \cdot x_i$$
donde $x_0 = 1$ actúa como el sesgo (bias) que desplaza la recta de decisión.
Lo verdaderamente poderoso viene después: aplicar una función de activación que introduce no linealidad. Sin ella, apilar muchas capas de neuronas equivale a una sola transformación lineal y el modelo pierde su capacidad para capturar patrones complejos. La función sigmoide, por ejemplo, "dobla" esa recta para que el modelo pueda aprender fronteras de decisión curvilíneas:
$$\sigma(v) = \frac{1}{1 + e^{-v}}$$
Una red neuronal es, entonces, una composición de varias de estas transformaciones organizadas en capas:
- Capa de entrada: captura los datos crudos (píxeles de una imagen).
- Capas ocultas: aprenden representaciones intermedias; cada capa extrae características más abstractas que la anterior.
- Capa de salida: produce la decisión final (en nuestro caso, a qué dígito corresponde la imagen).
El aprendizaje ocurre mediante el algoritmo de retropropagación combinado con descenso de gradiente: el modelo calcula cuánto se equivocó en una predicción, determina en qué medida contribuyó cada peso a ese error (derivada parcial), y ajusta los pesos en la dirección contraria al gradiente para reducir el error. Este ciclo se repite cientos o miles de veces hasta que el modelo generaliza correctamente.
Como señala Ballard (2018), las redes neuronales clásicas con capas densas (fully connected) son la base conceptual sobre la que se construyen arquitecturas más complejas. En este ejercicio implementamos precisamente esa arquitectura fundacional aplicada a la clasificación de dígitos manuscritos.
El Dataset MNIST¶
El dataset MNIST (Modified National Institute of Standards and Technology) contiene 70,000 imágenes de dígitos manuscritos del 0 al 9: 60,000 para entrenamiento y 10,000 para prueba. Cada imagen tiene dimensiones de 28×28 píxeles en escala de grises, lo que significa que cada imagen se representa como una matriz de 784 valores numéricos entre 0 y 255 (Dadhich, 2018). Es el conjunto de referencia por excelencia para aprender redes neuronales aplicadas a visión por computador.
2. Configuración del Proyecto¶
Importamos las librerías necesarias:
- TensorFlow/Keras: para definir, compilar y entrenar la red neuronal.
- MNIST: dataset integrado en Keras; no requiere descarga manual.
- to_categorical: convierte las etiquetas numéricas (0-9) a vectores one-hot.
- matplotlib: para visualizar imágenes y resultados del entrenamiento.
# ============================================================
# CONFIGURACIÓN DEL PROYECTO — Semana 5 Visión por Computador
# Red Neuronal Simple para Reconocimiento de Dígitos MNIST
# NRC-8773 | Especialización en IA | UNIMINUTO | Junio 2026
# ============================================================
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
# TensorFlow y Keras
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten, Input
from tensorflow.keras.utils import to_categorical
# Verificar versión de TensorFlow
print(f'✅ TensorFlow versión: {tf.__version__}')
print(f'✅ NumPy versión: {np.__version__}')
print('\n🎯 Proyecto: Reconocimiento de Dígitos Manuscritos con Red Neuronal Simple')
print('👥 Equipo: Alexander Oviedo, Maria Fernanda Ruiz, Neheman Jaller, William Obando')
✅ TensorFlow versión: 2.20.0 ✅ NumPy versión: 2.0.2 🎯 Proyecto: Reconocimiento de Dígitos Manuscritos con Red Neuronal Simple 👥 Equipo: Alexander Oviedo, Maria Fernanda Ruiz, Neheman Jaller, William Obando
3. Importación y Exploración del Dataset MNIST¶
Keras incluye MNIST de forma nativa. Al llamar mnist.load_data(), el sistema descarga automáticamente ~11 MB y los divide en:
- Conjunto de entrenamiento (train): 60,000 imágenes + etiquetas → el modelo aprende de estos datos.
- Conjunto de prueba (test): 10,000 imágenes + etiquetas → evaluamos qué tan bien generalizó el modelo.
Cada imagen tiene forma (28, 28) y cada píxel tiene un valor entero entre 0 (negro) y 255 (blanco).
# ============================================================
# IMPORTACIÓN DEL DATASET MNIST
# ============================================================
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
# Información del dataset
print('📊 Información del Dataset MNIST')
print('=' * 45)
print(f' Conjunto de entrenamiento: {X_train.shape[0]:,} imágenes')
print(f' Conjunto de prueba: {X_test.shape[0]:,} imágenes')
print(f' Dimensiones de cada imagen: {X_train.shape[1]} × {X_train.shape[2]} píxeles')
print(f' Rango de valores: [{X_train.min()}, {X_train.max()}]')
print(f' Clases (dígitos): {np.unique(Y_train)}')
print(f' Total de parámetros de entrada: {28*28} neuronas')
📊 Información del Dataset MNIST ============================================= Conjunto de entrenamiento: 60,000 imágenes Conjunto de prueba: 10,000 imágenes Dimensiones de cada imagen: 28 × 28 píxeles Rango de valores: [0, 255] Clases (dígitos): [0 1 2 3 4 5 6 7 8 9] Total de parámetros de entrada: 784 neuronas
# ============================================================
# VISUALIZACIÓN DE EJEMPLOS DEL DATASET
# ============================================================
fig, axes = plt.subplots(2, 10, figsize=(16, 4))
fig.suptitle('Ejemplos del Dataset MNIST — Dígitos del 0 al 9',
fontsize=13, fontweight='bold', y=1.02)
# Mostrar un ejemplo de cada dígito (dos filas)
for digito in range(10):
# Encontrar índice del dígito en el dataset
idx = np.where(Y_train == digito)[0][0]
idx2 = np.where(Y_train == digito)[0][5]
axes[0, digito].imshow(X_train[idx], cmap='gray')
axes[0, digito].set_title(f'Dígito: {digito}', fontsize=9)
axes[0, digito].axis('off')
axes[1, digito].imshow(X_train[idx2], cmap='gray')
axes[1, digito].axis('off')
plt.tight_layout()
plt.show()
print('✅ Visualización completada — Cada columna muestra dos escrituras del mismo dígito')
✅ Visualización completada — Cada columna muestra dos escrituras del mismo dígito
4. Preprocesamiento de los Datos¶
Antes de alimentar las imágenes a la red neuronal, necesitamos dos transformaciones fundamentales:
4.1 Reshape y Normalización¶
Agregamos una dimensión de canal: de (28, 28) → (28, 28, 1). La capa Flatten del modelo necesita esta forma explícita. Luego normalizamos dividiendo por 255 para llevar los valores al rango [0, 1]. Esto acelera la convergencia del entrenamiento y mejora la estabilidad numérica (Dadhich, 2018).
4.2 One-Hot Encoding¶
Las etiquetas originales son enteros (0 a 9). La red produce 10 probabilidades (una por clase), así que necesitamos que las etiquetas tengan la misma forma. to_categorical convierte, por ejemplo, el dígito 3 en el vector [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] (Ballard, 2018).
# ============================================================
# PREPROCESAMIENTO — Reshape, Normalización y One-Hot Encoding
# ============================================================
# 1. Reshape: agregar dimensión de canal y convertir a float32
x_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32') / 255
x_test = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32') / 255
# 2. One-Hot Encoding de las etiquetas
y_train = to_categorical(Y_train) # shape: (60000, 10)
y_test = to_categorical(Y_test) # shape: (10000, 10)
# Verificación
print('📐 Dimensiones después del preprocesamiento:')
print(f' x_train: {x_train.shape} (imágenes normalizadas)')
print(f' x_test: {x_test.shape}')
print(f' y_train: {y_train.shape} (etiquetas one-hot)')
print(f' y_test: {y_test.shape}')
print(f'\n🔢 Ejemplo — Etiqueta del primer dato de entrenamiento:')
print(f' Dígito real: {Y_train[0]}')
print(f' One-hot: {y_train[0].astype(int)}')
print(f' Rango de píxeles: [{x_train.min():.2f}, {x_train.max():.2f}]')
📐 Dimensiones después del preprocesamiento: x_train: (60000, 28, 28, 1) (imágenes normalizadas) x_test: (10000, 28, 28, 1) y_train: (60000, 10) (etiquetas one-hot) y_test: (10000, 10) 🔢 Ejemplo — Etiqueta del primer dato de entrenamiento: Dígito real: 5 One-hot: [0 0 0 0 0 1 0 0 0 0] Rango de píxeles: [0.00, 1.00]
5. Arquitectura de la Red Neuronal¶
Definimos una red neuronal secuencial simple con tres capas:
Entrada: Imagen 28×28×1 (784 píxeles)
│
▼ Flatten — Aplanado
Vector de 784 neuronas
│
▼ Dense(128) + ReLU — Capa oculta
128 representaciones aprendidas
│
▼ Dense(10) + Softmax — Capa de salida
10 probabilidades (una por dígito 0-9)
¿Por qué 128 neuronas en la capa oculta?
128 es un valor ampliamente utilizado en la literatura como punto de partida para MNIST. Provee suficiente capacidad para capturar las características visuales de los dígitos (curvas, líneas, bucles) sin generar sobreajuste. Ballard (2018) indica que este número es un hiperparámetro que se ajusta experimentalmente; en nuestro caso, el docente lo confirmó como una elección adecuada para este ejercicio introductorio.
Función de activación ReLU:
ReLU(x) = max(0, x) — Devuelve el valor si es positivo, 0 si es negativo. Es la función de activación más usada en capas ocultas por su eficiencia computacional y por mitigar el problema del vanishing gradient.
Función de activación Softmax:
Convierte un vector de valores en una distribución de probabilidades que suman 1. El modelo asigna toda la probabilidad a la clase con el valor más alto (argmax).
# ============================================================
# DEFINICIÓN DE LA ARQUITECTURA DEL MODELO
# ============================================================
modelo = Sequential([
# Capa de entrada: define la forma de los datos
Input(shape=(28, 28, 1)),
# Capa 1 — Flatten: convierte la imagen 28×28×1 en un vector de 784 elementos
# Este proceso de "aplanado" es lo que el docente describió como crucial:
# llevar la imagen bidimensional a una única dimensión (Clase S5, 52:33)
Flatten(),
# Capa 2 — Oculta: 128 neuronas con activación ReLU
# Aprende representaciones intermedias de los dígitos
Dense(128, activation='relu'),
# Capa 3 — Salida: 10 neuronas con Softmax (una por dígito 0-9)
# Produce una distribución de probabilidades sobre las 10 clases
Dense(10, activation='softmax')
])
# Resumen del modelo
print('🏗️ Arquitectura del Modelo:')
modelo.summary()
🏗️ Arquitectura del Modelo:
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ flatten (Flatten) │ (None, 784) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (Dense) │ (None, 128) │ 100,480 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 10) │ 1,290 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 101,770 (397.54 KB)
Trainable params: 101,770 (397.54 KB)
Non-trainable params: 0 (0.00 B)
6. Compilación del Modelo¶
Antes de entrenar, configuramos tres elementos clave:
| Parámetro | Valor | Justificación |
|---|---|---|
| Optimizador | Adam | Variante del descenso de gradiente con tasa de aprendizaje adaptativa; es el estándar moderno para la mayoría de problemas |
| Función de pérdida | categorical_crossentropy | Función apropiada para clasificación multiclase con etiquetas one-hot; mide la diferencia entre la distribución predicha y la real |
| Métrica | accuracy | Porcentaje de imágenes clasificadas correctamente; fácil de interpretar |
# ============================================================
# COMPILACIÓN DEL MODELO
# ============================================================
modelo.compile(
optimizer='adam', # Optimizador adaptativo
loss='categorical_crossentropy', # Función de pérdida para clasificación multiclase
metrics=['accuracy'] # Métrica de evaluación: exactitud
)
print('✅ Modelo compilado correctamente')
print(' Optimizador: Adam')
print(' Función de pérdida: Categorical Crossentropy')
print(' Métrica: Accuracy')
✅ Modelo compilado correctamente Optimizador: Adam Función de pérdida: Categorical Crossentropy Métrica: Accuracy
7. Entrenamiento del Modelo¶
El entrenamiento se realiza con los siguientes hiperparámetros:
- Épocas (epochs = 5): El modelo realiza 5 pasadas completas por el dataset de entrenamiento.
- Batch size (= 100): El dataset se divide en lotes de 100 imágenes. Por época, el modelo actualiza sus pesos 600 veces (60,000 / 100). Esta estrategia de mini-batch gradient descent reduce el consumo computacional manteniendo buena generalización.
- Validation data: Evaluamos el desempeño en el conjunto de prueba al final de cada época para monitorear el sobreajuste.
Como explicó el docente en clase: "si yo divido eso [el dataset] en cien, en unas épocas, ponle que sean cinco o diez épocas... puedo consumir o disminuir al cinco por ciento de su consumo digital" (Clase S5, 49:14).
# ============================================================
# ENTRENAMIENTO DEL MODELO
# ============================================================
print('🚀 Iniciando entrenamiento...')
print(f' Imágenes de entrenamiento: {x_train.shape[0]:,}')
print(f' Épocas: 5 | Batch size: 100')
print('=' * 60)
historia = modelo.fit(
x_train, y_train,
epochs=5,
batch_size=100,
validation_data=(x_test, y_test),
verbose=1
)
print('\n✅ Entrenamiento completado')
🚀 Iniciando entrenamiento... Imágenes de entrenamiento: 60,000 Épocas: 5 | Batch size: 100 ============================================================ Epoch 1/5 600/600 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9086 - loss: 0.3333 - val_accuracy: 0.9481 - val_loss: 0.1802 Epoch 2/5 600/600 ━━━━━━━━━━━━━━━━━━━━ 3s 6ms/step - accuracy: 0.9566 - loss: 0.1529 - val_accuracy: 0.9624 - val_loss: 0.1278 Epoch 3/5 600/600 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.9689 - loss: 0.1074 - val_accuracy: 0.9697 - val_loss: 0.1026 Epoch 4/5 600/600 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9763 - loss: 0.0817 - val_accuracy: 0.9721 - val_loss: 0.0874 Epoch 5/5 600/600 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - accuracy: 0.9813 - loss: 0.0661 - val_accuracy: 0.9726 - val_loss: 0.0882 ✅ Entrenamiento completado
# ============================================================
# VISUALIZACIÓN DEL HISTORIAL DE ENTRENAMIENTO
# ============================================================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Evolución del Entrenamiento — Red Neuronal MNIST',
fontsize=13, fontweight='bold')
epocas = range(1, len(historia.history['accuracy']) + 1)
# Gráfica de Accuracy
ax1.plot(epocas, historia.history['accuracy'], 'b-o', label='Train Accuracy', linewidth=2)
ax1.plot(epocas, historia.history['val_accuracy'], 'r-o', label='Val Accuracy', linewidth=2)
ax1.set_title('Exactitud por Época', fontweight='bold')
ax1.set_xlabel('Época')
ax1.set_ylabel('Accuracy')
ax1.legend()
ax1.set_ylim([0.88, 1.0])
ax1.grid(True, alpha=0.3)
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.1%}'))
# Gráfica de Loss
ax2.plot(epocas, historia.history['loss'], 'b-o', label='Train Loss', linewidth=2)
ax2.plot(epocas, historia.history['val_loss'], 'r-o', label='Val Loss', linewidth=2)
ax2.set_title('Pérdida por Época', fontweight='bold')
ax2.set_xlabel('Época')
ax2.set_ylabel('Loss')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Métricas finales
val_loss, val_acc = modelo.evaluate(x_test, y_test, verbose=0)
print(f'\n📈 Resultados finales en el conjunto de prueba:')
print(f' Exactitud (Accuracy): {val_acc:.4f} ({val_acc*100:.2f}%)')
print(f' Pérdida (Loss): {val_loss:.4f}')
📈 Resultados finales en el conjunto de prueba: Exactitud (Accuracy): 0.9726 (97.26%) Pérdida (Loss): 0.0882
8. Prueba del Modelo con Imágenes Individuales¶
Ahora probaremos el modelo con imágenes individuales del conjunto de prueba para observar cómo realiza las predicciones. El modelo devuelve un vector de 10 probabilidades; seleccionamos el índice con la probabilidad más alta usando argmax.
# ============================================================
# PRUEBA CON IMÁGENES INDIVIDUALES
# ============================================================
# Probar con varios índices
indices_prueba = [0, 42, 100, 669, 999, 2543]
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
fig.suptitle('Predicciones del Modelo en Imágenes de Prueba',
fontsize=13, fontweight='bold')
axes = axes.flatten()
for i, n in enumerate(indices_prueba):
imagen = x_test[n, :, :]
prediccion = modelo.predict(imagen.reshape(1, 28, 28, 1), verbose=0)
digito_predicho = np.argmax(prediccion)
digito_real = Y_test[n]
confianza = prediccion[0][digito_predicho] * 100
correcto = '✅' if digito_predicho == digito_real else '❌'
axes[i].imshow(imagen.reshape(28, 28), cmap='gray')
axes[i].set_title(
f'Real: {digito_real} | Predicho: {digito_predicho} {correcto}\n'
f'Confianza: {confianza:.1f}%',
fontsize=10, color='green' if digito_predicho == digito_real else 'red'
)
axes[i].axis('off')
plt.tight_layout()
plt.show()
# ============================================================
# PRUEBA CON IMÁGENES INDIVIDUALES CARGADAS EXTERNAMENTE
# ============================================================
# Librería para descargar archivos desde Google Drive
import gdown
# Librería para manejo de archivos y rutas
import os
# Importar librería OpenCV
import cv2 as cv
# Función para mostrar imágenes en Google Colab
from google.colab.patches import cv2_imshow
enlaces_drive = [
'https://drive.google.com/file/d/1oX0Ly6zt5y8wlAF5mdPgwxF0N_8BvKeC/view?usp=sharing',
'https://drive.google.com/file/d/13kLo4x4tCHl7kvaZe5VN2cw22SQdrkym/view?usp=sharing',
'https://drive.google.com/file/d/1cTutZbXG_q8VUKGBoc6oO30EfO0cDX-x/view?usp=sharing',
'https://drive.google.com/file/d/1QrKNrbt8kVQq44P260I8mT4RH3bWn0aF/view?usp=sharing',
'https://drive.google.com/file/d/1r7sRvqohjRN643ZgBGle2R1UWdkABi8U/view?usp=sharing',
'https://drive.google.com/file/d/1gzHOA5ZiawGoucxfGEoCaXEo-Zd8kCld/view?usp=sharing'
]
# Lista para almacenar rutas locales de las imágenes
rutas_imagenes = []
print("Descargando imágenes desde Google Drive...")
# Recorrer enlaces y descargar imágenes
for i, url in enumerate(enlaces_drive):
# Nombre local de cada imagen
nombre_local = f"imagen_{i+1}.jpg"
# Verificar si la imagen ya existe
if not os.path.exists(nombre_local):
# Descargar imagen desde Google Drive
gdown.download(url, nombre_local, quiet=True, fuzzy=True)
# Guardar ruta local
rutas_imagenes.append(nombre_local)
print("¡Descargas completadas!")
# Mostrar las imágenes procesadas
fig, axes = plt.subplots(2, 3, figsize=(8, 6))
axes = axes.flatten()
for i, ruta in enumerate(rutas_imagenes):
# Cargar imagen
imagen = cv.imread(ruta)
# Redimensionar a 28x28
imagen = cv.resize(imagen, (28, 28), interpolation=cv.INTER_AREA)
# Convertir BGR -> RGB para visualizar correctamente
imagen_rgb = cv.cvtColor(imagen, cv.COLOR_BGR2RGB)
axes[i].imshow(imagen_rgb)
axes[i].set_title(f'Imagen {i+1}')
axes[i].axis('off')
plt.tight_layout()
plt.show()
Descargando imágenes desde Google Drive... ¡Descargas completadas!
imagenes_procesadas = []
for ruta in rutas_imagenes:
# Leer en escala de grises
imagen = cv.imread(ruta, cv.IMREAD_GRAYSCALE)
# Redimensionar
imagen = cv.resize(imagen, (28, 28), interpolation=cv.INTER_AREA)
# Normalizar
imagen = imagen.astype('float32') / 255.0
imagenes_procesadas.append(imagen)
# Crear una ventana con 6 espacios para mostrar imágenes
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
axes = axes.flatten()
# Recorrer las imágenes procesadas
for i, imagen in enumerate(imagenes_procesadas):
# Realizar la predicción del modelo
prediccion = modelo.predict(
imagen.reshape(1, 28, 28, 1),
verbose=0
)
# Obtener el número predicho
digito_predicho = np.argmax(prediccion)
# Obtener el porcentaje de confianza
confianza = prediccion[0][digito_predicho] * 100
# Mostrar la imagen
axes[i].imshow(imagen, cmap='gray')
# Mostrar resultado de la predicción
axes[i].set_title(
f'Predicho: {digito_predicho}\n'
f'Confianza: {confianza:.1f}%'
)
# Ocultar ejes
axes[i].axis('off')
# Ajustar distribución de imágenes
plt.tight_layout()
# Mostrar resultados
plt.show()
# ============================================================
# ANÁLISIS GLOBAL — Correctas vs Incorrectas en todo el test
# ============================================================
predicciones_all = modelo.predict(x_test, verbose=0)
digitos_predichos = np.argmax(predicciones_all, axis=1)
digitos_reales = Y_test
correctas = np.sum(digitos_predichos == digitos_reales)
incorrectas = len(digitos_reales) - correctas
print('📊 Análisis de predicciones en el conjunto de prueba (10,000 imágenes):')
print(f' ✅ Predicciones correctas: {correctas:,} ({correctas/100:.1f}%)')
print(f' ❌ Predicciones incorrectas: {incorrectas:,} ({incorrectas/100:.1f}%)')
# Precisión por dígito
print('\n🔢 Precisión por dígito:')
for d in range(10):
mask = digitos_reales == d
prec = np.sum(digitos_predichos[mask] == d) / np.sum(mask)
print(f' Dígito {d}: {prec*100:.1f}%')
📊 Análisis de predicciones en el conjunto de prueba (10,000 imágenes): ✅ Predicciones correctas: 9,726 (97.3%) ❌ Predicciones incorrectas: 274 (2.7%) 🔢 Precisión por dígito: Dígito 0: 98.7% Dígito 1: 98.9% Dígito 2: 97.0% Dígito 3: 98.9% Dígito 4: 94.9% Dígito 5: 96.0% Dígito 6: 98.3% Dígito 7: 96.7% Dígito 8: 95.1% Dígito 9: 97.8%
9. Análisis de Resultados¶
El modelo alcanzó una exactitud de validación de aproximadamente 97.5% en 5 épocas, lo cual es un resultado sólido para una red neuronal densa simple.
Interpretación¶
- La exactitud de entrenamiento (~98.15%) es ligeramente mayor que la de validación (~97.47%), lo cual indica un ajuste saludable sin sobreajuste significativo.
- La pérdida decrece consistentemente en ambos conjuntos, lo que confirma que el descenso de gradiente está convergiendo correctamente.
- Algunos dígitos son más difíciles de clasificar (p.ej., el 4 y el 9 comparten formas similares; el 3 y el 8 también). Esto explica los errores residuales.
Limitaciones del modelo¶
- Al ser una red densa (no convolucional), no aprovecha la estructura espacial de la imagen. Una CNN captaría mejor las curvas y patrones locales.
- Solo 5 épocas son suficientes para MNIST por su relativa simplicidad, pero no para datasets más complejos.
- El modelo fue entrenado solo con imágenes de MNIST; dígitos escritos en condiciones muy distintas (papel oscuro, escritura muy inclinada) podrían ser mal clasificados.
Evaluación con Imágenes Externas¶
Al probar el modelo con imágenes cargadas externamente, se observó una disminución en la precisión y un aumento en la cantidad de errores de clasificación en comparación con las imágenes del conjunto de prueba MNIST.
Posibles causas¶
- Durante el proceso de redimensionamiento a 28×28 píxeles, algunas imágenes pudieron perder detalles importantes, reduciendo la claridad de los trazos del número.
- Las imágenes externas presentan características diferentes a las utilizadas durante el entrenamiento, como variaciones en iluminación, grosor de línea, contraste y calidad de imagen.
- El fondo de las imágenes puede no coincidir con el formato original de MNIST (dígitos claros sobre fondo oscuro), lo que afecta la capacidad del modelo para reconocer correctamente los patrones aprendidos.
- La posición, orientación o tamaño del dígito dentro de la imagen puede diferir de las muestras del conjunto de entrenamiento, generando una distribución de datos distinta a la esperada por la red neuronal.
Colombia y la IA — Pregunta orientadora 2¶
Colombia avanza de manera progresiva en la adopción de inteligencia artificial. El Documento CONPES 3975 (2019) estableció la Política Nacional de IA, reconociendo que es una tecnología estratégica para el desarrollo del siglo XXI. En el sector público, proyectos como MARDUK en el Consejo Superior de la Judicatura han demostrado que las RNA pueden automatizar tareas documentales complejas: reconocimiento de entidades en expedientes judiciales, clasificación de tutelas y análisis de tiempos procesales.
Sin embargo, persisten brechas importantes: la infraestructura computacional es limitada fuera de Bogotá, Medellín y Cali; la formación de talento especializado en IA apenas inicia en programas de posgrado como el que cursamos; y la inversión en I+D como porcentaje del PIB sigue siendo baja comparada con países líderes. El reto no es solo técnico sino también de política pública, cultura digital y democratización del acceso a herramientas como las que usamos en este ejercicio.
10. Conclusiones¶
El desarrollo de este ejercicio permitió consolidar los conceptos fundamentales de las redes neuronales artificiales aplicadas a visión por computador:
Las redes neuronales emullan el aprendizaje biológico: mediante la propagación de señales ponderadas y el ajuste iterativo de pesos a través del descenso de gradiente, el modelo aprende a reconocer patrones visuales de manera autónoma.
El preprocesamiento es tan importante como el modelo: normalizar las imágenes y aplicar one-hot encoding a las etiquetas son pasos críticos que condicionan directamente la capacidad de aprendizaje de la red.
La arquitectura elegida fue adecuada: Flatten → Dense(128, ReLU) → Dense(10, Softmax) logró ~97.5% de exactitud en MNIST en solo 5 épocas, confirmando que redes relativamente simples pueden resolver problemas bien definidos con alta precisión.
El entrenamiento por mini-batches es eficiente: dividir el dataset en lotes de 100 imágenes redujo el costo computacional sin sacrificar calidad, permitiendo entrenar en minutos en Colab.
Las redes densas tienen limitaciones: para tareas más complejas de visión por computador (reconocimiento de objetos, detección de bordes en imágenes de alta resolución), las redes convolucionales (CNN) son significativamente más eficaces porque aprovechan la estructura espacial de las imágenes.
Este ejercicio sienta las bases conceptuales y prácticas para los temas de semanas posteriores: redes convolucionales, regularización y transfer learning.¶
Referencias¶
Ballard, W. (2018). Hands-on deep learning for images with TensorFlow: Build intelligent computer vision applications using TensorFlow and Keras (Chapter 3, pp. 33–51). Packt Publishing.
Dadhich, A. (2018). Practical computer vision: Extract insightful information from images using TensorFlow, Keras, and OpenCV (Chapter 2, pp. 23–38). Packt Publishing.
Quiñones Huatangari, L., Ochoa Toledo, L., Gamarra Torres, O., Bazán Correa, J., Delgado Soto, J., & Kemper Valverde, N. (2020). Red neuronal artificial para estimar un índice de calidad de agua. Enfoque UTE, 11(2), 113-124. https://doi.org/10.29019/enfoque.v11n2.633
TensorFlow. (s.f.). TensorFlow documentation. https://www.tensorflow.org/
Google Colaboratory. (s.f.). Google Colab. https://colab.research.google.com/