DCGAN
DCGAN简介
DCGAN是基于DeConvlution的GAN,DeConvlution即反卷积,反卷积 DeConv 又叫 Fractional Strided Conv ,或者转置卷积 TransposeConv(更规范的表述),计算操作回顾第九课反卷积和反池化部分;
DCGAN的训练过程和GAN是一样的,只是数据从mnist变得更复杂,所以基于卷积网络进行特征提取作为判别器,基于反卷积作为生成器输出伪造图像;
论文中,生成器从100维向量作为隐式空间,生成
(
3
,
64
,
64
)
(3,64,64)
(3,64,64)的图像,生成器网络如下:
下面将会使用DCGAN生成celebA,注意,实验中的生成器与上图的结构不一样
DCGAN生成celebA
celebA介绍
CelebA是CelebFaces Attribute的缩写,意即名人人脸属性数据集,其包含10,177个名人身份的202,599张人脸图片,每张图片都做好了特征标记,CelebA由香港中文大学开放提供,广泛用于人脸相关的计算机视觉训练任务,官方网址:Large-scale CelebFaces Attributes (CelebA) Dataset
下载后有如下目录:
Anno存放标注信息文件,Eval是training、validation及testing数据集的划分注释,Img则是存放相应的人脸图像,README.txt是CelebA介绍文件;
在Img下有以下目录:
本次实验的DCGAN生成celebA,只使用img_align_celeba.zip
准备工作
首先导入必要的包和模块:
import torchvision.utils as vutils
import torchvision
from torchvision import transforms
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
基于ImageFolder
加载数据集:
image_size=64
batch_size=128
dataroot="./data"
"""
目录结构:
data
img_align_celeba
图片1
图片2
...
DCGAN脚本
"""
num_workers = 0
dataset = torchvision.datasets.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)),
]))
ImageFolder
一般用法为:
dataset = torchvision.datasets.ImageFolder(root,transform=None)
torchvision.datasets.ImageFolder
要求数据集按照如下方式组织:
# 第一类
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png
# 第二类
root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
...
根目录 root
下存储的是类别文件夹(如cat,dog),每个类别文件夹下存储对应类别的图像;
ImageFolder的参数有:
- root:图片存储的根目录,即各类别文件夹所在目录的上一级目录,在上面的例子中是
'./root'
- transform:对图片进行预处理的操作
读取目录成功后,各个类别的标签依次对应为 0,1, 2,…
ImageFolder有以下实例变量:
- self.classes:用一个 list 保存类别名称,
["dog","cat"]
- self.class_to_idx:类别的标签,字典保存,
{"dog":0,"cat":1}
- self.imgs:一个列表,每个元素为元组(图片路径,标签),
{img path,0}
使用dataloader:
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
基于dataloader获取一个batch数据:
real_batch=next(iter(dataloader))
# real_batch[0]保存图像张量,real_batch[1]保存每个图像的类别
# real_batch[0] (batchsize,channel,height,width)
# real_batch[1] (batchsize,)
# 取出一个图像张量
print(real_batch[0][0])
使用torchvision.utils.make_grid
将多幅图像可视化到grid中,从batch中获取了64张图像,可视化到
8
×
8
8 \times 8
8×8的网格里,注意plt.inshow(),接收数组或PIL图像:
plt.figure(figsize=(8,8))
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu().numpy(), (1,2,0)))
plt.show()
torchvision.utils.make_grid
用于生成一个图像组成的网格,常用参数有:
torchvision.utils.make_grid(tensor, padding=2, normalize=False, range=None)
- tensor:一个4维的张量,比如形状为 ( B , C , H , W ) (B,C,H,W) (B,C,H,W)
- padding:每个子图周围的零填充(黑色)
- normalize:如果为True,将网格中所有图像的值缩放到参数range之间,如果为False,则不缩放
- range: 元组(min, max),默认为该网格对应张量的像素最小值与像素最大值
模型定义
在论文中,提到了一个初始化技巧,将有助于训练:
- 把模型的所有卷积网络参数都初始化为
mean=0, std=0.02
; - BatchNorm的可学习参数初始化为
mean=1.0, std=0.02
;
定义初始化参数的函数:
def weights_init(m):
# 获取对象m所属类的类名
classname = m.__class__.__name__
# 如果Conv在classname中出现了,进行以下初始化
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
# 如果BatchNorm在classname中出现了,进行以下初始化
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
# torch.nn.init.constant_(tensor, val) 向tensor填充值val
nn.init.constant_(m.bias.data, 0)
定义生成网络,注意一个细节,由于之后输入判别器的真实图像(即真实图像预处理后的张量)值在-1到1之间,所以用tanh作为激活函数,nn.ConvTranspose2d
的使用方法回顾第九课,nn.BatchNorm2d
的使用方法回顾第七课:
nz = 100 # latent vector的大小
ngf = 64 # 生成器各层滤波器基数
ndf = 64 # 判别器各层滤波器基数
nc = 3 # 生成器输出张量的通道数
class Generator(nn.Module):
def __init__(self):
super().__init__()
self.main=nn.Sequential(
# torch.nn.ConvTranspose2d(in_channels, out_channels,kernel_size, stride=1, padding=0, bias=True, dilation=1)
# input (nz) x 1 x 1
nn.ConvTranspose2d(nz, ngf * 8, 4, stride=1, padding=0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(),
# state size. (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(),
# state size. (ngf*4) x 8 x 8
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(),
# state size. (ngf*2) x 16 x 16
nn.ConvTranspose2d(ngf * 2, ngf, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(),
# state size. (ngf) x 32 x 32
nn.ConvTranspose2d(ngf, nc, 4, stride=2, padding=1, bias=False),
# 由于输入判别器的真实图像(真实图像预处理后的张量)值在-1到1之间,所以用tanh作激活函数
nn.Tanh()
# state size. (nc) x 64 x 64
)
def forward(self,x):
return self.main(x)
实例化生成器,并初始化其参数:
netg=Generator().to(device)
print(netg)
# nn.Module.apply将weights_init函数递归地应用到netg的每一个子模型
netg.apply(weights_init)
nn.Module.apply
apply(fn)
参数:
- fn (m)
model.apply函数可以不断遍历model的各个模块m,并在模块m上应用函数fn
定义判别网络,并实例化网络,初始化参数:
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.main=nn.Sequential(
# input is (nc) x 64 x 64
nn.Conv2d(nc, ndf, 4, stride=2, padding=1, bias=False),
nn.LeakyReLU(0.2),
# state size. (ndf) x 32 x 32
nn.Conv2d(ndf, ndf * 2, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2),
# state size. (ndf*2) x 16 x 16
nn.Conv2d(ndf * 2, ndf * 4, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2),
# state size. (ndf*4) x 8 x 8
nn.Conv2d(ndf * 4, ndf * 8, 4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2),
# state size. (ndf*8) x 4 x 4
nn.Conv2d(ndf * 8, 1, 4, stride=1, padding=0, bias=False),
nn.Sigmoid()
# state size. (1) x 1 x 1
)
def forward(self,x):
return self.main(x)
netd=Discriminator().to(device)
netd.apply(weights_init)
训练生成网络与判别网络
训练过程与第十课的GAN训练过程一样,先设置训练参数,依然采用二元交叉熵计算损失,并选择Adam作为优化方法:
# 训练设置
lr = 0.0002
beta1 = 0.5
loss_fn = nn.BCELoss()
d_optimizer = torch.optim.Adam(netd.parameters(), lr=lr, betas=(beta1, 0.999))
g_optimizer = torch.optim.Adam(netg.parameters(), lr=lr, betas=(beta1, 0.999))
训练网络:
# 训练
total_steps = len(dataloader)
num_epochs = 5
for epoch in range(num_epochs):
for i, (images, _) in enumerate(dataloader):
# iamges (batch_size,c,h,w)
batch_size=images.shape[0]
images = images.to(device)
real_labels = torch.ones(batch_size).to(device) # (batch_size,)
fake_labels = torch.zeros(batch_size).to(device) # (batch_size,)
real_outputs = netd(images).view(-1) # (batch_size,)
# 真实图片的损失
d_loss_real = loss_fn(real_outputs, real_labels)
# 生成 fake images
# latent space
z = torch.randn(batch_size, nz, 1, 1).to(device) # (batch_size,100,1,1)
fake_images = netg(z) # (batch_size,3,64,64)
# 将G的输出从计算图中剥离,避免在训练D时涉及到G的梯度
fake_outputs = netd(fake_images.detach()).view(-1) # (batch_size,)
d_loss_fake = loss_fn(fake_outputs, fake_labels)
d_loss = d_loss_real + d_loss_fake
# 更新D
d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()
# 更新G
# 不能detach,因为要追踪G的梯度
g_outputs = netd(fake_images).view(-1) # (batch_size,)
g_loss = loss_fn(g_outputs, real_labels)
# 由于D还在计算图中,所以需要将D的梯度置0
d_optimizer.zero_grad()
g_optimizer.zero_grad()
g_loss.backward()
g_optimizer.step()
# 一般来说 G loss 越小越好
if i % 10 == 0:
print("Epoch[{}/{}],Step[{}/{}],D_loss:{},G_loss:{},D(x):{},D(G(z)):{}".format(
epoch, num_epochs, i, total_steps,
d_loss.item(),
g_loss.item(),
real_outputs.mean().item(),
fake_outputs.mean().item()
))
保存两个网络的参数:
torch.save(netd.state_dict(), "dcgan_d.th")
torch.save(netg.state_dict(), "dcgan_g.th")
使用生成器生成伪造图像:
# 生成器生成图片
# (64,100,1,1) nz:latent space
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
# 不进行梯度跟踪,节省显存
with torch.no_grad():
fake = netg(fixed_noise).clone().cpu() # (batch_size,3,64,64)
real_batch = next(iter(dataloader))
# Plot the real images
plt.figure(figsize=(30,30))
plt.subplot(1,2,1)
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))
# Plot the fake images
plt.subplot(1,2,2)
plt.title("Fake Images")
plt.imshow(np.transpose(vutils.make_grid(fake, padding=2, normalize=True), (1,2,0)))
plt.show()
CycleGAN
CycleGAN即Cycle-Consistent Adversarial Networks,也用于生成图像,但其生成的图像内容可以由人来主观进行选择,可以用于风格迁移,去除马赛克,更换人脸等其他用途;CycleGAN与常规配对训练数据的不同之处在于,CycleGAN可以实现无配对数据的训练;
CycleGAN的创新点在于能够在源域和目标域之间,无须建立训练数据间一对一的映射,就可实现源到目标域的迁移;
网络的架构如下:
CycleGAN网络即图
(
a
)
(a)
(a)所示,可以看出其实包含了两个GAN,cycle-consistency loss确保了两个分布之间的转换;
在
(
a
)
(a)
(a)中,存在两个分布
X
X
X和
Y
Y
Y,而生成器G,F分别是
X
X
X到
Y
Y
Y和
Y
Y
Y到
X
X
X的映射,两个判别器Dx,Dy可以对转换后的图片进行判别;
在
(
b
)
(b)
(b)中,生成器G将
X
X
X分布转换到
Y
Y
Y分布,记为
Y
^
\widehat{Y}
Y
,将该分布与真实的
Y
Y
Y传递给判别器Dy进行训练,提高Dy对于
Y
Y
Y分布数据的分辨能力,单向从
X
X
X到
Y
Y
Y可以训练G与Dy,目标函数为:
L
G
A
N
(
G
,
D
Y
,
X
,
Y
)
=
E
y
∼
p
d
a
t
a
(
y
)
[
l
o
g
D
Y
(
y
)
]
+
E
x
∼
p
d
a
t
a
(
x
)
[
l
o
g
(
1
−
D
Y
(
G
(
x
)
)
]
L_{GAN}(G,D_{Y},X,Y)=E_{y\sim p_{data}(y)}[logD_{Y}(y)]+E_{x\sim p_{data}(x)}[log(1-D_{Y}(G(x))]
LGAN(G,DY,X,Y)=Ey∼pdata(y)[logDY(y)]+Ex∼pdata(x)[log(1−DY(G(x))]
其中,
E
x
∼
p
d
a
t
a
(
x
)
E_{x\sim p_{data}(x)}
Ex∼pdata(x)代表从
X
X
X分布中采样得到的分布期望;
此时再将
Y
^
\widehat{Y}
Y
用生成器F转换到
X
X
X分布,记为
X
^
\widehat{X}
X
,现在加入cycle-consistency函数,确保还原后的
X
^
\widehat{X}
X
与原始
X
X
X接近,使用L1损失衡量(两项之差的绝对值):
L
c
y
c
,
X
=
E
x
∼
p
d
a
t
a
(
x
)
[
∣
∣
F
(
G
(
x
)
)
−
x
∣
∣
1
]
L_{cyc,X}=E_{x\sim p_{data}(x)}[||F(G(x))-x||_{1}]
Lcyc,X=Ex∼pdata(x)[∣∣F(G(x))−x∣∣1]
同样的进行分布
Y
Y
Y到
X
X
X转换:
L
G
A
N
(
F
,
D
X
,
X
,
Y
)
=
E
x
∼
p
d
a
t
a
(
x
)
[
l
o
g
D
X
(
x
)
]
+
E
y
∼
p
d
a
t
a
(
y
)
[
l
o
g
(
1
−
D
X
(
F
(
y
)
)
]
L_{GAN}(F,D_{X},X,Y)=E_{x\sim p_{data}(x)}[logD_{X}(x)]+E_{y\sim p_{data}(y)}[log(1-D_{X}(F(y))]
LGAN(F,DX,X,Y)=Ex∼pdata(x)[logDX(x)]+Ey∼pdata(y)[log(1−DX(F(y))]
L
c
y
c
,
Y
=
E
y
∼
p
d
a
t
a
(
y
)
[
∣
∣
G
(
F
(
y
)
)
−
y
∣
∣
1
]
L_{cyc,Y}=E_{y\sim p_{data}(y)}[||G(F(y))-y||_{1}]
Lcyc,Y=Ey∼pdata(y)[∣∣G(F(y))−y∣∣1]
总的目标函数为:
L
G
,
F
,
D
X
,
D
Y
=
L
G
A
N
(
G
,
D
Y
,
X
,
Y
)
+
L
G
A
N
(
F
,
D
X
,
X
,
Y
)
+
L
c
y
c
,
X
+
L
c
y
c
,
Y
L_{G,F,D_{X},D_{Y}}=L_{GAN}(G,D_{Y},X,Y)+L_{GAN}(F,D_{X},X,Y)+L_{cyc,X}+L_{cyc,Y}
LG,F,DX,DY=LGAN(G,DY,X,Y)+LGAN(F,DX,X,Y)+Lcyc,X+Lcyc,Y
根据该目标优化参数,从而使CycleGAN在无配对数据集上进行学习:
m
i
n
G
,
F
m
a
x
D
x
,
D
Y
L
G
,
F
,
D
X
,
D
Y
min_{G,F}max_{D_{x},D_{Y}}L_{G,F,D_{X},D_{Y}}
minG,FmaxDx,DYLG,F,DX,DY
从优化的目标公式中看出,应先训练判别器,再训练生成器;训练判别器时,应最大化目标函数,训练生成器时,应最小化目标函数;
下面是CycleGAN实现的图像迁移: