---
# System prepended metadata

title: Denoising Diffusion Probabilistic Model
tags: [UNet, Generative AI, Deep learning]

---

# Denoising Diffusion Probabilistic Model


[TOC]

**Paper** : https://arxiv.org/abs/2006.11239


示範 **Example** : https://github.com/jason19990305/Denoising-Diffusion-Probabilistic-Model.git

## Introduction

**DDPM(Denoising Diffusion Probabilistic Model)** 原論文是針對圖像生成設計的，但我們會用 `sklearn`  生成一個 **2D** 版的瑞士捲來示範，**Neural Network** 也是最簡單的 **MLP(Multilayer Perceptron )**，**DDPM** 其中一個想法是將生成的方式從一步到位，改成了多個步驟 (**Timestep**)來完成，減少 **Neural Network** 的壓力，降低生成的難度，多個步驟是怎麼實現的呢?，假如你用沙子堆出了瑞士捲的圖形，然後讓它飛走，這個過程就是擴散(**Diffusion**)，這個過程也可以稱做 **Forward Process**，如果將這個過程倒放，讓 **Neural Network** 學習從一攤隨機的沙子飛回來變成瑞士捲的圖形，這個過程就稱為 **Reverse Process**。

## Example

這個章節用 **sklearn** 生成一個 **2D** 的點，形狀類似瑞士捲，並且示範擴散與去雜訊的過程

### Forward Process

在這個階段，不需要 Neural Network 參與，在每一個 Timestep $t$ 往原始數據加入一點雜訊，原本的真實數據為 $x_0$，經過 $T$ 次加入雜訊的過程，變成一個看起來毫無規則的 $x_T$，這是一個固定且已知的過程，我們可以精確計算出任意時間點 $t$ 的數據長甚麼樣子，以下用 40 的點的瑞士捲，經過 $T=200$ 的擴散過程，並且對每一個點加上軌跡 : 


