本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
🍊作者简介:秃头小苏,致力于用最通俗的语言描述问题
🍊往期回顾:对抗生成网络GAN系列——GAN原理及手写数字生成小案例
🍊近期目标:写好专栏的每一篇文章
🍊支持小苏:点赞👍🏼、收藏⭐、留言📩
对抗生成网络GAN系列——DCGAN简介及人脸图像生成案例
写在前面
前段时间,我已经写过一篇关于GAN的理论讲解,并且结合理论做了一个手写数字生成的小案例,对GAN原理不清楚的可以点击☞☞☞跳转了解详情。🌱🌱🌱
为唤醒大家的记忆,这里我再来用一句话对GAN的原理进行总结:GAN网络即是通过生成器和判别器的不断相互对抗,不断优化,直到判别器难以判断生成器生成图像的真假。
那么接下来我就要开始讲述DCGAN了喔,读到这里我就默认大家对GAN的原理已经掌握了,开始发车。🚖🚖🚖
DCGAN重点知识把握
DCGAN简介
我们先来看一下DCGAN的全称——Deep Convolutional Genrative Adversarial Networks
。这大家应该都能看懂叭,就是说这次我们将生成对抗网络和深度学习结合到一块儿了,现在看这篇文章的一些观点其实觉得是很平常的,没有特别出彩之处,但是这篇文章是在16年发布的,在当时能提出一些思想确实是难得。
其实呢,这篇文章的原理和GAN基本是一样的。不同之处只在生成网络模型和判别网络模型的搭建上,因为这篇文章结合了深度学习嘛,所以在模型搭建中使用了卷积操作【注:在上一篇GAN网络模型搭建中我们只使用的全连接层】。介于此,我不会再介绍DCGAN的原理,重点将放在DCGAN网络模型的搭建上。【注:这样看来DCGAN就很简单了,确实也是这样的。但是大家也不要掉以轻心喔,这里还是有一些细节的,我也是花了很长的时间来阅读文档和做实验来理解的,觉得理解差不多了,才来写了这篇文章。】
那么接下来就来讲讲DCGAN生成模型和判别模型的设计,跟我一起来看看叭!!!
DCGAN生成模型、判别模型设计✨✨✨
在具体到生成模型和判别模型的设计前,我们先来看论文中给出的一段话,如下图所示:
这里我还是翻译一下,如下图所示:
上图给出了设计生成模型和判别模型的基本准则,后文我们搭建模型时也是严格按照这个来的。【注意上图黄色背景的分数卷积喔,后文会详细叙述】
生成网络模型🧅🧅🧅
话不多说,直接放论文中生成网络结构图,如下:
图1 生成网络模型
看到这张图不知道大家是否有几秒的迟疑,反正我当时是这样的,这个结构给人一种熟悉的感觉,但又觉得非常的陌生。好了,不卖关子了,我们一般看到的卷积结构都是特征图的尺寸越来越小,是一个下采样的过程;而这个结构特征图的尺寸越来越大,是一个上采样的过程。那么这个上采样是怎么实现的呢,这就要说到主角分数卷积
了。【又可以叫转置卷积(transposed convolution)和反卷积(deconvolution),但是pytorch官方不建议取反卷积的名称,论文中更是说这个叫法是错误的,所以我们尽量不要去用反卷积这个名称,同时后文我会统一用转置卷积来表述,因为这个叫法最多,我认为也是最贴切的】
⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔ 转置卷积专场⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔
转置卷积理论📝📝📝
这里我将通过一个小例子来讲述转置卷积的步骤,并通过代码来验证这个步骤的正确性。首先我们先来看看转置卷积的步骤,如下:
- 在输入特征图元素间填充s-1行、列0(其中s表示转置卷积的步距,注意这里的步长s和卷积操作中的有些不同)
- 在输入特征图四周填充k-p-1行、列0(其中k表示转置卷积kernel_size大小,p为转置卷积的padding,注意这里的padding和卷积操作中的有些不同)
- 将卷积核参数上下、左右翻转
- 做正常卷积运算(padding=0,s=1)
是不是还是懵逼的状态呢,不用急,现在就通过一个例子来讲述这个过程。首先我们假设输入特征图的尺寸为2*2大小,s=2,k=3,p=0,如下图所示:
第一步我们需要在特征图元素间填充s-1=1 行、列 0 (即填充1行0,1列0),变换后特征图如下:
第二步我们需要在输入特征图四周填充k-p-1=2 行、列0(即填充2行0,2列0),变换后特征图如下:
第三步我们需要将卷积核上下、左右翻转,得到新的卷积核【卷积核尺寸为k=3】,卷积核变化过程如下:
最后一步,我们做正常的卷积即可【注:拿第二步得到的特征图和第三步翻转后得到的卷积核做正常卷积】,结果如下:
至此我们就从完成了转置卷积,从一个2*2大小的特征图变成了一个5*5大小的特征图,如下图所示(忽略了中间步骤):
为了让大家更直观的感受转置卷积的过程,我从Github
上down了一个此过程动态图供大家参考,如下:【注:需要动态图点击☞☞☞自取】
通过上文的讲述,相信你已经对转置卷积的步骤比较清楚了。这时候你就可以试试图1中结构,看看应用上述的方法能否得到对应的结构。需要注意的是,在第一次转置卷积时,使用的参数k=4,s=1,p=0
,后面的参数都为k=4,s=2,p=1
,如下图所示:
如果你按照我的步骤试了试,可能会发出一些吐槽,这也太麻烦了,我只想计算一下经过转置卷积后特征图的的变化,即知道输入特征图尺寸以及k、s、p算出输出特征图尺寸,这步骤也太复杂了。于是好奇有没有什么公式可以很方便的计算呢?enmmm,我这么说,那肯定有嘛,公式如下图所示:
对于上述公式我做3点说明:
- 在转置卷积的官方文档中,参数还有output_padding 和dilation参数也会影响输出特征图的大小,但这里我们没使用,公式就不加上这俩了,感兴趣的可以自己去阅读一下文档,写的很详细。🌵🌵🌵
- 对于stride[0],stride[1]、padding[0],padding[1]、kernel_size[0],kernel_size[1]该怎么理解?其实啊这些都是卷积的基本知识,这些参数设置时可以设置一个整数或者一个含两个整数的元组,*[0]表示在高度上进行操作,*[1]表示在宽度上进行操作。有关这部分在官方文档上也有写,大家可自行查看。为方便大家,我截了一下这部分的图片,如下:
- 这点我带大家宏观的理解一下这个公式,在传统卷积中,往往卷积核k越小、padding越大,得到的特征图尺寸越大;而在转置卷积中,从公式可以看出,卷积核k越大,padding越小,得到的特征图尺寸越大,关于这一点相信你也能从前文所述的转置卷积理论部分有所感受。🌿🌿🌿
现在有了这个公式,大家再去试试叭。
转置卷积实验📝📝📝
接下来我将通过一个小实验验证上面的过程,代码如下:
import torch
import torch.nn as nn
#转置卷积
def transposed_conv_official():
feature_map = torch.as_tensor([[1, 2],
[0, 1]], dtype=torch.float32).reshape([1, 1, 2, 2])
print(feature_map)
trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1,
kernel_size=3, stride=2, bias=False)
trans_conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 1],
[1, 1, 0],
[0, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(trans_conv.weight)
output = trans_conv(feature_map)
print(output)
def transposed_conv_self():
feature_map = torch.as_tensor([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 2, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=torch.float32).reshape([1, 1, 7, 7])
print(feature_map)
conv = nn.Conv2d(in_channels=1, out_channels=1,
kernel_size=3, stride=1, bias=False)
conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 0],
[0, 1, 1],
[1, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(conv.weight)
output = conv(feature_map)
print(output)
def main():
transposed_conv_official()
print("---------------")
transposed_conv_self()
if __name__ == '__main__':
main()
首先我们先通过transposed_conv_official()
函数来封装一个转置卷积过程,可以看到我们的输入为[[1,2],[0,1]],卷积核为[[1,0,1],[1,1,0],[0,0,1]],采用k=3,s=2,p=0进行转置卷积【注:这些参数和我前文讲解转置卷积步骤的用例参数是一致的】,我们来看一下程序输出的结果:可以发现程序输出和我们前面理论计算得到的结果是一致的。
接着我们封装了transposed_conv_self
函数,这个函数定义的是一个正常的卷积,输入是理论第2步得到的特征图,卷积核是第三步翻转后得到的卷积核,经过卷积后输出结果如下:结果和前面的一致。
那么通过这个例子就大致证明了转置卷积的步骤确实是我们理论步骤所述。
【呼~~这部分终于讲完了,其实我觉得转置卷积倒是这篇论文很核心的一个知识点,这部分参考链接如下:参考视频🥎🥎🥎】
⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔转置卷积专场⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔
判别模型网络🧅🧅🧅
同样的,直接放出判别模型的网络结构图,如下:【注:这部分原论文中没有给出图例,我自己简单画了一个,没有论文中图示美观,但也大致能表示卷积的过程,望大家见谅】
判别网络真的没什么好讲的,就是传统的卷积操作,对卷积不了解的建议阅读一下我的这篇文章🧨🧨🧨
这里我给出程序执行的网络模型结构的结果,这部分就结束了:
DCGAN人脸生成实战✨✨✨
这部分我们将来实现一个人脸生成的实战项目,我们先来看一下人脸一步步生成的动画效果,如下图所示:
我们可以看到随着迭代次数增加,人脸生成的效果是越来越好的,说句不怎么恰当的话,最后生成的图片是像个人的。看到这里,是不是都兴致勃勃了呢,下面就让我们一起来学学叭。🏆🏆🏆
秉持着授人以鱼不如授人以渔的原则,这里我就不带大家一句一句的分析代码了,都是比较简单的,官方文档写的也非常详细,我再叙述一篇也没有什么意义。哦,对了,这部分代码参考的是pytorch官网上DCGAN的教程,链接如下:DCGAN实战教程🎈🎈🎈
我来简单介绍一下官方教程的使用,点击上文链接会进入下图的界面:这个界面正常滑动就是对这个项目的解释,包括原理、代码及代码运行结果,大家首先要做的应该是阅读一遍这个文档,基本可以解决大部分的问题。那么接下来对于不明白的就可以点击下图中绿框链接修改一些代码来调试我们不懂的问题,这样基本就都会明白了。【框1是google提供的一个免费的GPU运算平台,就类似是云端的jupyter notebook
,但这个需要梯子,大家自备;框2 是下载notebook到本地;框3是项目的Github地址】
那方法都教给大家了,大家快去试试叭!!!
作为一个负责的博主👨🦳👨🦳👨🦳,当然不会就甩一个链接就走人啦,下面我会帮助大家排查一下代码中的一些难点,大家看完官方文档后如果有不明白的记得回来看看喔。🥂🥂🥂当然,如果有什么不理解的地方且我下文没有提及欢迎评论区讨论交流。🛠🛠🛠
数据集加载🧅🧅🧅
首先我来说一下数据集的加载,这部分不难,却十分重要。对于我们自己的数据集,我们先用ImageFolder
方法创建dataset
,代码如下:
# Create the dataset
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)),
]))
需要强调的是root=dataroot
表示我们自己数据集的路径,在这个路径下必须还有一个子目录。怎么理解呢,我举个例子。比如我现在有一个人脸图片数据集,其存放在文件夹2下面,我们不能将root的路径指定为文件夹2,而是将文件夹2放入一个新文件夹1里面,root的路径指定为文件夹1。
对于上面代码的transforms操作做一个简要的概括,transforms.Resize
将图片尺寸进行缩放、transforms.CenterCrop
对图片进行中心裁剪、transforms.ToTensor、transforms.Normalize
最终会将图片数据归一化到[-1,1]之间,这部分不懂的可以参考我的这篇博文:pytorch中的transforms.ToTensor和transforms.Normalize理解🍚🍚🍚
有了dataset
后,就可以通过DataLoader
方法来加载数据集了,代码如下:
# Create the dataloader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=True, num_workers=workers)
生成模型搭建🧅🧅🧅
接下来我们来说说生成网络模型的搭建,代码如下:不知道大家有没有发现pytorch官网此部分搭建的网络模型和论文中给出的是有一点差别的,这里我修改成了和论文中一样的模型,从训练效果来看,两者差别是不大的。【注:下面代码是我修改过的】
# Generator Code
class Generator(nn.Module):
def __init__(self, ngpu):
super(Generator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( nz, ngf * 16, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 16),
nn.ReLU(True),
# state size. (ngf*16) x 4 x 4
nn.ConvTranspose2d(ngf * 16, ngf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# state size. (ngf*8) x 8 x 8
nn.ConvTranspose2d( ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# state size. (ngf*4) x 16 x 16
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# state size. (ngf * 2) x 32 x 32
nn.ConvTranspose2d( ngf * 2, nc, 4, 2, 1, bias=False),
nn.Tanh()
# state size. (nc) x 64 x 64
)
def forward(self, input):
return self.main(input)
我觉得这个模型搭建步骤大家应该都是较为清楚的,但我当时对这个第一步即从一个100维的噪声向量如何变成变成一个1024*4*4的特征图还是比较疑惑的。这里就为大家解答一下,我们可以看看在训练过程中传入的噪声代码,即输入为: noise = torch.randn(b_size, nz, 1, 1, device=device)
,这是一个100*1*1的特征图,这样是不是一下子恍然大悟了呢,那我们的第一步也就是从100*1*1的特征图经转置卷积变成1024*4*4的特征图。
模型训练🧅🧅🧅
这部分我在上一篇GAN网络讲解中已经介绍过,但是我没有细讲,这里我想重点讲一下BCELOSS损失函数。【就是二值交叉熵损失函数啦】我们先来看一下pytorch官网对这个函数的解释,如下图所示:
其中N表示batch_size,wnw_n应该表示一个权重系数,默认为1【这个是我猜的哈,在官网没看到对这一部分的解释】,yny_n表示标签值,xnx_n表示数据。我们会对每个batch_size的数据都计算一个lnl_n ,最后求平均或求和。【默认求均值】
看到这里大家可能还是一知半解,不用担心,我举一个小例子大家就明白了。首先我们初始化一些输入数据和标签:
import torch
import math
input = torch.randn(3,3)
target = torch.FloatTensor([[0, 1, 1], [1, 1, 0], [0, 0, 0]])
来看看输入数据和标签的结果:
接着我们要让输入数据经过Sigmoid函数将其归一化到[0,1]之间【BCELOSS函数要求】:
m = torch.nn.Sigmoid()
m(input)
输出的结果如下:
最后我们就可以使用BCELOSS函数计算输入数据和标签的损失了:
loss =torch.nn.BCELoss()
loss(m(input), target)
输出结果如下:
大家记住这个值喔!!!
上文似乎只是介绍了BCELOSS怎么用,具体怎么算的好像并不清楚,下面我们就根据官方给的公式来一步一步手动计算这个损失,看看结果和调用函数是否一致,如下:
r11 = 0 * math.log(0.8172) + (1-0) * math.log(1-0.8172)
r12 = 1 * math.log(0.8648) + (1-1) * math.log(1-0.8648)
r13 = 1 * math.log(0.4122) + (1-1) * math.log(1-0.4122)
r21 = 1 * math.log(0.3266) + (1-1) * math.log(1-0.3266)
r22 = 1 * math.log(0.6902) + (1-1) * math.log(1-0.6902)
r23 = 0 * math.log(0.5620) + (1-0) * math.log(1-0.5620)
r31 = 0 * math.log(0.2024) + (1-0) * math.log(1-0.2024)
r32 = 0 * math.log(0.2884) + (1-0) * math.log(1-0.2884)
r33 = 0 * math.log(0.5554) + (1-0) * math.log(1-0.5554)
BCELOSS = -(1/9) * (r11 + r12+ r13 + r21 + r22 + r23 + r31 + r32 + r33)
来看看结果叭:
你会发现调用BCELOSS
函数和手动计算的结果是一致的,只是精度上有差别,这说明我们前面所说的理论公式是正确的。【注:官方还提供了一种函数——BCEWithLogitsLoss
,其和BCELOSS
大致一样,只是对输入的数据不需要再调用Sigmoid函数将其归一化到[0,1]之间,感兴趣的可以阅读看看】
这个损失函数讲完训练部分就真没什么可讲的了,哦,这里得提一下,在计算生成器的损失时,我们不是最小化log(1−D(G(Z)))log(1-D(G(Z))) ,而是最大化logD(G(z))logD(G(z)) 。这个在GAN网络论文中也有提及,我上一篇没有说明这点,这里说声抱歉,论文中说是这样会更好的收敛,这里大家注意一下就好。
番外篇——使用服务器训练如何保存图片和训练损失✨✨✨
不知道大家运行这个代码有没有遇到这样尬尴的处境:
- 无法科学上网,用不了google提供的免费GPU
- 自己电脑没有GPU,这个模型很难跑完
- 有服务器,但是官方提供的代码并没有保存最后生成的图片和损失,自己又不会改
前两个我没法帮大家解决,那么我就来说说怎么来保存图片和训练损失。首先来说说怎么保存图片,这个就很简单啦,就使用一个save_image
函数即可,具体如下图所示:【在训练部分添加】
接下来说说怎么保存训练损失,通过torch.save()
方法保存代码如下:
#保存LOSS
G_losses = torch.tensor(G_losses)
D_losses = torch.tensor(D_losses)
torch.save(G_losses, 'LOSS\GL')
torch.save(D_losses, 'LOSS\DL')
代码执行完后,损失保存在LOSS文件夹下,一个文件为GL,一个为DL。这时候我们需要创建一个.py文件
来加载损失并可视化,.py
文件内容如下:
import torch
import torch.utils.data
import matplotlib.pyplot as plt
#绘制LOSS曲线
G_losses = torch.load('F:\老师发放论文\经典网络模型\GAN系列\DCGAN\LOSS\GL')
D_losses = torch.load('F:\老师发放论文\经典网络模型\GAN系列\DCGAN\LOSS\DL')
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()
最后来看看保存的图片和损失,如下图所示:
小结
至此,DCGAN就全部讲完啦,希望大家都能有所收获。有什么问题欢迎评论区讨论交流!!!GAN系列近期还会出cycleGAN的讲解和四季风格转换的demo,后期会考虑出瑕疵检测方面的GAN网络,如AnoGAN等等,敬请期待。🏵🏵🏵
如若文章对你有所帮助,那就🛴🛴🛴