แผนการเรียนรู้
/

Generative AI for Image

สัปดาห์ที่ 14

GAN และ Diffusion Models

1. GAN: Generator vs Discriminator

2. DCGAN: Deep Convolutional GAN

3. Training GANs

4. Diffusion Models

5. Stable Diffusion

Learning Roadmap

1
GAN Basics
Generator, Discriminator, minimax
2
DCGAN
Convolutional GAN architecture
3
Diffusion
Denoising, sampling, Stable Diffusion
4
Practical
Generate faces, digits, art

GAN (Generative Adversarial Network)

- Generator (G): สร้างภาพปลอมจาก noise

- Discriminator (D): แยกภาพจริง vs ปลอม

- แข่งกัน: G สร้างให้เหมือนจริง, D จับให้ได้

- ผลลัพธ์: G สร้างภาพเหมือนจริงมาก

[ตัวอย่าง] G สร้างหน้าคนปลอม, D แยกว่าปลอมหรือจริง

# GAN: two networks competing
# z (noise) -> [Generator] -> fake_image
# real_image + fake_image -> [Discriminator] -> real/fake

# Minimax game:
# min_G max_D V(D,G) =
#   E[log D(x)] + E[log(1 - D(G(z)))]

# Training:
# 1. Train D: maximize log(D(x)) + log(1-D(G(z)))
# 2. Train G: maximize log(D(G(z)))

# At equilibrium:
# D(x) = 0.5 (can't tell real from fake)
# G generates perfect images

GAN Architecture

import torch
import torch.nn as nn

class Generator(nn.Module):
    def __init__(self, latent_dim=100, img_dim=784):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(latent_dim, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, img_dim),
            nn.Tanh()  # output [-1, 1]
        )
    def forward(self, z):
        return self.net(z)

class Discriminator(nn.Module):
    def __init__(self, img_dim=784):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(img_dim, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )
    def forward(self, x):
        return self.net(x)

# Create models
G = Generator(latent_dim=100, img_dim=784)
D = Discriminator(img_dim=784)

- Generator: noise -> image (Tanh output)

- Discriminator: image -> real/fake (Sigmoid)

- LeakyReLU: ช่วย gradient flow

- _size ค่อยๆ เพิ่มขึ้น: 100 -> 256 -> 512 -> 784

[สำคัญ] ใช้ LeakyReLU แทน ReLU ใน GANs

Training GANs

# Training loop
criterion = nn.BCELoss()
opt_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))
opt_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))

for epoch in range(100):
    for real_imgs, _ in dataloader:
        batch_size = real_imgs.size(0)
        real_imgs = real_imgs.view(batch_size, -1)

        # Labels
        real_labels = torch.ones(batch_size, 1)
        fake_labels = torch.zeros(batch_size, 1)

        # Train Discriminator
        z = torch.randn(batch_size, 100)
        fake_imgs = G(z).detach()

        real_loss = criterion(D(real_imgs), real_labels)
        fake_loss = criterion(D(fake_imgs), fake_labels)
        d_loss = (real_loss + fake_loss) / 2

        opt_D.zero_grad()
        d_loss.backward()
        opt_D.step()

        # Train Generator
        z = torch.randn(batch_size, 100)
        fake_imgs = G(z)
        g_loss = criterion(D(fake_imgs), real_labels)

        opt_G.zero_grad()
        g_loss.backward()
        opt_G.step()

- Step 1: Train D on real + fake

- Step 2: Train G (fool D)

- .detach(): ไม่ update G ตอน train D

- betas=(0.5, 0.999): สำหรับ GANs

[ปัญหา] Mode collapse = G สร้างภาพเดิมซ้ำๆ

DCGAN

class DCGenerator(nn.Module):
    def __init__(self, latent_dim=100):
        super().__init__()
        self.net = nn.Sequential(
            # z: (batch, 100, 1, 1)
            nn.ConvTranspose2d(100, 512, 4, 1, 0),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            # -> (batch, 512, 4, 4)

            nn.ConvTranspose2d(512, 256, 4, 2, 1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            # -> (batch, 256, 8, 8)

            nn.ConvTranspose2d(256, 128, 4, 2, 1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            # -> (batch, 128, 16, 16)

            nn.ConvTranspose2d(128, 3, 4, 2, 1),
            nn.Tanh()
            # -> (batch, 3, 32, 32)
        )
    def forward(self, z):
        return self.net(z.view(-1, 100, 1, 1))

- DCGAN: Deep Convolutional GAN

- ใช้ ConvTranspose2d (upscaling)

- BatchNorm: ช่วยให้ training เสถียร

- no fully-connected layers

[สำคัญ] BatchNorm + ReLU/Tanh = DCGAN guidelines

DCGAN Discriminator

class DCDiscriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            # (batch, 3, 32, 32)
            nn.Conv2d(3, 64, 4, 2, 1),
            nn.LeakyReLU(0.2),
            # -> (batch, 64, 16, 16)

            nn.Conv2d(64, 128, 4, 2, 1),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2),
            # -> (batch, 128, 8, 8)

            nn.Conv2d(128, 256, 4, 2, 1),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2),
            # -> (batch, 256, 4, 4)

            nn.Conv2d(256, 1, 4, 1, 0),
            nn.Sigmoid()
            # -> (batch, 1, 1, 1)
        )
    def forward(self, x):
        return self.net(x).view(-1, 1)

# Usage
D = DCDiscriminator()
z = torch.randn(8, 100)
fake_imgs = G(z)
pred = D(fake_imgs)  # (8, 1) - real/fake score

- Discriminator: image -> real/fake score

- ใช้ Conv2d (downscaling)

- LeakyReLU(0.2): ช่วย gradient flow

- Output: scalar (0=fake, 1=real)

[สำคัญ] ไม่ใช้ BatchNorm ใน Discriminator ของ DCGAN

GAN Challenges

- Mode collapse: G สร้างภาพเดิมซ้ำๆ

- Training instability: D ชนะ G หรือ vice versa

- No convergence: training ไม่ converge

- Evaluation: วัดคุณภาพภาพยาก

[เคล็ดลับ] ใช้ Wasserstein GAN (WGAN) สำหรับ training ที่เสถียรกว่า

# Common issues:

# 1. Mode collapse
# G generates same image every time
# Fix: add noise, use WGAN

# 2. Training instability
# D loss -> 0 (D too strong)
# G loss -> infinity
# Fix: lower lr for D, train D less

# 3. No convergence
# Loss oscillates, never stabilizes
# Fix: use BatchNorm, adjust architecture

# Evaluation metrics:
# - FID (Fréchet Inception Distance)
#   lower = better
# - IS (Inception Score)
#   higher = better

# Tips:
# - Use Adam optimizer
# - Train D more than G
# - Add noise to labels

Diffusion Models

- Forward process: ค่อยๆ เพิ่ม noise จนเป็น Gaussian

- Reverse process: เรียนรู้ undo noise กลับเป็นภาพ

- training เสถียรกว่า GANs

- คุณภาพสูงมาก (Stable Diffusion, DALL-E)

[ตัวอย่าง] ภาพสุนัข -> เพิ่ม noise -> noise -> เรียนรู้ undo -> สร้างสุนัขใหม่

# Diffusion: two processes

# Forward: add noise gradually
# x_0 -> x_1 -> x_2 -> ... -> x_T (pure noise)
# q(x_t | x_{t-1}) = N(x_t; sqrt(1-beta_t)*x_{t-1}, beta_t*I)

# Reverse: learn to denoise
# x_T -> x_{T-1} -> ... -> x_1 -> x_0 (clean image)
# p(x_{t-1} | x_t) = N(x_{t-1}; mu_theta(x_t, t), sigma_t*I)

# Training:
# 1. Take image x_0
# 2. Sample random timestep t
# 3. Add noise: x_t = sqrt(alpha_t)*x_0 + sqrt(1-alpha_t)*eps
# 4. Predict noise: eps_theta(x_t, t)
# 5. Loss = MSE(eps, eps_theta(x_t, t))

U-Net for Diffusion

