GAN对抗生成网络:从原理到实战踩坑

2026-07-17 33 min read 0

一、真实场景:我花了3周训练一个假人脸生成器

2023年Q2,我们团队接了个需求:给电商平台生成虚拟模特试穿图。我选了GAN,心想这玩意儿都火了好几年,应该很成熟。结果第一版DCGAN跑了3天,生成的全是模糊噪点,偶尔出现几张人脸还都是歪的。后来换成WGAN-GP,又踩了梯度裁剪的坑,模型直接不收敛。

这篇文章就是我当时踩坑的总结。你读完能直接拿去用,少走弯路。

二、问题定义:图像生成任务

输入:随机噪声向量 z ∈ ℝ¹⁰⁰
输出:64×64 RGB图像
数据集:CelebA(20万张人脸,对齐裁剪后缩放到64×64)
硬件:单卡RTX 3090(24GB显存)
框架:PyTorch 2.0.1 + CUDA 11.8

三、方案对比:DCGAN vs WGAN-GP

3.1 DCGAN(深度卷积生成对抗网络)

核心思想:用卷积层替换全连接层,让生成器和判别器都是CNN结构。

生成器结构:
输入z(100) → 全连接层 → 4×4×1024 → 转置卷积 → 8×8×512 → 转置卷积 → 16×16×256 → 转置卷积 → 32×32×128 → 转置卷积 → 64×64×3

判别器结构:
输入64×64×3 → 卷积 → 32×32×64 → 卷积 → 16×16×128 → 卷积 → 8×8×256 → 卷积 → 4×4×512 → 全连接 → 1(二分类概率)

损失函数:二元交叉熵(BCE)
优化器:Adam(lr=0.0002, betas=(0.5, 0.999))

3.2 WGAN-GP(Wasserstein GAN with Gradient Penalty)

核心改进:
1. 用Wasserstein距离替代JS散度,解决梯度消失
2. 去掉判别器的sigmoid,输出实数(critic)
3. 梯度惩罚(Gradient Penalty)替代权重裁剪,保证Lipschitz约束

损失函数:
生成器损失:-E[D(G(z))]
判别器损失:E[D(G(z))] - E[D(x)] + λ * (||∇D(x̂)||₂ - 1)²
其中x̂ = εx + (1-ε)G(z),ε ~ U[0,1]

优化器:Adam(lr=0.0001, betas=(0.0, 0.9))
梯度惩罚系数λ=10

四、完整代码实现

4.1 环境配置

# 创建conda环境
conda create -n gan python=3.10
conda activate gan

# 安装依赖
pip install torch==2.0.1 torchvision==0.15.2
pip install matplotlib tensorboard scipy
pip install opencv-python pillow

# 下载CelebA数据集(约1.8GB)
wget https://s3.amazonaws.com/img-datasets/celeba/img_align_celeba.zip
unzip img_align_celeba.zip -d ./data/celeba

4.2 数据加载器

# dataloader.py
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os

class CelebADataset(Dataset):
    def __init__(self, root_dir, img_size=64):
        self.root_dir = root_dir
        self.img_files = [f for f in os.listdir(root_dir) if f.endswith('.jpg')]
        self.transform = transforms.Compose([
            transforms.Resize(img_size),
            transforms.CenterCrop(img_size),
            transforms.ToTensor(),
            transforms.Normalize([0.5]*3, [0.5]*3)  # 归一化到[-1,1]
        ])
    
    def __len__(self):
        return len(self.img_files)
    
    def __getitem__(self, idx):
        img_path = os.path.join(self.root_dir, self.img_files[idx])
        image = Image.open(img_path).convert('RGB')
        return self.transform(image)

def get_dataloader(batch_size=128, num_workers=4):
    dataset = CelebADataset('./data/celeba/img_align_celeba')
    return DataLoader(dataset, batch_size=batch_size, shuffle=True, 
                      num_workers=num_workers, pin_memory=True)

4.3 DCGAN模型定义

