【生成对抗网络系列】二、DCGAN
目录
- 参考资料
- 第1章 前言
- 第2章 转置卷积(Transposed Convolution)
- 2.1 前言
- 2.2 卷积
- 2.3 转置卷积操作
- 2.4 Pytorch中的转置卷积
- 第3章 DCGAN网络架构
- 3.1 网络改进
- 3.2 训练参数
- 第4章 Pytorch实现DCGAN
- 4.1 生成器Generator
- 4.2 判别器Discriminator
- 4.3 完整代码
参考资料
论文:
Deep Convolution Generative Adversarial Networks
博客:
GANs系列:DCGAN原理简介与基础GAN的区别对比
【生成对抗网络】Deep Convolution GAN (DCGAN) 详细解读
基于pytorch的DCGAN代码实现(DCGAN基本原理+代码讲解)
torch学习 (三十七):DCGAN详解
DCGAN pytorch代码实现及代码详解
第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
参考:
PyTorch-GAN
首先附上一张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= 8self.init_size = opt.img_size // 4self.l1 = nn.Sequential(# opt.latent_dim是噪声维度,值为100# 128 * self.init_size ** 2 = 128x64 = 8192nn.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 blockself.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 = 2ds_size = opt.img_size // 2 ** 4self.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 tqdmimport torchvision.transforms as transforms
from torchvision.utils import save_imagefrom torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variableimport torch.nn as nn
import torch.nn.functional as F
import torchos.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中检索到了Convtorch.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= 8self.init_size = opt.img_size // 4self.l1 = nn.Sequential(# opt.latent_dim是噪声维度,值为100# 128 * self.init_size ** 2 = 128x64 = 8192nn.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 blockself.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 = 2ds_size = opt.img_size // 2 ** 4self.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,假图片标签为0valid = 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 inputreal_imgs = Variable(imgs.type(Tensor))# -----------------# 训练 Generator# -----------------# 梯度归零optimizer_G.zero_grad()# 产生输入噪声,维度为latent_dimz = 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) + iif batches_done % opt.sample_interval == 0:save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
