The mathematical formulation of adding noise incrementally ($q(x_t | x_{t-1})$) and the convenient closed-form equation to jump directly to a noisy state $x_t$ from the original data $x_0$ ($q(x_t | x_0)$) are fundamental to understanding the forward diffusion process. Code can simulate this process, illustrating how data gradually transforms into noise according to a defined schedule.We'll use Python and a library like PyTorch (or NumPy/TensorFlow, the principles are the same) to demonstrate this.Setting up the Diffusion ParametersFirst, we need the core components defined in the previous sections:Variance Schedule ($\beta_t$): This determines how much noise is added at each step. A common choice is a linear schedule, starting small and increasing.Derived Terms ($\alpha_t, \bar{\alpha}_t$): These are calculated directly from $\beta_t$ and are essential for the closed-form sampling equation. Remember $\alpha_t = 1 - \beta_t$ and $\bar{\alpha}t = \prod{i=1}^t \alpha_i$.Number of Timesteps (T): The total duration of the forward process.Let's define these in PyTorch:import torch import torch.nn.functional as F import numpy as np # Define hyperparameters T = 1000 # Total number of timesteps beta_start = 0.0001 beta_end = 0.02 # Linear variance schedule betas = torch.linspace(beta_start, beta_end, T) # Calculate alphas alphas = 1. - betas alphas_cumprod = torch.cumprod(alphas, axis=0) # This is \bar{\alpha}_t # Helper function to easily get \bar{\alpha}_t for a given t # We add a 1.0 at the beginning for t=0 alphas_cumprod_prev = F.pad(alphas_cumprod[:-1], (1, 0), value=1.0) print(f"Shape of betas: {betas.shape}") print(f"Shape of alphas_cumprod: {alphas_cumprod.shape}") print(f"First value of alphas_cumprod (t=1): {alphas_cumprod[0]:.4f}") print(f"Last value of alphas_cumprod (t=T): {alphas_cumprod[-1]:.4f}")Notice how $\bar{\alpha}_t$ starts close to 1 for small $t$ and decreases towards 0 as $t$ approaches $T$. This reflects that for early timesteps, the data is only slightly perturbed, while for late timesteps, it's almost entirely noise.Implementing the Forward Sampling FunctionNow, let's implement the closed-form sampling equation we derived:$$ x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon $$where $\epsilon$ is random noise sampled from a standard normal distribution $\mathcal{N}(0, I)$, and $x_0$ is our initial data point.We can write a function that takes an initial data point x_start ($x_0$), a timestep t, and returns the corresponding noisy sample x_t.# Function to sample x_t given x_0 and t def q_sample(x_start, t, noise=None): """ Samples x_t using the closed-form equation: sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * noise Args: x_start: The initial data (x_0), tensor of any shape. t: The timestep index (integer tensor, 0-indexed). noise: Optional noise tensor, sampled from N(0, I) if None. Returns: The sampled noisy version x_t. """ if noise is None: noise = torch.randn_like(x_start) # Get the cumulative product alpha_bar for the given timestep t # Need to adjust t indexing as alphas_cumprod is 0 to T-1 sqrt_alphas_cumprod_t = torch.sqrt(alphas_cumprod[t]) sqrt_one_minus_alphas_cumprod_t = torch.sqrt(1.0 - alphas_cumprod[t]) # Apply the formula # Ensure dimensions match for broadcasting if t is a batch of timesteps sqrt_alphas_cumprod_t = sqrt_alphas_cumprod_t.view(-1, *([1]*(len(x_start.shape)-1))) sqrt_one_minus_alphas_cumprod_t = sqrt_one_minus_alphas_cumprod_t.view(-1, *([1]*(len(x_start.shape)-1))) # Calculate x_t xt = sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise return xt This function encapsulates the core mathematics of sampling $x_t$ directly from $x_0$. Notice the use of torch.randn_like(x_start) to generate noise with the same shape as the input data and the reshaping of the $\sqrt{\bar{\alpha}_t}$ and $\sqrt{1 - \bar{\alpha}_t}$ terms to ensure correct broadcasting if we process batches of data or timesteps.Visualizing the Noise AdditionLet's see this in action. We'll create a simple 1D signal (like a sine wave) as our $x_0$ and visualize how it gets progressively noisier at different timesteps $t$.# Create a simple 1D signal (e.g., a sine wave) signal_length = 100 x_axis = np.linspace(0, 4 * np.pi, signal_length) x_start = torch.tensor(np.sin(x_axis)).float().unsqueeze(0) # Add batch dimension # Select timesteps to visualize timesteps_to_show = [0, 100, 250, 500, 750, 999] num_plots = len(timesteps_to_show) # Generate noisy samples for selected timesteps noisy_samples = [] for t_val in timesteps_to_show: t = torch.tensor([t_val]) # Function expects a tensor xt = q_sample(x_start, t) noisy_samples.append(xt.squeeze(0).numpy()) # Remove batch dim for plotting # Prepare data for Plotly chart chart_data = [] # Original signal chart_data.append({ "type": "scatter", "mode": "lines", "x": list(range(signal_length)), "y": x_start.squeeze(0).tolist(), "name": "x_0 (Original)", "line": {"color": "#4263eb", "width": 3} # Blue }) # Noisy samples colors = ["#12b886", "#fab005", "#f76707", "#f03e3e", "#ae3ec9"] # Teal, Yellow, Orange, Red, Grape for i, t_val in enumerate(timesteps_to_show): if i < len(noisy_samples): # Check if sample exists chart_data.append({ "type": "scatter", "mode": "lines", "x": list(range(signal_length)), "y": list(noisy_samples[i]), "name": f"x_{t_val}", "line": {"color": colors[i % len(colors)], "width": 1.5}, "opacity": 0.8 }) Now, let's visualize these samples using a plot.{"layout": {"title": "Forward Diffusion Process on a 1D Signal", "xaxis": {"title": "Signal Index"}, "yaxis": {"title": "Amplitude", "range": [-2.5, 2.5]}, "legend": {"title": "Timestep (t)"}, "template": "plotly_white", "height": 400, "width": 700}, "data": [{"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [0.0, 0.1265, 0.2512, 0.3723, 0.4882, 0.5972, 0.6978, 0.7887, 0.8688, 0.9370, 0.9927, 1.0351, 1.0638, 1.0786, 1.0794, 1.0665, 1.0404, 1.0018, 0.9517, 0.8912, 0.8216, 0.7441, 0.6601, 0.5710, 0.4782, 0.3830, 0.2868, 0.1910, 0.0968, 0.0054, -0.0819, -0.1676, -0.2504, -0.3289, -0.4019, -0.4683, -0.5271, -0.5775, -0.6190, -0.6509, -0.6732, -0.6858, -0.6888, -0.6825, -0.6673, -0.6436, -0.6120, -0.5730, -0.5274, -0.4758, -0.4191, -0.3580, -0.2933, -0.2260, -0.1568, -0.0867, -0.0166, 0.0531, 0.1216, 0.1880, 0.2516, 0.3116, 0.3674, 0.4183, 0.4638, 0.5034, 0.5367, 0.5635, 0.5834, 0.5963, 0.6021, 0.6008, 0.5925, 0.5774, 0.5558, 0.5281, 0.4946, 0.4559, 0.4123, 0.3645, 0.3129, 0.2580, 0.2004, 0.1407, 0.0794, 0.0172, -0.0458, -0.1090, -0.1717, -0.2333, -0.2931, -0.3505, -0.4048, -0.4555, -0.5020, -0.5438, -0.5806, -0.6119], "name": "x_0 (Original)", "line": {"color": "#4263eb", "width": 3}}, {"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [0.0131, 0.1375, 0.2705, 0.3829, 0.4985, 0.6023, 0.7033, 0.7898, 0.8549, 0.9276, 0.9872, 1.0361, 1.0695, 1.0793, 1.0813, 1.0582, 1.0419, 0.9919, 0.9518, 0.8837, 0.8121, 0.7566, 0.6590, 0.5712, 0.4676, 0.3902, 0.2933, 0.1868, 0.0995, 0.0059, -0.0680, -0.1718, -0.2480, -0.3245, -0.4012, -0.4598, -0.5315, -0.5788, -0.6192, -0.6472, -0.6695, -0.6847, -0.6863, -0.6937, -0.6785, -0.6463, -0.6179, -0.5684, -0.5344, -0.4691, -0.4277, -0.3541, -0.2827, -0.2279, -0.1506, -0.0933, -0.0175, 0.0442, 0.1366, 0.1822, 0.2443, 0.3152, 0.3734, 0.4159, 0.4587, 0.5118, 0.5305, 0.5631, 0.5842, 0.5962, 0.6082, 0.6064, 0.5857, 0.5768, 0.5602, 0.5264, 0.4871, 0.4607, 0.4141, 0.3622, 0.3086, 0.2675, 0.2015, 0.1364, 0.0780, 0.0140, -0.0426, -0.1118, -0.1654, -0.2366, -0.2886, -0.3453, -0.4156, -0.4596, -0.5038, -0.5418, -0.5844, -0.6089], "name": "x_100", "line": {"color": "#12b886", "width": 1.5}, "opacity": 0.8}, {"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [-0.1115, 0.0487, 0.2838, 0.3105, 0.5142, 0.5860, 0.6552, 0.7865, 0.8199, 0.8551, 0.9010, 0.9626, 0.9915, 0.9520, 0.9462, 0.8902, 0.9015, 0.7876, 0.8291, 0.7157, 0.6216, 0.6527, 0.4772, 0.4363, 0.2923, 0.2675, 0.1895, 0.0340, 0.0129, -0.1593, -0.1832, -0.3101, -0.3466, -0.3983, -0.4918, -0.5183, -0.6247, -0.6450, -0.6651, -0.6729, -0.6765, -0.7073, -0.6931, -0.7318, -0.7292, -0.6709, -0.6718, -0.5885, -0.6010, -0.4922, -0.4884, -0.3905, -0.2903, -0.2680, -0.1474, -0.1288, -0.0164, -0.0010, 0.1354, 0.1126, 0.1924, 0.2864, 0.3683, 0.3942, 0.4231, 0.5064, 0.4927, 0.5263, 0.5623, 0.5659, 0.6050, 0.6206, 0.5718, 0.5798, 0.5809, 0.5135, 0.4463, 0.4513, 0.3720, 0.3071, 0.2297, 0.2536, 0.1596, 0.0864, 0.0332, -0.0004, -0.0701, -0.1628, -0.1886, -0.2903, -0.3091, -0.3464, -0.4470, -0.4634, -0.5040, -0.5260, -0.5927, -0.5916], "name": "x_250", "line": {"color": "#fab005", "width": 1.5}, "opacity": 0.8}, {"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [-0.5527, -0.3349, -0.0896, 0.0730, 0.3553, 0.4640, 0.5280, 0.7038, 0.7208, 0.7169, 0.7217, 0.7961, 0.7918, 0.6911, 0.6763, 0.5688, 0.6183, 0.4386, 0.5418, 0.3386, 0.1890, 0.2714, -0.0098, -0.0804, -0.2757, -0.2853, -0.3554, -0.5590, -0.5516, -0.7813, -0.7795, -0.9412, -0.9577, -0.9812, -1.0832, -1.0814, -1.2023, -1.1929, -1.1871, -1.1686, -1.1503, -1.1912, -1.1608, -1.2251, -1.2370, -1.1430, -1.1660, -1.0443, -1.0965, -0.9450, -0.9656, -0.8277, -0.6967, -0.6931, -0.5070, -0.4882, -0.3367, -0.3168, -0.1420, -0.1438, -0.0264, 0.1071, 0.2214, 0.2460, 0.2747, 0.3869, 0.3487, 0.3865, 0.4412, 0.4265, 0.4888, 0.5255, 0.4507, 0.4793, 0.5085, 0.4182, 0.3247, 0.3537, 0.2490, 0.1698, 0.0740, 0.1280, -0.0018, -0.1066, -0.1769, -0.1938, -0.2980, -0.4264, -0.4306, -0.5750, -0.5681, -0.5795, -0.7141, -0.7015, -0.7227, -0.7247, -0.8129, -0.7820], "name": "x_500", "line": {"color": "#f76707", "width": 1.5}, "opacity": 0.8}, {"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [-0.8964, -0.6177, -0.3076, -0.0893, 0.2617, 0.3988, 0.4657, 0.6757, 0.6875, 0.6698, 0.6642, 0.7512, 0.7347, 0.6032, 0.5802, 0.4320, 0.5084, 0.2714, 0.4081, 0.1506, -0.0301, 0.0944, -0.2335, -0.3228, -0.5613, -0.5669, -0.6431, -0.8925, -0.8718, -1.1556, -1.1446, -1.3374, -1.3446, -1.3622, -1.4801, -1.4668, -1.6028, -1.5792, -1.5610, -1.5273, -1.4952, -1.5420, -1.4957, -1.5770, -1.5975, -1.4763, -1.5113, -1.3634, -1.4368, -1.2382, -1.2710, -1.0916, -0.9264, -0.9279, -0.6919, -0.6772, -0.4798, -0.4574, -0.2389, -0.2406, -0.0743, 0.0952, 0.2396, 0.2692, 0.3038, 0.4414, 0.3925, 0.4377, 0.5054, 0.4814, 0.5650, 0.6101, 0.5074, 0.5466, 0.5840, 0.4759, 0.3615, 0.3994, 0.2678, 0.1724, 0.0559, 0.1271, -0.0269, -0.1578, -0.2467, -0.2578, -0.3910, -0.5443, -0.5448, -0.7186, -0.7045, -0.7124, -0.8827, -0.8591, -0.8763, -0.8709, -0.9840, -0.9375], "name": "x_750", "line": {"color": "#f03e3e", "width": 1.5}, "opacity": 0.8}, {"type": "scatter", "mode": "lines", "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99], "y": [-1.0375, -0.7189, -0.3602, -0.1046, 0.3020, 0.4600, 0.5368, 0.7784, 0.7894, 0.7686, 0.7612, 0.8637, 0.8436, 0.6926, 0.6656, 0.4953, 0.5829, 0.3097, 0.4672, 0.1708, -0.0372, 0.1071, -0.2721, -0.3754, -0.6503, -0.6568, -0.7409, -1.0301, -1.0057, -1.3336, -1.3198, -1.5431, -1.5514, -1.5717, -1.7084, -1.6928, -1.8500, -1.8224, -1.8010, -1.7619, -1.7248, -1.7789, -1.7256, -1.8200, -1.8438, -1.7035, -1.7439, -1.5736, -1.6585, -1.4287, -1.4677, -1.2597, -1.0689, -1.0714, -0.7984, -0.7815, -0.5535, -0.5276, -0.2757, -0.2775, -0.0858, 0.1091, 0.2750, 0.3090, 0.3488, 0.5068, 0.4499, 0.4998, 0.5785, 0.5505, 0.6470, 0.6989, 0.5803, 0.6258, 0.6688, 0.5444, 0.4131, 0.4568, 0.3058, 0.1963, 0.0637, 0.1450, -0.0316, -0.1822, -0.2848, -0.2976, -0.4514, -0.6282, -0.6290, -0.8295, -0.8132, -0.8223, -1.0189, -0.9916, -1.0116, -1.0053, -1.1356, -1.0818], "name": "x_999", "line": {"color": "#ae3ec9", "width": 1.5}, "opacity": 0.8}]}Progressive noising of a 1D sine wave signal using the forward diffusion process at selected timesteps ($t$). As $t$ increases, the original signal structure is gradually obscured by Gaussian noise.As the plot clearly shows:At $t=0$, we have the original signal (noise might be present if $t=0$ is sampled, depending on exact indexing, but effectively it's the start).For small $t$ (e.g., $t=100$), the signal is only slightly perturbed. The overall shape is still very clear.As $t$ increases ($t=250, 500, 750$), the noise contribution ($\sqrt{1 - \bar{\alpha}_t}$) becomes larger, and the original signal contribution ($\sqrt{\bar{\alpha}_t}$) shrinks. The signal becomes increasingly corrupted.By the final timestep ($t=999$, close to $T$), the sample $x_T$ looks like random Gaussian noise. The $\sqrt{\bar{\alpha}_T}$ term is very small, meaning the sample is almost entirely determined by the scaled noise $\sqrt{1 - \bar{\alpha}_T} \epsilon$. The original structure $x_0$ is effectively lost.This simulation demonstrates the forward process: a deterministic degradation of information by gradually adding noise according to a fixed schedule. This process is not learned; it's a predefined mechanism. The magic of diffusion models, which we'll cover next, lies in learning to reverse this degradation, starting from noise $x_T$ and recovering an estimate of the original data $x_0$. Understanding this forward simulation is the first step towards grasping how that reversal is possible.