{
  "source_file": "0930",
  "source_platform": "Medium",
  "author": "Muhammad Ardi Putra",
  "title_listed": "Diffusion Model Paper Walkthrough: The Art of Noise",
  "keywords": "",
  "domain": "图像处理",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://towardsdatascience.com/the-art-of-noise/",
  "publish_date": "2025-04-02",
  "corpus_stage": "",
  "notes": "",
  "id": "bea8a2022ef0",
  "collected_at": "2026-10-02T02:55:56",
  "title": "Diffusion Model Paper Walkthrough: The Art of Noise | 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[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\n# Diffusion Model Paper Walkthrough: The Art of Noise\nUnderstanding and implementing a diffusion model from scratch with PyTorch\n[Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nApril 2, 202533 min read\n![](../images/000.jpg)\nPhoto by Jr Korpa on Unsplash\n## **Introduction**\nIn my last several articles I talked about generative deep learning algorithms, which mostly are related to text generation tasks. So, I think it would be interesting to switch to generative algorithms for image generation now. We knew that nowadays there have been plenty of deep learning models specialized for generating images out there, such as Autoencoder, Variational Autoencoder (VAE), Generative Adversarial Network (GAN) and Neural Style Transfer (NST). I actually got some of my writings about these topics posted on Medium as well. I provide you the links at the end of this article if you want to read them.\nIn today’s article, I would like to discuss the so-called _diffusion model_ — one of the most impactful models in the field of deep learning for image generation. The idea of this algorithm was first proposed in the paper titled _Deep Unsupervised Learning using Nonequilibrium Thermodynamics_ written by Sohl-Dickstein _et al._ back in 2015 [1]. Their framework was then developed further by Ho _et al._ in 2020 in their paper titled _Denoising Diffusion Probabilistic Models_ [2]. _DDPM_ was later adapted by OpenAI and Google to develop DALLE-2 and Imagen, which we knew that these models have impressive capabilities to generate high-quality images.\nLearn this step by step with the interactive [Machine Learning](https://roadmap.sh/machine-learning) roadmap.\n### **How Diffusion Model Works**\nGenerally speaking, diffusion model works by generating image from noise. We can think of it like an artist transforming a splash of paint on a canvas into a beautiful artwork. In order to do so, the diffusion model needs to be trained first. There are two main steps required to be followed to train the model, namely _forward diffusion_ and _backward diffusion_.\n![](../images/001.png)\nFigure 1. The forward and backward diffusion process [3].\nAs you can see in the above figure, forward diffusion is a process where Gaussian noise is applied to the original image iteratively. We keep adding the noise until the image is completely unrecognizable, at which point we can say that the image now lies in the _latent space_. Different from Autoencoders and GANs where the latent space typically has a lower dimension than the original image, the latent space in DDPM maintains the exact same dimensionality as the original one. This noising process follows the principle of a Markov Chain, meaning that the image at timestep _t_ is affected only by timestep _t_ -1. Forward diffusion is considered easy since what we basically do is just adding some noise step by step.\nThe second training phase is called backward diffusion, which our objective here is to remove the noise little by little until we obtain a clear image. This process follows the principle of the _reverse_ Markov Chain, where the image at timestep _t_ -1 can only be obtained based on the image at timestep _t_. Such a denoising process is really difficult since we need to guess which pixels are noise and which ones belong to the actual image content. Thus, we need to employ a neural network model to do so.\nDDPM uses U-Net as the basis of the deep learning architecture for backward diffusion. However, instead of using the original U-Net model [4], we need to make several modifications to it so that it will be more suitable for our task. Later on, I am going to train this model on the MNIST Handwritten Digit dataset [5], and we will see whether it can generate similar images.\nWell, that was pretty much all the fundamental concepts you need to know about diffusion models for now. In the next sections we are going to get even deeper into the details while implementing the algorithm from scratch.\n···\n## **PyTorch Implementation**\nWe are going to start by importing the required modules. In case you’re not yet familiar with the imports below, both `torch` and `torchvision` are the libraries we’ll use for preparing the model and the dataset. Meanwhile, `matplotlib` and `tqdm` will help us display images and progress bars.\npython\n\n```\n# Codeblock 1\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\n\nfrom torch.optim import Adam\nfrom torch.utils.data import DataLoader\nfrom torchvision import datasets, transforms\nfrom tqdm import tqdm\n```\n\nAs the modules have been imported, the next thing to do is to initialize some config parameters. Look at the Codeblock 2 below for the details.\npython\n\n```\n# Codeblock 2\nIMAGE_SIZE     = 28     #(1)\nNUM_CHANNELS   = 1      #(2)\n\nBATCH_SIZE     = 2\nNUM_EPOCHS     = 10\nLEARNING_RATE  = 0.001\n\nNUM_TIMESTEPS  = 1000   #(3)\nBETA_START     = 0.0001 #(4)\nBETA_END       = 0.02   #(5)\nTIME_EMBED_DIM = 32     #(6)\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available else \"cpu\")  #(7)\nDEVICE\n```\n\nmarkup\n\n```\n# Codeblock 2 Outputdevice(type='cuda')\n```\n\nAt the lines marked with `#(1)` and `#(2)` I set `IMAGE_SIZE` and `NUM_CHANNELS` to 28 and 1, which these numbers are obtained from the image dimension in the MNIST dataset. The `BATCH_SIZE`, `NUM_EPOCHS`, and `LEARNING_RATE` variables are pretty straightforward, so I don’t think I need to explain them further.\nAt line `#(3)`, the variable `NUM_TIMESTEPS` denotes the number of iterations in the forward and backward diffusion process. Timestep 0 is the condition where the image is in its original state (the leftmost image in Figure 1). In this case, since we set this parameter to 1000, timestep number 999 is going to be the condition where the image is completely unrecognizable (the rightmost image in Figure 1). It is important to keep in mind that the choice of the number of timesteps involves a tradeoff between model accuracy and computational cost. If we assign a small value for `NUM_TIMESTEPS`, the inference time is going to be shorter, yet the resulting image might not be really good since the model has fewer steps to refine the image in the backward diffusion stage. On the other hand, increasing `NUM_TIMESTEPS` will slow down the inference process, but we can expect the output image to have better quality thanks to the gradual denoising process which results in a more precise reconstruction.\nNext, the `BETA_START` (`#(4)`) and `BETA_END` (`#(5)`) variables are used to control the amount of Gaussian noise added at each timestep, whereas `TIME_EMBED_DIM` (`#(6)`) is employed to determine the feature vector length for storing the timestep information. Lastly, at line `#(7)` I assign `“cuda”` to the `DEVICE` variable if PyTorch detects GPU installed in our machine. I highly recommend you run this project on GPU since training a diffusion model is computationally expensive. In addition to the above parameters, the values set for `NUM_TIMESTEPS`, `BETA_START` and `BETA_END` are all adopted directly from the DDPM paper [2].\nThe complete implementation will be done in several steps: constructing the U-Net model, preparing the dataset, defining noise scheduler for the diffusion process, training, and inference. We are going to discuss each of those stages in the following sub-sections.\n···\n### **The U-Net Architecture: Time Embedding**\nAs I’ve mentioned earlier, the basis of a diffusion model is U-Net. This architecture is used because its output layer is suitable to represent an image, which definitely makes sense since it was initially introduced for image segmentation task at the first place. The following figure shows what the original U-Net architecture looks like.\n![](../images/body-04bfc2e38c4fcbab3142.png)\nFigure 2. The original U-Net model proposed in [4].\nHowever, it is necessary to modify this architecture so that it can also take into account the timestep information. Not only that, since we will only use MNIST dataset, we also need to make the model smaller. Just remember the convention in deep learning that simpler models are often more effective for simple tasks.\nIn the figure below I show you the entire U-Net model that has been modified. Here you can see that the _time embedding_ tensor is injected to the model at every stage, which will later be done by element-wise summation, allowing the model to capture the timestep information. Next, instead of repeating each of the downsampling and the upsampling stages four times like the original U-Net, in this case we will only repeat each of them twice. Additionally, it is worth noting that the stack of downsampling stages is also known as the _encoder_ , whereas the stack of upsampling stages is often called the _decoder_.\n![](../images/body-12c0cc1315802ac8d679.png)\nFigure 3. The modified U-Net model for our diffusion task [3].\nNow let’s start constructing the architecture by creating a class for generating the time embedding tensor, which the idea is similar to the _positional embedding_ in Transformer. See the Codeblock 3 below for the details.\npython\n\n```\n# Codeblock 3\nclass TimeEmbedding(nn.Module):\n    def forward(self):\n        time = torch.arange(NUM_TIMESTEPS, device=DEVICE).reshape(NUM_TIMESTEPS, 1)  #(1)\n        print(f\"time\\t\\t: {time.shape}\")\n          \n        i = torch.arange(0, TIME_EMBED_DIM, 2, device=DEVICE)\n        denominator = torch.pow(10000, i/TIME_EMBED_DIM)\n        print(f\"denominator\\t: {denominator.shape}\")\n          \n        even_time_embed = torch.sin(time/denominator)  #(1)\n        odd_time_embed  = torch.cos(time/denominator)  #(2)\n        print(f\"even_time_embed\\t: {even_time_embed.shape}\")\n        print(f\"odd_time_embed\\t: {odd_time_embed.shape}\")\n          \n        stacked = torch.stack([even_time_embed, odd_time_embed], dim=2)  #(3)\n        print(f\"stacked\\t\\t: {stacked.shape}\")\n        time_embed = torch.flatten(stacked, start_dim=1, end_dim=2)  #(4)\n        print(f\"time_embed\\t: {time_embed.shape}\")\n          \n        return time_embed\n```\n\nWhat we basically do in the above code is to create a tensor of size `NUM_TIMESTEPS` × `TIME_EMBED_DIM` (1000×32), where every single row of this tensor will contain the timestep information. Later on, each of the 1000 timesteps will be represented by a feature vector of length 32. The values in the tensor themselves are obtained based on the two equations in Figure 4. In the Codeblock 3 above, these two equations are implemented at line `#(1)` and `#(2)`, each forming a tensor having the size of 1000×16. Next, these tensors are combined using the code at line `#(3)` and `#(4)`.\nHere I also print out every single step done in the above codeblock so that you can get a better understanding of what is actually being done in the TimeEmbedding class. If you still want more explanation about the above code, feel free to read my previous post about Transformer which you can access through the link at the end of this article. Once you clicked the link, you can just scroll all the way down to the Positional Encoding section.\n![](../images/body-21a49479c977a7614644.png)\nFigure 4. The sinusoidal positional encoding formula from the Transformer paper [6].\nNow let’s check if the `TimeEmbedding` class works properly using the following testing code. The resulting output shows that it successfully produced a tensor of size 1000×32, which is exactly what we expected earlier.\npython\n\n```\n# Codeblock 4\ntime_embed_test = TimeEmbedding()\nout_test = time_embed_test()\n```\n\nmarkup\n\n```\n# Codeblock 4 Outputtime            : torch.Size([1000, 1])denominator     : torch.Size([16])even_time_embed : torch.Size([1000, 16])odd_time_embed  : torch.Size([1000, 16])stacked         : torch.Size([1000, 16, 2])time_embed      : torch.Size([1000, 32])\n```\n\n···\n### **The U-Net Architecture: DoubleConv**\nIf you take a closer look at the modified architecture, you will see that we actually got lots of repeating patterns, such as the ones highlighted in yellow boxes in the following figure.\n![](../images/body-b133c100909774cf55c8.png)\nFigure 5. The processes done inside the yellow boxes will be implemented in the `DoubleConv` class [3].\nThese five yellow boxes share the same structure, where they consist of two convolution layers with the time embedding tensor injected right after the first convolution operation is performed. So, what we are going to do now is to create another class named `DoubleConv` to reproduce this structure. Look at the Codeblock 5a and 5b below to see how I do that.\npython\n\n```\n# Codeblock 5a\nclass DoubleConv(nn.Module):\n    def __init__(self, in_channels, out_channels):  #(1)\n        super().__init__()\n        \n        self.conv_0 = nn.Conv2d(in_channels=in_channels,  #(2)\n                                out_channels=out_channels, \n                                kernel_size=3, \n                                bias=False, \n                                padding=1)\n        self.bn_0 = nn.BatchNorm2d(num_features=out_channels)  #(3)\n        \n        self.time_embedding = TimeEmbedding()  #(4)\n        self.linear = nn.Linear(in_features=TIME_EMBED_DIM,  #(5)\n                                out_features=out_channels)\n        \n        self.conv_1 = nn.Conv2d(in_channels=out_channels,  #(6)\n                                out_channels=out_channels, \n                                kernel_size=3, \n                                bias=False, \n                                padding=1)\n        self.bn_1 = nn.BatchNorm2d(num_features=out_channels)  #(7)\n        \n        self.relu = nn.ReLU(inplace=True)  #(8)\n```\n\nThe two inputs of the `__init__()` method above gives us flexibility to configure the number of input and output channels (`#(1)`) so that the `DoubleConv` class can be used to instantiate all the five yellow boxes simply by adjusting its input arguments. As the name suggests, here we initialize two convolution layers (line `#(2)` and `#(6)`), each followed by a batch normalization layer and a ReLU activation function. Keep in mind that the two normalization layers need to be initialized separately (line `#(3)` and `#(7)`) since each of them has their own trainable normalization parameters. Meanwhile, the ReLU activation function should only be initialized once (`#(8)`) because it contains no parameters, allowing it to be used multiple times in different parts of the network. At line `#(4)`, we initialize the `TimeEmbedding` layer we created earlier, which will later be connected to a standard linear layer (`#(5)`). This linear layer is responsible to adjust the dimension of the time embedding tensor so that the resulting output can be summed with the output from the first convolution layer in an element-wise manner.\nNow let’s take a look at the Codeblock 5b below to better understand the flow of the `DoubleConv` block. Here you can see that the `forward()` method accepts two inputs: the raw image `x` and the timestep information `t` as shown at line `#(1)`. We initially process the image with the first Conv-BN-ReLU sequence (`#(2–4)`). This Conv-BN-ReLU structure is typically used when working with CNN-based models, even if the illustration does not explicitly show the batch normalization and the ReLU layers. Apart from the image, we then take the _t_ -th timestep information from our embedding tensor of the corresponding image (`#(5)`) and pass it through the linear layer (`#(6)`). We still need to expand the dimension of the resulting tensor using the code at line `#(7)` before performing element-wise summation at line `#(8)`. Finally, we process the resulting tensor with the second Conv-BN-ReLU sequence (`#(9–11)`).\npython\n\n```\n# Codeblock 5b\n    def forward(self, x, t):  #(1)\n        print(f'images\\t\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        \n        x = self.conv_0(x)  #(2)\n        x = self.bn_0(x)    #(3)\n        x = self.relu(x)    #(4)\n        print(f'\\nafter first conv\\t: {x.size()}')\n        \n        time_embed = self.time_embedding()[t]      #(5)\n        print(f'\\ntime_embed\\t\\t: {time_embed.size()}')\n        \n        time_embed = self.linear(time_embed)       #(6)\n        print(f'time_embed after linear\\t: {time_embed.size()}')\n        \n        time_embed = time_embed[:, :, None, None]  #(7)\n        print(f'time_embed expanded\\t: {time_embed.size()}')\n        \n        x = x + time_embed  #(8)\n        print(f'\\nafter summation\\t\\t: {x.size()}')\n        \n        x = self.conv_1(x)  #(9)\n        x = self.bn_1(x)    #(10)\n        x = self.relu(x)    #(11)\n        print(f'after second conv\\t: {x.size()}')\n        \n        return x\n```\n\nTo see if our `DoubleConv` implementation works properly, we are going to test it with the Codeblock 6 below. Here I want to simulate the very first instance of this block, which corresponds to the leftmost yellow box in Figure 5. To do so, we need to we need to set the `in_channels` and `out_channels` parameters to 1 and 64, respectively (`#(1)`). Next, we initialize two input tensors, namely `x_test` and `t_test`. The `x_test` tensor has the size of 2×1×28×28, representing a batch of two grayscale images having the size of 28×28 (`#(2)`). Keep in mind that this is just a dummy tensor of random values which will be replaced with the actual images from MNIST dataset later in the training phase. Meanwhile, `t_test` is a tensor containing the timestep numbers of the corresponding images (`#(3)`). The values for this tensor are randomly selected between 0 and `NUM_TIMESTEPS` (1000). Note that the datatype of this tensor must be an integer since the numbers will be used for indexing, as shown at line `#(5)` back in Codeblock 5b. Lastly, at line `#(4)` we pass both `x_test` and `t_test` tensors to the `double_conv_test` layer.\nBy the way, I re-run the previous codeblocks with the `print()` functions removed prior to running the following code so that the outputs will look neater.\npython\n\n```\n# Codeblock 6\ndouble_conv_test = DoubleConv(in_channels=1, out_channels=64).to(DEVICE)  #(1)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)  #(2)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)  #(3)\n\nout_test = double_conv_test(x_test, t_test)  #(4)\n```\n\nmarkup\n\n```\n# Codeblock 6 Outputimages                  : torch.Size([2, 1, 28, 28])   #(1)timesteps               : torch.Size([2]), tensor([468, 304], device='cuda:0')  #(2)after first conv        : torch.Size([2, 64, 28, 28])  #(3)time_embed              : torch.Size([2, 32])          #(4)time_embed after linear : torch.Size([2, 64])time_embed expanded     : torch.Size([2, 64, 1, 1])    #(5)after summation         : torch.Size([2, 64, 28, 28])  #(6)after second conv       : torch.Size([2, 64, 28, 28])  #(7)\n```\n\nThe shape of our original input tensors can be seen at lines `#(1)` and `#(2)` in the above output. Specifically at line `#(2)`, I also print out the two timesteps that we selected randomly. In this example we assume that each of the two images in the x tensor are already noised with the noise level from 468-th and 304-th timesteps prior to being fed into the network. We can see that the shape of the image tensor x changes to 2×64×28×28 after being passed through the first convolution layer (`#(3)`). Meanwhile, the size of our time embedding tensor becomes 2×32 (`#(4)`), which is obtained by extracting rows 468 and 304 from the original embedding of size 1000×32. In order to allow element-wise summation to be performed (`#(6)`), we need to map the 32-dimensional time embedding vectors into 64 and expand their axes, resulting in a tensor of size 2×64×1×1 (`#(5)`) so that it can be broadcast to the 2×64×28×28 tensor. After the summation is done, we then pass the tensor through the second convolution layer, at which point the tensor dimension does not change at all (`#(7)`).  \n\n···\n### **The U-Net Architecture: Encoder**\nAs we have successfully implemented the `DoubleConv` block, the next step to do is to implement the so-called `DownSample` block. In Figure 6 below, this corresponds to the parts enclosed in the red box.\n![](../images/body-79b13b9b11ecf022c2cc.png)\nFigure 6. The parts of the network highlighted in red are the so-called `DownSample` blocks [3].\nThe purpose of a `DownSample` block is to reduce the spatial dimension of an image, but it is important to note that at the same time it increases the number of channels. In order to achieve this, we can simply stack a `DoubleConv` block and a maxpooling operation. In this case the pooling uses 2×2 kernel size with the stride of 2, causing the spatial dimension of the image to be twice as small as the input. The implementation of this block can be seen in Codeblock 7 below.  \n\npython\n\n```\n# Codeblock 7\nclass DownSample(nn.Module):\n    def __init__(self, in_channels, out_channels):  #(1)\n        super().__init__()\n        \n        self.double_conv = DoubleConv(in_channels=in_channels,  #(2)\n                                      out_channels=out_channels)\n        self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)    #(3)\n    \n    def forward(self, x, t):  #(4)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        \n        convolved = self.double_conv(x, t)   #(5)\n        print(f'\\nafter double conv\\t: {convolved.size()}')\n        \n        maxpooled = self.maxpool(convolved)  #(6)\n        print(f'after pooling\\t\\t: {maxpooled.size()}')\n        \n        return convolved, maxpooled          #(7)\n```\n\nHere I set the `__init__()` method to take number of input and output channels so that we can use it for creating the two `DownSample` blocks highlighted in Figure 6 without needing to write them in separate classes (`#(1)`). Next, the `DoubleConv` and the maxpooling layers are initialized at line `#(2)` and `#(3)`, respectively. Remember that since the `DoubleConv` block accepts image `x` and the corresponding timestep `t` as the inputs, we also need to set the `forward()` method of this `DownSample` block such that it accepts both of them as well (`#(4)`). The information contained in x and t are then combined as the two tensors are processed by the `double_conv` layer, which the output is stored in the variable named `convolved` (`#(5)`). Afterwards, we now actually perform the downsampling with the maxpooling operation at line `#(6)`, producing a tensor named `maxpooled`. It is important to note that both the `convolved` and `maxpooled` tensors are going to be returned, which is essentially done because we will later bring `maxpooled` to the next downsampling stage, whereas the `convolved` tensor will be transferred directly to the upsampling stage in the decoder through skip-connections.\nNow let’s test the `DownSample` class using the Codeblock 8 below. The input tensors used here are exactly the same as the ones in Codeblock 6. Based on the resulting output, we can see that the pooling operation successfully converted the output of the `DoubleConv` block from 2×64×28×28 (`#(1)`) to 2×64×14×14 (`#(2)`), indicating that our DownSample class works properly.\npython\n\n```\n# Codeblock 8\ndown_sample_test = DownSample(in_channels=1, out_channels=64).to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\n\nout_test = down_sample_test(x_test, t_test)\n```\n\nmarkup\n\n```\n# Codeblock 8 Outputoriginal          : torch.Size([2, 1, 28, 28])timesteps         : torch.Size([2]), tensor([468, 304], device='cuda:0')after double conv : torch.Size([2, 64, 28, 28])  #(1)after pooling     : torch.Size([2, 64, 14, 14])  #(2)\n```\n\n···\n### **The U-Net Architecture: Decoder**\nWe need to introduce the so-called `UpSample` block in the decoder, which is responsible for reverting the tensor in the intermediate layers to the original image dimension. In order to maintain a symmetrical structure, the number of `UpSample` blocks must match that of the `DownSample` blocks. Look at the Figure 7 below to see where the two `UpSample` blocks are placed.\n![](../images/body-16423f9b7786e1df6b97.png)\nFigure 7. The components inside the blue boxes are the so-called `UpSample` blocks [3].\nSince both `UpSample` blocks are structurally identical, we can just initialize a single class for them, just like the `DownSample` class we created earlier. Look at the Codeblock 9 below to see how I implement it.\npython\n\n```\n# Codeblock 9\nclass UpSample(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        \n        self.conv_transpose = nn.ConvTranspose2d(in_channels=in_channels,  #(1)\n                                                 out_channels=out_channels, \n                                                 kernel_size=2, stride=2)  #(2)\n        self.double_conv = DoubleConv(in_channels=in_channels,  #(3)\n                                      out_channels=out_channels)\n        \n    def forward(self, x, t, connection):  #(4)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        print(f'connection\\t\\t: {connection.size()}')\n        \n        x = self.conv_transpose(x)  #(5)\n        print(f'\\nafter conv transpose\\t: {x.size()}')\n        \n        x = torch.cat([x, connection], dim=1)  #(6)\n        print(f'after concat\\t\\t: {x.size()}')\n        \n        x = self.double_conv(x, t)  #(7)\n        print(f'after double conv\\t: {x.size()}')\n        \n        return x\n```\n\nIn the `__init__()` method, we use `nn.ConvTranspose2d` to upsample the spatial dimension (`#(1)`). Both the kernel size and stride are set to 2 so that the output will be twice as large (`#(2)`). Next, the `DoubleConv` block will be employed to reduce the number of channels, while at the same time combining the timestep information from the time embedding tensor (`#(3)`).\nThe flow of this `UpSample` class is a bit more complicated than the `DownSample` class. If we take a closer look at the architecture, we’ll see that that we also have a skip-connection coming directly from the encoder. Thus, we need the `forward()` method to accept another argument in addition to the original image `x` and the timestep `t`, namely the residual tensor `connection` (`#(4)`). The first thing we do inside this method is to process the original image `x` with the transpose convolution layer (`#(5)`). In fact, not only upsampling the spatial size, but this layer also reduces the number of channels at the same time. However, the resulting tensor is then directly concatenated with `connection` in a channel-wise manner (`#(6)`), causing it to seem like no channel reduction is performed. It is important to know that at this point these two tensors are just concatenated, meaning that the information from the two are not yet combined. We finally feed these concatenated tensors to the `double_conv` layer (`#(7)`), allowing them to share information to each other through the learnable parameters inside the convolution layers.\nThe Codeblock 10 below shows how I test the `UpSample` class. The size of the tensors to be passed through are set according to the second upsampling block, i.e., the rightmost blue box in Figure 7.\npython\n\n```\n# Codeblock 10\nup_sample_test = UpSample(in_channels=128, out_channels=64).to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, 128, 14, 14)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\nconnection_test = torch.randn((BATCH_SIZE, 64, 28, 28)).to(DEVICE)\n\nout_test = up_sample_test(x_test, t_test, connection_test)\n```\n\nIn the resulting output below, if we compare the input tensor (`#(1)`) with the final tensor shape (`#(2)`), we can clearly see that the number of channels successfully reduced from 128 to 64, while at the same time the spatial dimension increased from 14×14 to 28×28. This essentially means that our `UpSample` class is now ready to be used in the main U-Net architecture.\nmarkup\n\n```\n# Codeblock 10 Outputoriginal             : torch.Size([2, 128, 14, 14])   #(1)timesteps            : torch.Size([2]), tensor([468, 304], device='cuda:0')connection           : torch.Size([2, 64, 28, 28])after conv transpose : torch.Size([2, 64, 28, 28])after concat         : torch.Size([2, 128, 28, 28])after double conv    : torch.Size([2, 64, 28, 28])    #(2)\n```\n\n···\n### **The U-Net Architecture: Putting All Components Together**\nOnce all U-Net components have been created, what we are going to do next is to wrap them together into a single class. Look at the Codeblock 11a and 11b below for the details.\npython\n\n```\n# Codeblock 11a\nclass UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n      \n        self.downsample_0 = DownSample(in_channels=NUM_CHANNELS,  #(1)\n                                       out_channels=64)\n        self.downsample_1 = DownSample(in_channels=64,            #(2)\n                                       out_channels=128)\n      \n        self.bottleneck   = DoubleConv(in_channels=128,           #(3)\n                                       out_channels=256)\n      \n        self.upsample_0   = UpSample(in_channels=256,             #(4)\n                                     out_channels=128)\n        self.upsample_1   = UpSample(in_channels=128,             #(5)\n                                     out_channels=64)\n      \n        self.output = nn.Conv2d(in_channels=64,                   #(6)\n                                out_channels=NUM_CHANNELS,\n                                kernel_size=1)\n```\n\nYou can see in the `__init__()` method above that we initialize two downsampling (`#(1–2)`) and two upsampling (`#(4–5)`) blocks, which the number of input and output channels are set according to the architecture shown in the illustration. There are actually two additional components I haven’t explained yet, namely the _bottleneck_ (`#(3)`) and the _output_ layer (`#(6)`). The former is essentially just a `DoubleConv` block, which acts as the main connection between the encoder and the decoder. Look at the Figure 8 below to see which components of the network belong to the _bottleneck_ layer. Next, the _output_ layer is a standard convolution layer which is responsible to turn the 64-channel image produced by the last `UpSampling` stage into 1-channel only. This operation is done using a kernel of size 1×1, meaning that it combines information across all channels while operating independently at each pixel position.  \n\n![](../images/body-f8276748c787de8d5a35.png)\nFigure 8. The bottleneck layer (the lower part of the model) acts as the main bridge between the encoder and the decoder of U-Net [3].\nI guess the `forward()` method of the entire U-Net in the following codeblock is pretty straightforward, as what we essentially do here is pass the tensors from one layer to another — just don’t forget to include the skip connections between the downsampling and upsampling blocks.\npython\n\n```\n# Codeblock 11b\n    def forward(self, x, t):  #(1)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n            \n        convolved_0, maxpooled_0 = self.downsample_0(x, t)\n        print(f'\\nmaxpooled_0\\t\\t: {maxpooled_0.size()}')\n            \n        convolved_1, maxpooled_1 = self.downsample_1(maxpooled_0, t)\n        print(f'maxpooled_1\\t\\t: {maxpooled_1.size()}')\n            \n        x = self.bottleneck(maxpooled_1, t)\n        print(f'after bottleneck\\t: {x.size()}')\n    \n        upsampled_0 = self.upsample_0(x, t, convolved_1)\n        print(f'upsampled_0\\t\\t: {upsampled_0.size()}')\n            \n        upsampled_1 = self.upsample_1(upsampled_0, t, convolved_0)\n        print(f'upsampled_1\\t\\t: {upsampled_1.size()}')\n            \n        x = self.output(upsampled_1)\n        print(f'final output\\t\\t: {x.size()}')\n            \n        return x\n```\n\nNow let’s see whether we have correctly constructed the U-Net class above by running the following testing code.\npython\n\n```\n# Codeblock 12\nunet_test = UNet().to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\n\nout_test = unet_test(x_test, t_test)\n```\n\nmarkup\n\n```\n# Codeblock 12 Outputoriginal         : torch.Size([2, 1, 28, 28])   #(1)timesteps        : torch.Size([2]), tensor([468, 304], device='cuda:0')maxpooled_0      : torch.Size([2, 64, 14, 14])  #(2)maxpooled_1      : torch.Size([2, 128, 7, 7])   #(3)after bottleneck : torch.Size([2, 256, 7, 7])   #(4)upsampled_0      : torch.Size([2, 128, 14, 14])upsampled_1      : torch.Size([2, 64, 28, 28])final output     : torch.Size([2, 1, 28, 28])   #(5)\n```\n\nWe can see in the above output that the two downsampling stages successfully converted the original tensor of size 1×28×28 (`#(1)`) into 64×14×14 (`#(2)`) and 128×7×7 (`#(3)`), respectively. This tensor is then passed through the bottleneck layer, causing its number of channels to expand to 256 without changing the spatial dimension (`#(4)`). Lastly, we upsample the tensor twice before eventually shrinking the number of channels to 1 (`#(5)`). Based on this output, it looks like our model is working properly. Thus, it is now ready to be trained for our diffusion task.\n···\n### **Dataset Preparation**\nAs we have successfully created the entire U-Net architecture, the next thing to do is to prepare the MNIST Handwritten Digit dataset. Before actually loading it, we need to define the preprocessing steps first using the `transforms.Compose()` method from Torchvision, as shown at line `#(1)` in Codeblock 13. There are two things we do here: converting the images into PyTorch tensors which also scales the pixel values from 0–255 to 0–1 (`#(2)`), and normalize them so that the final pixel values ranging between -1 and 1 (`#(3)`). Next, we download the dataset using `datasets.MNIST()`. In this case, we are going to take the images from the training data, hence we use `train=True` (`#(5)`). Don’t forget to pass the `transform` variable we initialized earlier to the `transform` parameter (`transform=transform`) so that it will automatically preprocess the images as we load them (`#(6)`). Lastly, we need to employ `DataLoader` to load the images from `mnist_dataset` (`#(7)`). The arguments I use for the input parameters are intended to randomly pick `BATCH_SIZE` (2) images from the dataset in each iteration.\npython\n\n```\n# Codeblock 13\ntransform = transforms.Compose([  #(1)\n    transforms.ToTensor(),        #(2)\n    transforms.Normalize((0.5,), (0.5,))  #(3)\n])\n\nmnist_dataset = datasets.MNIST(   #(4)\n    root='./data', \n    train=True,           #(5)\n    download=True, \n    transform=transform   #(6)\n)\n\nloader = DataLoader(mnist_dataset,  #(7)\n                    batch_size=BATCH_SIZE,\n                    drop_last=True, \n                    shuffle=True)\n```\n\nIn the following codeblock, I try to load a batch of images from the dataset. In every iteration, `loader` provides both the images and the corresponding labels, hence we need to store them in two separate variables: `images` and `labels`.\npython\n\n```\n# Codeblock 14\nimages, labels = next(iter(loader))\n\nprint('images\\t\\t:', images.shape)\nprint('labels\\t\\t:', labels.shape)\nprint('min value\\t:', images.min())\nprint('max value\\t:', images.max())\n```\n\nWe can see in the resulting output below that the `images` tensor has the size of 2×1×28×28 (`#(1)`), indicating that two grayscale images of size 28×28 have been successfully loaded. Here we can also see that the length of the `labels` tensor is 2, which matches the number of the loaded images (`#(2)`). Note that in this case the labels are going to be completely ignored. My plan here is that I just want the model to generate any number it previously seen from the entire training dataset without even knowing what number it actually is. Lastly, this output also shows that the preprocessing works properly, as the pixel values now range between -1 and 1.\nmarkup\n\n```\n# Codeblock 14 Outputimages    : torch.Size([2, 1, 28, 28])  #(1)labels    : torch.Size([2])             #(2)min value : tensor(-1.)max value : tensor(1.)\n```\n\nRun the following code if you want to see what the image we just loaded looks like.\npython\n\n```\n# Codeblock 15   \nplt.imshow(images[0].squeeze(), cmap='gray')\nplt.show()\n```\n\n![](../images/body-c7cb07997c5e24470c67.png)\nFigure 9. Output from Codeblock 15 [3].\n···\n### Noise Scheduler\nIn this section we are going to talk about how the forward and backward diffusion are performed, which the process essentially involves adding or removing noise little by little at each timestep. It is necessary to know that we basically want a uniform amount of noise across all timesteps, where in the forward diffusion the image should be completely full of noise exactly at timestep 1000, while in the backward diffusion, we have to get the completely clear image at timestep 0. Hence, we need something to control the noise amount for each timestep. Later in this section, I am going to implement a class named `NoiseScheduler` to do so. — This will probably be the most mathy section of this article, as I’ll display many equations here. But don’t worry about that since we’ll focus on implementing these equations rather than discussing the mathematical derivations.\nNow let’s take a look at the equations in Figure 10 which I will implement in the `__init__()` method of the `NoiseScheduler` class below.\n![](../images/body-f613a04eb434a3653139.png)\nFigure 10. The equations we need to implement in the **__init__()** method of the `**NoiseScheduler**`class [3].\npython\n\n```\n# Codeblock 16a\nclass NoiseScheduler:\n    def __init__(self):\n        self.betas = torch.linspace(BETA_START, BETA_END, NUM_TIMESTEPS)  #(1)\n        self.alphas = 1. - self.betas\n        self.alphas_cum_prod = torch.cumprod(self.alphas, dim=0)\n        self.sqrt_alphas_cum_prod = torch.sqrt(self.alphas_cum_prod)\n        self.sqrt_one_minus_alphas_cum_prod = torch.sqrt(1. - self.alphas_cum_prod)\n```\n\nThe above code works by creating multiple sequences of numbers, all of them are basically controlled by `BETA_START` (0.0001), `BETA_END` (0.02), and `NUM_TIMESTEPS` (1000). The first sequence we need to instantiate is the `betas` itself, which is done using `torch.linspace()` (`#(1)`). What it essentially does is that it generates a 1-dimensional tensor of length 1000 starting from 0.0001 to 0.02, where every single element in this tensor corresponds to a single timestep. The interval between each element is uniform, allowing us to generate uniform amount of noise throughout all timesteps as well. With this `betas` tensor, we then compute `alphas`, `alphas_cum_prod`, `sqrt_alphas_cum_prod` and `sqrt_one_minus_alphas_cum_prod` based on the four equations in Figure 10. Later on, these tensors will act as the basis of how the noise is generated or removed during the diffusion process.\nDiffusion is normally done in a sequential manner. However, the forward diffusion process is deterministic, hence we can derive the original equation into a closed form so that we can obtain the noise at a specific timestep without having to iteratively add noise from the very beginning. The Figure 11 below shows what the closed form of the forward diffusion looks like, where _x₀_ represents the original image while epsilon (_ϵ)_ denotes an image made up of random Gaussian noise. We can think of this equation as a weighted combination, where we combine the clear image and the noise according to weights determined by the timestep, resulting in an image with a specific amount of noise.\n![](../images/body-a254f11cd915f946f26c.png)\nFigure 11. The closed form of the forward diffusion process [3].\nThe implementation of this equation can be seen in Codeblock 16b. In this `forward_diffusion()` method, _x₀_ and _ϵ_ are denoted as `original` and `noise`. Here you need to keep in mind that these two input variables are images, whereas `sqrt_alphas_cum_prod_t` and `sqrt_one_minus_alphas_cum_prod_t` are scalars. Thus, we need to adjust the shape of these two scalars (`#(1)` and `#(2)`) so that the operation at line `#(3)` can be performed. The `noisy_image` variable is going to be the output of this function, which I guess the name is self-explanatory.\npython\n\n```\n# Codeblock 16b\n    def forward_diffusion(self, original, noise, t):\n        sqrt_alphas_cum_prod_t = self.sqrt_alphas_cum_prod[t]\n        sqrt_alphas_cum_prod_t = sqrt_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1)  #(1)\n        \n        sqrt_one_minus_alphas_cum_prod_t = self.sqrt_one_minus_alphas_cum_prod[t]\n        sqrt_one_minus_alphas_cum_prod_t = sqrt_one_minus_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1)  #(2)\n        \n        noisy_image = sqrt_alphas_cum_prod_t * original + sqrt_one_minus_alphas_cum_prod_t * noise  #(3)\n        \n        return noisy_image\n```\n\nNow let’s talk about backward diffusion. In fact, this one is a bit more complicated than the forward diffusion since we need three more equations here. Before I give you these equations, let me show you the implementation first. See the Codeblock 16c below.\npython\n\n```\n# Codeblock 16c\n    def backward_diffusion(self, current_image, predicted_noise, t):  #(1)\n        denoised_image = (current_image - (self.sqrt_one_minus_alphas_cum_prod[t] * predicted_noise)) / self.sqrt_alphas_cum_prod[t]  #(2)\n        denoised_image = 2 * (denoised_image - denoised_image.min()) / (denoised_image.max() - denoised_image.min()) - 1  #(3)\n        \n        current_prediction = current_image - ((self.betas[t] * predicted_noise) / (self.sqrt_one_minus_alphas_cum_prod[t]))  #(4)\n        current_prediction = current_prediction / torch.sqrt(self.alphas[t])  #(5)\n        \n        if t == 0:  #(6)\n            return current_prediction, denoised_image\n        \n        else:\n            variance = (1 - self.alphas_cum_prod[t-1]) / (1. - self.alphas_cum_prod[t])  #(7)\n            variance = variance * self.betas[t]  #(8)\n            sigma = variance ** 0.5\n            z = torch.randn(current_image.shape).to(DEVICE)\n            current_prediction = current_prediction + sigma*z\n            \n            return current_prediction, denoised_image\n```\n\nLater in the inference phase, the `backward_diffusion()` method will be called inside a loop that iterates `NUM_TIMESTEPS` (1000) times, starting from _t_ = 999, continued with _t_ = 998, and so on all the way to _t_ = 0. This function is responsible to remove the noise from the image iteratively based on the `current_image` (the image produced by the previous denoising step), the `predicted_noise` (the noise predicted by U-Net in the previous step), and the timestep information `t` (`#(1)`). In each iteration, noise removal is done using the equation shown in Figure 12, which in Codeblock 16c, this corresponds to lines `#(4-5)`.\n![](../images/body-907c22fe6041a99cb577.png)\nFigure 12. The equation used for removing noise from the image [3].\nAs long as we haven’t reached _t_ = 0, we will compute the variance based on the equation in Figure 13 (`#(7–8)`). This variance will then be used to introduce another controlled noise to simulate the stochasticity in the backward diffusion process since the noise removal equation in Figure 12 is a deterministic approximation. This is essentially also the reason that we don’t calculate the variance once we reached _t_ = 0 (`#(6)`) since we no longer need to add more noise as the image is completely clear already.\n![](../images/body-1bdea3c2c916a22ae787.png)\nFigure 13. The equation used to calculate variance for introducing controlled noise [3].\nDifferent from `current_prediction` which aims to estimate the image of the previous timestep (_xₜ₋₁_), the objective of the `denoised_image` tensor is to reconstruct the original image (_x₀_). Thanks to these different objectives, we need a separate equation to compute `denoised_image`, which can be seen in Figure 14 below. The implementation of the equation itself is written at line `#(2–3)`.\n![](../images/body-0c62cec7f177735e6a42.png)\nFigure 14. The equation for reconstructing the original image [3].\nNow let’s test the `NoiseScheduler` class we created above. In the following codeblock, I instantiate a `NoiseScheduler` object and print out the attributes associated with it, which are all computed using the equation in Figure 10 based on the values stored in the `betas` attribute. Remember that the actual length of these tensors is `NUM_TIMESTEPS` (1000), but here I only print out the first 6 elements.\npython\n\n```\n# Codeblock 17\nnoise_scheduler = NoiseScheduler()\n\nprint(f'betas\\t\\t\\t\\t: {noise_scheduler.betas[:6]}')\nprint(f'alphas\\t\\t\\t\\t: {noise_scheduler.alphas[:6]}')\nprint(f'alphas_cum_prod\\t\\t\\t: {noise_scheduler.alphas_cum_prod[:6]}')\nprint(f'sqrt_alphas_cum_prod\\t\\t: {noise_scheduler.sqrt_alphas_cum_prod[:6]}')\nprint(f'sqrt_one_minus_alphas_cum_prod\\t: {noise_scheduler.sqrt_one_minus_alphas_cum_prod[:6]}')\n```\n\nmarkup\n\n```\n# Codeblock 17 Outputbetas                          : tensor([1.0000e-04, 1.1992e-04, 1.3984e-04, 1.5976e-04, 1.7968e-04, 1.9960e-04])alphas                         : tensor([0.9999, 0.9999, 0.9999, 0.9998, 0.9998, 0.9998])alphas_cum_prod                : tensor([0.9999, 0.9998, 0.9996, 0.9995, 0.9993, 0.9991])sqrt_alphas_cum_prod           : tensor([0.9999, 0.9999, 0.9998, 0.9997, 0.9997, 0.9996])sqrt_one_minus_alphas_cum_prod : tensor([0.0100, 0.0148, 0.0190, 0.0228, 0.0264, 0.0300])\n```\n\nThe above output indicates that our `__init__()` method works as expected. Next, we are going to test the `forward_diffusion()` method. If you go back to Figure 16b, you will see that `forward_diffusion()` accepts three inputs: original image, noise image and the timestep number. Let’s just use the image from the MNIST dataset we loaded earlier for the first input (`#(1)`) and a random Gaussian noise of the exact same size for the second one (`#(2)`). Run the Codeblock 18 below to see what these two images look like.\npython\n\n```\n# Codeblock 18\nimage = images[0]  #(1)\nnoise = torch.randn_like(image)  #(2)\n\nplt.imshow(image.squeeze(), cmap='gray')\nplt.show()\nplt.imshow(noise.squeeze(), cmap='gray')\nplt.show()\n```\n\n![](../images/body-34ef2ca7f6166ab26a3c.png)\nFigure 15. The two images to be used as the original (left) and the noise image (right). The one on the left is the same image I showed earlier in Figure 9 [3].\nAs we already got the image and the noise ready, what we need to do afterwards is to pass them to the `forward_diffusion()` method alongside the _t_. I actually tried to run the Codeblock 19 below multiple times with _t_ = 50, 100, 150, and so on up to _t_ = 300. You can see in Figure 16 that the image becomes less clear as the parameter increases. In this case, the image is going to be completely filled by noise when the _t_ is set to 999.\npython\n\n```\n# Codeblock 19\nnoisy_image_test = noise_scheduler.forward_diffusion(image.to(DEVICE), noise.to(DEVICE), t=50)\n\nplt.imshow(noisy_image_test[0].squeeze().cpu(), cmap='gray')\nplt.show()\n```\n\n![](../images/body-8b8656b008a48083e7f1.png)\nFigure 16. The result of the forward diffusion process at t=50, 100, 150, and so on until t=300 [3].\nUnfortunately, we cannot test the `backward_diffusion()` method since this process requires us to have our U-Net model trained. So, let’s just skip this part for now. I’ll show you how we can actually use this function later in the inference phase.\n···\n### Training\nAs the U-Net model, MNIST dataset, and the noise scheduler are ready, we can now prepare a function for training. Before we do that, I instantiate the model and the noise scheduler in Codeblock 20 below.\npython\n\n```\n# Codeblock 20\nmodel = UNet().to(DEVICE)\nnoise_scheduler = NoiseScheduler()\n```\n\nThe entire training procedure is implemented in the `train()` function shown in Codeblock 21. Before doing anything, we first initialize the optimizer and the loss function, which in this case we use Adam and MSE, respectively (`#(1–2)`). What we basically want to do here is to train the model such that it will be able to predict the noise contained in the input image, which later on, the predicted noise will be used as the basis of the denoising process in the backward diffusion stage. To actually train the model, we first need to perform forward diffusion using the code at line `#(6)`. This noising process will be done on the `images` tensor (`#(3)`) using the random noise generated at line `#(4)`. Next, we take random number somewhere between 0 and `NUM_TIMESTEPS` (1000) for the `t` (`#(5)`), which is essentially done because we want our model to see images of varying noise levels as an approach to improve generalization. As the noisy images have been generated, we then pass it through the U-Net model alongside the chosen `t` (`#(7)`). The input `t` here is useful for the model as it indicates the current noise level in the image. Lastly, the loss function we initialized earlier is responsible to compute the difference between the actual noise and the predicted noise from the original image (`#(8)`). So, the objective of this training is basically to make the predicted noise as similar as possible to the noise we generated at line `#(4)`.\npython\n\n```\n# Codeblock 21\ndef train():\n    optimizer = Adam(model.parameters(), lr=LEARNING_RATE)  #(1)\n    loss_function = nn.MSELoss()  #(2)\n    losses = []\n    \n    for epoch in range(NUM_EPOCHS):\n        print(f'Epoch no {epoch}')\n        \n        for images, _ in tqdm(loader):\n            \n            optimizer.zero_grad()\n\n            images = images.float().to(DEVICE)  #(3)\n            noise = torch.randn_like(images)  #(4)\n            t = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,))  #(5)\n\n            noisy_images = noise_scheduler.forward_diffusion(images, noise, t).to(DEVICE)  #(6)\n            predicted_noise = model(noisy_images, t)  #(7)\n            loss = loss_function(predicted_noise, noise)  #(8)\n            \n            losses.append(loss.item())\n            loss.backward()\n            optimizer.step()\n\n    return losses\n```\n\nNow let’s run the above training function using the codeblock below. Sit back and relax while waiting the training completes. In my case, I used Kaggle Notebook with Nvidia GPU P100 turned on, and it took around 45 minutes to finish.\npython\n\n```\n# Codeblock 22\nlosses = train()\n```\n\nIf we take a look at the loss graph, it seems like our model learned pretty well as the value is generally decreasing over time with a rapid drop at early stages and a more stable (yet still decreasing) trend in the later stages. So, I think we can expect good results later in the inference phase.\npython\n\n```\n# Codeblock 23\nplt.plot(losses)\n```\n\n![](../images/body-7d4c78c3478d3702cf36.png)\nFigure 17. How the loss value decreases as the training goes [3].\n···\n### Inference\nAt this point we have already got our model trained, so we can now perform inference on it. Look at the Codeblock 24 below to see how I implement the `inference()` function.\npython\n\n```\n# Codeblock 24\ndef inference():\n\n    denoised_images = []  #(1)\n    \n    with torch.no_grad():  #(2)\n        current_prediction = torch.randn((64, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)  #(3)\n        \n        for i in tqdm(reversed(range(NUM_TIMESTEPS))):  #(4)\n            predicted_noise = model(current_prediction, torch.as_tensor(i).unsqueeze(0))  #(5)\n            current_prediction, denoised_image = noise_scheduler.backward_diffusion(current_prediction, predicted_noise, torch.as_tensor(i))  #(6)\n\n            if i%100 == 0:  #(7)\n                denoised_images.append(denoised_image)\n            \n        return denoised_images\n```\n\nAt the line marked with `#(1)` I initialize an empty list which will be used to store the denoising result every 100 timesteps (`#(7)`). This will later allow us to see how the backward diffusion goes. The actual inference process is encapsulated inside `torch.no_grad()` (`#(2)`). Remember that in diffusion models we generate images from a completely random noise, which we assume that these images are initially at _t_ = 999. To implement this, we can simply use `torch.randn()` as shown at line `#(3)`. Here we initialize a tensor of size 64×1×28×28, indicating that we are about to generate 64 images simultaneously. Next, we write a `for` loop that iterates backwards starting from 999 to 0 (`#(4)`). Inside this loop, we feed the current image and the timestep as the input for the trained U-Net and let it predict the noise (`#(5)`). The actual backward diffusion is then performed at line `#(6)`. At the end of the iteration, we should get new images similar to the ones we have in our dataset. Now let’s call the `inference()` function in the following codeblock.\npython\n\n```\n# Codeblock 25\ndenoised_images = inference()\n```\n\nAs the inference completed, we can now see what the resulting images look like. The Codeblock 26 below is used to display the first 42 images we just generated.\npython\n\n```\n# Codeblock 26\nfig, axes = plt.subplots(ncols=7, nrows=6, figsize=(10, 8))\n\ncounter = 0\n\nfor i in range(6):\n    for j in range(7):\n        axes[i,j].imshow(denoised_images[-1][counter].squeeze().detach().cpu().numpy(), cmap='gray')  #(1)\n        axes[i,j].get_xaxis().set_visible(False)\n        axes[i,j].get_yaxis().set_visible(False)\n        counter += 1\n\nplt.show()\n```\n\n![](../images/body-f13aca317d323b0d9977.png)\nFigure 18. The images generated by the diffusion model trained on the MNIST Handwritten Digit dataset [3].\nIf we take a look at the above codeblock, you can see that the indexer of `[-1]` at line `#(1)` indicates that we only display the images from the last iteration (which corresponds to timestep 0). This is the reason that the images you see in Figure 18 are all free from noise. I do acknowledge that this might not be the best of a result since not all the generated images are valid digit numbers. — But hey, this instead indicates that these images are not merely duplicates from the original dataset.\nHere we can also visualize the backward diffusion process using the Codeblock 27 below. You can see in the resulting output in Figure 19 that we initially start from a complete random noise, which gradually disappears as we move to the right.\npython\n\n```\n# Codeblock 27\nfig, axes = plt.subplots(ncols=10, figsize=(24, 8))\n\nsample_no = 0\ntimestep_no = 0\n\nfor i in range(10):\n    axes[i].imshow(denoised_images[timestep_no][sample_no].squeeze().detach().cpu().numpy(), cmap='gray')\n    axes[i].get_xaxis().set_visible(False)\n    axes[i].get_yaxis().set_visible(False)\n    timestep_no += 1\n\nplt.show()\n```\n\n![](../images/body-c36db7d505f29591ef4a.png)\nFigure 19. What the image looks like at timestep 900, 800, 700 and so on until timestep 0 [3].\n···\n## Ending\nThere are plenty of directions you can go from here. First, you might probably need to tweak the parameter configurations in Codeblock 2 if you want better results. Second, it is also possible to modify the U-Net model by implementing attention layers in addition to the stack of convolution layers we used in the downsampling and the upsampling stages. This does not guarantee you to obtain better results especially for a simple dataset like this, but it’s definitely worth trying. Third, you can also try to use a more complex dataset if you want to challenge yourself.\nWhen it comes to practical applications, there are actually lots of things you can do with diffusion models. The simplest one might be for data augmentation. With diffusion model, we can easily generate new images from a specific data distribution. For example, suppose we are working on an image classification project, but the number of images in the classes are imbalanced. To address this problem, it is possible for us to take the images from the minority class and feed them into a diffusion model. By doing so, we can ask the trained diffusion model to generate a number of samples from that class as many as we want.\nAnd well, that’s pretty much everything about the theory and the implementation of diffusion model. Thanks for reading, I hope you learn something new today!\n_You can access the code used in this project through_[ _this link_](https://github.com/MuhammadArdiPutra/medium_articles/blob/main/The%20Art%20of%20Noise.ipynb) _. Here are also the links to my previous articles about_[ _Autoencoder_](https://becominghuman.ai/the-deep-autoencoder-in-action-digit-reconstruction-bf177ccbb8c0) _,_[_Variational Autoencoder (VAE)_](https://becominghuman.ai/using-variational-autoencoder-vae-to-generate-new-images-14328877e88d)_,_[_Neural Style Transfer (NST)_](https://towardsdatascience.com/paper-walkthrough-neural-style-transfer-fc5c978cdaed)_, and_[ _Transformer_](https://towardsdatascience.com/paper-walkthrough-attention-is-all-you-need-80399cdc59e1) _._\n···\n## References\n[1] Jascha Sohl-Dickstein _et al_.Deep Unsupervised Learning using Nonequilibrium Thermodynamics. Arxiv. <https://arxiv.org/pdf/1503.03585> [Accessed December 27, 2024].\n[2] Jonathan Ho _et al_. Denoising Diffusion Probabilistic Models. Arxiv. <https://arxiv.org/pdf/2006.11239> [Accessed December 27, 2024].\n[3] Image created originally by author.\n[4] Olaf Ronneberger _et al_. U-Net: Convolutional Networks for Biomedical  \nImage Segmentation. Arxiv. <https://arxiv.org/pdf/1505.04597> [Accessed December 27, 2024].\n[5] Yann LeCun _et al_. The MNIST Database of Handwritten Digits. <https://yann.lecun.com/exdb/mnist/> [Accessed December 30, 2024] (Creative Commons Attribution-Share Alike 3.0 license).\n[6] Ashish Vaswani _et al_. Attention Is All You Need. Arxiv. <https://arxiv.org/pdf/1706.03762> [Accessed September 29, 2024].\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[Editor's Picks](https://towardsdatascience.com/tag/editors-pick/)[Computer Vision](https://towardsdatascience.com/tag/computer-vision/)[Generative Ai](https://towardsdatascience.com/tag/generative-ai/)[Machine Learning](https://towardsdatascience.com/tag/machine-learning/)[Pytorch](https://towardsdatascience.com/tag/pytorch/)\nShare This Article\n  * [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&title=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\n  * [](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&title=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\n  * [](https://x.com/share?url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&text=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\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.jpg)](https://towardsdatascience.com/are-you-ready-for-machine-learning-math-fc5a08fc8130/)\n## [The Math you need for Machine Learning](https://towardsdatascience.com/are-you-ready-for-machine-learning-math-fc5a08fc8130/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA list of resources that will help you level up with the math required for machine learning.\n[Santiago Valdarrama](https://towardsdatascience.com/author/svpino/)\nApril 30, 20214 min read\n  * [![](../images/003.png)](https://towardsdatascience.com/testing-the-consistency-of-reported-machine-learning-performance-scores-by-the-mlscorecheck-package-e1baaba57610/)\n## [Testing the Consistency of Reported Machine Learning Performance Scores by the mlscorecheck Package](https://towardsdatascience.com/testing-the-consistency-of-reported-machine-learning-performance-scores-by-the-mlscorecheck-package-e1baaba57610/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA small step towards the big leap of reproducible machine learning science\n[Gyorgy Kovacs](https://towardsdatascience.com/author/gyuriofkovacs/)\nNovember 12, 202312 min read\n  * [![](../images/004.jpeg)](https://towardsdatascience.com/topic-modelling-in-production-e3b3e99e4fca/)\n## [Topic Modelling in production](https://towardsdatascience.com/topic-modelling-in-production-e3b3e99e4fca/)\n[ChatGPT](https://towardsdatascience.com/category/artificial-intelligence/chatgpt/)\nLeveraging LangChain to move from ad-hoc Jupyter Notebooks to production modular service\n[Mariya Mansurova](https://towardsdatascience.com/author/miptgirl/)\nOctober 30, 202323 min read\n  * [![](../images/005.png)](https://towardsdatascience.com/deep-time-series-forecasting-with-flow-forecast-part-1-avocado-prices-276a59eb454f/)\n## [Deep Time Series Forecasting with Flow Forecast Part 1: Avocado Prices](https://towardsdatascience.com/deep-time-series-forecasting-with-flow-forecast-part-1-avocado-prices-276a59eb454f/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nPractical examples of using Deep Learning for Time Series Framework Flow Forecast to forecast temporal data\n[Isaac Godfried](https://towardsdatascience.com/author/igodfried/)\nNovember 11, 20225 min read\n  * [![](../images/006.jpeg)](https://towardsdatascience.com/optimizing-data-transfer-in-ai-ml-workloads/)\n## [Optimizing Data Transfer in AI/ML Workloads](https://towardsdatascience.com/optimizing-data-transfer-in-ai-ml-workloads/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nA deep dive on data transfer bottlenecks, their identification, and their resolution with the help of NVIDIA Nsight™ Systems\n[Chaim Rand](https://towardsdatascience.com/author/chaimrand/)\nJanuary 3, 202614 min read\n  * [![](../images/007.jpg)](https://towardsdatascience.com/marginal-joint-and-conditional-probabilities-explained-by-data-scientist-4225b28907a4/)\n## [Marginal, Joint and Conditional Probabilities explained By Data Scientist](https://towardsdatascience.com/marginal-joint-and-conditional-probabilities-explained-by-data-scientist-4225b28907a4/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nLearning Probability the Data Science way\n[Kurtis Pykes](https://towardsdatascience.com/author/kurtispykes/)\nSeptember 5, 20204 min read\n  * [![](../images/008.jpeg)](https://towardsdatascience.com/unwrapping-the-swiss-roll-9249301bd6b7/)\n## [Unwrapping the Swiss Roll](https://towardsdatascience.com/unwrapping-the-swiss-roll-9249301bd6b7/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nDiffusion maps: The amazing dimensionality reduction algorithm you may have never heard of\n[Sebastian Dick](https://towardsdatascience.com/author/sebastiandick42/)\nDecember 13, 202010 min read\n  * [![](../images/009.png)](https://towardsdatascience.com/running-keras-using-cygwin-shell-987d211f3899/)\n## [Running Keras using Cygwin Shell](https://towardsdatascience.com/running-keras-using-cygwin-shell-987d211f3899/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nThe Purpose\n[Natan Katz](https://towardsdatascience.com/author/natan-katz/)\nMarch 16, 20216 min read\n  * [![](../images/010.png)](https://towardsdatascience.com/a-demonstration-of-using-vision-transformers-in-pytorch-mnist-handwritten-digit-recognition-407eafbc15b0/)\n## [A Demonstration of Using Vision Transformers in PyTorch: MNIST Handwritten Digit Recognition](https://towardsdatascience.com/a-demonstration-of-using-vision-transformers-in-pytorch-mnist-handwritten-digit-recognition-407eafbc15b0/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nA hands-on example (with code) of how one can use the popular PyTorch framework to apply the Vision Transformer to a practical image task.\n[Stan Kriventsov](https://towardsdatascience.com/author/stankriventsov/)\nOctober 9, 20205 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": "Diffusion Model Paper Walkthrough: The Art of Noise | Towards Data Science",
    "description": "Understanding and implementing a diffusion model from scratch with PyTorch",
    "keywords": null,
    "author": "Muhammad Ardi Putra",
    "og:locale": "en_US",
    "og:type": "article",
    "og:title": "Diffusion Model Paper Walkthrough: The Art of Noise",
    "og:description": "Understanding and implementing a diffusion model from scratch with PyTorch",
    "og:url": "https://towardsdatascience.com/the-art-of-noise/",
    "og:site_name": "Towards Data Science",
    "og:image": "https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/jr-korpa-SgG6o7wDzd0-unsplash-scaled-1.jpg",
    "og:image:width": "2560",
    "og:image:height": "1706",
    "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": "33 min read",
    "article:published_time": "2025-04-02T19:12:22.000Z",
    "article:modified_time": "2025-04-02T19:12:22.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": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_000.txt",
      "chars": 218
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_001.txt",
      "chars": 333
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_002.txt",
      "chars": 40
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_003.txt",
      "chars": 932
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_004.txt",
      "chars": 77
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_005.txt",
      "chars": 257
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_006.txt",
      "chars": 1044
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_007.txt",
      "chars": 958
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_008.txt",
      "chars": 308
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_009.txt",
      "chars": 504
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_010.txt",
      "chars": 764
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_011.txt",
      "chars": 284
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_012.txt",
      "chars": 245
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_013.txt",
      "chars": 1040
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_014.txt",
      "chars": 340
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_015.txt",
      "chars": 364
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_016.txt",
      "chars": 929
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_017.txt",
      "chars": 898
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_018.txt",
      "chars": 235
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_019.txt",
      "chars": 446
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_020.txt",
      "chars": 443
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_021.txt",
      "chars": 194
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_022.txt",
      "chars": 155
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_023.txt",
      "chars": 74
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_024.txt",
      "chars": 397
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_025.txt",
      "chars": 574
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_026.txt",
      "chars": 1063
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_027.txt",
      "chars": 406
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_028.txt",
      "chars": 491
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_029.txt",
      "chars": 182
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_030.txt",
      "chars": 183
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_031.txt",
      "chars": 76
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_032.txt",
      "chars": 840
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_033.txt",
      "chars": 32
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_034.txt",
      "chars": 32
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_035.txt",
      "chars": 648
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_036.txt",
      "chars": 45
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_037.txt",
      "chars": 369
    },
    {
      "language": "txt",
      "file": "attachments/bea8a2022ef0/code/block_038.txt",
      "chars": 344
    }
  ],
  "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-20T18:02:35Z"
    },
    {
      "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-20T18:02:35Z"
    },
    {
      "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-20T18:02:35Z"
    },
    {
      "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-20T18:02:35Z"
    }
  ],
  "audio": [],
  "videos": [
    {
      "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/deep-learning/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/muhammad_ardi/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/paper-walkthrough-neural-style-transfer-fc5c978cdaed"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/paper-walkthrough-attention-is-all-you-need-80399cdc59e1"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/computer-vision/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/generative-ai/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/machine-learning/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/tag/pytorch/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/are-you-ready-for-machine-learning-math-fc5a08fc8130/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/category/artificial-intelligence/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/svpino/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/testing-the-consistency-of-reported-machine-learning-performance-scores-by-the-mlscorecheck-package-e1baaba57610/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/author/gyuriofkovacs/"
    },
    {
      "type": "internal",
      "href": "https://towardsdatascience.com/topic-modelling-in-production-e3b3e99e4fca/"
    },
    {
      "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://roadmap.sh/machine-learning"
    },
    {
      "type": "external",
      "href": "https://github.com/MuhammadArdiPutra/medium_articles/blob/main/The%20Art%20of%20Noise.ipynb"
    },
    {
      "type": "external",
      "href": "https://becominghuman.ai/the-deep-autoencoder-in-action-digit-reconstruction-bf177ccbb8c0"
    },
    {
      "type": "external",
      "href": "https://becominghuman.ai/using-variational-autoencoder-vae-to-generate-new-images-14328877e88d"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/pdf/1503.03585"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/pdf/2006.11239"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/pdf/1505.04597"
    },
    {
      "type": "external",
      "href": "https://yann.lecun.com/exdb/mnist/"
    },
    {
      "type": "external",
      "href": "https://arxiv.org/pdf/1706.03762"
    },
    {
      "type": "external",
      "href": "https://www.facebook.com/sharer/sharer.php?title=Diffusion+Model+Paper+Walkthrough%3A+The+Art+of+Noise&u=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F"
    },
    {
      "type": "external",
      "href": "https://www.linkedin.com/shareArticle?mini=true&title=Diffusion+Model+Paper+Walkthrough%3A+The+Art+of+Noise&url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F"
    },
    {
      "type": "external",
      "href": "https://x.com/share?text=Diffusion+Model+Paper+Walkthrough%3A+The+Art+of+Noise&url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F"
    },
    {
      "type": "external",
      "href": "https://www.youtube.com/c/TowardsDataScience"
    },
    {
      "type": "external",
      "href": "https://www.threads.net/@towardsdatascience"
    },
    {
      "type": "external",
      "href": "https://bsky.app/profile/towardsdatascience.com"
    },
    {
      "type": "external",
      "href": "https://thenewstack.io"
    },
    {
      "type": "external",
      "href": "https://roadmap.sh"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-20T17:31:03Z",
  "source_url": "https://towardsdatascience.com/the-art-of-noise/",
  "delivery_status": "unverified",
  "discovery_status": "http_403",
  "missing_reason": "源页面未重新核验:http_403；已按保留正文补采图片",
  "repair_error": "",
  "actual_video_files": [],
  "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[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\n# Diffusion Model Paper Walkthrough: The Art of Noise\nUnderstanding and implementing a diffusion model from scratch with PyTorch\n[Muhammad Ardi Putra](https://towardsdatascience.com/author/muhammad_ardi/)\nApril 2, 202533 min read\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/jr-korpa-SgG6o7wDzd0-unsplash-scaled-1.jpg)\nPhoto by Jr Korpa on Unsplash\n## **Introduction**\nIn my last several articles I talked about generative deep learning algorithms, which mostly are related to text generation tasks. So, I think it would be interesting to switch to generative algorithms for image generation now. We knew that nowadays there have been plenty of deep learning models specialized for generating images out there, such as Autoencoder, Variational Autoencoder (VAE), Generative Adversarial Network (GAN) and Neural Style Transfer (NST). I actually got some of my writings about these topics posted on Medium as well. I provide you the links at the end of this article if you want to read them.\nIn today’s article, I would like to discuss the so-called _diffusion model_ — one of the most impactful models in the field of deep learning for image generation. The idea of this algorithm was first proposed in the paper titled _Deep Unsupervised Learning using Nonequilibrium Thermodynamics_ written by Sohl-Dickstein _et al._ back in 2015 [1]. Their framework was then developed further by Ho _et al._ in 2020 in their paper titled _Denoising Diffusion Probabilistic Models_ [2]. _DDPM_ was later adapted by OpenAI and Google to develop DALLE-2 and Imagen, which we knew that these models have impressive capabilities to generate high-quality images.\nLearn this step by step with the interactive [Machine Learning](https://roadmap.sh/machine-learning) roadmap.\n### **How Diffusion Model Works**\nGenerally speaking, diffusion model works by generating image from noise. We can think of it like an artist transforming a splash of paint on a canvas into a beautiful artwork. In order to do so, the diffusion model needs to be trained first. There are two main steps required to be followed to train the model, namely _forward diffusion_ and _backward diffusion_.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/Forward-Backward-Diffusion-2-1024x396.png)\nFigure 1. The forward and backward diffusion process [3].\nAs you can see in the above figure, forward diffusion is a process where Gaussian noise is applied to the original image iteratively. We keep adding the noise until the image is completely unrecognizable, at which point we can say that the image now lies in the _latent space_. Different from Autoencoders and GANs where the latent space typically has a lower dimension than the original image, the latent space in DDPM maintains the exact same dimensionality as the original one. This noising process follows the principle of a Markov Chain, meaning that the image at timestep _t_ is affected only by timestep _t_ -1. Forward diffusion is considered easy since what we basically do is just adding some noise step by step.\nThe second training phase is called backward diffusion, which our objective here is to remove the noise little by little until we obtain a clear image. This process follows the principle of the _reverse_ Markov Chain, where the image at timestep _t_ -1 can only be obtained based on the image at timestep _t_. Such a denoising process is really difficult since we need to guess which pixels are noise and which ones belong to the actual image content. Thus, we need to employ a neural network model to do so.\nDDPM uses U-Net as the basis of the deep learning architecture for backward diffusion. However, instead of using the original U-Net model [4], we need to make several modifications to it so that it will be more suitable for our task. Later on, I am going to train this model on the MNIST Handwritten Digit dataset [5], and we will see whether it can generate similar images.\nWell, that was pretty much all the fundamental concepts you need to know about diffusion models for now. In the next sections we are going to get even deeper into the details while implementing the algorithm from scratch.\n···\n## **PyTorch Implementation**\nWe are going to start by importing the required modules. In case you’re not yet familiar with the imports below, both `torch` and `torchvision` are the libraries we’ll use for preparing the model and the dataset. Meanwhile, `matplotlib` and `tqdm` will help us display images and progress bars.\npython\n\n```\n# Codeblock 1\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\n\nfrom torch.optim import Adam\nfrom torch.utils.data import DataLoader\nfrom torchvision import datasets, transforms\nfrom tqdm import tqdm\n```\n\nAs the modules have been imported, the next thing to do is to initialize some config parameters. Look at the Codeblock 2 below for the details.\npython\n\n```\n# Codeblock 2\nIMAGE_SIZE     = 28     #(1)\nNUM_CHANNELS   = 1      #(2)\n\nBATCH_SIZE     = 2\nNUM_EPOCHS     = 10\nLEARNING_RATE  = 0.001\n\nNUM_TIMESTEPS  = 1000   #(3)\nBETA_START     = 0.0001 #(4)\nBETA_END       = 0.02   #(5)\nTIME_EMBED_DIM = 32     #(6)\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available else \"cpu\")  #(7)\nDEVICE\n```\n\nmarkup\n\n```\n# Codeblock 2 Outputdevice(type='cuda')\n```\n\nAt the lines marked with `#(1)` and `#(2)` I set `IMAGE_SIZE` and `NUM_CHANNELS` to 28 and 1, which these numbers are obtained from the image dimension in the MNIST dataset. The `BATCH_SIZE`, `NUM_EPOCHS`, and `LEARNING_RATE` variables are pretty straightforward, so I don’t think I need to explain them further.\nAt line `#(3)`, the variable `NUM_TIMESTEPS` denotes the number of iterations in the forward and backward diffusion process. Timestep 0 is the condition where the image is in its original state (the leftmost image in Figure 1). In this case, since we set this parameter to 1000, timestep number 999 is going to be the condition where the image is completely unrecognizable (the rightmost image in Figure 1). It is important to keep in mind that the choice of the number of timesteps involves a tradeoff between model accuracy and computational cost. If we assign a small value for `NUM_TIMESTEPS`, the inference time is going to be shorter, yet the resulting image might not be really good since the model has fewer steps to refine the image in the backward diffusion stage. On the other hand, increasing `NUM_TIMESTEPS` will slow down the inference process, but we can expect the output image to have better quality thanks to the gradual denoising process which results in a more precise reconstruction.\nNext, the `BETA_START` (`#(4)`) and `BETA_END` (`#(5)`) variables are used to control the amount of Gaussian noise added at each timestep, whereas `TIME_EMBED_DIM` (`#(6)`) is employed to determine the feature vector length for storing the timestep information. Lastly, at line `#(7)` I assign `“cuda”` to the `DEVICE` variable if PyTorch detects GPU installed in our machine. I highly recommend you run this project on GPU since training a diffusion model is computationally expensive. In addition to the above parameters, the values set for `NUM_TIMESTEPS`, `BETA_START` and `BETA_END` are all adopted directly from the DDPM paper [2].\nThe complete implementation will be done in several steps: constructing the U-Net model, preparing the dataset, defining noise scheduler for the diffusion process, training, and inference. We are going to discuss each of those stages in the following sub-sections.\n···\n### **The U-Net Architecture: Time Embedding**\nAs I’ve mentioned earlier, the basis of a diffusion model is U-Net. This architecture is used because its output layer is suitable to represent an image, which definitely makes sense since it was initially introduced for image segmentation task at the first place. The following figure shows what the original U-Net architecture looks like.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXdjpJyxjmNlNnUcIxY6wXEogYJDlF5uTqHCLJALivKYjk7kG3xWqL2shDz-pz6OmlL477aOCUDNCJ6et0uFb6Y52igmGd6tupVCONcv-2zu2JOuf_Wk1Ihpv-R1CgJzF85GnD84EQ.png)\nFigure 2. The original U-Net model proposed in [4].\nHowever, it is necessary to modify this architecture so that it can also take into account the timestep information. Not only that, since we will only use MNIST dataset, we also need to make the model smaller. Just remember the convention in deep learning that simpler models are often more effective for simple tasks.\nIn the figure below I show you the entire U-Net model that has been modified. Here you can see that the _time embedding_ tensor is injected to the model at every stage, which will later be done by element-wise summation, allowing the model to capture the timestep information. Next, instead of repeating each of the downsampling and the upsampling stages four times like the original U-Net, in this case we will only repeat each of them twice. Additionally, it is worth noting that the stack of downsampling stages is also known as the _encoder_ , whereas the stack of upsampling stages is often called the _decoder_.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXcNuBTQRFRshgamZs_ZOQvNqJ2adbaXYXLQ4Ahsta7KbQY1Kf2hUtKXm9NyNViPWK8DvnGGJsk0xrZwMItPNzKvm9lldL9jBn7ex-fdhVlhPGhpQpQNBs-JufaFdstTR_wahWSbtQ.png)\nFigure 3. The modified U-Net model for our diffusion task [3].\nNow let’s start constructing the architecture by creating a class for generating the time embedding tensor, which the idea is similar to the _positional embedding_ in Transformer. See the Codeblock 3 below for the details.\npython\n\n```\n# Codeblock 3\nclass TimeEmbedding(nn.Module):\n    def forward(self):\n        time = torch.arange(NUM_TIMESTEPS, device=DEVICE).reshape(NUM_TIMESTEPS, 1)  #(1)\n        print(f\"time\\t\\t: {time.shape}\")\n          \n        i = torch.arange(0, TIME_EMBED_DIM, 2, device=DEVICE)\n        denominator = torch.pow(10000, i/TIME_EMBED_DIM)\n        print(f\"denominator\\t: {denominator.shape}\")\n          \n        even_time_embed = torch.sin(time/denominator)  #(1)\n        odd_time_embed  = torch.cos(time/denominator)  #(2)\n        print(f\"even_time_embed\\t: {even_time_embed.shape}\")\n        print(f\"odd_time_embed\\t: {odd_time_embed.shape}\")\n          \n        stacked = torch.stack([even_time_embed, odd_time_embed], dim=2)  #(3)\n        print(f\"stacked\\t\\t: {stacked.shape}\")\n        time_embed = torch.flatten(stacked, start_dim=1, end_dim=2)  #(4)\n        print(f\"time_embed\\t: {time_embed.shape}\")\n          \n        return time_embed\n```\n\nWhat we basically do in the above code is to create a tensor of size `NUM_TIMESTEPS` × `TIME_EMBED_DIM` (1000×32), where every single row of this tensor will contain the timestep information. Later on, each of the 1000 timesteps will be represented by a feature vector of length 32. The values in the tensor themselves are obtained based on the two equations in Figure 4. In the Codeblock 3 above, these two equations are implemented at line `#(1)` and `#(2)`, each forming a tensor having the size of 1000×16. Next, these tensors are combined using the code at line `#(3)` and `#(4)`.\nHere I also print out every single step done in the above codeblock so that you can get a better understanding of what is actually being done in the TimeEmbedding class. If you still want more explanation about the above code, feel free to read my previous post about Transformer which you can access through the link at the end of this article. Once you clicked the link, you can just scroll all the way down to the Positional Encoding section.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXeYZdpj_71RUdrb5tPPkT6CMAoyGIndJFT1r_BDAKJPKN27lR7ChHp71zziKDqVBJ8MTOFltK96I8YgZL6C9gGi3tRzzJI-ZNIFQrepUIuCJbRklId49hwlBVo2Smt7xSV6fYq2vg.png)\nFigure 4. The sinusoidal positional encoding formula from the Transformer paper [6].\nNow let’s check if the `TimeEmbedding` class works properly using the following testing code. The resulting output shows that it successfully produced a tensor of size 1000×32, which is exactly what we expected earlier.\npython\n\n```\n# Codeblock 4\ntime_embed_test = TimeEmbedding()\nout_test = time_embed_test()\n```\n\nmarkup\n\n```\n# Codeblock 4 Outputtime            : torch.Size([1000, 1])denominator     : torch.Size([16])even_time_embed : torch.Size([1000, 16])odd_time_embed  : torch.Size([1000, 16])stacked         : torch.Size([1000, 16, 2])time_embed      : torch.Size([1000, 32])\n```\n\n···\n### **The U-Net Architecture: DoubleConv**\nIf you take a closer look at the modified architecture, you will see that we actually got lots of repeating patterns, such as the ones highlighted in yellow boxes in the following figure.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXfF_CZmqPr9mzs3sWssa18hE0TWy2fh0mM_hmrxothSCmGBP-s70xoYz_P0q18WNIxSMqOKIMPIbKRHzKa44fvQMvPZdk8FYLFT8FgjnSbtNGpxnFmmKLF-kx4Kn6ByBU51eTx0sw.png)\nFigure 5. The processes done inside the yellow boxes will be implemented in the `DoubleConv` class [3].\nThese five yellow boxes share the same structure, where they consist of two convolution layers with the time embedding tensor injected right after the first convolution operation is performed. So, what we are going to do now is to create another class named `DoubleConv` to reproduce this structure. Look at the Codeblock 5a and 5b below to see how I do that.\npython\n\n```\n# Codeblock 5a\nclass DoubleConv(nn.Module):\n    def __init__(self, in_channels, out_channels):  #(1)\n        super().__init__()\n        \n        self.conv_0 = nn.Conv2d(in_channels=in_channels,  #(2)\n                                out_channels=out_channels, \n                                kernel_size=3, \n                                bias=False, \n                                padding=1)\n        self.bn_0 = nn.BatchNorm2d(num_features=out_channels)  #(3)\n        \n        self.time_embedding = TimeEmbedding()  #(4)\n        self.linear = nn.Linear(in_features=TIME_EMBED_DIM,  #(5)\n                                out_features=out_channels)\n        \n        self.conv_1 = nn.Conv2d(in_channels=out_channels,  #(6)\n                                out_channels=out_channels, \n                                kernel_size=3, \n                                bias=False, \n                                padding=1)\n        self.bn_1 = nn.BatchNorm2d(num_features=out_channels)  #(7)\n        \n        self.relu = nn.ReLU(inplace=True)  #(8)\n```\n\nThe two inputs of the `__init__()` method above gives us flexibility to configure the number of input and output channels (`#(1)`) so that the `DoubleConv` class can be used to instantiate all the five yellow boxes simply by adjusting its input arguments. As the name suggests, here we initialize two convolution layers (line `#(2)` and `#(6)`), each followed by a batch normalization layer and a ReLU activation function. Keep in mind that the two normalization layers need to be initialized separately (line `#(3)` and `#(7)`) since each of them has their own trainable normalization parameters. Meanwhile, the ReLU activation function should only be initialized once (`#(8)`) because it contains no parameters, allowing it to be used multiple times in different parts of the network. At line `#(4)`, we initialize the `TimeEmbedding` layer we created earlier, which will later be connected to a standard linear layer (`#(5)`). This linear layer is responsible to adjust the dimension of the time embedding tensor so that the resulting output can be summed with the output from the first convolution layer in an element-wise manner.\nNow let’s take a look at the Codeblock 5b below to better understand the flow of the `DoubleConv` block. Here you can see that the `forward()` method accepts two inputs: the raw image `x` and the timestep information `t` as shown at line `#(1)`. We initially process the image with the first Conv-BN-ReLU sequence (`#(2–4)`). This Conv-BN-ReLU structure is typically used when working with CNN-based models, even if the illustration does not explicitly show the batch normalization and the ReLU layers. Apart from the image, we then take the _t_ -th timestep information from our embedding tensor of the corresponding image (`#(5)`) and pass it through the linear layer (`#(6)`). We still need to expand the dimension of the resulting tensor using the code at line `#(7)` before performing element-wise summation at line `#(8)`. Finally, we process the resulting tensor with the second Conv-BN-ReLU sequence (`#(9–11)`).\npython\n\n```\n# Codeblock 5b\n    def forward(self, x, t):  #(1)\n        print(f'images\\t\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        \n        x = self.conv_0(x)  #(2)\n        x = self.bn_0(x)    #(3)\n        x = self.relu(x)    #(4)\n        print(f'\\nafter first conv\\t: {x.size()}')\n        \n        time_embed = self.time_embedding()[t]      #(5)\n        print(f'\\ntime_embed\\t\\t: {time_embed.size()}')\n        \n        time_embed = self.linear(time_embed)       #(6)\n        print(f'time_embed after linear\\t: {time_embed.size()}')\n        \n        time_embed = time_embed[:, :, None, None]  #(7)\n        print(f'time_embed expanded\\t: {time_embed.size()}')\n        \n        x = x + time_embed  #(8)\n        print(f'\\nafter summation\\t\\t: {x.size()}')\n        \n        x = self.conv_1(x)  #(9)\n        x = self.bn_1(x)    #(10)\n        x = self.relu(x)    #(11)\n        print(f'after second conv\\t: {x.size()}')\n        \n        return x\n```\n\nTo see if our `DoubleConv` implementation works properly, we are going to test it with the Codeblock 6 below. Here I want to simulate the very first instance of this block, which corresponds to the leftmost yellow box in Figure 5. To do so, we need to we need to set the `in_channels` and `out_channels` parameters to 1 and 64, respectively (`#(1)`). Next, we initialize two input tensors, namely `x_test` and `t_test`. The `x_test` tensor has the size of 2×1×28×28, representing a batch of two grayscale images having the size of 28×28 (`#(2)`). Keep in mind that this is just a dummy tensor of random values which will be replaced with the actual images from MNIST dataset later in the training phase. Meanwhile, `t_test` is a tensor containing the timestep numbers of the corresponding images (`#(3)`). The values for this tensor are randomly selected between 0 and `NUM_TIMESTEPS` (1000). Note that the datatype of this tensor must be an integer since the numbers will be used for indexing, as shown at line `#(5)` back in Codeblock 5b. Lastly, at line `#(4)` we pass both `x_test` and `t_test` tensors to the `double_conv_test` layer.\nBy the way, I re-run the previous codeblocks with the `print()` functions removed prior to running the following code so that the outputs will look neater.\npython\n\n```\n# Codeblock 6\ndouble_conv_test = DoubleConv(in_channels=1, out_channels=64).to(DEVICE)  #(1)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)  #(2)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)  #(3)\n\nout_test = double_conv_test(x_test, t_test)  #(4)\n```\n\nmarkup\n\n```\n# Codeblock 6 Outputimages                  : torch.Size([2, 1, 28, 28])   #(1)timesteps               : torch.Size([2]), tensor([468, 304], device='cuda:0')  #(2)after first conv        : torch.Size([2, 64, 28, 28])  #(3)time_embed              : torch.Size([2, 32])          #(4)time_embed after linear : torch.Size([2, 64])time_embed expanded     : torch.Size([2, 64, 1, 1])    #(5)after summation         : torch.Size([2, 64, 28, 28])  #(6)after second conv       : torch.Size([2, 64, 28, 28])  #(7)\n```\n\nThe shape of our original input tensors can be seen at lines `#(1)` and `#(2)` in the above output. Specifically at line `#(2)`, I also print out the two timesteps that we selected randomly. In this example we assume that each of the two images in the x tensor are already noised with the noise level from 468-th and 304-th timesteps prior to being fed into the network. We can see that the shape of the image tensor x changes to 2×64×28×28 after being passed through the first convolution layer (`#(3)`). Meanwhile, the size of our time embedding tensor becomes 2×32 (`#(4)`), which is obtained by extracting rows 468 and 304 from the original embedding of size 1000×32. In order to allow element-wise summation to be performed (`#(6)`), we need to map the 32-dimensional time embedding vectors into 64 and expand their axes, resulting in a tensor of size 2×64×1×1 (`#(5)`) so that it can be broadcast to the 2×64×28×28 tensor. After the summation is done, we then pass the tensor through the second convolution layer, at which point the tensor dimension does not change at all (`#(7)`).  \n\n···\n### **The U-Net Architecture: Encoder**\nAs we have successfully implemented the `DoubleConv` block, the next step to do is to implement the so-called `DownSample` block. In Figure 6 below, this corresponds to the parts enclosed in the red box.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXeMfBBmQHI-bVKf_gBq-ns2lsQE3nebE1MUzsul-HULsPItk3egRY0Io4G2YTBrBWoSJ38nHotjqHwKoHebvZPVQM0wQzMOw5EfQizcqLba1A9xgFZ4GZ0gHHGyMtDIZAnxe4rOdQ.png)\nFigure 6. The parts of the network highlighted in red are the so-called `DownSample` blocks [3].\nThe purpose of a `DownSample` block is to reduce the spatial dimension of an image, but it is important to note that at the same time it increases the number of channels. In order to achieve this, we can simply stack a `DoubleConv` block and a maxpooling operation. In this case the pooling uses 2×2 kernel size with the stride of 2, causing the spatial dimension of the image to be twice as small as the input. The implementation of this block can be seen in Codeblock 7 below.  \n\npython\n\n```\n# Codeblock 7\nclass DownSample(nn.Module):\n    def __init__(self, in_channels, out_channels):  #(1)\n        super().__init__()\n        \n        self.double_conv = DoubleConv(in_channels=in_channels,  #(2)\n                                      out_channels=out_channels)\n        self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)    #(3)\n    \n    def forward(self, x, t):  #(4)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        \n        convolved = self.double_conv(x, t)   #(5)\n        print(f'\\nafter double conv\\t: {convolved.size()}')\n        \n        maxpooled = self.maxpool(convolved)  #(6)\n        print(f'after pooling\\t\\t: {maxpooled.size()}')\n        \n        return convolved, maxpooled          #(7)\n```\n\nHere I set the `__init__()` method to take number of input and output channels so that we can use it for creating the two `DownSample` blocks highlighted in Figure 6 without needing to write them in separate classes (`#(1)`). Next, the `DoubleConv` and the maxpooling layers are initialized at line `#(2)` and `#(3)`, respectively. Remember that since the `DoubleConv` block accepts image `x` and the corresponding timestep `t` as the inputs, we also need to set the `forward()` method of this `DownSample` block such that it accepts both of them as well (`#(4)`). The information contained in x and t are then combined as the two tensors are processed by the `double_conv` layer, which the output is stored in the variable named `convolved` (`#(5)`). Afterwards, we now actually perform the downsampling with the maxpooling operation at line `#(6)`, producing a tensor named `maxpooled`. It is important to note that both the `convolved` and `maxpooled` tensors are going to be returned, which is essentially done because we will later bring `maxpooled` to the next downsampling stage, whereas the `convolved` tensor will be transferred directly to the upsampling stage in the decoder through skip-connections.\nNow let’s test the `DownSample` class using the Codeblock 8 below. The input tensors used here are exactly the same as the ones in Codeblock 6. Based on the resulting output, we can see that the pooling operation successfully converted the output of the `DoubleConv` block from 2×64×28×28 (`#(1)`) to 2×64×14×14 (`#(2)`), indicating that our DownSample class works properly.\npython\n\n```\n# Codeblock 8\ndown_sample_test = DownSample(in_channels=1, out_channels=64).to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\n\nout_test = down_sample_test(x_test, t_test)\n```\n\nmarkup\n\n```\n# Codeblock 8 Outputoriginal          : torch.Size([2, 1, 28, 28])timesteps         : torch.Size([2]), tensor([468, 304], device='cuda:0')after double conv : torch.Size([2, 64, 28, 28])  #(1)after pooling     : torch.Size([2, 64, 14, 14])  #(2)\n```\n\n···\n### **The U-Net Architecture: Decoder**\nWe need to introduce the so-called `UpSample` block in the decoder, which is responsible for reverting the tensor in the intermediate layers to the original image dimension. In order to maintain a symmetrical structure, the number of `UpSample` blocks must match that of the `DownSample` blocks. Look at the Figure 7 below to see where the two `UpSample` blocks are placed.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXfZED4KpeQBYQei0TkimOP8n4IgFNnYRkD-eTPaybokJ7MhGqEronOUw9ngKM7fIgLPeXTdeQkfOXixeoOa3OnKZ3eeThAkXtEfwZDoJqGhzcBqgtxKCgOPSVubOPv6hypIsbne.png)\nFigure 7. The components inside the blue boxes are the so-called `UpSample` blocks [3].\nSince both `UpSample` blocks are structurally identical, we can just initialize a single class for them, just like the `DownSample` class we created earlier. Look at the Codeblock 9 below to see how I implement it.\npython\n\n```\n# Codeblock 9\nclass UpSample(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        \n        self.conv_transpose = nn.ConvTranspose2d(in_channels=in_channels,  #(1)\n                                                 out_channels=out_channels, \n                                                 kernel_size=2, stride=2)  #(2)\n        self.double_conv = DoubleConv(in_channels=in_channels,  #(3)\n                                      out_channels=out_channels)\n        \n    def forward(self, x, t, connection):  #(4)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n        print(f'connection\\t\\t: {connection.size()}')\n        \n        x = self.conv_transpose(x)  #(5)\n        print(f'\\nafter conv transpose\\t: {x.size()}')\n        \n        x = torch.cat([x, connection], dim=1)  #(6)\n        print(f'after concat\\t\\t: {x.size()}')\n        \n        x = self.double_conv(x, t)  #(7)\n        print(f'after double conv\\t: {x.size()}')\n        \n        return x\n```\n\nIn the `__init__()` method, we use `nn.ConvTranspose2d` to upsample the spatial dimension (`#(1)`). Both the kernel size and stride are set to 2 so that the output will be twice as large (`#(2)`). Next, the `DoubleConv` block will be employed to reduce the number of channels, while at the same time combining the timestep information from the time embedding tensor (`#(3)`).\nThe flow of this `UpSample` class is a bit more complicated than the `DownSample` class. If we take a closer look at the architecture, we’ll see that that we also have a skip-connection coming directly from the encoder. Thus, we need the `forward()` method to accept another argument in addition to the original image `x` and the timestep `t`, namely the residual tensor `connection` (`#(4)`). The first thing we do inside this method is to process the original image `x` with the transpose convolution layer (`#(5)`). In fact, not only upsampling the spatial size, but this layer also reduces the number of channels at the same time. However, the resulting tensor is then directly concatenated with `connection` in a channel-wise manner (`#(6)`), causing it to seem like no channel reduction is performed. It is important to know that at this point these two tensors are just concatenated, meaning that the information from the two are not yet combined. We finally feed these concatenated tensors to the `double_conv` layer (`#(7)`), allowing them to share information to each other through the learnable parameters inside the convolution layers.\nThe Codeblock 10 below shows how I test the `UpSample` class. The size of the tensors to be passed through are set according to the second upsampling block, i.e., the rightmost blue box in Figure 7.\npython\n\n```\n# Codeblock 10\nup_sample_test = UpSample(in_channels=128, out_channels=64).to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, 128, 14, 14)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\nconnection_test = torch.randn((BATCH_SIZE, 64, 28, 28)).to(DEVICE)\n\nout_test = up_sample_test(x_test, t_test, connection_test)\n```\n\nIn the resulting output below, if we compare the input tensor (`#(1)`) with the final tensor shape (`#(2)`), we can clearly see that the number of channels successfully reduced from 128 to 64, while at the same time the spatial dimension increased from 14×14 to 28×28. This essentially means that our `UpSample` class is now ready to be used in the main U-Net architecture.\nmarkup\n\n```\n# Codeblock 10 Outputoriginal             : torch.Size([2, 128, 14, 14])   #(1)timesteps            : torch.Size([2]), tensor([468, 304], device='cuda:0')connection           : torch.Size([2, 64, 28, 28])after conv transpose : torch.Size([2, 64, 28, 28])after concat         : torch.Size([2, 128, 28, 28])after double conv    : torch.Size([2, 64, 28, 28])    #(2)\n```\n\n···\n### **The U-Net Architecture: Putting All Components Together**\nOnce all U-Net components have been created, what we are going to do next is to wrap them together into a single class. Look at the Codeblock 11a and 11b below for the details.\npython\n\n```\n# Codeblock 11a\nclass UNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n      \n        self.downsample_0 = DownSample(in_channels=NUM_CHANNELS,  #(1)\n                                       out_channels=64)\n        self.downsample_1 = DownSample(in_channels=64,            #(2)\n                                       out_channels=128)\n      \n        self.bottleneck   = DoubleConv(in_channels=128,           #(3)\n                                       out_channels=256)\n      \n        self.upsample_0   = UpSample(in_channels=256,             #(4)\n                                     out_channels=128)\n        self.upsample_1   = UpSample(in_channels=128,             #(5)\n                                     out_channels=64)\n      \n        self.output = nn.Conv2d(in_channels=64,                   #(6)\n                                out_channels=NUM_CHANNELS,\n                                kernel_size=1)\n```\n\nYou can see in the `__init__()` method above that we initialize two downsampling (`#(1–2)`) and two upsampling (`#(4–5)`) blocks, which the number of input and output channels are set according to the architecture shown in the illustration. There are actually two additional components I haven’t explained yet, namely the _bottleneck_ (`#(3)`) and the _output_ layer (`#(6)`). The former is essentially just a `DoubleConv` block, which acts as the main connection between the encoder and the decoder. Look at the Figure 8 below to see which components of the network belong to the _bottleneck_ layer. Next, the _output_ layer is a standard convolution layer which is responsible to turn the 64-channel image produced by the last `UpSampling` stage into 1-channel only. This operation is done using a kernel of size 1×1, meaning that it combines information across all channels while operating independently at each pixel position.  \n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/AD_4nXfhOC6uIlf7opq_dG28VlQRfZ3FtOgEmkG5ct_VC9Fu94ulQgucT2oj7YKHotbwEyFwiBiacBiyyi7iQBS39oyKlHqC6ZC75WTfu-WMeiwatVbHb2SUtQEsBktkV6FJ27zLYpnn.png)\nFigure 8. The bottleneck layer (the lower part of the model) acts as the main bridge between the encoder and the decoder of U-Net [3].\nI guess the `forward()` method of the entire U-Net in the following codeblock is pretty straightforward, as what we essentially do here is pass the tensors from one layer to another — just don’t forget to include the skip connections between the downsampling and upsampling blocks.\npython\n\n```\n# Codeblock 11b\n    def forward(self, x, t):  #(1)\n        print(f'original\\t\\t: {x.size()}')\n        print(f'timesteps\\t\\t: {t.size()}, {t}')\n            \n        convolved_0, maxpooled_0 = self.downsample_0(x, t)\n        print(f'\\nmaxpooled_0\\t\\t: {maxpooled_0.size()}')\n            \n        convolved_1, maxpooled_1 = self.downsample_1(maxpooled_0, t)\n        print(f'maxpooled_1\\t\\t: {maxpooled_1.size()}')\n            \n        x = self.bottleneck(maxpooled_1, t)\n        print(f'after bottleneck\\t: {x.size()}')\n    \n        upsampled_0 = self.upsample_0(x, t, convolved_1)\n        print(f'upsampled_0\\t\\t: {upsampled_0.size()}')\n            \n        upsampled_1 = self.upsample_1(upsampled_0, t, convolved_0)\n        print(f'upsampled_1\\t\\t: {upsampled_1.size()}')\n            \n        x = self.output(upsampled_1)\n        print(f'final output\\t\\t: {x.size()}')\n            \n        return x\n```\n\nNow let’s see whether we have correctly constructed the U-Net class above by running the following testing code.\npython\n\n```\n# Codeblock 12\nunet_test = UNet().to(DEVICE)\n\nx_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)\nt_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)\n\nout_test = unet_test(x_test, t_test)\n```\n\nmarkup\n\n```\n# Codeblock 12 Outputoriginal         : torch.Size([2, 1, 28, 28])   #(1)timesteps        : torch.Size([2]), tensor([468, 304], device='cuda:0')maxpooled_0      : torch.Size([2, 64, 14, 14])  #(2)maxpooled_1      : torch.Size([2, 128, 7, 7])   #(3)after bottleneck : torch.Size([2, 256, 7, 7])   #(4)upsampled_0      : torch.Size([2, 128, 14, 14])upsampled_1      : torch.Size([2, 64, 28, 28])final output     : torch.Size([2, 1, 28, 28])   #(5)\n```\n\nWe can see in the above output that the two downsampling stages successfully converted the original tensor of size 1×28×28 (`#(1)`) into 64×14×14 (`#(2)`) and 128×7×7 (`#(3)`), respectively. This tensor is then passed through the bottleneck layer, causing its number of channels to expand to 256 without changing the spatial dimension (`#(4)`). Lastly, we upsample the tensor twice before eventually shrinking the number of channels to 1 (`#(5)`). Based on this output, it looks like our model is working properly. Thus, it is now ready to be trained for our diffusion task.\n···\n### **Dataset Preparation**\nAs we have successfully created the entire U-Net architecture, the next thing to do is to prepare the MNIST Handwritten Digit dataset. Before actually loading it, we need to define the preprocessing steps first using the `transforms.Compose()` method from Torchvision, as shown at line `#(1)` in Codeblock 13. There are two things we do here: converting the images into PyTorch tensors which also scales the pixel values from 0–255 to 0–1 (`#(2)`), and normalize them so that the final pixel values ranging between -1 and 1 (`#(3)`). Next, we download the dataset using `datasets.MNIST()`. In this case, we are going to take the images from the training data, hence we use `train=True` (`#(5)`). Don’t forget to pass the `transform` variable we initialized earlier to the `transform` parameter (`transform=transform`) so that it will automatically preprocess the images as we load them (`#(6)`). Lastly, we need to employ `DataLoader` to load the images from `mnist_dataset` (`#(7)`). The arguments I use for the input parameters are intended to randomly pick `BATCH_SIZE` (2) images from the dataset in each iteration.\npython\n\n```\n# Codeblock 13\ntransform = transforms.Compose([  #(1)\n    transforms.ToTensor(),        #(2)\n    transforms.Normalize((0.5,), (0.5,))  #(3)\n])\n\nmnist_dataset = datasets.MNIST(   #(4)\n    root='./data', \n    train=True,           #(5)\n    download=True, \n    transform=transform   #(6)\n)\n\nloader = DataLoader(mnist_dataset,  #(7)\n                    batch_size=BATCH_SIZE,\n                    drop_last=True, \n                    shuffle=True)\n```\n\nIn the following codeblock, I try to load a batch of images from the dataset. In every iteration, `loader` provides both the images and the corresponding labels, hence we need to store them in two separate variables: `images` and `labels`.\npython\n\n```\n# Codeblock 14\nimages, labels = next(iter(loader))\n\nprint('images\\t\\t:', images.shape)\nprint('labels\\t\\t:', labels.shape)\nprint('min value\\t:', images.min())\nprint('max value\\t:', images.max())\n```\n\nWe can see in the resulting output below that the `images` tensor has the size of 2×1×28×28 (`#(1)`), indicating that two grayscale images of size 28×28 have been successfully loaded. Here we can also see that the length of the `labels` tensor is 2, which matches the number of the loaded images (`#(2)`). Note that in this case the labels are going to be completely ignored. My plan here is that I just want the model to generate any number it previously seen from the entire training dataset without even knowing what number it actually is. Lastly, this output also shows that the preprocessing works properly, as the pixel values now range between -1 and 1.\nmarkup\n\n```\n# Codeblock 14 Outputimages    : torch.Size([2, 1, 28, 28])  #(1)labels    : torch.Size([2])             #(2)min value : tensor(-1.)max value : tensor(1.)\n```\n\nRun the following code if you want to see what the image we just loaded looks like.\npython\n\n```\n# Codeblock 15   \nplt.imshow(images[0].squeeze(), cmap='gray')\nplt.show()\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image.png)\nFigure 9. Output from Codeblock 15 [3].\n···\n### Noise Scheduler\nIn this section we are going to talk about how the forward and backward diffusion are performed, which the process essentially involves adding or removing noise little by little at each timestep. It is necessary to know that we basically want a uniform amount of noise across all timesteps, where in the forward diffusion the image should be completely full of noise exactly at timestep 1000, while in the backward diffusion, we have to get the completely clear image at timestep 0. Hence, we need something to control the noise amount for each timestep. Later in this section, I am going to implement a class named `NoiseScheduler` to do so. — This will probably be the most mathy section of this article, as I’ll display many equations here. But don’t worry about that since we’ll focus on implementing these equations rather than discussing the mathematical derivations.\nNow let’s take a look at the equations in Figure 10 which I will implement in the `__init__()` method of the `NoiseScheduler` class below.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image-1.png)\nFigure 10. The equations we need to implement in the **__init__()** method of the `**NoiseScheduler**`class [3].\npython\n\n```\n# Codeblock 16a\nclass NoiseScheduler:\n    def __init__(self):\n        self.betas = torch.linspace(BETA_START, BETA_END, NUM_TIMESTEPS)  #(1)\n        self.alphas = 1. - self.betas\n        self.alphas_cum_prod = torch.cumprod(self.alphas, dim=0)\n        self.sqrt_alphas_cum_prod = torch.sqrt(self.alphas_cum_prod)\n        self.sqrt_one_minus_alphas_cum_prod = torch.sqrt(1. - self.alphas_cum_prod)\n```\n\nThe above code works by creating multiple sequences of numbers, all of them are basically controlled by `BETA_START` (0.0001), `BETA_END` (0.02), and `NUM_TIMESTEPS` (1000). The first sequence we need to instantiate is the `betas` itself, which is done using `torch.linspace()` (`#(1)`). What it essentially does is that it generates a 1-dimensional tensor of length 1000 starting from 0.0001 to 0.02, where every single element in this tensor corresponds to a single timestep. The interval between each element is uniform, allowing us to generate uniform amount of noise throughout all timesteps as well. With this `betas` tensor, we then compute `alphas`, `alphas_cum_prod`, `sqrt_alphas_cum_prod` and `sqrt_one_minus_alphas_cum_prod` based on the four equations in Figure 10. Later on, these tensors will act as the basis of how the noise is generated or removed during the diffusion process.\nDiffusion is normally done in a sequential manner. However, the forward diffusion process is deterministic, hence we can derive the original equation into a closed form so that we can obtain the noise at a specific timestep without having to iteratively add noise from the very beginning. The Figure 11 below shows what the closed form of the forward diffusion looks like, where _x₀_ represents the original image while epsilon (_ϵ)_ denotes an image made up of random Gaussian noise. We can think of this equation as a weighted combination, where we combine the clear image and the noise according to weights determined by the timestep, resulting in an image with a specific amount of noise.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image-2.png)\nFigure 11. The closed form of the forward diffusion process [3].\nThe implementation of this equation can be seen in Codeblock 16b. In this `forward_diffusion()` method, _x₀_ and _ϵ_ are denoted as `original` and `noise`. Here you need to keep in mind that these two input variables are images, whereas `sqrt_alphas_cum_prod_t` and `sqrt_one_minus_alphas_cum_prod_t` are scalars. Thus, we need to adjust the shape of these two scalars (`#(1)` and `#(2)`) so that the operation at line `#(3)` can be performed. The `noisy_image` variable is going to be the output of this function, which I guess the name is self-explanatory.\npython\n\n```\n# Codeblock 16b\n    def forward_diffusion(self, original, noise, t):\n        sqrt_alphas_cum_prod_t = self.sqrt_alphas_cum_prod[t]\n        sqrt_alphas_cum_prod_t = sqrt_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1)  #(1)\n        \n        sqrt_one_minus_alphas_cum_prod_t = self.sqrt_one_minus_alphas_cum_prod[t]\n        sqrt_one_minus_alphas_cum_prod_t = sqrt_one_minus_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1)  #(2)\n        \n        noisy_image = sqrt_alphas_cum_prod_t * original + sqrt_one_minus_alphas_cum_prod_t * noise  #(3)\n        \n        return noisy_image\n```\n\nNow let’s talk about backward diffusion. In fact, this one is a bit more complicated than the forward diffusion since we need three more equations here. Before I give you these equations, let me show you the implementation first. See the Codeblock 16c below.\npython\n\n```\n# Codeblock 16c\n    def backward_diffusion(self, current_image, predicted_noise, t):  #(1)\n        denoised_image = (current_image - (self.sqrt_one_minus_alphas_cum_prod[t] * predicted_noise)) / self.sqrt_alphas_cum_prod[t]  #(2)\n        denoised_image = 2 * (denoised_image - denoised_image.min()) / (denoised_image.max() - denoised_image.min()) - 1  #(3)\n        \n        current_prediction = current_image - ((self.betas[t] * predicted_noise) / (self.sqrt_one_minus_alphas_cum_prod[t]))  #(4)\n        current_prediction = current_prediction / torch.sqrt(self.alphas[t])  #(5)\n        \n        if t == 0:  #(6)\n            return current_prediction, denoised_image\n        \n        else:\n            variance = (1 - self.alphas_cum_prod[t-1]) / (1. - self.alphas_cum_prod[t])  #(7)\n            variance = variance * self.betas[t]  #(8)\n            sigma = variance ** 0.5\n            z = torch.randn(current_image.shape).to(DEVICE)\n            current_prediction = current_prediction + sigma*z\n            \n            return current_prediction, denoised_image\n```\n\nLater in the inference phase, the `backward_diffusion()` method will be called inside a loop that iterates `NUM_TIMESTEPS` (1000) times, starting from _t_ = 999, continued with _t_ = 998, and so on all the way to _t_ = 0. This function is responsible to remove the noise from the image iteratively based on the `current_image` (the image produced by the previous denoising step), the `predicted_noise` (the noise predicted by U-Net in the previous step), and the timestep information `t` (`#(1)`). In each iteration, noise removal is done using the equation shown in Figure 12, which in Codeblock 16c, this corresponds to lines `#(4-5)`.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image-3.png)\nFigure 12. The equation used for removing noise from the image [3].\nAs long as we haven’t reached _t_ = 0, we will compute the variance based on the equation in Figure 13 (`#(7–8)`). This variance will then be used to introduce another controlled noise to simulate the stochasticity in the backward diffusion process since the noise removal equation in Figure 12 is a deterministic approximation. This is essentially also the reason that we don’t calculate the variance once we reached _t_ = 0 (`#(6)`) since we no longer need to add more noise as the image is completely clear already.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image-5.png)\nFigure 13. The equation used to calculate variance for introducing controlled noise [3].\nDifferent from `current_prediction` which aims to estimate the image of the previous timestep (_xₜ₋₁_), the objective of the `denoised_image` tensor is to reconstruct the original image (_x₀_). Thanks to these different objectives, we need a separate equation to compute `denoised_image`, which can be seen in Figure 14 below. The implementation of the equation itself is written at line `#(2–3)`.\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/image-4.png)\nFigure 14. The equation for reconstructing the original image [3].\nNow let’s test the `NoiseScheduler` class we created above. In the following codeblock, I instantiate a `NoiseScheduler` object and print out the attributes associated with it, which are all computed using the equation in Figure 10 based on the values stored in the `betas` attribute. Remember that the actual length of these tensors is `NUM_TIMESTEPS` (1000), but here I only print out the first 6 elements.\npython\n\n```\n# Codeblock 17\nnoise_scheduler = NoiseScheduler()\n\nprint(f'betas\\t\\t\\t\\t: {noise_scheduler.betas[:6]}')\nprint(f'alphas\\t\\t\\t\\t: {noise_scheduler.alphas[:6]}')\nprint(f'alphas_cum_prod\\t\\t\\t: {noise_scheduler.alphas_cum_prod[:6]}')\nprint(f'sqrt_alphas_cum_prod\\t\\t: {noise_scheduler.sqrt_alphas_cum_prod[:6]}')\nprint(f'sqrt_one_minus_alphas_cum_prod\\t: {noise_scheduler.sqrt_one_minus_alphas_cum_prod[:6]}')\n```\n\nmarkup\n\n```\n# Codeblock 17 Outputbetas                          : tensor([1.0000e-04, 1.1992e-04, 1.3984e-04, 1.5976e-04, 1.7968e-04, 1.9960e-04])alphas                         : tensor([0.9999, 0.9999, 0.9999, 0.9998, 0.9998, 0.9998])alphas_cum_prod                : tensor([0.9999, 0.9998, 0.9996, 0.9995, 0.9993, 0.9991])sqrt_alphas_cum_prod           : tensor([0.9999, 0.9999, 0.9998, 0.9997, 0.9997, 0.9996])sqrt_one_minus_alphas_cum_prod : tensor([0.0100, 0.0148, 0.0190, 0.0228, 0.0264, 0.0300])\n```\n\nThe above output indicates that our `__init__()` method works as expected. Next, we are going to test the `forward_diffusion()` method. If you go back to Figure 16b, you will see that `forward_diffusion()` accepts three inputs: original image, noise image and the timestep number. Let’s just use the image from the MNIST dataset we loaded earlier for the first input (`#(1)`) and a random Gaussian noise of the exact same size for the second one (`#(2)`). Run the Codeblock 18 below to see what these two images look like.\npython\n\n```\n# Codeblock 18\nimage = images[0]  #(1)\nnoise = torch.randn_like(image)  #(2)\n\nplt.imshow(image.squeeze(), cmap='gray')\nplt.show()\nplt.imshow(noise.squeeze(), cmap='gray')\nplt.show()\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/1Gbb6akWrUG3z8FiFSzUbGw.png)\nFigure 15. The two images to be used as the original (left) and the noise image (right). The one on the left is the same image I showed earlier in Figure 9 [3].\nAs we already got the image and the noise ready, what we need to do afterwards is to pass them to the `forward_diffusion()` method alongside the _t_. I actually tried to run the Codeblock 19 below multiple times with _t_ = 50, 100, 150, and so on up to _t_ = 300. You can see in Figure 16 that the image becomes less clear as the parameter increases. In this case, the image is going to be completely filled by noise when the _t_ is set to 999.\npython\n\n```\n# Codeblock 19\nnoisy_image_test = noise_scheduler.forward_diffusion(image.to(DEVICE), noise.to(DEVICE), t=50)\n\nplt.imshow(noisy_image_test[0].squeeze().cpu(), cmap='gray')\nplt.show()\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/1d2WlcbHqUY5xAS_CEJ-x1A.png)\nFigure 16. The result of the forward diffusion process at t=50, 100, 150, and so on until t=300 [3].\nUnfortunately, we cannot test the `backward_diffusion()` method since this process requires us to have our U-Net model trained. So, let’s just skip this part for now. I’ll show you how we can actually use this function later in the inference phase.\n···\n### Training\nAs the U-Net model, MNIST dataset, and the noise scheduler are ready, we can now prepare a function for training. Before we do that, I instantiate the model and the noise scheduler in Codeblock 20 below.\npython\n\n```\n# Codeblock 20\nmodel = UNet().to(DEVICE)\nnoise_scheduler = NoiseScheduler()\n```\n\nThe entire training procedure is implemented in the `train()` function shown in Codeblock 21. Before doing anything, we first initialize the optimizer and the loss function, which in this case we use Adam and MSE, respectively (`#(1–2)`). What we basically want to do here is to train the model such that it will be able to predict the noise contained in the input image, which later on, the predicted noise will be used as the basis of the denoising process in the backward diffusion stage. To actually train the model, we first need to perform forward diffusion using the code at line `#(6)`. This noising process will be done on the `images` tensor (`#(3)`) using the random noise generated at line `#(4)`. Next, we take random number somewhere between 0 and `NUM_TIMESTEPS` (1000) for the `t` (`#(5)`), which is essentially done because we want our model to see images of varying noise levels as an approach to improve generalization. As the noisy images have been generated, we then pass it through the U-Net model alongside the chosen `t` (`#(7)`). The input `t` here is useful for the model as it indicates the current noise level in the image. Lastly, the loss function we initialized earlier is responsible to compute the difference between the actual noise and the predicted noise from the original image (`#(8)`). So, the objective of this training is basically to make the predicted noise as similar as possible to the noise we generated at line `#(4)`.\npython\n\n```\n# Codeblock 21\ndef train():\n    optimizer = Adam(model.parameters(), lr=LEARNING_RATE)  #(1)\n    loss_function = nn.MSELoss()  #(2)\n    losses = []\n    \n    for epoch in range(NUM_EPOCHS):\n        print(f'Epoch no {epoch}')\n        \n        for images, _ in tqdm(loader):\n            \n            optimizer.zero_grad()\n\n            images = images.float().to(DEVICE)  #(3)\n            noise = torch.randn_like(images)  #(4)\n            t = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,))  #(5)\n\n            noisy_images = noise_scheduler.forward_diffusion(images, noise, t).to(DEVICE)  #(6)\n            predicted_noise = model(noisy_images, t)  #(7)\n            loss = loss_function(predicted_noise, noise)  #(8)\n            \n            losses.append(loss.item())\n            loss.backward()\n            optimizer.step()\n\n    return losses\n```\n\nNow let’s run the above training function using the codeblock below. Sit back and relax while waiting the training completes. In my case, I used Kaggle Notebook with Nvidia GPU P100 turned on, and it took around 45 minutes to finish.\npython\n\n```\n# Codeblock 22\nlosses = train()\n```\n\nIf we take a look at the loss graph, it seems like our model learned pretty well as the value is generally decreasing over time with a rapid drop at early stages and a more stable (yet still decreasing) trend in the later stages. So, I think we can expect good results later in the inference phase.\npython\n\n```\n# Codeblock 23\nplt.plot(losses)\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/1HvaaL6gp0s0t-rq6HGh4hA.png)\nFigure 17. How the loss value decreases as the training goes [3].\n···\n### Inference\nAt this point we have already got our model trained, so we can now perform inference on it. Look at the Codeblock 24 below to see how I implement the `inference()` function.\npython\n\n```\n# Codeblock 24\ndef inference():\n\n    denoised_images = []  #(1)\n    \n    with torch.no_grad():  #(2)\n        current_prediction = torch.randn((64, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)  #(3)\n        \n        for i in tqdm(reversed(range(NUM_TIMESTEPS))):  #(4)\n            predicted_noise = model(current_prediction, torch.as_tensor(i).unsqueeze(0))  #(5)\n            current_prediction, denoised_image = noise_scheduler.backward_diffusion(current_prediction, predicted_noise, torch.as_tensor(i))  #(6)\n\n            if i%100 == 0:  #(7)\n                denoised_images.append(denoised_image)\n            \n        return denoised_images\n```\n\nAt the line marked with `#(1)` I initialize an empty list which will be used to store the denoising result every 100 timesteps (`#(7)`). This will later allow us to see how the backward diffusion goes. The actual inference process is encapsulated inside `torch.no_grad()` (`#(2)`). Remember that in diffusion models we generate images from a completely random noise, which we assume that these images are initially at _t_ = 999. To implement this, we can simply use `torch.randn()` as shown at line `#(3)`. Here we initialize a tensor of size 64×1×28×28, indicating that we are about to generate 64 images simultaneously. Next, we write a `for` loop that iterates backwards starting from 999 to 0 (`#(4)`). Inside this loop, we feed the current image and the timestep as the input for the trained U-Net and let it predict the noise (`#(5)`). The actual backward diffusion is then performed at line `#(6)`. At the end of the iteration, we should get new images similar to the ones we have in our dataset. Now let’s call the `inference()` function in the following codeblock.\npython\n\n```\n# Codeblock 25\ndenoised_images = inference()\n```\n\nAs the inference completed, we can now see what the resulting images look like. The Codeblock 26 below is used to display the first 42 images we just generated.\npython\n\n```\n# Codeblock 26\nfig, axes = plt.subplots(ncols=7, nrows=6, figsize=(10, 8))\n\ncounter = 0\n\nfor i in range(6):\n    for j in range(7):\n        axes[i,j].imshow(denoised_images[-1][counter].squeeze().detach().cpu().numpy(), cmap='gray')  #(1)\n        axes[i,j].get_xaxis().set_visible(False)\n        axes[i,j].get_yaxis().set_visible(False)\n        counter += 1\n\nplt.show()\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/1v2HpNVQlem0kpG_AJDrmtQ.png)\nFigure 18. The images generated by the diffusion model trained on the MNIST Handwritten Digit dataset [3].\nIf we take a look at the above codeblock, you can see that the indexer of `[-1]` at line `#(1)` indicates that we only display the images from the last iteration (which corresponds to timestep 0). This is the reason that the images you see in Figure 18 are all free from noise. I do acknowledge that this might not be the best of a result since not all the generated images are valid digit numbers. — But hey, this instead indicates that these images are not merely duplicates from the original dataset.\nHere we can also visualize the backward diffusion process using the Codeblock 27 below. You can see in the resulting output in Figure 19 that we initially start from a complete random noise, which gradually disappears as we move to the right.\npython\n\n```\n# Codeblock 27\nfig, axes = plt.subplots(ncols=10, figsize=(24, 8))\n\nsample_no = 0\ntimestep_no = 0\n\nfor i in range(10):\n    axes[i].imshow(denoised_images[timestep_no][sample_no].squeeze().detach().cpu().numpy(), cmap='gray')\n    axes[i].get_xaxis().set_visible(False)\n    axes[i].get_yaxis().set_visible(False)\n    timestep_no += 1\n\nplt.show()\n```\n\n![](https://assets.insightmediagroup.io/media/wp-content/uploads/2025/04/1M06Pa2xOQw7K0CqC6J5JRA.png)\nFigure 19. What the image looks like at timestep 900, 800, 700 and so on until timestep 0 [3].\n···\n## Ending\nThere are plenty of directions you can go from here. First, you might probably need to tweak the parameter configurations in Codeblock 2 if you want better results. Second, it is also possible to modify the U-Net model by implementing attention layers in addition to the stack of convolution layers we used in the downsampling and the upsampling stages. This does not guarantee you to obtain better results especially for a simple dataset like this, but it’s definitely worth trying. Third, you can also try to use a more complex dataset if you want to challenge yourself.\nWhen it comes to practical applications, there are actually lots of things you can do with diffusion models. The simplest one might be for data augmentation. With diffusion model, we can easily generate new images from a specific data distribution. For example, suppose we are working on an image classification project, but the number of images in the classes are imbalanced. To address this problem, it is possible for us to take the images from the minority class and feed them into a diffusion model. By doing so, we can ask the trained diffusion model to generate a number of samples from that class as many as we want.\nAnd well, that’s pretty much everything about the theory and the implementation of diffusion model. Thanks for reading, I hope you learn something new today!\n_You can access the code used in this project through_[ _this link_](https://github.com/MuhammadArdiPutra/medium_articles/blob/main/The%20Art%20of%20Noise.ipynb) _. Here are also the links to my previous articles about_[ _Autoencoder_](https://becominghuman.ai/the-deep-autoencoder-in-action-digit-reconstruction-bf177ccbb8c0) _,_[_Variational Autoencoder (VAE)_](https://becominghuman.ai/using-variational-autoencoder-vae-to-generate-new-images-14328877e88d)_,_[_Neural Style Transfer (NST)_](https://towardsdatascience.com/paper-walkthrough-neural-style-transfer-fc5c978cdaed)_, and_[ _Transformer_](https://towardsdatascience.com/paper-walkthrough-attention-is-all-you-need-80399cdc59e1) _._\n···\n## References\n[1] Jascha Sohl-Dickstein _et al_.Deep Unsupervised Learning using Nonequilibrium Thermodynamics. Arxiv. <https://arxiv.org/pdf/1503.03585> [Accessed December 27, 2024].\n[2] Jonathan Ho _et al_. Denoising Diffusion Probabilistic Models. Arxiv. <https://arxiv.org/pdf/2006.11239> [Accessed December 27, 2024].\n[3] Image created originally by author.\n[4] Olaf Ronneberger _et al_. U-Net: Convolutional Networks for Biomedical  \nImage Segmentation. Arxiv. <https://arxiv.org/pdf/1505.04597> [Accessed December 27, 2024].\n[5] Yann LeCun _et al_. The MNIST Database of Handwritten Digits. <https://yann.lecun.com/exdb/mnist/> [Accessed December 30, 2024] (Creative Commons Attribution-Share Alike 3.0 license).\n[6] Ashish Vaswani _et al_. Attention Is All You Need. Arxiv. <https://arxiv.org/pdf/1706.03762> [Accessed September 29, 2024].\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[Editor's Picks](https://towardsdatascience.com/tag/editors-pick/)[Computer Vision](https://towardsdatascience.com/tag/computer-vision/)[Generative Ai](https://towardsdatascience.com/tag/generative-ai/)[Machine Learning](https://towardsdatascience.com/tag/machine-learning/)[Pytorch](https://towardsdatascience.com/tag/pytorch/)\nShare This Article\n  * [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&title=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\n  * [](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&title=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\n  * [](https://x.com/share?url=https%3A%2F%2Ftowardsdatascience.com%2Fthe-art-of-noise%2F&text=Diffusion%20Model%20Paper%20Walkthrough%3A%20The%20Art%20of%20Noise)\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/2021/04/0tIfHwSlmAWaytvD9-scaled.jpg)](https://towardsdatascience.com/are-you-ready-for-machine-learning-math-fc5a08fc8130/)\n## [The Math you need for Machine Learning](https://towardsdatascience.com/are-you-ready-for-machine-learning-math-fc5a08fc8130/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA list of resources that will help you level up with the math required for machine learning.\n[Santiago Valdarrama](https://towardsdatascience.com/author/svpino/)\nApril 30, 20214 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2023/11/1dPfbe49P39nlZppBY0dr0w.png)](https://towardsdatascience.com/testing-the-consistency-of-reported-machine-learning-performance-scores-by-the-mlscorecheck-package-e1baaba57610/)\n## [Testing the Consistency of Reported Machine Learning Performance Scores by the mlscorecheck Package](https://towardsdatascience.com/testing-the-consistency-of-reported-machine-learning-performance-scores-by-the-mlscorecheck-package-e1baaba57610/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nA small step towards the big leap of reproducible machine learning science\n[Gyorgy Kovacs](https://towardsdatascience.com/author/gyuriofkovacs/)\nNovember 12, 202312 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2023/10/1zrrzdFgAnm7YAyIYtINhoA.jpeg)](https://towardsdatascience.com/topic-modelling-in-production-e3b3e99e4fca/)\n## [Topic Modelling in production](https://towardsdatascience.com/topic-modelling-in-production-e3b3e99e4fca/)\n[ChatGPT](https://towardsdatascience.com/category/artificial-intelligence/chatgpt/)\nLeveraging LangChain to move from ad-hoc Jupyter Notebooks to production modular service\n[Mariya Mansurova](https://towardsdatascience.com/author/miptgirl/)\nOctober 30, 202323 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2022/11/1wx1JEDKHBfX5Fusd2iODpA.png)](https://towardsdatascience.com/deep-time-series-forecasting-with-flow-forecast-part-1-avocado-prices-276a59eb454f/)\n## [Deep Time Series Forecasting with Flow Forecast Part 1: Avocado Prices](https://towardsdatascience.com/deep-time-series-forecasting-with-flow-forecast-part-1-avocado-prices-276a59eb454f/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nPractical examples of using Deep Learning for Time Series Framework Flow Forecast to forecast temporal data\n[Isaac Godfried](https://towardsdatascience.com/author/igodfried/)\nNovember 11, 20225 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2026/01/1-4.jpeg)](https://towardsdatascience.com/optimizing-data-transfer-in-ai-ml-workloads/)\n## [Optimizing Data Transfer in AI/ML Workloads](https://towardsdatascience.com/optimizing-data-transfer-in-ai-ml-workloads/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nA deep dive on data transfer bottlenecks, their identification, and their resolution with the help of NVIDIA Nsight™ Systems\n[Chaim Rand](https://towardsdatascience.com/author/chaimrand/)\nJanuary 3, 202614 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2020/09/040qVQz-JrIiA9R1O-scaled.jpg)](https://towardsdatascience.com/marginal-joint-and-conditional-probabilities-explained-by-data-scientist-4225b28907a4/)\n## [Marginal, Joint and Conditional Probabilities explained By Data Scientist](https://towardsdatascience.com/marginal-joint-and-conditional-probabilities-explained-by-data-scientist-4225b28907a4/)\n[Artificial Intelligence](https://towardsdatascience.com/category/artificial-intelligence/)\nLearning Probability the Data Science way\n[Kurtis Pykes](https://towardsdatascience.com/author/kurtispykes/)\nSeptember 5, 20204 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2020/12/1ZkvcETEVFVV5LU1IFsLqoA.jpeg)](https://towardsdatascience.com/unwrapping-the-swiss-roll-9249301bd6b7/)\n## [Unwrapping the Swiss Roll](https://towardsdatascience.com/unwrapping-the-swiss-roll-9249301bd6b7/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nDiffusion maps: The amazing dimensionality reduction algorithm you may have never heard of\n[Sebastian Dick](https://towardsdatascience.com/author/sebastiandick42/)\nDecember 13, 202010 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2021/03/1tbvAvMB7mi_w9EjhLtyLxw.png)](https://towardsdatascience.com/running-keras-using-cygwin-shell-987d211f3899/)\n## [Running Keras using Cygwin Shell](https://towardsdatascience.com/running-keras-using-cygwin-shell-987d211f3899/)\n[Deep Learning](https://towardsdatascience.com/category/artificial-intelligence/deep-learning/)\nThe Purpose\n[Natan Katz](https://towardsdatascience.com/author/natan-katz/)\nMarch 16, 20216 min read\n  * [![](https://assets.insightmediagroup.io/media/wp-content/uploads/2020/10/1-DBSfgxHUuknIqmyDVKwCg.png)](https://towardsdatascience.com/a-demonstration-of-using-vision-transformers-in-pytorch-mnist-handwritten-digit-recognition-407eafbc15b0/)\n## [A Demonstration of Using Vision Transformers in PyTorch: MNIST Handwritten Digit Recognition](https://towardsdatascience.com/a-demonstration-of-using-vision-transformers-in-pytorch-mnist-handwritten-digit-recognition-407eafbc15b0/)\n[Machine Learning](https://towardsdatascience.com/category/artificial-intelligence/machine-learning/)\nA hands-on example (with code) of how one can use the popular PyTorch framework to apply the Vision Transformer to a practical image task.\n[Stan Kriventsov](https://towardsdatascience.com/author/stankriventsov/)\nOctober 9, 20205 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/bea8a2022ef0/pages/",
  "body_markdown_path": "attachments/bea8a2022ef0/pages/archived-body.md",
  "local_html": "attachments/bea8a2022ef0/pages/archived-body.html",
  "body_source": "prior_capture"
}