![ddpm_part1_forward_traj](https://hackmd.io/_uploads/H1A8YgnGbe.gif =70%x)




### Reverse Process



這個步驟是 **DDPM** 的靈魂所在，**Model** 會專注在預測單一 **Step** 的雜訊，我們的目標就是從 $X_T$ 開始，一步步去除雜訊，最終還原回 $x_0$，我們會建立一個 **Noise Predictor**，他作用是用來預測當前含有雜訊的資料 $x_t$ 裡的雜訊，並且將 $x_{t-1}$ 到 $x_t$ 之間的雜訊去掉，這個去雜訊的過程不斷重複最後就能夠還原出原本的瑞士捲


![ddpm_training_reverse](https://hackmd.io/_uploads/ByvwYgnM-e.gif =70%x)


## Detail


以下講解實作細節

### Forward Process




在執行 **Forward Process (Diffusion Process)** 的過程，$x_t$ 中的 $t$ 代表加了幾次雜訊，每個 **step** 加雜訊的方式 : 

$$
x_t=\sqrt{1-\beta_t}\cdot x_{t-1}+\sqrt{\beta_t}\cdot \epsilon_t
$$

其中
* $\beta_t$ : 雜訊規劃，這是一個預先設定好的數值，介於 0~1 之間，決定了第 $t$ 個 Step 的雜訊強度，$\beta_t$ 越大，代表這一步的雜訊越多，原數據保留的越少。
* $\epsilon_t$ : 代表在 **Timestep** $t$ **Sample** 的雜訊，這個雜訊使用常態分佈 $\epsilon\sim\mathcal{N}(0,I)$ 得到
* $x_t$ : 當下的瑞士捲狀態，是上一刻瑞士捲 $x_{t-1}$ 與雜訊混和的結果
* $x_{t-1}$ : 上一刻的瑞士捲狀態

```python=
def q_step(self, x_prev, t, noise=None):
    """
    One step of forward diffusion (Markov Chain) for visualization.
    x_t = sqrt(1 - beta_t) * x_{t-1} + sqrt(beta_t) * epsilon
    """
    if noise is None:
        noise = torch.randn_like(x_prev)
            
    beta_t = self.betas[t]  # beta_t
    alpha_t = self.alphas[t] # alpha_t = 1 - beta_t
        
    return torch.sqrt(alpha_t) * x_prev + torch.sqrt(beta_t) * noise
```

在 Example 中我們用 40 個點來畫圖，在 Call `q_step` 時，`x_prev` 代表的就是 $x_{t-1}$，每個 $x_t$ 都是 40 個表達瑞士捲的 2D 點，所以他的維度是 `[40,2]`，而對應的 Noise 也是 `[40,2]` 

### Reverse Process


#### Neural Network


我們訓練的目標是透過 **Neural Network** 預測雜訊來去除雜訊，這個 **Neural Network** 我們稱為 **Noise Predictor**，因為瑞士捲只是一個多個 **2D** 點組成的，Noise Predictor 的 **Input** 是 $x_t$，實際在訓練時，我們會隨機取 `batch_size` 個點，所以 $x_t$ 的維度是 `[batch_size,2]`，Noise Predictor 要做的就是預測每個點的 **Noise**，所以他的 **Input Size** 是 2，**Output Size** 也是 **2**，實作上會直接丟一個 **batch** 進去，會一次 **predict** 出 `batch_size` 個雜訊，然後我們就用 **MSE(Mean Square Error)** 計算預測出的 **Noise** 跟實際混和進 $x_t$ 的 **Noise** 差多少，目標就是要越接近越好。


```python=
class NoisePredictor(nn.Module):
    def __init__(self, input_dim=2, time_dim=32):
        super().__init__()
        self.time_dim = time_dim
        
        # Time Embedding
        self.time_mlp = nn.Sequential(
            SinusoidalPositionEmbeddings(time_dim),
            nn.Linear(time_dim, time_dim),
            nn.ReLU()
        )
        
        # MLP Backbone
        # Increased hidden dim 128 -> 256 for better capacity
        self.model = nn.Sequential(
            nn.Linear(input_dim + time_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, input_dim)
        )

    def forward(self, x, t):
        t_emb = self.time_mlp(t)
        x_input = torch.cat([x, t_emb], dim=1)
        return self.model(x_input)
```


另一個細節是對時間的 **Embeddings**，如果 **time step** 設為 **200**，我們要告訴 **Noise Predictor** 現在是第幾個 **step**，但 **200** 對於 **Neural Network** 來說太大了，如果限制數值範圍小一點的話，有助於訓練，所以會將 **Timestep** 做正規化，並且將它升維(**32** 維)成一個向量，有助於給 **Neural Network** 一個更清楚的訊號，並且會增加一層 **Linear Layer** 讓他可以自由調整這個 **Vector**，將特徵放大

針對正規化+升維，`T=200` 的情況，用 `dim=32` 視覺化會像這樣 : 

![position_embedding_vis](https://hackmd.io/_uploads/HyFCeB3zWg.gif =70%x)




```python=
class SinusoidalPositionEmbeddings(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.dim = dim

    def forward(self, time):
        device = time.device
        half_dim = self.dim // 2
        embeddings = math.log(10000) / (half_dim - 1)
        embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
        embeddings = time[:, None] * embeddings[None, :]
        embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
        return embeddings
```

### Training

以下列出 **Training** 步驟 : 

1. **Random Sample** 出 `indices`，用來隨機從 $x_0$ 取 `batch_size` 個點
2. **Random Sample** 出 `t`，用來隨機選擇 **0~T** 之間的時間點，並且亂數的數量為 `batch_size`
3. 用常態分布生成 **Noise**，並透過這個 **Noise** 和 `t` 得到 $x_t$，但這個 $x_t$ 裡面其實是各種不同時刻的，因為 `t` 是隨機選擇的，所以裡面會有 `batch_size` 個不同 `t` 雜訊混和的 (x,y) 點
4. 將 $x_t$ 和 `t` 輸入 **Noise Predictor**，然後預測出 noise
5. 用 **MSE** 計算實際 **noise** 和預測的 **noise** 之間的差異

```python=
def fit(self, dataset, epochs, batch_size, lr, forward_diffusion, device):
    optimizer = optim.Adam(self.parameters(), lr=lr)
    loss_fn = nn.MSELoss()
    timesteps = forward_diffusion.timesteps
        
    print(f"Start Training ({epochs} steps)...")
    for epoch in range(epochs):
        # 1. Sample Batch
        indices = torch.randint(0, len(dataset), (batch_size,))
        x_0 = dataset[indices] # dataset should already be on device
            
        # 2. Sample Random Timesteps t
        t = torch.randint(0, timesteps, (batch_size,)).long().to(device)
            
        # 3. Add Noise (Forward Process)
        noise = torch.randn_like(x_0)
        x_t = forward_diffusion.q_sample(x_0, t, noise)
            
        # 4. Predict Noise
        predicted_noise = self(x_t, t)
            
        # 5. Optimization
        loss = loss_fn(predicted_noise, noise)
            
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
            
        if epoch % 2000 == 0:
            print(f"Step {epoch}, Loss: {loss.item():.6f}")
        
    print("Training Complete!")
```

### Forward (q_sample)


這個 **function** `q_sample` 可以給 $x_0$ 和指定時刻 $t$ 得到對應的 $x_t$

正常來說給定一個時刻 $t$ 我們需要用 **for loop** 一步一步從 $x_0$ 慢慢加入雜訊，但是為了訓練效率，這邊可以做簡化，原本的計算方式展開 : 

$$
x_t = \sqrt{1-\beta_t}\cdot x_{t-1}+\sqrt{\beta_t}\cdot\epsilon_{t-1}
$$
將 $1-\beta_t$ 帶換成 $\alpha_t$ 
$$
x_t = \sqrt{\alpha_t}\cdot x_{t-1}+\sqrt{1-\alpha_t}\cdot\epsilon_{t-1}
$$

再展開

$$
x_t=\sqrt{\alpha_t}\cdot(\sqrt{\alpha_{t-1}}\cdot x_{t-2}+\sqrt{1-\alpha_{t-1}}\cdot\epsilon_{t-2})+\sqrt{1-\alpha_t}\cdot\epsilon_{t-1}
$$

相乘 : 

$$
x_t=\sqrt{\alpha_t\alpha_{t-1}}\cdot x_{t-2}+\sqrt{\alpha_t(1-\alpha_{t-1})}\cdot\epsilon_{t-2}+\sqrt{1-\alpha_t}\cdot\epsilon_{t-1}
$$

其中 $\sqrt{\alpha_t(1-\alpha_{t-1})}$ 和 $\sqrt{1-\alpha_t}$ 可以視為針對高斯雜訊的 **Standard Deviation**，根據兩個常態分佈加法下的封閉(兩個常態分佈合併時)，**Standard Deviation** $A+B=\sqrt{A^2+B^2}$

![gaussian_combination](https://hackmd.io/_uploads/SJX3bP3MWl.png)


$$
A=\sqrt{\alpha_t}\sqrt{1-\alpha_{t-1}}
$$

$$
B=\sqrt{1-\alpha_{t}}
$$

$$
\sqrt{A^2+B^2}=[\alpha_t(1-\alpha_{t-1})]+[1-\alpha_t]
$$

$$
=\alpha_t-\alpha_t\alpha_{t-1}+1-\alpha_t
$$

$$
=1-\alpha_t\alpha_{t-1}
$$

所以就可以寫成

$$
x_t=\sqrt{\alpha_t\alpha_{t-1}}\cdot x_{t-2}+\sqrt{1-\alpha_t\alpha_{t-1}}\cdot\epsilon
$$

因為 $\epsilon_{t-1}$ 和 $\epsilon_{t-2}$ 都是從同一個常態分佈 **Sample** 的，在統計上即便有 1000 個 $\epsilon$ 相加，也都跟重新 **Sample** 一個 $\epsilon$ 是一樣的

上面的 **Equation** 還需要展開到 `t=0`，但我們可以寫成累乘的形式

$$
\bar{\alpha_t} = \prod^t_{i=1}a_i
$$

$$
x_t=\sqrt{\bar{\alpha_t}}\cdot x_0+\sqrt{1-\bar{\alpha_t}}\cdot \epsilon
$$

這樣我們就可以預先算好所有 $\bar{\alpha_t}$ 來用



```python=
def q_sample(self, x_0, t, noise=None):
        """
        Input data x_0 and timestep t, return noised x_t
        Formula: x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon
        """
        if noise is None:
            noise = torch.randn_like(x_0)
        
        # Get coefficients for timestep t (handling batch t)
        # Using self.sqrt_alphas_cumprod
        sqrt_alpha_bar_t = self.sqrt_alphas_cumprod[t].reshape(-1, 1)
        sqrt_one_minus_alpha_bar_t = self.sqrt_one_minus_alphas_cumprod[t].reshape(-1, 1)
        
        # Reparameterization trick
        return sqrt_alpha_bar_t * x_0 + sqrt_one_minus_alpha_bar_t * noise
```


### Inference (p_sample)



現在我們訓練完了一個可以預測 **Noise** 的 **Neural Network**，現在要講解如何將預測的雜訊從 $x_t$ 解雜訊成 $x_{t-1}$ : 

1. $x_t$ : 當前要去雜訊的數據
2. $t$ : 當前時刻
3. $\beta_t$ : 預先定義好的參數，要跟 **Forward Process** 一樣
4. $\epsilon_\theta$ : **Model** 預測的 **Noise**
5. $\mu$ : `mean` 代表 **Reverse Process** 的 $x_{t-1}$
6. $\sigma_t$ : 代表標準差(**Standard Deviation**)，其實就是 **Forward Process** 施加雜訊時用的 $\sqrt{\beta_t}$
7. $z$ : 為隨機雜訊 $z\sim\mathcal{N}(0,I)$ 

**Reverse Diffusion Process** 的 **Equation** : 
$$
\mu=\frac{1}{\sqrt{\alpha_t}}(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\epsilon_\theta)+\sigma_tz
$$

> 後面有推導

其中 $\mu$ 其實就是 $x_{t-1}$，也就是將 $x_t$ 去雜訊後的樣子


```python=
def p_sample(model, x_t, t, betas, sqrt_one_minus_alphas_cumprod):
    """
    Reverse process step: Predict mean and variance to sample x_{t-1} from x_t
    Formula: x_{t-1} = 1/sqrt(alpha_t) * (x_t - (beta_t / sqrt(1-alpha_bar_t)) * epsilon_theta) + sigma_t * z
    """
    # 1. Get coefficients
    beta_t = betas[t]
    alpha_t = 1 - beta_t
    sqrt_alpha_t = torch.sqrt(alpha_t)
    
    # 2. Predict Noise (epsilon_theta)
    # t is scalar, so create a tensor batch
    t_tensor = torch.tensor([t] * len(x_t)).long().to(x_t.device)
    epsilon_theta = model(x_t, t_tensor)
    
    # 3. Compute Mean
    # This formula removes the predicted noise to estimate the previous state
    sqrt_one_minus_alpha_bar_t = sqrt_one_minus_alphas_cumprod[t]
    
    mean = (1 / sqrt_alpha_t) * (x_t - (beta_t / sqrt_one_minus_alpha_bar_t) * epsilon_theta)
    
    # 4. Add Variance (Sigma * z)
    if t > 0:
        z = torch.randn_like(x_t)
        sigma_t = torch.sqrt(beta_t) # Simple option for variance
        return mean + sigma_t * z
    else:
        return mean # No noise added at the final step t=0
```


效果在上面展示過了，下面展示一個簡化版

---

非官方簡化版本 : 

$$
x_t = \sqrt{\alpha_t}\cdot x_{t-1}+\sqrt{1-\alpha_t}\cdot\epsilon_{t-1}
$$

直接移項 (減右半，除$\sqrt{\alpha_t}$) : 

$$ 
x_{t-1} = \frac{1}{\sqrt{\alpha_t}} (x_t - \sqrt{1-\alpha_t} \epsilon_{t-1}) 
$$

但$\epsilon_{t-1}$ 不太準確，他代表的是加在 $x_{t-1}$ 的 **Noise**，我們應該用 **Noise Predictor** 預測的雜訊 $\epsilon_\theta$，這樣也能產生類似的效果

> 在數學上並不準確，跟原始 **DDPM** 是不同的

會像這樣 :

![ddpm_training_reverse_simple](https://hackmd.io/_uploads/BJrRbspGbg.gif =70%x)

且經過實驗如果不加 `mean + sigma_t * z` 的話簡化版會變成一個點，有點像給一點阻力?，不要讓他在 **t** 還沒接近 0 之前就完全消除所有雜訊，因為 **Forward Process** 的時候就是前期雜訊較大，後期雜訊較小，所以讓他在處理最後的細節時，可以讓點都盡量散一點，不要縮成一個點，也就是直接釘在平均值上，我覺得應該是 t 較大時不同方向的 x、y 基本上是完全破壞的，**Noise Predictor** 不會預測出一個特定方向的 **Noise**，當 t 接近 0 的時候其實 **Noise Predictor** 就可以推敲出原圖的位置了，所以生成的 **Noise** 就能將 x、y 推到對應的位置

會像這樣 : 

![ddpm_training_reverse](https://hackmd.io/_uploads/rkKz_oazWe.gif =70%x)

> 我們在 **Forward Process** 的時候加的雜訊都是 $\mathcal{N}(0,I)$，所以不會因為 **t** 不一樣 **Noise Predictor** 就會給出不同尺度的 **Noise**，主要是 **Standard Deviation** 調整變化量，也就是 $\beta_t$。**Neural Network** 其實在 **t** 時刻也只是預測了在這個時候 **Noise** 的期望值而已


## Generate Image

### CNN

**Convolution Neural Network**

**Deep Learning** 常用的全連接層 (**Fully Connected Layer**)或多層感知機(**Multi-Layer Perceptron**) 長這樣 : 

![image](https://hackmd.io/_uploads/B10HWFK-Ze.png =70%x)


而 **CNN** 的連接方式不一樣，是模仿影像處裡常用的卷積運算設計的，說連接反而會有點誤導，用 **Gaussian Filter** 的影像處理舉例 : 

![1_Ra4DG6PT0hxnvH2aW2OUKw](https://hackmd.io/_uploads/BygFG3FtZbe.gif =70%x)

**Deep Learning** 可訓練的部分是 **Weight** 和 **Bias**，而 **CNN** 的架構 **Weight** 就是 **Filter** 的數值，**Bias** 則是在卷積運算完成後加上 : 

![image](https://hackmd.io/_uploads/H1jppYKZ-e.png)

以 **Pytorch** 的 **Conv2d** 舉例的話，每個參數的意義如下 : 
1. `in_channel` : 代表 **Input Channel**，如果是彩色影像，通常會有 **RGB** 三個 **Channel**，所以 `in_channel` 就要填 **3**
2. `out_channel` : 代表 **Output Channel**，代表經過這層 **Layer** 後要有幾個 **Channel**，除了 **Input** 的原圖以外，其他的影像我們通常會叫他 **Feature Map**，這個 **Feature Map** 會隨著層數增加，越來越厚
3. `kernel_size` : 代表 **Filter** 的大小，像上面那張圖就是 **3**，代表 **3x3** 的 **Filter**
4. `stride` : 卷積運算移動的 **Step** 大小，預設為 **1**
5. `padding` : 讓圖片的 **Width** 和 **Height** 增加的大小，因為卷積會讓圖片縮小(**Width/Height - 1**)，所以很常使用 `padding` 用 **0** 來填充圖片外圍，讓圖片通過 **Conv2d** 時可以保持一樣的大小，`padding` 預設為 0


![1_O06nY1U7zoP4vE5AZEnxKA](https://hackmd.io/_uploads/rJ7xE9KWWl.gif =70%x)


**Weight** 和 **Bias** 數量的計算方式，`input_channel x kernel_size x kernel_size x output_channel + output_channel`

### Convolution Transpose 2D


這個是用來做 **Deconvolution**(反卷積)的


**ConvTranspose2d** : `stride = 1`
![upload_4acc4dabe15b606153a23b5a50552d71](https://hackmd.io/_uploads/ryokn3rVyl.gif)
`Image size : 2x2 -> 4x4`


**ConvTranspose2d** : `stride = 2`
![upload_67d4127c4e659fdf3204eb3b7f4d3b30](https://hackmd.io/_uploads/HyVJh3BNkg.gif)

`Image size : 2x2 -> 5x5`



### Max Pooling



**Max Pooling** 的計算方式，他會指定一個大小，比如 **2x2** 的方框，在 **Feature Map** 中以 **Stride = 2** 的步伐移動，每次計算會取方框內數值最大的，最後 **Feature Map** 的大小會減半。




![image](https://hackmd.io/_uploads/SJDIWs0Z-e.png)


**Max Pooling** 的作用是提取最重要的特徵，還能減少計算量，還有一個作用是他能涵蓋這個 **2x2** 範圍內的 **Feature**，不要小看她只有 **2x2**，經過多次的 **Max Pooling**，最後的 **Feature Map** 涵蓋的範圍就會非常大，只要圖片裡有得到他要的 **Feature**，最後的高維特徵就會被捕捉到，在影像辨識領域他尤為重要


![image](https://hackmd.io/_uploads/Bk68JYeGZe.png)

### U-Net



**U-Net** 有分成 **Encoder** 和 **Decoder** ，左半為 **Encoder** 右半為 **Decoder** : 

![image](https://hackmd.io/_uploads/Sy61AqF-bx.png)


**Encoder** 期間，每次縮減 **Image** 尺寸前，會經過兩次 **Convolution Layer**，然後 **Feature Map** 數量一致

**Encoder** 會將圖片漸漸縮小，並且 **Feature Map** 數量漸漸增加，**Decoder** 會將圖片漸漸變大，並且 **Feature Map** 數量漸漸變少


灰色箭頭的部分就是 **Encoder** 和 **Decoder** 的 **Feature Map** 組合起來


最後一層是將所有的 **Feature Map** 的數量直接縮成一張(**Binary Mask**)，也就是最後會看到的圖片或 **Mask**

**Mask Example** : 
![image](https://hackmd.io/_uploads/SyFHMjT-be.png =60%x)





## Detail

### DiffUNet


參考網址 : 
https://learnopencv.com/denoising-diffusion-probabilistic-models/


**Forward Process** 在這邊是對一個彩色影像施加雜訊的過程 : 

![image](https://hackmd.io/_uploads/SyGmgXS7-e.png)


**Reverse Process** 則是預測雜訊，並慢慢消除雜訊的過程 : 

![image](https://hackmd.io/_uploads/HkPBlmHQWx.png)


我示範的是用比較原始的 **UNet**，但是他就不是生成一個 Mask，而是用來預測 **Noise**


預測影像的 **Noise** 一樣需要對整數 **t** 做 **Embedding**，將 **t** 變成一個不重複的 **Vector**
**Position Embedding** : 

```python!
class SinusoidalPositionEmbeddings(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.dim = dim

    def forward(self, time):
        device = time.device
        half_dim = self.dim // 2
        embeddings = torch.log(torch.tensor(10000.)) / (half_dim - 1)
        embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
        embeddings = time[:, None] * embeddings[None, :]
        embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
        return embeddings
```


---


**UNet** 包含 **Encoder** 和 **Decoder**，這兩個部分在每一個階層，都會經過兩次的 **Convolution** + **ReLu**，並且我們要將代表 **t** 的 **Vector** 加上去，並且讓 **Convolution** 輸出的 **feature map** 通過 **Group Normalization** : 

```python!
# ---------------------------------
# Block Module 
# ---------------------------------
class Block(nn.Module):
    def __init__(self, in_channels, out_channels, time_dim):
        super().__init__()
        
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
        # GroupNorm for better training stability
        # Using 32 groups for GroupNorm
        self.gn1 = nn.GroupNorm(32, out_channels)
        
        # Project time embeddings to match output channel dimension for residual connection
        self.time_mlp = nn.Linear(time_dim, out_channels)

        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        self.gn2 = nn.GroupNorm(32, out_channels)


    def forward(self, x, t):
        
        # 1. Conv + GN + ReLU
        x = self.conv1(x)
        x = self.gn1(x)
        x = torch.relu(x)
        
        # 2. Add time embedding
        t_emb = self.time_mlp(t)
        
        # Expand t_emb to match x's dimensions
        t_emb = t_emb.view(t_emb.shape[0], t_emb.shape[1], 1, 1) # [batch_size, out_channels, 1, 1]
        x = x + t_emb
        
        # 3. Conv + GN + ReLU
        x = self.conv2(x)
        x = self.gn2(x)
        x = torch.relu(x)
        return x
```


---

`Image Size : 256x256`

**DiffUNet** 的本體就可以定義如下，最大的不同就是在 **Encoder** 和 **Decoder** 的 **Block** 多一個 **Time Embedding** : 

```python!
# ---------------------------------
# DiffUNet Model
# ---------------------------------
class DiffUNet(nn.Module):
    def __init__(self, input_channels=3, time_dim=32):
        super().__init__()
        
        self.time_embedding = nn.Sequential(
            SinusoidalPositionEmbeddings(time_dim),
            nn.Linear(time_dim, time_dim),
            nn.ReLU()
        )
        
        # Encoder 
        # 256 -> 128
        self.encoder1 = Block(input_channels, 64, time_dim)
        self.pool1 = nn.MaxPool2d(2)
        
        # 128 -> 64
        self.encoder2 = Block(64, 128, time_dim)
        self.pool2 = nn.MaxPool2d(2)
        
        # 64 -> 32
        self.encoder3 = Block(128, 256, time_dim)
        self.pool3 = nn.MaxPool2d(2)
        
        # 32 -> 16 
        self.encoder4 = Block(256, 512, time_dim)
        self.pool4 = nn.MaxPool2d(2)
        
        # 16 -> 8
        self.encoder5 = Block(512, 512, time_dim)
        self.pool5 = nn.MaxPool2d(2)
        
        # Bottleneck
        # feature map : 512x16x16 -> 1024x16x16
        self.bottleneck = Block(512, 1024, time_dim)
            
        # Decoder
        # 8 -> 16
        self.up5 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)
        # concat with encoder5 output : 512 + 512 = 1024
        self.decoder5 = Block(1024, 512, time_dim)
        
        # 16 -> 32
        self.up4 = nn.ConvTranspose2d(512, 512, kernel_size=2, stride=2)
        # concat with encoder4 output : 512 + 512 = 1024
        self.decoder4 = Block(1024, 512, time_dim)
        
        # 32 -> 64
        self.up3 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)
        self.decoder3 = Block(512, 256, time_dim)
        
        # 64 -> 128
        self.up2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
        # concat with encoder2 output : 128 + 128 = 256
        self.decoder2 = Block(256, 128, time_dim)
        
        # 128 -> 256
        self.up1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)
        # concat with encoder1 output : 64 + 64 = 128
        self.decoder1 = Block(128, 64, time_dim)
        
        # Output layer
        # feature map : 64x16x16 -> 3x16x16
        self.out_conv = nn.Conv2d(64, input_channels, kernel_size=1)
        
    def forward(self, x, t):
        # Time embedding
        t_emb = self.time_embedding(t)
        
        # Encoder
        e1 = self.encoder1(x, t_emb)
        p1 = self.pool1(e1)
        e2 = self.encoder2(p1, t_emb)
        p2 = self.pool2(e2)
        e3 = self.encoder3(p2, t_emb)
        p3 = self.pool3(e3)
        e4 = self.encoder4(p3, t_emb)
        p4 = self.pool4(e4)
        e5 = self.encoder5(p4, t_emb)
        p5 = self.pool5(e5)
        # Bottleneck
        b = self.bottleneck(p5, t_emb)
        # Decoder
        u5 = self.up5(b)
        d5 = self.decoder5(torch.cat([u5, e5], dim=1), t_emb)
        u4 = self.up4(d5)
        d4 = self.decoder4(torch.cat([u4, e4], dim=1), t_emb)
        u3 = self.up3(d4)
        d3 = self.decoder3(torch.cat([u3, e3], dim=1), t_emb)
        u2 = self.up2(d3)
        d2 = self.decoder2(torch.cat([u2, e2], dim=1), t_emb)
        u1 = self.up1(d2)
        d1 = self.decoder1(torch.cat([u1, e1], dim=1), t_emb)
        out = self.out_conv(d1)
        return out
```


### EMA (Exponential Moving Average)

為了增進 **Model** 穩定性，我們會對 **Model** 的 **Parameter** 變化進行 **Moving Average**，希望能讓 **Weight** 的變化不要那麼大


```python!
# ---------------------------------
# EMA (Exponential Moving Average) Class
# ---------------------------------
class EMA:
    def __init__(self, model, beta = 0.995):
        self.beta = beta
        self.step = 0
        
        # Create a copy of the model for EMA
        self.ema_model = copy.deepcopy(model)

        # Freeze the EMA model parameters
        for param in self.ema_model.parameters():
            param.requires_grad_(False)
    def update(self, model):
        self.step += 1
        
        for current_param , ema_param in zip(model.parameters(), self.ema_model.parameters()):
            # Update EMA parameter 
            ema_param.data.mul_(self.beta)
            ema_param.data.add_(current_param.data * (1.0 - self.beta))
    def copy_to(self, model):
        model.load_state_dict(self.ema_model.state_dict())
    
    def save_pretrained(self, path):
        torch.save(self.ema_model.state_dict(), path)
    
```

一開始會複製一份 **DiffUNet** ，並在每次更新參數後 **Call** `update()` 來更新 `ema_model` ，更新方式很簡單，就是舊的部分佔 `0.995` 然後更新後的 **model** 佔 `0.005` ，然後更新 `ema_model`，所以最後如果想存 **Model** 或是 **Inference** 的話，就使用這個 `ema_model`

### Dataset


我這次只用 `Oxford-IIIT Pet Dataset` 裡面挑三張貓的照片來訓練，比較需要注意的是為了讓 **Noise** 傳播正常， 我們會希望整張圖是 **Normalization** 過的，所以我們可以用 `torchvision.transforms` 來做前處理 : 


```python!
class OxfordPetLoader:
    def __init__(self, root='./data', batch_size=8, image_size=256, download=True, cat_only=True):
        self.root = root
        self.batch_size = batch_size
        
        self.transform = transforms.Compose([
            transforms.Resize(image_size),
            transforms.CenterCrop(image_size),
            transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
        ])
        
        full_dataset = datasets.OxfordIIITPet(
            root=root, 
            split='trainval', 
            target_types='category', 
            download=download,
            transform=self.transform
        )
        
        if cat_only:
            # 1. Define cat breeds
            cat_breeds = [
                "Abyssinian", "Bengal", "Birman", "Bombay", "British Shorthair", 
                "Egyptian Mau", "Maine Coon", "Persian", "Ragdoll", "Russian Blue", 
                "Siamese", "Sphynx"
            ]
            
            # 2. Get corresponding label IDs
            # full_dataset.class_to_idx 是一個 dict {'Abyssinian': 0, ...}
            cat_ids = set()
            for cat in cat_breeds:
                if cat in full_dataset.class_to_idx:
                    cat_ids.add(full_dataset.class_to_idx[cat])
            
            print(f"Cat Label IDs: {cat_ids}")

            # 3. Filter dataset for cat images only
            # OxfordIIITPet does not provide direct access to labels, so we access the private attribute
            # full_dataset._labels is a list of labels corresponding to each image
            all_labels = full_dataset._labels 
            
            cat_indices = [i for i, label in enumerate(all_labels) if label in cat_ids]
            
            self.dataset = torch.utils.data.Subset(full_dataset, cat_indices)
            print(f"Filtered Oxford-Pet for Cats. Total Images: {len(self.dataset)}")
        else:
            self.dataset = full_dataset


    def get_loader(self):
        return DataLoader(
            self.dataset,
            batch_size=self.batch_size,
            shuffle=True, # Shuffle for training
            num_workers=get_optimal_num_workers(), # Get CPU cores - 1
            pin_memory=True # Pin memory for faster transfers
        )
```


### p_sample



為了優化圖片的生成品質，我多加了 $x_0$ **Predict** 和 **Clipping**

步驟 : 
`x0_pred`
$$
x'_0 = \frac{(x_t-\sqrt{1-\bar\alpha_t}\cdot \epsilon_\theta)}{\sqrt{\bar\alpha_t}}
$$

**clipping**
$$
x'_0 = clip(x'_0,-1,1)
$$

反推 $\epsilon$
$$
\epsilon=\frac{(x_t-\sqrt{\bar\alpha_t}\cdot x'_0)}{\sqrt{1-\bar\alpha_t}}
$$

計算 **mean**

$$
\mu=\frac{1}{\sqrt\alpha_t}(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\cdot \epsilon)
$$







```python!
class ReverseDiffusion:
    @staticmethod
    @torch.no_grad()
    def p_sample(model , x_t , t, betas ,clip_range=(-1.0, 1.0) , clip_denoised=True):
        
        # Initialize alphas
        # Compute alphas and alpha_bars
        alphas = 1.0 - betas
        beta_t = betas[t]
        alpha_t = alphas[t]
        alpha_bar_t = torch.prod(alphas[:t+1])
        
        # Pre-calculate square roots
        sqrt_alpha_t = torch.sqrt(alpha_t)
        sqrt_one_minus_alpha_bar_t = torch.sqrt(1.0 - alpha_bar_t)
        
        # 1. Predict noise using the model
        t_tensor = torch.full((x_t.shape[0],), t, device=x_t.device, dtype=torch.long)
        epsilon_theta = model(x_t, t_tensor)

        # 2. Estimate x0 from x_t and predicted noise
        # x0_pred = (x_t - sqrt(1-alpha_bar_t) * eps) / sqrt(alpha_bar_t)
        sqrt_alpha_bar_t = torch.sqrt(alpha_bar_t)
        x0_pred = (x_t - sqrt_one_minus_alpha_bar_t * epsilon_theta) / sqrt_alpha_bar_t

        # 3. Clip x0_pred to specified range
        if clip_denoised:
            low, high = clip_range
            x0_pred = torch.clamp(x0_pred, low, high)

        # 4. Compute epsilon used for posterior mean calculation
        epsilon_used = (x_t - sqrt_alpha_bar_t * x0_pred) / sqrt_one_minus_alpha_bar_t

        # 5. Compute the mean of the posterior q(x_{t-1} | x_t, x_0)
        mean = (1.0 / sqrt_alpha_t) * (x_t - (beta_t / sqrt_one_minus_alpha_bar_t) * epsilon_used)
        
        if t > 0:
            z = torch.randn_like(x_t)
            return mean + torch.sqrt(beta_t) * z
        else:
            return mean
```

### Training loop



```python!
# Initialize a copy of the model for EMA (Exponential Moving Average)
    ema_model = EMA(model, beta=0.995)
    
    # Training Loop
    print(f"Start Training ({EPOCHS} steps)...")
    for epoch in range(EPOCHS):
        time_start = time.time()
        total_loss = 0.0
        
        # 1. Sample random timesteps
        t = torch.randint(0, TIMESTEPS, (BATCH_SIZE,), device=device)
            
        # 2. Forward diffusion process
        noise = torch.randn_like(imgs)
        x_t = forward_diffusion.q_sample(imgs, t, noise=noise)

        # 3. Predict noise
        predicted_noise = model(x_t, t)
            
        # 4. Compute loss
        loss = loss_function(predicted_noise, noise)
            
        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        
        # Gradient clipping (optional, can help with stability)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)       
        # Update parameters
        optimizer.step()
        # Update EMA model
        ema_model.update(model)
        # Step the learning rate scheduler
        scheduler.step()

        total_loss += loss.item()
        avg_loss = total_loss / N_IMAGE
        time_end = time.time()
        if (epoch + 1) % 500 == 0 or epoch == 0:
            print(f"Epoch [{epoch + 1}/{EPOCHS}], Loss: {avg_loss:.4f}, Time: {time_end - time_start:.2f}s")
```



### GIF



![ddpm_cat_reverse_process_1](https://hackmd.io/_uploads/S1O2EErXWe.gif)




### Reverse Process 推導



**Forward Process** 更新 (**One step**):

$$
x_t=\sqrt{1-\beta_t}\cdot x_{t-1}+\sqrt{\beta_t}\cdot \epsilon
$$



![image](https://hackmd.io/_uploads/HkPBlmHQWx.png)


* **Forward Process** 可以表達成 **Gaussian Distribution** 型式 : 

$$
q(x_t|x_{t-1})=\mathcal{N}(x_t;\sqrt{1-\beta_t}x_{t-1},\beta_tI)
$$

相當於 **Normal Distribution** 的 $\mu$ 就是 $\sqrt{1-\beta_t}x_{t-1}$，$\sigma$ 就是 $\sqrt{\beta_t}$，藉由這兩個參數，**Sample** 出 $x_t$



---


假如我們已知原圖 $x_0$ 和當前有雜訊的圖 $x_t$，在這個情況下 **Sample** 出 $x_{t-1}$ 的 **Distribution** 可以根據貝氏定理用三個 **Distribution** 組合 : 

根據聯合機率 $P(A|B)=\frac{P(A,B)}{P(B)}$ : 
$$
q(x_{t-1}|x_t,x_0)=\frac{q(x_t,x_{t-1},x_0)}{q(x_t,x_0)}
$$

根據連鎖律 $P(C,B,A)=P(C|A,B)P(B|A)P(A)$，$q(x_t,x_{t-1},x_0)=q(x_t|x_{t-1},x_0)\cdot q(x_{t-1}|x_0)\cdot q(x_0)$，從右至左為知道 $x_0$ **Sample** 出 $x_{t-1}$ 的機率，然後在 $x_0$ 和 $x_{t-1}$ 都已知的情況下，**Sample** 出 $x_t$ 的機率，在 $q(x_t|x_{t-1},x_0)$ 的部分，因為 $x_t$ 是由 $x_{t-1}$ 來的，所以不需要知道 $x_0$，可以簡化成 $q(x_t,x_{t-1})$。


根據連鎖律 $P(B,A)=P(B|A)P(A)$，$q(x_t,x_0)=q(x_t|x_0)\cdot q(x_0)$，然後可以寫成 : 
$$
q(x_{t-1}|x_t,x_0)=\frac{q(x_{t-1}|x_{t-1})\cdot q(x_{t-1}|x_0)\cdot q(x_0)}{q(x_t|x_0)\cdot q(x_0)}
$$


分子分母都有 $q(x_0)$ 所以可以消掉 : 


$$
q(x_{t-1}|x_t,x_0)=\frac{q(x_t|x_{t-1})\cdot q(x_{t-1}|x_0)}{q(x_t|x_0)}
$$

再來因為我們要解的 **Equation** 是關於 $x_{t-1}$ 的 **Distribution**，$x_{t-1}$ 是我們的變數，其他的 $x_t$ 和 $x_0$ 都被視為已知的常數，所以分母的部分可以先拿掉，後面再補回來，改成正比的關係式 :

$$
q(x_{t-1}|x_t,x_0)\propto q(x_t|x_{t-1})\cdot q(x_{t-1}|x_0)
$$


---

將右半邊寫成 **Gaussian Distribution** : 
$$
q(x_t|x_{t-1})=\mathcal{N}(x_t;\sqrt{\alpha_t}x_{t-1},\beta_tI)
$$

$$
q(x_{t-1}|x_0)=\mathcal{N}(x_{t-1};\sqrt{\bar\alpha_{t-1}}x_0,(1-\bar\alpha_{t-1})I)
$$

---

接下來為了找出 **Gaussian Distribution** 的 $\mu$，要把 **PDF(Probability Density Function)** 的指數項部分拿出來化簡，也就是 $e^{-\frac{1}{2\sigma^2}(x_{t-1}-\mu)^2}$ ，且 $e^a\cdot e^b=e^{a+b}$ : 

$\large \mathrm{Exponent}\propto -\frac{1}{2}[\frac{(x_t-\sqrt\alpha_tx_{t-1})^2}{\beta_t}+\frac{(x_{t-1}-\sqrt{\bar\alpha_{t-1}}x_0)^2}{1-\bar\alpha_{t-1}}]$


左半展開 : 

$$
\frac{x_t^2 - 2\sqrt{\alpha_t}x_t x_{t-1} + \alpha_t x_{t-1}^2}{\beta_t}
$$


右半邊展開 : 
$$ 
\frac{x_{t-1}^2 - 2\sqrt{\bar{\alpha}_{t-1}}x_0 x_{t-1} + \bar{\alpha}_{t-1}x_0^2}{1-\bar{\alpha}_{t-1}} 
$$


---

兩邊的二次項 $x_{t-1}^2$ 的係數 : 
$$
\frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}}
$$


一次項的係數 : 
$$
-2 \left( \frac{\sqrt{\alpha_t} x_t}{\beta_t} + \frac{\sqrt{\bar{\alpha}_{t-1}} x_0}{1-\bar{\alpha}_{t-1}} \right)
$$

---

在 **Gaussian Distribution** $\mathcal{N}(x'\mu,\sum)$ 中，指數展開式 $-\frac{1}{2} (\Sigma^{-1} x^2 - 2\Sigma^{-1}\mu x + \dots)$，所以 **Mean** $\mu$ = (一次項系數除以-2)/(二次項係數) : 

$$
\tilde{\mu}_t (x_t, x_0) = \frac{ \frac{\sqrt{\alpha_t} x_t}{\beta_t} + \frac{\sqrt{\bar{\alpha}_{t-1}} x_0}{1-\bar{\alpha}_{t-1}} }{ \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}} }
$$

---

化簡分母(二次項係數) : 
已知 $\bar{\alpha}_t = \alpha_t \bar{\alpha}_{t-1}$ 且 $\beta_t = 1 - \alpha_t$

$$
\begin{aligned}
\text{分母} &= \frac{\alpha_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}} \\
&= \frac{\alpha_t(1-\bar{\alpha}_{t-1}) + \beta_t}{\beta_t(1-\bar{\alpha}_{t-1})} \\
&= \frac{\alpha_t - \alpha_t \bar{\alpha}_{t-1} + 1 - \alpha_t}{\beta_t(1-\bar{\alpha}_{t-1})} \quad (\text{把 } \beta_t \text{ 換成 } 1-\alpha_t) \\
&= \frac{1 - \alpha_t \bar{\alpha}_{t-1}}{\beta_t(1-\bar{\alpha}_{t-1})} \\
&= \frac{1 - \bar{\alpha}_t}{\beta_t(1-\bar{\alpha}_{t-1})} \quad (\text{因為 } \alpha_t \bar{\alpha}_{t-1} = \bar{\alpha}_t)
\end{aligned}
$$

---

將分母倒數乘上去 : 

$$
\tilde{\mu}_t = \left( \frac{\sqrt{\alpha_t} x_t}{\beta_t} + \frac{\sqrt{\bar{\alpha}_{t-1}} x_0}{1-\bar{\alpha}_{t-1}} \right) \cdot \frac{\beta_t(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}
$$

* $x_t$ 的係數 : 
$$
\frac{\sqrt{\alpha_t}}{\beta_t} \cdot \frac{\beta_t(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t} = \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}
$$

* $x_0$ 的係數 : 
$$
\frac{\sqrt{\bar{\alpha}_{t-1}}}{1-\bar{\alpha}_{t-1}} \cdot \frac{\beta_t(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t} = \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1-\bar{\alpha}_t}
$$

最後得到 **Mean** : 
$$
\tilde{\mu}_t = \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1-\bar{\alpha}_t}x_0  
$$

---

為了把 $x_0$ 帶換掉，從 **Forward Process** 的 **Equation** 來下手 : 

$$
x_t = \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon
$$

移項 : 

$$
x_0 = \frac{x_t - \sqrt{1-\bar{\alpha}_t}\epsilon_\theta}{\sqrt{\bar{\alpha}_t}}
$$


帶入至 **Mean** : 
$$
\tilde{\mu}_t = \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}x_t + \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1-\bar{\alpha}_t} \left[ \frac{x_t - \sqrt{1-\bar{\alpha}_t}\epsilon_\theta}{\sqrt{\bar{\alpha}_t}} \right]
$$

$\sqrt{\bar{\alpha}_t}$ 可以拆成 $\sqrt{\alpha_t}\sqrt{\bar{\alpha}_{t-1}}$，所以 $\frac{\sqrt{\bar{\alpha}_{t-1}}}{\sqrt{\bar{\alpha}_t}} = \frac{1}{\sqrt{\alpha_t}}$

整理 : 
$$
\tilde{\mu}_t = \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}x_t + \frac{\beta_t}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} (x_t - \sqrt{1-\bar{\alpha}_t}\epsilon_\theta)
$$


也可以寫成 : 

$$
\tilde{\mu}_t = \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t}x_t + \frac{\beta_t}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)}x_t -\frac{\beta_t\sqrt{1-\bar{\alpha}_t}\epsilon_\theta)}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)}
$$

然後合併 $x_t$ 的係數 : 
$$
\begin{aligned}
\text{Coef}(x_t) &= \frac{\sqrt{\alpha_t}(1-\bar{\alpha}_{t-1})}{1-\bar{\alpha}_t} + \frac{\beta_t}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} \\
&= \frac{1}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} \left[ \alpha_t(1-\bar{\alpha}_{t-1}) + \beta_t \right] \quad (\text{通分，提出} \frac{1}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)}) \\
&= \frac{1}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} \left[ \alpha_t - \bar{\alpha}_t + 1 - \alpha_t \right] \quad (\beta_t=1-\alpha_t, \alpha_t\bar{\alpha}_{t-1}=\bar{\alpha}_t) \\
&= \frac{1-\bar{\alpha}_t}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} \\
&= \frac{1}{\sqrt{\alpha_t}}
\end{aligned}
$$


再來看看剛剛沒處理的，最右邊的那一項(有$\epsilon$ 的部分) : 
$$
\frac{\beta_t}{\sqrt{\alpha_t}(1-\bar{\alpha}_t)} \cdot \sqrt{1-\bar{\alpha}_t} \cdot \epsilon_\theta
$$

可以寫成 : 
$$
\frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}\sqrt{1-\bar\alpha_t}} \cdot \sqrt{1-\bar{\alpha}_t} \cdot \epsilon_\theta
$$

