GAN自2014年诞生以来,衍生出非常多的变体,可以从不同维度进行分类:
-
按网络结构分:
-
基础GAN:使用多层感知器(MLP)作为生成器和判别器。
-
DCGAN:将卷积神经网络(CNN)引入GAN,是图像生成领域的重要里程碑。
-
条件GAN(CGAN):在生成器和判别器中都加入条件信息(如类别标签),从而控制生成数据的类型。
-
循环一致性GAN(CycleGAN):能在无配对数据的情况下,实现两个不同图像域之间的转换。
-
自注意力GAN(SAGAN):在模型中加入自注意力机制,擅长捕捉图像中的长距离依赖关系,生成更精细的全局结构。
-
-
按损失函数/训练方式分:
-
WGAN / WGAN-GP:使用Wasserstein距离作为损失函数,解决了传统GAN训练不稳定、模式崩溃等问题。
-
LSGAN:使用最小二乘损失函数,使得训练更稳定,生成的图像质量更高。
-
EBGAN:将判别器视为一个能量函数,为训练提供了新的思路。
-
-
按应用领域分:
-
超分辨率(SRGAN, ESRGAN):用于提升图像分辨率。
-
图像合成(StyleGAN):以能生成高质量、高分辨率且细节可控的图像而闻名。
-
视频生成(VGAN, MoCoGAN):用于生成视频序列。
-
非常重要的俩个gan网络的变体:
1. DCGAN (Deep Convolutional GAN)
DCGAN是GAN发展史上的一个里程碑,它将深度学习中的卷积神经网络(CNN) 成功引入GAN框架。
核心特点 :用卷积层 (Convolutional)构建判别器,用转置卷积层(Transposed Convolutional)构建生成器。这种设计使得模型能高效地处理图像数据,并生成质量较高的图片。
主要用途 :作为图像生成的基础架构,被广泛应用,也常用于数据增强,解决分类任务中的样本不平衡问题。
局限性:训练过程可能仍不稳定。
2. AEGAN (AutoEncoder-based GAN)
AEGAN(基于自编码器的生成对抗网络)。
核心特点 :AEGAN的核心创新在于,它使用一个自编码器(Autoencoder) 作为GAN的生成器。
主要用途 :AEGAN的一个典型应用是生成"不可感知"的图像(如人脸),用于保护个人隐私,防止被商用人脸识别系统滥用。
除了以上两种,还有一些GAN变体在分类任务上表现突出:
辅助分类器GAN(AC-GAN) :它的判别器除了判断真伪,还会预测图像的类别标签 ,使其特别适合同时进行图像生成和分类的任务。
半监督GAN(SGAN) :通过结合少量有标签和大量无标签数据进行训练,在半监督学习场景下的分类任务中表现出色。
InfoGAN :能够无监督地学习到数据中可解释的、有意义的特征表示,这些特征可以用于后续的分类任务。
综合王者:StyleGAN 系列
StyleGAN(及其升级版StyleGAN2、StyleGAN2-ADA)是目前最主流的选择,它本身是一个高质量的人脸生成器,但研究者们发现可以通过操控其内部的"潜在空间"(Latent Space)来精确控制生成人脸的各种属性。
核心原理 :通过找到与"年龄"、"性别"等属性对应的特定"方向"(例如一个
age.npy文件),在StyleGAN的潜在空间里沿着这个方向移动,就能实现对该属性的连续、精细控制。工具与应用 :市面上很多基于StyleGAN的人脸编辑应用和开源项目,例如PaddleGAN、
gan-face-editor以及seeprettyface系列,都支持年龄和性别的编辑。
专精模型:条件GAN (cGAN) 及其变体
这类模型在设计时就针对特定属性进行了优化,能实现更可控的编辑。
Age-cGAN (Age Conditional GAN) :专注于人脸老化/年轻化 的模型。它通过在生成器中输入明确的年龄标签 (如
y=35)来控制输出人脸的年龄,目标是让生成的人脸在年龄变化后,依然能保持本人的身份特征(Identity-preserving)。IC-DGAN:一个较新的框架,结合了潜在扩散模型,能进行包括年龄、性别、表情在内的多属性、高分辨率的人脸编辑。
CBEGAN (Conditional BEGAN):条件边界均衡GAN,是BEGAN的扩展,明确支持对年龄、性别等5种人脸属性进行条件生成和编辑。
其他值得关注的模型
TLGAN (Transparent Latent GAN):通过控制其"透明潜在空间"的特定维度,来定制生成具有指定年龄、性别等属性的高逼真图像。
GANalyzer:一个框架,包含一组转换函数,用于在预训练好的GAN(如StyleGAN)的潜在空间中进行属性编辑,支持包括年龄和性别在内的多种属性。
DCGan的一个demo:
from pathlib import Path
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
# 设置随机种子,使结果可复现,因为这俩这生成的随机种子是独立的
torch.manual_seed(42)
np.random.seed(42)
#---------------------------------------------彩色图训练---------------------------------------------
# 1. 数据加载
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = datasets.CIFAR10(root='./data', train=True, download=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=0) # 减小batch
class Generator(nn.Module):
def __init__(self, noise_dim=100):
super().__init__()
self.model = nn.Sequential(
nn.Linear(noise_dim, 4*4*512, bias=False),
nn.BatchNorm1d(4*4*512),
nn.LeakyReLU(0.2, inplace=True),
nn.Unflatten(1, (512, 4, 4)),
# 4x4 -> 8x8
nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2, inplace=True),
# 8x8 -> 16x16
nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True),
# 16x16 -> 32x32
nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.LeakyReLU(0.2, inplace=True),
# 输出 3 通道
nn.ConvTranspose2d(64, 3, kernel_size=3, stride=1, padding=1, bias=False),
nn.Tanh()
)
def forward(self, z):
return self.model(z)
# 3. 判别器(输入通道改为3)
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=5, stride=2, padding=2),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout(0.3),
nn.Conv2d(64, 128, kernel_size=5, stride=2, padding=2),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout(0.3),
nn.Flatten(),
nn.Linear(128*8*8, 1) # 注意:CIFAR-10 32x32 经过两次 stride=2 变为 8x8,所以是 128*8*8
)
def forward(self, img):
return self.model(img)
# ---------------------------------------------彩色图训练---------------------------------------------
# ---------------------------------------------灰度图训练---------------------------------------------
# # ==================== 1. 加载并预处理数据 ====================
# # 使用 torchvision 加载 MNIST 数据集,将像素值归一化到 [-1, 1]
# transform = transforms.Compose([
# transforms.ToTensor(), # 将 PIL 图像或 numpy 数组转换为 [0,1] 的 Tensor
# transforms.Normalize((0.5,), (0.5,)) # 归一化到 [-1, 1]: (x - 0.5) / 0.5
# ])
#
# # 下载训练集
# train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
# train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True, num_workers=0)
#
# # ==================== 2. 构建生成器 ====================
# class Generator(nn.Module):
# """生成器:从 100 维噪声生成 28x28 灰度图"""
# # 张量形状遵循 (批量大小, 通道数, 高度, 宽度) 的惯例
# def __init__(self, noise_dim=100):
# super(Generator, self).__init__()
# self.model = nn.Sequential(
# # 全连接层,输出 7*7*256 个特征,bias是否添加偏置
# nn.Linear(noise_dim, 7*7*256, bias=False),
# nn.BatchNorm1d(7*7*256),
# #inplace=True 表示激活函数直接在原始输入张量上进行修改,而不是创建一个新的输出张量。
# nn.LeakyReLU(0.2, inplace=True),
#
# # 重塑为 7x7x256 的特征图
# # nn.Unflatten(1, (256, 7, 7)) 的作用是将输入张量的第 1 个维度(即索引为 1 的维度,也就是通道/特征维度)拆分成 (256, 7, 7) 三个子维度。
# nn.Unflatten(1, (256, 7, 7)), # 将 (batch, 7*7*256) 变为 (batch, 256, 7, 7)
#
# # 上采样到 14x14
# nn.ConvTranspose2d(256, 128, kernel_size=5, stride=2, padding=2, output_padding=1, bias=False),
# nn.BatchNorm2d(128),
# nn.LeakyReLU(0.2, inplace=True),
#
# # 上采样到 28x28
# nn.ConvTranspose2d(128, 64, kernel_size=5, stride=2, padding=2, output_padding=1, bias=False),
# nn.BatchNorm2d(64),
# nn.LeakyReLU(0.2, inplace=True),
#
# # 输出 1 通道,tanh 将像素值压到 [-1,1]
# nn.ConvTranspose2d(64, 1, kernel_size=5, stride=1, padding=2, bias=False),
# nn.Tanh()
# )
#
# def forward(self, z):
# return self.model(z)
#
# # ==================== 3. 构建判别器 ====================
# class Discriminator(nn.Module):
# """判别器:输入 28x28 灰度图,输出一个标量(logit)"""
# def __init__(self):
# super(Discriminator, self).__init__()
# self.model = nn.Sequential(
# nn.Conv2d(1, 64, kernel_size=5, stride=2, padding=2),
# #卷积:输出=(输入+2*padding-kernel_size)/stride+1
# #反卷积:输出 = stride*(输入-1)-2*padding+kernel_size+output_padding
# #转置卷积也称为反卷积:输出=stride*(输入-1)-2*padding+kernel_size+output_padding
# nn.LeakyReLU(0.2, inplace=True),
# nn.Dropout(0.3),
#
# nn.Conv2d(64, 128, kernel_size=5, stride=2, padding=2),
# nn.LeakyReLU(0.2, inplace=True),
# nn.Dropout(0.3),
#
# nn.Flatten(),
# nn.Linear(128*7*7, 1) # 输出 logit (BCEWithLogitsLoss(二元交叉熵 + Sigmoid))
# #在深度学习中,logits 是指神经网络在应用最终的激活函数(如 Sigmoid 或 Softmax)之前,直接输出的原始数值(Raw Values)
# )
#
# def forward(self, img):
# return self.model(img)
## 训练时
# criterion = nn.BCEWithLogitsLoss() # 不需要sigmod的损失函数
# criterion = nn.BCELoss() 需要sigmod()损失函数
# ---------------------------------------------灰度图训练---------------------------------------------
# ==================== 4. 实例化模型、损失函数和优化器 ====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
generator = Generator().to(device)
discriminator = Discriminator().to(device)
# 二元交叉熵损失,结合了 sigmoid 和 BCE(与 TensorFlow 的 from_logits=True 对应)
criterion = nn.BCEWithLogitsLoss()
# 优化器,学习率 0.0002,beta1=0.5
g_optimizer = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))
# ==================== 5. 训练过程 ====================
EPOCHS = 500
noise_dim = 100
num_examples_to_generate = 16
# 固定一组噪声,用于观察生成效果的变化
fixed_noise = torch.randn(num_examples_to_generate, noise_dim, device=device)
def train_step(real_images):
"""单步训练:更新生成器和判别器"""
batch_size = real_images.size(0) #real_images 的形状是 (64, 3, 32, 32),表示有 64 张 3 通道 32×32 的图像
# 真实标签和假标签(用于损失计算)
real_labels = torch.ones(batch_size, 1, device=device) #真实标签全1
fake_labels = torch.zeros(batch_size, 1, device=device)#假标签全0
# --------------------- 训练判别器 ---------------------
# 生成随机噪声
noise = torch.randn(batch_size, noise_dim, device=device)#从标准正态分布中采样随机噪声 noise
fake_images = generator(noise)#生成器 generator 将噪声映射为假图像 fake_images(与真实图像形状相同)
# 判别器对真实图片和假图片的输出
real_output = discriminator(real_images)
fake_output = discriminator(fake_images.detach()) # detach 防止梯度传到生成器#若没有 detach(),梯度的流向会同时更新生成器
# 计算先判别器的总损失 = 真实样本损失 + 假样本损失。
d_loss_real = criterion(real_output, real_labels)# 判别器对真实图像的预测与真实标签(全1)的损失,希望真实图像输出接近1
d_loss_fake = criterion(fake_output, fake_labels)# 判别器对假图像的预测与假标签(全0)的损失,希望假图像输出接近0
d_loss = d_loss_real + d_loss_fake # 总判别器损失为两者之和。这样判别器学会同时正确分类真实图像和假图像
# 反向传播和优化
d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()
# --------------------- 训练生成器 ---------------------
# 生成器的目标是欺骗判别器,使判别器将生成的假图像判断为真实
# 重新使用之前生成的 fake_images,但此时要计算生成器梯度
fake_output = discriminator(fake_images)
g_loss = criterion(fake_output, real_labels) # 希望判别器将假图片判断为真
# 大的空间会延迟平衡的修补,这样增加了周期的时间。所以空间不断增加,周期也不断增大,其中的因果也不断的增加。
# 空间、因果、平衡什么时候会达到临界,这决定了整个宇宙的结局。
# 简单的东西周期短时间也短也深。从简单到复杂的东西周期长也深入。正态分布图的宇宙意义非常独特!
g_optimizer.zero_grad()
g_loss.backward()
g_optimizer.step()
return g_loss.item(), d_loss.item() # 返回去生成器的损失和判别器的损失
# ========== 修改图像保存函数 ==========
def generate_and_save_images(epoch, fixed_noise):
generator.eval()
with torch.no_grad():
fake_images = generator(fixed_noise).cpu()
generator.train()
fake_images = fake_images * 0.5 + 0.5 # 反归一化
fig = plt.figure(figsize=(4, 4))
for i in range(num_examples_to_generate):
plt.subplot(4, 4, i+1)
plt.imshow(fake_images[i].permute(1, 2, 0)) # (C,H,W) → (H,W,C)
plt.axis('off')
# plt.savefig(f'image_at_epoch_{epoch:03d}.png')
plt.savefig(Path('result') / f'image_at_epoch_{epoch:03d}.png')
plt.close()
# 训练循环
for epoch in range(1, EPOCHS+1):
g_loss_avg = 0
d_loss_avg = 0
num_batches = 0
for batch_idx, (real_images, _) in enumerate(train_loader):
real_images = real_images.to(device)
g_loss, d_loss = train_step(real_images)
g_loss_avg += g_loss
d_loss_avg += d_loss
num_batches += 1
g_loss_avg /= num_batches
d_loss_avg /= num_batches
print(f'Epoch {epoch:2d}, Gen Loss: {g_loss_avg:.4f}, Disc Loss: {d_loss_avg:.4f}')
generate_and_save_images(epoch, fixed_noise)
print("训练完成!")
