1. Generative Adversarial Networks(GAN)
由故事可知,GAN需要两个网络——G(Generator)&D(Discriminator),也就是一个绘画者和一个纠错者。
- G是一个生成图片的网络(就因为这是一个网络,所以需要
输入值
),而这个输入值就是一个随机的噪声z,通过这个噪声生成图片,记做G(z)。- D是一个判别网络,判别一张图片是不是
真实的
。它的输入参数是一张图片x,输出D(x)代表x为真实图片的概率。如果概率为1,就代表100%是真实的图片;如果概率为0,就代表不可能是真实的图片。
生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。
最后博弈的结果是什么?在最理想的状态下,G可以生成足以“以假乱真”的图片G(z)。对于D来说,它难以判定G生成的图片究竟是不是真实的,因此D(G(z)) = 0.5。我们把这种状态成为纳什均衡!
使用公式表示就是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0K40Jkak-1639442662167)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/20210131175543.png)]
- 整个式子由两项构成。x表示真实图片,z表示输入G网络的噪声,而G(z)表示G网络生成的图片。
- D(x)表示D网络判断真实图片是否真实的概率(因为x就是真实的,所以对于D来说,这个值越接近1越好)。而D(G(z))是D网络判断G生成的图片的是否真实的概率。
- G的目的:上面提到过,D(G(z))是D网络判断G生成的图片是否真实的概率,G应该希望自己生成的图片“越接近真实越好”。也就是说,G希望D(G(z))尽可能得大,这时V(D, G)会变小。因此我们看到式子的最前面的记号是min_G。
- D的目的:D的能力越强,D(x)应该越大,D(G(x))应该越小。这时V(D,G)会变大。因此式子对于D来说是求最大(max_D)
既然此公式可以作为判别的标准,那么我们就把该公式记为损失函数!
在pytorch中,可以直接调用nn.BCELoss()
进行表示!
那么,如何构建优化函数呢?
2. DCGAN
我们知道深度学习中对图像处理应用最好的模型是CNN,那么如何把CNN与GAN结合?DCGAN是这方面最好的尝试之一(论文地址:[1511.06434] Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks)
DCGAN的原理和GAN是一样的,这里就不在赘述。它只是把上述的G和D换成了两个卷积神经网络(CNN)。但不是直接换就可以了,DCGAN对卷积神经网络的结构做了一些改变,以提高样本的质量和收敛的速度,这些改变有:
- 取消所有pooling层。G网络中使用转置卷积(transposed convolutional layer)进行上采样,D网络中用加入stride的卷积代替pooling。
- 在D和G中均使用batch normalization
- 去掉FC层,使网络变为全卷积网络
- G网络中使用ReLU作为激活函数,最后一层使用tanh
- D网络中使用LeakyReLU作为激活函数
3. 代码
结构:
代码:
main.py
import argparse
import torch
import torchvision
import torchvision.utils as vutils
import torch.nn as nn
from random import randint
from model import NetD, NetG
parser = argparse.ArgumentParser()
parser.add_argument('--batchSize', type=int, default=64)
parser.add_argument('--imageSize', type=int, default=96)
parser.add_argument('--nz', type=int, default=100, help='size of the latent z vector')
parser.add_argument('--ngf', type=int, default=64)
parser.add_argument('--ndf', type=int, default=64)
parser.add_argument('--epoch', type=int, default=25, help='number of epochs to train for')
parser.add_argument('--lr', type=float, default=0.0002, help='learning rate, default=0.0002')
parser.add_argument('--beta1', type=float, default=0.5, help='beta1 for adam. default=0.5')
parser.add_argument('--data_path', default='data/', help='folder to train data')
parser.add_argument('--outf', default='imgs/', help='folder to output images and model checkpoints')
opt = parser.parse_args()
# 定义是否使用GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 图像读入与预处理
transforms = torchvision.transforms.Compose([
torchvision.transforms.Scale(opt.imageSize),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])
dataset = torchvision.datasets.ImageFolder(opt.data_path, transform=transforms)
dataloader = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=opt.batchSize,
shuffle=True,
drop_last=True,
)
# 转到GPU上
netG = NetG(opt.ngf, opt.nz).to(device)
netD = NetD(opt.ndf).to(device)
criterion = nn.BCELoss()
optimizerG = torch.optim.Adam(netG.parameters(), lr=opt.lr, betas=(opt.beta1, 0.999))
optimizerD = torch.optim.Adam(netD.parameters(), lr=opt.lr, betas=(opt.beta1, 0.999))
label = torch.FloatTensor(opt.batchSize)
real_label = 1
fake_label = 0
for epoch in range(100):
for i, (imgs, _) in enumerate(dataloader):
# 固定生成器G,训练鉴别器D
optimizerD.zero_grad() # 训练D
# 让D尽可能的把真图片判别为1
imgs = imgs.to(device) # 转到GPU上
output = netD(imgs)
label.data.fill_(real_label) # 把评价标准全部列为1(真)
label = label.to(device)
errD_real = criterion(output, label) # 使用D判别时,全部列为真
errD_real.backward()
# 让D尽可能把假图片判别为0
label.data.fill_(fake_label) # 把评价标准全部列为0(假)
noise = torch.randn(opt.batchSize, opt.nz, 1, 1)
noise = noise.to(device)
fake = netG(noise) # 生成假图
output = netD(fake.detach()) # 避免梯度传到G,因为G不用更新。其中,detach就是截断反向传播的梯度流。
errD_fake = criterion(output, label) # 使用D判别时,全部列为假
errD_fake.backward()
errD = errD_fake + errD_real # 总的损失参数,仅展示时使用,我们一般分开进行backward,不会使用到这个
optimizerD.step()
# 固定鉴别器D,训练生成器G
optimizerG.zero_grad()
# 让D尽可能把G生成的假图判别为1
label.data.fill_(real_label)
label = label.to(device)
output = netD(fake)
errG = criterion(output, label)
errG.backward()
optimizerG.step()
print('[%d/%d][%d/%d] Loss_D: %.3f Loss_G %.3f'
% (epoch, opt.epoch, i, len(dataloader), errD.item(), errG.item()))
vutils.save_image(fake.data,
'%s/fake_samples_epoch_%03d.png' % (opt.outf, epoch),
normalize=True)
torch.save(netG.state_dict(), '%s/netG_%03d.pth' % (opt.outf, epoch))
torch.save(netD.state_dict(), '%s/netD_%03d.pth' % (opt.outf, epoch))
model.py
# 我们使用的是 DCGAN, 所以在model是CNN
import torch.nn as nn
# 定义生成器网络G
class NetG(nn.Module):
def __init__(self, ngf, nz):
super(NetG, self).__init__()
# layer1输入的是一个100x1x1的随机噪声, 输出尺寸(ngf*8)x4x4
self.layer1 = nn.Sequential(
nn.ConvTranspose2d(nz, ngf * 8, kernel_size=4, stride=1, padding=0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(inplace=True)
)
# layer2输出尺寸(ngf*4)x8x8
self.layer2 = nn.Sequential(
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(inplace=True)
)
# layer3输出尺寸(ngf*2)x16x16
self.layer3 = nn.Sequential(
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(inplace=True)
)
# layer4输出尺寸(ngf)x32x32
self.layer4 = nn.Sequential(
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(inplace=True)
)
# layer5输出尺寸 3x96x96
self.layer5 = nn.Sequential(
nn.ConvTranspose2d(ngf, 3, 5, 3, 1, bias=False),
nn.Tanh()
)
# 定义NetG的前向传播
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
return out
# 定义鉴别器网络D
class NetD(nn.Module):
def __init__(self, ndf):
super(NetD, self).__init__()
# layer1 输入 3 x 96 x 96, 输出 (ndf) x 32 x 32
self.layer1 = nn.Sequential(
nn.Conv2d(3, ndf, kernel_size=5, stride=3, padding=1, bias=False),
nn.BatchNorm2d(ndf),
nn.LeakyReLU(0.2, inplace=True)
)
# layer2 输出 (ndf*2) x 16 x 16
self.layer2 = nn.Sequential(
nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True)
)
# layer3 输出 (ndf*4) x 8 x 8
self.layer3 = nn.Sequential(
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True)
)
# layer4 输出 (ndf*8) x 4 x 4
self.layer4 = nn.Sequential(
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True)
)
# layer5 输出一个数(概率)
self.layer5 = nn.Sequential(
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
# 定义NetD的前向传播
def forward(self ,x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
return out
4. 结果:
训练1次:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oald9OA2-1639442662174)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/20210131191944.png)]
训练50次:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CvxNhYMg-1639442662176)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/fake_samples_epoch_049.png)]
训练100次:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YydFTND5-1639442662179)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/fake_samples_epoch_099.png)]
5. 附录:
5.1 超参数书写标准——Argparse
我们在写参数的时候,经常是这种情况:
lr = 0.01
batch_size = 64
但是,这样写就出现种情况——不美观、不规范!
为了让代码更加美观,我们使用更加规整的class
class opt(object):
lr = 0.01
batch_size = 64
这样的话,已经做到的初步的美观。但是不符合class
的思想!
因此,我们使用Argparse
进行表示:
书 写 格 式 : − − 参 数 名 称 , t y p e = 数 据 类 型 , d e f a u l t = 默 认 值 \color{red} {书写格式:--参数名称, type=数据类型, default=默认值} 书写格式:−−参数名称,type=数据类型,default=默认值
parser = argparse.ArgumentParser()
parser.add_argument('--batchSize', type=int, default=64)
parser.add_argument('--imageSize', type=int, default=96)
parser.add_argument('--nz', type=int, default=100, help='size of the latent z vector')
parser.add_argument('--ngf', type=int, default=64)
parser.add_argument('--ndf', type=int, default=64)
parser.add_argument('--epoch', type=int, default=25, help='number of epochs to train for')
parser.add_argument('--lr', type=float, default=0.0002, help='learning rate, default=0.0002')
parser.add_argument('--beta1', type=float, default=0.5, help='beta1 for adam. default=0.5')
parser.add_argument('--data_path', default='data/', help='folder to train data')
parser.add_argument('--outf', default='imgs/', help='folder to output images and model checkpoints')
opt = parser.parse_args()
当然,这个还是有更多好处的。比如随时可以调参:
- 打开【Edit Run Configuration】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIv22zl7-1639442662183)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/20210131173007.png)]
- 【Edit Run Configuration】展示,并且把加入的参数名称和参数放到这个里面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SqDACltQ-1639442662185)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/20210131173224.png)]
- 修改参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b0kpiZCm-1639442662189)(https://wang-1304725667.cos.ap-chengdu.myqcloud.com/mardown/20210131172319.png)]
5.2 BCELoss
这个是专门给GAN
的loss函数
5.3 参考文献
https://www.pianshen.com/article/1532910235/
https://zhuanlan.zhihu.com/p/24767059
https://blog.youkuaiyun.com/qq_22210253/article/details/85222093