所以可以消掉 $\sqrt{1-\bar\alpha_t}$ : 
$$
\frac{\beta_t}{\sqrt{\alpha_t}\sqrt{1-\bar\alpha_t}}\cdot \epsilon_\theta
$$

合併回 **Mean** : 

$$
\mu_\theta(x_t, t) = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon_\theta \right)
$$





## DDIM

**Github Example** : https://github.com/jason19990305/Denoising-Diffusion-Implicit-Models.git

**DDIM (Denoising Diffusion Implicit Models)** 是一個 **DDPM** 的改良版，主要能加速 **Reverse Process** 的速度，雖然 **DDIM** 的數學定義上是非 **Markov Chain** ，但 **Forward Process Equation** 是一樣的 : 


* **Forward Process** 的 **One Step Equation** : 
$$
x_t = \sqrt{1-\beta_t}\cdot x_{t-1}+\sqrt{\beta_t}\cdot\epsilon_{t-1}
$$

* 直接從 $x_0$ **Diffusion** 到 $x_t$
$$
x_t=\sqrt{\bar{\alpha_t}}\cdot x_0+\sqrt{1-\bar{\alpha_t}}\cdot \epsilon
$$


* **DDPM Reverse Process** 的 **Equation**
$$
x_{t-1} = \frac{1}{\sqrt{\alpha_t}}(x_t - \frac{1-\alpha_t}{\sqrt{1-\bar{\alpha}_t}}\epsilon_\theta) + \sigma_t z
$$




