Pytorch 使用DCGAN实现二次元人物头像生成(实现代码+公式推导)
GAN介绍
算法主体
推导证明(之后将补全完整过程)
随机梯度下降训练D,G
DCGAN介绍及相关原理
Pytorch实现二次元人物头像生成
如何使用GAN生成二次元头像
数据准备
代码实现
判别、生成模型均一轮迭代
判别每一轮迭代,生成模型每五轮迭代
图片生成
总结
本篇文章主要是关于李宏毅教授授课视频中的作业进行介绍,小白博主所作工作只是将现有的知识内容结合网路上一些优秀作者的总结以博文的形式加上自己的理解复述一遍。本文主要还是我的学习总结,因为网上的一些知识分布比较零散故作整理叙述。如果有不对的地方,还请帮忙指正,如有出现禁止转载的图片,文字内容请联系我修改。
相关参考:
李宏毅2020机器学习深度学习(完整版)国语:链接: link.
何之源:GAN学习指南:从原理入门到制作生成Demo链接: link
张先生-您好.Pytorch实现GAN 生成动漫头像 链接: link.
Dean0Winchester.深度学习—图像卷积与反卷积(最完美的解释)链接: link.
GAN介绍
生成对抗网络( G A N GAN GAN, G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Generative Adversarial Networks GenerativeAdversarialNetworks )是深度学习中一种无监督(即不需要给样本数据打标签)的学习方法, G A N GAN GAN的核心思想源自博弈论中的纳什均衡,博弈的双方分别是 G e n e r a t o Generato Generator(负责生成图片的生成器 G G G)和 D i s c r i m i n a t o r Discriminator Discriminator(负责判断图片真假的判别器 D D D)。 D D D的目的在于揪出由 G G G “伪造”的“假”图片给它打低分, G G G的目的在于尽最大可能地模仿真图片从而“欺骗” D D D获取高分。
参与这场游戏双方再不断地较量中完成自身地优化,从而实现了各自判别能力和生成能力的提升,直到双方达到一种动态的平衡。
如上图所示,蝴蝶扮演 G A N GAN GAN中的生成器,而波波鸟则扮演判别器,蝴蝶为了逃避波波鸟(判别器)的捕食(识别)需要不断的朝着树叶(真样本)进化,而波波鸟为了能够捕食(识别)出蝴蝶(用假样本伪装自己的生成器)也需要不断进化,比方说,最开始波波鸟理解的叶子(真样本)只停留在不是彩色这一层面上,随着训练,波波鸟的认知开始进化,从不是彩色转向棕色是叶子(真样本),而蝴蝶得到反馈之后为了继续欺骗波波鸟开始学会让生成的翅膀是棕色。
这个过程将反复进行下去,最后可以达到,波波鸟可以真正认出叶子,蝴蝶可以让生成的翅膀(假样本)与叶子(真样本)近乎一致为止。
算法主体
算法总流程:
G A N GAN GAN算法简单来说,就是固定生成器,训练判别器;然后固定训练好的判别器,再训练生成器的反复过程。
逐条解读算法流程:
首先初始化 D D D, G G G的参数,先固定生成器,进入判别器的学习过程。
判别器学习过程
-
采样 :从数据集中抽样出 m m m张样本,同时借助某种分布(通常使用正态分布)生成 m m m个 n n n维噪声样本。
-
获取假样本:将噪声样本作为输入,得到生成器输出的假样本。
-
判别器D的目标函数 :直观上来看,我们要达到的目的,就是让判别器对生成器生成的假样本尽可能地严格,对数据集抽样出来的真样本尽可能地宽松,那么我们可以构建这样一个目标函数,让判别器给真样本尽可能高的分数,给假样本尽可能低的分数。
m a x D max_D maxD v ∗ v^* v∗ = 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} ∑i=1m l o g log log( D D D( x i x^i xi)) + + + 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} ∑i=1m l o g log log( D D D(1 − - − x ∗ i x*^i x∗i))
D D D( x x x)代表 x x x为真实图片的概率,如果为 1 1 1,就代表100%是真实的图片,而输出为 0 0 0,就代表不可能是真实的图片。代码实现中通常借由 s i g m o i d sigmoid sigmoid函数来表达
要让 l o g log log( D D D( x ∗ i x*^i x∗i))尽可能小,等效于让 l o g log log( D D D(1 − - − x ∗ i x*^i x∗i))尽可能大。以此为我们的目的函数进行求导做梯度上升(亦可加负号求 m i n min min做梯度下降)从而更新判别器参数。
生成器学习过程 -
噪声采样 :借助同种分布(通常使用正态分布)生成另外 m m m个 n n n维噪声样本。
-
生成器G的目标函数:
v ∗ v^* v∗ = 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} ∑i=1m l o g log log( D D D( G G G( z i z^i zi)))
先直观的理解为什么要这样定义 G G G目标函数,根据之前的分析,要让生成器生成的图片足够接近真样本,那么就需要骗过判别器,从而我们可以让最大化生成图片在 D D D的打分为目标来引导我们的生成器完成参数迭代。
在最理想的状态下, G G G可以生成足以“以假乱真”的图片 G G G( z z z)。对于 D D D来说,它难以判定 G G G生成的图片究竟是不是真实的,因此 D D D( G G G( z z z)) = 0.5 0.5 0.5
推导证明
说了那么多,那么我们如何用一条数学语言来描述 G A N GAN GAN的核心原理呢?
G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Generative Adversarial Networks GenerativeAdversarialNetworks 的作者 I a n Ian Ian G o o d f e l l o w Goodfellow Goodfellow大佬给出了答案:
分析这个公式:
1.整个式子由两项构成。 x x x表示真实图片, z z z表示输入 G G G网络的噪声,而 G G G( z z z)表示 G G G网络生成的图片。
2. D D D( x x x)表示 D D D网络判断真实图片是否真实的概率(因为 x x x就是真实的,所以对于 D D D来说,这个值越接近 1 1 1越好)。而 D D D( G G G( z z z))是 D D D网络判断 G G G生成的图片的是否真实的概率。
3. D D D的目的: D D D的能力越强, D D D( x x x)应该越大, D D D( G G G( x x x))应该越小。这时 V V V( D D D, G G G)会变大。因此式子对于 D D D来说是求最大( m a x max max D D D)
4. G G G的目的: 上面提到过, D D D( G G G( z z z))是 D D D网络判断 G G G生成的图片是否真实的概率, G G G应该希望自己生成的图片“越接近真实越好”。也就是说,(实际上,我们不是同时训练两者,当 D D D训练好时,第一项好样本的得分期望是定值), G G G希望 D D D( G G G( z z z))尽可能得大,那么 V V V( D D D, G G G)会变小。因此我们看到式子的最前面的记号是 m i n min min G G G。
5. 为什么是期望: 我们假设我们取出的样本的统计特性可以代表整个数据分布(包括数据集数据分布和 G G G生成器生成数据的分布),那么我们之前表示的均值可以近似等价于各自数据分布的期望。
我们可以用张形象的图来描述我们是如何选择我们的生成器和判别器的。
我们选取判别器的标准,是取 m a x max max V V V( D D D, G G G),那么对应到图片就是找到函数的最高点,在我们完成判别器的选取后(即找到函数最高点后),我们下一步的目标(更新生成器),就是找出 m i n min min m a x max max V V V( D D D, G G G)(此时 m a x max max V V V( D D D, G G G)即为红点),我们要做的就是选取红点中最低的,选出的 G i G_i Gi即为我们想要的生成器。
随机梯度下降训练D,G
随机梯度下降法用于减少陷入局部最优的风险,,进而可以更接近全局最优的方法。
该怎么做,论文也给出了算法(不愧是大佬),本文代码使用的是 A d a m 优 化 算 法 Adam优化算法 Adam优化算法(利用”惯性“来跳出局部最优的方法),此处不做详细说明:
这里红框圈出的部分是我们要额外注意的。第一步我们训练 D D D, D D D是希望 V V V( G G G, D D D)越大越好,所以是加上梯度( a s c e n d i n g ascending ascending)。第二步训练 G G G时, V V V( G G G, D D D)越小越好,所以是减去梯度( d e s c e n d i n g descending descending)。整个训练过程交替进行。
DCGAN介绍及相关原理
使用过 C N N CNN CNN的朋友一定不会对运用 C N N CNN CNN进行图像样本分类的方法感到陌生,我们很容易就会想到, G A N GAN GAN中的判别器是否可以用 C N N CNN CNN做二元分类器来进行替代,那么如何把 C N N CNN CNN与 G A N GAN GAN很好的结合起来?
《 U n s u p e r v i s e d R e p r e s e n t a t i o n L e a r n i n g w i t h D e e p C o n v o l u t i o n a l G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks UnsupervisedRepresentationLearningwithDeepConvolutionalGenerativeAdversarialNetworks》论文告诉了我们答案。
论文作者将 G A N GAN GAN中的生成器,判别器都用两个卷积神经网络来进行替换。用二元分类的 C N N CNN CNN来做判别器,用反卷积代替卷积操作的卷积神经网络来做生成器,反卷积操作可简单理解为往原有的小像素矩阵中进行填充,之后再进行卷积操作,从而实现从”小到大”(从而可以实现用一组列向量生成一个像素矩阵)的变化。
D C G A N DCGAN DCGAN对卷积神经网络的结构做了一些改变,以提高样本的质量和收敛的速度,这些改变有:
1.取消所有 p o o l i n g pooling pooling层。 G G G网络中使用转置卷积( t r a n s p o s e d c o n v o l u t i o n a l l a y e r transposed convolutional layer transposedconvolutionallayer)进行上采样, D D D网络中用加入 s t r i d e stride stride的卷积代替 p o o l i n g pooling pooling。
2.在 D D D和 G G G中均使用 b a t c h n o r m a l i z a t i o n batch normalization batchnormalization
3.去掉 F C FC FC层,使网络变为全卷积网络
4. G G G网络中使用 R e L U ReLU ReLU作为激活函数,最后一层使用 t a n h tanh tanh
5. D D D网络中使用 L e a k y R e L U LeakyReLU LeakyReLU作为激活函数
D C G A N DCGAN DCGAN中的 G G G网络示意:
Pytorch实现二次元人物头像生成
如何使用GAN生成二次元头像
借助我们之前的推导快速地理解一下动漫头像生成的 G A N GAN GAN原理。如下图所示,判别器将数据集中的采样做真样本,由噪声采样经生成器反卷积生成的图像做假样本进行每代的更新,生成器则以让判别器尽可能地给自己生成的图像打高分为目的进行模型的更新迭代。
我们也可以用合作的思想去理解生成器同判别器的关系:
初始化两个模型后,再依照我们之前提及的模型 G G G, D D D固定一方,更新另一方的方法进行反复迭代。数据集提供的样本做真样本,由 G G G生成的样本做假样本。
之所以要这样做,是因为,如果我们只用数据集抽样出的样本训练 D D D,(而我们收集的图片都是由真人绘制的好图片),换言之,我们只能为判别器提供标签为 1 1 1的样本,除此之外没有反例,这样训练出来的 D D D从来没有见过反例,此时给他一张由 G G G生成的假图片,它也会给它打高分,可以这样理解,在它看来假图片和真图片都是图片,都应该打高分( D D D并没有学会我们想要它学会的特征)。
所以,负面的例子对判别模型来说至关重要,于是我们可以让数据集中抽样出来的样本做真样本(得分为1,real),让生成器生成的样本做假样本(得分为0,fake)。
生成算法:给出一系列的正例,以及随机生成的反例。在每一个轮次中让 D D D学习这些正,反例,训练好判别器之后,固定住。接着训练我们的生成器,使它生成的反例能够尽可能地在判别器中得高分。
算法主体:和之前提及的一样,初始参数,固定一方,训练另一方,让 G G G学会“欺骗”判别器。
另一种思路,我们可以认为所有的画手在画二次元美少女的时候都服从一种固定的规律,比如眼睛的位置,头发的走向,依照一些让人感觉舒服的比例画出好的图片,又因为机器理解图片是借助像素矩阵实现的(所以你喜欢的不是二次元美少女是矩阵啊),用数学的思维来理解,就是所有的美少女头像都是服从一种复杂分布的。如图所示,我们在这个分布里面取样本可以得到我们想要的五官分布正常的好样本,而脱离这个分布之外,我们可以获取各种“抽象派”画作。
那么生成器 G G G要做的就是找出这个分布,并完成对它的模拟,从而让随机噪声经过生成器后能够得到满足这个好分布的图像。
那么我们可以从极大似然估计的方法去计算这个分布的参数,通过图中的数学变化可以发现,求参数的最大似然等价于求 P d a t a P_data Pdata分布与 P G P_G PG分布的 K L KL KL散度。那么我们求似然最大就等价于求散度最小。
同时我们再来看判别器的目标函数,经过求导带入极值点后我们发现,判别器的目标函数最大值就是两个 K L KL KL散度之和。从而判别器,生成器联系了起来,生成器的目标函数可以从 m i n min min K L KL KL改写成 m i n min min m a x max max V V V ( G G G, D D D)。到这各位可以回过头去再看看推到证明中的那个式子。
数据准备
本次采用的数据集约有 16000 16000 16000张,像素为96X96,可以看到一些熟悉的角色(诸如凉宫,长门),下文提供下载链接,网路上也有很多其他的数据集,诸君也可以借助爬虫放自己喜欢的角色进去(京都粉可以尝试爬京都的作画,生成图片会比较接近京都脸)。
网盘链接:链接:https://pan.baidu.com/s/1MFulwMQJ78U2MCqRUYjkMg
提取码:58v6
复制这段内容后打开百度网盘手机App,操作更方便哦
代码实现
代码部分主要参考:张先生-您好.Pytorch实现GAN 生成动漫头像
from tqdm import tqdm
import torch
import torchvision as tv
from torch.utils.data import DataLoader
import torch.nn as nn# config类中定义超参数,
class Config(object):"""定义一个配置类"""# 0.参数调整data_path = '/root/PycharmProjects/untitled/'virs = "result"num_workers = 4 # 多线程img_size = 96 # 剪切图片的像素大小batch_size = 256 # 批处理数量max_epoch = 400 # 最大轮次lr1 = 2e-4 # 生成器学习率lr2 = 2e-4 # 判别器学习率beta1 = 0.5 # 正则化系数,Adam优化器参数gpu = False # 是否使用GPU运算(建议使用)nz = 100 # 噪声维度ngf = 64 # 生成器的卷积核个数ndf = 64 # 判别器的卷积核个数# 1.模型保存路径save_path = 'imgs2/' # opt.netg_path生成图片的保存路径# 判别模型的更新频率要高于生成模型d_every = 1 # 每一个batch 训练一次判别器g_every = 5 # 每1个batch训练一次生成模型save_every = 5 # 每save_every次保存一次模型netd_path = Nonenetg_path = None# 测试数据gen_img = "result.png"# 选择保存的照片# 一次生成保存64张图片gen_num = 64gen_search_num = 512gen_mean = 0 # 生成模型的噪声均值gen_std = 1 # 噪声方差# 实例化Config类,设定超参数,并设置为全局参数
opt = Config()# 定义Generation生成模型,通过输入噪声向量来生成图片
class NetG(nn.Module):# 构建初始化函数,传入opt类def __init__(self, opt):super(NetG, self).__init__()# self.ngf生成器特征图数目self.ngf = opt.ngfself.Gene = nn.Sequential(# 假定输入为opt.nz*1*1维的数据,opt.nz维的向量# output = (input - 1)*stride + output_padding - 2*padding + kernel_size# 把一个像素点扩充卷积,让机器自己学会去理解噪声的每个元素是什么意思。nn.ConvTranspose2d(in_channels=opt.nz, out_channels=self.ngf * 8, kernel_size=4, stride=1, padding=0, bias =False),nn.BatchNorm2d(self.ngf * 8),nn.ReLU(inplace=True),# 输入一个4*4*ngf*8nn.ConvTranspose2d(in_channels=self.ngf * 8, out_channels=self.ngf * 4, kernel_size=4, stride=2, padding=1, bias =False),nn.BatchNorm2d(self.ngf * 4),nn.ReLU(inplace=True),# 输入一个8*8*ngf*4nn.ConvTranspose2d(in_channels=self.ngf * 4, out_channels=self.ngf * 2, kernel_size=4, stride=2, padding=1,bias=False),nn.BatchNorm2d(self.ngf * 2),nn.ReLU(inplace=True),# 输入一个16*16*ngf*2nn.ConvTranspose2d(in_channels=self.ngf * 2, out_channels=self.ngf, kernel_size=4, stride=2, padding=1, bias =False),nn.BatchNorm2d(self.ngf),nn.ReLU(inplace=True),# 输入一张32*32*ngfnn.ConvTranspose2d(in_channels=self.ngf, out_channels=3, kernel_size=5, stride=3, padding=1, bias =False),# Tanh收敛速度快于sigmoid,远慢于relu,输出范围为[-1,1],输出均值为0nn.Tanh(),)# 输出一张96*96*3def forward(self, x):return self.Gene(x)# 构建Discriminator判别器
class NetD(nn.Module):def __init__(self, opt):super(NetD, self).__init__()self.ndf = opt.ndf# DCGAN定义的判别器,生成器没有池化层self.Discrim = nn.Sequential(# 卷积层# 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,此处设定filer过滤器有64个,输出通道自然也就是64。# 因为对图片作了灰度处理,此处通道数为1,# 卷积核大小kernel_size,步长stride,对称填0行列数padding# input:(bitch_size, 3, 96, 96),bitch_size = 单次训练的样本量# output:(bitch_size, ndf, 32, 32), (96 - 5 +2 *1)/3 + 1 =32# LeakyReLu= x if x>0 else nx (n为第一个函数参数),开启inplace(覆盖)可以节省内存,取消反复申请内存的过程# LeakyReLu取消了Relu的负数硬饱和问题,是否对模型优化有效有待考证nn.Conv2d(in_channels=3, out_channels= self.ndf, kernel_size= 5, stride= 3, padding= 1, bias=False),nn.LeakyReLU(negative_slope=0.2, inplace= True),# input:(ndf, 32, 32)nn.Conv2d(in_channels= self.ndf, out_channels= self.ndf * 2, kernel_size= 4, stride= 2, padding= 1, bias=False),nn.BatchNorm2d(self.ndf * 2),nn.LeakyReLU(0.2, True),# input:(ndf *2, 16, 16)nn.Conv2d(in_channels= self.ndf * 2, out_channels= self.ndf *4, kernel_size= 4, stride= 2, padding= 1,bias=False),nn.BatchNorm2d(self.ndf * 4),nn.LeakyReLU(0.2, True),# input:(ndf *4, 8, 8)nn.Conv2d(in_channels= self.ndf *4, out_channels= self.ndf *8, kernel_size= 4, stride= 2, padding= 1, bias=False),nn.BatchNorm2d(self.ndf *8),nn.LeakyReLU(0.2, True),# input:(ndf *8, 4, 4)# output:(1, 1, 1)nn.Conv2d(in_channels= self.ndf *8, out_channels= 1, kernel_size= 4, stride= 1, padding= 0, bias=True),# 调用sigmoid函数解决分类问题# 因为判别模型要做的是二分类,故用sigmoid即可,因为sigmoid返回值区间为[0,1],# 可作判别模型的打分标准nn.Sigmoid())def forward(self, x):# 展平后返回return self.Discrim(x).view(-1)def train(**kwargs):# 配置属性# 如果函数无字典输入则使用opt中设定好的默认超参数for k_, v_ in kwargs.items():setattr(opt, k_, v_)# device(设备),分配设备if opt.gpu:device = torch.device("cuda")else:device = torch.device('cpu')# 数据预处理1# transforms 模块提供一般图像转换操作类的功能,最后转成floatTensor# tv.transforms.Compose用于组合多个tv.transforms操作,定义好transforms组合操作后,直接传入图片即可进行处理# tv.transforms.Resize,对PIL Image对象作resize运算, 数值保存类型为float64# tv.transforms.CenterCrop, 中心裁剪# tv.transforms.ToTensor,将opencv读到的图片转为torch image类型(通道,像素,像素),且把像素范围转为[0,1]# tv.transforms.Normalize,执行image = (image - mean)/std 数据归一化操作,一参数是mean,二参数std# 因为是三通道,所以mean = (0.5, 0.5, 0.5),从而转成[-1, 1]范围transforms = tv.transforms.Compose([# 3*96*96tv.transforms.Resize(opt.img_size), # 缩放到 img_size* img_size# 中心裁剪成96*96的图片。因为本实验数据已满足96*96尺寸,可省略tv.transforms.CenterCrop(opt.img_size),# ToTensor 和 Normalize 搭配使用tv.transforms.ToTensor(),tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])# 加载数据并使用定义好的transforms对图片进行预处理,这里用的是直接定义法# dataset是一个包装类,将数据包装成Dataset类,方便之后传入DataLoader中# 写法2:# 定义类Dataset(Datasets)包装类,重写__getitem__(进行transforms系列操作)、__len__方法(获取样本个数)# ### 两种写法有什么区别dataset = tv.datasets.ImageFolder(root=opt.data_path, transform=transforms)# 数据预处理2# 查看drop_last操作,dataloader = DataLoader(dataset, # 数据加载batch_size=opt.batch_size, # 批处理大小设置shuffle=True, # 是否进行洗牌操作#num_workers=opt.num_workers, # 是否进行多线程加载数据设置drop_last=True # 为True时,如果数据集大小不能被批处理大小整除,则设置为删除最后一个不完整的批处理。)# 初始化网络netg, netd = NetG(opt), NetD(opt)# 判断网络是否有权重数值# ### storage存储map_location = lambda storage, loc: storage# torch.load模型加载,即有模型加载模型在该模型基础上进行训练,没有模型则从头开始# f:类文件对象,如果有模型对象路径,则加载返回# map_location:一个函数或字典规定如何remap存储位置# net.load_state_dict将加载出来的模型数据加载到构建好的net网络中去if opt.netg_path:netg.load_state_dict(torch.load(f=opt.netg_path, map_location=map_location))if opt.netd_path:netd.load_state_dict(torch.load(f=opt.netd_path, map_location=map_location))# 搬移模型到之前指定设备,本文采用的是cpu,分配设备netd.to(device)netg.to(device)# 定义优化策略# torch.optim包内有多种优化算法,# Adam优化算法,是带动量的惯性梯度下降算法optimize_g = torch.optim.Adam(netg.parameters(), lr=opt.lr1, betas=(opt.beta1, 0.999))optimize_d = torch.optim.Adam(netd.parameters(), lr=opt.lr2, betas=(opt.beta1, 0.999))# 计算目标值和预测值之间的交叉熵损失函数# BCEloss:-w(ylog x +(1 - y)log(1 - x))# y为真实标签,x为判别器打分(sigmiod,1为真0为假),加上负号,等效于求对应标签下的最大得分# to(device),用于指定CPU/GPUcriterions = nn.BCELoss().to(device)# 定义标签,并且开始注入生成器的输入noisetrue_labels = torch.ones(opt.batch_size).to(device)fake_labels = torch.zeros(opt.batch_size).to(device)# 生成满足N(1,1)标准正态分布,opt.nz维(100维),opt.batch_size个数的随机噪声noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)# 用于保存模型时作生成图像示例fix_noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)# 训练网络# 设置迭代for epoch in range(opt.max_epoch):# tqdm(iterator()),函数内嵌迭代器,用作循环的进度条显示for ii_, (img, _) in tqdm((enumerate(dataloader))):# 将处理好的图片赋值real_img = img.to(device)# 开始训练生成器和判别器# 注意要使得生成的训练次数小于一些# 每一轮更新一次判别器if ii_ % opt.d_every == 0:# 优化器梯度清零optimize_d.zero_grad()# 训练判别器# 把判别器的目标函数分成两段分别进行反向求导,再统一优化# 真图# 把所有的真样本传进netd进行训练,output = netd(real_img)# 用之前定义好的交叉熵损失函数计算损失error_d_real = criterions(output, true_labels)# 误差反向计算error_d_real.backward()# 随机生成的假图# .detach() 返回相同数据的 tensor ,且 requires_grad=False# .detach()做截断操作,生成器不记录判别器采用噪声的梯度noises = noises.detach()# 通过生成模型将随机噪声生成为图片矩阵数据fake_image = netg(noises).detach()# 将生成的图片交给判别模型进行判别output = netd(fake_image)# 再次计算损失函数的计算损失error_d_fake = criterions(output, fake_labels)# 误差反向计算# 求导和优化(权重更新)是两个独立的过程,只不过优化时一定需要对应的已求取的梯度值。# 所以求得梯度值很关键,而且,经常会累积多种loss对某网络参数造成的梯度,一并更新网络。error_d_fake.backward()‘’‘关于为什么要分两步计算loss:我们已经知道,BCEloss相当于计算对应标签下的得分,那么我们把真样本传入时,因为标签恒为1,BCE此时只有第一项,即真样本得分项要补齐成前文提到的判别器目标函数,需要再添置假样本得分项,故两次分开计算梯度,各自最大化各自的得分(假样本得分是log(1-D(x)))再统一进行梯度下降即可’‘’# 计算一次Adam算法,完成判别模型的参数迭代# 多个不同loss的backward()来累积同一个网络的grad,计算一次Adam即可optimize_d.step()# 训练判别器if ii_ % opt.g_every == 0:optimize_g.zero_grad()# 用于netd作判别训练和用于netg作生成训练两组噪声需不同noises.data.copy_(torch.randn(opt.batch_size, opt.nz, 1, 1))fake_image = netg(noises)output = netd(fake_image)# 此时判别器已经固定住了,BCE的一项为定值,再求最小化相当于求二项即G得分的最大化error_g = criterions(output, true_labels)error_g.backward()# 计算一次Adam算法,完成判别模型的参数迭代optimize_g.step()# 保存模型if (epoch + 1) % opt.save_every == 0:fix_fake_image = netg(fix_noises)tv.utils.save_image(fix_fake_image.data[:64], "%s/%s.png" % (opt.save_path, epoch), normalize=True)torch.save(netd.state_dict(), 'imgs2/' + 'netd_{0}.pth'.format(epoch))torch.save(netg.state_dict(), 'imgs2/' + 'netg_{0}.pth'.format(epoch))# @torch.no_grad():数据不需要计算梯度,也不会进行反向传播
@torch.no_grad()
def generate(**kwargs):# 用训练好的模型来生成图片for k_, v_ in kwargs.items():setattr(opt, k_, v_)device = torch.device("cuda") if opt.gpu else torch.device("cpu")# 加载训练好的权重数据netg, netd = NetG(opt).eval(), NetD(opt).eval()# 两个参数返回第一个map_location = lambda storage, loc: storage# opt.netd_path等参数有待修改netd.load_state_dict(torch.load('imgs2/netd_399.pth', map_location=map_location), False)netg.load_state_dict(torch.load('imgs2/netg_399.pth', map_location=map_location), False)netd.to(device)netg.to(device)# 生成训练好的图片# 初始化512组噪声,选其中好的拿来保存输出。noise = torch.randn(opt.gen_search_num, opt.nz, 1, 1).normal_(opt.gen_mean, opt.gen_std).to(device)fake_image = netg(noise)score = netd(fake_image).detach()# 挑选出合适的图片# 取出得分最高的图片indexs = score.topk(opt.gen_num)[1]result = []for ii in indexs:result.append(fake_image.data[ii])# 以opt.gen_img为文件名保存生成图片tv.utils.save_image(torch.stack(result), opt.gen_img, normalize=True, range=(-1, 1))def main():# 训练模型train()# 生成图片generate()if __name__ == '__main__':main()
判别、生成模型均一轮迭代,200轮次
每 50 50 50轮展示一次
判别每一轮迭代,生成模型每五轮迭代,400轮次
每 100 100 100轮展示一次
理论上,生成器的更新频率应该小于判别器,因为生成器变化太快,会导致 V V V( G G G, D D D)变化剧烈,从而最开始的最高点不再满足最高要求。
图片生成
用训练好的生成器,随机输入一组噪声,生成结果如下。效果不理想,希望大佬们能够给出意见让我能够学习改进一下。(找到原因了,数据集太小了,目前在收集数据中,之后更新此处)
总结
博主第一次写博文,很多不足之处希望多多包涵,之后会陆续补齐李教授的其他作业讲解,希望能够帮助到一些和我一样刚入门的朋友,也希望大佬可以帮忙提意见帮助我改进不足之处。