Tutorial en papel de FPN: Aprovechamiento de la pirámide interna

Estaba hablando de YOLOv3[1]. Uno de los factores que hace que esta versión de YOLO sea mejor que sus predecesoras es su capacidad para detectar objetos pequeños gracias a su cuello tipo FPN adoptado por el modelo. Desafortunadamente, mi explicación sobre FPN en ese artículo no fue del todo exhaustiva ya que me estaba centrando más en YOLOv3. Por lo tanto, en este artículo decidí escribir específicamente sobre FPN desde su artículo original titulado "Redes piramidales de funciones para la detección de objetos".[2]para que pueda comprender mejor qué es realmente y cómo funciona. No solo eso, aquí también demostraré cómo implementar FPN desde cero y cómo conectarlo con una red troncal de CNN y un cabezal RPN.

Columna vertebral, cuello y cabeza

Antes de entrar en FPN, primero debemos saber que la estructura de un modelo de detección de objetos es diferente de la del modelo de clasificación, en el que la principal diferencia radica en la última capa. En un modelo de clasificación típico, la última capa comprende una serie de neuronas donde cada una de ellas corresponde a cada clase disponible en el conjunto de datos. O, en el caso de la clasificación binaria, la capa de salida solo consta de una única neurona, que es responsable de predecir si una muestra pertenece a la clase 0 o 1. Este tipo de capa de salida en realidad no es adecuada para tareas de detección ya que también requiere neuronas dedicadas a predecir la ubicación y el tamaño de un objeto además de su clase.

Entonces, para que un modelo pueda predecir la ubicación y el tamaño de un objeto, necesitamos reemplazar la capa de salida, es decir, el cabezal de clasificación, con el llamado cabezal de detección. Las capas restantes (todo excepto la cabeza) se denominan comúnmente columna vertebral. Algunos modelos que utilizan esta estructura son YOLOv1 y YOLOv2, donde utilizan una pila de capas convolucionales como columna vertebral y un cabezal específico para predecir la ubicación y el tamaño del objeto dentro de una imagen, así como su clase.

Los modelos de detección de objetos más antiguos, como YOLOv1 y YOLOv2 mencionados anteriormente, solo constan de una columna vertebral y una cabeza. Con el paso del tiempo, los investigadores descubrieron que esta estructura aún no es del todo óptima, por lo que finalmente se les ocurrió una idea añadiendo un nuevo componente llamado cuello. Como sugiere el nombre, esto es esencialmente algo que colocamos entre la columna vertebral y la cabeza. Y FPN, del que hablaremos en este artículo, es uno de los primeros cuellos propuestos para los modelos de detección de objetos. Mire la Figura 1 a continuación para ver la vista arquitectónica de alto nivel de los modelos de detección de objetos más antiguos y modernos.

Figura 1. La arquitectura de un modelo de detección de objetos en general.[3].

La columna vertebral de un modelo es la principal responsable de realizar la extracción de características, mientras que el cuello es útil para mejorar la calidad de las características y la cabeza es para hacer predicciones. Con base en esta noción, podemos decir que al aplicar FPN, una red puede potencialmente lograr una mayor precisión gracias al mecanismo de mejora de funciones realizado por el cuello.

La evolución del mecanismo de detección multiescala

Anteriormente mencioné que usar columna vertebral y cabezal de detección sin cuello no es del todo óptimo. Esto se refiere especialmente a su capacidad para detectar objetos pequeños. Ahora echemos un vistazo a la Figura 2 a continuación. Las dos primeras versiones de YOLO que mencioné anteriormente utilizan la estructura de la imagen (b), donde el cuadro delimitador y las predicciones de clase de objeto se realizan únicamente sobre el mapa de características producido por la capa más profunda de la columna vertebral. Este método es válido, pero eficaz sólo para objetos grandes. La razón es bastante sencilla: a medida que una imagen se adentra más en una red, la dimensión espacial se reduce y, lo que es más importante, la información de píxeles contenida en los mapas de características más profundos se convierte en una representación de varios píxeles vecinos en los menos profundos, lo que hace que la información espacial se mezcle. Al hacer esto, los mapas de características de capas más profundas obtuvieron un gran campo receptivo, lo que permitió detectar y reconocer fácilmente objetos grandes. Sin embargo, la degradación de la información espacial a medida que nos adentramos nos impide detectar objetos pequeños con precisión porque necesitamos una ubicación detallada de los píxeles para predecir las coordenadas exactas de los objetos.

Además, el tamaño del campo receptivo de un mapa de características se correlaciona positivamente con la cantidad de información semántica que contiene. En la siguiente figura, un mapa de características de alta información semántica se indica con un contorno azul grueso. Esta es esencialmente la razón por la cual el mapa de características más profundo en (b) tiene el contorno más grueso.

Figura 2. Comparación de diferentes arquitecturas piramidales de características[2].

El enfoque más sencillo para permitir que una red detecte simultáneamente objetos grandes y pequeños es mediante el uso de una pirámide de imágenes destacada (a). Este método puede lograr una alta precisión porque podemos hacer predicciones a partir de diferentes resoluciones de imagen. Básicamente, lo que se hace aquí es cambiar la escala de nuestra imagen de entrada a múltiples escalas, realizar la extracción de características de forma independiente en cada escala y hacer predicciones en los mapas de características resultantes. El mapa de características más pequeño es responsable de detectar objetos grandes, mientras que el más grande está especializado para detectar objetos pequeños gracias a su información espacial detallada. Sin embargo, este método es costoso desde el punto de vista computacional ya que necesitamos procesar múltiples imágenes sin procesar de diferentes escalas a la vez.

Los autores de SSD (Detector de caja múltiple de disparo único) propusieron otra solución, que en la Figura 2 anterior se conoce como jerarquía de características piramidal (c). Entonces, en lugar de alimentar la red con la misma imagen de diferentes tamaños, los autores de SSD intentaron usar solo la imagen más grande y utilizar la estructura piramidal interna de la columna vertebral de CNN para hacer predicciones de diferentes escalas. Este enfoque permite que el sistema sea computacionalmente más factible que la opción (a). Sin embargo, aquí tenemos una compensación como (b), donde el mapa de características de la capa más profunda contiene una gran cantidad de información semántica pero tiene información espacial mínima, mientras que el mapa de características de la capa menos profunda tiene mucha información espacial pero no tiene tanta información semántica. Es importante señalar que una información espacial detallada puede no ser muy importante para objetos grandes, ya que sólo podemos aproximarnos a la forma general de ese objeto. Sin embargo, tanto la información espacial como la semántica son necesarias para detectar objetos pequeños porque no solo las coordenadas detalladas, sino que el modelo también necesita comprender qué hay realmente dentro del cuadro delimitador. Entonces, si bien es cierto que el método (c) es capaz de detectar objetos tanto grandes como pequeños, su capacidad para detectar estos últimos aún no es óptima.

Y aquí es donde FPN surge como solución. Si observamos la imagen (d) en la Figura 2, podemos ver que las predicciones se realizan sobre los mapas de características correspondientes, todos los cuales son semánticamente ricos. Básicamente, esto permite detectar con precisión objetos de diferentes escalas, incluidos los más pequeños. Hablaremos sobre los detalles de cómo FPN enriquece los mapas de características en la sección siguiente.

Cómo funciona FPN

La idea de FPN es inyectar información de los mapas de características más profundos en los menos profundos y, al hacerlo, tendremos mapas de características menos profundos que no solo contendrán alta información espacial sino también alta información semántica proveniente de la parte más profunda de la red. En teoría, esto debería dar como resultado una mejor precisión de detección en objetos pequeños, ya que los mapas de características grandes ahora están enriquecidos con mucha información semántica. Para lograrlo, introducen el llamado camino de arriba hacia abajo y conexiones laterales. Puede ver la arquitectura FPN completa en la Figura 3 a continuación, que es esencialmente la versión detallada de la de la Figura 2 (d).

Figura 3. La arquitectura FPN detallada[3].

Los autores de este artículo decidieron utilizar ResNet-50 y ResNet-101 como columna vertebral. Supongamos que usáramos el primero, luego tendríamos las capas conv2, conv3, conv4 y conv5 repetidas 3, 4, 6 y 3 veces, respectivamente, como lo sugieren los detalles arquitectónicos de ResNet en la Figura 4. C2, C3, C4 y C5, que son los tensores producidos por la última capa de la etapa correspondiente, se transferirán a la ruta de arriba hacia abajo a través de conexiones laterales, es decir, las flechas que salen de la columna vertebral.

