![Revisit consent button](../images/body-df29beacd7496220cc94.svg)
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 ![](../images/body-2159bb67f3de8ad870eb.svg)
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 Search[](https://towardsdatascience.com/)Toggle Mobile Navigation
[](https://towardsdatascience.com/)
Publish AI, ML & data-science insights to a global community of data professionals.
Sign in[Submit an Article](https://write.insightmediagroup.io)
[Latest](https://towardsdatascience.com/latest/)[Editor's Picks](https://towardsdatascience.com/tag/editors-pick/)[Newsletter](https://towardsdatascience.com/tag/the-variable/)[ShipAI](https://towardsdatascience.com/shipai/)
* * *
[Write For TDS](https://towardsdatascience.com/submissions/)
[LinkedIn](https://www.linkedin.com/company/towards-data-science/)[X](https://x.com/TDataScience)Toggle Search
[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)
# CSPNet Paper Walkthrough: Just Better, No Tradeoffs
A review of the Cross-Stage Partial Network paper — and a from-scratch PyTorch implementation
[Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)
May 3, 202623 min read
![](../images/000.jpg)
Photo by [Mihály Köles](https://unsplash.com/@mihaly_koles?utm_source=medium&utm_medium=referral) 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.
···
## A Brief History of CSPNet
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](https://roadmap.sh/machine-learning) roadmap.
![](../images/001.png)
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.
### Objectives
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.
![](../images/body-589390aa42b2500aabd9.png)
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).
···
## The Detailed CSPNet Architecture
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.
![](../images/body-4ee173ccde6a609db6b1.png)
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.
### Experimental Results
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.
![](../images/body-209a358dbcfc042a5c31.png)
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.
![](../images/body-c0f1461f43ffef414fb9.png)
Figure 5. Performance improvement of DenseNet-201-Elastic and ResNeXt-50 after implementing the CSPNet mechanism [1].
### The Mathematical Expression of CSPDenseNet
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.
![](../images/body-beee74a750dfb448a8b8.png)
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ᴜ_.
![](../images/body-c8d85a5093f98fbc101b.png)
Figure 7. The mathematical expression of the forward propagation in CSPDenseNet block [1].
···
## CSPDenseNet Implementation
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.
![](../images/body-ee91dbedfab1eba3e6ec.png)
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]
```

### Bottleneck Block Implementation
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.
### Dense Block Implementation
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])
```

### First Transition
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])
```

### Second Transition
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])
```

### The CSPDenseNet Model
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])
```

···
## Ending
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].
···
## References
[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](https://medium.com/u/9801a58700ac). 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](https://github.com/MuhammadArdiPutra/medium_articles/blob/main/Deep%20Learning%20From%20Scratch/CSPNet.ipynb) [Accessed October 1, 2025].
## Register Now to Continue Reading
### Unlock Towards Data Science with a Free Account
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
* * *
![](../images/body-59e1c17daab1fcb582c5.jpeg)
Written By
## [Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)
Deep learning storyteller
[See all from Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)
[Deep Dives](https://towardsdatascience.com/tag/deep-dives/)[Artificial Intelligence](https://towardsdatascience.com/tag/artificial-intelligence/)[Cnn](https://towardsdatascience.com/tag/cnn/)[Machine Learning](https://towardsdatascience.com/tag/machine-learning/)[Neural Network](https://towardsdatascience.com/tag/neural-network/)
Share This Article
  * [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Ftowardsdatascience.com%2Fcspnet-paper-walkthrough-just-better-no-tradeoff%2F&title=CSPNet%20Paper%20Walkthrough%3A%20Just%20Better%2C%20No%20Tradeoffs)
  * [](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Ftowardsdatascience.com%2Fcspnet-paper-walkthrough-just-better-no-tradeoff%2F&title=CSPNet%20Paper%20Walkthrough%3A%20Just%20Better%2C%20No%20Tradeoffs)
  * [](https://x.com/share?url=https%3A%2F%2Ftowardsdatascience.com%2Fcspnet-paper-walkthrough-just-better-no-tradeoff%2F&text=CSPNet%20Paper%20Walkthrough%3A%20Just%20Better%2C%20No%20Tradeoffs)


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](https://towardsdatascience.com/submissions/)
## Related Articles
  * [![](../images/002.jpeg)](https://towardsdatascience.com/how-to-create-a-celebrity-look-alike-demo-and-run-inference-on-an-npu-1e4e9de660cc/)
## [How to create a celebrity-look-alike demo and run inference on an NPU](https://towardsdatascience.com/how-to-create-a-celebrity-look-alike-demo-and-run-inference-on-an-npu-1e4e9de660cc/)
[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/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](https://towardsdatascience.com/author/janwerth/)
December 9, 202015 min read
  * [![](../images/003.jpeg)](https://towardsdatascience.com/nlp-gaining-insights-from-text-reviews-94ef955c58c0/)
## [NLP: Gaining insights from text reviews](https://towardsdatascience.com/nlp-gaining-insights-from-text-reviews-94ef955c58c0/)
[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/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](https://towardsdatascience.com/author/fredrik-olsson/)
December 17, 202011 min read
  * [![](../images/004.jpeg)](https://towardsdatascience.com/how-to-use-large-language-models-llm-in-your-own-domains-b4dff2d08464/)
## [How to Use Large Language Models (LLM) in Your Own Domains](https://towardsdatascience.com/how-to-use-large-language-models-llm-in-your-own-domains-b4dff2d08464/)
[ChatGPT](https://towardsdatascience.com/category/artificial-intelligence/chatgpt/)
Canonical and research-proven techniques to adapt large language models to domain specific tasks and the intuition of their effectiveness.
[Eileen Pangu](https://towardsdatascience.com/author/eileen-code4fun/)
March 20, 20239 min read
  * [![](../images/005.jpg)](https://towardsdatascience.com/the-machine-learning-lessons-ive-learned-this-month-4/)
## [The Machine Learning Lessons I’ve Learned This Month](https://towardsdatascience.com/the-machine-learning-lessons-ive-learned-this-month-4/)
[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)
February 2026: exchange with others, documentation, and MLOps
[Pascal Janetzky](https://towardsdatascience.com/author/pascaljanetzky/)
March 2, 20265 min read
  * [![](../images/006.jpg)](https://towardsdatascience.com/stacked-autoencoders-f0a4391ae282/)
## [Stacked Autoencoders.](https://towardsdatascience.com/stacked-autoencoders-f0a4391ae282/)
[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)
Extract important features from data using deep learning.
[Rajas Bakshi](https://towardsdatascience.com/author/rajas-bakshi/)
June 28, 20216 min read
  * [![](../images/007.png)](https://towardsdatascience.com/the-map-of-meaning-how-embedding-models-understand-human-language/)
## [The Map of Meaning: How Embedding Models “Understand” Human Language](https://towardsdatascience.com/the-map-of-meaning-how-embedding-models-understand-human-language/)
[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)
A Non-Technical Deep Dive into How AI Uses Digital Fingerprints to Organize the World’s Information.
[Gustavo Santos](https://towardsdatascience.com/author/gustavorsantos/)
March 31, 202611 min read
  * [![](../images/008.png)](https://towardsdatascience.com/automatic-text-summarization-simplified-3b7c10c4093a/)
## [Automatic Text Summarization : Simplified](https://towardsdatascience.com/automatic-text-summarization-simplified-3b7c10c4093a/)
[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)
Take a peek into the world of Automatic Text Summarization
[Prakhar Ganesh](https://towardsdatascience.com/author/prakhargannu/)
June 13, 20195 min read
  * [![](../images/009.png)](https://towardsdatascience.com/fluorescent-neuronal-cells-dataset-part-iii-287c2a4f1a22/)
## [Fluorescent Neuronal Cells dataset - part III](https://towardsdatascience.com/fluorescent-neuronal-cells-dataset-part-iii-287c2a4f1a22/)
[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)
Benchmark metrics for evaluation
[Luca Clissa](https://towardsdatascience.com/author/luca.clissa/)
December 13, 20225 min read
  * [![](../images/010.png)](https://towardsdatascience.com/numpy-cheatsheet-for-essential-functions-python-2e7d8618d688/)
## [Cheatsheet for NumPy: Essential and Lesser-Known Functions](https://towardsdatascience.com/numpy-cheatsheet-for-essential-functions-python-2e7d8618d688/)
[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)
Learn various essential functions of NumPy which you might be unaware of (with code)
[Shantnu &amp; Kartik](https://towardsdatascience.com/author/beginningofthefuture/)
September 3, 20208 min read


  * [YouTube](https://www.youtube.com/c/TowardsDataScience)
  * [X](https://x.com/TDataScience)
  * [LinkedIn](https://www.linkedin.com/company/towards-data-science/)
  * [Threads](https://www.threads.net/@towardsdatascience)
  * [Bluesky](https://bsky.app/profile/towardsdatascience.com)


[](https://towardsdatascience.com/)
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 Newsletter[Write For TDS](https://towardsdatascience.com/submissions/)•[About](https://towardsdatascience.com/about-towards-data-science-d691af11cc2f/)•[Advertise](https://contact.towardsdatascience.com/advertise-with-towards-data-science)•[Privacy Policy](https://towardsdatascience.com/privacy-policy/)•[Terms of Use](https://towardsdatascience.com/website-terms-of-use/)
© Insight Media Group, LLC 2026•[The New Stack](https://thenewstack.io)•[roadmap.sh](https://roadmap.sh)•[Creator Network](https://insightmediagroup.io/creator-network)
