Intentamos entrenar un modelo de red neuronal muy profundo, un problema que podemos encontrar es el problema del gradiente que desaparece. Básicamente, se trata de un problema en el que la actualización del peso de un modelo durante el entrenamiento se ralentiza o incluso se detiene, lo que provoca que el modelo no mejore. Cuando una red es muy profunda, el cálculo del gradiente durante la retropropagación implica multiplicar muchos términos derivados mediante la regla de la cadena. Recuerde que si multiplicamos números pequeños (normalmente menos de 1) demasiadas veces, los números resultantes se volverán extremadamente pequeños. En el caso de las redes neuronales, estos números se utilizan como base para la actualización del peso. Entonces, si el gradiente es muy pequeño, entonces la actualización del peso será muy lenta, lo que hará que el entrenamiento también sea lento.
Para abordar este problema de gradiente que desaparece, podemos utilizar rutas de acceso directo para que los gradientes puedan fluir más fácilmente a través de una red profunda. Una de las arquitecturas más populares que intenta resolver esto es ResNet, donde implementa conexiones de salto que saltan varias capas en la red. Esta idea es adoptada por DenseNet, donde las conexiones de salto se implementan de manera mucho más agresiva, lo que lo hace mejor que ResNet en el manejo del problema del gradiente que desaparece. En este artículo me gustaría hablar sobre cómo funciona exactamente DenseNet y cómo implementar la arquitectura desde cero.
La arquitectura DenseNet
Bloque denso
DenseNet se propuso originalmente en un artículo titulado "Redes convolucionales densamente conectadas" escrito por Gao Huang et al. allá por 2016[1]. De hecho, la idea principal de DenseNet es resolver el problema del gradiente que desaparece. La razón por la que funciona mejor que ResNet es porque las rutas de acceso directo se ramifican desde una sola capa a todas las demás capas posteriores. Para ilustrar mejor esta idea, puede ver en la Figura 1 a continuación que el tensor de entrada x₀ se reenvía a H₁, H₂, H₃, H₄ y las capas de transición. Hacemos lo mismo con todas las capas dentro de este bloque, haciendo que todos los tensores estén conectados densamente, de ahí el nombre DenseNet. Con todas estas conexiones rápidas, la información puede fluir sin problemas entre capas. No solo eso, sino que este mecanismo también permite la reutilización de funciones, donde cada capa puede beneficiarse directamente de las funciones producidas por todas las capas anteriores.
En una CNN estándar, si tenemos L capas, también tendremos L conexiones. Suponiendo que la ilustración anterior es solo una CNN tradicional de 5 capas, básicamente solo tenemos 5 flechas rectas que salen de cada tensor. En DenseNet, si tenemos L capas, tendremos L(L+1)/2 conexiones. Entonces, en el caso anterior básicamente obtuvimos 5(5+1)/2 = 15 conexiones en total. Puedes verificar esto contando manualmente las flechas una por una: 5 flechas rojas, 4 flechas verdes, 3 flechas moradas, 2 flechas amarillas y 1 flecha marrón.
Otra diferencia clave entre ResNet y DenseNet es cómo combinan información de diferentes capas. En ResNet, combinamos información de dos tensores mediante suma de elementos, que se puede definir matemáticamente en la Figura 2 a continuación. En lugar de realizar una suma por elementos, DenseNet combina información mediante concatenación por canales como se expresa en la Figura 3. Con este mecanismo, los mapas de características producidos por todas las capas anteriores se concatenan con la salida de la capa actual antes de usarse finalmente como entrada de la capa siguiente.
Realizar una concatenación de canales como esta en realidad tiene un efecto secundario: la cantidad de mapas de características crece a medida que nos adentramos en la red. En el ejemplo que les mostré en la Figura 1, inicialmente tenemos un tensor de entrada de 6 canales. La capa H₁ procesa este tensor y produce un tensor de 4 canales. Luego, estos dos tensores se concatenan antes de reenviarlos a H₂. Básicamente, esto significa que la capa de H₂ acepta 10 canales. Siguiendo el mismo patrón, luego tendremos las capas H₃, H₄ y de transición para aceptar tensores de 14, 18 y 22 canales, respectivamente. En realidad, este es un ejemplo de DenseNet que utiliza el parámetro de tasa de crecimiento de 4, lo que significa que cada capa produce 4 nuevos mapas de características. Más adelante usaremos k para denotar este parámetro como se sugiere en el artículo original.
A pesar de tener conexiones tan complejas, DenseNet es en realidad mucho más eficiente en comparación con la CNN tradicional en términos de cantidad de parámetros. Hagamos un poco de matemáticas para demostrarlo. La estructura que se muestra en la Figura 1 consta de 4 capas de conversión (ignoremos la capa de transición por ahora). Para calcular cuántos parámetros tiene una capa convolucional, simplemente podemos calcular canales_de_entrada × altura_del_núcleo × ancho_del_núcleo × canales_de_salida. Suponiendo que todas estas convoluciones usan un kernel de 3 × 3, nuestras capas en la arquitectura DenseNet tendrían la siguiente cantidad de parámetros:
H₁ → 6×3×3×4 = 216 H₂ → 10×3×3×4 = 360 H₃ → 14×3×3×4 = 504 H₄ → 18×3×3×4 = 648
Sumando estos cuatro números, tendremos 1728 parámetros en total. Tenga en cuenta que este número no incluye el término de sesgo. Ahora, si intentamos crear exactamente la misma estructura con una CNN tradicional, necesitaremos la siguiente cantidad de parámetros para cada capa:
H₁ → 6×3×3×10 = 540 H₂ → 10×3×3×14 = 1260 H₃ → 14×3×3×18 = 2268 H₄ → 18×3×3×22 = 3564
En resumen, una CNN tradicional alcanza 7.632 parámetros, ¡eso es más de 4 veces más! Teniendo en cuenta este recuento de parámetros, podemos ver claramente que DenseNet es mucho más liviano que las CNN tradicionales. La razón por la que DenseNet puede ser tan eficiente es por el mecanismo de reutilización de características, donde en lugar de calcular todos los mapas de características desde cero, solo calcula k mapas de características y los concatena con los mapas de características existentes de las capas anteriores.
Capa de transición
La estructura que les mostré anteriormente es en realidad solo el componente principal del modelo DenseNet, al que se hace referencia como bloque denso. La figura 4 a continuación muestra cómo se ensamblan estos bloques de construcción, donde tres de ellos están conectados por las llamadas capas de transición. Cada capa de transición consta de una convolución seguida de una capa de agrupación. Este componente tiene dos responsabilidades principales: primero, reducir la dimensión espacial del tensor y segundo, reducir el número de canales. La reducción de la dimensión espacial es una práctica estándar al construir un modelo basado en CNN, donde los mapas de características más profundos normalmente deberían tener una dimensión más baja que los menos profundos. Mientras tanto, es necesario reducir la cantidad de canales porque podrían aumentar drásticamente debido al mecanismo de concatenación de canales realizado dentro de cada capa en el bloque denso.
Para comprender cómo la capa de transición reduce los canales, debemos observar el parámetro del factor de compresión. Este parámetro, al que los autores se refieren como θ (theta), debe tener un valor entre 0 y 1. Supongamos que establecemos θ en 0,2, entonces el número de canales que se reenviarán al siguiente bloque denso será solo el 20 % del número total de canales producidos por el bloque denso actual.
Toda la arquitectura DenseNet
Una vez que hemos entendido el bloque denso y la capa de transición, ahora podemos pasar a la arquitectura DenseNet completa que se muestra en la Figura 5 a continuación. Inicialmente acepta una imagen RGB de tamaño 224 × 224, que luego se procesa mediante una capa conv de 7 × 7 y una capa de agrupación máxima de 3 × 3. Tenga en cuenta que estas dos capas utilizan el paso de 2, lo que hace que la dimensión espacial se reduzca a 112 × 112 y 56 × 56, respectivamente. En este punto, el tensor está listo para pasar a través del primer bloque denso, que consta de 6 bloques de cuello de botella; hablaré más sobre este componente muy pronto. La salida resultante se reenviará a la primera capa de transición, seguida del segundo bloque denso, y así sucesivamente hasta que finalmente alcancemos la capa de agrupación promedio global. Finalmente, pasamos el tensor a la capa completamente conectada que es responsable de hacer predicciones de clases.
En realidad, hay varios detalles más que necesito explicar con respecto a la arquitectura anterior. Primero, no se menciona explícitamente la cantidad de mapas de características producidos en cada paso. Esto se debe esencialmente a que la arquitectura es adaptativa según los parámetros k y θ. La única capa con un número fijo es la primera capa de convolución (la de 7 × 7), que produce 64 mapas de características (no se muestran en la figura). En segundo lugar, también es importante tener en cuenta que cada capa de convolución que se muestra en la arquitectura sigue la secuencia BN-ReLU-conv-dropout, excepto la convolución 7×7 que no incluye la capa de abandono. En tercer lugar, los autores implementaron varias variantes de DenseNet, a las que se refieren como DenseNet (la básica), DenseNet-B (la variante que usa bloques de cuello de botella), DenseNet-C (la que utiliza el factor de compresión θ) y DenseNet-BC (la variante que emplea ambos). La arquitectura que se muestra en la Figura 5 es la variante DenseNet-B (o DenseNet-BC).
El llamado bloque de cuello de botella en sí es la pila de convoluciones de 1×1 y 3×3. La conversión 1×1 se utiliza para reducir el número de canales a 4k antes de reducirla aún más a k mediante la conversión 3×3 posterior. La razón de esto es que la convolución 3×3 es computacionalmente costosa en tensores con muchos canales. Entonces, para hacer el cálculo más rápido, primero debemos reducir los canales usando la conversión 1×1. Más adelante en la sección de codificación implementaremos esta variante de DenseNet-BC. Sin embargo, si desea implementar el DenseNet estándar (o DenseNet-C), simplemente puede omitir la conv 1 × 1 para que cada bloque denso solo comprenda convoluciones 3 × 3.
Algunos resultados experimentales
En el artículo se ve que los autores realizaron muchos experimentos comparando DenseNet con otros modelos. En esta sección les mostraré algunas cosas interesantes que descubrieron.
El primer resultado experimental que encontré interesante es que DenseNet en realidad tiene un rendimiento mucho mejor que ResNet. La Figura 6 anterior muestra que supera consistentemente a ResNet en todas las profundidades de la red. Al comparar variantes con precisión similar, DenseNet es en realidad mucho más eficiente. Echemos un vistazo más de cerca a la variante DenseNet-201. Aquí puede ver que el error de validación es casi el mismo que el de ResNet-101. A pesar de ser 2 veces más profundo (201 frente a 101 capas), es aproximadamente 2 veces más pequeño en términos de parámetros y FLOP (operaciones de punto flotante).
A continuación, los autores también realizaron un estudio de ablación con respecto al uso de la capa de cuello de botella y el factor de compresión. Podemos ver en la Figura 7 anterior que utilizar tanto la capa de cuello de botella dentro del bloque denso como realizar la reducción del recuento de canales en la capa de transición permite que el modelo alcance una mayor precisión (DenseNet-BC). Puede parecer un poco contradictorio ver que la reducción en el número de canales debido al factor de compresión mejora la precisión. De hecho, en el aprendizaje profundo, demasiadas funciones podrían perjudicar la precisión debido a la redundancia de información. Por lo tanto, reducir la cantidad de canales puede percibirse como un mecanismo de regularización que puede evitar que el modelo se sobreajuste, permitiéndole obtener una mayor precisión de validación.
DenseNet desde cero
Como hemos entendido la teoría subyacente detrás de DenseNet, ahora podemos implementar la arquitectura desde cero. Lo que debemos hacer primero es importar los módulos requeridos e inicializar las variables configurables. En el bloque de código 1 a continuación, k y θ que analizamos anteriormente se denotan como CRECIMIENTO y COMPRESIÓN, cuyos valores se establecen en 12 y 0,5, respectivamente. Estos dos valores son los valores predeterminados indicados en el documento, que definitivamente podemos cambiar si queremos. A continuación, aquí también inicializo la lista REPETICIONES para almacenar la cantidad de bloques de cuello de botella dentro de cada bloque denso.
# Bloque de código 1 importar antorcha importar antorcha.nn como nn CRECIMIENTO = 12 COMPRESIÓN = 0.5 REPETICIONES = [6, 12, 24, 16]
Implementación de cuellos de botella
Ahora echemos un vistazo a la clase Bottleneck a continuación para ver cómo implemento la pila de convoluciones 1×1 y 3×3. Anteriormente mencioné que cada capa de convolución sigue la estructura BN-ReLU-Conv-dropout, por lo que aquí necesitamos inicializar todas estas capas en el método __init__().
Las dos capas de convolución se inicializan como conv0 y conv1, cada una con sus correspondientes capas de normalización por lotes. No olvide configurar el parámetro out_channels de la capa conv0 en CRECIMIENTO*4 porque queremos que devuelva mapas de características de 4k (consulte la línea marcada con #(1)). Esta cantidad de mapas de características se reducirá aún más mediante la capa conv1 a k configurando out_channels en CRECIMIENTO (#(2)). Como se han inicializado todas las capas, ahora podemos definir el flujo en el método forward(). Solo tenga en cuenta que al final del proceso tenemos que concatenar el tensor resultante (out) con el original (x) para implementar la conexión de salto (#(3)).
# Bloque de código 2 clase Cuello de botella (nn.Module): def __init__(self, in_channels): super().__init__() self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) self.bn0 = nn.BatchNorm2d(num_features=in_channels) self.conv0 = nn.Conv2d(in_channels=in_channels, out_channels=CRECIMIENTO*4, #(1) kernel_size=1, padding=0, sesgo=False) self.bn1 = nn.BatchNorm2d(num_features=CRECIMIENTO*4) self.conv1 = nn.Conv2d(in_channels=CRECIMIENTO*4, out_channels=CRECIMIENTO, #(2) kernel_size=3, padding=1, sesgo=False) def forward(self, x): print(f'originalt: {x.size()}') out = self.dropout(self.conv0(self.relu(self.bn0(x)))) print(f'after conv0t: {out.size()}') out = self.dropout(self.conv1(self.relu(self.bn1(out)))) print(f'after conv1t: {out.size()}') concatenado = torch.cat((out, x), dim=1) #(3) print(f'after concatt: {concatenated.size()}') return concatenado
Para comprobar si nuestra clase Bottleneck funciona correctamente, ahora crearemos una que acepte 64 mapas de características y pasaremos un tensor ficticio a través de ella. La capa de cuello de botella que creo a continuación corresponde esencialmente al primer cuello de botella dentro del primer bloque denso (consulte la Figura 5 si no está seguro). Entonces, para simular el flujo real de la red, pasaremos un tensor de tamaño 64×56×56, que es esencialmente la forma producida por la capa maxpooling de 3×3.
# Cuello de botella del bloque de código 3 = Cuello de botella(in_channels=64) x = torch.randn(1, 64, 56, 56) x = cuello de botella(x)
Una vez que se ejecuta el código anterior, aparecerá el siguiente resultado en nuestra pantalla.
# Bloque de código 3 Salida original: torch.Size([1, 64, 56, 56]) después de conv0: torch.Size([1, 48, 56, 56]) #(1) después de conv1: torch.Size([1, 12, 56, 56]) #(2) después de concat: torch.Size([1, 76, 56, 56])
Aquí podemos ver que nuestra capa conv0 redujo con éxito los mapas de características de 64 a 48 (#(1)), donde 48 es el 4k (recuerde que nuestra k es 12). Este tensor de 48 canales luego es procesado por la capa conv1, lo que reduce aún más el número de mapas de características a k (#(2)). Este tensor de salida luego se concatena con el original, lo que da como resultado un tensor de 64+12 = 76 mapas de características. Y aquí es donde comienza el patrón. Más adelante en el bloque denso, si repetimos este cuello de botella varias veces, cada capa producirá:
segunda capa → 64+(2×12) = 88 mapas de características tercera capa → 64+(3×12) = 100 mapas de características cuarta capa → 64+(4×12) = 112 mapas de características y así sucesivamente…
Implementación de bloques densos
Ahora creemos la clase DenseBlock para almacenar la secuencia de instancias de Bottleneck. Mire el bloque de código 4 a continuación para ver cómo lo hago. La forma de hacerlo es bastante fácil, simplemente podemos inicializar una lista de módulos (#(1)) y luego agregar los bloques de cuello de botella uno por uno (#(3)). Tenga en cuenta que debemos realizar un seguimiento del número de canales de entrada de cada cuello de botella utilizando la variable current_in_channels (#(2)). Por último, en el método forward() podemos simplemente pasar el tensor secuencialmente.
# Codeblock 4 clase DenseBlock(nn.Module): def __init__(self, in_channels, repeticiones): super().__init__() self.bottlenecks = nn.ModuleList() #(1) para i en rango(repeticiones): current_in_channels = in_channels + i*CROWTH #(2) self.bottlenecks.append(Bottleneck(in_channels=current_in_channels)) #(3) def adelante(self, x): para i, cuello de botella en enumerate(self.bottlenecks): x = cuello de botella(x) print(f'después del cuello de botella #{i}t: {x.size()}') return x
Podemos probar el código anterior simulando el primer bloque denso de la red. Puede ver en la Figura 5 que contiene 6 bloques de cuello de botella, por lo que en el bloque de código 5 a continuación configuro el parámetro de repeticiones en ese número (#(1)). Podemos ver en el resultado resultante que el tensor de entrada, que inicialmente tiene la forma de 64×56×56, se transforma a 136×56×56. Los 136 mapas de características provienen de 64+(6×12), que sigue el patrón que les di anteriormente.
# Bloque de código 5 bloque_denso = Bloque_denso(en_canales=64, repeticiones=6) #(1) x = torch.randn(1, 64, 56, 56) x = bloque_denso(x) # Bloque de código 5 Salida después del cuello de botella #0: antorcha.Tamaño([1, 76, 56, 56]) después del cuello de botella #1: antorcha.Tamaño([1, 88, 56, 56]) después del cuello de botella #2: antorcha.Tamaño([1, 100, 56, 56]) después del cuello de botella #3: antorcha.Tamaño([1, 112, 56, 56]) después del cuello de botella #4: antorcha.Tamaño([1, 124, 56, 56]) después del cuello de botella #5: antorcha.Tamaño([1, 136, 56, 56])
Capa de transición
El siguiente componente que implementaremos es la capa de transición, que se muestra en el bloque de código 6 a continuación. De manera similar a las capas de convolución en los bloques de cuello de botella, aquí también usamos la estructura BN-ReLU-conv-dropout, pero esta tiene una capa de agrupación promedio adicional al final (#(1)). No olvide establecer el paso de esta capa de agrupación en 2 para reducir la dimensión espacial a la mitad.
# Transición de clase de bloque de código 6 (nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.bn = nn.BatchNorm2d(num_features=in_channels) self.relu = nn.ReLU() self.conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, padding=0, sesgo=False) self.dropout = nn.Dropout(p=0.2) self.pool = nn.AvgPool2d(kernel_size=2, stride=2) #(1) def forward(self, x): print(f'originalt: {x.size()}') out = self.pool(self.dropout(self.conv(self.relu(self.bn(x))))) print(f'después de la transición: {out.size()}') regresar
Ahora echemos un vistazo al código de prueba en el bloque de código 7 a continuación para ver cómo se transforma un tensor a medida que pasa a través de la red anterior. En este ejemplo estoy intentando simular la primera capa de transición, es decir, la que está justo después del primer bloque denso. Esta es esencialmente la razón por la que configuré esta capa para que acepte 136 canales. Anteriormente mencioné que esta capa se usa para reducir la dimensión del canal a través del parámetro θ, por lo que para implementarla simplemente podemos multiplicar el número de mapas de características de entrada con la variable COMPRESSION para el parámetro out_channels.
# Transición del bloque de código 7 = Transición(canales_entrada=136, canales_salida=int(136*COMPRESIÓN)) x = torch.randn(1, 136, 56, 56) x = transición(x)
Una vez que se ejecuta el código anterior, deberíamos obtener el siguiente resultado. Aquí puede ver que la dimensión espacial del tensor de entrada se reduce de 56 × 56 a 28 × 28, mientras que el número de canales también se reduce de 136 a 68. Esto esencialmente indica que nuestra implementación de la capa de transición es correcta.
# Bloque de código 7 Salida original: torch.Size([1, 136, 56, 56]) después de la transición: torch.Size([1, 68, 28, 28])
Toda la arquitectura DenseNet
Como hemos implementado con éxito los componentes principales del modelo DenseNet, ahora vamos a construir la arquitectura completa. Aquí separo los métodos __init__() y forward() en dos bloques de código porque son bastante largos. Sólo asegúrese de colocar Codeblock 8a y 8b dentro de la misma celda del cuaderno si desea ejecutarlo por su cuenta.
# Codeblock 8a clase DenseNet(nn.Module): def __init__(self): super().__init__() self.first_conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, #(1) stride=2, #(2) padding=3, #(3) sesgo=False) self.first_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) #(4) channel_count = 64 # Bloque denso #0 self.dense_block_0 = DenseBlock(in_channels=channel_count, repeticiones=REPEATS[0]) #(5) cuenta_canales = int(cuenta_canales+REPETENCIAS[0]*CRECIMIENTO) #(6) self.transition_0 = Transición(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) #(7) # Bloque denso #1 self.dense_block_1 = DenseBlock(in_channels=channel_count, repeticiones=REPEATS[1]) cuenta_canales = int(cuenta_canales+REPETENCIAS[1]*CRECIMIENTO) self.transition_1 = Transición(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) # # Bloque denso #2 self.dense_block_2 = DenseBlock(in_channels=channel_count, repeticiones=REPEATS[2]) cuenta_canales = int(cuenta_canales+REPETENCIAS[2]*CRECIMIENTO) self.transition_2 = Transición(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) # Bloque denso #3 self.dense_block_3 = DenseBlock(in_channels=channel_count, repeticiones=REPEATS[3]) cuenta_canales = int(cuenta_canales+REPETENCIAS[3]*CRECIMIENTO) self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1,1)) #(8) self.fc = nn.Linear(in_features=channel_count, out_features=1000) #(9)
Lo primero que hacemos en el método __init__() anterior es inicializar las capas first_conv y first_pool. Tenga en cuenta que estas dos capas no pertenecen al bloque denso ni a la capa de transición, por lo que debemos inicializarlas manualmente como instancias nn.Conv2d y nn.MaxPool2d. De hecho, estas dos capas iniciales son bastante singulares. La capa de convolución utiliza un núcleo muy grande de tamaño 7×7 (#(1)) con un paso de 2 (#(2)). Por lo tanto, no solo captura información de un área grande, sino que esta capa también realiza una reducción de resolución espacial en el lugar. Aquí también necesitamos establecer el relleno en 3 (#(3)) para compensar el núcleo grande y que la dimensión espacial no se reduzca demasiado. A continuación, la capa de agrupación es diferente de las de la capa de transición, donde utilizamos una agrupación máxima de 3×3 en lugar de una agrupación promedio de 2×2 (#(4)).
Una vez terminadas las dos primeras capas, lo que hacemos a continuación es inicializar los bloques densos y las capas de transición. La idea es bastante sencilla: necesitamos inicializar los bloques densos que constan de varios bloques de cuello de botella (cuyo número de cuellos de botella se pasa a través del parámetro de repeticiones (#(5))). Recuerde realizar un seguimiento del recuento de canales de cada paso (#(6,7)) para que podamos hacer coincidir la forma de entrada de la capa siguiente con la forma de salida de la anterior. Y luego básicamente hacemos exactamente lo mismo con los bloques densos restantes y las capas de transición.
Cuando llegamos al último bloque denso, ahora inicializamos la capa de agrupación promedio global (#(8)), que es responsable de tomar el valor promedio en la dimensión espacial, antes de inicializar finalmente el encabezado de clasificación (#(9)). Finalmente, como todas las capas se han inicializado, ahora podemos conectarlas todas dentro del método forward() a continuación.
# Bloque de código 8b def forward(self, x): print(f'originaltt: {x.size()}') x = self.first_conv(x) print(f'after first_convt: {x.size()}') x = self.first_pool(x) print(f'after first_poolt: {x.size()}') x = self.dense_block_0(x) print(f'after bloque_denso_0t: {x.size()}') x = self.transition_0(x) print(f'después de la transición_0t: {x.size()}') x = self.dense_block_1(x) print(f'después de la transición_1t: {x.size()}') x = self.transition_1(x) print(f'después de la transición_1t: {x.size()}') x = self.dense_block_2(x) print(f'después de densa_bloque_2t: {x.size()}') x = self.transition_2(x) print(f'después de transición_2t: {x.size()}') x = self.dense_block_3(x) print(f'después de densa_block_3t: {x.size()}') x = self.avgpool(x) print(f'después avgpooltt: {x.size()}') x = torch.flatten(x, start_dim=1) print(f'después de flattentt: {x.size()}') x = self.fc(x) print(f'después de fctt: {x.size()}') return x
Eso es básicamente toda la implementación de la arquitectura DenseNet. Podemos probar si funciona correctamente ejecutando Codeblock 9 a continuación. Aquí pasamos el tensor x a través de la red, en la que simula un lote de una única imagen RGB de 224 × 224.
# Bloque de código 9 densanet = DenseNet() x = torch.randn(1, 3, 224, 224) x = densanet(x)
Y a continuación se muestra cómo se ve el resultado. Aquí imprimo intencionalmente la forma del tensor después de cada paso para que puedas ver claramente cómo se transforma el tensor en toda la red. A pesar de tener tantas capas, esta es en realidad la variante DenseNet más pequeña, es decir, DenseNet-121. De hecho, puede hacer que el modelo sea aún más grande cambiando los valores en la lista REPETICIONES de acuerdo con el número de bloques de cuello de botella dentro de cada bloque denso que se muestra en la Figura 5.
# Bloque de código 9 Salida original: torch.Size([1, 3, 224, 224]) después de first_conv : torch.Size([1, 64, 112, 112]) después de first_pool : torch.Size([1, 64, 56, 56]) después del cuello de botella #0 : torch.Size([1, 76, 56, 56]) después del cuello de botella #1 : torch.Size([1, 88, 56, 56]) después del cuello de botella #2: torch.Size([1, 100, 56, 56]) después del cuello de botella #3: torch.Size([1, 112, 56, 56]) después del cuello de botella #4: torch.Size([1, 124, 56, 56]) después del cuello de botella #5: torch.Size([1, 136, 56, 56]) después de denso_bloque_0: antorcha.Tamaño([1, 136, 56, 56]) después de transición_0: antorcha.Tamaño([1, 68, 28, 28]) después del cuello de botella #0: antorcha.Tamaño([1, 80, 28, 28]) después del cuello de botella #1: antorcha.Tamaño([1, 92, 28, 28]) después del cuello de botella #2: antorcha.Tamaño([1, 104, 28, 28]) después del cuello de botella #3: antorcha.Tamaño([1, 116, 28, 28]) después del cuello de botella #4: antorcha.Tamaño([1, 128, 28, 28]) después del cuello de botella #5: antorcha.Tamaño([1, 140, 28, 28]) después del cuello de botella #6 : antorcha.Tamaño([1, 152, 28, 28]) después del cuello de botella #7 : antorcha.Tamaño([1, 164, 28, 28]) después del cuello de botella #8 : antorcha.Tamaño([1, 176, 28, 28]) después del cuello de botella #9 : antorcha.Tamaño([1, 188, 28, 28]) después del cuello de botella #10 : antorcha.Tamaño([1, 200, 28, 14, 14]) después del cuello de botella n.° 1: antorcha.Tamaño ([1, 130, 14, 14]) después del cuello de botella n.° 2: antorcha.Tamaño ([1, 142, 14, 14]) después del cuello de botella n.° 3: antorcha.Tamaño ([1, 154, 14, 14]) después del cuello de botella n.° 4: antorcha.Tamaño ([1, 166, 14, 14]) después del cuello de botella n.° 5 : antorcha.Tamaño([1, 178, 14, 14]) después del cuello de botella #6 : antorcha.Tamaño([1, 190, 14, 14]) después del cuello de botella #7 : antorcha.Tamaño([1, 202, 14, 14]) después del cuello de botella #8 : antorcha.Tamaño([1, 214, 14, 14]) después del cuello de botella #9 : antorcha.Tamaño([1, 226, 14, 14]) después del cuello de botella n.° 10: antorcha. Tamaño ([1, 238, 14, 14]) después del cuello de botella n.° 11: antorcha. Tamaño ([1, 250, 14, 14]) después del cuello de botella n.° 12: antorcha. 14, 14]) después del cuello de botella n.° 14: antorcha. Tamaño ([1, 286, 14, 14]) después del cuello de botella n. 14]) después del cuello de botella #18: antorcha.Tamaño([1, 334, 14, 14]) después del cuello de botella #19: antorcha.Tamaño([1, 346, 14, 14]) después del cuello de botella #20: antorcha.Tamaño([1, 358, 14, 14]) después del cuello de botella #21: antorcha.Tamaño([1, 370, 14, 14]) después cuello de botella #22: antorcha.Tamaño([1, 382, 14, 14]) después del cuello de botella #23: antorcha.Tamaño([1, 394, 14, 14]) después de bloque_denso_2: antorcha.Tamaño([1, 394, 14, 14]) después de transición_2: antorcha.Tamaño([1, 197, 7, 7]) después del cuello de botella #0: torch.Size([1, 209, 7, 7]) después del cuello de botella #1: torch.Size([1, 221, 7, 7]) después del cuello de botella #2: torch.Size([1, 233, 7, 7]) después del cuello de botella #3: torch.Size([1, 245, 7, 7]) después del cuello de botella #4: torch.Size([1, 257, 7, 7]) después del cuello de botella #5: antorcha.Tamaño([1, 269, 7, 7]) después del cuello de botella #6: antorcha.Tamaño([1, 281, 7, 7]) después del cuello de botella #7: antorcha.Tamaño([1, 293, 7, 7]) después del cuello de botella #8: antorcha.Tamaño([1, 305, 7, 7]) después del cuello de botella #9: antorcha.Tamaño([1, 317, 7, 7]) después del cuello de botella #10: antorcha.Tamaño([1, 329, 7, 7]) después del cuello de botella #11: antorcha.Tamaño([1, 341, 7, 7]) después del cuello de botella #12: antorcha.Tamaño([1, 353, 7, 7]) después del cuello de botella #13: antorcha.Tamaño([1, 365, 7, 7]) después cuello de botella #14: antorcha.Tamaño ([1, 377, 7, 7]) después del cuello de botella #15: antorcha. 389]) después de fc: antorcha.Tamaño([1, 1000])
Final
Creo que eso es prácticamente todo lo relacionado con la teoría y la implementación del modelo DenseNet. También puedes encontrar todos los códigos anteriores en mi repositorio de GitHub.[2]. ¡Nos vemos en mi próximo artículo!
Referencias
[1]Gao Huang et al. Redes convolucionales densamente conectadas. Arxiv. https://arxiv.org/abs/1608.06993 [Consultado el 18 de septiembre de 2025].
[2]MuhammadArdiPutra. Red densa. GitHub. https://github.com/MuhammadArdiPutra/medium_articles/blob/main/DenseNet.ipynb [Consultado el 18 de septiembre de 2025].