Figura 4. La arquitectura ResNet [3, 4].

La vía de arriba hacia abajo se utiliza para transferir información semántica desde capas más profundas, mientras que las conexiones laterales se utilizan para preservar la información espacial. Agregamos los dos realizando una suma por elementos, cuyo proceso detallado se muestra en la Figura 5 a continuación. Para los tensores que provienen de la columna vertebral (C), primero debemos aplicarles conv 1 × 1. Esta capa de convolución es responsable de ajustar el número de canales para que coincida con el tensor que viene de la ruta de arriba hacia abajo. El tensor de la ruta de arriba hacia abajo (M+1) se somete a un muestreo ascendente 2 veces del vecino más cercano. Estos procesos se realizan esencialmente porque necesitamos que ambos tensores tengan exactamente la misma dimensión para que se pueda realizar la suma de elementos. A medida que se realiza la suma, el tensor resultante ahora se denomina M. Este tensor tiene algún efecto de alias debido al proceso de muestreo superior que hicimos anteriormente, por lo que necesitamos aplicar una convolución de 3 × 3 para reducir ese efecto. Finalmente, obtuvimos el tensor P, que está listo para ser enviado al cabezal de detección.

Figura 5. Cómo se agregan los mapas de características de la conexión lateral (C) y la ruta de arriba hacia abajo (M+1)[3].

Tenga en cuenta que todos los procesos descritos en la Figura 5 anterior solo se aplican a M2, M3 y M4. Calcular M5 es en realidad mucho más simple (ver Figura 6), donde lo único que debemos hacer es ajustar el número de canales usando 1×1 conv para hacerlo uniforme con los tensores en las otras conexiones laterales. El tensor M5 en sí no necesita procesarse más con conv 3×3 porque no hay nada que suavizar debido a la ausencia de un mecanismo de sobremuestreo. Entonces, básicamente podemos decir que P5 es exactamente el mismo tensor que M5.

Figura 6. La forma de calcular M5 y P5 es ligeramente diferente a la de M2-M4 y P2-P4.[3].

Y bueno, creo que eso es todo sobre la teoría detrás de FPN. En la siguiente sección, lo llevaré a la vista de nivel inferior de la arquitectura implementándola desde cero con PyTorch.

FPN desde cero

Columna vertebral de CNN

Entonces, como se ve en el bloque de código 1 a continuación, lo primero que debemos hacer en el código es importar los módulos necesarios.

# Bloque de código 1 importar antorcha importar antorcha.nn como nn

Dado que el enfoque de este artículo es FPN, aquí usaré un modelo ficticio para la columna vertebral en lugar de usar la ResNet real para simplificar las cosas. Pero aún así, las capas en el código se nombran de acuerdo con las Figuras 3 y 4: conv1, conv2, conv3, conv4 y conv5 como se muestra en el bloque de código 2 a continuación. La dimensión del tensor de salida de cada etapa también se establece de acuerdo con la arquitectura ResNet original. Entonces, aunque esta columna vertebral es simplemente un modelo simple basado en CNN, puedes considerarlo como una ResNet normal.

A continuación, lo que hacemos dentro del método forward() es conectar todas las capas. Si observa más de cerca el código, notará que cada capa de convolución va seguida de una función de activación ReLU y una capa de maxpooling. La capa maxpooling en sí está configurada para tener un paso de 2, lo que efectivamente reduce a la mitad la dimensión espacial del mapa de características. Al repetir las capas de maxpooling varias veces, nuestro mapa de características se hará más pequeño a medida que nos adentremos en la red. Básicamente, esto crea una estructura piramidal dentro de la columna vertebral de CNN que FPN aprovecha para lograr una alta precisión de detección en diferentes escalas de objetos. En CNN, reducir la dimensión espacial como esta es una práctica estándar para reducir la complejidad computacional y compensar el aumento del número de canales.

Aún dentro del método forward(), no olvides clonar el tensor principal x como se muestra en las líneas marcadas con #(1), #(2) y #(3). Los tensores copiados, que se denominan c2, c3 y c4, serán los valores de retorno de la clase CNN junto con el mapa de características del flujo principal (c5) (#(4)).