---


**DDIM Reverse Process** 推導 : 

**Forward Process** : 
$$
x_t=\sqrt{\bar{\alpha_t}}\cdot x_0+\sqrt{1-\bar{\alpha_t}}\cdot \epsilon_t
$$

如果替換 $t$ 成 $t-1$ : 
$$
x_{t-1}=\sqrt{\bar{\alpha}_{t-1}}\cdot x_0+\sqrt{1-\bar{\alpha}_{t-1}}\cdot \epsilon_{t-1}
$$

**Forward Process** 移項求 $x_0$

$$
x_0=\frac{x_t-\sqrt{1-\bar\alpha_t}\cdot\epsilon_\theta}{\sqrt{\bar\alpha_t}}
$$


其中因為我們知道 **Noise Predictor** 可以預測 $\epsilon_t$，所以這邊換成 $\epsilon_\theta$ 代表他預測的雜訊，上面這段 **Equation** 代表的是能從任何 $x_t$ 反猜出 $x_0$，但現實是沒辦法一步到位


將 $x_0$ 帶入 $x_{t-1}$ 的 **Equation** : 

$$
x_{t-1}=\sqrt{\bar{\alpha}_{t-1}}\cdot \frac{x_t-\sqrt{1-\bar\alpha_t}\cdot\epsilon_\theta}{\sqrt{\bar\alpha_t}}+\sqrt{1-\bar{\alpha}_{t-1}}\cdot \epsilon_\theta
$$