class UNet(nn.Module):
    def __init__(self, in_channels=3, time_dim=256):
        super().__init__()
        # Encoder
        self.enc1 = self.conv_block(in_channels, 64)
        self.enc2 = self.conv_block(64, 128)
        self.enc3 = self.conv_block(128, 256)

        # Bottleneck
        self.bottleneck = self.conv_block(256, 512)

        # Decoder
        self.dec3 = self.conv_block(512 + 256, 256)
        self.dec2 = self.conv_block(256 + 128, 128)
        self.dec1 = self.conv_block(128 + 64, 64)

        # Time embedding
        self.time_mlp = nn.Sequential(
            nn.Linear(time_dim, 512),
            nn.ReLU(),
            nn.Linear(512, 512)
        )

    def conv_block(self, in_ch, out_ch):
        return nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, padding=1),
            nn.GroupNorm(8, out_ch),
            nn.GELU()
        )

    def forward(self, x, t):
        # x: (batch, 3, H, W)
        # t: (batch, time_dim)
        t_emb = self.time_mlp(t)
        # ... encoder, bottleneck, decoder
        return noise_pred

- U-Net: encoder-decoder with skip connections

- Time embedding: บอกว่า noise level เท่าไหร่

- GroupNorm: ดีกว่า BatchNorm สำหรับ diffusion

- GELU: activation ที่นิยมใน transformers

[สำคัญ] U-Net ทำนาย noise ที่ต้องลบออก

Training Diffusion Model

def train_diffusion(model, dataloader, epochs=100):
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

    for epoch in range(epochs):
        for images, _ in dataloader:
            batch_size = images.size(0)

            # Sample random timestep
            t = torch.randint(0, T, (batch_size,))

            # Add noise
            noise = torch.randn_like(images)
            alpha_t = alphas[t].view(-1, 1, 1, 1)
            alpha_bar_t = alphas_cumprod[t].view(-1, 1, 1, 1)

            noisy_images = torch.sqrt(alpha_bar_t) * images + \
                          torch.sqrt(1 - alpha_bar_t) * noise

            # Predict noise
            noise_pred = model(noisy_images, t)

            # Loss
            loss = F.mse_loss(noise_pred, noise)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

# alphas, alphas_cumprod = schedule for noise

- Sample random timestep t

- Add noise: \(x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon\)

- Predict noise: \(\epsilon_\theta(x_t, t)\)

- Loss = MSE(predicted noise, actual noise)

[สำคัญ] Simple loss function = MSE เท่านั้น!

Sampling (Generation)

@torch.no_grad()
def sample(model, num_samples, image_size):
    # Start from pure noise
    x = torch.randn(num_samples, 3, image_size, image_size)

    for t in reversed(range(T)):
        # Predict noise
        noise_pred = model(x, torch.tensor([t]))

        # Denoise
        alpha_t = alphas[t]
        alpha_bar_t = alphas_cumprod[t]
        beta_t = betas[t]

        # Remove noise
        x = (1 / torch.sqrt(alpha_t)) * \
            (x - (beta_t / torch.sqrt(1 - alpha_bar_t)) * noise_pred)

        # Add noise (except at t=0)
        if t > 0:
            noise = torch.randn_like(x)
            x = x + torch.sqrt(beta_t) * noise

    return x

# Generate 16 images
samples = sample(model, 16, 32)
save_image(samples, 'generated.png', nrow=4)

- เริ่มจาก noise ล้วน

- ค่อยๆ ลบ noise ทีละ step

- reversed(range(T)): จาก T ถึง 0

- เพิ่ม noise เล็กน้อยเพื่อ diversity

[สำคัญ] Sampling ช้ากว่า GAN (หลายร้อย steps)

Stable Diffusion

- Latent Diffusion: ทำงานใน latent space (ไม่ใช่ pixel space)

- VAE: บีบอัดภาพเป็น latent representation

- Text Encoder: CLIP สำหรับ text condition

- เร็วกว่า diffusion แบบเดิมมาก

[Architectures] VAE + U-Net + CLIP = Stable Diffusion

from diffusers import StableDiffusionPipeline

# Load Stable Diffusion
pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16
)
pipe = pipe.to("cuda")

# Generate from text prompt
prompt = "a photo of an astronaut riding a horse"
image = pipe(prompt, num_inference_steps=50).images[0]
image.save("astronaut.png")

# Steps:
# 1. Text -> CLIP -> text embeddings
# 2. Random noise in latent space
# 3. U-Net denoise with text condition
# 4. VAE decode latent -> image