# Bloque de código 2 clase CNN(nn.Module): def __init__(self): super().__init__() self.relu = nn.ReLU() self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(in_channels=64, out_channels=256, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1) self.conv4 = nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, padding=1) self.conv5 = nn.Conv2d(in_channels=1024, out_channels=2048, kernel_size=3, padding=1) def forward(self, x): print(f'originalt: {x.size()}n') x = self.relu(self.conv1(x)) print(f'after conv1t: {x.size()}') x = self.maxpool(x) print(f'después de maxpoolt: {x.size()}n') x = self.relu(self.conv2(x)) print(f'después de conv2t: {x.size()}') x = self.maxpool(x) print(f'después de maxpoolt: {x.size()}n') c2 = x.clone() #(1) x = self.relu(self.conv3(x)) print(f'después de conv3t: {x.size()}') x = self.maxpool(x) print(f'después de maxpoolt: {x.size()}n') c3 = x.clone() #(2) x = self.relu(self.conv4(x)) print(f'después de conv4t: {x.size()}') x = self.maxpool(x) print(f'después de maxpoolt: {x.size()}n') c4 = x.clone() #(3) x = self.relu(self.conv5(x)) print(f'después de conv5t: {x.size()}') c5 = self.maxpool(x) print(f'después de maxpoolt: {c5.size()}n') return c2, c3, c4, c5 #(4)

Una vez finalizada la clase CNN, intentaremos pasar una imagen RGB ficticia de tamaño 224 × 224 a través de la red. Esta dimensión del tensor se elige en función de la forma de entrada del ResNet original.

# Bloque de código 3 cnn = CNN() x = torch.randn(1, 3, 224, 224) out_cnn = cnn(x)

Y a continuación se muestra cómo se ve el resultado. Aquí podemos ver que el número de canales después de cada capa conv coincide exactamente con la estructura ResNet dada en la Figura 4. No solo eso, también se ve que la dimensión espacial de nuestro tensor ficticio se redujo exitosamente a la mitad después de cada etapa gracias a las capas de maxpooling. Básicamente, esto indica que nuestro modelo CNN simple realmente imita la estructura general de un modelo ResNet.

# Bloque de código 3 Salida original: torch.Size([1, 3, 224, 224]) después de conv1: torch.Size([1, 64, 224, 224]) después de maxpool: torch.Size([1, 64, 112, 112]) después de conv2: torch.Size([1, 256, 112, 112]) después de maxpool: torch.Size([1, 256, 56, 56]) después de conv3: torch.Size([1, 512, 56, 56]) after maxpool: torch.Size([1, 512, 28, 28]) after conv4: torch.Size([1, 1024, 28, 28]) after maxpool: torch.Size([1, 1024, 14, 14]) después de conv5: antorcha.Tamaño([1, 2048, 14, 14]) después de maxpool: antorcha.Tamaño([1, 2048, 7, 7])

También podemos comprobar cómo se ven los tensores devueltos ejecutando el siguiente código. Puede ver en el resultado resultante que el tensor c2 tiene la forma de 256 × 56 × 56, c3 tiene la forma 512 × 28 × 28, y así sucesivamente. Por cierto, puede ignorar el número 1 en el eje 0, ya que solo indica la cantidad de muestras que pasamos dentro de un solo lote.

# Bloque de código 4 c2, c3, c4, c5 = out_cnn print(c2.shape) print(c3.shape) print(c4.shape) print(c5.shape) # Bloque de código 4 Salida torch.Size([1, 256, 56, 56]) torch.Size([1, 512, 28, 28]) torch.Size([1, 1024, 14, 14]) antorcha.Tamaño([1, 2048, 7, 7])

Cuello FPN

