Ya puedes entrenar una CNN para reconocer una imagen. Ahora quieres cambiar su arquitectura con una razón concreta: ¿conviene apilar operaciones pequeñas, mirar varias escalas en paralelo o dejar una ruta que conserve la entrada? Compararás esas ideas reconociendo las mismas diez categorías de prendas de Fashion-MNIST.
Tiempo estimado de lectura: 65 minutos, más cerca de 10 minutos de ejecución en CPU.
Nivel: Básico. Tipo de capítulo: teórico-aplicado.
Herramientas: PyTorch, torchvision, NumPy, pandas, matplotlib y scikit-learn.
Al terminar podrás reconocer las ideas de VGG, Inception y ResNet, calcular el alcance de filtros apilados y distinguir concatenar de sumar. Podrás comparar redes llanas y residuales con la misma profundidad, los mismos parámetros y pesos iniciales idénticos, además de interpretar sus curvas y su coste.
Primero ubicarás las familias en una línea histórica. Después comprobarás sus operaciones con números pequeños y construirás versiones reducidas. Entrenarás seis configuraciones, elegirás con validación y abrirás prueba para informar sus resultados. La última lectura conectará esas piezas con las redes preentrenadas del capítulo siguiente.
TipLo que necesitas recordar
Concepto
En una línea
Dónde se explica
Tensor, forma, nn.Module y forward
Números organizados y definición del recorrido por la red
14.2 Arquitectura: decidir por dónde viaja la información
NotaArquitectura, profundidad y anchura
La arquitectura define las operaciones de una red y cómo se conectan. La profundidad cuenta etapas sucesivas; aquí indicaremos el número de convoluciones. La anchura expresa cuántos canales o unidades hay en una etapa.
Piensa en un taller que recibe una pieza y la transforma. Puedes añadir estaciones en serie, poner estaciones en paralelo o conservar una copia de la pieza mientras calculas una modificación. Las tres decisiones cambian el recorrido, aunque cada estación use herramientas conocidas.
La tabla ubica algunas familias y el problema de diseño al que respondieron. Los enlaces llevan a los trabajos originales. Las fechas corresponden a sus primeras publicaciones; VGG e Inception circularon en 2014 y ResNet en 2015 antes de sus publicaciones en conferencias.
Modernizar los bloques convolucionales y su entrenamiento
En el experimento usarás versiones pequeñas de tres ideas: bloques tipo VGG, ramas tipo Inception y conexiones residuales. La historia describe familias completas; las redes que vas a construir permiten observar sus mecanismos con una CPU.
14.3 Un conjunto de prendas para comparar las conexiones
Trabajarás con Fashion-MNIST, de Xiao, Rasul y Vollgraf (2017), licencia MIT. Sus imágenes tienen resolución nativa de 28 × 28 y diez categorías. Se cargan desde su distribución oficial con torchvision. Una prenda tiene contornos y regiones interiores que las redes tendrán que combinar para distinguirla.
Seleccionamos de forma estratificada 8000 de las 60000 imágenes de desarrollo: 6400 para entrenamiento y 1600 para validación. Las 10000 oficiales de prueba se reservan completas. Todas las redes reciben los mismos índices y píxeles en la escala de 0 a 1.
La primera celda fija una sola semilla y registra el entorno. El archivo documentado apoyo_cnn.py reúne entrenamiento, evaluación y gráficas; puedes desplegar su código al final.
from pathlib import Pathimport copyimport jsonimport platformimport timeimport numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport torchimport torchvisionfrom torch import nnfrom torch.nn import functional as Ffrom IPython.display import displayimport apoyo_cnn as apoyoSEMILLA =42np.random.seed(SEMILLA)torch.manual_seed(SEMILLA)torch.set_num_threads(2)dispositivo = torch.device("cpu")artefactos = Path("artefactos_pytorch")artefactos.mkdir(exist_ok=True)versiones = {"python": platform.python_version(), "torch": str(torch.__version__),"torchvision": torchvision.__version__, "numpy": np.__version__,"pandas": pd.__version__, "dispositivo": str(dispositivo), "hilos": 2,}print(versiones)
La salida python, torch, torchvision y dispositivo identifica las bibliotecas y la CPU de esta comparación. El resto del experimento comparte esa configuración y la semilla 42.
Carga las prendas y comprueba sus formas y cantidades. La galería muestra una imagen de entrenamiento por categoría.
La entrada de ajuste tiene forma (6400, 1, 28, 28). La salida confirma 6400 imágenes para aprender, 1600 para elegir y 10000 para informar el resultado final. La galería permite comparar contornos de prendas antes de estudiar las distintas conexiones que procesarán esos mismos píxeles.
14.4 VGG: mirar más lejos con filtros pequeños
NotaBloque y campo receptivo
Un bloque agrupa operaciones que se repiten en una arquitectura. El campo receptivo teórico de una salida es la región de la entrada que puede influir en ella a través de esas operaciones.
Con una convolución de 3 × 3, una salida combina tres posiciones por eje. Si una segunda convolución de 3 × 3 combina esas salidas, alcanza cinco posiciones de la imagen original. Es como revisar primero tres casillas vecinas y luego combinar tres revisiones que se superponen.
Dos filtros de tres posiciones alcanzan cinco posiciones de la entrada.
Con 64 canales tanto de entrada como de salida, una convolución de 5 × 5 con sesgo tiene (25 × 64 + 1) × 64 parámetros. Dos de 3 × 3 tienen 2 × (9 × 64 + 1) × 64. Comprueba las cantidades antes de decidir qué bloque usar.
Una convolución de 5 × 5 tiene 102464 parámetros; las dos de 3 × 3 suman 73856, incluidos los sesgos. Su alcance espacial teórico coincide en 5 × 5 bajo estas condiciones. Con una ReLU intermedia, las dos operaciones pequeñas introducen una transformación adicional además de ese cambio en cantidad de pesos.
Nuestra VGG pequeña repetirá dos convoluciones de 3 × 3 con ReLU y un pooling. Sus canales crecen de 8 a 16 y a 32, mientras los tamaños bajan de 28 a 14, a 7 y a 3. Flatten reúne 32 × 3 × 3 = 288 valores para una capa densa de 64 y la salida de diez logits. El pooling sobre siete posiciones conserva tres regiones completas de dos.
Tres bloques sucesivos de convoluciones pequeñas antes de la cabeza.
La celda define esa red y muestra sus parámetros. Los pesos se inicializan para capas con ReLU; la salida lineal usa Xavier, como se explica en optimización.
def inicializar(modelo):"""Recibe una red y devuelve el mismo modelo con He y salida Xavier."""for capa in modelo.modules():ifisinstance(capa, (nn.Conv2d, nn.Linear)): nn.init.kaiming_normal_(capa.weight, nonlinearity="relu") nn.init.zeros_(capa.bias) lineales = [capa for capa in modelo.modules() ifisinstance(capa, nn.Linear)] nn.init.xavier_uniform_(lineales[-1].weight)return modelodef crear_vgg():"""Devuelve tres bloques pequeños tipo VGG y su cabeza de clasificación.""" capas = [] entrada =1for salida in [8, 16, 32]: capas.extend([ nn.Conv2d(entrada, salida, 3, padding=1), nn.ReLU(), nn.Conv2d(salida, salida, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ]) entrada = salidareturn nn.Sequential(*capas, nn.Flatten(), nn.Linear(288, 64), nn.ReLU(), nn.Linear(64, 10))vgg = inicializar(crear_vgg())print("Parámetros VGG pequeña:", sum(p.numel() for p in vgg.parameters()))print("Forma de salida:", tuple(vgg(torch.zeros(2, 1, 28, 28)).shape))
Parámetros VGG pequeña: 37186
Forma de salida: (2, 10)
La VGG pequeña suma 37186 parámetros y convierte dos imágenes en una salida (2, 10). Las convoluciones y los pooling llevan las prendas hasta 32 mapas de 3 × 3; los 288 valores resultantes alimentan la capa densa. Puedes relacionar así el tamaño de la entrada de esa capa con el último bloque del diagrama.
14.5 Inception: trabajar con varias ventanas en paralelo
NotaRama y concatenación
Una rama recibe la misma entrada que otras rutas paralelas y aplica sus propias operaciones. Concatenar reúne sus salidas agregando canales. Conserva por separado los valores de cada rama.
Piensa en tres personas que observan la misma tela con ventanas de distinto tamaño. Una revisa cada posición y mezcla sus canales; otra mira una región de 3 × 3 y otra de 5 × 5. Reunir sus observaciones produce una pila de mapas más ancha para la siguiente etapa.
La convolución de 1 × 1 combina canales en cada posición. Si recibe dieciséis canales, cada salida calcula una suma de dieciséis valores ponderados y un sesgo, sin incluir posiciones vecinas. En diseños completos puede reducir canales antes de filtros de mayor tamaño.
Nuestro bloque tendrá tres ramas de 1 × 1, 3 × 3 y 5 × 5, con padding 0, 1 y 2. Cada una conserva alto y ancho. Si cada rama entrega ocho canales, concatenarlas produce veinticuatro.
Tres ramas paralelas se reúnen por el eje de canales.
La red pequeña utiliza dos bloques de estas ramas.
NotaPromedio global
Un promedio global reduce cada mapa a la media de sus posiciones: de cuarenta y ocho mapas obtienes cuarenta y ocho números. Es un resumen espacial que alimenta la cabeza sin aplanar todas las posiciones.
La celda comprueba las formas de las ramas y define la red que entrenaremos.
class Ramas(nn.Module):"""Combina tres convoluciones paralelas conservando sus canales separados."""def__init__(self, entrada, salida):super().__init__()self.ramas = nn.ModuleList([ nn.Conv2d(entrada, salida, kernel, padding=kernel //2)for kernel in [1, 3, 5] ])def forward(self, entrada): resultados = [F.relu(rama(entrada)) for rama inself.ramas]return torch.cat(resultados, dim=1)def crear_inception():"""Devuelve una red de dos bloques de ramas con promedio global."""return nn.Sequential( nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), Ramas(8, 8), nn.MaxPool2d(2), Ramas(24, 16), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(48, 64), nn.ReLU(), nn.Linear(64, 10), )ramas_control = Ramas(16, 8)print("Tres ramas de ocho canales:", tuple(ramas_control(torch.zeros(2, 16, 14, 14)).shape))inception = inicializar(crear_inception())print("Parámetros de la red de ramas:", sum(p.numel() for p in inception.parameters()))
Tres ramas de ocho canales: (2, 24, 14, 14)
Parámetros de la red de ramas: 19618
Las tres ramas producen la forma (2, 24, 14, 14): dos imágenes, veinticuatro canales y catorce posiciones por eje. El 24 sale de reunir tres grupos de ocho mapas. La red completa de ramas tiene 19618 parámetros, que se contrastarán con sus resultados después del entrenamiento.
14.6 ResNet: aprender una modificación de la entrada
NotaConexión residual e identidad
Una conexión residual suma la entrada x a una transformación F(x). La identidad es la ruta que conserva x sin cambiarla. En este capítulo el bloque residual calcula ReLU(F(x) + x) y el llano calcula ReLU(F(x)).
Si ya tienes un borrador útil, puedes aprender una corrección en lugar de escribirlo entero otra vez. Esa es la intuición del atajo: conservar una ruta para los valores recibidos y combinarla con una modificación aprendida. F(x) tendrá dos convoluciones de 3 × 3 con una ReLU entre ellas.
Para x = [1, 2] y F(x) = [0.5, -0.5], la ruta llana entrega [0.5, 0] después de ReLU. La residual suma primero y entrega [1.5, 1.5]. Conserva la entrada y añade su corrección por posición.
La ruta de identidad se suma a la transformación y después se aplica ReLU.
NotaProyección y suma compatible
Una proyección transforma la ruta de entrada para hacer compatibles sus dimensiones con F(x), por ejemplo con una convolución de 1 × 1. Para sumar posición a posición necesitas el mismo número de canales y el mismo tamaño espacial.
En las parejas del experimento los bloques conservarán ocho canales y el tamaño espacial. Así la identidad puede pasar directamente, sin parámetros nuevos. El pooling se aplicará después de los bloques tercero y sexto cuando existan, fuera de la suma.
Comprueba la suma numérica y una proyección de ocho a dieciséis canales. Después define el bloque llano y el residual usando las mismas dos convoluciones.
La salida llana es [0.5, 0.0], mientras la residual conserva y modifica la entrada para producir [1.5, 1.5]. La proyección cambia la forma a (2, 16, 14, 14): duplica los canales y mantiene el tamaño espacial. En las parejas siguientes los canales permanecen en ocho, por lo que la ruta de identidad puede sumarse directamente.
14.7 Preparar una comparación controlada
NotaAblación, degradación y dificultad de ajuste
Una ablación cambia una pieza para estudiar su efecto, manteniendo otras condiciones fijas. Aquí la pieza es sumar o no la identidad.
La degradación por profundidad describe un ajuste de entrenamiento peor al añadir capas en determinadas condiciones. Se distingue del sobreajuste, donde las imágenes conocidas mejoran mientras la generalización empeora.
Construirás una pareja de cuatro bloques y otra de ocho. Cada bloque tiene dos convoluciones y hay una convolución de entrada: son nueve y diecisiete convoluciones, respectivamente. Dentro de cada pareja, ambas redes tendrán el mismo número de parámetros y copias exactas de sus pesos iniciales.
Esas copias evitan comparar un atajo con una inicialización afortunada diferente. Los lotes también compartirán la semilla de su generador. Entre cuatro y ocho bloques sí cambia la cantidad de parámetros: la comparación que aísla la conexión se hace dentro de cada pareja.
La siguiente celda crea las redes y comprueba igualdad de pesos y parámetros antes de entrenar. La VGG y la red de ramas completan las seis configuraciones.
modelos = {"VGG pequeña": vgg, "Ramas Inception": inception}comprobaciones = []for cantidad in [4, 8]: llana = inicializar(Profunda(cantidad, False)) residual = copy.deepcopy(llana)for bloque in residual.bloques: bloque.residual =True cantidad_llana =sum(p.numel() for p in llana.parameters()) cantidad_residual =sum(p.numel() for p in residual.parameters()) iguales =all(torch.equal(llana.state_dict()[nombre], residual.state_dict()[nombre])for nombre in llana.state_dict())assert cantidad_llana == cantidad_residualassert iguales comprobaciones.append({"Bloques": cantidad, "Convoluciones": 1+2* cantidad,"Parámetros por red": cantidad_llana, "Pesos iguales": iguales}) modelos[f"Llana {cantidad}"] = llana modelos[f"Residual {cantidad}"] = residualdisplay(pd.DataFrame(comprobaciones))
Bloques
Convoluciones
Parámetros por red
Pesos iguales
0
4
9
4842
True
1
8
17
9514
True
La pareja de cuatro bloques tiene nueve convoluciones y 4842 parámetros por red. La de ocho tiene diecisiete y 9514. En ambas filas, Pesos iguales es True: copiar la red llana y habilitar el atajo conserva exactamente sus pesos iniciales. Esa comprobación hace visible qué condición se mantiene y cuál cambia.
14.8 Entrenar con una regla compartida
Las seis redes usarán las mismas 6400 prendas de entrenamiento, las mismas 1600 de validación, CE, Adam con tasa 0.001 y lotes de 128. El máximo inicial será 60 épocas y la paciencia 5. Cada modelo recuperará la versión de menor CE de validación y comprobará que su mejor época quede antes del máximo; el apoyo amplía ese máximo si hace falta.
Primero entrena las dos redes pequeñas de patrones VGG e Inception, y después las parejas de cuatro y ocho bloques. La tabla inicial resume ajuste y selección; aún no consulta prueba.
Modelo: VGG pequeña
Mejor época < máximo: 15 < 60 = True
Modelo: Ramas Inception
Mejor época < máximo: 45 < 60 = True
Modelo: Llana 4
Mejor época < máximo: 39 < 60 = True
Modelo: Residual 4
Mejor época < máximo: 33 < 60 = True
Modelo: Llana 8
Mejor época < máximo: 28 < 60 = True
Modelo: Residual 8
Mejor época < máximo: 33 < 60 = True
mejor_epoca
epocas_ejecutadas
maximo_final
perdida_validacion
segundos
parametros
VGG pequeña
15.0
20.0
60.0
0.359052
29.558750
37186.0
Ramas Inception
45.0
50.0
60.0
0.467256
60.518111
19618.0
Llana 4
39.0
44.0
60.0
0.496204
115.020774
4842.0
Residual 4
33.0
38.0
60.0
0.536384
110.868787
4842.0
Llana 8
28.0
33.0
60.0
0.402676
140.859458
9514.0
Residual 8
33.0
38.0
60.0
0.379235
138.075200
9514.0
Las seis comprobaciones quedaron antes del máximo 60: VGG eligió la época 15, las ramas Inception la 45, la llana de cuatro la 39, la residual de cuatro la 33, la llana de ocho la 28 y la residual de ocho la 33. Sus pérdidas mínimas de validación fueron 0.359052, 0.467256, 0.496204, 0.536384, 0.402676 y 0.379235, respectivamente. La tabla muestra que cada modelo agotó cinco épocas adicionales sin un nuevo mínimo.
14.9 Curvas: qué aprendió cada pareja
Las pérdidas de entrenamiento se miden sobre las imágenes utilizadas para ajustar pesos. Las de validación usan las prendas reservadas para elegir. Se calculan al terminar cada época, en modo evaluación y con los mismos pesos.
Las curvas siguientes colocan cada pareja en una fila. La izquierda muestra ajuste y la derecha validación. Para estudiar una dificultad de entrenamiento, comienza por el panel de ajuste; después mira qué ocurre al pasar a las imágenes reservadas.
Figura 14.2: Con y sin atajos a cuatro y ocho bloques
Llana 4 CE ajuste final: 0.501618 CE validación final: 0.50843
Residual 4 CE ajuste final: 0.541578 CE validación final: 0.540028
Llana 8 CE ajuste final: 0.340506 CE validación final: 0.444138
Residual 8 CE ajuste final: 0.303702 CE validación final: 0.38392
Con cuatro bloques, la pérdida de entrenamiento al cierre fue 0.501618 para la llana y 0.541578 para la residual. Sus mínimos de validación también favorecieron a la llana: 0.496204 frente a 0.536384. Con ocho bloques se invirtió ese orden de validación, con 0.402676 para la llana y 0.379235 para la residual. Las curvas permiten seguir cómo se acercan después de un inicio diferente.
La llana de ocho terminó con pérdida de ajuste 0.340506, por debajo del 0.501618 de la llana de cuatro. En este ensayo, la mayor profundidad vino acompañada de mejor ajuste. Los valores al cierre corresponden a cantidades distintas de épocas; compara también puntos de una misma época si quieres igualar el número de recorridos por los datos. Las versiones que se informarán en prueba se eligen por sus mínimos de validación.
14.10 Elegir y abrir prueba
Antes de consultar prueba, selecciona la red con menor CE de validación entre las seis configuraciones. Después calcula una vez por modelo sus predicciones sobre las 10000 prendas oficiales. Así puedes informar todos los resultados sin cambiar la elección.
La tabla muestra desempeño, parámetros y segundos por separado. Los tiempos incluyen las evaluaciones al cerrar épocas; miden el ajuste de este experimento, no el tiempo que tardaría atender una solicitud con una sola imagen.
La selección previa eligió VGG pequeña, con CE de validación 0.359052. En prueba obtuvo 85.97 %, frente a 82.05 % de las ramas Inception. La pareja de cuatro bloques dio 80.38 % para la llana y 79.90 % para la residual; la de ocho dio 83.41 % y 84.41 %. El sentido de la diferencia entre los atajos cambió según la profundidad en este conjunto y presupuesto.
Tip¿Entonces los atajos no ayudan?
Ayudan cuando la red es profunda. ResNet los propuso para redes de decenas y cientos de capas, en las que una red llana empieza a entrenar peor a medida que crece. Con 9 convoluciones, la red llana de cuatro bloques aprende sin dificultad y el atajo no aporta: su pérdida de entrenamiento al cierre es 0.501618 frente a 0.541578 de la residual. Con 17 convoluciones aparece la ventaja: la residual de ocho bloques cierra con 0.303702 frente a 0.340506 y gana en prueba, 84.41 % frente a 83.41 %. Lo esperable, según He y colaboradores (2015), es que la diferencia crezca con más bloques; repetir la pareja con 30 o 50 bloques es un buen ejercicio para tu computador.
La siguiente figura relaciona tamaño y exactitud de prueba. Cada símbolo tiene una etiqueta de modelo, por lo que puedes leer los puntos sin distinguir colores. Las configuraciones VGG e Inception tienen parámetros y operaciones distintos; utiliza esta tabla para describir compromisos de esas redes concretas.
figura, eje = plt.subplots(figsize=(9, 5))for indice, fila in tabla.iterrows(): marca = ["o", "s", "^", "D", "v", "P"][indice] eje.scatter(fila["parametros"], fila["Exactitud prueba"], marker=marca, s=65) eje.annotate(fila["Modelo"], (fila["parametros"], fila["Exactitud prueba"]), xytext=(5, 5), textcoords="offset points", fontsize=9)eje.set(xlabel="Parámetros totales", ylabel="Exactitud en prueba", title="Tamaño y resultado de las seis configuraciones")figura.tight_layout()plt.show()
Figura 14.3: Tamaño y exactitud de las configuraciones
VGG combina 37186 parámetros y 85.97 % de exactitud; las ramas Inception tienen 19618 y alcanzan 82.05 %. La residual de ocho utiliza 9514 parámetros y llega a 84.41 %, pero su ajuste tomó 138.08 segundos frente a 29.56 de VGG. Esos tiempos también incluyen distintas cantidades de épocas: 38 para esa residual y 20 para VGG. Tamaño, tiempo y aciertos describen aspectos diferentes de la configuración.
La matriz y la galería del modelo elegido reutilizan sus predicciones ya calculadas. El promedio macro del informe da igual peso a cada categoría.
La categoría Camisa tiene la menor sensibilidad, 0.5740: el modelo elegido reconoce 574 de sus 1000 ejemplos. La mayor confusión individual es camisa como abrigo, con 152 casos. La precisión de Abrigo es 0.7027 frente a su sensibilidad 0.8510, consistente con recibir predicciones de otras categorías. Examina los contornos de las prendas que aparecen en la galería al proponer una revisión posterior.
14.11 Un error al reunir ramas
Una convolución de 3 × 3 sin padding produce un mapa más pequeño que otra de 1 × 1. Vamos a intentar concatenarlas a propósito: el mensaje permite reconocer qué dimensión falta igualar.
RuntimeError: Sizes of tensors must match except in dimension 1. Expected size 28 but got size 26 for tensor number 1 in the list.
Concatenación corregida: (2, 16, 28, 28)
El mensaje dice Expected size 28 but got size 26: la rama de 1 × 1 conserva el alto y ancho, mientras la de 3 × 3 sin borde los reduce. Al añadir padding 1, la concatenación corregida produce (2, 16, 28, 28).
Lo que ves
Qué pasó
Cómo se arregla
Sizes of tensors must match
Las ramas conservan tamaños espaciales distintos
Usa padding que conserve alto y ancho antes de concatenar
Un canal final distinto del esperado
Se confundió sumar con concatenar
La suma conserva canales; la concatenación agrega los de cada rama
Error al sumar el atajo
x y F(x) tienen formas incompatibles
Conserva las dimensiones o proyecta la ruta de entrada
14.12 Las piezas que continúan en arquitecturas modernas
NotaConvolución separable y escalado compuesto
Una convolución separable en profundidad divide el trabajo en filtrar espacialmente cada canal y después mezclar los canales con una operación de 1 × 1. MobileNet utiliza esa separación para reducir cómputo.
El escalado compuesto coordina cambios de profundidad, anchura y resolución. EfficientNet estudia ese equilibrio en lugar de ampliar solo una dimensión.
Estos diseños combinan y reorganizan piezas. MobileNetV2 agrega una organización particular de bloques y atajos; ConvNeXt revisa el diseño de bloques convolucionales y su entrenamiento con criterios más recientes. Reconocer cada pieza permite leer una arquitectura sin aprender todos sus nombres de memoria.
14.13 Alcance de las conclusiones
Las redes pequeñas de este capítulo representan ideas; sus resultados no reproducen los modelos de los trabajos originales. Fashion-MNIST mantiene una resolución pequeña y un dominio de prendas en gris. Las diferencias medidas corresponden a este subconjunto, inicialización, tasa y presupuesto; una comparación para otra aplicación necesita imágenes representativas y repeticiones dentro del desarrollo.
El número de parámetros representa almacenamiento de pesos, mientras el coste de ejecutar depende también del número de posiciones, ramas y operaciones. Las curvas permiten observar dificultad de ajuste y sobreajuste, pero para explicar gradientes o afirmar un efecto estable conviene medirlos y repetir las parejas con otras semillas. El criterio usado aquí queda fijado por validación.
14.14 Ejercicios
Calcula el campo receptivo de tres convoluciones de 3 × 3 con paso uno, sin pooling entre ellas.
Si una rama entrega 8 canales y otra 16, ¿cuántos deja una concatenación? ¿Se pueden sumar directamente?
Para x = [2, -1] y F(x) = [-0.5, 3], calcula las salidas llana y residual después de ReLU.
Explica por qué copiar pesos iniciales mejora la comparación de cada pareja.
Propón una repetición que estudie la variabilidad de la conexión residual sin usar prueba para elegir la semilla.
Identifica en la tabla histórica una familia centrada en cómputo y otra en facilitar el ajuste de redes profundas.
TipOrientaciones
El alcance teórico es 7 × 7: cada convolución añade dos posiciones por eje bajo esas condiciones.
La concatenación entrega 24 canales si coinciden lote, alto y ancho. Para sumar posición a posición debes igualar los canales, por ejemplo mediante proyección.
La llana produce [0, 3]; la residual suma [1.5, 2], que ReLU conserva.
Ambas redes parten de los mismos números y reciben lotes en el mismo orden; la pieza modificada es el atajo.
Declara varias semillas antes de ejecutar, repite dentro del desarrollo e informa su dispersión; reserva una evaluación final independiente.
MobileNet separa operaciones para reducir cálculo; ResNet incorpora rutas de identidad para facilitar el aprendizaje de modificaciones.
14.15 Glosario de este capítulo
Término
Significado
Arquitectura, profundidad y anchura
Organización de conexiones, etapas sucesivas y canales de cada etapa
Bloque
Grupo de operaciones repetido en una red
Campo receptivo teórico
Región de entrada que puede influir en una salida
Rama y concatenación
Ruta paralela y unión de sus salidas por canales
Promedio global
Un promedio de las posiciones espaciales por canal
Conexión residual e identidad
Sumar una transformación a una ruta que conserva la entrada
Proyección
Transformación que hace compatibles las dimensiones del atajo
Ablación
Comparación que modifica una pieza manteniendo otras condiciones
Degradación por profundidad
Peor ajuste de entrenamiento al añadir capas en ciertas condiciones
Convolución separable
Separar filtrado espacial por canal y mezcla de canales
Escalado compuesto
Coordinar profundidad, anchura y resolución al ampliar una red
Apilar filtros pequeños, reunir ramas y sumar una identidad modifican el recorrido de una misma entrada. La comparación de cuatro bloques favoreció a la red llana por validación; con ocho favoreció a la residual. Entre las seis configuraciones, VGG pequeña fue elegida con CE 0.359052 y alcanzó 85.97 % en prueba. Puedes leer estas cifras junto con la comprobación de pesos iniciales y la tabla de coste para argumentar una decisión concreta.
En transferencia de aprendizaje y ajuste fino aprovecharás pesos que otra red ya aprendió. La pregunta cambiará de cómo conectar bloques a qué conservar y qué adaptar de esa red para reconocer flores.
"""Apoyo para los experimentos CNN de la Biblioteca BIT.Reúne carga oficial de datos, entrenamiento con validación y gráficos.La semilla y el dispositivo los decide el notebook. Ninguna función usa pruebapara ajustar pesos ni elegir una época. Los valores devueltos permiten escribirlas interpretaciones en el texto del capítulo, después de ejecutar."""from pathlib import Pathimport contextlibimport copyimport ioimport timeimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport torchfrom sklearn.metrics import classification_report, confusion_matrixfrom sklearn.model_selection import train_test_splitfrom torch.nn import functional as Ffrom torch.utils.data import DataLoader, TensorDatasetfrom torchvision.datasets import FashionMNIST, MNISTdef datos(nombre, cantidad, semilla):"""Carga MNIST o Fashion-MNIST y devuelve tensores, etiquetas e índices. Recibe el nombre, el tamaño del subconjunto de desarrollo y la semilla. Descarga los archivos oficiales con torchvision la primera vez y después los reutiliza desde la caché. Devuelve un diccionario con entrenamiento, validación (20 % del subconjunto), prueba oficial e índices de desarrollo. """ clase = FashionMNIST if nombre =="fashion"else MNIST raiz = Path.home() /".cache"/"bit-datasets"with contextlib.redirect_stdout(io.StringIO()):with contextlib.redirect_stderr(io.StringIO()): desarrollo = clase(raiz, train=True, download=True) prueba = clase(raiz, train=False, download=True) etiquetas = desarrollo.targets.numpy() indices = np.arange(len(etiquetas)) elegidos, _ = train_test_split( indices, train_size=cantidad, stratify=etiquetas, random_state=semilla ) ajuste, validacion = train_test_split( elegidos, test_size=0.2, stratify=etiquetas[elegidos], random_state=semilla )assertnotset(ajuste).intersection(validacion) imagenes = desarrollo.data.unsqueeze(1).float() /255 entrada_prueba = prueba.data.unsqueeze(1).float() /255return {"entrenamiento": TensorDataset(imagenes[ajuste], desarrollo.targets[ajuste]),"validacion": TensorDataset(imagenes[validacion], desarrollo.targets[validacion]),"prueba": TensorDataset(entrada_prueba, prueba.targets),"indices_ajuste": ajuste,"indices_validacion": validacion, }def cargador(conjunto, semilla, mezclar=False, lote=128):"""Recibe datos, semilla y opciones; devuelve lotes con generador explícito.""" generador = torch.Generator().manual_seed(semilla)return DataLoader( conjunto, batch_size=lote, shuffle=mezclar, generator=generador, num_workers=0 )@torch.inference_mode()def evaluar(modelo, conjunto, dispositivo, semilla):"""Recibe modelo y datos; devuelve CE media, exactitud, etiquetas y probabilidades.""" modelo.eval() total =0.0 salidas = [] etiquetas = []for entrada, respuesta in cargador(conjunto, semilla): logits = modelo(entrada.to(dispositivo)) total += F.cross_entropy( logits, respuesta.to(dispositivo), reduction="sum" ).item() salidas.append(logits.cpu()) etiquetas.append(respuesta) logits = torch.cat(salidas) reales = torch.cat(etiquetas)assert torch.isfinite(logits).all()return {"perdida": total /len(conjunto),"exactitud": (logits.argmax(1) == reales).float().mean().item(),"reales": reales.numpy(),"probabilidades": logits.softmax(1).numpy(), }def entrenar(modelo, ajuste, validacion, dispositivo, semilla, maximo=60, paciencia=5, tasa=0.001):"""Ajusta con Adam y restaura el mejor estado según CE de validación. Recibe modelo, dos conjuntos, dispositivo, semilla y presupuesto. Devuelve historial por época y resumen. Las curvas de ajuste también se miden en modo evaluación al cerrar la época, sobre datos usados para aprender. Si el mínimo queda en el borde se amplía el máximo, conservando el modelo y el optimizador. Si exige más de 1100 segundos o 240 épocas, se detiene con un error para revisar el diseño antes de abrir prueba. """ modelo.to(dispositivo) optimizador = torch.optim.Adam(modelo.parameters(), lr=tasa) lotes = cargador(ajuste, semilla, mezclar=True) historial = [] mejor =float("inf") estado =None espera =0 limite = maximo inicio = time.perf_counter()for epoca inrange(1, 241): modelo.train()for entrada, respuesta in lotes: entrada = entrada.to(dispositivo) respuesta = respuesta.to(dispositivo) optimizador.zero_grad(set_to_none=True) perdida = F.cross_entropy(modelo(entrada), respuesta)assert torch.isfinite(perdida) perdida.backward() optimizador.step() medidas_ajuste = evaluar(modelo, ajuste, dispositivo, semilla) medidas_val = evaluar(modelo, validacion, dispositivo, semilla) historial.append({"epoca": epoca, "perdida_ajuste": medidas_ajuste["perdida"],"perdida_validacion": medidas_val["perdida"],"exactitud_ajuste": medidas_ajuste["exactitud"],"exactitud_validacion": medidas_val["exactitud"], })if medidas_val["perdida"] < mejor: mejor = medidas_val["perdida"] mejor_epoca = epoca estado = copy.deepcopy(modelo.state_dict()) espera =0else: espera +=1if time.perf_counter() - inicio >1100:raiseRuntimeError("Revisar presupuesto CPU antes de abrir prueba")if espera >= paciencia:breakif epoca == limite:if mejor_epoca < limite:break limite *=2print("Máximo ampliado a", limite)assert mejor_epoca < limite, "Ampliar presupuesto antes de evaluar prueba" modelo.load_state_dict(estado) modelo.eval() resumen = {"mejor_epoca": mejor_epoca, "epocas_ejecutadas": epoca,"maximo_final": limite, "perdida_validacion": mejor,"segundos": time.perf_counter() - inicio,"parametros": sum(p.numel() for p in modelo.parameters()), }print("Mejor época < máximo:", mejor_epoca, "<", limite, "=", mejor_epoca < limite)return pd.DataFrame(historial), resumendef curvas(historial, titulo, mejor_epoca):"""Recibe historial, título y época elegida; dibuja CE y exactitud comparables.""" figura, ejes = plt.subplots(1, 2, figsize=(10, 3.6))for eje, medida, nombre inzip( ejes, ["perdida", "exactitud"], ["Entropía cruzada", "Exactitud"] ): eje.plot(historial.epoca, historial[medida +"_ajuste"], "-o", color="#1786B3", label="Entrenamiento (evaluación)", markersize=3) eje.plot(historial.epoca, historial[medida +"_validacion"], "--s", color="#EE5400", label="Validación", markersize=3) eje.axvline(mejor_epoca, color="#89878C", linestyle=":", label="Mejor época") eje.set(xlabel="Época", ylabel=nombre) eje.legend(fontsize=7) figura.suptitle(titulo) figura.tight_layout() plt.show()def diagnostico(resultado, nombres, imagenes):"""Recibe predicciones e imágenes; devuelve matriz y tabla de métricas por clase. Dibuja matriz con conteos y hasta ocho errores. Reutiliza las predicciones calculadas al abrir prueba, sin volver a evaluar el modelo. """ reales = resultado["reales"] probabilidades = resultado["probabilidades"] predichos = probabilidades.argmax(1) matriz = confusion_matrix(reales, predichos, labels=np.arange(len(nombres))) reporte = classification_report( reales, predichos, target_names=nombres, output_dict=True, zero_division=0 ) figura, eje = plt.subplots(figsize=(7, 6)) eje.imshow(matriz, cmap="Blues") eje.set_xticks(range(len(nombres)), nombres, rotation=65, ha="right", fontsize=8) eje.set_yticks(range(len(nombres)), nombres, fontsize=8) eje.set(xlabel="Categoría predicha", ylabel="Categoría real", title="Conteos en prueba")for i inrange(len(nombres)):for j inrange(len(nombres)): eje.text(j, i, str(matriz[i, j]), ha="center", va="center", fontsize=7, color="white"if matriz[i, j] > matriz.max() /2else"black") eje.grid(False) figura.tight_layout() plt.show() errores = np.flatnonzero(reales != predichos)[:8] figura, ejes = plt.subplots(2, 4, figsize=(10, 5))for eje, indice inzip(ejes.flat, errores): eje.imshow(imagenes[indice, 0], cmap="gray") eje.set_title(f"Real: {nombres[reales[indice]]}\nPredicción: {nombres[predichos[indice]]}", fontsize=8, )for eje in ejes.flat: eje.axis("off") figura.tight_layout() plt.show()return matriz, pd.DataFrame(reporte).T.loc[nombres + ["macro avg"]]