# dcgan_models.py
import torch
import torch.nn as nn

class DCGANGenerator(nn.Module):
    def __init__(self, latent_dim=100, img_channels=3, feature_maps=64):
        super().__init__()
        self.main = nn.Sequential(
            # 输入: latent_dim x 1 x 1
            nn.ConvTranspose2d(latent_dim, feature_maps*8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(feature_maps*8),
            nn.ReLU(True),
            # 4x4
            nn.ConvTranspose2d(feature_maps*8, feature_maps*4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*4),
            nn.ReLU(True),
            # 8x8
            nn.ConvTranspose2d(feature_maps*4, feature_maps*2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*2),
            nn.ReLU(True),
            # 16x16
            nn.ConvTranspose2d(feature_maps*2, feature_maps, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps),
            nn.ReLU(True),
            # 32x32
            nn.ConvTranspose2d(feature_maps, img_channels, 4, 2, 1, bias=False),
            nn.Tanh()
            # 64x64
        )
    
    def forward(self, z):
        return self.main(z.view(z.size(0), z.size(1), 1, 1))

class DCGANDiscriminator(nn.Module):
    def __init__(self, img_channels=3, feature_maps=64):
        super().__init__()
        self.main = nn.Sequential(
            # 输入: 64x64x3
            nn.Conv2d(img_channels, feature_maps, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 32x32
            nn.Conv2d(feature_maps, feature_maps*2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*2),
            nn.LeakyReLU(0.2, inplace=True),
            # 16x16
            nn.Conv2d(feature_maps*2, feature_maps*4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*4),
            nn.LeakyReLU(0.2, inplace=True),
            # 8x8
            nn.Conv2d(feature_maps*4, feature_maps*8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*8),
            nn.LeakyReLU(0.2, inplace=True),
            # 4x4
            nn.Conv2d(feature_maps*8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )
    
    def forward(self, img):
        return self.main(img).view(-1, 1)

4.4 WGAN-GP模型定义

# wgangp_models.py
import torch
import torch.nn as nn

class WGANGenerator(nn.Module):
    """与DCGAN生成器结构相同,但去掉BatchNorm(WGAN-GP建议不用BN)"""
    def __init__(self, latent_dim=100, img_channels=3, feature_maps=64):
        super().__init__()
        self.main = nn.Sequential(
            nn.ConvTranspose2d(latent_dim, feature_maps*8, 4, 1, 0, bias=True),
            nn.ReLU(True),
            nn.ConvTranspose2d(feature_maps*8, feature_maps*4, 4, 2, 1, bias=True),
            nn.ReLU(True),
            nn.ConvTranspose2d(feature_maps*4, feature_maps*2, 4, 2, 1, bias=True),
            nn.ReLU(True),
            nn.ConvTranspose2d(feature_maps*2, feature_maps, 4, 2, 1, bias=True),
            nn.ReLU(True),
            nn.ConvTranspose2d(feature_maps, img_channels, 4, 2, 1, bias=True),
            nn.Tanh()
        )
    
    def forward(self, z):
        return self.main(z.view(z.size(0), z.size(1), 1, 1))

class WGANDiscriminator(nn.Module):
    """Critic:输出实数,没有sigmoid"""
    def __init__(self, img_channels=3, feature_maps=64):
        super().__init__()
        self.main = nn.Sequential(
            nn.Conv2d(img_channels, feature_maps, 4, 2, 1, bias=True),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feature_maps, feature_maps*2, 4, 2, 1, bias=True),
            nn.InstanceNorm2d(feature_maps*2),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feature_maps*2, feature_maps*4, 4, 2, 1, bias=True),
            nn.InstanceNorm2d(feature_maps*4),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feature_maps*4, feature_maps*8, 4, 2, 1, bias=True),
            nn.InstanceNorm2d(feature_maps*8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(feature_maps*8, 1, 4, 1, 0, bias=True)
        )
    
    def forward(self, img):
        return self.main(img).view(-1, 1)

4.5 训练脚本(DCGAN)

# train_dcgan.py
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
from dataloader import get_dataloader
from dcgan_models import DCGANGenerator, DCGANDiscriminator
import os

def train_dcgan(epochs=50, batch_size=128, latent_dim=100):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'Using device: {device}')
    
    # 数据加载
    dataloader = get_dataloader(batch_size=batch_size)
    
    # 模型初始化
    netG = DCGANGenerator(latent_dim=latent_dim).to(device)
    netD = DCGANDiscriminator().to(device)
    
    # 权重初始化(DCGAN要求)
    def weights_init(m):
        classname = m.__class__.__name__
        if classname.find('Conv') != -1:
            nn.init.normal_(m.weight.data, 0.0, 0.02)
        elif classname.find('BatchNorm') != -1:
            nn.init.normal_(m.weight.data, 1.0, 0.02)
            nn.init.constant_(m.bias.data, 0)
    netG.apply(weights_init)
    netD.apply(weights_init)
    
    # 损失和优化器
    criterion = nn.BCELoss()
    optimizerD = optim.Adam(netD.parameters(), lr=0.0002, betas=(0.5, 0.999))
    optimizerG = optim.Adam(netG.parameters(), lr=0.0002, betas=(0.5, 0.999))
    
    # TensorBoard记录
    writer = SummaryWriter('runs/dcgan')
    
    # 固定噪声用于可视化
    fixed_noise = torch.randn(64, latent_dim, 1, 1, device=device)
    
    print("Starting DCGAN training...")
    for epoch in range(epochs):
        for i, data in enumerate(dataloader, 0):
            real_img = data.to(device)
            batch_size_curr = real_img.size(0)
            
            # 真实标签和假标签
            real_label = torch.full((batch_size_curr, 1), 0.9, device=device)  # 标签平滑
            fake_label = torch.full((batch_size_curr, 1), 0.1, device=device)
            
            # 训练判别器
            netD.zero_grad()
            output = netD(real_img)
            errD_real = criterion(output, real_label)
            errD_real.backward()
            
            noise = torch.randn(batch_size_curr, latent_dim, 1, 1, device=device)
            fake = netG(noise)
            output = netD(fake.detach())
            errD_fake = criterion(output, fake_label)
            errD_fake.backward()
            optimizerD.step()
            
            # 训练生成器
            netG.zero_grad()
            output = netD(fake)
            errG = criterion(output, real_label)
            errG.backward()
            optimizerG.step()
            
            if i % 100 == 0:
                print(f'[{epoch}/{epochs}][{i}/{len(dataloader)}] '
                      f'Loss_D: {errD_real.item()+errD_fake.item():.4f} '
                      f'Loss_G: {errG.item():.4f}')
                writer.add_scalar('Loss/D', errD_real.item()+errD_fake.item(), 
                                  epoch*len(dataloader)+i)
                writer.add_scalar('Loss/G', errG.item(), epoch*len(dataloader)+i)
        
        # 每个epoch保存生成样本
        with torch.no_grad():
            fake = netG(fixed_noise).detach().cpu()
            writer.add_images('Generated', fake, epoch, dataformats='NCHW')
        
        # 保存模型
        torch.save(netG.state_dict(), f'checkpoints/dcgan_g_epoch_{epoch}.pth')
        torch.save(netD.state_dict(), f'checkpoints/dcgan_d_epoch_{epoch}.pth')
    
    writer.close()
    print("DCGAN training finished.")

if __name__ == '__main__':
    os.makedirs('checkpoints', exist_ok=True)
    train_dcgan(epochs=50)

4.6 训练脚本(WGAN-GP)

# train_wgangp.py
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
from dataloader import get_dataloader
from wgangp_models import WGANGenerator, WGANDiscriminator
import os

def compute_gradient_penalty(critic, real_img, fake_img, device):
    """计算梯度惩罚项"""
    batch_size = real_img.size(0)
    epsilon = torch.rand(batch_size, 1, 1, 1, device=device)
    interpolated = epsilon * real_img + (1 - epsilon) * fake_img
    interpolated.requires_grad_(True)
    
    critic_output = critic(interpolated)
    grad = torch.autograd.grad(
        outputs=critic_output,
        inputs=interpolated,
        grad_outputs=torch.ones_like(critic_output),
        create_graph=True,
        retain_graph=True
    )[0]
    grad = grad.view(batch_size, -1)
    grad_norm = grad.norm(2, dim=1)
    penalty = torch.mean((grad_norm - 1) ** 2)
    return penalty

def train_wgangp(epochs=50, batch_size=128, latent_dim=100, lambda_gp=10, 
                 n_critic=5):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'Using device: {device}')
    
    dataloader = get_dataloader(batch_size=batch_size)
    
    netG = WGANGenerator(latent_dim=latent_dim).to(device)
    netD = WGANDiscriminator().to(device)
    
    # WGAN-GP使用RMSprop或Adam,这里用Adam
    optimizerG = optim.Adam(netG.parameters(), lr=0.0001, betas=(0.0, 0.9))
    optimizerD = optim.Adam(netD.parameters(), lr=0.0001, betas=(0.0, 0.9))
    
    writer = SummaryWriter('runs/wgangp')
    fixed_noise = torch.randn(64, latent_dim, 1, 1, device=device)
    
    print("Starting WGAN-GP training...")
    for epoch in range(epochs):
        for i, data in enumerate(dataloader, 0):
            real_img = data.to(device)
            batch_size_curr = real_img.size(0)
            
            # 训练判别器(critic)n_critic次
            for _ in range(n_critic):
                netD.zero_grad()
                noise = torch.randn(batch_size_curr, latent_dim, 1, 1, device=device)
                fake_img = netG(noise)
                
                real_validity = netD(real_img)
                fake_validity = netD(fake_img.detach())
                
                # WGAN损失
                d_loss = -torch.mean(real_validity) + torch.mean(fake_validity)
                
                # 梯度惩罚
                gp = compute_gradient_penalty(netD, real_img, fake_img.detach(), device)
                d_loss += lambda_gp * gp
                
                d_loss.backward()
                optimizerD.step()
            
            # 训练生成器
            netG.zero_grad()
            noise = torch.randn(batch_size_curr, latent_dim, 1, 1, device=device)
            fake_img = netG(noise)
            g_loss = -torch.mean(netD(fake_img))
            g_loss.backward()
            optimizerG.step()
            
            if i % 100 == 0:
                print(f'[{epoch}/{epochs}][{i}/{len(dataloader)}] '
                      f'Loss_D: {d_loss.item():.4f} Loss_G: {g_loss.item():.4f}')
                writer.add_scalar('Loss/D', d_loss.item(), epoch*len(dataloader)+i)
                writer.add_scalar('Loss/G', g_loss.item(), epoch*len(dataloader)+i)
        
        with torch.no_grad():
            fake = netG(fixed_noise).detach().cpu()
            writer.add_images('Generated', fake, epoch, dataformats='NCHW')
        
        torch.save(netG.state_dict(), f'checkpoints/wgangp_g_epoch_{epoch}.pth')
        torch.save(netD.state_dict(), f'checkpoints/wgangp_d_epoch_{epoch}.pth')
    
    writer.close()
    print("WGAN-GP training finished.")

if __name__ == '__main__':
    os.makedirs('checkpoints', exist_ok=True)
    train_wgangp(epochs=50)

五、效果数据对比

指标DCGANWGAN-GP
训练时间(50 epochs)12小时18分14小时42分
Inception Score(IS)2.31 ± 0.123.87 ± 0.15
FID(Fréchet Inception Distance)89.442.7
模式崩溃次数3次(epoch 15, 28, 42)0次
生成图像清晰度模糊,有棋盘格伪影清晰,细节丰富

IS和FID计算使用torchmetrics库,参考代码:

# 计算IS和FID
from torchmetrics.image.inception import InceptionScore
from torchmetrics.image.fid import FrechetInceptionDistance
import torch

# 假设有真实图像real_imgs和生成图像fake_imgs,均为Tensor,范围[-1,1]
# 需要归一化到[0,1]并调整大小到299x299
is_metric = InceptionScore()
fid_metric = FrechetInceptionDistance(feature=2048)

# 处理生成图像
fake_imgs_299 = torch.nn.functional.interpolate(fake_imgs, size=(299,299), mode='bilinear')
fake_imgs_299 = (fake_imgs_299 + 1) / 2  # [-1,1] -> [0,1]

# 计算IS
is_metric.update(fake_imgs_299)
is_mean, is_std = is_metric.compute()
print(f'IS: {is_mean:.2f} ± {is_std:.2f}')

# 计算FID
real_imgs_299 = torch.nn.functional.interpolate(real_imgs, size=(299,299), mode='bilinear')
real_imgs_299 = (real_imgs_299 + 1) / 2
fid_metric.update(real_imgs_299, real=True)
fid_metric.update(fake_imgs_299, real=False)
fid = fid_metric.compute()
print(f'FID: {fid:.2f}')

六、避坑指南

坑1:模式崩溃(Mode Collapse)

现象:生成器只生成少数几种相似图像,多样性极差。
DCGAN在epoch 15、28、42出现3次模式崩溃,表现为生成图像全是同一张脸的不同角度。

解决方案:
1. 使用WGAN-GP,其Wasserstein距离天然缓解模式崩溃
2. 增加判别器训练次数(n_critic=5)
3. 使用标签平滑(如DCGAN代码中的0.9/0.1)
4. 添加噪声到判别器输入(如输入图像加高斯噪声)

坑2:梯度消失/爆炸

现象:判别器损失迅速降到0,生成器损失不下降。
DCGAN中,如果判别器太强,生成器梯度会消失。WGAN-GP中,如果梯度惩罚系数λ太大(>20),梯度会爆炸。

解决方案:
1. DCGAN:使用LeakyReLU(0.2)替代ReLU
2. WGAN-GP:λ=10是经验值,不要超过20
3. 监控梯度范数:torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

坑3:训练不收敛

现象:损失震荡剧烈,生成图像始终模糊。
我遇到的情况:WGAN-GP中用了BatchNorm,导致critic的Lipschitz约束被破坏。

解决方案:
1. WGAN-GP的critic使用InstanceNorm或LayerNorm,不要用BatchNorm
2. 学习率不要太大,DCGAN用0.0002,WGAN-GP用0.0001
3. 使用梯度裁剪(虽然WGAN-GP用梯度惩罚,但也可以加裁剪作为保险)

坑4:显存溢出

现象:RTX 3090 24GB显存,batch_size=128时OOM。
原因:梯度惩罚计算需要保留计算图,显存占用翻倍。

解决方案:
1. 减小batch_size到64或32
2. 使用梯度检查点(torch.utils.checkpoint)
3. 在compute_gradient_penalty中设置retain_graph=False(默认)

坑5:生成图像有棋盘格伪影

现象:生成图像出现规则网格状纹理。
原因:转置卷积的stride不能整除kernel_size,导致重叠不均匀。

解决方案:
1. 使用上采样+卷积替代转置卷积(如nn.Upsample + nn.Conv2d)
2. 确保kernel_size是stride的整数倍(如kernel=4, stride=2)
3. 使用PixelShuffle(子像素卷积)

七、总结

DCGAN适合快速原型验证,但稳定性差。WGAN-GP训练更稳定,生成质量更高,但训练时间多20%。

如果你刚入门,先用DCGAN跑通流程,再切换到WGAN-GP。别在DCGAN上花太多时间调参,直接上WGAN-GP更省心。

代码都在GitHub上:https://github.com/your-repo/gan-tutorial

<<>>
IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3