Una vez completada la columna vertebral de CNN, pasemos al cuello de FPN. En el bloque de código 5 a continuación, primero inicializamos la capa de muestra superior (#(1)) que usaremos cada vez que queramos duplicar la dimensión espacial del tensor M. Aquí configuro el parámetro de modo al más cercano como se sugiere en el artículo, que en realidad es un método de interpolación muy simple, lo que permite que el proceso sea rápido. Eche un vistazo a la Figura 7 para ver cómo se ve una interpolación del vecino más cercano.

# Bloque de código 5 clase FPN(nn.Module): def __init__(self): super().__init__() self.upsample = nn.Upsample(scale_factor=2, mode='nearest') #(1) self.lateral_c5 = nn.Conv2d(in_channels=2048, out_channels=256, kernel_size=1) self.lateral_c4 = nn.Conv2d(canales_de_entrada=1024, canales_de_salida=256, tamaño_del_kernel=1) self.lateral_c3 = nn.Conv2d(canales_de_entrada=512, canales_de_salida=256, tamaño_del_kernel=1) self.lateral_c2 = nn.Conv2d(canales_de_entrada=256, canales_de_salida=256, tamaño_del_kernel=1) self.smooth_m4 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) self.smooth_m3 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) self.smooth_m2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) def forward(self, c2, c3, c4, c5): m5 = self.lateral_c5(c5) p5 = m5 m4 = self.upsample(m5) + self.lateral_c4(c4) p4 = self.smooth_m4(m4) m3 = self.upsample(m4) + self.lateral_c3(c3) p3 = self.smooth_m3(m3) m2 = self.upsample(m3) + self.lateral_c2(c2) p2 = self.smooth_m2(m2) devolver p2, p3, p4, p5

Figura 7. Un ejemplo de un proceso de muestreo ascendente 2x con método de interpolación del vecino más cercano[3].

Si regresa al bloque de código 4, verá que los tensores c{5,4,3,2} devueltos por la red troncal tienen un número diferente de canales. Esta es básicamente la razón por la que inicializamos las capas lateral_c{5,4,3,2} para procesar estos tensores de modo que los recuentos de canales resultantes sean uniformes. Según el artículo, necesitamos configurar estas capas de convolución para producir 256 canales de salida, razón por la cual usamos ese número para el parámetro out_channels.

A continuación, según la Figura 7, puede imaginar cuán pixelados están los mapas de características resultantes después de haber sido muestreados. Por lo tanto, necesitamos procesar más los tensores m{4,3,2} con las capas conv de 3 × 3 a las que me refiero como smooth_m{4,3,2} . Como todas las capas han sido inicializadas, lo que debemos hacer a continuación es ensamblarlas en el método forward() de acuerdo con la estructura que les mostré anteriormente en la Figura 3.

Además de esto, el documento también menciona que no necesitamos implementar ninguna no linealidad dentro de FPN, razón por la cual todas las capas de convolución en la clase FPN anterior no se siguen con ReLU. Ahora, en el bloque de código 6 a continuación, intento pasar los tensores C que obtuvimos anteriormente a través del cuello FPN que acabamos de crear. Podemos ver en el resultado que los tensores resultantes tienen diferentes resoluciones espaciales. Posteriormente, el tensor p2 (el que tiene una dimensión de 56 × 56) se enviará a un cabezal de detección para detectar objetos pequeños, mientras que p5 (el tensor de 7 × 7) será responsable de los objetos grandes.

# Bloque de código 6 fpn = FPN() out_fpn = fpn(c2, c3, c4, c5) p2, p3, p4, p5 = out_fpn print(p2.shape) print(p3.shape) print(p4.shape) print(p5.shape) # Codeblock 6 Salida torch.Size([1, 256, 56, 56]) torch.Size([1, 256, 28, 28]) antorcha.Tamaño([1, 256, 14, 14]) antorcha.Tamaño([1, 256, 7, 7])

Aquí ya hemos completado la parte FPN. Recuerde que FPN es solo el cuello de un modelo de detección, lo que esencialmente significa que en este punto todavía no tenemos la predicción del cuadro delimitador. Para obtener realmente el resultado de la predicción, necesitamos conectar un cabezal específico al FPN y, en este caso, usaré el cabezal RPN (Red de propuesta de región).

Jefe RPN

En caso de que aún no esté familiarizado con RPN, este es esencialmente el principio de un modelo de detección de objetos utilizado para crear cuadros delimitadores, que se propuso por primera vez en el artículo Faster R-CNN. Tenga en cuenta que, si bien en esta demostración nos referimos al RPN como un cabezal, tenga en cuenta que en realidad no es un cabezal de detección completo, ya que no tiene capacidad para realizar la clasificación de los objetos detectados.

Podemos ver en la arquitectura RPN a continuación que utiliza las llamadas capas cls y reg, que producen la puntuación de objetividad y las coordenadas del cuadro delimitador, respectivamente. El tensor de puntuación de objetividad tiene una longitud de 2k, donde k es el número de cajas de anclaje predeterminadas y 2 es la probabilidad de que la caja de anclaje correspondiente esté allí. Podemos pensar en esto como una clasificación binaria tratada con una representación única (objeto/no objeto). Mientras tanto, el número 4 en la longitud de 4k del tensor de coordenadas simplemente corresponde a la predicción xywh.

Figura 8. La estructura de RPN[4].

Volviendo a la implementación de nuestro código, en el bloque de código 7 a continuación inicializamos las capas intermedia, cls y reg dentro del método __init__() de la clase RPN. Tenga en cuenta que la capa intermedia es la única que usa convolución 3×3, mientras que tanto la capa cls como la reg usan convolución 1×1. Con respecto al número de canales, la capa intermedia asigna el tensor de entrada a 256 canales, mientras que cls y reg lo asignan a 2k y 4k, respectivamente. Finalmente, podemos simplemente conectar estas capas dentro del método forward().

# Bloque de código 7 NUM_ANCHORS = 3 clase RPN(nn.Module): def __init__(self): super().__init__() self.intermediate = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) self.cls = nn.Conv2d(in_channels=256, out_channels=NUM_ANCHORS*2, kernel_size=1) self.reg = nn.Conv2d(in_channels=256, out_channels=NUM_ANCHORS*4, kernel_size=1) def forward(self, x): x = self.intermediate(x) objectness_scores = self.cls(x) bbox_regressions = self.reg(x) return objectness_scores, bbox_regresiones

Ahora probemos si nuestra clase RPN funciona correctamente ejecutando el Codeblock 8 a continuación. Aquí lo pruebo en el mapa de características p2 que obtuvimos de Codeblock 6.

# Bloque de código 8 rpn = RPN() p2_objectness, p2_bbox = rpn(p2) print(p2_objectness.shape) print(p2_bbox.shape)

A continuación se muestra cómo se ve el resultado resultante. Puede ver que p2_objectness es un tensor que tiene un tamaño de 6 × 56 × 56, lo que indica que cada píxel en la dimensión espacial de 56 × 56 contiene 6 valores de predicción, donde los primeros 2 valores son para el primer cuadro de anclaje, los 2 segundos valores son para el segundo cuadro de anclaje y los 2 últimos valores son para el tercero. Lo mismo se aplica también al tensor p2_bbox, que en este caso contiene los valores xywh.

# Bloque de código 8 Salida antorcha.Tamaño([1, 6, 56, 56]) antorcha.Tamaño([1, 12, 56, 56])

El modelo de detección completo

En el bloque de código 9 a continuación, construiremos el modelo de detección completo para que pueda comprender mejor cómo funciona FPN junto con los otros componentes. Aquí, en el método __init__(), inicializo el cuello de botella de CNN, el cuello de FPN y el cabezal de RPN. En el método forward(), primero pasamos el tensor de imagen a la CNN (#(1)). Este backbone devuelve 4 tensores, los cuales están listos para ser conectados al FPN a través de conexiones laterales. A continuación, en la línea n.° (2) alimentamos todos los tensores C como entrada de FPN, produciendo los tensores P. Por último, utilizamos todas las P como entrada al RPN (#(3–4)). Tenga en cuenta que RPN comparte sus parámetros entre todos los cabezales de detección, por lo que solo necesitamos inicializarlo una vez y usarlo para todos los mapas de características de diferentes escalas.

# Bloque de código 9 clase DetectorModel(nn.Module): def __init__(self): super().__init__() self.cnn = CNN() self.fpn = FPN() self.rpn = RPN() def forward(self, x): c2, c3, c4, c5 = self.cnn(x) #(1) p2, p3, p4, p5 = self.fpn(c2, c3, c4, c5) #(2) p2_pred = self.rpn(p2) #(3) p3_pred = self.rpn(p3) p4_pred = self.rpn(p4) p5_pred = self.rpn(p5) #(4) return p2_pred, p3_pred, p4_pred, p5_pred

Ahora que el cabezal de detección está completo, podemos probarlo con el Codeblock 10 a continuación. Aquí intento pasar un tensor ficticio de tamaño 1×3×224×224, simulando una única imagen RGB de tamaño 224×224 (#(1)). A continuación, podemos simplemente pasarlo a través del modelo_detección (#(2)) y descomprimir los resultados de la predicción (#(3–4)).

# Bloque de código 10 modelo_detección = Modelo_detección() x = torch.randn(1, 3, 224, 224) #(1) p2_pred, p3_pred, p4_pred, p5_pred = modelo_detección(x) #(2) p2_objetividad, p2_bbox = p2_pred #(3) p3_objetividad, p3_bbox = p3_pred p4_objeto, p4_bbox = p4_pred p5_objeto, p5_bbox = p5_pred #(4) imprimir(p2_objeto.forma) imprimir(p3_objeto.forma) imprimir(p4_objeto.forma) imprimir(p5_objeto.forma) imprimir() imprimir(p2_bbox.forma) imprimir(p3_bbox.forma) imprimir(p4_bbox.forma) imprimir(p5_bbox.forma)

A continuación se muestra cómo se ve el resultado. Puede ver aquí que las dimensiones del tensor resultantes son las previstas, donde los tensores de objetividad y bbox contienen 6 y 12 valores para cada celda de la cuadrícula, respectivamente. Entonces, creo que esta implementación es correcta y, por lo tanto, está lista para ser entrenada para la tarea de detección de objetos.

# Bloque de código 10 Salida antorcha.Tamaño([1, 6, 56, 56]) antorcha.Tamaño([1, 6, 28, 28]) antorcha.Tamaño([1, 6, 14, 14]) antorcha.Tamaño([1, 6, 7, 7]) antorcha.Tamaño([1, 12, 56, 56]) antorcha.Tamaño([1, 12, 28, 28]) antorcha.Tamaño([1, 12, 14, 14]) antorcha.Tamaño([1, 12, 7, 7])

Final

Creo que se trata básicamente de la teoría subyacente y la implementación desde cero de FPN. Aquí lo desafío a que intente implementar FPN en la ResNet real en lugar de un modelo CNN ficticio como lo demostré anteriormente. De hecho, tengo un artículo aparte sobre ResNet, que puedes consultar como referencia.[5]. O también es posible utilizar otros modelos si lo desea, como VGG, ResNeXt, ConvNeXt, etc., ya que FPN básicamente puede funcionar en cualquier modelo de red troncal basado en CNN. No solo eso, también sería mejor si pudiera implementar un cabezal estilo YOLO como reemplazo de RPN, cuyos ejemplos se pueden ver en mis artículos anteriores en el número de referencias.[6]para YOLOv1,[7]para YOLOv2, y[1]para YOLOv3.

Avíseme si hay errores en mi escritura o en el código. ¡Gracias por leer! Por cierto, puedes encontrar el código utilizado en este artículo en mi repositorio de GitHub.[8].

Referencias

[1]Muhamad Ardi. Tutorial en papel de YOLOv3: incluso mejor, pero no tanto. Medio. https://ai.gopubby.com/yolov3-paper-walkthrough-even-better-but-not-that-much-4dc6c0c1b42c [Consultado el 1 de junio de 2026].

[2]Tsung-Yi Lin. Característica de redes piramidales para la detección de objetos. Arxiv. https://arxiv.org/abs/1612.03144 [Consultado el 9 de septiembre de 2025].

[3]Imagen creada originalmente por el autor.

[4]Shao Qing Ren. R-CNN más rápido: hacia la detección de objetos en tiempo real con redes de propuesta regional. Arxiv. https://arxiv.org/abs/1506.01497 [Consultado el 9 de septiembre de 2025].

[5]Muhamad Ardi. Tutorial en papel: red residual (ResNet). Python en inglés sencillo. https://medium.com/python-in-plain-english/paper-walkthrough-residual-network-resnet-62af58d1c521 [Consultado el 9 de septiembre de 2025].

[6]Muhamad Ardi. Tutorial en papel de YOLOv1: El día en que YOLO vio el mundo por primera vez. Medio. https://medium.com/ai-advances/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b [Consultado el 1 de junio de 2026].

[7]Muhamad Ardi. Tutorial en papel de YOLOv2 y YOLO9000: mejor, más rápido y más potente. Medio. https://ai.gopubby.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger-c9906e0438a3 [Consultado el 1 de junio de 2026].

[8]MuhammadArdiPutra. FPN. GitHub. https://github.com/MuhammadArdiPutra/medium_articles/blob/main/FPN.ipynb [Consultado el 9 de septiembre de 2025].