保留正文与补采附件。源页面当前未重新核验;具体限制见交付清单。
We value your privacy
We use cookies to enhance your browsing experience, serve personalised ads or content, and analyse our traffic. By clicking "Accept All", you consent to our use of cookies.
Customise Reject All Accept All
Customise Consent Preferences
We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.
The cookies that are categorised as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. ... Show more
NecessaryAlways Active
Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.
Cookie BCTempID
Duration 10 minutes
Description No description available.
Cookie __cf_bm
Duration 1 hour
Description This cookie, set by Cloudflare, is used to support Cloudflare Bot Management.
Cookie AWSALBCORS
Duration 7 days
Description Amazon Web Services set this cookie for load balancing.
Cookie _cfuvid
Duration session
Description Cloudflare sets this cookie to track users across sessions to optimize user experience by maintaining session consistency and providing personalized services
Cookie li_gc
Duration 6 months
Description Linkedin set this cookie for storing visitor's consent regarding using cookies for non-essential purposes.
Cookie __hssrc
Duration session
Description This cookie is set by Hubspot whenever it changes the session cookie. The __hssrc cookie set to 1 indicates that the user has restarted the browser, and if the cookie does not exist, it is assumed to be a new session.
Cookie __hssc
Duration 1 hour
Description HubSpot sets this cookie to keep track of sessions and to determine if HubSpot should increment the session number and timestamps in the __hstc cookie.
Cookie wpEmojiSettingsSupports
Duration session
Description WordPress sets this cookie when a user interacts with emojis on a WordPress site. It helps determine if the user's browser can display emojis properly.
Cookie BCSessionID
Duration 1 year 1 month 4 days
Description Blueconic sets this cookie as a unique identifier for the BlueConic profile.
Cookie _octo
Duration 1 year
Description No description available.
Cookie logged_in
Duration 1 year
Description No description available.
Cookie __Secure-YEC
Duration past
Description YouTube sets this cookie to stores the user's video player preferences using embedded YouTube video
Cookie __eoi
Duration 6 months
Description Description is currently not available.
Cookie AWSALBTGCORS
Duration 7 days
Description No description available.
Cookie login-status-p
Duration past
Description Description is currently not available.
Cookie AWSALBTG
Duration 7 days
Description No description available.
Cookie csrf_token
Duration session
Description No description available.
Cookie token_v2
Duration 1 day
Description Description is currently not available.
Cookie D
Duration 1 year
Description Description is currently not available.
Cookie PHPSESSID
Duration session
Description This cookie is native to PHP applications. The cookie stores and identifies a user's unique session ID to manage user sessions on the website. The cookie is a session cookie and will be deleted when all the browser windows are closed.
Cookie VISITOR_PRIVACY_METADATA
Duration 6 months
Description YouTube sets this cookie to store the user's cookie consent state for the current domain.
Cookie cookietest
Duration session
Description The cookietest cookie is typically used to determine whether the user's browser accepts cookies, essential for website functionality and user experience.
Cookie __Host-airtable-session
Duration 1 year
Description This cookie is used to enable us to integrate the services of Airtable.
Cookie __Host-airtable-session.sig
Duration 1 year
Description This cookie is used to enable us to integrate the services of Airtable.
Cookie m
Duration 1 year 1 month 4 days
Description Stripe sets this cookie for fraud prevention purposes. It identifies the device used to access the website, allowing the website to be formatted accordingly.
Cookie BIGipServer*
Duration session
Description Marketo sets this cookie to collect information about the user's online activity and build a profile about their interests to provide advertisements relevant to the user.
Cookie __cfruid
Duration session
Description Cloudflare sets this cookie to identify trusted web traffic.
Cookie _GRECAPTCHA
Duration 6 months
Description Google Recaptcha service sets this cookie to identify bots to protect the website against malicious spam attacks.
Cookie __Secure-YNID
Duration 6 months
Description Google cookie used to protect user security and prevent fraud, especially during the login process.
Cookie cookieyes-consent
Duration 1 year
Description CookieYes sets this cookie to remember users' consent preferences so that their preferences are respected on subsequent visits to this site. It does not collect or store any personal information about the site visitors.
Functional Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.
Cookie lidc
Duration 1 day
Description LinkedIn sets the lidc cookie to facilitate data center selection.
Cookie brw
Duration 1 year
Description No description available.
Cookie brwConsent
Duration 5 minutes
Description Description is currently not available.
Cookie WMF-Uniq
Duration 1 year
Description Description is currently not available.
Cookie loom_anon_comment
Duration 1 year
Description No description available.
Cookie loom_referral_video
Duration session
Description Description is currently not available.
Cookie VISITOR_INFO1_LIVE
Duration 6 months
Description A cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface.
Cookie yt-remote-connected-devices
Duration Never Expires
Description YouTube sets this cookie to store the user's video preferences using embedded YouTube videos.
Cookie ytidb::LAST_RESULT_ENTRY_KEY
Duration Never Expires
Description The cookie ytidb::LAST_RESULT_ENTRY_KEY is used by YouTube to store the last search result entry that was clicked by the user. This information is used to improve the user experience by providing more relevant search results in the future.
Cookie yt-remote-device-id
Duration Never Expires
Description YouTube sets this cookie to store the user's video preferences using embedded YouTube videos.
Cookie yt-remote-session-name
Duration session
Description The yt-remote-session-name cookie is used by YouTube to store the user's video player preferences using embedded YouTube video.
Cookie yt-remote-fast-check-period
Duration session
Description The yt-remote-fast-check-period cookie is used by YouTube to store the user's video player preferences for embedded YouTube videos.
Cookie yt-remote-session-app
Duration session
Description The yt-remote-session-app cookie is used by YouTube to store user preferences and information about the interface of the embedded YouTube video player.
Cookie yt-remote-cast-available
Duration session
Description The yt-remote-cast-available cookie is used to store the user's preferences regarding whether casting is available on their YouTube video player.
Cookie yt-remote-cast-installed
Duration session
Description The yt-remote-cast-installed cookie is used to store the user's video player preferences using embedded YouTube video.
Cookie cp_session
Duration 3 months
Description Codepen sets this cookie for Help systems found in the website.
Cookie loid
Duration 1 year 1 month 4 days
Description This cookie is set by the Reddit. The cookie enables the sharing of content from the website onto the social media platform.
Analytics Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.
Cookie __hstc
Duration 6 months
Description Hubspot set this main cookie for tracking visitors. It contains the domain, initial timestamp (first visit), last timestamp (last visit), current timestamp (this visit), and session number (increments for each subsequent session).
Cookie hubspotutk
Duration 6 months
Description HubSpot sets this cookie to keep track of the visitors to the website. This cookie is passed to HubSpot on form submission and used when deduplicating contacts.
Cookie _ga
Duration 1 year 1 month 4 days
Description Google Analytics sets this cookie to calculate visitor, session and campaign data and track site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognise unique visitors.
Cookie ga*
Duration 1 year 1 month 4 days
Description Google Analytics sets this cookie to store and count page views.
Cookie __Host-psifi.analyticsTrace
Duration 6 hours
Description Description is currently not available.
Cookie __Host-psifi.analyticsTraceV2
Duration 6 hours
Description Description is currently not available.
Cookie _gh_sess
Duration session
Description GitHub sets this cookie for temporary application and framework state between pages like what step the user is on in a multiple step form.
Cookie YSC
Duration session
Description YSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages.
Cookie ajs_anonymous_id
Duration 1 year
Description This cookie is set by Segment to count the number of people who visit a certain site by tracking if they have visited before.
Cookie vuid
Duration 1 year 1 month 4 days
Description Vimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website.
Performance Performance cookies are used to understand and analyse the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Cookie AWSALB
Duration 7 days
Description AWSALB is an application load balancer cookie set by Amazon Web Services to map the session to the target.
Cookie acq
Duration past
Description Description is currently not available.
Cookie acq.sig
Duration past
Description Description is currently not available.
Cookie ptc
Duration 2 years
Description No description available.
Advertisement Advertisement cookies are used to provide visitors with customised advertisements based on the pages you visited previously and to analyse the effectiveness of the ad campaigns.
Cookie muc_ads
Duration 1 year 1 month 4 days
Description Twitter sets this cookie to collect user behaviour and interaction data to optimize the website.
Cookie guest_id_marketing
Duration 1 year 1 month 4 days
Description Twitter sets this cookie to identify and track the website visitor.
Cookie guest_id_ads
Duration 1 year 1 month 4 days
Description Twitter sets this cookie to identify and track the website visitor.
Cookie personalization_id
Duration 1 year 1 month 4 days
Description Twitter sets this cookie to integrate and share features for social media and also store information about how the user uses the website, for tracking and targeting.
Cookie guest_id
Duration 1 year 1 month 4 days
Description Twitter sets this cookie to identify and track the website visitor. It registers if a user is signed in to the Twitter platform and collects information about ad preferences.
Cookie bcookie
Duration 1 year
Description LinkedIn sets this cookie from LinkedIn share buttons and ad tags to recognize browser IDs.
Cookie __Secure-ROLLOUT_TOKEN
Duration 6 months
Description YouTube sets this cookie to manage feature rollout and experimentation. It helps Google control which new features or interface changes are shown to users as part of testing and staged rollouts, ensuring consistent experience for a given user during an experiment.
Cookie yt.innertube::nextId
Duration Never Expires
Description YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.
Cookie yt.innertube::requests
Duration Never Expires
Description YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.
Cookie session_tracker
Duration session
Description This cookie is set by the Reddit. This cookie is used to identify trusted web traffic. It also helps in adverstising on the website.
Cookie edgebucket
Duration session
Description Reddit sets this cookie to save the information about a log-on Reddit user, for the purpose of advertisement recommendations and updating the content.
Cookie did
Duration 1 year
Description Arbor sets this cookie to show targeted ads to site visitors.This cookie expires after 2 months or 1 year.
Uncategorised Other uncategorised cookies are those that are being analysed and have not been classified into a category as yet. No cookies to display. Reject All Save My Preferences Accept All Toggle SearchToggle Mobile Navigation Publish AI, ML & data-science insights to a global community of data professionals. Sign inSubmit an Article LatestEditor's PicksNewsletterShipAI
Write For TDS LinkedInXToggle Search Deep Learning
Understanding how FPN allows deep learning models detecting small objects and how to implement it from scratch
Muhammad Ardi Putra
June 4, 202618 min read
Photo by
In my previous article I was talking about YOLOv3 [1]. One of the factors that makes this YOLO version better than its predecessors is its ability in detecting small objects thanks to its FPN-like neck adopted by the model. Unfortunately, my explanation about FPN in that article was not quite thorough since I was focusing more on YOLOv3 itself. Thus, in this article I decided to write specifically about FPN from its original paper titled “ Feature Pyramid Networks for Object Detection ” [2] so that you can get a better understanding of what it actually is and how it works. Not only that, here I will also demonstrate how to implement FPN from scratch and how to connect it with a CNN backbone and an RPN head.
···
Before we get into FPN, we first need to know that the structure of an object detection model is different from that of the classification model, in which the main difference lies in the very last layer. In a typical classification model, the last layer comprises a number of neurons where each of them corresponds to every single class available in the dataset. Or, in the case of binary classification, the output layer only consists of a single neuron, which is responsible to predict whether a sample belongs to class 0 or 1. This kind of output layer is actually not suitable for detection task since it also requires neurons dedicated for predicting the location and the size of an object in addition to its class.
So, in order for a model to be able to predict object location and size, we need to replace the output layer, i.e., the classification head , with the so-called detection head. The remaining layers themselves (everything except the head) are commonly called backbone. Some models that use this structure are YOLOv1 and YOLOv2, where they use a stack of convolution layers as the backbone and a specific head for predicting object location and size within an image as well as its class.
Learn this step by step with the interactive Machine Learning roadmap.
Older object detection models like YOLOv1 and YOLOv2 mentioned above only consist of a backbone and a head. As time went on, researchers found that this structure is still not quite optimal, hence they finally came up with an idea by adding a new component called neck. As the name suggests, this is essentially something we place between the backbone and the head. And FPN, which we are going to talk about in this article, is one of the earliest necks proposed for object detection models. Look at the Figure 1 below to see the high-level architectural view of the older and modern object detection models.
Figure 1. The architecture of an object detection model in general [3].
The backbone of a model is mainly responsible for performing feature extraction, while the neck is useful for enhancing feature quality, and the head is for making predictions. Based on this notion, we can say that by applying FPN, a network can potentially achieve better accuracy thanks to the feature enhancement mechanism performed by the neck.
Previously I mentioned that using backbone and detection head without neck is not quite optimal. This is especially regarding its capability in detecting small objects. Now let’s take a look at the Figure 2 below. The first two YOLO versions I mentioned earlier utilize the structure in image (b), where the bounding box and the object class predictions are made solely on top of the feature map produced by the deepest layer in the backbone. This method is valid, but is effective only for large objects. The reason is pretty straightforward: as an image gets deeper into a network, the spatial dimension shrinks, and more importantly, the pixel information contained in the deeper feature maps becomes a representation of several neighboring pixels in the shallower ones, which causes the spatial information to blend. By doing this, feature maps from deeper layers got a large receptive field, allowing large objects to be detected and recognized easily. However, the degradation of spatial information as we get deeper prevents us from detecting small objects accurately because we do need a detailed pixel location in order to predict the exact coordinates of the objects.
Additionally, the receptive field size of a feature map is positively correlated with the amount of semantic information it contains. In the figure below, a feature map of high semantic information is indicated by a thick blue outline. This is essentially why the deepest feature map in (b) has the thickest outline.
Figure 2. Comparison of different feature pyramid architectures [2].
The most straightforward approach to allow a network to simultaneously detect large and small objects is by using featurized image pyramid (a). This method is able to achieve high accuracy because we can make predictions from different image resolutions. What’s essentially done here is that we rescale our input image into multiple scales, perform feature extraction independently on each scale, and make predictions on the resulting feature maps. The smaller feature map is responsible to detect large objects, whereas the larger one is specialized to detect small objects thanks to its detailed spatial information. However, this method is computationally expensive since we need to process multiple raw images of different scales at once.
Another solution was proposed by the authors of SSD (Single Shot Multibox Detector), which in Figure 2 above is the one referred to as pyramidal feature hierarchy (c). So, instead of feeding the network with the same image of different sizes, the authors of SSD attempted to use only the largest image and utilize the internal pyramidal structure of the CNN backbone to make predictions of varying scales. This approach allows the system to be computationally more feasible than option (a). Nevertheless, here we actually got a tradeoff like (b), where the feature map from the deeper layer contains a large amount of semantic information yet having minimal spatial information, while the feature map from the shallower layer has a lot of spatial information but it does not have that much semantic information. It is important to note that a detailed spatial information might not be quite important for large objects as we can just approximate the general shape of that object. However, both spatial and semantic information are necessary for detecting small objects because not only the detailed coordinates, but the model also needs to understand what’s actually inside the bounding box. So, while it is true that method (c) is indeed able to detect both large and small objects, but its ability in detecting the latter is not yet optimal.
And here’s where FPN comes as a solution. If we take a look at image (d) in Figure 2, we can see that the predictions are made on top of the corresponding feature maps which are all semantically rich. This essentially allows objects of varying scales, including the smaller ones, to be detected accurately. We are going to talk about the details of how FPN enriches feature maps in the subsequent section.
···
The idea of FPN is to inject information from the deeper feature maps into the shallower ones, and by doing so we will have the shallower feature maps not only containing high spatial information but also high semantic information coming from the deeper part of the network. In theory, this should result in a better detection accuracy on small objects since the large feature maps are now enriched with a lot of semantic information. In order to achieve this, they introduce the so-called top-down pathway and lateral connections. You can see the complete FPN architecture in Figure 3 below, which is essentially the detailed version of the one in Figure 2 (d).
Figure 3. The detailed FPN architecture [3].
The authors of this paper decided to use ResNet-50 and ResNet-101 as the backbone. Suppose we were to use the former, we would later have the conv2 , conv3 , conv4 , and conv5 layers repeated 3, 4, 6, and 3 times, respectively, as suggested by the architectural details of ResNet in Figure 4. C2 , C3 , C4 , and C5 , which are the tensors produced by the last layer of the corresponding stage, are going to be transferred to the top-down pathway through lateral connections , i.e., the arrows going out from the backbone.
Figure 4. The ResNet architecture [3, 4].
The top-down pathway is used to transfer semantic information from deeper layers, whereas lateral connections are used to preserve spatial information. We aggregate the two by performing element-wise summation, which the detailed process is given in Figure 5 below. For the tensors that come from the backbone (C), we first need to apply 1×1 conv to them. This convolution layer is responsible for adjusting the number of channels so that it matches with the tensor coming from the top-down pathway. The tensor from the top-down pathway itself (M+1) undergoes 2× nearest-neighbor upsampling. These processes are essentially done because we need both tensors to have the exact same dimension so that element-wise summation can be performed. As the summation is done, the resulting tensor is now referred to as M. This tensor has some aliasing effect due to the upsampling process we did earlier, hence we need to apply a 3×3 convolution to reduce that effect. Finally, we got the P tensor, which is ready to be forwarded to the detection head.
Figure 5. How feature maps from lateral connection (C) and the top-down pathway (M+1) are aggregated [3].
Keep in mind that all processes described in Figure 5 above only apply to M2 , M3 , and M4. Computing M5 is actually much simpler (see Figure 6), where the only thing we need to do is just to adjust the number of channels using 1×1 conv to make it uniform with the tensors in the other lateral connections. The M5 tensor itself does not need to be processed further with 3×3 conv because there is nothing to be smoothed out due to the absence of upsampling mechanism. And so, we can basically say that P5 is the exact same tensor as M5.
Figure 6. The way to compute M5 and P5 is slightly different from that of M2-M4 and P2-P4 [3].
And well I think that’s everything about the theory behind FPN. In the next section I am going to bring you into the lower-level view of the architecture by implementing it from scratch with PyTorch.
···
So as seen in the Codeblock 1 below, the very first thing we need to do in the code is to import the required modules. python
# Codeblock 1
import torch
import torch.nn as nn
Since the focus of this article is FPN, here I will use a dummy model for the backbone instead of using the actual ResNet in order to simplify things. But still, the layers in the code are named according to Figure 3 and 4: conv1, conv2, conv3, conv4 and conv5 as shown in Codeblock 2 below. The output tensor dimension of each stage is also set according to the original ResNet architecture. So, although this backbone is just a plain CNN-based model, you can think of this like a normal ResNet.
Next, what we do inside the forward() method is to connect all the layers. If you take a closer look at the code, you will notice that each convolution layer is followed by a ReLU activation function and a maxpooling layer. The maxpooling layer itself is set to have the stride of 2, effectively halves the spatial dimension of the feature map. By repeating maxpooling layers multiple times, we will have our feature map gradually gets smaller as we get deeper into the network. This essentially creates a pyramidal structure within the CNN backbone which is leveraged by FPN to achieve high detection accuracy on varying object scales. In CNN, reducing spatial dimension like this is a standard practice to reduce computational complexity to compensate the increase of the number of channels.
Still within the forward() method, don’t forget to clone the main tensor x as shown at the lines marked with #(1), #(2), and #(3). The copied tensors, which are named c2, c3, and c4, will then be the return values of the CNN class alongside the feature map from the main flow (c5) (#(4)).
python
# Codeblock 2
class 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'original\t: {x.size()}\n')
x = self.relu(self.conv1(x))
print(f'after conv1\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
x = self.relu(self.conv2(x))
print(f'after conv2\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c2 = x.clone() #(1)
x = self.relu(self.conv3(x))
print(f'after conv3\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c3 = x.clone() #(2)
x = self.relu(self.conv4(x))
print(f'after conv4\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c4 = x.clone() #(3)
x = self.relu(self.conv5(x))
print(f'after conv5\t: {x.size()}')
c5 = self.maxpool(x)
print(f'after maxpool\t: {c5.size()}\n')
return c2, c3, c4, c5 #(4)
As the CNN class is done, we will now try to pass a dummy RGB image of size 224×224 through the network. This tensor dimension is chosen based on the input shape of the original ResNet.
python
# Codeblock 3
cnn = CNN()
x = torch.randn(1, 3, 224, 224)
out_cnn = cnn(x)
And below is what the output looks like. Here we can see that the number of channels after each conv layer matches exactly with the ResNet structure given in Figure 4. Not only that, it is also seen that the spatial dimension of our dummy tensor successfully halved after each stage thanks to the maxpooling layers. This essentially indicates that our simple CNN model really mimics the general structure of a ResNet model. markup
# Codeblock 3 Outputoriginal : torch.Size([1, 3, 224, 224])after conv1 : torch.Size([1, 64, 224, 224])after maxpool : torch.Size([1, 64, 112, 112])after conv2 : torch.Size([1, 256, 112, 112])after maxpool : torch.Size([1, 256, 56, 56])after 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])after conv5 : torch.Size([1, 2048, 14, 14])after maxpool : torch.Size([1, 2048, 7, 7])
We can also check what the returned tensors look like by running the code below. You can see in the resulting output that the c2 tensor has the shape of 256×56×56, c3 is of shape 512×28×28, and so on. By the way, you can just ignore the number 1 in the 0th axis since it only indicates the number of samples we pass within a single batch.
python
# Codeblock 4
c2, c3, c4, c5 = out_cnn
print(c2.shape)
print(c3.shape)
print(c4.shape)
print(c5.shape)
markup
# Codeblock 4 Outputtorch.Size([1, 256, 56, 56])torch.Size([1, 512, 28, 28])torch.Size([1, 1024, 14, 14])torch.Size([1, 2048, 7, 7])
···
As the CNN backbone is completed, now let’s move on to the FPN neck. In the Codeblock 5 below, we first initialize the upsample layer (#(1)) which we will use every time we want to double the spatial dimension of the M tensor. Here I set the mode parameter to nearest as suggested in the paper, which is actually a very simple interpolation method, allowing the process to be fast. Take a look at Figure 7 to see what a nearest-neighbor interpolation looks like.
python
# Codeblock 5
class 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(in_channels=1024, out_channels=256, kernel_size=1)
self.lateral_c3 = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1)
self.lateral_c2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=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)
return p2, p3, p4, p5
Figure 7. An example of a 2x upsampling process with nearest-neighbor interpolation method [3].
If you go back to Codeblock 4, you will see that the c{5,4,3,2}tensors returned by the backbone have different number of channels. This is basically the reason that we initialize the lateral_c{5,4,3,2}layers to process these tensors so that the resulting channel counts will be uniform. According to the paper, we need to set these convolution layers to produce 256 output channels, which is the reason why we use that number for the out_channelsparameter.
Next, based on the Figure 7, you can just imagine how pixelated the resulting feature maps are after being upsampled. Thus, we need to process the m{4,3,2}tensors further with the 3×3 conv layers which I refer to as smooth_m{4,3,2}. As all layers have been initialized, what we need to do next is to assemble them in the forward()method according to the structure I showed you earlier in Figure 3.
In addition to this, the paper also mentions that we don’t need to implement any nonlinearities within the FPN, which is the reason that all the convolution layers in the FPNclass above are not followed with ReLU. Now in Codeblock 6 below I try to pass the C tensors we obtained earlier through the FPN neck we just created. We can see in the output that the resulting tensors have different spatial resolutions. Later on, the p2tensor (the one that has 56×56 dimension) will be forwarded to a detection head to detect small objects, whereas p5(the 7×7 tensor) is going to be responsible for large objects.
python
# Codeblock 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)
markup
# Codeblock 6 Outputtorch.Size([1, 256, 56, 56])torch.Size([1, 256, 28, 28])torch.Size([1, 256, 14, 14])torch.Size([1, 256, 7, 7])
Here have already completed the FPN part. Remember that FPN is just the neck of a detection model, which essentially means that at this point we still haven’t got the bounding box prediction just yet. In order to actually obtain the prediction result, we need to connect a specific head to the FPN, and in this case I will use the RPN (Region Proposal Network) head. ···
In case you’re still not yet familiar with RPN, this is essentially the head of an object detection model used for creating bounding box, which was first proposed in the Faster R-CNN paper. Note that while in this demonstration we refer to the RPN as a head, keep in mind that it is actually not a complete detection head since it has no capability in performing classification on the detected objects.
We can see in the RPN architecture below that it utilizes the so-called cls layer and reg layer , which produce objectness score and the bounding box coordinates, respectively. The objectness score tensor has the length of 2 k , where k is the number of predetermined anchor boxes and 2 is the probability of the corresponding anchor box being there. We can think of this like a binary classification treated with a one-hot representation (object/non-object). Meanwhile, the number 4 in the 4 k length of the coordinates tensor simply correspond to the xywh prediction.
Figure 8. The structure of RPN [4].
Going back to our code implementation, in the Codeblock 7 below we initialize the intermediate, cls, and reg layers within the __init__() method of the RPN class. Note that the intermediate layer is the only one that uses 3×3 convolution, whereas both the cls and reg layers use 1×1 convs. Regarding the number of channels, the intermediate layer maps the input tensor into 256 channels, while the cls and reg map it into 2 k and 4 k , respectively. Finally, we can simply connect these layers within the forward() method.
python
# Codeblock 7
NUM_ANCHORS = 3
class 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_regressions
Now let’s test if our RPN class works properly by running the Codeblock 8 below. Here I test it on the p2 feature map we obtained from Codeblock 6.
python
# Codeblock 8
rpn = RPN()
p2_objectness, p2_bbox = rpn(p2)
print(p2_objectness.shape)
print(p2_bbox.shape)
Below is what the resulting output looks like. You can see that p2_objectness is a tensor having the size of 6×56×56, indicating that every single pixel in the 56×56 spatial dimension contains 6 prediction values, where the first 2 values are for the first anchor box, the second 2 values are for the second anchor box, and the last 2 values are for the third one. The similar thing also applies to the p2_bbox tensor, which in this case it contains the xywh values.
markup
# Codeblock 8 Outputtorch.Size([1, 6, 56, 56])torch.Size([1, 12, 56, 56])
···
In Codeblock 9 below we are going to construct the entire detection model so that you can better understand how FPN works together with the other components. Here in the __init__() method I initialize the CNN bottleneck, FPN neck, and RPN head. In the forward() method, we first pass the image tensor into the CNN (#(1)). This backbone returns 4 tensors, which are ready to be connected to the FPN through lateral connections. Next, at line #(2) we feed all the C tensors as the input of FPN, producing the P tensors. Lastly, we use all the P s as the input to the RPN (#(3–4)). Keep in mind that RPN shares its parameters across all detection heads, so we only need to initialize it once and use it for all feature map of different scales.
python
# Codeblock 9
class DetectionModel(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
Now as the detection head is complete, we can test it with the Codeblock 10 below. Here I try to pass a dummy tensor of size 1×3×224×224, simulating a single RGB image of size 224×224 (#(1)). Next, we can just pass it through the detection_model (#(2)) and unpack the prediction results (#(3–4)).
python
# Codeblock 10
detection_model = DetectionModel()
x = torch.randn(1, 3, 224, 224) #(1)
p2_pred, p3_pred, p4_pred, p5_pred = detection_model(x) #(2)
p2_objectness, p2_bbox = p2_pred #(3)
p3_objectness, p3_bbox = p3_pred
p4_objectness, p4_bbox = p4_pred
p5_objectness, p5_bbox = p5_pred #(4)
print(p2_objectness.shape)
print(p3_objectness.shape)
print(p4_objectness.shape)
print(p5_objectness.shape)
print()
print(p2_bbox.shape)
print(p3_bbox.shape)
print(p4_bbox.shape)
print(p5_bbox.shape)
Below is what the output looks like. You can see here that the resulting tensor dimensions are as intended, where the objectness and bbox tensors contain 6 and 12 values for each grid cell, respectively. So, I believe this implementation is correct and thus ready to be trained for object detection task.
markup
# Codeblock 10 Outputtorch.Size([1, 6, 56, 56])torch.Size([1, 6, 28, 28])torch.Size([1, 6, 14, 14])torch.Size([1, 6, 7, 7])torch.Size([1, 12, 56, 56])torch.Size([1, 12, 28, 28])torch.Size([1, 12, 14, 14])torch.Size([1, 12, 7, 7])
···
I think that’s pretty much all about the underlying theory and the from-scratch implementation of FPN. Here I challenge you to try implementing FPN on the real ResNet instead of a dummy CNN model like I demonstrated above. I actually got a separate article about ResNet, which you can check as a reference [5]. Or, it is also possible to use other models if you want, such as VGG, ResNeXt, ConvNeXt, etc since FPN can basically work on any CNN-based backbone model. Not only that, it would also be better if you can implement YOLO-style head as a replacement of RPN, which the examples can be seen in my previous articles given at references number [6] for YOLOv1, [7] for YOLOv2, and [1] for YOLOv3. Please let me know if there are mistakes in my writing or in the code. Thanks for reading! By the way, you can find the code used in this article in my GitHub repo [8]. ···
[1] Muhammad Ardi. YOLOv3 Paper Walkthrough: Even Better, but Not That Much. Medium. https://ai.gopubby.com/yolov3-paper-walkthrough-even-better-but-not-that-much-4dc6c0c1b42c [Accessed June 1, 2026]. [2] Tsung-Yi Lin. Feature Pyramid Networks for Object Detection. Arxiv. https://arxiv.org/abs/1612.03144 [Accessed September 9, 2025]. [3] Image created originally by author. [4] Shaoqing Ren. Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks. Arxiv. https://arxiv.org/abs/1506.01497 [Accessed September 9, 2025]. [5] Muhammad Ardi. Paper Walkthrough: Residual Network (ResNet). Python in Plain English. https://medium.com/python-in-plain-english/paper-walkthrough-residual-network-resnet-62af58d1c521 [Accessed September 9, 2025]. [6] Muhammad Ardi. YOLOv1 Paper Walkthrough: The Day YOLO First Saw the World. Medium. https://medium.com/ai-advances/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b [Accessed June 1, 2026]. [7] Muhammad Ardi. YOLOv2 & YOLO9000 Paper Walkthrough: Better, Faster, Stronger. Medium. https://ai.gopubby.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger-c9906e0438a3 [Accessed June 1, 2026]. [8] MuhammadArdiPutra. FPN. GitHub. https://github.com/MuhammadArdiPutra/medium_articles/blob/main/FPN.ipynb [Accessed September 9, 2025].
All articles on Towards Data Science are available to registered users. Create a free account today to access this article and thousands more. Register for freeSign in to your account
Written By
Deep learning storyteller See all from Muhammad Ardi Putra Deep DivesArtificial IntelligenceDeep LearningMachine LearningPython Share This Article
Towards Data Science is a community publication. Submit your insights to reach our global audience and earn through the TDS Author Payment Program. Write for TDS
Machine Learning Implementing dueling double deep q learning with TensorFlow 2.x Abhishek Suran July 10, 20202 min read
Deep Learning How to use BERT to calculate the semantic similarity between two texts Ruben Winastwan February 15, 202311 min read
Deep Learning Learn how to debug your Python program with Visual Studio Code Marcello Politi December 5, 20227 min read
Artificial Intelligence Notes From Natural Lanaguage Processing Specialization Course1 Week1 Kurtis Pykes October 17, 20208 min read
Data Science Ideas to express simultaneous data value changes in multiple locations over time Boriharn K November 4, 20237 min read
Machine Learning Introduction Khalil Zlaoui October 1, 20218 min read
The Variable Gain a high-level understanding of how the different pieces in your workflow come together TDS Editors November 20, 20253 min read
Deep Learning From model to production using Jupyter notebook ASM Saiful Islam January 25, 202112 min read
Deep Learning How does KL-Weight affect the model? Writing a Bayesian CNN for chest X-Ray images. Kaan Bıçakcı April 12, 20225 min read
Your home for data science and AI. The world's leading publication for data science, data analytics, data engineering, machine learning, and artificial intelligence professionals. Subscribe to Our NewsletterWrite For TDS•About•Advertise•Privacy Policy•Terms of Use © Insight Media Group, LLC 2026•The New Stack•roadmap.sh•Creator Network
Close