GAN 生成對抗網路
以下以「**生成手寫數字影像,作為 OCR 或影像辨識系統的合成訓練資料**」為實務案例,使用 PyTorch 建立一個簡單的 GAN。資料集採用 MNIST,影像大小為 \(28 \times 28\)。
---
## 1. GAN 架構
GAN 包含兩個神經網路:
- **Generator(生成器,G)**
輸入隨機噪聲 \(z\),輸出一張假影像。
- **Discriminator(判別器,D)**
輸入一張影像,判斷它是真實資料或生成資料。
簡單來說:
- 判別器希望正確分辨真、假影像。
- 生成器希望產生足以欺騙判別器的影像。
---
## 2. 安裝套件
```bash
pip install torch torchvision matplotlib
```
---
## 3. 完整 PyTorch 實作
```python
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
# =========================
# 1. 基本設定
# =========================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
seed = 42
torch.manual_seed(seed)
batch_size = 128
latent_dim = 100
image_dim = 28 * 28
epochs = 20
learning_rate = 0.0002
os.makedirs("outputs", exist_ok=True)
# =========================
# 2. 載入 MNIST
# =========================
# 將像素值從 [0, 1] 映射到 [-1, 1]
# 因為 Generator 最後使用 Tanh
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
dataset = datasets.MNIST(
root="./data",
train=True,
download=True,
transform=transform
)
dataloader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=True,
num_workers=2,
pin_memory=True if torch.cuda.is_available() else False
)
# =========================
# 3. 建立 Generator
# =========================
class Generator(nn.Module):
def __init__(self, latent_dim=100):
super().__init__()
self.model = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 512),
nn.BatchNorm1d(512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 1024),
nn.BatchNorm1d(1024),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(1024, image_dim),
nn.Tanh()
)
def forward(self, z):
image = self.model(z)
return image.view(-1, 1, 28, 28)
# =========================
# 4. 建立 Discriminator
# =========================
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Linear(image_dim, 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout(0.3),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout(0.3),
# 不使用 Sigmoid,搭配 BCEWithLogitsLoss
nn.Linear(256, 1)
)
def forward(self, image):
image = image.view(image.size(0), -1)
return self.model(image)
G = Generator(latent_dim).to(device)
D = Discriminator().to(device)
# =========================
# 5. 權重初始化
# =========================
def initialize_weights(model):
for module in model.modules():
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.constant_(module.bias, 0)
initialize_weights(G)
initialize_weights(D)
# =========================
# 6. Loss 與 Optimizer
# =========================
criterion = nn.BCEWithLogitsLoss()
optimizer_G = optim.Adam(
G.parameters(),
lr=learning_rate,
betas=(0.5, 0.999)
)
optimizer_D = optim.Adam(
D.parameters(),
lr=learning_rate,
betas=(0.5, 0.999)
)
# 固定噪聲,用來觀察每個 epoch 的生成結果
fixed_noise = torch.randn(64, latent_dim, device=device)
# =========================
# 7. GAN 訓練
# =========================
for epoch in range(epochs):
G.train()
D.train()
total_g_loss = 0.0
total_d_loss = 0.0
for real_images, _ in dataloader:
real_images = real_images.to(device)
batch_size_now = real_images.size(0)
real_labels = torch.ones(batch_size_now, 1, device=device)
fake_labels = torch.zeros(batch_size_now, 1, device=device)
# ---------------------------------
# Step A: 訓練 Discriminator
# ---------------------------------
optimizer_D.zero_grad()
# 判斷真實影像
real_logits = D(real_images)
d_real_loss = criterion(real_logits, real_labels)
# 生成假影像
z = torch.randn(batch_size_now, latent_dim, device=device)
fake_images = G(z)
# detach 避免更新 Generator
fake_logits = D(fake_images.detach())
d_fake_loss = criterion(fake_logits, fake_labels)
d_loss = d_real_loss + d_fake_loss
d_loss.backward()
optimizer_D.step()
# ---------------------------------
# Step B: 訓練 Generator
# ---------------------------------
optimizer_G.zero_grad()
z = torch.randn(batch_size_now, latent_dim, device=device)
fake_images = G(z)
fake_logits = D(fake_images)
# Generator 希望 D 將假影像判斷為真
g_loss = criterion(fake_logits, real_labels)
g_loss.backward()
optimizer_G.step()
total_d_loss += d_loss.item()
total_g_loss += g_loss.item()
avg_d_loss = total_d_loss / len(dataloader)
avg_g_loss = total_g_loss / len(dataloader)
# 儲存固定噪聲產生的影像
G.eval()
with torch.no_grad():
generated_images = G(fixed_noise)
save_image(
generated_images,
f"outputs/epoch_{epoch + 1:03d}.png",
nrow=8,
normalize=True
)
print(
f"Epoch [{epoch + 1:02d}/{epochs}] "
f"D Loss: {avg_d_loss:.4f}, "
f"G Loss: {avg_g_loss:.4f}"
)
# 儲存模型
torch.save(G.state_dict(), "outputs/generator.pth")
torch.save(D.state_dict(), "outputs/discriminator.pth")
print("訓練完成,模型已儲存至 outputs/。")
```
---
## 4. 關鍵設定說明
### 4.1 資料正規化
MNIST 原始像素值為 \([0,1]\),程式將其轉成:
\[
[-1, 1]
\]
因為生成器最後一層使用:
```python
nn.Tanh()
```
因此真實影像與生成影像必須使用相同的數值範圍。
---
### 4.2 Generator 設計
Generator 的輸入是 100 維隨機向量:
```python
z = torch.randn(batch_size, latent_dim)
```
經過多層全連接網路後,輸出:
```text
1 × 28 × 28
```
實務上,對於較大的影像,可以改用反卷積或上採樣卷積,例如:
```text
Linear → Reshape → ConvTranspose2d → BatchNorm → ReLU
```
本案例使用全連接層,是為了讓模型容易理解與快速執行。
---
### 4.3 Discriminator 設計
Discriminator 將影像攤平成 784 維向量,輸出一個 logit:
```python
nn.Linear(256, 1)
```
這裡沒有加 `Sigmoid`,因為使用:
```python
nn.BCEWithLogitsLoss()
```
此損失函數已經內含 Sigmoid,通常比自行使用 `Sigmoid + BCELoss` 更穩定。
---
### 4.4 訓練順序
每一個 batch 會進行兩個步驟:
#### 訓練判別器
```python
d_real_loss = criterion(D(real_images), real_labels)
d_fake_loss = criterion(D(fake_images.detach()), fake_labels)
```
使用 `detach()` 是為了避免判別器更新時,同時反向更新 Generator。
#### 訓練生成器
```python
g_loss = criterion(D(fake_images), real_labels)
```
Generator 將假影像標記成「真」,藉此學習欺騙 Discriminator。
---
### 4.5 Optimizer 設定
GAN 常使用 Adam:
```python
optim.Adam(
model.parameters(),
lr=0.0002,
betas=(0.5, 0.999)
)
```
其中:
- learning rate:`0.0002`
- `beta1=0.5` 常見於 GAN,可降低訓練震盪
- batch size:`128`
- epoch:`20`
實際訓練時,可能需要 30~100 個 epochs 才能得到較清晰的數字。
---
## 5. 產生結果
程式會在 `outputs/` 資料夾產生:
```text
epoch_001.png
epoch_002.png
...
epoch_020.png
generator.pth
discriminator.pth
```
其中每張 `epoch_xxx.png` 為 64 張生成影像的排列圖,可以觀察模型逐步學習的過程:
- 初期:大多是雜訊
- 中期:開始出現數字輪廓
- 後期:數字形狀較清晰,但可能出現重複或變形
---
## 6. 模型評估
GAN 不適合只使用準確率或均方誤差評估,因為生成影像不一定要與某一張真實影像完全相同。常見的評估方式如下。
### 6.1 視覺化評估
固定相同的 `fixed_noise`,比較不同 epoch 的影像:
```python
fixed_noise = torch.randn(64, latent_dim, device=device)
```
這樣可以觀察模型是否逐漸生成清楚的手寫數字。
---
### 6.2 使用判別器分數
可以計算判別器對生成影像的平均信心:
```python
import torch
G.load_state_dict(torch.load("outputs/generator.pth", map_location=device))
D.load_state_dict(torch.load("outputs/discriminator.pth", map_location=device))
G.eval()
D.eval()
with torch.no_grad():
z = torch.randn(1000, latent_dim, device=device)
fake_images = G(z)
fake_logits = D(fake_images)
fake_scores = torch.sigmoid(fake_logits)
print("平均生成影像真實分數:",
fake_scores.mean().item())
```
不過要注意:
> 判別器分數高不一定代表生成影像品質真的好,因為判別器本身也可能過度適應訓練資料。
---
### 6.3 多樣性評估
除了影像清晰度,也要確認生成器沒有發生 **Mode Collapse**,也就是只會生成少數幾種數字。
可以先計算生成影像的平均像素標準差:
```python
with torch.no_grad():
z = torch.randn(1000, latent_dim, device=device)
fake_images = G(z)
pixel_std = fake_images.std().item()
print("生成影像像素標準差:", pixel_std)
```
這只能作為簡單參考。更完整的方式是:
- 將生成影像交給 MNIST 分類器,觀察數字類別是否涵蓋 0~9
- 比較每個數字類別的生成數量
- 使用 FID 或 KID
- 計算生成影像與真實影像在特徵空間的距離
---
## 7. 實務上的評估建議
若將此 GAN 用於產生 OCR 訓練資料,可以採用以下流程:
1. 生成 10,000 張手寫數字影像。
2. 使用已訓練好的 MNIST 分類器進行分類。
3. 檢查 0~9 每一類的比例。
4. 人工檢視部分生成影像。
5. 將生成資料加入原始 MNIST,重新訓練 OCR 模型。
6. 比較加入 GAN 資料前後的驗證集準確率。
如果加入生成資料後,OCR 模型在驗證集上的準確率提升,且各類生成影像分布合理,才表示 GAN 具備實務效益。
---
## 8. 常見問題與改善方式
### 生成影像過於模糊
可以:
- 增加訓練 epochs
- 使用 DCGAN 架構
- 增加 Generator 和 Discriminator 的層數
- 使用卷積層取代全連接層
### 發生 Mode Collapse
可以:
- 使用 Wasserstein GAN(WGAN)
- 使用 Gradient Penalty
- 降低 Discriminator 的學習速度
- 使用 label smoothing
- 使用 minibatch discrimination
### Loss 不穩定
GAN 的 G Loss 和 D Loss 不一定會像一般分類模型一樣單調下降,應搭配:
- 生成影像視覺化
- 類別分布
- FID/KID
- 下游任務表現
共同評估,而不能只看 Loss。
---
## 9. 本案例重點整理
| 項目 | 設定 |
|---|---|
| 資料集 | MNIST |
| 影像尺寸 | \(1 \times 28 \times 28\) |
| 潛在向量維度 | 100 |
| Generator 輸出函數 | Tanh |
| Discriminator Loss | BCEWithLogitsLoss |
| Optimizer | Adam |
| Learning rate | 0.0002 |
| Batch size | 128 |
| 訓練 epochs | 20 |
| 評估方法 | 影像視覺化、判別器分數、多樣性、下游分類效能 |
這個模型適合作為 GAN 的入門實務案例;若要處理人臉、商品圖片或醫療影像,則應進一步改用卷積式 DCGAN、WGAN-GP 或 StyleGAN 等架構。
相關學習地圖、教學課程
Python 人工智慧