保留正文与补采附件。源页面当前未重新核验;具体限制见交付清单。
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
A review of the Cross-Stage Partial Network paper — and a from-scratch PyTorch implementation
Muhammad Ardi Putra
May 3, 202623 min read
Photo by Mihály Köles on Unsplash
How do you make your CNN-based model more lightweight? Just take the smaller version of that model, right? Like with ResNet, for instance, if ResNet-152 feels too heavy, why not just use ResNet-101? Or in the case of DenseNet, why not go with DenseNet-121 rather than DenseNet-169? — Yes, that’s true, but you would have to sacrifice some accuracy for that. Basically, if you want a lighter model then you should expect your accuracy to drop as well.
Now, what if I told you about a model that’s more lightweight than its base but can still compete on accuracy? Meet CSPNet (Cross Stage Partial Network). You’ll be surprised that it can effectively reduce computational complexity while maintaining high accuracy — no tradeoff! In this article we are going to talk about the CSPNet architecture, including how it works and how to implement it from scratch.
···
CSPNet was first introduced in a paper titled “ CSPNet: A New Backbone That Can Enhance Learning Capability of CNN ” written by Wang et al. back in November 2019 [1]. CSPNet was originally proposed to address the limitations of DenseNet. Despite already being computationally cheaper than ResNet, the authors thought that the computation of DenseNet itself is still considered expensive. Take a look at the main building block of a DenseNet in Figure 1 below to understand why.
Learn this step by step with the interactive Machine Learning roadmap.
Figure 1. The main building block of a DenseNet model [2].
In a DenseNet building block — called dense block — every convolution layer takes information from all previous layers, causing it to have a lot of redundant gradient information that makes training inefficient. We can think of it like a student taught by 5 different teachers for the same material. It’s actually good since the student can get multiple perspectives about that specific topic. However, at some point it becomes redundant and thus inefficient. In the case of DenseNet, we can see the deeper layers as students and all the tensors from shallower layers as teachers. In the example above, if we assume H₄ as our student, then the x₀ , x₁ , x₂ , and x₃ tensors act as the teachers. Here you can just imagine how that student would get overwhelmed by all that information!
Before we get into CSPNet, I actually have a whole separate article specifically talking about DenseNet (reference [3]), which I highly recommend you read if you want the full picture of how this architecture works.
The objective of CSPNet is to enable a network to have cheaper computational complexity and better gradient combination. The reason for the latter is that most gradient information in DenseNet consists of duplicates of each other. It is important to note that CSPNet is not a standalone network. Instead, it is a new paradigm we apply to DenseNet.
Now let’s take a look at Figure 2 below to see how CSPNet achieves its objectives. You can see the illustration on the left that the number of feature maps gradually increases as we get deeper into the network. If you have read my previous article about DenseNet, this is essentially something we can control through the growth rate parameter, i.e., the number of feature maps produced by each convolution layer within a dense block. In fact, this increase in the number of feature maps is what the authors see as a computational bottleneck.
Figure 2. Left: the original DenseNet building block (same as Figure 1). Right: The CSPNet version of the DenseNet building block (called CSPDenseNet) [1].
By applying the Cross Stage Partial mechanism, we can basically make the computation of a DenseNet cheaper. If we take a look at the illustration on the right, we can see that we have an additional branch coming out from x₀ that goes directly to the so-called Partial Transition Layer. There are at least two advantages we get with this mechanism, which are in accordance with the objectives I mentioned earlier. First, we can save lots of computations since the number of feature maps processed by the dense block is only half of the original one. And second, the gradient information becomes more diverse since we got an additional path with unprocessed feature maps that avoids the redundant gradient information. So in short, the idea of CSPNet eliminates the computational redundancy of DenseNet (through the skip-path) while at the same time still preserves its feature-reuse property (through the dense block).
···
Speaking of the details, the original feature map is first divided into two parts in channel-wise manner, where each of them will be processed in different paths. Suppose we got 64 input channels, the first 32 feature maps (part 1) will skip through all computations, whereas the remaining 32 (part 2) will be processed by a dense block. Although this splitting step is pretty easy, the merging step is actually not quite trivial. You can see in Figure 3 below that we got several different mechanisms to do so.
Figure 3. Several different ways to perform feature combination in CSPNet [1].
In the structure referred to as fusion first (c), we concatenate the part 1 tensor with the part 2 tensor that has been processed by the dense block prior to passing them through the transition layer. So, option (c) is actually pretty straightforward to implement because the spatial dimension of the two tensors is exactly the same, allowing us to concatenate them easily.
In my previous article [3], I mentioned that the transition layer of a DenseNet is used to reduce both the spatial dimension and the number of channels. In fact, this property requires us to rethink how to implement the fusion last (d) structure. This is essentially because the transition layer will cause the part 2 tensor to have a smaller spatial dimension than the part 1 tensor. So technically speaking, we need to either apply something like a pooling with a stride of 2 to the part 1 branch or simply omitting the downsampling operation in the transition layer. By doing this, the spatial dimension of the two tensors will be the same, and thus they are now concatenable.
Instead of just using a single transition layer placed either before or after feature combination, the authors also proposed another method which they refer to as CSPDenseNet (b). We can think of this as a combination of (c) and (d), where we got two transition layers placed before and after the tensor concatenation process. In this particular case, the first transition layer (the one placed in the part 2 branch) will perform channel reduction by cross-channel pooling , i.e., a pooling layer that operates across channel dimension. Meanwhile, the second transition layer will perform both spatial downsampling and channel count reduction. So basically, in this approach we reduce the number of channels twice — well, at least that’s what I understand from the paper about the two transition layers, as the detailed processes within these layers are not explicitly discussed.
Talking about the experimental results regarding these feature combination mechanisms, it is explained in the paper that fusion last (d) is better than fusion first (c), where the former can significantly reduce computational complexity while only suffers from a very slight drop in accuracy. Variant (c) actually also reduces computational complexity, yet the degradation in accuracy is also significant. Authors found that variant (b) obtained an even better result than the two. Figure 4 below displays several experimental results showing how the three feature combination mechanisms performed compared to the base model. However, instead of using DenseNet, they somehow decided to use PeleeNet to compare these structures.
Figure 4. Performance comparison of the base PeleeNet (corresponds to (a) in Figure 3), CSPPeleeNet (b), PeleeNet with fusion first method (c), and PeleeNet with fusion last method (d) [1].
Based on the above figure, we can see that the CSP fusion last (green) indeed performs better compared to the CSP fusion first (red). This is based on the fact that its accuracy only degrades by 0.1% from its base model while having 21% smaller computational complexity. Meanwhile, even though CSP fusion first successfully reduces computational complexity by 26%, but the accuracy drop is pretty significant as it performs 1.5% worse than the base PeleeNet. And the most impressive structure is the CSPPeleeNet variant (blue), i.e., the one that utilizes two transition layers. Here we can clearly see that although the computational complexity is reduced by 13%, the accuracy of the model actually improves by 0.2% — again, no tradeoff!
Not only that, but the authors also tried to implement CSPNet on other backbone models. The results in Figure 5 below shows that the CSPNet structure successfully reduces the computational complexity of DenseNet -201-Elastic and ResNeXt-50 by 19% and 22%, respectively. It is interesting to see that the accuracy of the ResNeXt model improves despite the reduction in model complexity, which is in accordance with the result obtained by CSPPeleeNet in Figure 4.
Figure 5. Performance improvement of DenseNet-201-Elastic and ResNeXt-50 after implementing the CSPNet mechanism [1].
For those who love math, here I got you some notations that you might find interesting to know. Figures 6 and 7 below display the mathematical expressions of DenseNet and CSPDenseNet blocks during the forward propagation phase.
In the DenseNet block, x₁ corresponds to the tensor produced by the first conv layer w₁ based on the input tensor x₀. Next, we concatenate the original tensor x₀ with x₁ and use them as the input for the w₂ layer (or to be more precise, w is actually the weights of the conv layer, not the conv layer itself). We keep producing more feature maps and concatenating the existing ones as we get deeper into the network. In this way, we can basically say that the outputs of all previous layers become the input of the current layer.
Figure 6. The mathematical representation of forward propagation within a DenseNet block [1].
The case is different for CSPDenseNet. You can see in the notation below that we got x₀’ and x₀’’ , which we previously refer to as the part 1 and part 2. The x₀’’ tensor undergoes processing like the one in DenseNet block until we got xₖ. Next, the output of this dense block is then forwarded to the first transition layer, which is denoted as wᴛ. The resulting tensor xᴛ is then concatenated with the part 1 tensor x₀’ before eventually being passed through the second transition layer wᴜ to obtain the final output tensor xᴜ.
Figure 7. The mathematical expression of the forward propagation in CSPDenseNet block [1].
···
Now let’s get even deeper into the CSPNet architecture by implementing it from scratch. Although we can basically apply the CSPNet structure to any backbone, here I am going to do that on the DenseNet model to match it with the illustrations and equations I showed you earlier. Figure 8 below displays what the complete DenseNet architecture looks like. Just remember that every single dense block in this architecture originally follows the DenseNet structure in Figure 3a, and our objective here is to replace all these dense blocks with CSPDenseNet block illustrated in Figure 3b.
Figure 8. The complete DenseNet architecture [2].
The first thing we do is to import the required modules and initialize the configurable parameters as shown in Codeblock 1. The GROWTH variable is the growth rate parameter, which denotes the number of feature maps produced by each bottleneck within the dense block. Next, CHANNEL_POOLING is the parameter we use to adjust the behavior of the channel-pooling mechanism in our first transition layer. Here I set this parameter to 0.8, meaning that we will shrink the number of channels to 80% of its original channel count. The COMPRESSION parameter works similarly to the CHANNEL_POOLING variable, yet this one operates in the second transition layer. Finally, here we define the REPEATS list, which is used to set the number of bottleneck blocks we will initialize within the dense block of each stage.
python
# Codeblock 1
import torch
import torch.nn as nn
GROWTH = 12
CHANNEL_POOLING = 0.8
COMPRESSION = 0.5
REPEATS = [6, 12, 24, 16]
Below is the implementation of the bottleneck block to be placed within the dense block. This Bottleneck class is exactly the same as the one I used in my DenseNet article [3]. I directly copy-pasted the code from there since we don’t need to modify this part at all. Just keep in mind that a bottleneck block comprises a 1×1 convolution followed by a 3×3 convolution.
python
# Codeblock 2
class Bottleneck(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=GROWTH*4,
kernel_size=1,
padding=0,
bias=False)
self.bn1 = nn.BatchNorm2d(num_features=GROWTH*4)
self.conv1 = nn.Conv2d(in_channels=GROWTH*4,
out_channels=GROWTH,
kernel_size=3,
padding=1,
bias=False)
def forward(self, x):
print(f'original\t: {x.size()}')
out = self.dropout(self.conv0(self.relu(self.bn0(x))))
print(f'after conv0\t: {out.size()}')
out = self.dropout(self.conv1(self.relu(self.bn1(out))))
print(f'after conv1\t: {out.size()}')
concatenated = torch.cat((out, x), dim=1)
print(f'after concat\t: {concatenated.size()}')
return concatenated
The following testing code simulates the first bottleneck block within the dense block. Remember that the very first conv layer in the architecture (the one with 7×7 kernel) produces 64 feature maps, but since in the case of CSPNet we only want to process half of them (the part 2 tensor), hence here we will test it with a tensor of 32 feature maps. python
# Codeblock 3
bottleneck = Bottleneck(in_channels=32)
x = torch.randn(1, 32, 56, 56)
x = bottleneck(x)
text
# Codeblock 3 Output
original : torch.Size([1, 32, 56, 56])
after conv0 : torch.Size([1, 48, 56, 56])
after conv1 : torch.Size([1, 12, 56, 56])
after concat : torch.Size([1, 44, 56, 56])
You can see in the resulting output above that the number of feature maps becomes 44 at the end of the process, where this number is obtained by adding the input channel count and the growth rate, i.e., 32 + 12 = 44. Again, you can just check out my DenseNet article [3] if you want to get a better understanding about this calculation.
Now to create a sequence of bottleneck blocks easily, we can just wrap it inside the DenseBlock class in Codeblock 4 below. Later on, we can just specify the number of bottleneck blocks to be stacked through the repeats parameter. Again, this class is also copy-pasted from my DenseNet article, so I am not going to explain it any further.
python
# Codeblock 4
class DenseBlock(nn.Module):
def __init__(self, in_channels, repeats):
super().__init__()
self.bottlenecks = nn.ModuleList()
for i in range(repeats):
current_in_channels = in_channels + i * GROWTH
self.bottlenecks.append(Bottleneck(in_channels=current_in_channels))
def forward(self, x):
print(f'original\t\t\t: {x.size()}')
for i, bottleneck in enumerate(self.bottlenecks):
x = bottleneck(x)
print(f'after bottleneck #{i}\t\t: {x.size()}')
return x
In order to check if our DenseBlock class works properly, we will test it using the Codeblock 5 below. Here I am trying to simulate the part 2 tensor processed by the first dense block, which contains a sequence of 6 bottleneck blocks.
python
# Codeblock 5
dense_block = DenseBlock(in_channels=32, repeats=6)
x = torch.randn(1, 32, 56, 56)
x = dense_block(x)
And below is what the output looks like. Here we can clearly see that each bottleneck block successfully increases the feature maps by 12. text
# Codeblock 5 Output
original : torch.Size([1, 32, 56, 56])
after bottleneck #0 : torch.Size([1, 44, 56, 56])
after bottleneck #1 : torch.Size([1, 56, 56, 56])
after bottleneck #2 : torch.Size([1, 68, 56, 56])
after bottleneck #3 : torch.Size([1, 80, 56, 56])
after bottleneck #4 : torch.Size([1, 92, 56, 56])
after bottleneck #5 : torch.Size([1, 104, 56, 56])
Remember that the CSPDenseNet variant in Figure 3b uses two transition layers. In this section we are going to discuss the first transition layer, i.e., the one used to process the tensor in the part 2 branch. Here we will not perform spatial downsampling, which is the reason why you don’t see any pooling layer within the __init__() method in Codeblock 6 below. Instead, here we will only perform cross-channel pooling, which can be perceived as a standard pooling operation yet is done across the channel dimension. To implement it, we can simply use a 1×1 convolution (#(2)) and specify the number of output channels we want (#(1)). We can think of it like this: in a spatial downsampling process, we can basically do that by using either pooling or a strided convolution layer, which in the latter case it will aggregate the pixel values with specific weightings from the local neighborhood. In the case of cross-channel pooling, since we don’t have a specific PyTorch layer for that, we can simply replace it with a pointwise convolution layer, which by doing so we can basically aggregate pixel values across the channel dimension.
python
# Codeblock 6
class FirstTransition(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, #(1)
kernel_size=1, #(2)
padding=0,
bias=False)
self.dropout = nn.Dropout(p=0.2)
def forward(self, x):
print(f'original\t\t: {x.size()}')
out = self.dropout(self.conv(self.relu(self.bn(x))))
print(f'after first_transition\t: {out.size()}')
return out
The result given in the Codeblock 5 Output shows that the part 2 tensor will have the shape of 104×56×56 after being processed by the dense block. Thus, in the testing code below I will use this tensor shape to simulate the first transition layer within that stage. To adjust the number of output channels, we can simply multiply the input channel count with the CHANNEL_POOLING variable we initialized earlier as shown at line #(1) in Codeblock 7 below.
python
# Codeblock 7
first_transition = FirstTransition(in_channels=104,
out_channels=int(104*CHANNEL_POOLING)) #(1)
x = torch.randn(1, 104, 56, 56)
x = first_transition(x)
Now as the code above is run, we can see that the number of feature maps shrinks from 104 to 83 (80% of the original). text
# Codeblock 7 Output
original : torch.Size([1, 104, 56, 56])
after first_transition : torch.Size([1, 83, 56, 56])
The structure of the second transition layer is quite a bit the same as the first one, except that here we also have an average pooling layer with a stride of 2 to reduce the spatial dimension by half (#(1)).
python
# Codeblock 8
class SecondTransition(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,
bias=False)
self.dropout = nn.Dropout(p=0.2)
self.pool = nn.AvgPool2d(kernel_size=2, stride=2) #(1)
def forward(self, x):
print(f'original\t\t: {x.size()}')
out = self.pool(self.dropout(self.conv(self.relu(self.bn(x)))))
print(f'after second_transition\t: {out.size()}')
return out
Remember that the tensor coming into the second transition layer is a concatenation of the part 1 and the part 2 tensors. This is essentially the reason why in the testing code below I set this layer to accept 32 + 83 = 115 feature maps. Similar to the first transition layer, here we multiply this number of feature maps with the COMPRESSION variable (#(1)) to reduce the number of channels even further.
python
# Codeblock 9
second_transition = SecondTransition(in_channels=115,
out_channels=int(115*COMPRESSION)) #(1)
x = torch.randn(1, 115, 56, 56)
x = second_transition(x)
In the resulting output below we can see that the spatial dimension halves thanks to the average pooling layer. At the same time, the number of feature maps also decreases from 115 to 57 since we set the COMPRESSION parameter to 0.5.
text
# Codeblock 9 Output
original : torch.Size([1, 115, 56, 56])
after second_transition : torch.Size([1, 57, 28, 28])
With all the components ready, we can now build the entire CSPDenseNet architecture, which I break down in Codeblocks 10a, 10b, and 10c below. Let’s now focus on the Codeblock 10a first, where I initialize all the layers according to the structure given in Figure 8. Here you can see at line #(1) that we initialize a 7×7 convolution layer, which acts as the input layer of the network. This layer is then followed by a maxpooling layer (#(2)). These two layers use the stride of 2, meaning that the spatial dimensions of the input tensor will be reduced to one-fourth of its original size.
python
# Codeblock 10a
class CSPDenseNet(nn.Module):
def __init__(self):
super().__init__()
self.first_conv = nn.Conv2d(in_channels=3, #(1)
out_channels=64,
kernel_size=7,
stride=2,
padding=3,
bias=False)
self.first_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) #(2)
channel_count = 64
##### Stage 0
self.dense_block_0 = DenseBlock(in_channels=channel_count//2,
repeats=REPEATS[0])
self.first_transition_0 = FirstTransition(in_channels=(channel_count//2)+(REPEATS[0]*GROWTH),
out_channels=int(((channel_count//2)+(REPEATS[0]*GROWTH))*CHANNEL_POOLING))
channel_count = (channel_count - (channel_count//2)) + int(((channel_count//2)+(REPEATS[0]*GROWTH))*CHANNEL_POOLING)
self.second_transition_0 = SecondTransition(in_channels=channel_count,
out_channels=int(channel_count*COMPRESSION))
channel_count = int(channel_count*COMPRESSION)
#####
##### Stage 1
self.dense_block_1 = DenseBlock(in_channels=channel_count//2,
repeats=REPEATS[1])
self.first_transition_1 = FirstTransition(in_channels=(channel_count//2)+(REPEATS[1]*GROWTH),
out_channels=int(((channel_count//2)+(REPEATS[1]*GROWTH))*CHANNEL_POOLING))
channel_count = (channel_count - (channel_count//2)) + int(((channel_count//2)+(REPEATS[1]*GROWTH))*CHANNEL_POOLING)
self.second_transition_1 = SecondTransition(in_channels=channel_count,
out_channels=int(channel_count*COMPRESSION))
channel_count = int(channel_count*COMPRESSION)
#####
##### Stage 2
self.dense_block_2 = DenseBlock(in_channels=channel_count//2,
repeats=REPEATS[2])
self.first_transition_2 = FirstTransition(in_channels=(channel_count//2)+(REPEATS[2]*GROWTH),
out_channels=int(((channel_count//2)+(REPEATS[2]*GROWTH))*CHANNEL_POOLING))
channel_count = (channel_count - (channel_count//2)) + int(((channel_count//2)+(REPEATS[2]*GROWTH))*CHANNEL_POOLING)
self.second_transition_2 = SecondTransition(in_channels=channel_count,
out_channels=int(channel_count*COMPRESSION))
channel_count = int(channel_count*COMPRESSION)
#####
##### Stage 3
self.dense_block_3 = DenseBlock(in_channels=channel_count//2,
repeats=REPEATS[3])
self.first_transition_3 = FirstTransition(in_channels=(channel_count//2)+(REPEATS[3]*GROWTH),
out_channels=int(((channel_count//2)+(REPEATS[3]*GROWTH))*CHANNEL_POOLING))
channel_count = (channel_count - (channel_count//2)) + int(((channel_count//2)+(REPEATS[3]*GROWTH))*CHANNEL_POOLING)
#####
self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1,1)) #(3)
self.fc = nn.Linear(in_features=channel_count, out_features=1000) #(4)
Still with the above codeblock, here I group the layers I initialize based on the stage they belong to. Let’s now focus on the part I refer to as Stage 0. Here you can see that we got a dense block (dense_block_0) and the first transition layer (first_transition_0). These two components are responsible to process the part 2 tensor. Next, we initialize the second transition layer (second_transition_0), which is used to process the concatenation result of the part 1 and part 2 tensors. Since the channel count is dynamic depending on the GROWTH, CHANNEL_POOLING, COMPRESSION, and REPEATS variables, we need to keep track of the channel count after each step so that the model can adaptively adjust itself according to these variables. We do the same thing for all the remaining stages, except in Stage 3 we don’t initialize the second transition layer since at that point we won’t reduce the channels and the spatial dimension any further. Instead, we will directly pass the concatenated part 1 and part 2 tensors to the average pooling (#(3)) and the classification (#(4)) layers. And that ends our discussion about the Codeblock 10a above.
Before we get into the forward() method, there is another function we need to create: split_channels(). As the name suggests, this function, which is written in Codeblock 10b below, is used to split a tensor into part 1 and part 2. The if-else statement here is used to check if the number of channels is odd or even. In fact, it would be very easy if the channel count is an even number as we can just divide them into two (#(4)). But if the channel count is odd, we need to manually determine the size of each part as seen at line #(1) and #(2) before eventually splitting them (#(3)).
python
# Codeblock 10b
def split_channels(self, x):
channel_count = x.size(1)
if channel_count%2 != 0:
split_size_2 = channel_count // 2 #(1)
split_size_1 = channel_count - split_size_2 #(2)
return torch.split(x, [split_size_1, split_size_2], dim=1) #(3)
else:
return torch.split(x, channel_count // 2, dim=1) #(4)
As we have finished defining the __init__() and the split_channel() methods, we can now implement the forward() method in Codeblock 10c below. Generally speaking, what we do here is simply forward the tensor sequentially. But now let’s pay attention to the part I refer to as Stage 0. Here you can see that after the tensor is passed through the first_pool layer (#(1)), we then split it into two using the split_channels() function we declared earlier (#(2)). From there, we now obtain the part1 and part2 tensors. We will leave the part1 tensor as is all the way to the end of the stage. Meanwhile, for the part2 tensor, we will process it with the dense block (#(3)) and the first transition layer (#(4)). Next, we concatenate the resulting tensor with the part1 tensor to create the skip-connection (#(5)). And then, we finally pass it through the second transition layer (#(6)). The same steps are repeated for all stages until we eventually reach the output layer to make classification. Just remember that the Stage 3 is quite different because here we don’t have the second transition layer.
python
# Codeblock 10c
def forward(self, x):
print(f'original\t\t\t: {x.size()}')
x = self.first_conv(x)
print(f'after first_conv\t\t: {x.size()}')
x = self.first_pool(x) #(1)
print(f'after first_pool\t\t: {x.size()}\n')
##### Stage 0
part1, part2 = self.split_channels(x) #(2)
print(f'part1\t\t\t\t: {part1.size()}')
print(f'part2\t\t\t\t: {part2.size()}')
part2 = self.dense_block_0(part2) #(3)
print(f'part2 after dense block 0\t: {part2.size()}')
part2 = self.first_transition_0(part2) #(4)
print(f'part2 after first trans 0\t: {part2.size()}')
x = torch.cat((part1, part2), dim=1) #(5)
print(f'after concatenate\t\t: {x.size()}')
x = self.second_transition_0(x) #(6)
print(f'after second transition 0\t: {x.size()}\n')
##### Stage 1
part1, part2 = self.split_channels(x)
print(f'part1\t\t\t\t: {part1.size()}')
print(f'part2\t\t\t\t: {part2.size()}')
part2 = self.dense_block_1(part2)
print(f'part2 after dense block 1\t: {part2.size()}')
part2 = self.first_transition_1(part2)
print(f'part2 after first trans 1\t: {part2.size()}')
x = torch.cat((part1, part2), dim=1)
print(f'after concatenate\t\t: {x.size()}')
x = self.second_transition_1(x)
print(f'after second transition 1\t: {x.size()}\n')
##### Stage 2
part1, part2 = self.split_channels(x)
print(f'part1\t\t\t\t: {part1.size()}')
print(f'part2\t\t\t\t: {part2.size()}')
part2 = self.dense_block_2(part2)
print(f'part2 after dense block 2\t: {part2.size()}')
part2 = self.first_transition_2(part2)
print(f'part2 after first trans 2\t: {part2.size()}')
x = torch.cat((part1, part2), dim=1)
print(f'after concatenate\t\t: {x.size()}')
x = self.second_transition_2(x)
print(f'after second transition 2\t: {x.size()}\n')
##### Stage 3
part1, part2 = self.split_channels(x)
print(f'part1\t\t\t\t: {part1.size()}')
print(f'part2\t\t\t\t: {part2.size()}')
part2 = self.dense_block_3(part2)
print(f'part2 after dense block 2\t: {part2.size()}')
part2 = self.first_transition_3(part2)
print(f'part2 after first trans 2\t: {part2.size()}')
x = torch.cat((part1, part2), dim=1)
print(f'after concatenate\t\t: {x.size()}\n')
x = self.avgpool(x)
print(f'after avgpool\t\t\t: {x.size()}')
x = torch.flatten(x, start_dim=1)
print(f'after flatten\t\t\t: {x.size()}')
x = self.fc(x)
print(f'after fc\t\t\t: {x.size()}')
return x
Now let’s test the CSPDenseNet class we just created by running the Codeblock 11 below. Here I use a dummy tensor of shape 3×224×224 to simulate a 224×224 RGB image passed through the network. python
# Codeblock 11
cspdensenet = CSPDenseNet()
x = torch.randn(1, 3, 224, 224)
x = cspdensenet(x)
And below is what the output looks like. Here you can see that every time a tensor gets into a network, our split_channels() method correctly divides the tensor into two (#(1–2)). Then, the bottleneck block within each stage also correctly adds the number of channels of the part 2 tensor by 12 before eventually being passed through the first transition layer. The first transition layer itself successfully reduces the number of channels by 20% as seen at line #(3), simulating the cross-channel pooling mechanism. Afterwards, the resulting tensor is then concatenated with the tensor from part 1 (#(4)) and passed through the second transition layer (#(5)) to further reduce the number of channels and halve the spatial dimension. We do the same thing for all stages until eventually we got the 1000-class prediction.
text
# Codeblock 11 Output
original : torch.Size([1, 3, 224, 224])
after first_conv : torch.Size([1, 64, 112, 112])
after first_pool : torch.Size([1, 64, 56, 56])
part1 : torch.Size([1, 32, 56, 56]) #(1)
part2 : torch.Size([1, 32, 56, 56]) #(2)
after bottleneck #0 : torch.Size([1, 44, 56, 56])
after bottleneck #1 : torch.Size([1, 56, 56, 56])
after bottleneck #2 : torch.Size([1, 68, 56, 56])
after bottleneck #3 : torch.Size([1, 80, 56, 56])
after bottleneck #4 : torch.Size([1, 92, 56, 56])
after bottleneck #5 : torch.Size([1, 104, 56, 56])
part2 after dense block 0 : torch.Size([1, 104, 56, 56])
part2 after first trans 0 : torch.Size([1, 83, 56, 56]) #(3)
after concatenate : torch.Size([1, 115, 56, 56]) #(4)
after second transition 0 : torch.Size([1, 57, 28, 28]) #(5)
part1 : torch.Size([1, 29, 28, 28])
part2 : torch.Size([1, 28, 28, 28])
after bottleneck #0 : torch.Size([1, 40, 28, 28])
after bottleneck #1 : torch.Size([1, 52, 28, 28])
after bottleneck #2 : torch.Size([1, 64, 28, 28])
after bottleneck #3 : torch.Size([1, 76, 28, 28])
after bottleneck #4 : torch.Size([1, 88, 28, 28])
after bottleneck #5 : torch.Size([1, 100, 28, 28])
after bottleneck #6 : torch.Size([1, 112, 28, 28])
after bottleneck #7 : torch.Size([1, 124, 28, 28])
after bottleneck #8 : torch.Size([1, 136, 28, 28])
after bottleneck #9 : torch.Size([1, 148, 28, 28])
after bottleneck #10 : torch.Size([1, 160, 28, 28])
after bottleneck #11 : torch.Size([1, 172, 28, 28])
part2 after dense block 1 : torch.Size([1, 172, 28, 28])
part2 after first trans 1 : torch.Size([1, 137, 28, 28])
after concatenate : torch.Size([1, 166, 28, 28])
after second transition 1 : torch.Size([1, 83, 14, 14])
part1 : torch.Size([1, 42, 14, 14])
part2 : torch.Size([1, 41, 14, 14])
after bottleneck #0 : torch.Size([1, 53, 14, 14])
after bottleneck #1 : torch.Size([1, 65, 14, 14])
after bottleneck #2 : torch.Size([1, 77, 14, 14])
after bottleneck #3 : torch.Size([1, 89, 14, 14])
after bottleneck #4 : torch.Size([1, 101, 14, 14])
after bottleneck #5 : torch.Size([1, 113, 14, 14])
after bottleneck #6 : torch.Size([1, 125, 14, 14])
after bottleneck #7 : torch.Size([1, 137, 14, 14])
after bottleneck #8 : torch.Size([1, 149, 14, 14])
after bottleneck #9 : torch.Size([1, 161, 14, 14])
after bottleneck #10 : torch.Size([1, 173, 14, 14])
after bottleneck #11 : torch.Size([1, 185, 14, 14])
after bottleneck #12 : torch.Size([1, 197, 14, 14])
after bottleneck #13 : torch.Size([1, 209, 14, 14])
after bottleneck #14 : torch.Size([1, 221, 14, 14])
after bottleneck #15 : torch.Size([1, 233, 14, 14])
after bottleneck #16 : torch.Size([1, 245, 14, 14])
after bottleneck #17 : torch.Size([1, 257, 14, 14])
after bottleneck #18 : torch.Size([1, 269, 14, 14])
after bottleneck #19 : torch.Size([1, 281, 14, 14])
after bottleneck #20 : torch.Size([1, 293, 14, 14])
after bottleneck #21 : torch.Size([1, 305, 14, 14])
after bottleneck #22 : torch.Size([1, 317, 14, 14])
after bottleneck #23 : torch.Size([1, 329, 14, 14])
part2 after dense block 2 : torch.Size([1, 329, 14, 14])
part2 after first trans 2 : torch.Size([1, 263, 14, 14])
after concatenate : torch.Size([1, 305, 14, 14])
after second transition 2 : torch.Size([1, 152, 7, 7])
part1 : torch.Size([1, 76, 7, 7])
part2 : torch.Size([1, 76, 7, 7])
after bottleneck #0 : torch.Size([1, 88, 7, 7])
after bottleneck #1 : torch.Size([1, 100, 7, 7])
after bottleneck #2 : torch.Size([1, 112, 7, 7])
after bottleneck #3 : torch.Size([1, 124, 7, 7])
after bottleneck #4 : torch.Size([1, 136, 7, 7])
after bottleneck #5 : torch.Size([1, 148, 7, 7])
after bottleneck #6 : torch.Size([1, 160, 7, 7])
after bottleneck #7 : torch.Size([1, 172, 7, 7])
after bottleneck #8 : torch.Size([1, 184, 7, 7])
after bottleneck #9 : torch.Size([1, 196, 7, 7])
after bottleneck #10 : torch.Size([1, 208, 7, 7])
after bottleneck #11 : torch.Size([1, 220, 7, 7])
after bottleneck #12 : torch.Size([1, 232, 7, 7])
after bottleneck #13 : torch.Size([1, 244, 7, 7])
after bottleneck #14 : torch.Size([1, 256, 7, 7])
after bottleneck #15 : torch.Size([1, 268, 7, 7])
part2 after dense block 2 : torch.Size([1, 268, 7, 7])
part2 after first trans 2 : torch.Size([1, 214, 7, 7])
after concatenate : torch.Size([1, 290, 7, 7])
after avgpool : torch.Size([1, 290, 1, 1])
after flatten : torch.Size([1, 290])
after fc : torch.Size([1, 1000])
···
And that’s it! We have successfully learned CSPNet and implemented it on DenseNet backbone. As I’ve mentioned earlier, we can actually use the idea of CSPNet to improve the performance of any other backbone models such as ResNet or ResNeXt. So here I challenge you to implement CSPNet on these models from scratch. To be honest I cannot confirm that my implementation is 100% correct since the official GitHub repo [4] of the paper does not provide the PyTorch implementation — but that’s at least everything I understand from the manuscript. Please let me know if you find any mistake in the code or in my explanations. Thanks for reading, and see you again in my next article. Bye! Btw you can also find the code used in this article on my GitHub repo [5]. ···
[1] Chien-Yao Wang et al. CSPnet: A New Backbone That Can Enhance Learning Capability of CNN. Arxiv. https://arxiv.org/abs/1911.11929 [Accessed October 1, 2025]. [2] Gao Huang et al. Densely Connected Convolutional Networks. Arxiv. https://arxiv.org/abs/1608.06993 [Accessed September 18, 2025]. [3] Muhammad Ardi. DenseNet Paper Walkthrough: All Connected. Towards Data Science. https://towardsdatascience.com/densenet-paper-walkthrough-all-connected/ [Accessed April 26, 2026]. [4] WongKinYiu. CrossStagePartialNetworks. GitHub. https://github.com/WongKinYiu/CrossStagePartialNetworks [Accessed October 1, 2025]. [5] MuhammadArdiPutra. CSPNet. GitHub. https://github.com/MuhammadArdiPutra/medium_articles/blob/main/DenseNet.ipynb [Accessed October 1, 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 IntelligenceCnnMachine LearningNeural Network 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
Deep Learning Full detailed explanation on how to create, transform, and infer a celebrity-look-alike demo with TensorFlow on an i.MX 8M Plus NPU... Jan Werth December 9, 202015 min read
Machine Learning Every time we express ourselves, either verbally or in written text, these expressions carry a lot of information. What subjects we talk... Fredrik Olsson December 17, 202011 min read
ChatGPT Canonical and research-proven techniques to adapt large language models to domain specific tasks and the intuition of their effectiveness. Eileen Pangu March 20, 20239 min read
Machine Learning February 2026: exchange with others, documentation, and MLOps Pascal Janetzky March 2, 20265 min read
Deep Learning Extract important features from data using deep learning. Rajas Bakshi June 28, 20216 min read
Artificial Intelligence A Non-Technical Deep Dive into How AI Uses Digital Fingerprints to Organize the World’s Information. Gustavo Santos March 31, 202611 min read
Deep Learning Take a peek into the world of Automatic Text Summarization Prakhar Ganesh June 13, 20195 min read
Deep Learning Benchmark metrics for evaluation Luca Clissa December 13, 20225 min read
Machine Learning Learn various essential functions of NumPy which you might be unaware of (with code) Shantnu & Kartik September 3, 20208 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