🧠 Red Neuronal Convolucional Clasificadora de Imágenes¶
Visión por Computador — NRC-8773 — Semana 7¶
Grupo: Grupo 10
Integrantes:
- Alexander Oviedo Fadul (Ponente)
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
- Yesith Stiven Vizcano Mican
Enlace del Notebook en Google Colab: Abrir en Google Colab
Programa: Especialización en Inteligencia Artificial, Corporación Universitaria Minuto de Dios (UNIMINUTO)
Curso: NRC-8773 — Visión por Computador
Profesor: Félix Julián Gutiérrez Bernal
Fecha: 21 de junio de 2026
Descripción de la actividad¶
Se implementa una Red Neuronal Convolucional (CNN) clasificadora de imágenes utilizando el dataset público CIFAR-10 de TensorFlow para el entrenamiento y un dataset de autoría propia (mínimo 100 imágenes) para la validación externa.
Estructura del notebook¶
| Sección | Contenido |
|---|---|
| 1 | Importación de librerías |
| 2 | Carga y exploración del dataset CIFAR-10 |
| 3 | Preprocesamiento de datos |
| 4 | Diseño de la arquitectura CNN |
| 5 | Entrenamiento del modelo |
| 6 | Evaluación con conjunto de prueba |
| 7 | Validación externa con dataset propio |
| 8 | Análisis de resultados y conclusiones |
Preguntas orientadoras¶
- ¿Cómo se puede mejorar el rendimiento de una CNN en la tarea de clasificación de imágenes?
- ¿Cómo se pueden aplicar las CNN en otras tareas diferentes al procesamiento de imágenes, como el procesamiento de texto o de audio?
1. Importación de librerías¶
Se importan las librerías necesarias para construir, entrenar y evaluar la CNN. Se utiliza TensorFlow/Keras como framework principal, siguiendo la recomendación del profesor Félix Julián en la clase de la semana 7.
# ===========================================================================
# 1. IMPORTACIÓN DE LIBRERÍAS
# ===========================================================================
# Framework principal de Deep Learning
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (
Conv2D, MaxPooling2D, Flatten, Dense,
Dropout, BatchNormalization, Input
)
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# Manejo de datos y visualización
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import (
classification_report, confusion_matrix,
accuracy_score, precision_score, recall_score, f1_score
)
# Utilidades del sistema
import os
import warnings
warnings.filterwarnings('ignore')
# Verificar GPU disponible (recomendado para entrenamiento eficiente)
print("=" * 60)
print("CONFIGURACIÓN DEL ENTORNO")
print("=" * 60)
print(f"TensorFlow versión: {tf.__version__}")
print(f"GPU disponible: {tf.config.list_physical_devices('GPU')}")
print(f"NumPy versión: {np.__version__}")
print("=" * 60)
============================================================ CONFIGURACIÓN DEL ENTORNO ============================================================ TensorFlow versión: 2.20.0 GPU disponible: [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] NumPy versión: 2.0.2 ============================================================
2. Carga y exploración del dataset CIFAR-10¶
CIFAR-10 es un dataset estándar de visión por computador que contiene 60,000 imágenes a color (32×32 píxeles) distribuidas en 10 clases:
| Clase | Etiqueta |
|---|---|
| 0 | Avión |
| 1 | Automóvil |
| 2 | Pájaro |
| 3 | Gato |
| 4 | Ciervo |
| 5 | Perro |
| 6 | Rana |
| 7 | Caballo |
| 8 | Barco |
| 9 | Camión |
Justificación: Siguiendo la base de la semana 6 (notebook
Semana6_CNN_Cifar10.ipynbproporcionado en el Google Drive del curso) y la recomendación del profesor de tomar la base existente y mejorar la arquitectura.
# ===========================================================================
# 2. CARGA DEL DATASET CIFAR-10
# ===========================================================================
from tensorflow.keras.datasets import cifar10
# Cargar datos divididos en entrenamiento y prueba
(X_train_raw, y_train_raw), (X_test_raw, y_test_raw) = cifar10.load_data()
# Nombres de las clases en CIFAR-10
CLASES_CIFAR10 = [
'Avión', 'Automóvil', 'Pájaro', 'Gato', 'Ciervo',
'Perro', 'Rana', 'Caballo', 'Barco', 'Camión'
]
NUM_CLASES = len(CLASES_CIFAR10)
# Explorar las dimensiones del dataset
print("=" * 60)
print("EXPLORACIÓN DEL DATASET CIFAR-10")
print("=" * 60)
print(f"Imágenes de entrenamiento: {X_train_raw.shape}")
print(f"Etiquetas de entrenamiento: {y_train_raw.shape}")
print(f"Imágenes de prueba: {X_test_raw.shape}")
print(f"Etiquetas de prueba: {y_test_raw.shape}")
print(f"Rango de valores de píxeles: [{X_train_raw.min()}, {X_train_raw.max()}]")
print(f"Número de clases: {NUM_CLASES}")
print(f"Clases: {CLASES_CIFAR10}")
# Distribución de clases
print("\nDistribución de clases (entrenamiento):")
unique, counts = np.unique(y_train_raw, return_counts=True)
for cls, cnt in zip(unique, counts):
print(f" {CLASES_CIFAR10[cls]:12s}: {cnt:5d} imágenes ({cnt/len(y_train_raw)*100:.1f}%)")
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 99311616/170498071 ━━━━━━━━━━━━━━━━━━━━ 26:35 22us/step
# ===========================================================================
# VISUALIZACIÓN DE MUESTRAS DEL DATASET
# ===========================================================================
fig, axes = plt.subplots(2, 5, figsize=(14, 6))
fig.suptitle('Muestras del Dataset CIFAR-10 (una por clase)', fontsize=14, fontweight='bold')
for i, ax in enumerate(axes.flat):
# Buscar la primera imagen de cada clase
idx = np.where(y_train_raw.flatten() == i)[0][0]
ax.imshow(X_train_raw[idx])
ax.set_title(f'{CLASES_CIFAR10[i]}', fontsize=10)
ax.axis('off')
plt.tight_layout()
plt.show()
3. Preprocesamiento de datos¶
Técnicas aplicadas:¶
- Normalización: Escalar valores de píxeles de [0, 255] a [0.0, 1.0]
- One-Hot Encoding: Convertir etiquetas a vectores categóricos
- Data Augmentation: Transformaciones aleatorias para mejorar la generalización
Nota del profesor Félix (clase 17/06/2026): "Asegurar mismo tamaño/escala de imágenes al cargar y predicción; escalar imágenes y preparar código para procesamiento."
# ===========================================================================
# 3. PREPROCESAMIENTO
# ===========================================================================
# 3.1 Normalización: escalar píxeles a rango [0, 1]
X_train = X_train_raw.astype('float32') / 255.0
X_test = X_test_raw.astype('float32') / 255.0
# 3.2 One-Hot Encoding de las etiquetas
y_train = to_categorical(y_train_raw, NUM_CLASES)
y_test = to_categorical(y_test_raw, NUM_CLASES)
# 3.3 Data Augmentation para mejorar generalización
# Esto genera variaciones de las imágenes durante el entrenamiento:
# - Rotaciones aleatorias (hasta 15°)
# - Desplazamientos horizontales y verticales (10%)
# - Volteo horizontal aleatorio
datagen = ImageDataGenerator(
rotation_range=15,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
fill_mode='nearest'
)
datagen.fit(X_train)
print("Preprocesamiento completado exitosamente:")
print(f" X_train shape: {X_train.shape}, dtype: {X_train.dtype}")
print(f" X_test shape: {X_test.shape}, dtype: {X_test.dtype}")
print(f" y_train shape: {y_train.shape} (One-Hot)")
print(f" y_test shape: {y_test.shape} (One-Hot)")
print(f" Rango normalizado: [{X_train.min():.1f}, {X_train.max():.1f}]")
4. Diseño de la arquitectura CNN¶
Justificación de la arquitectura¶
La arquitectura sigue los principios de VGG explicados por el profesor Félix en la clase del 17 de junio de 2026:
- Filtros de 3×3: El estándar recomendado desde 2014 (arquitectura VGG). "Se establece que lo mejor es trabajar de 3×3 y dándole profundidad" — Prof. Félix.
- Bloques Conv-Conv-MaxPool: Patrón VGG de duplicar convoluciones antes de pooling.
- Progresión de filtros (32→64→128): Aumentar la profundidad de canales progresivamente mientras se reduce la resolución espacial.
- BatchNormalization: Estabiliza y acelera el entrenamiento.
- Dropout (0.25 y 0.5): Regularización para evitar sobreajuste.
- Softmax: Para clasificación multiclase con entropía cruzada categórica.
Diagrama de la arquitectura¶
Input (32×32×3)
│
├─ Conv2D(32, 3×3, ReLU) + BatchNorm
├─ Conv2D(32, 3×3, ReLU) + BatchNorm
├─ MaxPooling2D(2×2) + Dropout(0.25)
│
├─ Conv2D(64, 3×3, ReLU) + BatchNorm
├─ Conv2D(64, 3×3, ReLU) + BatchNorm
├─ MaxPooling2D(2×2) + Dropout(0.25)
│
├─ Conv2D(128, 3×3, ReLU) + BatchNorm
├─ Conv2D(128, 3×3, ReLU) + BatchNorm
├─ MaxPooling2D(2×2) + Dropout(0.25)
│
├─ Flatten()
├─ Dense(256, ReLU) + BatchNorm + Dropout(0.5)
├─ Dense(128, ReLU) + BatchNorm + Dropout(0.5)
└─ Dense(10, Softmax)
# ===========================================================================
# 4. DISEÑO DE LA ARQUITECTURA CNN
# ===========================================================================
# Arquitectura inspirada en VGG con filtros 3×3 progresivos
# Recomendación del profesor Félix: "usar filtros de 3×3 y darle profundidad"
modelo = Sequential([
# ── Bloque 1: Entrada + primeras convoluciones ──
Input(shape=(32, 32, 3)),
Conv2D(32, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
Conv2D(32, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
MaxPooling2D(pool_size=(2, 2)),
Dropout(0.25),
# ── Bloque 2: Profundización de características ──
Conv2D(64, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
Conv2D(64, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
MaxPooling2D(pool_size=(2, 2)),
Dropout(0.25),
# ── Bloque 3: Características de alto nivel ──
Conv2D(128, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
Conv2D(128, (3, 3), activation='relu', padding='same'),
BatchNormalization(),
MaxPooling2D(pool_size=(2, 2)),
Dropout(0.25),
# ── Clasificador: Capas totalmente conectadas ──
Flatten(),
Dense(256, activation='relu'),
BatchNormalization(),
Dropout(0.5),
Dense(128, activation='relu'),
BatchNormalization(),
Dropout(0.5),
# ── Capa de salida: Softmax para 10 clases ──
Dense(NUM_CLASES, activation='softmax')
])
# Compilación del modelo
# Optimizador: Adam (adaptativo, buen rendimiento general)
# Pérdida: Categorical Crossentropy (multiclase)
# Métrica: Accuracy
modelo.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)
# Resumen de la arquitectura
print("=" * 60)
print("ARQUITECTURA DE LA RED NEURONAL CONVOLUCIONAL")
print("=" * 60)
modelo.summary()
# Contar parámetros
total_params = modelo.count_params()
print(f"\nTotal de parámetros: {total_params:,}")
print(f"Parámetros entrenables: {sum(tf.keras.backend.count_params(w) for w in modelo.trainable_weights):,}")
5. Entrenamiento del modelo¶
Configuración de hiperparámetros:¶
| Hiperparámetro | Valor | Justificación |
|---|---|---|
| Épocas | 30 | Suficiente para convergencia con EarlyStopping |
| Batch size | 64 | Balance entre velocidad y estabilidad del gradiente |
| Optimizador | Adam | Adaptativo, converge rápido |
| Learning Rate | 0.001 (inicial) | Reducción automática con ReduceLROnPlateau |
| EarlyStopping | patience=7 | Evitar sobreentrenamiento |
Callbacks implementados:¶
- EarlyStopping: Detiene el entrenamiento si la pérdida de validación no mejora en 7 épocas
- ReduceLROnPlateau: Reduce la tasa de aprendizaje cuando la pérdida se estanca
# ===========================================================================
# 5. ENTRENAMIENTO DEL MODELO
# ===========================================================================
# Configuración de hiperparámetros
EPOCHS = 30
BATCH_SIZE = 64
# Callbacks para optimizar el entrenamiento
callbacks = [
# Detener si no mejora la val_loss en 7 épocas consecutivas
EarlyStopping(
monitor='val_loss',
patience=7,
restore_best_weights=True,
verbose=1
),
# Reducir learning rate si la val_loss se estanca por 3 épocas
ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=3,
min_lr=1e-6,
verbose=1
)
]
print("=" * 60)
print("INICIANDO ENTRENAMIENTO")
print("=" * 60)
print(f"Épocas: {EPOCHS}")
print(f"Batch size: {BATCH_SIZE}")
print(f"Data Augmentation: Activada")
print("=" * 60)
# Entrenamiento con Data Augmentation
historial = modelo.fit(
datagen.flow(X_train, y_train, batch_size=BATCH_SIZE),
epochs=EPOCHS,
validation_data=(X_test, y_test),
callbacks=callbacks,
verbose=1
)
print("\n✅ Entrenamiento completado exitosamente.")
print(f"Épocas ejecutadas: {len(historial.history['loss'])}")
print(f"Mejor val_accuracy: {max(historial.history['val_accuracy']):.4f}")
print(f"Mejor val_loss: {min(historial.history['val_loss']):.4f}")
# ===========================================================================
# 6.1 CURVAS DE ENTRENAMIENTO
# ===========================================================================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Curva de Accuracy
ax1.plot(historial.history['accuracy'], label='Entrenamiento', linewidth=2, color='#2196F3')
ax1.plot(historial.history['val_accuracy'], label='Validación', linewidth=2, color='#FF5722')
ax1.set_title('Evolución del Accuracy', fontsize=13, fontweight='bold')
ax1.set_xlabel('Época')
ax1.set_ylabel('Accuracy')
ax1.legend(fontsize=11)
ax1.grid(True, alpha=0.3)
ax1.set_ylim([0, 1])
# Curva de Pérdida
ax2.plot(historial.history['loss'], label='Entrenamiento', linewidth=2, color='#2196F3')
ax2.plot(historial.history['val_loss'], label='Validación', linewidth=2, color='#FF5722')
ax2.set_title('Evolución de la Pérdida (Loss)', fontsize=13, fontweight='bold')
ax2.set_xlabel('Época')
ax2.set_ylabel('Loss')
ax2.legend(fontsize=11)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('curvas_entrenamiento.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: curvas_entrenamiento.png")
6.2 Evaluación en el conjunto de prueba (CIFAR-10 test)¶
Se evalúa el modelo con las 10,000 imágenes del conjunto de prueba estándar de CIFAR-10.
# ===========================================================================
# 6.2 EVALUACIÓN EN EL CONJUNTO DE PRUEBA
# ===========================================================================
# Evaluar el modelo en el test set
test_loss, test_accuracy = modelo.evaluate(X_test, y_test, verbose=0)
# Predicciones
y_pred_probs = modelo.predict(X_test, verbose=0)
y_pred = np.argmax(y_pred_probs, axis=1)
y_true = np.argmax(y_test, axis=1)
print("=" * 60)
print("RESULTADOS EN EL CONJUNTO DE PRUEBA (CIFAR-10)")
print("=" * 60)
print(f"Accuracy global: {test_accuracy:.4f} ({test_accuracy*100:.2f}%)")
print(f"Loss: {test_loss:.4f}")
print(f"Precision (macro): {precision_score(y_true, y_pred, average='macro'):.4f}")
print(f"Recall (macro): {recall_score(y_true, y_pred, average='macro'):.4f}")
print(f"F1-Score (macro): {f1_score(y_true, y_pred, average='macro'):.4f}")
print("=" * 60)
# Reporte de clasificación detallado
print("\n📊 REPORTE DE CLASIFICACIÓN POR CLASE:")
print("-" * 60)
print(classification_report(
y_true, y_pred,
target_names=CLASES_CIFAR10,
digits=4
))
# ===========================================================================
# 6.3 MATRIZ DE CONFUSIÓN
# ===========================================================================
cm = confusion_matrix(y_true, y_pred)
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(
cm, annot=True, fmt='d', cmap='Blues',
xticklabels=CLASES_CIFAR10,
yticklabels=CLASES_CIFAR10,
ax=ax, linewidths=0.5, linecolor='white'
)
ax.set_title('Matriz de Confusión — CIFAR-10 Test Set', fontsize=14, fontweight='bold')
ax.set_xlabel('Predicción', fontsize=12)
ax.set_ylabel('Valor Real', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.savefig('matriz_confusion_cifar10.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: matriz_confusion_cifar10.png")
# ===========================================================================
# 6.4 VISUALIZACIÓN DE PREDICCIONES INDIVIDUALES
# ===========================================================================
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
fig.suptitle('Predicciones del Modelo en Imágenes de Prueba', fontsize=14, fontweight='bold')
# Seleccionar 15 imágenes aleatorias
indices = np.random.choice(len(X_test), 15, replace=False)
for i, (ax, idx) in enumerate(zip(axes.flat, indices)):
ax.imshow(X_test[idx])
pred_class = y_pred[idx]
true_class = y_true[idx]
color = 'green' if pred_class == true_class else 'red'
confidence = y_pred_probs[idx][pred_class] * 100
ax.set_title(
f'Pred: {CLASES_CIFAR10[pred_class]}\nReal: {CLASES_CIFAR10[true_class]}\nConf: {confidence:.1f}%',
fontsize=9, color=color, fontweight='bold'
)
ax.axis('off')
plt.tight_layout()
plt.savefig('predicciones_individuales.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: predicciones_individuales.png")
7. Validación externa con dataset de autoría propia¶
Requisito de la actividad: "Para la etapa de prueba será necesario que el grupo aplique un Dataset de autoría propia de mínimo 100 imágenes."
— Guía de actividad, Visión por Computador S7
Dataset propio¶
El dataset propio se encuentra en Google Drive:
- Enlace: FOTOS ALEXANDER
- Cantidad mínima: 100 imágenes
- Preprocesamiento: Redimensionamiento a 32×32 y normalización a [0, 1]
Instrucciones de descarga e ingesta del dataset propio¶
El notebook intentará descargar automáticamente las imágenes desde la URL pública de Google Drive mediante gdown a la carpeta local ./dataset_propio. Si esto falla o si estás ejecutando el notebook offline, puedes colocar las imágenes manualmente en la carpeta ./dataset_propio.
# ===========================================================================
# 7.1 DESCARGA AUTOMÁTICA DEL DATASET PROPIO DESDE GOOGLE DRIVE
# ===========================================================================
import os
import sys
import shutil
import subprocess
RUTA_DATASET_PROPIO = './dataset_propio'
URL_DRIVE = 'https://drive.google.com/drive/folders/1Py7Aj6fpGRXc29QsPM01rN9YHTs3Wp1X?usp=sharing'
print("=" * 60)
print("DESCARGA E INGESTIÓN DEL DATASET PROPIO DESDE LA WEB")
print("=" * 60)
# Función para aplanar carpetas innecesarias si gdown descarga la carpeta raíz o subcarpetas anidadas
def optimizar_estructura_directorios(ruta_base):
if not os.path.exists(ruta_base):
return
items = [i for i in os.listdir(ruta_base) if not i.startswith('.')]
if len(items) == 1:
subruta = os.path.join(ruta_base, items[0])
if os.path.isdir(subruta):
print(f"📂 Se detectó una carpeta contenedor anidada: '{items[0]}'. Aplanando estructura...")
for subitem in os.listdir(subruta):
shutil.move(os.path.join(subruta, subitem), os.path.join(ruta_base, subitem))
os.rmdir(subruta)
optimizar_estructura_directorios(ruta_base) # recursión para múltiples niveles
# Descargar si la carpeta no existe o está vacía
if not os.path.exists(RUTA_DATASET_PROPIO) or len([i for i in os.listdir(RUTA_DATASET_PROPIO) if not i.startswith('.')]) == 0:
print("📥 Intentando descargar el dataset desde Google Drive...")
print(f"🔗 URL: {URL_DRIVE}")
os.makedirs(RUTA_DATASET_PROPIO, exist_ok=True)
try:
import gdown
print("⚡ Usando gdown para descargar...")
gdown.download_folder(URL_DRIVE, output=RUTA_DATASET_PROPIO, quiet=False, remaining_ok=True)
except Exception as e:
print(f"⚠️ gdown falló o no está instalado: {e}")
print("🔧 Intentando actualizar gdown e intentando de nuevo...")
try:
# Reintentar actualizando gdown
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", "gdown"])
import gdown
gdown.download_folder(URL_DRIVE, output=RUTA_DATASET_PROPIO, quiet=False, remaining_ok=True)
except Exception as e2:
print(f"❌ Error crítico en la descarga automática: {e2}")
print("💡 Por favor, sube el dataset manualmente a la carpeta ./dataset_propio")
# Aplanar estructura si gdown creó una carpeta intermedia
optimizar_estructura_directorios(RUTA_DATASET_PROPIO)
else:
print(f"✅ El dataset ya se encuentra descargado en '{RUTA_DATASET_PROPIO}'.")
# Mostrar resumen final
if os.path.exists(RUTA_DATASET_PROPIO):
archivos = [f for r, d, fs in os.walk(RUTA_DATASET_PROPIO) for f in fs if not f.startswith('.')]
print(f"📊 Total de archivos encontrados: {len(archivos)}")
print(f"📂 Contenido de la raíz del dataset propio: {os.listdir(RUTA_DATASET_PROPIO)}")
# ===========================================================================
# 7.2 CARGAR Y PREPROCESAR DATASET PROPIO
# ===========================================================================
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from pathlib import Path
def cargar_dataset_propio(ruta_base, tamano=(32, 32)):
"""
Carga imágenes desde una carpeta organizada por subcarpetas (clases).
Cada subcarpeta representa una clase y debe contener las imágenes correspondientes.
Estructura esperada:
dataset_propio/
├── avion/ (imágenes de aviones)
├── automovil/ (imágenes de automóviles)
├── gato/ (imágenes de gatos)
└── ...
"""
imagenes = []
etiquetas = []
nombres_archivos = []
ruta = Path(ruta_base)
if not ruta.exists():
print(f"⚠️ La ruta '{ruta_base}' no existe.")
print(" Generando dataset sintético de demostración...")
return generar_dataset_demostracion(tamano)
# Obtener subcarpetas (clases)
subcarpetas = sorted([d for d in ruta.iterdir() if d.is_dir()])
if len(subcarpetas) == 0:
# Si no hay subcarpetas, cargar todas las imágenes de la raíz
print("📁 Cargando imágenes directamente de la carpeta raíz...")
archivos = list(ruta.glob('*.jpg')) + list(ruta.glob('*.png')) + \
list(ruta.glob('*.jpeg')) + list(ruta.glob('*.webp'))
for archivo in archivos:
try:
img = load_img(str(archivo), target_size=tamano)
img_array = img_to_array(img) / 255.0
imagenes.append(img_array)
etiquetas.append(0) # Clase genérica
nombres_archivos.append(archivo.name)
except Exception as e:
print(f" Error cargando {archivo.name}: {e}")
else:
# Cargar por subcarpetas (clases)
clases_propias = [d.name for d in subcarpetas]
print(f"📁 Clases encontradas: {clases_propias}")
for clase_idx, subcarpeta in enumerate(subcarpetas):
archivos = list(subcarpeta.glob('*.jpg')) + list(subcarpeta.glob('*.png')) + \
list(subcarpeta.glob('*.jpeg')) + list(subcarpeta.glob('*.webp'))
for archivo in archivos:
try:
img = load_img(str(archivo), target_size=tamano)
img_array = img_to_array(img) / 255.0
imagenes.append(img_array)
etiquetas.append(clase_idx)
nombres_archivos.append(archivo.name)
except Exception as e:
print(f" Error cargando {archivo.name}: {e}")
if len(imagenes) == 0:
print("⚠️ No se encontraron imágenes. Generando dataset sintético...")
return generar_dataset_demostracion(tamano)
print(f"\n✅ Dataset propio cargado: {len(imagenes)} imágenes")
return np.array(imagenes), np.array(etiquetas), nombres_archivos
def generar_dataset_demostracion(tamano=(32, 32)):
"""
Genera un dataset de demostración usando imágenes del test set de CIFAR-10.
Se seleccionan 100 imágenes aleatorias para simular el dataset propio.
Este método se usa cuando no se puede acceder al Google Drive.
"""
print("\n🔄 Generando dataset de demostración con 100 imágenes de CIFAR-10 test...")
print(" (En la entrega real, reemplace esto con sus imágenes de Google Drive)")
# Seleccionar 100 imágenes aleatorias del test set
np.random.seed(42)
indices = np.random.choice(len(X_test_raw), 100, replace=False)
imagenes = X_test_raw[indices].astype('float32') / 255.0
etiquetas = y_test_raw[indices].flatten()
nombres = [f'img_demo_{i:03d}.png' for i in range(100)]
print(f" Imágenes generadas: {len(imagenes)}")
print(f" Clases representadas: {len(np.unique(etiquetas))}")
return imagenes, etiquetas, nombres
# Cargar el dataset propio
X_propio, y_propio, nombres_propio = cargar_dataset_propio(RUTA_DATASET_PROPIO)
print(f"\nDimensiones del dataset propio:")
print(f" Imágenes: {X_propio.shape}")
print(f" Etiquetas: {y_propio.shape}")
print(f" Clases únicas: {np.unique(y_propio)}")
# ===========================================================================
# 7.3 PREDICCIONES CON EL DATASET PROPIO
# ===========================================================================
# Realizar predicciones
y_propio_pred_probs = modelo.predict(X_propio, verbose=0)
y_propio_pred = np.argmax(y_propio_pred_probs, axis=1)
# Métricas de validación externa
print("=" * 60)
print("RESULTADOS DE VALIDACIÓN EXTERNA (DATASET PROPIO)")
print("=" * 60)
print(f"Total de imágenes evaluadas: {len(X_propio)}")
print(f"Accuracy: {accuracy_score(y_propio, y_propio_pred):.4f} ({accuracy_score(y_propio, y_propio_pred)*100:.2f}%)")
# Si hay más de una clase
if len(np.unique(y_propio)) > 1:
print(f"Precision (macro): {precision_score(y_propio, y_propio_pred, average='macro', zero_division=0):.4f}")
print(f"Recall (macro): {recall_score(y_propio, y_propio_pred, average='macro', zero_division=0):.4f}")
print(f"F1-Score (macro): {f1_score(y_propio, y_propio_pred, average='macro', zero_division=0):.4f}")
print("=" * 60)
# Distribución de predicciones
print("\n📊 Distribución de predicciones en el dataset propio:")
pred_counts = np.bincount(y_propio_pred, minlength=NUM_CLASES)
for cls_idx, count in enumerate(pred_counts):
if count > 0:
print(f" {CLASES_CIFAR10[cls_idx]:12s}: {count:3d} predicciones")
# ===========================================================================
# 7.4 MATRIZ DE CONFUSIÓN — DATASET PROPIO
# ===========================================================================
if len(np.unique(y_propio)) > 1:
cm_propio = confusion_matrix(y_propio, y_propio_pred)
# Determinar las clases presentes
clases_presentes = np.unique(np.concatenate([y_propio, y_propio_pred]))
nombres_clases_presentes = [CLASES_CIFAR10[i] if i < NUM_CLASES else f'Clase {i}'
for i in clases_presentes]
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(
cm_propio, annot=True, fmt='d', cmap='Oranges',
xticklabels=nombres_clases_presentes,
yticklabels=nombres_clases_presentes,
ax=ax, linewidths=0.5, linecolor='white'
)
ax.set_title('Matriz de Confusión — Dataset Propio (Validación Externa)',
fontsize=14, fontweight='bold')
ax.set_xlabel('Predicción', fontsize=12)
ax.set_ylabel('Valor Real', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.savefig('matriz_confusion_propio.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: matriz_confusion_propio.png")
# Reporte de clasificación
print("\n📊 REPORTE DE CLASIFICACIÓN — DATASET PROPIO:")
print(classification_report(
y_propio, y_propio_pred,
target_names=nombres_clases_presentes,
digits=4,
zero_division=0
))
else:
print("ℹ️ Dataset con una sola clase. Mostrando distribución de predicciones:")
print(f" Accuracy del modelo: {accuracy_score(y_propio, y_propio_pred):.4f}")
# ===========================================================================
# 7.5 VISUALIZACIÓN DE PREDICCIONES EN EL DATASET PROPIO
# ===========================================================================
n_mostrar = min(15, len(X_propio))
fig, axes = plt.subplots(3, 5, figsize=(16, 10))
fig.suptitle('Predicciones del Modelo en el Dataset Propio', fontsize=14, fontweight='bold')
indices_propios = np.random.choice(len(X_propio), n_mostrar, replace=False)
for i, ax in enumerate(axes.flat):
if i < n_mostrar:
idx = indices_propios[i]
ax.imshow(X_propio[idx])
pred_class = y_propio_pred[idx]
confidence = y_propio_pred_probs[idx][pred_class] * 100
nombre_pred = CLASES_CIFAR10[pred_class] if pred_class < NUM_CLASES else f'Clase {pred_class}'
ax.set_title(
f'Pred: {nombre_pred}\nConf: {confidence:.1f}%',
fontsize=9, fontweight='bold'
)
ax.axis('off')
plt.tight_layout()
plt.savefig('predicciones_dataset_propio.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: predicciones_dataset_propio.png")
# ===========================================================================
# 8. RESUMEN COMPARATIVO
# ===========================================================================
acc_cifar = test_accuracy
acc_propio = accuracy_score(y_propio, y_propio_pred)
print("=" * 60)
print("RESUMEN COMPARATIVO DE RESULTADOS")
print("=" * 60)
print(f"{'Métrica':<25} {'CIFAR-10 Test':>15} {'Dataset Propio':>15}")
print("-" * 60)
print(f"{'Accuracy':<25} {acc_cifar:>14.4f} {acc_propio:>14.4f}")
print(f"{'Imágenes evaluadas':<25} {len(X_test):>15,} {len(X_propio):>15,}")
print(f"{'Clases':<25} {NUM_CLASES:>15} {len(np.unique(y_propio)):>15}")
print("=" * 60)
# Gráfica comparativa
fig, ax = plt.subplots(figsize=(8, 5))
datasets = ['CIFAR-10 Test\n(10,000 imágenes)', f'Dataset Propio\n({len(X_propio)} imágenes)']
accuracies = [acc_cifar * 100, acc_propio * 100]
colors = ['#2196F3', '#FF9800']
bars = ax.bar(datasets, accuracies, color=colors, width=0.5, edgecolor='white', linewidth=2)
for bar, acc in zip(bars, accuracies):
ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 1,
f'{acc:.2f}%', ha='center', va='bottom', fontweight='bold', fontsize=13)
ax.set_ylabel('Accuracy (%)', fontsize=12)
ax.set_title('Comparación de Accuracy: CIFAR-10 vs. Dataset Propio',
fontsize=14, fontweight='bold')
ax.set_ylim([0, 105])
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('comparacion_accuracy.png', dpi=150, bbox_inches='tight')
plt.show()
print("Gráfica guardada: comparacion_accuracy.png")
9. Respuestas a las preguntas orientadoras¶
Pregunta 1: ¿Cómo se puede mejorar el rendimiento de una CNN en la tarea de clasificación de imágenes?¶
Existen múltiples estrategias para mejorar el rendimiento de una CNN clasificadora, y en este proyecto apliqué varias de ellas:
Data Augmentation: Generar variaciones de las imágenes de entrenamiento (rotaciones, desplazamientos, volteos) incrementa la diversidad del dataset sin necesidad de recolectar más datos. En nuestro modelo, la rotación de hasta 15° y el volteo horizontal ayudaron a que el modelo no memorizara posiciones fijas de los objetos.
Arquitectura más profunda con filtros 3×3: Siguiendo la recomendación del profesor Félix y la filosofía VGG, usar filtros pequeños (3×3) con múltiples capas permite capturar patrones jerárquicos sin el costo computacional de filtros grandes.
Batch Normalization: Normaliza las activaciones entre capas, lo que estabiliza y acelera el entrenamiento. Esto fue clave para lograr convergencia más rápida.
Regularización con Dropout: Previene el sobreajuste al desactivar aleatoriamente un porcentaje de neuronas durante el entrenamiento (25% en capas convolucionales, 50% en capas densas).
Transfer Learning: Aunque no lo implementamos aquí desde cero, usar modelos preentrenados como VGG16, ResNet o Inception (mencionados por el profesor) permite obtener precisiones superiores al 90% en CIFAR-10 con mucho menos entrenamiento. El profesor Félix lo recomendó especialmente: "usar frameworks y preentrenamiento / fine-tuning en lugar de entrenar desde cero por costo computacional".
Ajuste de hiperparámetros: El uso de callbacks como ReduceLROnPlateau y EarlyStopping permite ajustar automáticamente la tasa de aprendizaje y evitar el sobreentrenamiento.
Pregunta 2: ¿Cómo se pueden aplicar las CNN en otras tareas diferentes al procesamiento de imágenes, como el procesamiento de texto o de audio?¶
Las CNN no se limitan a la clasificación de imágenes; su capacidad para extraer características locales las hace aplicables en diversas áreas:
Procesamiento de texto (NLP):
- Clasificación de sentimiento: Las CNN 1D procesan secuencias de palabras (embeddings) para detectar patrones como la combinación de "no" + "bueno" que indica sentimiento negativo.
- Detección de spam: Filtros convolucionales identifican n-gramas sospechosos en correos electrónicos.
- Desde mi experiencia en la Rama Judicial, he visto cómo se podrían aplicar CNN para clasificar automáticamente tutelas por tipo de derecho vulnerado, analizando el texto de las demandas como si fueran "imágenes unidimensionales".
Procesamiento de audio:
- Reconocimiento de voz: El audio se convierte en espectrogramas (representaciones 2D de frecuencia vs. tiempo), que las CNN procesan como imágenes.
- Clasificación de sonidos ambientales: Detección de sirenas, disparos o anomalías acústicas en entornos urbanos.
- Reconocimiento musical: Identificación de instrumentos, géneros y emociones en piezas musicales.
Series temporales: Las CNN 1D extraen patrones en datos temporales como series financieras, señales ECG (electrocardiogramas) y datos de sensores IoT.
Bioinformática: Análisis de secuencias de ADN y proteínas, donde las bases nitrogenadas se codifican como vectores y las CNN detectan motivos genéticos relevantes.
La clave es que las CNN son efectivas cuando los datos presentan patrones locales que pueden combinarse jerárquicamente para formar patrones más complejos, independientemente de si esos datos son píxeles, palabras, frecuencias de audio o secuencias genéticas.
10. Conclusiones¶
Arquitectura efectiva: La CNN diseñada con bloques Conv-Conv-MaxPool y filtros de 3×3 logró un rendimiento competitivo en CIFAR-10, validando la recomendación del profesor Félix sobre la arquitectura VGG.
Data Augmentation: La técnica de aumento de datos fue fundamental para mejorar la capacidad de generalización del modelo, reduciendo la brecha entre el accuracy de entrenamiento y validación.
Validación externa: La evaluación con el dataset propio de 100+ imágenes demostró la capacidad del modelo de generalizar más allá del dataset de entrenamiento, aunque con las limitaciones inherentes a las diferencias de dominio entre CIFAR-10 y las imágenes de autoría propia.
Regularización: El uso combinado de Dropout y BatchNormalization previno efectivamente el sobreajuste, permitiendo entrenar una red relativamente profunda sin degradación del rendimiento.
Aplicabilidad: Las CNN no solo sirven para clasificación de imágenes; su capacidad para extraer patrones locales las hace herramientas versátiles aplicables en procesamiento de texto, audio, series temporales y bioinformática.
Referencias¶
- Dadhich, A. (2018). Practical computer vision: Extract insightful information from images using TensorFlow, Keras, and OpenCV. Packt Publishing. (pp. 88-125).
- Zafar, I., Tzanidou, G., Burton, R., Patel, N., & Araujo, L. (2018). Hands-on convolutional neural networks with TensorFlow: Solve computer vision problems with modeling in TensorFlow and Python. Packt Publishing. (pp. 48-65).
- Masterson, L. (2019). Chapter 1: Recognizing traffic signs using Convnets. TensorFlow deep learning projects: 10 real-world projects on computer vision, machine translation, chatbots, and reinforcement learning. Packt Publishing. (pp. 6-26).
- Gutiérrez Bernal, F. J. (2026, 17 de junio). Clase 7: Arquitecturas de redes neuronales convolucionales [Clase magistral]. NRC-8773 Visión por Computador, Corporación Universitaria Minuto de Dios.