Saltar al contenido
← Programa de inteligencia artificial

Inteligencia artificial / Material de consulta

Red neuronal para imágenes

PyTorch · CPU, GPU opcional

Consulte las explicaciones, el código y las salidas de referencia en esta página.

Volver al aula →

Proyecto II · Una CNN pequeña con PyTorch

Overtone Education · Curso de inteligencia artificial

Objetivo

Entrenar y evaluar una red convolucional sobre digits (imágenes 8×8, scikit-learn). Corre en CPU; GPU opcional. Separar entrenamiento, validación y prueba. Los datos no incluyen aquí una partición por escritor: la prueba aleatoria es didáctica y no demuestra generalización entre personas.

Preparación

Ejecute de arriba abajo en Jupyter con Python 3.12 (entorno validado). Entorno externo al aula. Instale en un entorno virtual:

python -m pip install numpy scikit-learn torch ipykernel

Las versiones comprobadas se distribuyen en requirements-ia.txt. No necesita claves de API. Los ejemplos son educativos y no validan aplicaciones de impacto alto.

Pasos

1. Datos, semillas y tres particiones

Las intensidades originales van de 0 a 16; dividir por 16 es una transformación fija, no una estadística aprendida con la prueba.

Código de referencia

import copy
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
torch.manual_seed(7)
np.random.seed(7)
torch.set_num_threads(2)
datos = load_digits()
X = datos.images.astype("float32")[:,None,:,:]/16.0
y = datos.target.astype("int64")
X_dev,X_test,y_dev,y_test = train_test_split(X,y,test_size=.25,random_state=7,stratify=y)
X_train,X_valid,y_train,y_valid = train_test_split(X_dev,y_dev,test_size=.2,random_state=7,stratify=y_dev)
print({"train":len(y_train),"valid":len(y_valid),"test":len(y_test),"torch":torch.__version__})
train = DataLoader(TensorDataset(torch.from_numpy(X_train),torch.from_numpy(y_train)),batch_size=64,shuffle=True,generator=torch.Generator().manual_seed(7))

Salida comprobada

{'train': 1077, 'valid': 270, 'test': 450, 'torch': '2.14.1'}

2. Arquitectura y contrato

Entrada: lote × 1 × 8 × 8. Una convolución, activación y pooling producen 8 × 4 × 4 valores por imagen. CrossEntropyLoss recibe logits sin softmax previo.

Código de referencia

modelo = nn.Sequential(nn.Conv2d(1,8,3,padding=1),nn.ReLU(),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(8*4*4,32),nn.ReLU(),nn.Linear(32,10))
opt = torch.optim.Adam(modelo.parameters(),lr=.01)
perdida = nn.CrossEntropyLoss()
print("Parámetros:",sum(p.numel() for p in modelo.parameters()))
assert modelo(torch.zeros(2,1,8,8)).shape == (2,10)

Salida comprobada

Parámetros: 4538

3. Entrenar y seleccionar con validación

Seleccione el mejor estado usando validación. La prueba se consulta una vez al finalizar. Este ejemplo usa un máximo fijo de 10 épocas para mantener el cómputo pequeño.

Código de referencia

mejor_score = -1.0
mejor_estado = None
for epoca in range(10):
    modelo.train()
    suma = 0.0
    for xb,yb in train:
        opt.zero_grad()
        loss = perdida(modelo(xb),yb)
        loss.backward()
        opt.step()
        suma += float(loss.detach())*len(yb)
    modelo.eval()
    with torch.no_grad():
        score = float((modelo(torch.from_numpy(X_valid)).argmax(1)==torch.from_numpy(y_valid)).float().mean())
    if score>mejor_score:
        mejor_score = score
        mejor_estado = copy.deepcopy(modelo.state_dict())
    print(f"Época {epoca+1:02d} | pérdida train={suma/len(y_train):.4f} | exactitud valid={score:.3f}")

Salida comprobada

Época 01 | pérdida train=2.0789 | exactitud valid=0.715
Época 02 | pérdida train=0.9887 | exactitud valid=0.874
Época 03 | pérdida train=0.3992 | exactitud valid=0.896
Época 04 | pérdida train=0.2464 | exactitud valid=0.930
Época 05 | pérdida train=0.1913 | exactitud valid=0.926
Época 06 | pérdida train=0.1694 | exactitud valid=0.956
Época 07 | pérdida train=0.1351 | exactitud valid=0.952
Época 08 | pérdida train=0.1058 | exactitud valid=0.959

Salida comprobada

Época 09 | pérdida train=0.1191 | exactitud valid=0.970
Época 10 | pérdida train=0.0799 | exactitud valid=0.967

4. Evaluar el artefacto seleccionado

eval() cambia el comportamiento de capas que lo necesitan; no_grad() evita construir el grafo de derivadas durante evaluación.

Código de referencia

modelo.load_state_dict(mejor_estado)
modelo.eval()
with torch.no_grad():
    pred = modelo(torch.from_numpy(X_test)).argmax(1).numpy()
exactitud = float(np.mean(pred==y_test))
print("Exactitud de prueba:",round(exactitud,3))
print("Matriz de confusión (filas reales):")
print(confusion_matrix(y_test,pred))
indices_fallo = np.flatnonzero(pred!=y_test)
print("Primeros errores (índice, real, pred):",[(int(i),int(y_test[i]),int(pred[i])) for i in indices_fallo[:8]])

Salida comprobada

Exactitud de prueba: 0.953
Matriz de confusión (filas reales):
[[45  0  0  0  0  0  0  0  0  0]
 [ 0 43  0  0  2  0  1  0  0  0]
 [ 0  1 43  0  0  0  0  0  0  0]
 [ 0  0  2 40  0  2  0  0  2  0]
 [ 0  0  0  0 44  0  1  0  0  0]
 [ 0  0  0  0  1 44  0  0  0  1]
 [ 0  1  0  0  0  0 44  0  0  0]
 [ 0  0  0  0  1  0  0 44  0  0]
 [ 0  0  1  0  1  1  1  0 39  0]
 [ 0  0  0  0  0  2  0  0  0 43]]
Primeros errores (índice, real, pred): [(1, 5, 4), (8, 1, 4), (19, 7, 4), (23, 1, 4), (31, 3, 2), (54, 8, 6), (108, 3, 5), (124, 3, 8)]

Comprobaciones

Compruebe tamaños, salidas finitas y calidad mínima para detectar una implementación rota. No convierta esta condición en garantía de uso.

Código de referencia

assert len(y_train)+len(y_valid)+len(y_test)==1797
assert np.isfinite(exactitud) and exactitud>.8
assert pred.shape==y_test.shape
assert all(torch.isfinite(p).all() for p in modelo.parameters())
print("Comprobaciones aprobadas")

Salida comprobada

Comprobaciones aprobadas

Siguientes pasos

  1. Compare contra el SVC del aula usando exactamente las mismas particiones.
  2. Repita con otra semilla y mida tiempo y memoria.
  3. Para transferencia, utilice un backbone con licencia y preprocesamiento documentados. Congele primero el backbone; luego compare ajuste parcial.
  4. Use particiones por sujeto o fuente cuando exista esa dependencia.

Referencias: https://pytorch.org/tutorials/beginner/basics/intro.html y https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_digits.html