# Parameters:
# - num_inference_steps: 20-50 (more = better)
# - guidance_scale: 7.5 (higher = follow prompt more)
# - seed: random seed for reproducibility

Diffusion vs GAN

- GAN: เร็ว, training ยาก, mode collapse

- Diffusion: ช้า, training ง่าย, คุณภาพสูง

- GAN: inference step เดียว

- Diffusion: หลายร้อย steps

[สรุป] GAN = เร็วแต่ยาก / Diffusion = ช้าแต่ง่ายและดี

# Comparison:
#                 GAN        Diffusion
# Speed:          Fast       Slow
# Training:       Hard       Easy
# Quality:        Good       Excellent
# Diversity:      Low        High
# Mode collapse:  Yes        No
# Evaluation:     FID        FID/IS

# Use cases:
# GAN:
# - Real-time generation
# - Style transfer
# - Super-resolution

# Diffusion:
# - Text-to-image
# - Image editing
# - Inpainting
# - Video generation

ตัวอย่าง: Generate MNIST Digits

from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# Load MNIST
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.5], [0.5])
])
dataset = datasets.MNIST('./data', train=True,
                         download=True, transform=transform)
loader = DataLoader(dataset, batch_size=128, shuffle=True)

# DCGAN for MNIST
G = DCGenerator(latent_dim=100)
D = DCDiscriminator()
criterion = nn.BCELoss()
opt_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
opt_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))

# Train
for epoch in range(50):
    for real_imgs, _ in loader:
        batch_size = real_imgs.size(0)
        real_imgs = real_imgs.view(batch_size, -1)

        # Train D
        z = torch.randn(batch_size, 100)
        fake_imgs = G(z).detach()
        d_loss = criterion(D(real_imgs), torch.ones(batch_size, 1)) + \
                 criterion(D(fake_imgs), torch.zeros(batch_size, 1))
        opt_D.zero_grad()
        d_loss.backward()
        opt_D.step()

        # Train G
        z = torch.randn(batch_size, 100)
        fake_imgs = G(z)
        g_loss = criterion(D(fake_imgs), torch.ones(batch_size, 1))
        opt_G.zero_grad()
        g_loss.backward()
        opt_G.step()

- MNIST: 28x28 grayscale digits

- DCGAN: simple but effective

- 50 epochs พอสำหรับ MNIST

- ผลลัพธ์: ภาพ digit ปลอมที่เหมือนจริง

[เคล็ดลับ] เริ่มจาก MNIST ก่อนไป CIFAR-10/ CelebA

Django Dashboard + SSE

uv add django torch torchvision

uv run django-admin startproject wk14 .
uv run manage.py startapp dashboard

# dashboard/views.py
import json, torch
from django.http import StreamingHttpResponse

def train(request, model_type):
    def event_stream():
        if model_type == 'gan':
            G = Generator()
            D = Discriminator()
        elif model_type == 'dcgan':
            G = DCGenerator()
            D = DCDiscriminator()

        criterion = nn.BCELoss()
        opt_G = torch.optim.Adam(G.parameters(), lr=0.0002)
        opt_D = torch.optim.Adam(D.parameters(), lr=0.0002)

        for epoch in range(50):
            # Training loop
            # ...

            # Generate sample images
            with torch.no_grad():
                z = torch.randn(16, 100)
                fake_imgs = G(z)

            yield f"data: {json.dumps({'epoch': epoch, 'd_loss': d_loss.item(), 'g_loss': g_loss.item(), 'images': fake_imgs.tolist()})}\n\n"

    return StreamingHttpResponse(event_stream(), content_type='text/event-stream')

- Django: web framework สำหรับ backend

- SSE: real-time training updates

- แสดงผล loss และ generate images

- เลือก model: GAN, DCGAN

[โครงสร้าง] manage.py -> settings.py -> urls.py -> views.py -> templates/

SSE Training Stream

from django.http import StreamingHttpResponse
import json, time

