目录
参考资料
论文:
Deep Convolution Generative Adversarial Networks
博客:
【生成对抗网络】Deep Convolution GAN (DCGAN) 详细解读
基于pytorch的DCGAN代码实现(DCGAN基本原理+代码讲解)
第1章 前言
DCGAN
全称为:Deep Convolution Generative Adversarial Networks(深度卷积生成对抗网络),DCGAN将深度卷积神经网络CNN与生成对抗网络GAN结合用于无监督学习领域。
DCGAN的出发点并不是更改损失函数或者是对GAN的原理进行剖析,而是直接的对网络结构施加限制从而实现更为强大的生成模型,所以我们可以直接将DCGAN的设计方案嵌入到自己的GAN网络中。
第2章 转置卷积(Transposed Convolution)
2.1 前言
转置卷积(Transposed Convolution) 在语义分割或者对抗神经网络(GAN)中比较常见,其主要作用就是做上采样(UpSampling)。
在有些地方转置卷积又被称作
fractionally-strided convolution
或者deconvolution
,但deconvolution
具有误导性,不建议使用。
对于转置卷积需要注意的是:
- 转置卷积不是卷积的逆运算!
- 转置卷积也是卷积;
讲解视频参考:转置卷积(transposed convolution)
讲解博客参考:转置卷积(Transposed Convolution)
2.2 卷积
首先回顾下普通卷积,下图以stride=1,padding=0,kernel_size=3为例,假设输入特征图大小是4x4的(假设输入输出都是单通道),通过卷积后得到的特征图大小为2x2。
一般使用卷积的情况中,要么特征图变小(stride > 1),要么保持不变(stride = 1),当然也可以通过四周padding让特征图变大但没有意义。
关于卷积的详细介绍可以参考博文。
2.3 转置卷积操作
转置卷积主要作用就是起到 上采样
的作用。但转置卷积不是卷积的逆运算(一般卷积操作是不可逆的),它只能恢复到原来的大小(shape/size),数值与原来不同。转置卷积的运算步骤可以归为以下几步:
- (1)在输入特征图元素间填充
s-1
行、列0(其中s表示转置卷积的步距); - (2)在输入特征图四周填充
k-p-1
行、列0(其中k表示转置卷积的kernel_size大小,p为转置卷积的padding,注意这里的padding和卷积操作中有些不同); - (3)将卷积核参数
上下、左右翻转
; - (4)做正常卷积运算(填充0,步距1);
下面假设输入的特征图大小为2x2(假设输入输出都为单通道),通过转置卷积后得到4x4大小的特征图。这里使用的转置卷积核大小为k=3,stride=1,padding=0的情况(忽略偏执bias)。
- 首先在元素间填充s-1=0行、列0(等于0不用填充);
- 然后在特征图四周填充k-p-1=2行、列0;
- 接着对卷积核参数进行上下、左右翻转;
- 最后做正常卷积(填充0,步距1);
下图展示了转置卷积中不同 s
和 p
的情况:
转置卷积操作后特征图的大小可以通过如下公式计算:
H
o
u
t
=
(
H
i
n
−
1
)
×
s
t
r
i
d
e
[
0
]
−
2
×
p
a
d
d
i
n
g
[
0
]
+
k
e
r
n
e
l
_
s
i
z
e
[
0
]
W
o
u
t
=
(
W
i
n
−
1
)
×
s
t
r
i
d
e
[
1
]
−
2
×
p
a
d
d
i
n
g
[
1
]
+
k
e
r
n
e
l
_
s
i
z
e
[
1
]
H_{out}=(H_{in}−1)×stride[0]−2×padding[0]+kernel\_size[0] \\ W_{out}=(W_{in}−1)×stride[1]−2×padding[1]+kernel\_size[1]
Hout=(Hin−1)×stride[0]−2×padding[0]+kernel_size[0]Wout=(Win−1)×stride[1]−2×padding[1]+kernel_size[1]
其中stride[0]表示高度方向的stride,padding[0]表示高度方向的padding,kernel_size[0]表示高度方向的kernel_size,索引[1]都表示宽度方向上的。
通过上面公式可以看出padding越大,输出的特征矩阵高、宽越小,你可以理解为正向卷积过程中进行了padding然后得到了特征图,现在使用转置卷积还原到原来高、宽后要把之前的padding减掉。
2.4 Pytorch中的转置卷积
Pytorch官方关于转置卷积ConvTranspose2d
的文档
官方对转置卷积使用到的参数介绍:
上面讲的例子中已经介绍了in_channels, out_channels, kernel_size, stride, padding
这几个参数了,在官方提供的方法中还有:
output_padding
:在计算得到的输出特征图的高、宽方向各填充几行或列0(注意,这里只是在上下以及左右的一侧one side
填充,并不是两侧都填充,有兴趣自己做个实验看下),默认为0不使用。groups
:当使用到组卷积时才会用到的参数,默认为1即普通卷积。bias
:是否使用偏执bias,默认为True使用。dilation
:当使用到空洞卷积(膨胀卷积)时才会使用到的参数,默认为1即普通卷积。
输出特征图宽、高计算:
- Input: ( N , C i n , H i n , W i n ) o r ( C i n , H i n , W i n ) (N, C_{in}, H_{in}, W_{in})\ or\ (C_{in}, H_{in}, W_{in}) (N,Cin,Hin,Win) or (Cin,Hin,Win)
- Output: ( N , C o u t , H o u t , W o u t ) o r ( C o u t , H o u t , W o u t ) (N, C_{out}, H_{out}, W_{out})\ or\ (C_{out}, H_{out}, W_{out}) (N,Cout,Hout,Wout) or (Cout,Hout,Wout)
H o u t = ( H i n − 1 ) × s t r i d e [ 0 ] − 2 × p a d d i n g [ 0 ] + d i l a t i o n [ 0 ] × ( k e r n e l _ s i z e [ 0 ] − 1 ) + o u t p u t _ p a d d i n g [ 0 ] + 1 W o u t = ( W i n − 1 ) × s t r i d e [ 1 ] − 2 × p a d d i n g [ 1 ] + d i l a t i o n [ 1 ] × ( k e r n e l _ s i z e [ 1 ] − 1 ) + o u t p u t _ p a d d i n g [ 1 ] + 1 H_{out}=(H_{in}−1)×stride[0]−2×padding[0]+dilation[0]×(kernel\_size[0]−1)+output\_padding[0]+1 \\ W_{out}=(W_{in}-1)×stride[1]−2×padding[1]+dilation[1]×(kernel\_size[1]−1)+output\_padding[1]+1 Hout=(Hin−1)×stride[0]−2×padding[0]+dilation[0]×(kernel_size[0]−1)+output_padding[0]+1Wout=(Win−1)×stride[1]−2×padding[1]+dilation[1]×(kernel_size[1]−1)+output_padding[1]+1
第3章 DCGAN网络架构
3.1 网络改进
DCGAN主要是在网络架构上改进了原始GAN,DCGAN的生成器与判别器都利用CNN架构替换了原始GAN的全连接网络,主要改进之处有如下几个方面:
-
(1)全卷积网络(all convolutional net):用步幅卷积(strided convolutions)替代确定性空间池化函数(deterministic spatial pooling functions)(比如最大池化),让网络自己学习下采样方式。作者对
Generator
和Discriminator
都采用了这种方法。 -
(2)取消全连接层: 比如,使用
全局平均池化(global average pooling)
替代fully connected layer
。GAP
会降低收敛速度,但是可以提高模型的稳定性。 -
(3)批归一化(Batch Normalization):
BN
被证明是深度学习中非常重要的 加速收敛 和 减缓过拟合 的手段,这样有助于解决poor initialization
问题并帮助梯度流向更深的网络。
但是实践表明,将所有层都进行Batch Normalization,会导致样本震荡和模型不稳定,因此 只对生成器(G)的输出层和鉴别器(D)的输入层使用BN。
- (4)在
Generator
中除输出层使用Tanh
激活函数,其余层全部使用ReLu
激活函数。 - (5)在
Discriminator
所有层都使用LeakyReLU
激活函数。
概括来说,DCGAN的主要的tricks如下图所示:
原论文中的DCGAN生成器 G
的结构如下图所示:
原文中生成器的输入是一个 100 100 100 维的噪声,中间会通过 4 4 4 层卷积层(转置卷积),每通过一个卷积层通道数减半,长宽扩大一倍 ,最终产生一个 64 × 64 × 3 64\times64\times3 64×64×3 大小的图片输出。
3.2 训练参数
- 所有模型均采用小批量随机梯度下降(SGD)训练,Batch_size大小为128。
- 所有权重均根据零中心正态分布进行初始化,标准偏差为0.02。
- 在LeakyReLU中,斜率均设置为0.2。
- 使用了Adam优化器并调整超参数。基础GAN中建议的学习率0.001太高了,改为使用0.0002。
- 此外,将动量项β1保留在建议值0.9会导致训练振荡和不稳定性,而将其降低到0.5有助于稳定训练。
第4章 Pytorch实现DCGAN
参考:
首先附上一张GAN网络的结构图,DCGAN结构也一样,只是内部细节不一样。
DCGAN的结构如下:
4.1 生成器Generator
这里使用的是手写数据集Mnist,图片大小resize成 32 × 32 × 1 32\times32\times1 32×32×1,DCGAN的生成器部分采用CNN架构,并且使用转置卷积,输入噪声为100维的向量,输出为 32 × 32 × 1 32\times32\times1 32×32×1 的图片,具体网络结构如下所示:
对照着代码看比较清晰:
"""
1.生成器Generator
"""
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# Python中两个斜杠即双斜杠(//)表示地板除,即先做除法(/),然后向下取整(floor)
# opt.img_size = 32
# init_size= 8
self.init_size = opt.img_size // 4
self.l1 = nn.Sequential(
# opt.latent_dim是噪声维度,值为100
# 128 * self.init_size ** 2 = 128x64 = 8192
nn.Linear(opt.latent_dim, 128 * self.init_size ** 2),
)
self.conv_blocks = nn.Sequential(
nn.BatchNorm2d(128),
# (128, 8, 8) -> (128, 16, 16)
nn.Upsample(scale_factor=2), # 上采样,将图片放大两倍
# (128, 16, 16) -> (128, 16, 16)
nn.Conv2d(128, 128, 3, stride=1, padding=1),
nn.BatchNorm2d(128, 0.8),
nn.LeakyReLU(0.2, inplace=True),
# (128, 16, 16) -> (128, 32, 32)
nn.Upsample(scale_factor=2),
# (128, 32, 32) -> (64, 32, 32)
nn.Conv2d(128, 64, 3, stride=1, padding=1),
nn.BatchNorm2d(64, 0.8),
nn.LeakyReLU(0.2, inplace=True),
# (64, 32, 32) -> (1, 32, 32)
nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
nn.Tanh(),
)
def forward(self, z):
# l1函数进行的是Linear变换
out = self.l1(z)
# view是维度变换函数,可以看到out数据变成了四维数据:
# (batch,8192) -> (batch, 128, 8, 8)
out = out.view(out.shape[0], 128, self.init_size, self.init_size)
img = self.conv_blocks(out)
return img
4.2 判别器Discriminator
"""
2.判别器Discriminator
"""
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
def discriminator_block(in_filters, out_filters, bn=True):
# Conv卷积,Relu激活,Dropout将部分神经元失活,进而防止过拟合
block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout2d(0.25)]
# 如果bn这个参数为True,那么就需要在block块里面添加上BatchNorm的归一化函数
if bn:
block.append(nn.BatchNorm2d(out_filters, 0.8))
return block
self.model = nn.Sequential(
# (batch, 1, 32, 32) -> (batch, 16, 16, 16),输入层不使用BN
*discriminator_block(opt.channels, 16, bn=False),
# (batch, 16, 16, 16) -> (batch, 32, 8, 8)
*discriminator_block(16, 32),
# (batch, 32, 8, 8) -> (batch, 64, 4, 4)
*discriminator_block(32, 64),
# (batch, 64, 4, 4) -> (batch, 128, 2, 2)
*discriminator_block(64, 128)
)
# opt.img_size = 32
# ds_size= 32 / 16 = 2
ds_size = opt.img_size // 2 ** 4
self.adv_layer = nn.Sequential(
nn.Linear(128 * ds_size ** 2, 1), # (batch, 512) -> (batch, 1)
nn.Sigmoid())
def forward(self, img):
out = self.model(img) # (batch, 1, 32, 32) -> (batch, 128, 2, 2)
out = out.view(out.shape[0], -1) # (batch, 128, 2, 2) -> (batch, 512)
validity = self.adv_layer(out) # (batch, 512) -> (batch, 1)
return validity
4.3 完整代码
import argparse
import os
import numpy as np
from tqdm.autonotebook import tqdm
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch
os.makedirs("images", exist_ok=True)
# 命令行选项、参数和子命令解析器
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=200, help="迭代次数")
parser.add_argument("--batch_size", type=int, default=64, help="batch大小")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: 学习率")
parser.add_argument("--b1", type=float, default=0.5, help="adam: 动量梯度下降第一个参数")
parser.add_argument("--b2", type=float, default=0.999, help="adam: 动量梯度下降第二个参数")
parser.add_argument("--n_cpu", type=int, default=8, help="CPU个数")
parser.add_argument("--latent_dim", type=int, default=100, help="噪声数据生成维度")
parser.add_argument("--img_size", type=int, default=32, help="输入数据的维度")
parser.add_argument("--channels", type=int, default=1, help="输出数据的通道数")
parser.add_argument("--sample_interval", type=int, default=400, help="保存图像的迭代数")
opt = parser.parse_args()
print(opt)
# 判断GPU可用,有GPU用GPU,没有用CPU
cuda = True if torch.cuda.is_available() else False
"""
自定义初始化参数
"""
def weights_init_normal(m):
classname = m.__class__.__name__ # 获得类名
if classname.find("Conv") != -1: # 在类classname中检索到了Conv
torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find("BatchNorm2d") != -1:
torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
torch.nn.init.constant_(m.bias.data, 0.0)
"""
1.生成器Generator
"""
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# Python中两个斜杠即双斜杠(//)表示地板除,即先做除法(/),然后向下取整(floor)
# opt.img_size = 32
# init_size= 8
self.init_size = opt.img_size // 4
self.l1 = nn.Sequential(
# opt.latent_dim是噪声维度,值为100
# 128 * self.init_size ** 2 = 128x64 = 8192
nn.Linear(opt.latent_dim, 128 * self.init_size ** 2),
)
self.conv_blocks = nn.Sequential(
nn.BatchNorm2d(128),
# (128, 8, 8) -> (128, 16, 16)
nn.Upsample(scale_factor=2), # 上采样,将图片放大两倍
# (128, 16, 16) -> (128, 16, 16)
nn.Conv2d(128, 128, 3, stride=1, padding=1),
nn.BatchNorm2d(128, 0.8),
nn.LeakyReLU(0.2, inplace=True),
# (128, 16, 16) -> (128, 32, 32)
nn.Upsample(scale_factor=2),
# (128, 32, 32) -> (64, 32, 32)
nn.Conv2d(128, 64, 3, stride=1, padding=1),
nn.BatchNorm2d(64, 0.8),
nn.LeakyReLU(0.2, inplace=True),
# (64, 32, 32) -> (1, 32, 32)
nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
nn.Tanh(),
)
def forward(self, z):
# l1函数进行的是Linear变换
out = self.l1(z)
# view是维度变换函数,可以看到out数据变成了四维数据:
# (batch,8192) -> (batch, 128, 8, 8)
out = out.view(out.shape[0], 128, self.init_size, self.init_size)
img = self.conv_blocks(out)
return img
"""
2.判别器Discriminator
"""
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
def discriminator_block(in_filters, out_filters, bn=True):
# Conv卷积,Relu激活,Dropout将部分神经元失活,进而防止过拟合
block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout2d(0.25)]
# 如果bn这个参数为True,那么就需要在block块里面添加上BatchNorm的归一化函数
if bn:
block.append(nn.BatchNorm2d(out_filters, 0.8))
return block
self.model = nn.Sequential(
# (batch, 1, 32, 32) -> (batch, 16, 16, 16),输入层不使用BN
*discriminator_block(opt.channels, 16, bn=False),
# (batch, 16, 16, 16) -> (batch, 32, 8, 8)
*discriminator_block(16, 32),
# (batch, 32, 8, 8) -> (batch, 64, 4, 4)
*discriminator_block(32, 64),
# (batch, 64, 4, 4) -> (batch, 128, 2, 2)
*discriminator_block(64, 128)
)
# opt.img_size = 32
# ds_size= 32 / 16 = 2
ds_size = opt.img_size // 2 ** 4
self.adv_layer = nn.Sequential(
nn.Linear(128 * ds_size ** 2, 1), # (batch, 512) -> (batch, 1)
nn.Sigmoid())
def forward(self, img):
out = self.model(img) # (batch, 1, 32, 32) -> (batch, 128, 2, 2)
out = out.view(out.shape[0], -1) # (batch, 128, 2, 2) -> (batch, 512)
validity = self.adv_layer(out) # (batch, 512) -> (batch, 1)
return validity
# BCE损失函数
adversarial_loss = torch.nn.BCELoss()
# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()
if cuda: #初始化,将数据放在cuda上
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
# Initialize weights
generator.apply(weights_init_normal)
discriminator.apply(weights_init_normal)
# Configure data loader
os.makedirs("../../data/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader( #显卡加速
datasets.MNIST(
"../../data/mnist", #进行训练集下载
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
),
),
batch_size=opt.batch_size,
shuffle=True,
)
# 定义神经网络的优化器
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
# ----------
# Training
# ----------
for epoch in range(1, opt.n_epochs):
loop = tqdm(dataloader, colour='red', unit='img')
for i, (imgs, _) in enumerate(loop):
# Adversarial ground truths
# 因为 batch 是64,所以在这里 imgs.size(0) = 64
# 这两句话本质就是为64张真图片和64张假图片打上标签,真图片标签为1,假图片标签为0
valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
# Configure input
real_imgs = Variable(imgs.type(Tensor))
# -----------------
# 训练 Generator
# -----------------
# 梯度归零
optimizer_G.zero_grad()
# 产生输入噪声,维度为latent_dim
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
# 把噪声输入生成器,生成fake图像
gen_imgs = generator(z)
# 计算生成器的损失
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward() # 反向传播
optimizer_G.step() # 更新参数
# ---------------------
# 训练 Discriminator
# ---------------------
optimizer_D.zero_grad()
# 估计判别器的判别能力
real_loss = adversarial_loss(discriminator(real_imgs), valid) # 判别真实数据时的损失
fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake) # 判别假数据时的损失
d_loss = (real_loss + fake_loss) / 2 # 做平均
d_loss.backward()
optimizer_D.step()
# print(
# "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
# % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
# )
# 进度条参数
loop.set_description(f"Epoch [{epoch}/{opt.n_epochs}]")
loop.set_postfix(D_loss=d_loss.item(), G_loss=g_loss.item())
batches_done = epoch * len(dataloader) + i
if batches_done % opt.sample_interval == 0:
save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)