未完成,待续......
GAN 主要用于生成数据,如生成图像、音频、文本等。GAN 由两个网络组成:生成器(Generator)和判别器(Discriminator)。生成器的目标是生成看起来像真实数据的数据,而判别器的目标是区分真实数据和生成器生成的数据。生成器和判别器之间的对抗训练使生成器生成的数据越来越逼真。
GAN基础概念
生成式对抗网络(Generative Adversarial Networks,GAN)是一种无监督学习算法,由Ian Goodfellow
等人于2014年提出。GAN由两个网络组成:生成器(Generator)和判别器(Discriminator)。生成器的目标是生成看起来像真实数据的数据,而判别器的目标是区分真实数据和生成器生成的数据。生成器和判别器之间的对抗训练使生成器生成的数据越来越逼真。
生成器(Generator)
生成器是一个神经网络,它接收一个随机噪声作为输入,然后生成一些数据。生成器的目标是生成看起来像真实数据的数据。
判别器(Discriminator)
判别器是一个神经网络,它接收生成器生成的数据和真实数据,然后判断哪些数据是真实的,哪些数据是生成的。判别器的目标是区分真实数据和生成器生成的数据。
生成器损失和判别器损失
生成器损失和判别器损失是GAN的两个重要指标。生成器损失是生成器生成的数据被判别器判断为真实数据的概率的负对数似然。判别器损失是判别器判断生成器生成的数据为真实数据的概率和判别器判断真实数据为真实数据的概率之和的负对数似然。
对抗训练
生成器和判别器之间的对抗训练使生成器生成的数据越来越逼真。生成器和判别
GAN的原理
GAN的核心思想是通过两个网络的对抗训练来实现生成数据。生成器和判别器之间的对抗训练使生成器生成的数据越来越逼真。生成器和判别器的训练过程如下:
生成器生成一些数据,这些数据是随机噪声生成的。
判别器接收生成器生成的数据和真实数据,然后判断哪些数据是真实的,哪些数据是生成的。
判别器根据生成器生成的数据和真实数据的判断结果,更新自己的参数。
生成器根据判别器的判断结果,更新自己的参数。
重复步骤1-4,直到生成器生成的数据越来越逼真。
现在,我们定义一些符号:
从判别器开始,令 CHW
为3x64x64 的图像,输出是一个标量,表示输入图像是真实数据的概率。
对于生成器,令 CHW
为100x1x1 的随机噪声,输出是 CHW
为3x64x64 的图像。
生成器和判别器之间的对抗训练可以用下面的公式表示:
其中,
理论上,这个极小极大博弈的解是生成器生成的数据和真实数据的分布相同。也就是说,
GAN的应用
GAN已经在图像生成、图像修复、图像超分辨率、图像风格迁移等领域取得了很好的效果。下面是一些GAN的应用:
DCGAN:深度卷积生成对抗网络(Deep Convolutional Generative Adversarial Networks)是一种生成对抗网络,它使用卷积神经网络来生成图像。
CycleGAN:循环一致生成对抗网络(Cycle-Consistent Generative Adversarial Networks)是一种生成对抗网络,它可以实现图像风格迁移。
Pix2Pix:图像到图像的生成对抗网络(Image-to-Image Translation with Conditional Adversarial Networks)是一种生成对抗网络,它可以实现图像到图像的转换。
StarGAN:多域图像风格迁移生成对抗网络(StarGAN: Unified Generative Adversarial Networks for Multi-Domain Image-to-Image Translation)是一种生成对抗网络,它可以实现多域图像风格迁移。
DCGAN
DCGAN原论文:Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks
深度卷积生成对抗网络(Deep Convolutional Generative Adversarial Networks,DCGAN)是一种生成对抗网络,它使用卷积神经网络来生成图像。DCGAN的生成器和判别器都是卷积神经网络,生成器使用反卷积层(Deconvolutional Layer)来生成图像,判别器使用卷积层来判断图像是真实数据还是生成数据。
DCGAN 则是上面 GAN 的直接扩展,不同之处在于它在鉴别器和生成器中分别显式地使用卷积层和卷积转置层。这种结构使得 DCGAN 在图像生成任务上表现出色。鉴别器由步长卷积层和 LeakyReLU 激活函数组成,生成器由卷积转置层和 ReLU 激活函数组成。
在 鉴别器
中,输入是一个 3x64x64 的输入图像,输出是一个标量概率,表示输入来自真实数据分布。
在 生成器
中,输入是一个噪声向量,输出是一个 3x64x64 的 RGB 图像。生成器的输出是通过卷积转置层和 ReLU 激活函数生成的。
DCGAN上手实验
我们以
Pytorch
官方文档的教程为例,实现一个简单的 DCGAN 模型。原文链接:DCGAN Tutorial
我们将使用 Celeb-A
数据集,我们使用此数据集来训练一个 DCGAN 模型,以生成新的人脸图像。
- 定义基础参数
首先,我们定义一些基础参数:
import torch
# 数据集路径
dataroot = "data/celeba"
# 加载线程数
workers = 0
# 批次大小
batch_size = 128
# 图像的空间大小。所有图像将使用变换器调整大小为此大小。
image_size = 64
# 通道数
nc = 3
# 潜在向量的大小(生成器的输入大小)
nz = 100
# 与生成器中传输的特征图的深度相关
ngf = 64
# 设置通过鉴别器传播的特征图的深度
ndf = 64
# 训练轮次
num_epochs = 5
# 学习率固定为0.0002,至于为什么这里选择这个值,请参考原论文
lr = 0.0002
# Beta1超参数用于Adam优化器,固定为0.5,至于为什么这里选择这个值,请参考原论文
beta1 = 0.5
# 是否使用GPU
ngpu = 1
# 设置设备
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
- 数据集加载
我们需要加载 Celeb-A
数据集。下载请前往 Celeb-A 找到名为 img_align_celeba.zip
的文件,然后解压到 data/celeba
目录下。
解压后的目录结构如下:
data/celeba
├── img_align_celeba
│ ├── 000001.jpg
│ ├── 000002.jpg
│ ├── ...
然后,我们使用 torchvision
加载数据集:
import torch
from torchvision import datasets, transforms
import torchvision.datasets as dset
dataset = dset.ImageFolder(
root=dataroot,
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)),
]
),
)
dataloader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True, num_workers=workers
)
在这里,我们使用 ImageFolder
类加载数据集,然后使用 Compose
类创建一个图像变换器,将图像调整为 64x64
大小,并将像素值归一化到 [-1, 1]
之间。
我们可以测试一下数据集加载是否成功:
import matplotlib.pyplot as plt
import torchvision.utils as vutils
import numpy as np
# 获取一个批次的数据
real_batch = next(iter(dataloader))
# 显示图像
plt.figure(figsize=(8, 8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(
np.transpose(
vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),
(1, 2, 0),
)
)
如果一切正常,我们应该能看到一些人脸图像:
- 权重初始化
在
DCGAN
论文中,作者指出所有模型都应该使用均值为0
,标准差为0.02
的正态分布进行初始化。
在训练之前,我们需要初始化生成器和判别器的权重。我们使用 weights_init
函数来初始化权重:
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)
- 生成器
生成器是一个将潜在向量 z
转换为图像的神经网络。我们使用 ConvTranspose2d
层来实现生成器:
import torch.nn as nn
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# 输入是 Z,对 Z 进行卷积转置
# ConvTranspose2d 输入参数:(输入通道数,输出通道数,卷积核大小,步长,填充,偏置)
nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
# BatchNorm2d 输入参数:(通道数)
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# 状态大小。 (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# 状态大小。 (ngf*4) x 8 x 8
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# 状态大小。 (ngf*2) x 16 x 16
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# 状态大小。 (ngf) x 32 x 32
nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
nn.Tanh(),
# 状态大小。 (nc) x 64 x 64
)
def forward(self, input):
return self.main(input)
在生成器中,我们使用 ConvTranspose2d
层来实现卷积转置。再使用 BatchNorm2d
层来实现批量归一化。最后,我们使用 Tanh
激活函数来生成图像。
- 判别器
判别器是一个将图像转换为概率的神经网络。我们使用 Conv2d
层来实现判别器:
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
# 输入是 (nc) x 64 x 64
# Conv2d 输入参数:(输入通道数,输出通道数,卷积核大小,步长,填充,偏置)
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
# LeakyReLU 输入参数:(负斜率,是否原地操作)
nn.LeakyReLU(0.2, inplace=True),
# 状态大小。 (ndf) x 32 x 32
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小。 (ndf*2) x 16 x 16
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小。 (ndf*4) x 8 x 8
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小。 (ndf*8) x 4 x 4
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid(),
)
def forward(self, input):
return self.main(input)
在判别器中,我们使用 Conv2d
层来实现卷积。再使用 LeakyReLU
激活函数来实现非线性。最后,我们使用 Sigmoid
激活函数来输出概率。
- 实例化生成器和判别器
# 创建生成器
netG = Generator().to(device)
# 创建判别器
netD = Discriminator().to(device)
# 初始化权重
netG.apply(weights_init)
netD.apply(weights_init)
- 损失函数
我们使用二元交叉熵损失函数来计算生成器和判别器的损失,其数学表达式如下:
在这里,
# 创建二元交叉熵损失函数
criterion = nn.BCELoss()
# 创建一个固定的噪声向量,用于生成图像
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
# 真实标签
real_label = 1
# 假标签
fake_label = 0
- 优化器
我们使用 Adam
优化器来优化生成器和判别器的参数:
import torch.optim as optim
# 为生成器和判别器创建优化器
# Adam 输入参数:(参数,学习率,betas)
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))
- 训练模型
在训练模型时,我们需要依次执行以下步骤:
为真实数据和生成数据计算判别器的损失。
为生成器计算损失。
更新判别器和生成器的参数。
# 训练模型
for epoch in range(num_epochs):
for i, data in enumerate(dataloader, 0):
############################
# (1) 更新判别器网络: 最大化 log(D(x)) + log(1 - D(G(z)))
###########################
## 训练判别器网络,所有梯度清零
netD.zero_grad()
## 准备真实数据
real_cpu = data[0].to(device)
b_size = real_cpu.size(0)
label = torch.full((b_size,), real_label, device=device)
## 通过判别器网络传递真实数据
output = netD(real_cpu).view(-1)
## 计算真实数据的损失
errD_real = criterion(output, label)
## 计算真实数据的梯度
errD_real.backward()
D_x = output.mean().item()
## 准备假数据
noise = torch.randn(b_size, nz, 1, 1, device=device)
fake = netG(noise)
label.fill_(fake_label)
## 通过判别器网络传递假数据
output = netD(fake.detach()).view(-1)
## 计算假数据的损失
errD_fake = criterion(output, label)
## 计算假数据的梯度
errD_fake.backward()
D_G_z1 = output.mean().item()
## 计算判别器的总损失
errD = errD_real + errD_fake
## 更新判别器的参数
optimizerD.step()
############################
# (2) 更新生成器网络: 最大化 log(D(G(z)))
###########################
## 训练生成器网络,所有梯度清零
netG.zero_grad()
label.fill_(real_label) # 假数据标签是真实数据
## 通过判别器网络传递假数据
output = netD(fake).view(-1)
## 计算生成器的损失
errG = criterion(output, label)
## 计算生成器的梯度
errG.backward()
D_G_z2 = output.mean().item()
## 更新生成器的参数
optimizerG.step()
# 输出训练状态
if i % 50 == 0:
print(
"[{}/{}][{}/{}] Loss_D: {:.4f} Loss_G: {:.4f} D(x): {:.4f} D(G(z)): {:.4f}/{:.4f}".format(
epoch,
num_epochs,
i,
len(dataloader),
errD.item(),
errG.item(),
D_x,
D_G_z1,
D_G_z2,
)
)
# 保存生成器和判别器的参数
if i % 100 == 0:
vutils.save_image(
real_cpu, "%s/real_samples.png" % "./results", normalize=True
)
fake = netG(fixed_noise)
vutils.save_image(
fake.detach(),
"%s/fake_samples_epoch_%03d.png" % ("./results", epoch),
normalize=True,
)
# 保存模型
torch.save(netG.state_dict(), "netG.pth")
torch.save(netD.state_dict(), "netD.pth")
- 训练可视化
生成训练损失迭代图:
import matplotlib.pyplot as plt
# 训练损失迭代图
plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
# 读取损失数据
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()
可视化G进度:
# 生成器进度
fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
HTML(ani.to_jshtml())
经过以上步骤,我们就可以训练一个简单的 DCGAN 模型了,那么现在我们梳理一下完整代码:
# 导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
import torchvision.datasets as dset
import torchvision.utils as vutils
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
# 数据集路径
dataroot = "data/celeba"
# 加载线程数
workers = 0
# 批次大小
batch_size = 128
# 图像大小
image_size = 64
# 图像通道数
nc = 3
# 潜在向量大小
nz = 100
# 生成器特征图大小
ngf = 64
# 判别器特征图大小
ndf = 64
# 训练轮数
num_epochs = 5
# 学习率
lr = 0.0002
# 优化器beta1参数
beta1 = 0.5
# 使用的GPU数量
ngpu = 1
# 设置设备
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
# 创建数据集
dataset = dset.ImageFolder(
root=dataroot,
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)),
]
),
)
# 创建数据加载器
dataloader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True, num_workers=workers
)
# 定义权重初始化函数
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)
# 定义生成器
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.main = nn.Sequential(
# 输入是 Z,经过转置卷积
nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# 状态大小: (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# 状态大小: (ngf*4) x 8 x 8
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# 状态大小: (ngf*2) x 16 x 16
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# 状态大小: (ngf) x 32 x 32
nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
nn.Tanh(),
# 输出大小: (nc) x 64 x 64
)
def forward(self, input):
return self.main(input)
# 定义判别器
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
# 输入是 (nc) x 64 x 64
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小: (ndf) x 32 x 32
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小: (ndf*2) x 16 x 16
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小: (ndf*4) x 8 x 8
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
# 状态大小: (ndf*8) x 4 x 4
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid(),
# 输出大小: 标量
)
def forward(self, input):
return self.main(input)
# 创建生成器
netG = Generator().to(device)
netG.apply(weights_init)
# 创建判别器
netD = Discriminator().to(device)
netD.apply(weights_init)
# 定义损失函数
criterion = nn.BCELoss()
# 创建固定的噪声向量,用于生成图像
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
# 真实标签和假标签
real_label = 1.0
fake_label = 0.0
# 设置优化器
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))
# 训练循环
# 用于跟踪进度的列表
img_list = []
G_losses = []
D_losses = []
iters = 0
print("开始训练...")
for epoch in range(num_epochs):
for i, data in enumerate(dataloader, 0):
############################
# (1) 更新判别器
###########################
# 训练判别器,所有梯度清零
netD.zero_grad()
# 真实数据
real_cpu = data[0].to(device)
b_size = real_cpu.size(0)
label = torch.full((b_size,), real_label, device=device)
# 判别器对真实数据的输出
output = netD(real_cpu).view(-1)
# 计算真实数据的损失
errD_real = criterion(output, label)
# 反向传播
errD_real.backward()
D_x = output.mean().item()
# 生成假数据
noise = torch.randn(b_size, nz, 1, 1, device=device)
fake = netG(noise)
label.fill_(fake_label)
# 判别器对假数据的输出
output = netD(fake.detach()).view(-1)
# 计算假数据的损失
errD_fake = criterion(output, label)
# 反向传播
errD_fake.backward()
D_G_z1 = output.mean().item()
# 判别器总的损失
errD = errD_real + errD_fake
# 更新判别器
optimizerD.step()
############################
# (2) 更新生成器
###########################
netG.zero_grad()
label.fill_(real_label) # 生成器希望判别器认为生成的数据是真实的
# 判别器对假数据的输出
output = netD(fake).view(-1)
# 计算生成器的损失
errG = criterion(output, label)
# 反向传播
errG.backward()
D_G_z2 = output.mean().item()
# 更新生成器
optimizerG.step()
# 输出训练状态
if i % 50 == 0:
print(
"[%d/%d][%d/%d]\t判别器损失: %.4f\t生成器损失: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f"
% (
epoch + 1,
num_epochs,
i,
len(dataloader),
errD.item(),
errG.item(),
D_x,
D_G_z1,
D_G_z2,
)
)
# 保存损失以便后续绘图
G_losses.append(errG.item())
D_losses.append(errD.item())
# 检查生成器的进展,保存生成的图像
if (iters % 500 == 0) or (
(epoch == num_epochs - 1) and (i == len(dataloader) - 1)
):
with torch.no_grad():
fake = netG(fixed_noise).detach().cpu()
img_list.append(vutils.make_grid(fake, padding=2, normalize=True))
iters += 1
# 绘制损失曲线
plt.figure(figsize=(10, 5))
plt.title("训练期间生成器和判别器的损失")
plt.plot(G_losses, label="G")
plt.plot(D_losses, label="D")
plt.xlabel("迭代次数")
plt.ylabel("损失")
plt.legend()
plt.show()
# 动画显示生成器的训练进度
fig = plt.figure(figsize=(8, 8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i, (1, 2, 0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
plt.show()
# 保存模型
torch.save(netG.state_dict(), "netG.pth")
torch.save(netD.state_dict(), "netD.pth")
训练动态图: