{
  "source_file": "0930",
  "source_platform": "Medium",
  "author": "Muhammad Ardi Putra",
  "title_listed": "YOLOv3 Paper Walkthrough: Even Better, But Not That Much",
  "keywords": "",
  "domain": "图像处理",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/",
  "publish_date": "2026-03-02",
  "corpus_stage": "",
  "notes": "",
  "id": "0d9726dbcb9e",
  "collected_at": "2026-10-02T02:26:59",
  "title": "YOLOv3 Paper Walkthrough: Even Better, But Not That Much | Towards Data Science",
  "body_markdown": "![Revisit consent button](../images/body-df29beacd7496220cc94.svg)\nWe value your privacy\nWe 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.\nCustomise Reject All Accept All\nCustomise Consent Preferences ![](../images/body-2159bb67f3de8ad870eb.svg)\nWe use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.\nThe 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\nNecessaryAlways Active\nNecessary 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.\n  * Cookie\nBCTempID\n  * Duration\n10 minutes\n  * Description\nNo description available.\n\n\n  * Cookie\n__cf_bm\n  * Duration\n1 hour\n  * Description\nThis cookie, set by Cloudflare, is used to support Cloudflare Bot Management. \n\n\n  * Cookie\nAWSALBCORS\n  * Duration\n7 days\n  * Description\nAmazon Web Services set this cookie for load balancing.\n\n\n  * Cookie\n_cfuvid\n  * Duration\nsession\n  * Description\nCloudflare sets this cookie to track users across sessions to optimize user experience by maintaining session consistency and providing personalized services\n\n\n  * Cookie\nli_gc\n  * Duration\n6 months\n  * Description\nLinkedin set this cookie for storing visitor's consent regarding using cookies for non-essential purposes.\n\n\n  * Cookie\n__hssrc\n  * Duration\nsession\n  * Description\nThis 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.\n\n\n  * Cookie\n__hssc\n  * Duration\n1 hour\n  * Description\nHubSpot sets this cookie to keep track of sessions and to determine if HubSpot should increment the session number and timestamps in the __hstc cookie.\n\n\n  * Cookie\nwpEmojiSettingsSupports\n  * Duration\nsession\n  * Description\nWordPress 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.\n\n\n  * Cookie\nBCSessionID\n  * Duration\n1 year 1 month 4 days\n  * Description\nBlueconic sets this cookie as a unique identifier for the BlueConic profile.\n\n\n  * Cookie\n_octo\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nlogged_in\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\n__Secure-YEC\n  * Duration\npast\n  * Description\nYouTube sets this cookie to stores the user's video player preferences using embedded YouTube video\n\n\n  * Cookie\n__eoi\n  * Duration\n6 months\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nAWSALBTGCORS\n  * Duration\n7 days\n  * Description\nNo description available.\n\n\n  * Cookie\nlogin-status-p\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nAWSALBTG\n  * Duration\n7 days\n  * Description\nNo description available.\n\n\n  * Cookie\ncsrf_token\n  * Duration\nsession\n  * Description\nNo description available.\n\n\n  * Cookie\ntoken_v2\n  * Duration\n1 day\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nD\n  * Duration\n1 year\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nPHPSESSID\n  * Duration\nsession\n  * Description\nThis 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.\n\n\n  * Cookie\nVISITOR_PRIVACY_METADATA\n  * Duration\n6 months\n  * Description\nYouTube sets this cookie to store the user's cookie consent state for the current domain. \n\n\n  * Cookie\ncookietest\n  * Duration\nsession\n  * Description\nThe cookietest cookie is typically used to determine whether the user's browser accepts cookies, essential for website functionality and user experience.\n\n\n  * Cookie\n__Host-airtable-session\n  * Duration\n1 year\n  * Description\nThis cookie is used to enable us to integrate the services of Airtable.\n\n\n  * Cookie\n__Host-airtable-session.sig\n  * Duration\n1 year\n  * Description\nThis cookie is used to enable us to integrate the services of Airtable.\n\n\n  * Cookie\nm\n  * Duration\n1 year 1 month 4 days\n  * Description\nStripe sets this cookie for fraud prevention purposes. It identifies the device used to access the website, allowing the website to be formatted accordingly.\n\n\n  * Cookie\nBIGipServer*\n  * Duration\nsession\n  * Description\nMarketo 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. \n\n\n  * Cookie\n__cfruid\n  * Duration\nsession\n  * Description\nCloudflare sets this cookie to identify trusted web traffic.\n\n\n  * Cookie\n_GRECAPTCHA\n  * Duration\n6 months\n  * Description\nGoogle Recaptcha service sets this cookie to identify bots to protect the website against malicious spam attacks.\n\n\n  * Cookie\n__Secure-YNID\n  * Duration\n6 months\n  * Description\nGoogle cookie used to protect user security and prevent fraud, especially during the login process.\n\n\n  * Cookie\ncookieyes-consent\n  * Duration\n1 year\n  * Description\nCookieYes 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.\n\n\nFunctional\nFunctional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.\n  * Cookie\nlidc\n  * Duration\n1 day\n  * Description\nLinkedIn sets the lidc cookie to facilitate data center selection.\n\n\n  * Cookie\nbrw\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nbrwConsent\n  * Duration\n5 minutes\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nWMF-Uniq\n  * Duration\n1 year\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nloom_anon_comment\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nloom_referral_video\n  * Duration\nsession\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nVISITOR_INFO1_LIVE\n  * Duration\n6 months\n  * Description\nA cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface.\n\n\n  * Cookie\nyt-remote-connected-devices\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to store the user's video preferences using embedded YouTube videos.\n\n\n  * Cookie\nytidb::LAST_RESULT_ENTRY_KEY\n  * Duration\nNever Expires\n  * Description\nThe 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.\n\n\n  * Cookie\nyt-remote-device-id\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to store the user's video preferences using embedded YouTube videos.\n\n\n  * Cookie\nyt-remote-session-name\n  * Duration\nsession\n  * Description\nThe yt-remote-session-name cookie is used by YouTube to store the user's video player preferences using embedded YouTube video.\n\n\n  * Cookie\nyt-remote-fast-check-period\n  * Duration\nsession\n  * Description\nThe yt-remote-fast-check-period cookie is used by YouTube to store the user's video player preferences for embedded YouTube videos.\n\n\n  * Cookie\nyt-remote-session-app\n  * Duration\nsession\n  * Description\nThe yt-remote-session-app cookie is used by YouTube to store user preferences and information about the interface of the embedded YouTube video player.\n\n\n  * Cookie\nyt-remote-cast-available\n  * Duration\nsession\n  * Description\nThe yt-remote-cast-available cookie is used to store the user's preferences regarding whether casting is available on their YouTube video player.\n\n\n  * Cookie\nyt-remote-cast-installed\n  * Duration\nsession\n  * Description\nThe yt-remote-cast-installed cookie is used to store the user's video player preferences using embedded YouTube video.\n\n\n  * Cookie\ncp_session\n  * Duration\n3 months\n  * Description\nCodepen sets this cookie for Help systems found in the website.\n\n\n  * Cookie\nloid\n  * Duration\n1 year 1 month 4 days\n  * Description\nThis cookie is set by the Reddit. The cookie enables the sharing of content from the website onto the social media platform.\n\n\nAnalytics\nAnalytical 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.\n  * Cookie\n__hstc\n  * Duration\n6 months\n  * Description\nHubspot 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).\n\n\n  * Cookie\nhubspotutk\n  * Duration\n6 months\n  * Description\nHubSpot 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.\n\n\n  * Cookie\n_ga\n  * Duration\n1 year 1 month 4 days\n  * Description\nGoogle 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.\n\n\n  * Cookie\n_ga_*\n  * Duration\n1 year 1 month 4 days\n  * Description\nGoogle Analytics sets this cookie to store and count page views.\n\n\n  * Cookie\n__Host-psifi.analyticsTrace\n  * Duration\n6 hours\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\n__Host-psifi.analyticsTraceV2\n  * Duration\n6 hours\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\n_gh_sess\n  * Duration\nsession\n  * Description\nGitHub sets this cookie for temporary application and framework state between pages like what step the user is on in a multiple step form.\n\n\n  * Cookie\nYSC\n  * Duration\nsession\n  * Description\nYSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages.\n\n\n  * Cookie\najs_anonymous_id\n  * Duration\n1 year\n  * Description\nThis cookie is set by Segment to count the number of people who visit a certain site by tracking if they have visited before.\n\n\n  * Cookie\nvuid\n  * Duration\n1 year 1 month 4 days\n  * Description\nVimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website. \n\n\nPerformance\nPerformance 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.\n  * Cookie\nAWSALB\n  * Duration\n7 days\n  * Description\nAWSALB is an application load balancer cookie set by Amazon Web Services to map the session to the target.\n\n\n  * Cookie\nacq\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nacq.sig\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nptc\n  * Duration\n2 years\n  * Description\nNo description available.\n\n\nAdvertisement\nAdvertisement 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.\n  * Cookie\nmuc_ads\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to collect user behaviour and interaction data to optimize the website.\n\n\n  * Cookie\nguest_id_marketing\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to identify and track the website visitor.\n\n\n  * Cookie\nguest_id_ads\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to identify and track the website visitor.\n\n\n  * Cookie\npersonalization_id\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter 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.\n\n\n  * Cookie\nguest_id\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter 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.\n\n\n  * Cookie\nbcookie\n  * Duration\n1 year\n  * Description\nLinkedIn sets this cookie from LinkedIn share buttons and ad tags to recognize browser IDs.\n\n\n  * Cookie\n__Secure-ROLLOUT_TOKEN\n  * Duration\n6 months\n  * Description\nYouTube 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.\n\n\n  * Cookie\nyt.innertube::nextId\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.\n\n\n  * Cookie\nyt.innertube::requests\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.\n\n\n  * Cookie\nsession_tracker\n  * Duration\nsession\n  * Description\nThis cookie is set by the Reddit. This cookie is used to identify trusted web traffic. It also helps in adverstising on the website.\n\n\n  * Cookie\nedgebucket\n  * Duration\nsession\n  * Description\nReddit sets this cookie to save the information about a log-on Reddit user, for the purpose of advertisement recommendations and updating the content.\n\n\n  * Cookie\ndid\n  * Duration\n1 year\n  * Description\nArbor sets this cookie to show targeted ads to site visitors.This cookie expires after 2 months or 1 year.\n\n\nUncategorised\nOther uncategorised cookies are those that are being analysed and have not been classified into a category as yet.\nNo cookies to display.\nReject All Save My Preferences Accept All\nToggle Search[](https://towardsdatascience.com/)Toggle Mobile Navigation\n[](https://towardsdatascience.com/)\nPublish AI, ML & data-science insights to a global community of data professionals.\nSign in[Submit an Article](https://write.insightmediagroup.io)\n[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/)\n* * *\n[Write For TDS](https://towardsdatascience.com/submissions/)\n[LinkedIn](https://www.linkedin.com/company/towards-data-science/)[X](https://x.com/TDataScience)Toggle Search\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\n# YOLOv3 Paper Walkthrough: Even Better, But Not That Much\nA PyTorch implementation on the YOLOv3 architecture from scratch\n[Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nMarch 2, 202623 min read\n![](../images/000.jpg)\n[Photo](https://unsplash.com/photos/pile-of-brown-wooden-blocks-L0oJ4Dlfyuo) by [Volodymyr Hryshchenko](https://unsplash.com/@lunarts?utm_source=medium&utm_medium=referral) on Unsplash\nYOLOv2, which used to be the state-of-the-art object detection algorithm, looked to become obsolete thanks to the appearance of other methods like SSD (Single Shot Multibox Detector), DSSD (Deconvolutional Single Shot Detector), and RetinaNet. Finally, after two years since the introduction of YOLOv2, the authors decided to improve the algorithm where they eventually came up with the next YOLO version reported in a paper titled “ _YOLOv3: An Incremental Improvement_ ” [1]. As the title suggests, there were indeed not many things the authors improved upon YOLOv2 in terms of the underlying algorithm. But hey, when it comes to performance, it actually looks pretty impressive.\nIn this article I am going to talk about the modifications the authors made to YOLOv2 to create YOLOv3 and how to implement the model architecture from scratch with PyTorch. I highly recommend you reading my previous article about YOLOv1 [2, 3] and YOLOv2 [4] before this one, unless you already got a strong foundation in how these two earlier versions of YOLO work.\n···\nLearn this step by step with the interactive [AI Engineer](https://roadmap.sh/ai-engineer) roadmap.\n## What Makes YOLOv3 Better Than YOLOv2\n### The Vanilla Darknet-53\nThe modification the authors made was mainly related to the architecture, in which they proposed a backbone model referred to as Darknet-53. See the detailed structure of this network in Figure 1. As the name suggests, this model is an improvement upon the Darknet-19 used in YOLOv2. If you count the number of layers in Darknet-53, you will find that this network consists of 52 convolution layers and a single fully-connected layer at the end. Keep in mind that later when we implement it on YOLOv3, we will feed it with images of size 416×416 rather than 256×256 as written in the figure.\n![](../images/001.png)\nFigure 1. The vanilla Darknet-53 architecture [1].\nIf you’re familiar with Darknet-19, you must remember that it performs spatial downsmapling using maxpooling operations after every stack of several convolution layers. In Darknet-53, authors replaced these pooling operations with convolutions of stride 2. This was essentially done because maxpooling layer completely ignores non-maximum numbers, causing us to lose a lot of information contained in the lower intensity pixels. We can actually use average-pooling as an alternative, but in theory, this approach won’t be optimal either because all pixels within the small region are weighted the same. So as a solution, authors decided to use convolution layer with a stride of 2, which by doing so the model will be able to reduce image resolution while capturing spatial information with specific weightings. You can see the illustration for this in Figure 2 below.\n![](../images/body-006360f2b17cad23f9f9.png)\nFigure 2. How maxpooling, average-pooling and convolution with stride 2 differ from each other [5].\nNext, the backbone of this YOLO version is now equipped with residual blocks which the idea is originated from ResNet. One thing that I want to emphasize regarding our implementation is the activation function within the residual block. You can see in Figure 3 below that according to the original ResNet paper, the second activation function is placed after the element-wise summation. However, based on the other tutorials that I read [6, 7], I found that in the case of YOLOv3 the second activation function is placed right after the weight layer instead (before summation). So later in the implementation, I decided to follow the guide in these tutorials since the YOLOv3 paper does not give any explanations about it.\n![](../images/body-3bfe51ebcda6bc275751.png)\nFigure 3. A residual block [8]\n### Darknet-53 With Detection Heads\nKeep in mind that the architecture in Figure 1 is only meant for classification. Thus, we need to replace everything after the last residual block if we want to make it compatible for detection tasks. Again, the original YOLOv3 paper also does not provide the detailed implementation guide, hence I decided to search for it and eventually got one from the paper referenced as [9]. I redraw the illustration from that paper to make the architecture looks clearer as shown in Figure 4 below.\n![](../images/body-e18016fc7230659b5e51.png)\nFigure 4. The YOLOv3 architecture [5].\nThere are actually lots of things to explain regarding the above architecture. Now let’s start from the part I refer to as the _detection heads_. Different from the previous YOLO versions which relied on a single head, here in YOLOv3 we have 2 additional heads. Thus, we will later have 3 prediction tensors for every single input image. These three detection heads have different specializations: the leftmost head (13×13) is the one responsible to detect large objects, the middle head (26×26) is for detecting medium-sized objects, and the one on the right (52×52) is used to detect objects of small size. We can think of the 52×52 tensor as the feature map that contains the detailed representation of an image, hence is suitable to detect small objects. Conversely, the 13×13 prediction tensor is meant to detect large objects because of its lower spatial resolution which is effective at capturing the general shape of an object.\nStill with the detection head, you can also see in Figure 4 that the three prediction tensors have 255 channels. To understand where this number comes from, we first need to know that each detection head has 3 prior boxes. Following the rule given in YOLOv2, each of these prior boxes is configured such that it can predict its own object category independently. With this mechanism, the feature vector of each grid cell can be obtained by computing B×(5+C), where _B_ is the number of prior boxes, _C_ is the number of object classes, and 5 is the _xywh_ and the bounding box confidence (a.k.a. _objectness_). In the case of YOLOv3, each detection head has 3 prior boxes and 80 classes, assuming that we train it on 80-class COCO dataset. By plugging these numbers to the formula, we obtain 3×(5+80)=255 prediction values for a single grid cell.\nIn fact, using multi-head mechanism like this allows the model to detect more objects as compared to the earlier YOLO versions. Previously in YOLOv1, since an image is divided into 7×7 grid cells and each of those can predict 2 bounding boxes, hence there are 98 objects possible to be detected. Meanwhile in YOLOv2, an image is divided into 13×13 grid cells in which a single cell is capable of generating 5 bounding boxes, making YOLOv2 able to detect up to 845 objects within a single image. This essentially allows YOLOv2 to have a better recall than YOLOv1. In theory, YOLOv3 is potentially able to achieve an even higher recall, especially when tested on an image that contains a lot of objects thanks to the larger number of possible detections. We can calculate the number of maximum bounding boxes for a single image in YOLOv3 by computing (13×13×3) + (26×26×3) + (52×52×3) = 507 + 2028 + 8112 = 10647, where 13×13, 26×26, and 52×25 are the number of grid cells within each prediction tensor, while 3 is the number of prior boxes a single grid cell has.\nWe can also see in Figure 4 that there are two concatenation steps incorporated in the network, i.e., between the original Darknet-53 architecture and the detection heads. The objective of these steps is to combine information from the deeper layer with the one from the shallower layer. Combining information from different depths like this is important because when it comes to detecting smaller objects, we do need both a detailed spatial information (contained in the shallower layer) and a better semantic information (contained in the deeper layer). Keep in mind that the feature map from the deeper layer has a smaller spatial dimension, hence we need to expand it before actually doing the concatenation. This is essentially the reason that we need to place an _upsampling_ layer right before we do the concatenation.\n### Multi-Label Classification\nApart from the architecture, the authors also changed the class labeling mechanism. Instead of using a standard multiclass classification paradigm, they proposed to use the so-called multilabel classification. If you’re not yet familiar with it, this is basically a method where an image can be assigned multiple labels at once. Take a look at Figure 5 below to better understand this idea. In this example, the image on the left belongs to the class _person_ , _athlete_ , _runner_ , and _man_ simultaneously. Later on, YOLOv3 is also expected to be able to make multiple class predictions on the same detected object.\n![](../images/body-da077e395b6a7ad78270.png)\nFigure 5. Image with multiple labels [10].\nIn order for the model to predict multiple labels, we need to treat each class prediction output as an independent binary classifier. Look at Figure 6 below to see how multiclass classification differs from multilabel classification. The illustration on the left is a condition when we use a typical multiclass classification mechanism. Here you can see that the probabilities of all classes sum to 1 thanks to the nature of the softmax activation function within the output layer. In this example, since the class _camel_ is predicted with the highest probability, then the final prediction would be _camel_ regardless of how high the prediction confidence of the other classes is.\nOn the other hand, if we use multilabel classification, there is a possibility that the sum of all class prediction probabilities is greater than 1 because we use sigmoid activation function which by nature does not restrict the sum of all prediction confidence scores to 1. Thanks to this reason, later in the implementation we can simply apply a specific threshold to consider a class predicted. In the example below, if we assume that the threshold is 0.7, then the image will be predicted as both _cactus_ and _camel_.\n![](../images/body-ee0eb113e58f709b6514.png)\nFigure 6. Multiclass vs multilabel classification [10].\n### Modified Loss Function\nAnother modification the authors made was related to the loss function. Now look at the loss function of YOLOv1 in Figure 7 below. As a refresher, the 1st and 2nd rows are responsible to compute the bounding box loss, the 3rd and 4th rows are for the objectness confidence loss, and the 5th row is for computing the classification loss. Remember that in YOLOv1 the authors used SSE (Sum of Squared Errors) in all these five rows to make things simple.\n![](../images/body-cc59346bd10797c0b62d.png)\nFigure 7. The loss function of YOLOv1 [5, 11].\nIn YOLOv3, the authors decided to replace the objectness loss (the 3rd and 4th rows) with _binary cross entropy_ , considering that the predictions corresponding to this part is only either 1 or 0, i.e., whether there is an object midpoint or not. Thus, it makes more sense to treat this as a binary classification rather than a regression problem.\nBinary cross entropy will also be used in the classification loss (5th row). This is essentially because we use multilabel classification mechanism we discussed earlier, where we treat each of the output neuron as an independent binary classifier. Remember that if we were to perform a standard classification task, we typically need to set the loss function to _categorical cross entropy_ instead.\nNow below is what the loss function looks like after we replace the SSE with binary cross entropy for the objectness (green) and the multilabel classification (blue) parts. Note that this equation is created based on the YouTube tutorial I watched given at reference number [12] because, again, the authors do not explicitly provide the final loss function in the paper.\n![](../images/body-f0154f2ea1e0bd77caa9.png)\nFigure 8. The loss function of YOLOv3 [5, 12].\n### Some Experimental Results\nWith all the modifications discussed above, the authors found that the improvement in performance is pretty impressive. The first experimental result I want to show you is related to the performance of the backbone model in classifying images on ImageNet dataset. You can see in Figure 9 below that the improvement from Darknet-19 (YOLOv2) to Darknet-53 (YOLOv3) is quite significant in terms of both the top-1 accuracy (74.1 to 77.2) and the top-5 accuracy (91.8 to 93.8). It is necessary to acknowledge that ResNet-101 and ResNet-152 indeed also perform as good as Darknet-53 in accuracy, but if we compare the FPS (measured on Nvidia Titan X), we can see that Darknet-53 is a lot faster than both ResNet variants.\n![](../images/body-79833eecc099e51685d3.png)\nFigure 9. Performance of different backbones on ImageNet classification dataset [1].\nThe similar behavior could also be observed on object detection task, where it is seen in Figure 10 that all YOLOv3 variants successfully obtained the fastest computation time among all other methods despite not having the best accuracy. You can see in the figure that the largest YOLOv3 variant is nearly 4 times faster than the largest RetinaNet variant (51 ms vs 198 ms). Moreover, the largest YOLOv3 variant itself already surpasses the mAP of the smallest RetinaNet variant (33.0 vs 32.5) while still having a faster inference time (51 ms vs 73 ms). These experimental results essentially prove that YOLOv3 became the state-of-the-art object detection model in terms of computational speed at that moment.\n![](../images/body-19349432f30d351e9ca2.png)\nFigure 10. Performance of YOLOv3 compared to other object detection methods [10].\n···\n## YOLOv3 Architecture Implementation\nAs we have already discussed pretty much everything about the theory behind YOLOv3, we can now start implementing the architecture from scratch. In Codeblock 1 below, I import the `torch` module and its `nn` submodule. Here I also initialize the `NUM_PRIORS` and `NUM_CLASS` variables, in which these two correspond to the number of prior boxes within each grid cell and the number of object classes in the dataset, respectively.\npython\n\n```\n# Codeblock 1\nimport torch\nimport torch.nn as nn\n\nNUM_PRIORS = 3\nNUM_CLASS  = 80\n```\n\n### Convolutional Block Implementation\nWhat I am going to implement first is the main building block of the network, which I refer to as the `Convolutional` block as seen in Codeblock 2. The structure of this block is quite a bit the same as the one used in YOLOv2, where it follows the _Conv-BN-Leaky ReLU_ pattern. When we use this kind of structure, don’t forget to set the `bias` parameter of the conv layer to `False` (at line `#(1)`) because using bias term is somewhat useless if we directly place a batch normalization layer right after it. Here I also configure the padding of the conv layer such that it will automatically set to 1 whenever the kernel size is 3×3 or 0 whenever we use 1×1 kernel (`#(2)`). Next, as the `conv`, `bn`, and `leaky_relu` have been initialized, we can simply connect them all using the code written inside the `forward()` method (`#(3)`).\npython\n\n```\n# Codeblock 2\nclass Convolutional(nn.Module):\n    def __init__(self, \n                 in_channels, \n                 out_channels, \n                 kernel_size, \n                 stride=1):\n        super().__init__()\n        \n        self.conv = nn.Conv2d(in_channels=in_channels,\n                              out_channels=out_channels, \n                              kernel_size=kernel_size, \n                              stride=stride,\n                              bias=False,                            #(1)\n                              padding=1 if kernel_size==3 else 0)    #(2)\n        \n        self.bn = nn.BatchNorm2d(num_features=out_channels)\n        \n        self.leaky_relu = nn.LeakyReLU(negative_slope=0.1)\n        \n    def forward(self, x):        #(3)\n        print(f'original\\t: {x.size()}')\n\n        x = self.conv(x)\n        print(f'after conv\\t: {x.size()}')\n        \n        x = self.bn(x)\n        print(f'after bn\\t: {x.size()}')\n        \n        x = self.leaky_relu(x)\n        print(f'after leaky relu: {x.size()}')\n        \n        return x\n```\n\nWe just want to ensure that our main building block is working properly, we will test it by simulating the very first `Convolutional` block in Figure 1. Remember that since YOLOv3 takes an image of size 416×416 as the input, here in Codeblock 3 I create a dummy tensor of that shape to simulate an image passed through that layer. Also, note that here I leave the stride to the default (1) because at this point we don’t want to perform spatial downsampling.\npython\n\n```\n# Codeblock 3\nconvolutional = Convolutional(in_channels=3,\n                              out_channels=32,\n                              kernel_size=3)\n\nx = torch.randn(1, 3, 416, 416)\nout = convolutional(x)\n```\n\nmarkup\n\n```\n# Codeblock 3 Outputoriginal         : torch.Size([1, 3, 416, 416])after conv       : torch.Size([1, 32, 416, 416])after bn         : torch.Size([1, 32, 416, 416])after leaky relu : torch.Size([1, 32, 416, 416])\n```\n\nNow let’s test our `Convolutional` block again, but this time I’ll set the stride to 2 to simulate the second convolutional block in the architecture. We can see in the output below that the spatial dimension halves from 416×416 to 208×208, indicating that this approach is a valid replacement for the maxpooling layers we previously had in YOLOv1 and YOLOv2.\npython\n\n```\n# Codeblock 4\nconvolutional = Convolutional(in_channels=32,\n                              out_channels=64,\n                              kernel_size=3, \n                              stride=2)\n\nx = torch.randn(1, 32, 416, 416)\nout = convolutional(x)\n```\n\nmarkup\n\n```\n# Codeblock 4 Outputoriginal          : torch.Size([1, 32, 416, 416])after conv        : torch.Size([1, 64, 208, 208])after bn          : torch.Size([1, 64, 208, 208])after leaky relu  : torch.Size([1, 64, 208, 208])\n```\n\n### Residual Block Implementation\nAs the `Convolutional` block is done, what I am going to do now is to implement the next building block: `Residual`. This block generally follows the structure I displayed back in Figure 3, where it consists of a residual connection that skips through two `Convolutional` blocks. Take a look at the Codeblock 5 below to see how I implement it.\nThe two convolution layers themselves follow the pattern in Figure 1, where the first `Convolutional` halves the number of channels (`#(1)`) which will then be doubled again by the second `Convolutional` (`#(3)`). Here you also need to note that the first convolution uses 1×1 kernel (`#(2)`) whereas the second one uses 3×3 (`#(4)`). Next, what we do inside the `forward()` method is simply connecting the two convolutions sequentially, which the final output is summed with the original input tensor (`#(5)`) before being returned.\npython\n\n```\n# Codeblock 5\nclass Residual(nn.Module):\n    def __init__(self, num_channels):\n        super().__init__()\n        self.conv0 = Convolutional(in_channels=num_channels, \n                                   out_channels=num_channels//2,   #(1)\n                                   kernel_size=1,       #(2)\n                                   stride=1)\n        \n        self.conv1 = Convolutional(in_channels=num_channels//2,\n                                   out_channels=num_channels,      #(3)\n                                   kernel_size=3,       #(4)\n                                   stride=1)\n        \n    def forward(self, x):\n        original = x.clone()\n        print(f'original\\t: {x.size()}')\n        \n        x = self.conv0(x)\n        print(f'after conv0\\t: {x.size()}')\n        \n        x = self.conv1(x)\n        print(f'after conv1\\t: {x.size()}')\n        \n        x = x + original      #(5)\n        print(f'after summation\\t: {x.size()}')\n        \n        return x\n```\n\nWe will now test the `Residual` block we just created using the Codeblock 6 below. Here I set the `num_channels` parameter to 64 because I want to simulate the very first residual block in the Darknet-53 architecture (see Figure 1).\npython\n\n```\n# Codeblock 6\nresidual = Residual(num_channels=64)\n\nx = torch.randn(1, 64, 208, 208)\nout = residual(x)\n```\n\nmarkup\n\n```\n# Codeblock 6 Outputoriginal        : torch.Size([1, 64, 208, 208])after conv0     : torch.Size([1, 32, 208, 208])after conv1     : torch.Size([1, 64, 208, 208])after summation : torch.Size([1, 64, 208, 208])\n```\n\nIf you take a closer look at the above output, you will notice that the shape of the input and output tensors are exactly the same. This essentially allows us to repeat multiple residual blocks easily. In the Codeblock 7 below I try to stack 4 residual blocks and pass a tensor through it, simulating the very last stack of residual blocks in the architecture.\npython\n\n```\n# Codeblock 7\nresiduals = nn.ModuleList([])\nfor _ in range(4):\n    residual = Residual(num_channels=1024)\n    residuals.append(residual)\n    \nx = torch.randn(1, 1024, 13, 13)\n\nfor i in range(len(residuals)):\n    x = residuals[i](x)\n    print(f'after residuals #{i}\\t: {x.size()}')\n```\n\nmarkup\n\n```\n# Codeblock 7 Outputafter residuals #0 : torch.Size([1, 1024, 13, 13])after residuals #1 : torch.Size([1, 1024, 13, 13])after residuals #2 : torch.Size([1, 1024, 13, 13])after residuals #3 : torch.Size([1, 1024, 13, 13])\n```\n\n### Darknet-53 Implementation\nUsing the `Convolutional` and `Residual` building blocks we created earlier, we can now actually construct the Darknet-53 model. Everything I initialize inside the `__init__()` method below is based on the architecture in Figure 1. However, remember that we need to stop at the last residual block since we don’t need the global average pooling and the fully-connected layers. Not only that, at the lines marked with `#(1)` and `#(2)` I store the intermediate feature maps in separate variables (`branch0` and `branch1`). We will later return these feature maps alongside the output from the main flow (`x`) (`#(3)`) to implement the branches that flow into the three detection heads.\npython\n\n```\n# Codeblock 8\nclass Darknet53(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.convolutional0 = Convolutional(in_channels=3,\n                                            out_channels=32,\n                                            kernel_size=3)\n        \n        self.convolutional1 = Convolutional(in_channels=32,\n                                            out_channels=64,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals0 = nn.ModuleList([Residual(num_channels=64) for _ in range(1)])\n        \n        self.convolutional2 = Convolutional(in_channels=64,\n                                            out_channels=128,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals1 = nn.ModuleList([Residual(num_channels=128) for _ in range(2)])\n        \n        self.convolutional3 = Convolutional(in_channels=128,\n                                            out_channels=256,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals2 = nn.ModuleList([Residual(num_channels=256) for _ in range(8)])\n        \n        self.convolutional4 = Convolutional(in_channels=256,\n                                            out_channels=512,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals3 = nn.ModuleList([Residual(num_channels=512) for _ in range(8)])\n        \n        self.convolutional5 = Convolutional(in_channels=512,\n                                            out_channels=1024,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals4 = nn.ModuleList([Residual(num_channels=1024) for _ in range(4)])\n        \n    def forward(self, x):\n        print(f'original\\t\\t: {x.size()}\\n')\n        \n        x = self.convolutional0(x)\n        print(f'after convolutional0\\t: {x.size()}')\n        \n        x = self.convolutional1(x)\n        print(f'after convolutional1\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals0)):\n            x = self.residuals0[i](x)\n            print(f'after residuals0 #{i}\\t: {x.size()}')\n        \n        x = self.convolutional2(x)\n        print(f'\\nafter convolutional2\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals1)):\n            x = self.residuals1[i](x)\n            print(f'after residuals1 #{i}\\t: {x.size()}')\n            \n        x = self.convolutional3(x)\n        print(f'\\nafter convolutional3\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals2)):\n            x = self.residuals2[i](x)\n            print(f'after residuals2 #{i}\\t: {x.size()}')\n        \n        branch0 = x.clone()           #(1)\n            \n        x = self.convolutional4(x)\n        print(f'\\nafter convolutional4\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals3)):\n            x = self.residuals3[i](x)\n            print(f'after residuals3 #{i}\\t: {x.size()}')\n        \n        branch1 = x.clone()           #(2)\n            \n        x = self.convolutional5(x)\n        print(f'\\nafter convolutional5\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals4)):\n            x = self.residuals4[i](x)\n            print(f'after residuals4 #{i}\\t: {x.size()}')\n            \n        return branch0, branch1, x    #(3)\n```\n\nNow we test our `Darknet53` class by running the Codeblock 9 below. You can see in the resulting output that everything seems to work properly as the shape of the tensor correctly transforms according to the guide in Figure 1. One thing that I haven’t mentioned before is that this Darknet-53 architecture downscales the input image by a factor of 32. So, with this downsampling factor, an input image of shape 256×256 will become 8×8 in the end (as shown in Figure 1), whereas an input of shape 416×416 will result in a 13×13 prediction tensor.\npython\n\n```\n# Codeblock 9\ndarknet53 = Darknet53()\n\nx = torch.randn(1, 3, 416, 416)\nout = darknet53(x)\n```\n\nmarkup\n\n```\n# Codeblock 9 Outputoriginal              : torch.Size([1, 3, 416, 416])after convolutional0  : torch.Size([1, 32, 416, 416])after convolutional1  : torch.Size([1, 64, 208, 208])after residuals0 #0   : torch.Size([1, 64, 208, 208])after convolutional2  : torch.Size([1, 128, 104, 104])after residuals1 #0   : torch.Size([1, 128, 104, 104])after residuals1 #1   : torch.Size([1, 128, 104, 104])after convolutional3  : torch.Size([1, 256, 52, 52])after residuals2 #0   : torch.Size([1, 256, 52, 52])after residuals2 #1   : torch.Size([1, 256, 52, 52])after residuals2 #2   : torch.Size([1, 256, 52, 52])after residuals2 #3   : torch.Size([1, 256, 52, 52])after residuals2 #4   : torch.Size([1, 256, 52, 52])after residuals2 #5   : torch.Size([1, 256, 52, 52])after residuals2 #6   : torch.Size([1, 256, 52, 52])after residuals2 #7   : torch.Size([1, 256, 52, 52])after convolutional4  : torch.Size([1, 512, 26, 26])after residuals3 #0   : torch.Size([1, 512, 26, 26])after residuals3 #1   : torch.Size([1, 512, 26, 26])after residuals3 #2   : torch.Size([1, 512, 26, 26])after residuals3 #3   : torch.Size([1, 512, 26, 26])after residuals3 #4   : torch.Size([1, 512, 26, 26])after residuals3 #5   : torch.Size([1, 512, 26, 26])after residuals3 #6   : torch.Size([1, 512, 26, 26])after residuals3 #7   : torch.Size([1, 512, 26, 26])after convolutional5  : torch.Size([1, 1024, 13, 13])after residuals4 #0   : torch.Size([1, 1024, 13, 13])after residuals4 #1   : torch.Size([1, 1024, 13, 13])after residuals4 #2   : torch.Size([1, 1024, 13, 13])after residuals4 #3   : torch.Size([1, 1024, 13, 13])\n```\n\nAt this point we can also see what the outputs produced by the three branches look like simply by printing out the shapes of `branch0`, `branch1`, and `x` as shown in Codeblock 10 below. Notice that the spatial dimensions of these three tensors vary. Later on, the tensors from the deeper layers will be upsampled so that we can perform channel-wise concatenation with those from the shallower ones.\npython\n\n```\n# Codeblock 10\nprint(out[0].shape)      # branch0\nprint(out[1].shape)      # branch1\nprint(out[2].shape)      # x\n```\n\nmarkup\n\n```\n# Codeblock 10 Outputtorch.Size([1, 256, 52, 52])torch.Size([1, 512, 26, 26])torch.Size([1, 1024, 13, 13])\n```\n\n### Detection Head Implementation\nIf you go back to Figure 4, you will notice that each of the detection heads consists of two convolution layers. However, these two convolutions are not identical. In Codeblock 11 below I use the `Convolutional` block for the first one and the plain `nn.Conv2d` for the second one. This is essentially done because the second convolution acts as the final layer, hence is responsible for giving raw output (instead of being normalized and ReLU-ed).\npython\n\n```\n# Codeblock 11\nclass DetectionHead(nn.Module):\n    def __init__(self, num_channels):\n        super().__init__()\n        \n        self.convhead0 = Convolutional(in_channels=num_channels,\n                                       out_channels=num_channels*2,\n                                       kernel_size=3)\n        \n        self.convhead1 = nn.Conv2d(in_channels=num_channels*2, \n                                   out_channels=NUM_PRIORS*(NUM_CLASS+5), \n                                   kernel_size=1)\n        \n    def forward(self, x):\n        print(f'original\\t: {x.size()}')\n        \n        x = self.convhead0(x)\n        print(f'after convhead0\\t: {x.size()}')\n        \n        x = self.convhead1(x)\n        print(f'after convhead1\\t: {x.size()}')\n   \n        return x\n```\n\nNow in Codeblock 12 I’ll try to simulate the 13×13 detection head, hence I set the input feature map to have the shape of 512×13×13 (`#(1)`). By the way you’ll know where the number 512 comes from later in the subsequent section.\npython\n\n```\n# Codeblock 12\ndetectionhead = DetectionHead(num_channels=512)\n\nx = torch.randn(1, 512, 13, 13)    #(1)\nout = detectionhead(x)\n```\n\nAnd below is what the resulting output looks like. We can see here that the tensor expands to 1024×13×13 before eventually shrink to 255×13×13. Remember that in YOLOv3 as long as we set `NUM_PRIORS` to 3 and `NUM_CLASS` to 80, the number of output channel will always be 255 regardless of the number of input channel fed into the `DetectionHead`.\nmarkup\n\n```\n# Codeblock 12 Outputoriginal        : torch.Size([1, 512, 13, 13])after convhead0 : torch.Size([1, 1024, 13, 13])after convhead1 : torch.Size([1, 255, 13, 13])\n```\n\n### The Entire YOLOv3 Architecture\nOkay now — since we have initialized the main building blocks, what we need to do next is to construct the entire YOLOv3 architecture. Here I will also discuss the remaining components we haven’t covered. The code is quite long though, so I break it down into two codeblocks: Codeblock 13a and Codeblock 13b. Just ensure that these two codeblocks are written within the same notebook cell if you want to run it on your own.\nIn Codeblock 13a below, what we do first is to initialize the backbone model (`#(1)`). Next, we create a stack of 5 `Convolutional` blocks which alternately halves and doubles the number of channels. The conv block that reduces the channel count uses 1×1 kernel while the one that increases it uses 3×3 kernel, just like the structure we use in the `Residual` block. We initialize this stack of 5 convolutions for the three detection heads. Specifically for the feature maps that flow into the 26×26 and 52×52 heads, we need to initialize another convolution layer (`#(2)` and `#(4)`) and an upsampling layer (`#(3)` and `#(5)`) in addition to the 5 convolutions.\npython\n\n```\n# Codeblock 13a\nclass YOLOv3(nn.Module):\n    def __init__(self):\n        super().__init__()\n        \n        ###############################################\n        # Backbone initialization.\n        \n        self.darknet53 = Darknet53()    #(1)\n        \n        \n        ###############################################\n        # For 13x13 output.\n        \n        self.conv0  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        self.conv1  = Convolutional(in_channels=512, out_channels=1024, kernel_size=3)\n        self.conv2  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        self.conv3  = Convolutional(in_channels=512, out_channels=1024, kernel_size=3)\n        self.conv4  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        \n        self.detection_head_large_obj = DetectionHead(num_channels=512)\n        \n        \n        ###############################################\n        # For 26x26 output.\n        \n        self.conv5  = Convolutional(in_channels=512, out_channels=256, kernel_size=1)  #(2)\n        self.upsample0 = nn.Upsample(scale_factor=2)      #(3)\n        \n        self.conv6  = Convolutional(in_channels=768, out_channels=256, kernel_size=1)\n        self.conv7  = Convolutional(in_channels=256, out_channels=512, kernel_size=3)\n        self.conv8  = Convolutional(in_channels=512, out_channels=256, kernel_size=1)\n        self.conv9  = Convolutional(in_channels=256, out_channels=512, kernel_size=3)\n        self.conv10 = Convolutional(in_channels=512, out_channels=256, kernel_size=1)\n        \n        self.detection_head_medium_obj = DetectionHead(num_channels=256)\n        \n        \n        ###############################################\n        # For 52x52 output.\n        \n        self.conv11  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)  #(4)\n        self.upsample1 = nn.Upsample(scale_factor=2)      #(5)\n        \n        self.conv12  = Convolutional(in_channels=384, out_channels=128, kernel_size=1)\n        self.conv13  = Convolutional(in_channels=128, out_channels=256, kernel_size=3)\n        self.conv14  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)\n        self.conv15  = Convolutional(in_channels=128, out_channels=256, kernel_size=3)\n        self.conv16  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)\n        \n        self.detection_head_small_obj = DetectionHead(num_channels=128)\n```\n\nNow in Codeblock 13b we define the flow of the network inside the `forward()` method. Here we first pass the input tensor through the `darknet53` model (`#(1)`), which produces 3 output tensors: `branch0`, `branch1`, and `x`. Then, what we do next is to connect the layers one after another according to the flow given in Figure 4.\npython\n\n```\n# Codeblock 13b\n    def forward(self, x):\n        \n        ###############################################\n        # Backbone.\n        branch0, branch1, x = self.darknet53(x)      #(1)\n        print(f'branch0\\t\\t\\t: {branch0.size()}')\n        print(f'branch1\\t\\t\\t: {branch1.size()}')\n        print(f'x\\t\\t\\t: {x.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 13x13 detection head.\n        \n        x = self.conv0(x)\n        print(f'after conv0\\t\\t: {x.size()}')\n        \n        x = self.conv1(x)\n        print(f'after conv1\\t\\t: {x.size()}')\n        \n        x = self.conv2(x)\n        print(f'after conv2\\t\\t: {x.size()}')\n        \n        x = self.conv3(x)\n        print(f'after conv3\\t\\t: {x.size()}')\n        \n        x = self.conv4(x)\n        print(f'after conv4\\t\\t: {x.size()}')\n        \n        large_obj = self.detection_head_large_obj(x)\n        print(f'large object detection\\t: {large_obj.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 26x26 detection head.\n        \n        x = self.conv5(x)\n        print(f'after conv5\\t\\t: {x.size()}')\n        \n        x = self.upsample0(x)\n        print(f'after upsample0\\t\\t: {x.size()}')\n        \n        x = torch.cat([x, branch1], dim=1)\n        print(f'after concatenate\\t: {x.size()}')\n        \n        x = self.conv6(x)\n        print(f'after conv6\\t\\t: {x.size()}')\n        \n        x = self.conv7(x)\n        print(f'after conv7\\t\\t: {x.size()}')\n        \n        x = self.conv8(x)\n        print(f'after conv8\\t\\t: {x.size()}')\n        \n        x = self.conv9(x)\n        print(f'after conv9\\t\\t: {x.size()}')\n        \n        x = self.conv10(x)\n        print(f'after conv10\\t\\t: {x.size()}')\n        \n        medium_obj = self.detection_head_medium_obj(x)\n        print(f'medium object detection\\t: {medium_obj.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 52x52 detection head.\n        \n        x = self.conv11(x)\n        print(f'after conv11\\t\\t: {x.size()}')\n        \n        x = self.upsample1(x)\n        print(f'after upsample1\\t\\t: {x.size()}')\n        \n        x = torch.cat([x, branch0], dim=1)\n        print(f'after concatenate\\t: {x.size()}')\n        \n        x = self.conv12(x)\n        print(f'after conv12\\t\\t: {x.size()}')\n        \n        x = self.conv13(x)\n        print(f'after conv13\\t\\t: {x.size()}')\n        \n        x = self.conv14(x)\n        print(f'after conv14\\t\\t: {x.size()}')\n        \n        x = self.conv15(x)\n        print(f'after conv15\\t\\t: {x.size()}')\n        \n        x = self.conv16(x)\n        print(f'after conv16\\t\\t: {x.size()}')\n        \n        small_obj = self.detection_head_small_obj(x)\n        print(f'small object detection\\t: {small_obj.size()}\\n')\n        \n\n        ###############################################\n        # Return prediction tensors.\n        \n        return large_obj, medium_obj, small_obj\n```\n\nAs we have completed the `forward()` method, we can now test the entire YOLOv3 model by passing a single RGB image of size 416×416 as shown in Codeblock 14.\npython\n\n```\n# Codeblock 14\nyolov3 = YOLOv3()\n\nx = torch.randn(1, 3, 416, 416)\nout = yolov3(x)\n```\n\nBelow is what the output looks like after you run the codeblock above. Here we can see that everything seems to work properly as the dummy image successfully passed through all layers in the network. One thing that you might probably need to know is that the 768-channel feature map at line `#(4)` is obtained from the concatenation between the tensor at lines `#(2)` and `#(3)`. The similar thing also applies to the 384-channel tensor at line `#(6)`, in which it is the concatenation between the feature maps at lines `#(1)` and `#(5)`.\nmarkup\n\n```\n# Codeblock 14 Outputbranch0                 : torch.Size([1, 256, 52, 52])    #(1)branch1                 : torch.Size([1, 512, 26, 26])    #(2)x                       : torch.Size([1, 1024, 13, 13])after conv0             : torch.Size([1, 512, 13, 13])after conv1             : torch.Size([1, 1024, 13, 13])after conv2             : torch.Size([1, 512, 13, 13])after conv3             : torch.Size([1, 1024, 13, 13])after conv4             : torch.Size([1, 512, 13, 13])large object detection  : torch.Size([1, 255, 13, 13])after conv5             : torch.Size([1, 256, 13, 13])after upsample0         : torch.Size([1, 256, 26, 26])    #(3)after concatenate       : torch.Size([1, 768, 26, 26])    #(4)after conv6             : torch.Size([1, 256, 26, 26])after conv7             : torch.Size([1, 512, 26, 26])after conv8             : torch.Size([1, 256, 26, 26])after conv9             : torch.Size([1, 512, 26, 26])after conv10            : torch.Size([1, 256, 26, 26])medium object detection : torch.Size([1, 255, 26, 26])after conv11            : torch.Size([1, 128, 26, 26])after upsample1         : torch.Size([1, 128, 52, 52])    #(5)after concatenate       : torch.Size([1, 384, 52, 52])    #(6)after conv12            : torch.Size([1, 128, 52, 52])after conv13            : torch.Size([1, 256, 52, 52])after conv14            : torch.Size([1, 128, 52, 52])after conv15            : torch.Size([1, 256, 52, 52])after conv16            : torch.Size([1, 128, 52, 52])small object detection  : torch.Size([1, 255, 52, 52])\n```\n\nAnd just to make things clearer here I also print out the output of each detection head in Codeblock 15 below. We can see here that all the resulting prediction tensors have the shape that we expected earlier. Thus, I believe our YOLOv3 implementation is correct and hence ready to train.\npython\n\n```\n# Codeblock 15\nprint(out[0].shape)\nprint(out[1].shape)\nprint(out[2].shape)\n```\n\nmarkup\n\n```\n# Codeblock 15 Outputtorch.Size([1, 255, 13, 13])torch.Size([1, 255, 26, 26])torch.Size([1, 255, 52, 52])\n```\n\n···\nI think that’s pretty much everything about YOLOv3 and its architecture implementation from scratch. As we’ve seen above, the authors successfully made an impressive improvement in performance compared to the previous YOLO version, even though the changes they made to the system were not what they considered significant — hence the title “ _An Incremental Improvement_.” \nPlease let me know if you spot any mistake in this article. You can also find the fully-working code in my GitHub repo [13]. Thanks for reading, see ya in my next article!\n···\n## References\n[1] Joseph Redmon and Ali Farhadi. YOLOv3: An Incremental Improvement. Arxiv. <https://arxiv.org/abs/1804.02767> [Accessed August 24, 2025].\n[2] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv1 Paper Walkthrough: The Day YOLO First Saw the World. Medium. <https://ai.gopubby.com/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b> [Accessed March 1, 2026].\n[3] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv1 Loss Function Walkthrough: Regression for All. Medium. <https://ai.gopubby.com/yolov1-loss-function-walkthrough-regression-for-all-18c34be6d7cb> [Accessed March 1, 2026].\n[4] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv2 & YOLO9000 Paper Walkthrough: Better, Faster, Stronger. Towards Data Science. <https://towardsdatascience.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger/> [Accessed March 1, 2026].\n[5] Image originally created by author.\n[6] YOLO v3 introduction to object detection with TensorFlow 2. PyLessons. <https://pylessons.com/YOLOv3-TF2-introduction> [Accessed August 24, 2025].\n[7] aladdinpersson. YOLOv3. GitHub. <https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/Pytorch/object_detection/YOLOv3/model.py> [Accessed August 24, 2025].\n[8] Kaiming He _et al._ Deep Residual Learning for Image Recognition. Arxiv. <https://arxiv.org/abs/1512.03385> [Accessed August 24, 2025].\n[9] Langcai Cao _et al._ A Text Detection Algorithm for Image of Student Exercises Based on CTPN and Enhanced YOLOv3. IEEE Access. <https://ieeexplore.ieee.org/document/9200481> [Accessed August 24, 2025].\n[10] Image by author, partially generated by Gemini.\n[11] Joseph Redmon _et al._ You Only Look Once: Unified, Real-Time Object Detection. Arxiv. <https://arxiv.org/pdf/1506.02640> [Accessed July 5, 2025].\n[12] ML For Nerds. YOLO-V3: An Incremental Improvement || YOLO OBJECT DETECTION SERIES. YouTube. [https://www.youtube.com/watch?v=9fhAbvPWzKs&t=174s](https://www.youtube.com/watch?v=9fhAbvPWzKs&t=174s) [Accessed August 24, 2025].\n[13] MuhammadArdiPutra. Even Better, but Not That Much — YOLOv3. GitHub. <https://github.com/MuhammadArdiPutra/medium_articles/blob/main/Even%20Better%2C%20but%20Not%20That%20Much%20-%20YOLOv3.ipynb> [Accessed August 24, 2025].\n## Register Now to Continue Reading\n### Unlock Towards Data Science with a Free Account\nAll articles on Towards Data Science are available to registered users. Create a free account today to access this article and thousands more.\nRegister for freeSign in to your account\n* * *\n![](../images/body-59e1c17daab1fcb582c5.jpeg)\nWritten By\n## [Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nDeep learning storyteller\n[See all from Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\n[Deep Dives](https://towardsdatascience.com/tag/deep-dives/)[Computer Vision](https://towardsdatascience.com/tag/computer-vision/)[Deep Learning](https://towardsdatascience.com/tag/deep-learning/)[Neural Network](https://towardsdatascience.com/tag/neural-network/)[Pytorch](https://towardsdatascience.com/tag/pytorch/)\nShare This Article\n  * [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&title=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n  * [](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&title=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n  * [](https://x.com/share?url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&text=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n\n\nTowards Data Science is a community publication. Submit your insights to reach our global audience and earn through the TDS Author Payment Program.\n[Write for TDS](https://towardsdatascience.com/submissions/)\n## Related Articles\n  * [![](../images/002.png)](https://towardsdatascience.com/histogram-matching-ee3a67b4cbc1/)\n## [Histogram Matching](https://towardsdatascience.com/histogram-matching-ee3a67b4cbc1/)\n[](https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/)\nHow to generate a histogram for an image, how to equalize the histogram, and finally how to modify your image histogram to be similar to...\n[Ali Pourramezan Fard](https://towardsdatascience.com/author/aliprf/)\nOctober 16, 20205 min read\n  * [![](../images/003.png)](https://towardsdatascience.com/artificial-intelligence-get-your-users-to-label-your-data-b5fa7c0c9e00/)\n## [Artificial Intelligence: Get your users to label your data](https://towardsdatascience.com/artificial-intelligence-get-your-users-to-label-your-data-b5fa7c0c9e00/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nI wanted to call this article \"Parasitic Labeling of AI Training Data,\" but apparently that's too complicated. What I want to tell you...\n[Daniel Shapiro, PhD](https://towardsdatascience.com/author/lemaysolutions/)\nSeptember 19, 20174 min read\n  * [![](../images/004.png)](https://towardsdatascience.com/practical-introduction-to-automation-music-transcription-3ad8ad40eab6/)\n## [Practical Introduction to Automation Music Transcription](https://towardsdatascience.com/practical-introduction-to-automation-music-transcription-3ad8ad40eab6/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nFrom Audio to Model Input, BiLSTM Model Application and Even Segmentation Method\n[Yue Hu](https://towardsdatascience.com/author/yhu0206/)\nSeptember 28, 202012 min read\n  * [![](../images/005.jpg)](https://towardsdatascience.com/whats-new-in-computer-vision-and-object-detection-8e586c4429e3/)\n## [What's New in Computer Vision and Object Detection?](https://towardsdatascience.com/whats-new-in-computer-vision-and-object-detection-8e586c4429e3/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nOur weekly selection of must-read Editors' Picks and original features\n[TDS Editors](https://towardsdatascience.com/author/towardsdatascience/)\nJuly 11, 20243 min read\n  * [![](../images/006.jpg)](https://towardsdatascience.com/pondernet-explained-5e9571e657d/)\n## [PonderNet explained](https://towardsdatascience.com/pondernet-explained-5e9571e657d/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nImplementing a pondering network for the MNIST dataset\n[Conrad Cardona](https://towardsdatascience.com/author/conradcardona/)\nNovember 17, 202113 min read\n  * [![](../images/007.jpg)](https://towardsdatascience.com/audio-deep-learning-made-simple-part-3-data-preparation-and-augmentation-24c6e1f6b52/)\n## [Audio Deep Learning Made Simple (Part 3): Data Preparation and Augmentation](https://towardsdatascience.com/audio-deep-learning-made-simple-part-3-data-preparation-and-augmentation-24c6e1f6b52/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA Gentle Guide to enhancing Spectrogram features for optimal performance. Also Data Augmentation, in Plain English\n[Ketan Doshi](https://towardsdatascience.com/author/ketanhdoshi/)\nFebruary 24, 20217 min read\n  * [![](../images/008.png)](https://towardsdatascience.com/llms-txt-414d5121bcb3/)\n## [LLMs.txt Explained](https://towardsdatascience.com/llms-txt-414d5121bcb3/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nYour guide to the web's new LLM-ready content standard\n[Derick Ruiz](https://towardsdatascience.com/author/dericksozo/)\nNovember 28, 20245 min read\n  * [![](../images/009.jpeg)](https://towardsdatascience.com/road-signs-driving-you-crazy-e686d97f2480/)\n## [Road signs \"driving\" you crazy?](https://towardsdatascience.com/road-signs-driving-you-crazy-e686d97f2480/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nHere's how I built a program that reads them for you\n[Tara Prole](https://towardsdatascience.com/author/taraprole/)\nDecember 5, 202113 min read\n  * [![](../images/010.jpeg)](https://towardsdatascience.com/deep-transfer-learning-for-natural-language-processing-text-classification-with-universal-1a2c69e5baa9/)\n## [Deep Transfer Learning for Natural Language Processing - Text Classification with Universal...](https://towardsdatascience.com/deep-transfer-learning-for-natural-language-processing-text-classification-with-universal-1a2c69e5baa9/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA Guide to Demystifying Universal Sentence Encoders\n[Dipanjan (DJ) Sarkar](https://towardsdatascience.com/author/djsarkar/)\nDecember 4, 201818 min read\n\n\n  * [YouTube](https://www.youtube.com/c/TowardsDataScience)\n  * [X](https://x.com/TDataScience)\n  * [LinkedIn](https://www.linkedin.com/company/towards-data-science/)\n  * [Threads](https://www.threads.net/@towardsdatascience)\n  * [Bluesky](https://bsky.app/profile/towardsdatascience.com)\n\n\n[](https://towardsdatascience.com/)\nYour home for data science and AI. The world's leading publication for data science, data analytics, data engineering, machine learning, and artificial intelligence professionals.\nSubscribe 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/)\n© Insight Media Group, LLC 2026•[The New Stack](https://thenewstack.io)•[roadmap.sh](https://roadmap.sh)•[Creator Network](https://insightmediagroup.io/creator-network)\n####  Recommended Articles \nClose\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607686/image?etag=1782161464448&width=500&height=500) How Relevance Models Foreshadowed Transformers for NLP ](https://towardsdatascience.com/how-relevance-models-foreshadowed-transformers-for-nlp/ \"How Relevance Models Foreshadowed Transformers for NLP\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607639/image?etag=1781250125848&width=500&height=500) Javascript Fatigue: HTMX Is All You Need to Build ChatGPT — Part 1 ](https://towardsdatascience.com/javascript-fatigue-you-dont-need-js-to-build-chatgpt/ \"Javascript Fatigue: HTMX Is All You Need to Build ChatGPT — Part 1\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607930/image?etag=1782124566054&width=500&height=500) The Subset Sum Problem Solved in Linear Time for Dense Enough Inputs ](https://towardsdatascience.com/subset-sum-problem-solved-in-linear-time-for-dense-enough-inputs/ \"The Subset Sum Problem Solved in Linear Time for Dense Enough Inputs\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/3660/image?etag=1780176246070&width=500&height=500) Does Your Company Have a Data Strategy? ](https://towardsdatascience.com/does-your-company-have-a-data-strategy-e8944dbec4ae/ \"Does Your Company Have a Data Strategy?\")\n\n\n",
  "metadata": {
    "title": "YOLOv3 Paper Walkthrough: Even Better, But Not That Much | Towards Data Science",
    "description": "A PyTorch implementation on the YOLOv3 architecture from scratch",
    "keywords": null,
    "author": "Muhammad Ardi Putra",
    "og:locale": "en_US",
    "og:type": "article",
    "og:title": "YOLOv3 Paper Walkthrough: Even Better, But Not That Much",
    "og:description": "A PyTorch implementation on the YOLOv3 architecture from scratch",
    "og:url": "https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/",
    "og:site_name": "Towards Data Science",
    "og:image": "https://assets.insightmediagroup.io/media/wp-content/uploads/2026/03/volodymyr-hryshchenko-L0oJ4Dlfyuo-unsplash-scaled-1.jpg",
    "og:image:width": "2560",
    "og:image:height": "1173",
    "og:image:type": "image/jpeg",
    "twitter:card": "summary_large_image",
    "twitter:site": "@TDataScience",
    "twitter:creator": "@TDataScience",
    "twitter:label1": "Written by",
    "twitter:data1": "Muhammad Ardi Putra",
    "twitter:label2": "Est. reading time",
    "twitter:data2": "23 min read",
    "article:published_time": "2026-03-02T17:30:07.000Z",
    "article:modified_time": "2026-03-02T17:30:07.000Z",
    "article:author": "https://towardsdatascience.com/author/muhammad_ardi/"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 202,
    "error": ""
  },
  "code_blocks": [
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_000.txt",
      "chars": 81
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_001.txt",
      "chars": 1070
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_002.txt",
      "chars": 207
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_003.txt",
      "chars": 212
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_004.txt",
      "chars": 250
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_005.txt",
      "chars": 217
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_006.txt",
      "chars": 978
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_007.txt",
      "chars": 103
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_008.txt",
      "chars": 209
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_009.txt",
      "chars": 281
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_010.txt",
      "chars": 221
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_011.txt",
      "chars": 3588
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_012.txt",
      "chars": 90
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_013.txt",
      "chars": 1595
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_014.txt",
      "chars": 114
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_015.txt",
      "chars": 107
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_016.txt",
      "chars": 777
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_017.txt",
      "chars": 127
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_018.txt",
      "chars": 161
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_019.txt",
      "chars": 2452
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_020.txt",
      "chars": 2985
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_021.txt",
      "chars": 82
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_022.txt",
      "chars": 1531
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_023.txt",
      "chars": 75
    },
    {
      "language": "txt",
      "file": "attachments/0d9726dbcb9e/code/block_024.txt",
      "chars": 106
    }
  ],
  "images": [
    {
      "status": "http_404",
      "url": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607686/image?etag=1782161464448&width=500&height=500",
      "src": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607686/image?etag=1782161464448&width=500&height=500",
      "alt": "",
      "downloaded": false,
      "discovery": "saved_markdown",
      "retried_at": "2026-10-20T17:52:05Z"
    },
    {
      "status": "http_404",
      "url": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607639/image?etag=1781250125848&width=500&height=500",
      "src": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607639/image?etag=1781250125848&width=500&height=500",
      "alt": "",
      "downloaded": false,
      "discovery": "saved_markdown",
      "retried_at": "2026-10-20T17:52:05Z"
    },
    {
      "status": "http_404",
      "url": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607930/image?etag=1782124566054&width=500&height=500",
      "src": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607930/image?etag=1782124566054&width=500&height=500",
      "alt": "",
      "downloaded": false,
      "discovery": "saved_markdown",
      "retried_at": "2026-10-20T17:52:05Z"
    },
    {
      "status": "http_404",
      "url": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/3660/image?etag=1780176246070&width=500&height=500",
      "src": "https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/3660/image?etag=1780176246070&width=500&height=500",
      "alt": "",
      "downloaded": false,
      "discovery": "saved_markdown",
      "retried_at": "2026-10-20T17:52:05Z"
    }
  ],
  "audio": [],
  "videos": [
    {
      "url": "https://www.youtube.com/watch?v=9fhAbvPWzKs",
      "status": "ok",
      "local_path": "attachments/0d9726dbcb9e/videos/9fhAbvPWzKs.mp4",
      "bytes": 71724294,
      "valid": true,
      "height": 1080,
      "width": 1920,
      "duration": "1777.521000"
    },
    {
      "url": "https://www.youtube.com/c/TowardsDataScience",
      "kind": "channel_reference",
      "status": "reference_only"
    }
  ],
  "links": [
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/latest/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/editors-pick/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/the-variable/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/shipai/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/submissions/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/category/artificial-intelligence/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/muhammad_ardi/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/deep-dives/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/computer-vision/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/deep-learning/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/neural-network/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/pytorch/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/histogram-matching-ee3a67b4cbc1/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/aliprf/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/artificial-intelligence-get-your-users-to-label-your-data-b5fa7c0c9e00/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/lemaysolutions/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/practical-introduction-to-automation-music-transcription-3ad8ad40eab6/"
    },
    {
      "type": "external",
      "href": "https://write.insightmediagroup.io"
    },
    {
      "type": "external",
      "href": "https://www.linkedin.com/company/towards-data-science/"
    },
    {
      "type": "external",
      "href": "https://x.com/TDataScience"
    },
    {
      "type": "external",
      "href": "https://unsplash.com/photos/pile-of-brown-wooden-blocks-L0oJ4Dlfyuo"
    },
    {
      "type": "external",
      "href": "https://unsplash.com/@lunarts"
    },
    {
      "type": "external",
      "href": "https://roadmap.sh/ai-engineer"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/abs/1804.02767"
    },
    {
      "type": "external",
      "href": "https://medium.com/u/9801a58700ac"
    },
    {
      "type": "external",
      "href": "https://ai.gopubby.com/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b"
    },
    {
      "type": "external",
      "href": "https://ai.gopubby.com/yolov1-loss-function-walkthrough-regression-for-all-18c34be6d7cb"
    },
    {
      "type": "external",
      "href": "https://pylessons.com/YOLOv3-TF2-introduction"
    },
    {
      "type": "external",
      "href": "https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/Pytorch/object_detection/YOLOv3/model.py"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/abs/1512.03385"
    },
    {
      "type": "external",
      "href": "https://ieeexplore.ieee.org/document/9200481"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/pdf/1506.02640"
    },
    {
      "type": "external",
      "href": "https://www.youtube.com/watch?t=174s&v=9fhAbvPWzKs"
    },
    {
      "type": "external",
      "href": "https://github.com/MuhammadArdiPutra/medium_articles/blob/main/Even%20Better%2C%20but%20Not%20That%20Much%20-%20YOLOv3.ipynb"
    },
    {
      "type": "external",
      "href": "https://www.facebook.com/sharer/sharer.php?title=YOLOv3+Paper+Walkthrough%3A+Even+Better%2C+But+Not+That%C2%A0Much&u=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F"
    },
    {
      "type": "external",
      "href": "https://www.linkedin.com/shareArticle?mini=true&title=YOLOv3+Paper+Walkthrough%3A+Even+Better%2C+But+Not+That%C2%A0Much&url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F"
    },
    {
      "type": "external",
      "href": "https://x.com/share?text=YOLOv3+Paper+Walkthrough%3A+Even+Better%2C+But+Not+That%C2%A0Much&url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-20T17:27:47Z",
  "source_url": "https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/",
  "delivery_status": "unverified",
  "discovery_status": "http_403",
  "missing_reason": "源页面未重新核验:http_403；已按保留正文补采图片",
  "repair_error": "",
  "actual_video_files": [
    {
      "local_path": "attachments/0d9726dbcb9e/videos/9fhAbvPWzKs.mp4",
      "bytes": 71724294,
      "valid": true,
      "height": 1080,
      "width": 1920,
      "duration": "1777.521000",
      "status": "ok"
    }
  ],
  "expected_saved_body_images": 4,
  "image_discovery": "saved_markdown",
  "source_body_markdown": "![Revisit consent button](https://cdn-cookieyes.com/assets/images/revisit.svg)\nWe value your privacy\nWe 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.\nCustomise Reject All Accept All\nCustomise Consent Preferences ![](https://cdn-cookieyes.com/assets/images/close.svg)\nWe use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.\nThe 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\nNecessaryAlways Active\nNecessary 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.\n  * Cookie\nBCTempID\n  * Duration\n10 minutes\n  * Description\nNo description available.\n\n\n  * Cookie\n__cf_bm\n  * Duration\n1 hour\n  * Description\nThis cookie, set by Cloudflare, is used to support Cloudflare Bot Management. \n\n\n  * Cookie\nAWSALBCORS\n  * Duration\n7 days\n  * Description\nAmazon Web Services set this cookie for load balancing.\n\n\n  * Cookie\n_cfuvid\n  * Duration\nsession\n  * Description\nCloudflare sets this cookie to track users across sessions to optimize user experience by maintaining session consistency and providing personalized services\n\n\n  * Cookie\nli_gc\n  * Duration\n6 months\n  * Description\nLinkedin set this cookie for storing visitor's consent regarding using cookies for non-essential purposes.\n\n\n  * Cookie\n__hssrc\n  * Duration\nsession\n  * Description\nThis 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.\n\n\n  * Cookie\n__hssc\n  * Duration\n1 hour\n  * Description\nHubSpot sets this cookie to keep track of sessions and to determine if HubSpot should increment the session number and timestamps in the __hstc cookie.\n\n\n  * Cookie\nwpEmojiSettingsSupports\n  * Duration\nsession\n  * Description\nWordPress 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.\n\n\n  * Cookie\nBCSessionID\n  * Duration\n1 year 1 month 4 days\n  * Description\nBlueconic sets this cookie as a unique identifier for the BlueConic profile.\n\n\n  * Cookie\n_octo\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nlogged_in\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\n__Secure-YEC\n  * Duration\npast\n  * Description\nYouTube sets this cookie to stores the user's video player preferences using embedded YouTube video\n\n\n  * Cookie\n__eoi\n  * Duration\n6 months\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nAWSALBTGCORS\n  * Duration\n7 days\n  * Description\nNo description available.\n\n\n  * Cookie\nlogin-status-p\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nAWSALBTG\n  * Duration\n7 days\n  * Description\nNo description available.\n\n\n  * Cookie\ncsrf_token\n  * Duration\nsession\n  * Description\nNo description available.\n\n\n  * Cookie\ntoken_v2\n  * Duration\n1 day\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nD\n  * Duration\n1 year\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nPHPSESSID\n  * Duration\nsession\n  * Description\nThis 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.\n\n\n  * Cookie\nVISITOR_PRIVACY_METADATA\n  * Duration\n6 months\n  * Description\nYouTube sets this cookie to store the user's cookie consent state for the current domain. \n\n\n  * Cookie\ncookietest\n  * Duration\nsession\n  * Description\nThe cookietest cookie is typically used to determine whether the user's browser accepts cookies, essential for website functionality and user experience.\n\n\n  * Cookie\n__Host-airtable-session\n  * Duration\n1 year\n  * Description\nThis cookie is used to enable us to integrate the services of Airtable.\n\n\n  * Cookie\n__Host-airtable-session.sig\n  * Duration\n1 year\n  * Description\nThis cookie is used to enable us to integrate the services of Airtable.\n\n\n  * Cookie\nm\n  * Duration\n1 year 1 month 4 days\n  * Description\nStripe sets this cookie for fraud prevention purposes. It identifies the device used to access the website, allowing the website to be formatted accordingly.\n\n\n  * Cookie\nBIGipServer*\n  * Duration\nsession\n  * Description\nMarketo 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. \n\n\n  * Cookie\n__cfruid\n  * Duration\nsession\n  * Description\nCloudflare sets this cookie to identify trusted web traffic.\n\n\n  * Cookie\n_GRECAPTCHA\n  * Duration\n6 months\n  * Description\nGoogle Recaptcha service sets this cookie to identify bots to protect the website against malicious spam attacks.\n\n\n  * Cookie\n__Secure-YNID\n  * Duration\n6 months\n  * Description\nGoogle cookie used to protect user security and prevent fraud, especially during the login process.\n\n\n  * Cookie\ncookieyes-consent\n  * Duration\n1 year\n  * Description\nCookieYes 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.\n\n\nFunctional\nFunctional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.\n  * Cookie\nlidc\n  * Duration\n1 day\n  * Description\nLinkedIn sets the lidc cookie to facilitate data center selection.\n\n\n  * Cookie\nbrw\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nbrwConsent\n  * Duration\n5 minutes\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nWMF-Uniq\n  * Duration\n1 year\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nloom_anon_comment\n  * Duration\n1 year\n  * Description\nNo description available.\n\n\n  * Cookie\nloom_referral_video\n  * Duration\nsession\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nVISITOR_INFO1_LIVE\n  * Duration\n6 months\n  * Description\nA cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface.\n\n\n  * Cookie\nyt-remote-connected-devices\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to store the user's video preferences using embedded YouTube videos.\n\n\n  * Cookie\nytidb::LAST_RESULT_ENTRY_KEY\n  * Duration\nNever Expires\n  * Description\nThe 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.\n\n\n  * Cookie\nyt-remote-device-id\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to store the user's video preferences using embedded YouTube videos.\n\n\n  * Cookie\nyt-remote-session-name\n  * Duration\nsession\n  * Description\nThe yt-remote-session-name cookie is used by YouTube to store the user's video player preferences using embedded YouTube video.\n\n\n  * Cookie\nyt-remote-fast-check-period\n  * Duration\nsession\n  * Description\nThe yt-remote-fast-check-period cookie is used by YouTube to store the user's video player preferences for embedded YouTube videos.\n\n\n  * Cookie\nyt-remote-session-app\n  * Duration\nsession\n  * Description\nThe yt-remote-session-app cookie is used by YouTube to store user preferences and information about the interface of the embedded YouTube video player.\n\n\n  * Cookie\nyt-remote-cast-available\n  * Duration\nsession\n  * Description\nThe yt-remote-cast-available cookie is used to store the user's preferences regarding whether casting is available on their YouTube video player.\n\n\n  * Cookie\nyt-remote-cast-installed\n  * Duration\nsession\n  * Description\nThe yt-remote-cast-installed cookie is used to store the user's video player preferences using embedded YouTube video.\n\n\n  * Cookie\ncp_session\n  * Duration\n3 months\n  * Description\nCodepen sets this cookie for Help systems found in the website.\n\n\n  * Cookie\nloid\n  * Duration\n1 year 1 month 4 days\n  * Description\nThis cookie is set by the Reddit. The cookie enables the sharing of content from the website onto the social media platform.\n\n\nAnalytics\nAnalytical 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.\n  * Cookie\n__hstc\n  * Duration\n6 months\n  * Description\nHubspot 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).\n\n\n  * Cookie\nhubspotutk\n  * Duration\n6 months\n  * Description\nHubSpot 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.\n\n\n  * Cookie\n_ga\n  * Duration\n1 year 1 month 4 days\n  * Description\nGoogle 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.\n\n\n  * Cookie\n_ga_*\n  * Duration\n1 year 1 month 4 days\n  * Description\nGoogle Analytics sets this cookie to store and count page views.\n\n\n  * Cookie\n__Host-psifi.analyticsTrace\n  * Duration\n6 hours\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\n__Host-psifi.analyticsTraceV2\n  * Duration\n6 hours\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\n_gh_sess\n  * Duration\nsession\n  * Description\nGitHub sets this cookie for temporary application and framework state between pages like what step the user is on in a multiple step form.\n\n\n  * Cookie\nYSC\n  * Duration\nsession\n  * Description\nYSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages.\n\n\n  * Cookie\najs_anonymous_id\n  * Duration\n1 year\n  * Description\nThis cookie is set by Segment to count the number of people who visit a certain site by tracking if they have visited before.\n\n\n  * Cookie\nvuid\n  * Duration\n1 year 1 month 4 days\n  * Description\nVimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website. \n\n\nPerformance\nPerformance 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.\n  * Cookie\nAWSALB\n  * Duration\n7 days\n  * Description\nAWSALB is an application load balancer cookie set by Amazon Web Services to map the session to the target.\n\n\n  * Cookie\nacq\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nacq.sig\n  * Duration\npast\n  * Description\nDescription is currently not available.\n\n\n  * Cookie\nptc\n  * Duration\n2 years\n  * Description\nNo description available.\n\n\nAdvertisement\nAdvertisement 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.\n  * Cookie\nmuc_ads\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to collect user behaviour and interaction data to optimize the website.\n\n\n  * Cookie\nguest_id_marketing\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to identify and track the website visitor.\n\n\n  * Cookie\nguest_id_ads\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter sets this cookie to identify and track the website visitor.\n\n\n  * Cookie\npersonalization_id\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter 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.\n\n\n  * Cookie\nguest_id\n  * Duration\n1 year 1 month 4 days\n  * Description\nTwitter 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.\n\n\n  * Cookie\nbcookie\n  * Duration\n1 year\n  * Description\nLinkedIn sets this cookie from LinkedIn share buttons and ad tags to recognize browser IDs.\n\n\n  * Cookie\n__Secure-ROLLOUT_TOKEN\n  * Duration\n6 months\n  * Description\nYouTube 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.\n\n\n  * Cookie\nyt.innertube::nextId\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.\n\n\n  * Cookie\nyt.innertube::requests\n  * Duration\nNever Expires\n  * Description\nYouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.\n\n\n  * Cookie\nsession_tracker\n  * Duration\nsession\n  * Description\nThis cookie is set by the Reddit. This cookie is used to identify trusted web traffic. It also helps in adverstising on the website.\n\n\n  * Cookie\nedgebucket\n  * Duration\nsession\n  * Description\nReddit sets this cookie to save the information about a log-on Reddit user, for the purpose of advertisement recommendations and updating the content.\n\n\n  * Cookie\ndid\n  * Duration\n1 year\n  * Description\nArbor sets this cookie to show targeted ads to site visitors.This cookie expires after 2 months or 1 year.\n\n\nUncategorised\nOther uncategorised cookies are those that are being analysed and have not been classified into a category as yet.\nNo cookies to display.\nReject All Save My Preferences Accept All\nToggle Search[](https://towardsdatascience.com/)Toggle Mobile Navigation\n[](https://towardsdatascience.com/)\nPublish AI, ML & data-science insights to a global community of data professionals.\nSign in[Submit an Article](https://write.insightmediagroup.io)\n[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/)\n* * *\n[Write For TDS](https://towardsdatascience.com/submissions/)\n[LinkedIn](https://www.linkedin.com/company/towards-data-science/)[X](https://x.com/TDataScience)Toggle Search\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\n# YOLOv3 Paper Walkthrough: Even Better, But Not That Much\nA PyTorch implementation on the YOLOv3 architecture from scratch\n[Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nMarch 2, 202623 min read\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2026/03/volodymyr-hryshchenko-L0oJ4Dlfyuo-unsplash-scaled-1.jpg)\n[Photo](https://unsplash.com/photos/pile-of-brown-wooden-blocks-L0oJ4Dlfyuo) by [Volodymyr Hryshchenko](https://unsplash.com/@lunarts?utm_source=medium&utm_medium=referral) on Unsplash\nYOLOv2, which used to be the state-of-the-art object detection algorithm, looked to become obsolete thanks to the appearance of other methods like SSD (Single Shot Multibox Detector), DSSD (Deconvolutional Single Shot Detector), and RetinaNet. Finally, after two years since the introduction of YOLOv2, the authors decided to improve the algorithm where they eventually came up with the next YOLO version reported in a paper titled “ _YOLOv3: An Incremental Improvement_ ” [1]. As the title suggests, there were indeed not many things the authors improved upon YOLOv2 in terms of the underlying algorithm. But hey, when it comes to performance, it actually looks pretty impressive.\nIn this article I am going to talk about the modifications the authors made to YOLOv2 to create YOLOv3 and how to implement the model architecture from scratch with PyTorch. I highly recommend you reading my previous article about YOLOv1 [2, 3] and YOLOv2 [4] before this one, unless you already got a strong foundation in how these two earlier versions of YOLO work.\n···\nLearn this step by step with the interactive [AI Engineer](https://roadmap.sh/ai-engineer) roadmap.\n## What Makes YOLOv3 Better Than YOLOv2\n### The Vanilla Darknet-53\nThe modification the authors made was mainly related to the architecture, in which they proposed a backbone model referred to as Darknet-53. See the detailed structure of this network in Figure 1. As the name suggests, this model is an improvement upon the Darknet-19 used in YOLOv2. If you count the number of layers in Darknet-53, you will find that this network consists of 52 convolution layers and a single fully-connected layer at the end. Keep in mind that later when we implement it on YOLOv3, we will feed it with images of size 416×416 rather than 256×256 as written in the figure.\n![](https://assets.insightmediagroup.io/media/max/720/1*CVFj-QX6aKHARgN9dmIaIQ.png)\nFigure 1. The vanilla Darknet-53 architecture [1].\nIf you’re familiar with Darknet-19, you must remember that it performs spatial downsmapling using maxpooling operations after every stack of several convolution layers. In Darknet-53, authors replaced these pooling operations with convolutions of stride 2. This was essentially done because maxpooling layer completely ignores non-maximum numbers, causing us to lose a lot of information contained in the lower intensity pixels. We can actually use average-pooling as an alternative, but in theory, this approach won’t be optimal either because all pixels within the small region are weighted the same. So as a solution, authors decided to use convolution layer with a stride of 2, which by doing so the model will be able to reduce image resolution while capturing spatial information with specific weightings. You can see the illustration for this in Figure 2 below.\n![](https://assets.insightmediagroup.io/media/max/720/1*8r4SztjOR9OZp-Zt9FZVng.png)\nFigure 2. How maxpooling, average-pooling and convolution with stride 2 differ from each other [5].\nNext, the backbone of this YOLO version is now equipped with residual blocks which the idea is originated from ResNet. One thing that I want to emphasize regarding our implementation is the activation function within the residual block. You can see in Figure 3 below that according to the original ResNet paper, the second activation function is placed after the element-wise summation. However, based on the other tutorials that I read [6, 7], I found that in the case of YOLOv3 the second activation function is placed right after the weight layer instead (before summation). So later in the implementation, I decided to follow the guide in these tutorials since the YOLOv3 paper does not give any explanations about it.\n![](https://assets.insightmediagroup.io/media/max/720/1*PHwkhNUi3hl6RXwgjN6pHg.png)\nFigure 3. A residual block [8]\n### Darknet-53 With Detection Heads\nKeep in mind that the architecture in Figure 1 is only meant for classification. Thus, we need to replace everything after the last residual block if we want to make it compatible for detection tasks. Again, the original YOLOv3 paper also does not provide the detailed implementation guide, hence I decided to search for it and eventually got one from the paper referenced as [9]. I redraw the illustration from that paper to make the architecture looks clearer as shown in Figure 4 below.\n![](https://assets.insightmediagroup.io/media/max/720/1*3sMous_cRWrYEwxO2xRrCA.png)\nFigure 4. The YOLOv3 architecture [5].\nThere are actually lots of things to explain regarding the above architecture. Now let’s start from the part I refer to as the _detection heads_. Different from the previous YOLO versions which relied on a single head, here in YOLOv3 we have 2 additional heads. Thus, we will later have 3 prediction tensors for every single input image. These three detection heads have different specializations: the leftmost head (13×13) is the one responsible to detect large objects, the middle head (26×26) is for detecting medium-sized objects, and the one on the right (52×52) is used to detect objects of small size. We can think of the 52×52 tensor as the feature map that contains the detailed representation of an image, hence is suitable to detect small objects. Conversely, the 13×13 prediction tensor is meant to detect large objects because of its lower spatial resolution which is effective at capturing the general shape of an object.\nStill with the detection head, you can also see in Figure 4 that the three prediction tensors have 255 channels. To understand where this number comes from, we first need to know that each detection head has 3 prior boxes. Following the rule given in YOLOv2, each of these prior boxes is configured such that it can predict its own object category independently. With this mechanism, the feature vector of each grid cell can be obtained by computing B×(5+C), where _B_ is the number of prior boxes, _C_ is the number of object classes, and 5 is the _xywh_ and the bounding box confidence (a.k.a. _objectness_). In the case of YOLOv3, each detection head has 3 prior boxes and 80 classes, assuming that we train it on 80-class COCO dataset. By plugging these numbers to the formula, we obtain 3×(5+80)=255 prediction values for a single grid cell.\nIn fact, using multi-head mechanism like this allows the model to detect more objects as compared to the earlier YOLO versions. Previously in YOLOv1, since an image is divided into 7×7 grid cells and each of those can predict 2 bounding boxes, hence there are 98 objects possible to be detected. Meanwhile in YOLOv2, an image is divided into 13×13 grid cells in which a single cell is capable of generating 5 bounding boxes, making YOLOv2 able to detect up to 845 objects within a single image. This essentially allows YOLOv2 to have a better recall than YOLOv1. In theory, YOLOv3 is potentially able to achieve an even higher recall, especially when tested on an image that contains a lot of objects thanks to the larger number of possible detections. We can calculate the number of maximum bounding boxes for a single image in YOLOv3 by computing (13×13×3) + (26×26×3) + (52×52×3) = 507 + 2028 + 8112 = 10647, where 13×13, 26×26, and 52×25 are the number of grid cells within each prediction tensor, while 3 is the number of prior boxes a single grid cell has.\nWe can also see in Figure 4 that there are two concatenation steps incorporated in the network, i.e., between the original Darknet-53 architecture and the detection heads. The objective of these steps is to combine information from the deeper layer with the one from the shallower layer. Combining information from different depths like this is important because when it comes to detecting smaller objects, we do need both a detailed spatial information (contained in the shallower layer) and a better semantic information (contained in the deeper layer). Keep in mind that the feature map from the deeper layer has a smaller spatial dimension, hence we need to expand it before actually doing the concatenation. This is essentially the reason that we need to place an _upsampling_ layer right before we do the concatenation.\n### Multi-Label Classification\nApart from the architecture, the authors also changed the class labeling mechanism. Instead of using a standard multiclass classification paradigm, they proposed to use the so-called multilabel classification. If you’re not yet familiar with it, this is basically a method where an image can be assigned multiple labels at once. Take a look at Figure 5 below to better understand this idea. In this example, the image on the left belongs to the class _person_ , _athlete_ , _runner_ , and _man_ simultaneously. Later on, YOLOv3 is also expected to be able to make multiple class predictions on the same detected object.\n![](https://assets.insightmediagroup.io/media/max/720/1*mOQBTbKjBlhz_l4Y_QoKKA.png)\nFigure 5. Image with multiple labels [10].\nIn order for the model to predict multiple labels, we need to treat each class prediction output as an independent binary classifier. Look at Figure 6 below to see how multiclass classification differs from multilabel classification. The illustration on the left is a condition when we use a typical multiclass classification mechanism. Here you can see that the probabilities of all classes sum to 1 thanks to the nature of the softmax activation function within the output layer. In this example, since the class _camel_ is predicted with the highest probability, then the final prediction would be _camel_ regardless of how high the prediction confidence of the other classes is.\nOn the other hand, if we use multilabel classification, there is a possibility that the sum of all class prediction probabilities is greater than 1 because we use sigmoid activation function which by nature does not restrict the sum of all prediction confidence scores to 1. Thanks to this reason, later in the implementation we can simply apply a specific threshold to consider a class predicted. In the example below, if we assume that the threshold is 0.7, then the image will be predicted as both _cactus_ and _camel_.\n![](https://assets.insightmediagroup.io/media/max/720/1*II2EiDL7QYeqAXK3G5E7rw.png)\nFigure 6. Multiclass vs multilabel classification [10].\n### Modified Loss Function\nAnother modification the authors made was related to the loss function. Now look at the loss function of YOLOv1 in Figure 7 below. As a refresher, the 1st and 2nd rows are responsible to compute the bounding box loss, the 3rd and 4th rows are for the objectness confidence loss, and the 5th row is for computing the classification loss. Remember that in YOLOv1 the authors used SSE (Sum of Squared Errors) in all these five rows to make things simple.\n![](https://assets.insightmediagroup.io/media/max/720/1*dZe_pbOsA5FJ0RoUsl-0Hw.png)\nFigure 7. The loss function of YOLOv1 [5, 11].\nIn YOLOv3, the authors decided to replace the objectness loss (the 3rd and 4th rows) with _binary cross entropy_ , considering that the predictions corresponding to this part is only either 1 or 0, i.e., whether there is an object midpoint or not. Thus, it makes more sense to treat this as a binary classification rather than a regression problem.\nBinary cross entropy will also be used in the classification loss (5th row). This is essentially because we use multilabel classification mechanism we discussed earlier, where we treat each of the output neuron as an independent binary classifier. Remember that if we were to perform a standard classification task, we typically need to set the loss function to _categorical cross entropy_ instead.\nNow below is what the loss function looks like after we replace the SSE with binary cross entropy for the objectness (green) and the multilabel classification (blue) parts. Note that this equation is created based on the YouTube tutorial I watched given at reference number [12] because, again, the authors do not explicitly provide the final loss function in the paper.\n![](https://assets.insightmediagroup.io/media/max/720/1*AFHM1WaGRqCsNDaVUhZuWg.png)\nFigure 8. The loss function of YOLOv3 [5, 12].\n### Some Experimental Results\nWith all the modifications discussed above, the authors found that the improvement in performance is pretty impressive. The first experimental result I want to show you is related to the performance of the backbone model in classifying images on ImageNet dataset. You can see in Figure 9 below that the improvement from Darknet-19 (YOLOv2) to Darknet-53 (YOLOv3) is quite significant in terms of both the top-1 accuracy (74.1 to 77.2) and the top-5 accuracy (91.8 to 93.8). It is necessary to acknowledge that ResNet-101 and ResNet-152 indeed also perform as good as Darknet-53 in accuracy, but if we compare the FPS (measured on Nvidia Titan X), we can see that Darknet-53 is a lot faster than both ResNet variants.\n![](https://assets.insightmediagroup.io/media/max/720/1*PQbyfjIZc1Qh9T1uRz3y6A.png)\nFigure 9. Performance of different backbones on ImageNet classification dataset [1].\nThe similar behavior could also be observed on object detection task, where it is seen in Figure 10 that all YOLOv3 variants successfully obtained the fastest computation time among all other methods despite not having the best accuracy. You can see in the figure that the largest YOLOv3 variant is nearly 4 times faster than the largest RetinaNet variant (51 ms vs 198 ms). Moreover, the largest YOLOv3 variant itself already surpasses the mAP of the smallest RetinaNet variant (33.0 vs 32.5) while still having a faster inference time (51 ms vs 73 ms). These experimental results essentially prove that YOLOv3 became the state-of-the-art object detection model in terms of computational speed at that moment.\n![](https://assets.insightmediagroup.io/media/max/720/1*rhYRa9Vj0_WflxmKiLK7jA.png)\nFigure 10. Performance of YOLOv3 compared to other object detection methods [10].\n···\n## YOLOv3 Architecture Implementation\nAs we have already discussed pretty much everything about the theory behind YOLOv3, we can now start implementing the architecture from scratch. In Codeblock 1 below, I import the `torch` module and its `nn` submodule. Here I also initialize the `NUM_PRIORS` and `NUM_CLASS` variables, in which these two correspond to the number of prior boxes within each grid cell and the number of object classes in the dataset, respectively.\npython\n\n```\n# Codeblock 1\nimport torch\nimport torch.nn as nn\n\nNUM_PRIORS = 3\nNUM_CLASS  = 80\n```\n\n### Convolutional Block Implementation\nWhat I am going to implement first is the main building block of the network, which I refer to as the `Convolutional` block as seen in Codeblock 2. The structure of this block is quite a bit the same as the one used in YOLOv2, where it follows the _Conv-BN-Leaky ReLU_ pattern. When we use this kind of structure, don’t forget to set the `bias` parameter of the conv layer to `False` (at line `#(1)`) because using bias term is somewhat useless if we directly place a batch normalization layer right after it. Here I also configure the padding of the conv layer such that it will automatically set to 1 whenever the kernel size is 3×3 or 0 whenever we use 1×1 kernel (`#(2)`). Next, as the `conv`, `bn`, and `leaky_relu` have been initialized, we can simply connect them all using the code written inside the `forward()` method (`#(3)`).\npython\n\n```\n# Codeblock 2\nclass Convolutional(nn.Module):\n    def __init__(self, \n                 in_channels, \n                 out_channels, \n                 kernel_size, \n                 stride=1):\n        super().__init__()\n        \n        self.conv = nn.Conv2d(in_channels=in_channels,\n                              out_channels=out_channels, \n                              kernel_size=kernel_size, \n                              stride=stride,\n                              bias=False,                            #(1)\n                              padding=1 if kernel_size==3 else 0)    #(2)\n        \n        self.bn = nn.BatchNorm2d(num_features=out_channels)\n        \n        self.leaky_relu = nn.LeakyReLU(negative_slope=0.1)\n        \n    def forward(self, x):        #(3)\n        print(f'original\\t: {x.size()}')\n\n        x = self.conv(x)\n        print(f'after conv\\t: {x.size()}')\n        \n        x = self.bn(x)\n        print(f'after bn\\t: {x.size()}')\n        \n        x = self.leaky_relu(x)\n        print(f'after leaky relu: {x.size()}')\n        \n        return x\n```\n\nWe just want to ensure that our main building block is working properly, we will test it by simulating the very first `Convolutional` block in Figure 1. Remember that since YOLOv3 takes an image of size 416×416 as the input, here in Codeblock 3 I create a dummy tensor of that shape to simulate an image passed through that layer. Also, note that here I leave the stride to the default (1) because at this point we don’t want to perform spatial downsampling.\npython\n\n```\n# Codeblock 3\nconvolutional = Convolutional(in_channels=3,\n                              out_channels=32,\n                              kernel_size=3)\n\nx = torch.randn(1, 3, 416, 416)\nout = convolutional(x)\n```\n\nmarkup\n\n```\n# Codeblock 3 Outputoriginal         : torch.Size([1, 3, 416, 416])after conv       : torch.Size([1, 32, 416, 416])after bn         : torch.Size([1, 32, 416, 416])after leaky relu : torch.Size([1, 32, 416, 416])\n```\n\nNow let’s test our `Convolutional` block again, but this time I’ll set the stride to 2 to simulate the second convolutional block in the architecture. We can see in the output below that the spatial dimension halves from 416×416 to 208×208, indicating that this approach is a valid replacement for the maxpooling layers we previously had in YOLOv1 and YOLOv2.\npython\n\n```\n# Codeblock 4\nconvolutional = Convolutional(in_channels=32,\n                              out_channels=64,\n                              kernel_size=3, \n                              stride=2)\n\nx = torch.randn(1, 32, 416, 416)\nout = convolutional(x)\n```\n\nmarkup\n\n```\n# Codeblock 4 Outputoriginal          : torch.Size([1, 32, 416, 416])after conv        : torch.Size([1, 64, 208, 208])after bn          : torch.Size([1, 64, 208, 208])after leaky relu  : torch.Size([1, 64, 208, 208])\n```\n\n### Residual Block Implementation\nAs the `Convolutional` block is done, what I am going to do now is to implement the next building block: `Residual`. This block generally follows the structure I displayed back in Figure 3, where it consists of a residual connection that skips through two `Convolutional` blocks. Take a look at the Codeblock 5 below to see how I implement it.\nThe two convolution layers themselves follow the pattern in Figure 1, where the first `Convolutional` halves the number of channels (`#(1)`) which will then be doubled again by the second `Convolutional` (`#(3)`). Here you also need to note that the first convolution uses 1×1 kernel (`#(2)`) whereas the second one uses 3×3 (`#(4)`). Next, what we do inside the `forward()` method is simply connecting the two convolutions sequentially, which the final output is summed with the original input tensor (`#(5)`) before being returned.\npython\n\n```\n# Codeblock 5\nclass Residual(nn.Module):\n    def __init__(self, num_channels):\n        super().__init__()\n        self.conv0 = Convolutional(in_channels=num_channels, \n                                   out_channels=num_channels//2,   #(1)\n                                   kernel_size=1,       #(2)\n                                   stride=1)\n        \n        self.conv1 = Convolutional(in_channels=num_channels//2,\n                                   out_channels=num_channels,      #(3)\n                                   kernel_size=3,       #(4)\n                                   stride=1)\n        \n    def forward(self, x):\n        original = x.clone()\n        print(f'original\\t: {x.size()}')\n        \n        x = self.conv0(x)\n        print(f'after conv0\\t: {x.size()}')\n        \n        x = self.conv1(x)\n        print(f'after conv1\\t: {x.size()}')\n        \n        x = x + original      #(5)\n        print(f'after summation\\t: {x.size()}')\n        \n        return x\n```\n\nWe will now test the `Residual` block we just created using the Codeblock 6 below. Here I set the `num_channels` parameter to 64 because I want to simulate the very first residual block in the Darknet-53 architecture (see Figure 1).\npython\n\n```\n# Codeblock 6\nresidual = Residual(num_channels=64)\n\nx = torch.randn(1, 64, 208, 208)\nout = residual(x)\n```\n\nmarkup\n\n```\n# Codeblock 6 Outputoriginal        : torch.Size([1, 64, 208, 208])after conv0     : torch.Size([1, 32, 208, 208])after conv1     : torch.Size([1, 64, 208, 208])after summation : torch.Size([1, 64, 208, 208])\n```\n\nIf you take a closer look at the above output, you will notice that the shape of the input and output tensors are exactly the same. This essentially allows us to repeat multiple residual blocks easily. In the Codeblock 7 below I try to stack 4 residual blocks and pass a tensor through it, simulating the very last stack of residual blocks in the architecture.\npython\n\n```\n# Codeblock 7\nresiduals = nn.ModuleList([])\nfor _ in range(4):\n    residual = Residual(num_channels=1024)\n    residuals.append(residual)\n    \nx = torch.randn(1, 1024, 13, 13)\n\nfor i in range(len(residuals)):\n    x = residuals[i](x)\n    print(f'after residuals #{i}\\t: {x.size()}')\n```\n\nmarkup\n\n```\n# Codeblock 7 Outputafter residuals #0 : torch.Size([1, 1024, 13, 13])after residuals #1 : torch.Size([1, 1024, 13, 13])after residuals #2 : torch.Size([1, 1024, 13, 13])after residuals #3 : torch.Size([1, 1024, 13, 13])\n```\n\n### Darknet-53 Implementation\nUsing the `Convolutional` and `Residual` building blocks we created earlier, we can now actually construct the Darknet-53 model. Everything I initialize inside the `__init__()` method below is based on the architecture in Figure 1. However, remember that we need to stop at the last residual block since we don’t need the global average pooling and the fully-connected layers. Not only that, at the lines marked with `#(1)` and `#(2)` I store the intermediate feature maps in separate variables (`branch0` and `branch1`). We will later return these feature maps alongside the output from the main flow (`x`) (`#(3)`) to implement the branches that flow into the three detection heads.\npython\n\n```\n# Codeblock 8\nclass Darknet53(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.convolutional0 = Convolutional(in_channels=3,\n                                            out_channels=32,\n                                            kernel_size=3)\n        \n        self.convolutional1 = Convolutional(in_channels=32,\n                                            out_channels=64,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals0 = nn.ModuleList([Residual(num_channels=64) for _ in range(1)])\n        \n        self.convolutional2 = Convolutional(in_channels=64,\n                                            out_channels=128,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals1 = nn.ModuleList([Residual(num_channels=128) for _ in range(2)])\n        \n        self.convolutional3 = Convolutional(in_channels=128,\n                                            out_channels=256,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals2 = nn.ModuleList([Residual(num_channels=256) for _ in range(8)])\n        \n        self.convolutional4 = Convolutional(in_channels=256,\n                                            out_channels=512,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals3 = nn.ModuleList([Residual(num_channels=512) for _ in range(8)])\n        \n        self.convolutional5 = Convolutional(in_channels=512,\n                                            out_channels=1024,\n                                            kernel_size=3,\n                                            stride=2)\n        \n        self.residuals4 = nn.ModuleList([Residual(num_channels=1024) for _ in range(4)])\n        \n    def forward(self, x):\n        print(f'original\\t\\t: {x.size()}\\n')\n        \n        x = self.convolutional0(x)\n        print(f'after convolutional0\\t: {x.size()}')\n        \n        x = self.convolutional1(x)\n        print(f'after convolutional1\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals0)):\n            x = self.residuals0[i](x)\n            print(f'after residuals0 #{i}\\t: {x.size()}')\n        \n        x = self.convolutional2(x)\n        print(f'\\nafter convolutional2\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals1)):\n            x = self.residuals1[i](x)\n            print(f'after residuals1 #{i}\\t: {x.size()}')\n            \n        x = self.convolutional3(x)\n        print(f'\\nafter convolutional3\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals2)):\n            x = self.residuals2[i](x)\n            print(f'after residuals2 #{i}\\t: {x.size()}')\n        \n        branch0 = x.clone()           #(1)\n            \n        x = self.convolutional4(x)\n        print(f'\\nafter convolutional4\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals3)):\n            x = self.residuals3[i](x)\n            print(f'after residuals3 #{i}\\t: {x.size()}')\n        \n        branch1 = x.clone()           #(2)\n            \n        x = self.convolutional5(x)\n        print(f'\\nafter convolutional5\\t: {x.size()}\\n')\n        \n        for i in range(len(self.residuals4)):\n            x = self.residuals4[i](x)\n            print(f'after residuals4 #{i}\\t: {x.size()}')\n            \n        return branch0, branch1, x    #(3)\n```\n\nNow we test our `Darknet53` class by running the Codeblock 9 below. You can see in the resulting output that everything seems to work properly as the shape of the tensor correctly transforms according to the guide in Figure 1. One thing that I haven’t mentioned before is that this Darknet-53 architecture downscales the input image by a factor of 32. So, with this downsampling factor, an input image of shape 256×256 will become 8×8 in the end (as shown in Figure 1), whereas an input of shape 416×416 will result in a 13×13 prediction tensor.\npython\n\n```\n# Codeblock 9\ndarknet53 = Darknet53()\n\nx = torch.randn(1, 3, 416, 416)\nout = darknet53(x)\n```\n\nmarkup\n\n```\n# Codeblock 9 Outputoriginal              : torch.Size([1, 3, 416, 416])after convolutional0  : torch.Size([1, 32, 416, 416])after convolutional1  : torch.Size([1, 64, 208, 208])after residuals0 #0   : torch.Size([1, 64, 208, 208])after convolutional2  : torch.Size([1, 128, 104, 104])after residuals1 #0   : torch.Size([1, 128, 104, 104])after residuals1 #1   : torch.Size([1, 128, 104, 104])after convolutional3  : torch.Size([1, 256, 52, 52])after residuals2 #0   : torch.Size([1, 256, 52, 52])after residuals2 #1   : torch.Size([1, 256, 52, 52])after residuals2 #2   : torch.Size([1, 256, 52, 52])after residuals2 #3   : torch.Size([1, 256, 52, 52])after residuals2 #4   : torch.Size([1, 256, 52, 52])after residuals2 #5   : torch.Size([1, 256, 52, 52])after residuals2 #6   : torch.Size([1, 256, 52, 52])after residuals2 #7   : torch.Size([1, 256, 52, 52])after convolutional4  : torch.Size([1, 512, 26, 26])after residuals3 #0   : torch.Size([1, 512, 26, 26])after residuals3 #1   : torch.Size([1, 512, 26, 26])after residuals3 #2   : torch.Size([1, 512, 26, 26])after residuals3 #3   : torch.Size([1, 512, 26, 26])after residuals3 #4   : torch.Size([1, 512, 26, 26])after residuals3 #5   : torch.Size([1, 512, 26, 26])after residuals3 #6   : torch.Size([1, 512, 26, 26])after residuals3 #7   : torch.Size([1, 512, 26, 26])after convolutional5  : torch.Size([1, 1024, 13, 13])after residuals4 #0   : torch.Size([1, 1024, 13, 13])after residuals4 #1   : torch.Size([1, 1024, 13, 13])after residuals4 #2   : torch.Size([1, 1024, 13, 13])after residuals4 #3   : torch.Size([1, 1024, 13, 13])\n```\n\nAt this point we can also see what the outputs produced by the three branches look like simply by printing out the shapes of `branch0`, `branch1`, and `x` as shown in Codeblock 10 below. Notice that the spatial dimensions of these three tensors vary. Later on, the tensors from the deeper layers will be upsampled so that we can perform channel-wise concatenation with those from the shallower ones.\npython\n\n```\n# Codeblock 10\nprint(out[0].shape)      # branch0\nprint(out[1].shape)      # branch1\nprint(out[2].shape)      # x\n```\n\nmarkup\n\n```\n# Codeblock 10 Outputtorch.Size([1, 256, 52, 52])torch.Size([1, 512, 26, 26])torch.Size([1, 1024, 13, 13])\n```\n\n### Detection Head Implementation\nIf you go back to Figure 4, you will notice that each of the detection heads consists of two convolution layers. However, these two convolutions are not identical. In Codeblock 11 below I use the `Convolutional` block for the first one and the plain `nn.Conv2d` for the second one. This is essentially done because the second convolution acts as the final layer, hence is responsible for giving raw output (instead of being normalized and ReLU-ed).\npython\n\n```\n# Codeblock 11\nclass DetectionHead(nn.Module):\n    def __init__(self, num_channels):\n        super().__init__()\n        \n        self.convhead0 = Convolutional(in_channels=num_channels,\n                                       out_channels=num_channels*2,\n                                       kernel_size=3)\n        \n        self.convhead1 = nn.Conv2d(in_channels=num_channels*2, \n                                   out_channels=NUM_PRIORS*(NUM_CLASS+5), \n                                   kernel_size=1)\n        \n    def forward(self, x):\n        print(f'original\\t: {x.size()}')\n        \n        x = self.convhead0(x)\n        print(f'after convhead0\\t: {x.size()}')\n        \n        x = self.convhead1(x)\n        print(f'after convhead1\\t: {x.size()}')\n   \n        return x\n```\n\nNow in Codeblock 12 I’ll try to simulate the 13×13 detection head, hence I set the input feature map to have the shape of 512×13×13 (`#(1)`). By the way you’ll know where the number 512 comes from later in the subsequent section.\npython\n\n```\n# Codeblock 12\ndetectionhead = DetectionHead(num_channels=512)\n\nx = torch.randn(1, 512, 13, 13)    #(1)\nout = detectionhead(x)\n```\n\nAnd below is what the resulting output looks like. We can see here that the tensor expands to 1024×13×13 before eventually shrink to 255×13×13. Remember that in YOLOv3 as long as we set `NUM_PRIORS` to 3 and `NUM_CLASS` to 80, the number of output channel will always be 255 regardless of the number of input channel fed into the `DetectionHead`.\nmarkup\n\n```\n# Codeblock 12 Outputoriginal        : torch.Size([1, 512, 13, 13])after convhead0 : torch.Size([1, 1024, 13, 13])after convhead1 : torch.Size([1, 255, 13, 13])\n```\n\n### The Entire YOLOv3 Architecture\nOkay now — since we have initialized the main building blocks, what we need to do next is to construct the entire YOLOv3 architecture. Here I will also discuss the remaining components we haven’t covered. The code is quite long though, so I break it down into two codeblocks: Codeblock 13a and Codeblock 13b. Just ensure that these two codeblocks are written within the same notebook cell if you want to run it on your own.\nIn Codeblock 13a below, what we do first is to initialize the backbone model (`#(1)`). Next, we create a stack of 5 `Convolutional` blocks which alternately halves and doubles the number of channels. The conv block that reduces the channel count uses 1×1 kernel while the one that increases it uses 3×3 kernel, just like the structure we use in the `Residual` block. We initialize this stack of 5 convolutions for the three detection heads. Specifically for the feature maps that flow into the 26×26 and 52×52 heads, we need to initialize another convolution layer (`#(2)` and `#(4)`) and an upsampling layer (`#(3)` and `#(5)`) in addition to the 5 convolutions.\npython\n\n```\n# Codeblock 13a\nclass YOLOv3(nn.Module):\n    def __init__(self):\n        super().__init__()\n        \n        ###############################################\n        # Backbone initialization.\n        \n        self.darknet53 = Darknet53()    #(1)\n        \n        \n        ###############################################\n        # For 13x13 output.\n        \n        self.conv0  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        self.conv1  = Convolutional(in_channels=512, out_channels=1024, kernel_size=3)\n        self.conv2  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        self.conv3  = Convolutional(in_channels=512, out_channels=1024, kernel_size=3)\n        self.conv4  = Convolutional(in_channels=1024, out_channels=512, kernel_size=1)\n        \n        self.detection_head_large_obj = DetectionHead(num_channels=512)\n        \n        \n        ###############################################\n        # For 26x26 output.\n        \n        self.conv5  = Convolutional(in_channels=512, out_channels=256, kernel_size=1)  #(2)\n        self.upsample0 = nn.Upsample(scale_factor=2)      #(3)\n        \n        self.conv6  = Convolutional(in_channels=768, out_channels=256, kernel_size=1)\n        self.conv7  = Convolutional(in_channels=256, out_channels=512, kernel_size=3)\n        self.conv8  = Convolutional(in_channels=512, out_channels=256, kernel_size=1)\n        self.conv9  = Convolutional(in_channels=256, out_channels=512, kernel_size=3)\n        self.conv10 = Convolutional(in_channels=512, out_channels=256, kernel_size=1)\n        \n        self.detection_head_medium_obj = DetectionHead(num_channels=256)\n        \n        \n        ###############################################\n        # For 52x52 output.\n        \n        self.conv11  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)  #(4)\n        self.upsample1 = nn.Upsample(scale_factor=2)      #(5)\n        \n        self.conv12  = Convolutional(in_channels=384, out_channels=128, kernel_size=1)\n        self.conv13  = Convolutional(in_channels=128, out_channels=256, kernel_size=3)\n        self.conv14  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)\n        self.conv15  = Convolutional(in_channels=128, out_channels=256, kernel_size=3)\n        self.conv16  = Convolutional(in_channels=256, out_channels=128, kernel_size=1)\n        \n        self.detection_head_small_obj = DetectionHead(num_channels=128)\n```\n\nNow in Codeblock 13b we define the flow of the network inside the `forward()` method. Here we first pass the input tensor through the `darknet53` model (`#(1)`), which produces 3 output tensors: `branch0`, `branch1`, and `x`. Then, what we do next is to connect the layers one after another according to the flow given in Figure 4.\npython\n\n```\n# Codeblock 13b\n    def forward(self, x):\n        \n        ###############################################\n        # Backbone.\n        branch0, branch1, x = self.darknet53(x)      #(1)\n        print(f'branch0\\t\\t\\t: {branch0.size()}')\n        print(f'branch1\\t\\t\\t: {branch1.size()}')\n        print(f'x\\t\\t\\t: {x.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 13x13 detection head.\n        \n        x = self.conv0(x)\n        print(f'after conv0\\t\\t: {x.size()}')\n        \n        x = self.conv1(x)\n        print(f'after conv1\\t\\t: {x.size()}')\n        \n        x = self.conv2(x)\n        print(f'after conv2\\t\\t: {x.size()}')\n        \n        x = self.conv3(x)\n        print(f'after conv3\\t\\t: {x.size()}')\n        \n        x = self.conv4(x)\n        print(f'after conv4\\t\\t: {x.size()}')\n        \n        large_obj = self.detection_head_large_obj(x)\n        print(f'large object detection\\t: {large_obj.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 26x26 detection head.\n        \n        x = self.conv5(x)\n        print(f'after conv5\\t\\t: {x.size()}')\n        \n        x = self.upsample0(x)\n        print(f'after upsample0\\t\\t: {x.size()}')\n        \n        x = torch.cat([x, branch1], dim=1)\n        print(f'after concatenate\\t: {x.size()}')\n        \n        x = self.conv6(x)\n        print(f'after conv6\\t\\t: {x.size()}')\n        \n        x = self.conv7(x)\n        print(f'after conv7\\t\\t: {x.size()}')\n        \n        x = self.conv8(x)\n        print(f'after conv8\\t\\t: {x.size()}')\n        \n        x = self.conv9(x)\n        print(f'after conv9\\t\\t: {x.size()}')\n        \n        x = self.conv10(x)\n        print(f'after conv10\\t\\t: {x.size()}')\n        \n        medium_obj = self.detection_head_medium_obj(x)\n        print(f'medium object detection\\t: {medium_obj.size()}\\n')\n        \n        \n        ###############################################\n        # Flow to 52x52 detection head.\n        \n        x = self.conv11(x)\n        print(f'after conv11\\t\\t: {x.size()}')\n        \n        x = self.upsample1(x)\n        print(f'after upsample1\\t\\t: {x.size()}')\n        \n        x = torch.cat([x, branch0], dim=1)\n        print(f'after concatenate\\t: {x.size()}')\n        \n        x = self.conv12(x)\n        print(f'after conv12\\t\\t: {x.size()}')\n        \n        x = self.conv13(x)\n        print(f'after conv13\\t\\t: {x.size()}')\n        \n        x = self.conv14(x)\n        print(f'after conv14\\t\\t: {x.size()}')\n        \n        x = self.conv15(x)\n        print(f'after conv15\\t\\t: {x.size()}')\n        \n        x = self.conv16(x)\n        print(f'after conv16\\t\\t: {x.size()}')\n        \n        small_obj = self.detection_head_small_obj(x)\n        print(f'small object detection\\t: {small_obj.size()}\\n')\n        \n\n        ###############################################\n        # Return prediction tensors.\n        \n        return large_obj, medium_obj, small_obj\n```\n\nAs we have completed the `forward()` method, we can now test the entire YOLOv3 model by passing a single RGB image of size 416×416 as shown in Codeblock 14.\npython\n\n```\n# Codeblock 14\nyolov3 = YOLOv3()\n\nx = torch.randn(1, 3, 416, 416)\nout = yolov3(x)\n```\n\nBelow is what the output looks like after you run the codeblock above. Here we can see that everything seems to work properly as the dummy image successfully passed through all layers in the network. One thing that you might probably need to know is that the 768-channel feature map at line `#(4)` is obtained from the concatenation between the tensor at lines `#(2)` and `#(3)`. The similar thing also applies to the 384-channel tensor at line `#(6)`, in which it is the concatenation between the feature maps at lines `#(1)` and `#(5)`.\nmarkup\n\n```\n# Codeblock 14 Outputbranch0                 : torch.Size([1, 256, 52, 52])    #(1)branch1                 : torch.Size([1, 512, 26, 26])    #(2)x                       : torch.Size([1, 1024, 13, 13])after conv0             : torch.Size([1, 512, 13, 13])after conv1             : torch.Size([1, 1024, 13, 13])after conv2             : torch.Size([1, 512, 13, 13])after conv3             : torch.Size([1, 1024, 13, 13])after conv4             : torch.Size([1, 512, 13, 13])large object detection  : torch.Size([1, 255, 13, 13])after conv5             : torch.Size([1, 256, 13, 13])after upsample0         : torch.Size([1, 256, 26, 26])    #(3)after concatenate       : torch.Size([1, 768, 26, 26])    #(4)after conv6             : torch.Size([1, 256, 26, 26])after conv7             : torch.Size([1, 512, 26, 26])after conv8             : torch.Size([1, 256, 26, 26])after conv9             : torch.Size([1, 512, 26, 26])after conv10            : torch.Size([1, 256, 26, 26])medium object detection : torch.Size([1, 255, 26, 26])after conv11            : torch.Size([1, 128, 26, 26])after upsample1         : torch.Size([1, 128, 52, 52])    #(5)after concatenate       : torch.Size([1, 384, 52, 52])    #(6)after conv12            : torch.Size([1, 128, 52, 52])after conv13            : torch.Size([1, 256, 52, 52])after conv14            : torch.Size([1, 128, 52, 52])after conv15            : torch.Size([1, 256, 52, 52])after conv16            : torch.Size([1, 128, 52, 52])small object detection  : torch.Size([1, 255, 52, 52])\n```\n\nAnd just to make things clearer here I also print out the output of each detection head in Codeblock 15 below. We can see here that all the resulting prediction tensors have the shape that we expected earlier. Thus, I believe our YOLOv3 implementation is correct and hence ready to train.\npython\n\n```\n# Codeblock 15\nprint(out[0].shape)\nprint(out[1].shape)\nprint(out[2].shape)\n```\n\nmarkup\n\n```\n# Codeblock 15 Outputtorch.Size([1, 255, 13, 13])torch.Size([1, 255, 26, 26])torch.Size([1, 255, 52, 52])\n```\n\n···\nI think that’s pretty much everything about YOLOv3 and its architecture implementation from scratch. As we’ve seen above, the authors successfully made an impressive improvement in performance compared to the previous YOLO version, even though the changes they made to the system were not what they considered significant — hence the title “ _An Incremental Improvement_.” \nPlease let me know if you spot any mistake in this article. You can also find the fully-working code in my GitHub repo [13]. Thanks for reading, see ya in my next article!\n···\n## References\n[1] Joseph Redmon and Ali Farhadi. YOLOv3: An Incremental Improvement. Arxiv. <https://arxiv.org/abs/1804.02767> [Accessed August 24, 2025].\n[2] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv1 Paper Walkthrough: The Day YOLO First Saw the World. Medium. <https://ai.gopubby.com/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b> [Accessed March 1, 2026].\n[3] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv1 Loss Function Walkthrough: Regression for All. Medium. <https://ai.gopubby.com/yolov1-loss-function-walkthrough-regression-for-all-18c34be6d7cb> [Accessed March 1, 2026].\n[4] [Muhammad Ardi](https://medium.com/u/9801a58700ac). YOLOv2 & YOLO9000 Paper Walkthrough: Better, Faster, Stronger. Towards Data Science. <https://towardsdatascience.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger/> [Accessed March 1, 2026].\n[5] Image originally created by author.\n[6] YOLO v3 introduction to object detection with TensorFlow 2. PyLessons. <https://pylessons.com/YOLOv3-TF2-introduction> [Accessed August 24, 2025].\n[7] aladdinpersson. YOLOv3. GitHub. <https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/Pytorch/object_detection/YOLOv3/model.py> [Accessed August 24, 2025].\n[8] Kaiming He _et al._ Deep Residual Learning for Image Recognition. Arxiv. <https://arxiv.org/abs/1512.03385> [Accessed August 24, 2025].\n[9] Langcai Cao _et al._ A Text Detection Algorithm for Image of Student Exercises Based on CTPN and Enhanced YOLOv3. IEEE Access. <https://ieeexplore.ieee.org/document/9200481> [Accessed August 24, 2025].\n[10] Image by author, partially generated by Gemini.\n[11] Joseph Redmon _et al._ You Only Look Once: Unified, Real-Time Object Detection. Arxiv. <https://arxiv.org/pdf/1506.02640> [Accessed July 5, 2025].\n[12] ML For Nerds. YOLO-V3: An Incremental Improvement || YOLO OBJECT DETECTION SERIES. YouTube. [https://www.youtube.com/watch?v=9fhAbvPWzKs&t=174s](https://www.youtube.com/watch?v=9fhAbvPWzKs&t=174s) [Accessed August 24, 2025].\n[13] MuhammadArdiPutra. Even Better, but Not That Much — YOLOv3. GitHub. <https://github.com/MuhammadArdiPutra/medium_articles/blob/main/Even%20Better%2C%20but%20Not%20That%20Much%20-%20YOLOv3.ipynb> [Accessed August 24, 2025].\n## Register Now to Continue Reading\n### Unlock Towards Data Science with a Free Account\nAll articles on Towards Data Science are available to registered users. Create a free account today to access this article and thousands more.\nRegister for freeSign in to your account\n* * *\n![](https://assets.insightmediagroup.io/media/1787657598287_taxhya.jpeg)\nWritten By\n## [Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nDeep learning storyteller\n[See all from Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\n[Deep Dives](https://towardsdatascience.com/tag/deep-dives/)[Computer Vision](https://towardsdatascience.com/tag/computer-vision/)[Deep Learning](https://towardsdatascience.com/tag/deep-learning/)[Neural Network](https://towardsdatascience.com/tag/neural-network/)[Pytorch](https://towardsdatascience.com/tag/pytorch/)\nShare This Article\n  * [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&title=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n  * [](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&title=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n  * [](https://x.com/share?url=https%3A%2F%2Ftowardsdatascience.com%2Fyolov3-paper-walkthrough-even-better-but-not-that-much%2F&text=YOLOv3%20Paper%20Walkthrough%3A%20Even%20Better%2C%20But%20Not%20That%C2%A0Much)\n\n\nTowards Data Science is a community publication. Submit your insights to reach our global audience and earn through the TDS Author Payment Program.\n[Write for TDS](https://towardsdatascience.com/submissions/)\n## Related Articles\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2020/10/12dr6lEj1nAo8R1JRBmO1sw.png)](https://towardsdatascience.com/histogram-matching-ee3a67b4cbc1/)\n## [Histogram Matching](https://towardsdatascience.com/histogram-matching-ee3a67b4cbc1/)\n[](https://towardsdatascience.com/yolov3-paper-walkthrough-even-better-but-not-that-much/)\nHow to generate a histogram for an image, how to equalize the histogram, and finally how to modify your image histogram to be similar to...\n[Ali Pourramezan Fard](https://towardsdatascience.com/author/aliprf/)\nOctober 16, 20205 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2017/09/1lZz2I1gM53ADKVq0tiusSQ.png)](https://towardsdatascience.com/artificial-intelligence-get-your-users-to-label-your-data-b5fa7c0c9e00/)\n## [Artificial Intelligence: Get your users to label your data](https://towardsdatascience.com/artificial-intelligence-get-your-users-to-label-your-data-b5fa7c0c9e00/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nI wanted to call this article \"Parasitic Labeling of AI Training Data,\" but apparently that's too complicated. What I want to tell you...\n[Daniel Shapiro, PhD](https://towardsdatascience.com/author/lemaysolutions/)\nSeptember 19, 20174 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2020/09/1CYjN9p8Vpy7rwXunpoNyIA.png)](https://towardsdatascience.com/practical-introduction-to-automation-music-transcription-3ad8ad40eab6/)\n## [Practical Introduction to Automation Music Transcription](https://towardsdatascience.com/practical-introduction-to-automation-music-transcription-3ad8ad40eab6/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nFrom Audio to Model Input, BiLSTM Model Application and Even Segmentation Method\n[Yue Hu](https://towardsdatascience.com/author/yhu0206/)\nSeptember 28, 202012 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2024/07/0XG-hKElA13JBm3qX-scaled.jpg)](https://towardsdatascience.com/whats-new-in-computer-vision-and-object-detection-8e586c4429e3/)\n## [What's New in Computer Vision and Object Detection?](https://towardsdatascience.com/whats-new-in-computer-vision-and-object-detection-8e586c4429e3/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nOur weekly selection of must-read Editors' Picks and original features\n[TDS Editors](https://towardsdatascience.com/author/towardsdatascience/)\nJuly 11, 20243 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2021/11/0gixVHrd0ndFDwZzH-scaled.jpg)](https://towardsdatascience.com/pondernet-explained-5e9571e657d/)\n## [PonderNet explained](https://towardsdatascience.com/pondernet-explained-5e9571e657d/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nImplementing a pondering network for the MNIST dataset\n[Conrad Cardona](https://towardsdatascience.com/author/conradcardona/)\nNovember 17, 202113 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2021/02/0E1Gq_cVH7GcBmnM7-scaled.jpg)](https://towardsdatascience.com/audio-deep-learning-made-simple-part-3-data-preparation-and-augmentation-24c6e1f6b52/)\n## [Audio Deep Learning Made Simple (Part 3): Data Preparation and Augmentation](https://towardsdatascience.com/audio-deep-learning-made-simple-part-3-data-preparation-and-augmentation-24c6e1f6b52/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA Gentle Guide to enhancing Spectrogram features for optimal performance. Also Data Augmentation, in Plain English\n[Ketan Doshi](https://towardsdatascience.com/author/ketanhdoshi/)\nFebruary 24, 20217 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2024/11/1SUhj4n10S8PWllp3-yQZtw.png)](https://towardsdatascience.com/llms-txt-414d5121bcb3/)\n## [LLMs.txt Explained](https://towardsdatascience.com/llms-txt-414d5121bcb3/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nYour guide to the web's new LLM-ready content standard\n[Derick Ruiz](https://towardsdatascience.com/author/dericksozo/)\nNovember 28, 20245 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2021/12/1mkYI9nbLDj2_tkKrX-qBgQ-scaled.jpeg)](https://towardsdatascience.com/road-signs-driving-you-crazy-e686d97f2480/)\n## [Road signs \"driving\" you crazy?](https://towardsdatascience.com/road-signs-driving-you-crazy-e686d97f2480/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nHere's how I built a program that reads them for you\n[Tara Prole](https://towardsdatascience.com/author/taraprole/)\nDecember 5, 202113 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2018/12/1oCawxkOXAheeGGXUNsCEpw.jpeg)](https://towardsdatascience.com/deep-transfer-learning-for-natural-language-processing-text-classification-with-universal-1a2c69e5baa9/)\n## [Deep Transfer Learning for Natural Language Processing - Text Classification with Universal...](https://towardsdatascience.com/deep-transfer-learning-for-natural-language-processing-text-classification-with-universal-1a2c69e5baa9/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA Guide to Demystifying Universal Sentence Encoders\n[Dipanjan (DJ) Sarkar](https://towardsdatascience.com/author/djsarkar/)\nDecember 4, 201818 min read\n\n\n  * [YouTube](https://www.youtube.com/c/TowardsDataScience)\n  * [X](https://x.com/TDataScience)\n  * [LinkedIn](https://www.linkedin.com/company/towards-data-science/)\n  * [Threads](https://www.threads.net/@towardsdatascience)\n  * [Bluesky](https://bsky.app/profile/towardsdatascience.com)\n\n\n[](https://towardsdatascience.com/)\nYour home for data science and AI. The world's leading publication for data science, data analytics, data engineering, machine learning, and artificial intelligence professionals.\nSubscribe 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/)\n© Insight Media Group, LLC 2026•[The New Stack](https://thenewstack.io)•[roadmap.sh](https://roadmap.sh)•[Creator Network](https://insightmediagroup.io/creator-network)\n####  Recommended Articles \nClose\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607686/image?etag=1782161464448&width=500&height=500) How Relevance Models Foreshadowed Transformers for NLP ](https://towardsdatascience.com/how-relevance-models-foreshadowed-transformers-for-nlp/ \"How Relevance Models Foreshadowed Transformers for NLP\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607639/image?etag=1781250125848&width=500&height=500) Javascript Fatigue: HTMX Is All You Need to Build ChatGPT — Part 1 ](https://towardsdatascience.com/javascript-fatigue-you-dont-need-js-to-build-chatgpt/ \"Javascript Fatigue: HTMX Is All You Need to Build ChatGPT — Part 1\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/607930/image?etag=1782124566054&width=500&height=500) The Subset Sum Problem Solved in Linear Time for Dense Enough Inputs ](https://towardsdatascience.com/subset-sum-problem-solved-in-linear-time-for-dense-enough-inputs/ \"The Subset Sum Problem Solved in Linear Time for Dense Enough Inputs\")\n  * [ ![](https://h030.towardsdatascience.com/rest/v2/contentStores/d7d3a4ab-7caf-4383-bbd2-fbf7fb4f1787/items/3660/image?etag=1780176246070&width=500&height=500) Does Your Company Have a Data Strategy? ](https://towardsdatascience.com/does-your-company-have-a-data-strategy-e8944dbec4ae/ \"Does Your Company Have a Data Strategy?\")\n\n\n",
  "body_base_path": "attachments/0d9726dbcb9e/pages/",
  "body_markdown_path": "attachments/0d9726dbcb9e/pages/archived-body.md",
  "local_html": "attachments/0d9726dbcb9e/pages/archived-body.html",
  "body_source": "prior_capture"
}