def train_stream(request, model_type):
    def event_stream():
        G, D = build_models(model_type)
        criterion = nn.BCELoss()
        opt_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
        opt_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))

        for epoch in range(100):
            # Simplified training
            batch_size = 64

            # Train D
            z = torch.randn(batch_size, 100)
            fake_imgs = G(z).detach()
            d_loss = criterion(D(fake_imgs), torch.zeros(batch_size, 1))

            opt_D.zero_grad()
            d_loss.backward()
            opt_D.step()

            # Train G
            z = torch.randn(batch_size, 100)
            fake_imgs = G(z)
            g_loss = criterion(D(fake_imgs), torch.ones(batch_size, 1))

            opt_G.zero_grad()
            g_loss.backward()
            opt_G.step()

            # Generate samples
            with torch.no_grad():
                z = torch.randn(4, 100)
                samples = G(z).view(-1, 1, 28, 28)
                samples = (samples + 1) / 2  # denormalize

            yield f"data: {json.dumps({\n"
                  f"  'epoch': {epoch},\n"
                  f"  'd_loss': {d_loss.item():.4f},\n"
                  f"  'g_loss': {g_loss.item():.4f},\n"
                  f"  'samples': samples.numpy().tolist()\n"
                  f"})}\n\n"
            time.sleep(0.05)

    resp = StreamingHttpResponse(event_stream(),
        content_type='text/event-stream')
    resp['Cache-Control'] = 'no-cache'
    return resp

- StreamingHttpResponse: ส่ง data ทีละ epoch

- ส่งทั้ง d_loss, g_loss, และ samples

- Generate images ทุก epoch

- time.sleep(0.05): ให้ frontend อ่านทัน

[สำคัญ] SSE = one-way (server -> client), real-time updates

Frontend: GAN Dashboard

<!-- dashboard/templates/index.html -->
<div x-data="{ model: 'gan', training: false }">
  <h1>GAN Training Dashboard</h1>

  <select x-model="model">
    <option value="gan">Vanilla GAN</option>
    <option value="dcgan">DCGAN</option>
  </select>

  <button @click="startTrain()">
    Start Training
  </button>

  <div>
    <p>Epoch: <span x-text="epoch"></span></p>
    <p>D Loss: <span x-text="dLoss"></span></p>
    <p>G Loss: <span x-text="gLoss"></span></p>
  </div>

  <!-- Generated images grid -->
  <div class="grid grid-cols-4 gap-2">
    <template x-for="img in samples">
      <img :src="img" class="w-full" />
    </template>
  </div>

  <canvas id="lossChart" width="420" height="200"></canvas>
</div>

<script>
function startTrain() {
  const es = new EventSource('/train/' + model);
  es.onmessage = function(e) {
    const m = JSON.parse(e.data);
    epoch = m.epoch;
    dLoss = m.d_loss.toFixed(4);
    gLoss = m.g_loss.toFixed(4);
    samples = m.samples;
    drawLossChart(m.epoch, m.d_loss, m.g_loss);
  };
}
</script>

- ใช้ <select> เลือก GAN/DCGAN

- แสดง epoch, d_loss, g_loss แบบ live

- แสดง generated images grid

- ใช้ Canvas วาด loss curves

[เคล็ดลับ] ใช้ Alpine.js สำหรับ reactive UI

Part 5: เปรียบเทียบ Models

Vanilla GAN

  • - Simple architecture
  • - Training ยาก
  • - Mode collapse
  • - เร็ว

DCGAN

  • - Convolutional layers
  • - Training ง่ายกว่า
  • - คุณภาพดี
  • - นิยมใช้

Diffusion

  • - Training ง่ายมาก
  • - คุณภาพสูงสุด
  • - ช้า
  • - State-of-the-art

Summary + Homework

Key Takeaways

  • - GAN = Generator vs Discriminator
  • - DCGAN = Convolutional GAN (ดีกว่า)
  • - Diffusion = ค่อยๆ ลบ noise
  • - Stable Diffusion = Latent Diffusion + CLIP
  • - GAN เร็ว / Diffusion ดี
  • - ใช้ Hugging Face สำหรับ pre-trained models

Homework

สร้าง DCGAN สำหรับ MNIST:

  • 1. ใช้ MNIST dataset
  • 2. DCGAN: ConvTranspose2d + Conv2d
  • 3. Train 50 epochs
  • 4. แสดง generated digits 16 ภาพ
  • 5. Plot d_loss และ g_loss
  • 6. บันทึก model ด้วย state_dict()