原本預測 $x_0$ 替換掉之後，我們的目標變成預測 $x_{t-1}$ 了，這樣難度就降低很多，我們也可以稍微提高難度但結果不至於太差的 $x_t$，比如換成 $x_{t+10}$ 之類的，但計算累積 **Variance** 的部分也要跟著一起變

---

**DDIM** 的另一個特點就是允許跳點

**Example** : 
假如產生一個 **Array** 來指定跳點的 $t$ 
```python!
[0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 69, 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 129, 134, 138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190, 194, 199]
```
假如 $t=12$，那更新圖片的機算方式就是 : 
$$
x_{8}=\sqrt{\bar{\alpha}_{8}}\cdot \frac{x_{12}-\sqrt{1-\bar\alpha_{12}}\cdot\epsilon_\theta}{\sqrt{\bar\alpha_{12}}}+\sqrt{1-\bar{\alpha}_{8}}\cdot \epsilon_\theta
$$

而 **Noise Predictor** 的 **Input** 為 : 
$$
\epsilon_\theta(x_{12},t=12)
$$

### Result

![ddim_forward_traj](https://hackmd.io/_uploads/HkBHmf6mbe.gif)

![ddim_reverse_process](https://hackmd.io/_uploads/BkHrmG6XWx.gif)


![ddim_cat_reverse_process_0](https://hackmd.io/_uploads/S1irAMaXZe.gif)
