【模型解读】历数GAN的5大基本结构
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了【模型解读】历数GAN的5大基本结构相关的知识,希望对你有一定的参考价值。
参考技术A 首发于微信公众号《有三AI》【模型解读】历数GAN的5大基本结构
生成对抗网络是近几年来无监督学习领域里最大的进展,被誉为下一代深度学习,不管是研究热度还是论文数量,已经逼近甚至超越传统判别式的CNN架构。
这一次我们简单介绍一下生成对抗网络的主流模型结构,从一个生成器一个判别器到多个生成器多个判别器。
作者 | 言有三
编辑 | 言有三
我们这一期文章不打算从头开始讲述GAN,所以如果大家没有相关基础的,就先看一下我们上一期GAN的介绍。
【技术综述】有三说GANs(上)
一个基本的用于生成图像的GAN的结构就是这样的。
Generator就是生成器,它输入噪声,输出产生的图像。通常噪声就是一个一维的向量,经过reshape为二维图像,然后利用若干个反卷积层来学习上采样。
如全卷积的DCGAN模型[1],输入就是1*100的向量,然后经过一个全连接层学习,reshape到4*4*1024的张量,再经过4个上采样的反卷积网络,生成64*64的图。
Discrimator就是普通的CNN分类器,输入真实样本或者生成的假样本进行分类,在DCGAN中也是4个卷积层。
采用多个判别器[2]的好处带来了类似于boosting的优势,训练一个过于好的判别器,会损坏生成器的性能,这是GAN面临的一个大难题。如果能够训练多个没有那么强的判别器,然后进行boosting,可以取得不错的效果,甚至连dropout技术都可以应用进来。
多个判别器还可以相互进行分工,比如在图像分类中,一个进行粗粒度的分类,一个进行细粒度的分类。在语音任务中,各自用于不同声道的处理。
一般来说,生成器相比判别器要完成的任务更难,因为它要完成数据概率密度的拟合,而判别器只需要进行判别,导致影响GAN性能的一个问题就是模式坍塌,即生成高度相似的样本。
采用多个生成器单个判别器的方法,可以有效地缓解这个问题。
从上图结构可以看出,多个生成器采用同样的结构,在网络的浅层还共享权重。
在利用GAN进行半监督的图像分类任务时,判别器需要同时担任两个角色,即判别生成的假样本,以及预测类别,这对判别器提出了较高的要求。通过增加一个分类器可以分担判别器的工作量,即将捕捉样本和标签的条件分布这一任务交给生成器和分类器,而判别器只专注于区分真实样本和生成的样本。
这一类结构以Triple Generative Adversarial Nets为代表,下图是它的网络结构。
多个生成器和多个判别器就又有几种。
5.1 级联结构[5]
早期以DCGAN为代表的网络生成的图片分辨率太低,质量不够好,都不超过100×100,在32×32或者64×64左右。这是因为难以一次性学习到生成高分辨率的样本,收敛过程容易不稳定。
类似的问题在图像分割,目标检测中都存在。在目标检测中,级联网络被广泛使用,即采用从粗到精的方法依次改进检测器的性能。在图像分割中进行上采样时也采用学习小倍率的放大而不是大倍率的方法,如利用两个2倍上采样替换一个4倍的上采样,不仅可以增强网络的表达能力,还降低了学习难度。
基于此,金字塔GAN结构被提出并广泛使用,它参考图像领域里面的金字塔结构由粗到精一步一步生成图像,并添加残差进行学习。
上图就是它的结构,从低分辨率z3开始,逐级提升,最终生成I0,这是一个金字塔形状的结构,以下符号较多用图片代替。
5.2 并行与循环结构[6]
GAN有一大应用就是风格化,实现两个域之间的风格互换,以CycleGAN[6]为典型代表。它包含了多个生成器和多个判别器。Cycle的典型结构如下:
X和Y分别表示两个域的图像,可知这里存在两个生成器G和F,分别用于从X到Y的生成和Y到X到生成,包含两个判别器,分别是Dx和Dy。而损失本身也增加了一个循环损失,感兴趣读者可以去细读文章。
另外在cross domain学习中也常用到多判别器多生成器多结构,分别学习不同的域。而且各个域的判别器和生成器通常会共享一些权重,如下图是CoGAN[7]的网络结构。
另外还有一些零零散散的结构,比如3D GAN,RNN GAN,由于都是上面这几类的变种,不再统一介绍。
[1] Radford A, Metz L, Chintala S, et al. Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks[J]. international conference on learning representations, 2016.
[2] Durugkar I P, Gemp I, Mahadevan S, et al. Generative Multi-Adversarial Networks[J]. international conference on learning representations, 2017.
[3] Ghosh A, Kulharia V, Namboodiri V P, et al. Multi-Agent Diverse Generative Adversarial Networks[J]. computer vision and pattern recognition, 2018: 8513-8521.
[4] Chongxuan L I, Xu T, Zhu J, et al. Triple Generative Adversarial Nets[J]. neural information processing systems, 2017: 4088-4098.
[5] Denton E L, Chintala S, Szlam A, et al. Deep generative image models using a Laplacian pyramid of adversarial networks[J]. neural information processing systems, 2015: 1486-1494.
[6] Zhu J, Park T, Isola P, et al. Unpaired Image-to-Image Translation Using Cycle-Consistent Adversarial Networks[J]. international conference on computer vision, 2017: 2242-2251.
[7] Liu M, Tuzel O. Coupled Generative Adversarial Networks[J]. neural information processing systems, 2016: 469-477.
本系列的完整目录:
【模型解读】从LeNet到VGG,看卷积+池化串联的网络结构
【模型解读】network in network中的1*1卷积,你懂了吗
【模型解读】GoogLeNet中的inception结构,你看懂了吗
【模型解读】说说移动端基准模型MobileNets
【模型解读】pooling去哪儿了?
【模型解读】resnet中的残差连接,你确定真的看懂了?
【模型解读】“不正经”的卷积神经网络
【模型解读】“全连接”的卷积网络,有什么好?
【模型解读】从“局部连接”回到“全连接”的神经网络
【模型解读】深度学习网络只能有一个输入吗
【模型解读】从2D卷积到3D卷积,都有什么不一样
【模型解读】浅析RNN到LSTM
Pytorch搭建基本的GAN模型及训练过程
文章目录
概述
本文通过Pytorch搭建基本的GAN模型结构,并通过 torchvision 的 MNIST 数据集进行测试。
对于GAN模型的基本结构及公式的理解可以看前一篇博客:
GAN的理论知识及公式的理解
下文的实现完全对照这一篇博客的基本理论。
代码实战
代码是基于Pytorch环境创建,需要先安装Pytorch环境
Pytorch环境搭建教程链接:
Pytorch搭建教程
导包
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import torchvision
from torchvision import transforms
数据准备
# 对数据做归一化 (-1, 1)
transform = transforms.Compose([
transforms.ToTensor(), # 将数据转换成Tensor格式,channel, high, witch,数据在(0, 1)范围内
transforms.Normalize(0.5, 0.5) # 通过均值和方差将数据归一化到(-1, 1)之间
])
# 下载数据集
train_ds = torchvision.datasets.MNIST('data',
train=True,
transform=transform,
download=True)
# 设置dataloader
dataloader = torch.utils.data.DataLoader(train_ds, batch_size=64, shuffle=True)
# 返回一个批次的数据
imgs, _ = next(iter(dataloader))
# imgs的大小
imgs.shape
定义生成器
# 输入是长度为 100 的 噪声(正态分布随机数)
# 输出为(1, 28, 28)的图片
# linear 1 : 100----256
# linear 2: 256----512
# linear 2: 512----28*28
# reshape: 28*28----(1, 28, 28)
class Generator(nn.Module): #创建的 Generator 类继承自 nn.Module
def __init__(self): # 定义初始化方法
super(Generator, self).__init__() #继承父类的属性
self.main = nn.Sequential( #使用Sequential快速创建模型
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, 28*28),
nn.Tanh() # 输出层使用Tanh()激活函数,使输出-1, 1之间
)
def forward(self, x): # 定义前向传播 x 表示长度为100 的noise输入
img = self.main(x)
img = img.view(-1, 28, 28) #将img展平,转化成图片的形式,channel为1可写可不写
return img
定义判别器
## 输入为(1, 28, 28)的图片 输出为二分类的概率值,输出使用sigmoid激活 0-1
# BCEloss计算交叉熵损失
# nn.LeakyReLU f(x) : x>0 输出 x, 如果x<0 ,输出 a*x a表示一个很小的斜率,比如0.1
# 判别器中一般推荐使用 LeakyReLU
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
nn.Linear(28*28, 512), #输入是28*28的张量,也就是图片
nn.LeakyReLU(), # 小于0的时候保存一部分梯度
nn.Linear(512, 256),
nn.LeakyReLU(),
nn.Linear(256, 1), # 二分类问题,输出到1上
nn.Sigmoid()
)
def forward(self, x):
x = x.view(-1, 28*28)
x = self.main(x)
return x
初始化模型、优化器及损失计算函数
# 定义设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 初始化模型
gen = Generator().to(device)
dis = Discriminator().to(device)
# 优化器
d_optim = torch.optim.Adam(dis.parameters(), lr=0.0001)
g_optim = torch.optim.Adam(gen.parameters(), lr=0.0001)
# 损失函数
loss_fn = torch.nn.BCELoss()
绘图函数
def gen_img_plot(model, epoch, test_input):
prediction = np.squeeze(model(test_input).detach().cpu().numpy())
fig = plt.figure(figsize=(4, 4))
for i in range(16):
plt.subplot(4, 4, i+1)
plt.imshow((prediction[i] + 1)/2) # 确保prediction[i] + 1)/2输出的结果是在0-1之间
plt.axis('off')
plt.show()
test_input = torch.randn(16, 100, device=device)
GAN的训练
# 保存每个epoch所产生的loss值
D_loss = []
G_loss = []
# 训练循环
for epoch in range(20): #训练20个epoch
d_epoch_loss = 0 # 初始损失值为0
g_epoch_loss = 0
# len(dataloader)返回批次数,len(dataset)返回样本数
count = len(dataloader)
# 对dataloader进行迭代
for step, (img, _) in enumerate(dataloader): # enumerate加序号
img = img.to(device) #将数据上传到设备
size = img.size(0) # 获取每一个批次的大小
random_noise = torch.randn(size, 100, device=device) # 随机噪声的大小是size个
d_optim.zero_grad() # 将判别器前面的梯度归0
real_output = dis(img) # 判别器输入真实的图片,real_output是对真实图片的预测结果
# 得到判别器在真实图像上的损失
# 判别器对于真实的图片希望输出的全1的数组,将真实的输出与全1的数组进行比较
d_real_loss = loss_fn(real_output,
torch.ones_like(real_output))
d_real_loss.backward() # 求解梯度
gen_img = gen(random_noise)
# 判别器输入生成的图片,fake_output是对生成图片的预测
# 优化的目标是判别器,对于生成器的参数是不需要做优化的,需要进行梯度阶段,detach()会截断梯度,
# 得到一个没有梯度的Tensor,这一点很关键
fake_output = dis(gen_img.detach())
# 得到判别器在生成图像上的损失
d_fake_loss = loss_fn(fake_output,
torch.zeros_like(fake_output))
d_fake_loss.backward() # 求解梯度
d_loss = d_real_loss + d_fake_loss # 判别器总的损失等于两个损失之和
d_optim.step() # 进行优化
g_optim.zero_grad() # 将生成器的所有梯度归0
fake_output = dis(gen_img) # 将生成器的图片放到判别器中,此时不做截断,因为要优化生成器
# 生层器希望生成的图片被判定为真
g_loss = loss_fn(fake_output,
torch.ones_like(fake_output)) # 生成器的损失
g_loss.backward() # 计算梯度
g_optim.step() # 优化
# 将损失累加到定义的数组中,这个过程不需要计算梯度
with torch.no_grad():
d_epoch_loss += d_loss
g_epoch_loss += g_loss
# 计算每个epoch的平均loss,仍然使用这个上下文关联器
with torch.no_grad():
# 计算平均的loss值
d_epoch_loss /= count
g_epoch_loss /= count
# 将平均loss放入到loss数组中
D_loss.append(d_epoch_loss.item())
G_loss.append(g_epoch_loss.item())
# 打印当前的epoch
print('Epoch:', epoch)
# 调用绘图函数
gen_img_plot(gen, epoch, test_input)
输出
Epoch: 0
…(省略中间的迭代输出)
Epoch: 19
总共做了20次的迭代,可以看出,随着迭代次数的增加,生成的图片质量越来越好。
整体代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import torchvision
from torchvision import transforms
# 对数据做归一化 (-1, 1)
transform = transforms.Compose([
transforms.ToTensor(), # 将数据转换成Tensor格式,channel, high, witch,数据在(0, 1)范围内
transforms.Normalize(0.5, 0.5) # 通过均值和方差将数据归一化到(-1, 1)之间
])
# 下载数据集
train_ds = torchvision.datasets.MNIST('data',
train=True,
transform=transform,
download=True)
# 设置dataloader
dataloader = torch.utils.data.DataLoader(train_ds, batch_size=64, shuffle=True)
# 返回一个批次的数据
imgs, _ = next(iter(dataloader))
# imgs的大小
imgs.shape
# 输入是长度为 100 的 噪声(正态分布随机数)
# 输出为(1, 28, 28)的图片
# linear 1 : 100----256
# linear 2: 256----512
# linear 2: 512----28*28
# reshape: 28*28----(1, 28, 28)
class Generator(nn.Module): #创建的 Generator 类继承自 nn.Module
def __init__(self): # 定义初始化方法
super(Generator, self).__init__() #继承父类的属性
self.main = nn.Sequential( #使用Sequential快速创建模型
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, 28*28),
nn.Tanh() # 输出层使用Tanh()激活函数,使输出-1, 1之间
)
def forward(self, x): # 定义前向传播 x 表示长度为100 的noise输入
img = self.main(x)
img = img.view(-1, 28, 28) #将img展平,转化成图片的形式,channel为1可写可不写
return img
## 输入为(1, 28, 28)的图片 输出为二分类的概率值,输出使用sigmoid激活 0-1
# BCEloss计算交叉熵损失
# nn.LeakyReLU f(x) : x>0 输出 x, 如果x<0 ,输出 a*x a表示一个很小的斜率,比如0.1
# 判别器中一般推荐使用 LeakyReLU
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
nn.Linear(28*28, 512), #输入是28*28的张量,也就是图片
nn.LeakyReLU(), # 小于0的时候保存一部分梯度
nn.Linear(512, 256),
nn.LeakyReLU(),
nn.Linear(256, 1), # 二分类问题,输出到1上
nn.Sigmoid()
)
def forward(self, x):
x = x.view(-1, 28*28)
x = self.main(x)
return x
# 定义设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 初始化模型
gen = Generator().to(device)
dis = Discriminator().to(device)
# 优化器
d_optim = torch.optim.Adam(dis.parameters(), lr=0.0001)
g_optim = torch.optim.Adam(gen.parameters(), lr=0.0001)
# 损失函数
loss_fn = torch.nn.BCELoss()
def gen_img_plot(model, epoch, test_input):
prediction = np.squeeze(model(test_input).detach().cpu().numpy())
fig = plt.figure(figsize=(4, 4))
for i in range(16):
plt.subplot(4, 4, i+1)
plt.imshow((prediction[i] + 1)/2) # 确保prediction[i] + 1)/2输出的结果是在0-1之间
plt.axis('off')
plt.show()
test_input = torch.randn(16, 100, device=device)
# 保存每个epoch所产生的loss值
D_loss = []
G_loss = []
# 训练循环
for epoch in range(20): #训练20个epoch
d_epoch_loss = 0 # 初始损失值为0
g_epoch_loss = 0
# len(dataloader)返回批次数,len(dataset)返回样本数
count = len(dataloader)
# 对dataloader进行迭代
for step, (img, _) in enumerate(dataloader): # enumerate加序号
img = img.to(device) #将数据上传到设备
size = img.size(0) # 获取每一个批次的大小
random_noise = torch.randn(size, 100, device=device) # 随机噪声的大小是size个
d_optim.zero_grad() # 将判别器前面的梯度归0
real_output = dis(img) # 判别器输入真实的图片,real_output是对真实图片的预测结果
# 得到判别器在真实图像上的损失
# 判别器对于真实的图片希望输出的全1的数组,将真实的输出与全1的数组进行比较
d_real_loss = loss_fn(real_output,
torch.ones_like(real_output))
d_real_loss.backward() # 求解梯度
gen_img = gen(random_noise)
# 判别器输入生成的图片,fake_output是对生成图片的预测
# 优化的目标是判别器,对于生成器的参数是不需要做优化的,需要进行梯度阶段,detach()会截断梯度,
# 得到一个没有梯度的Tensor,这一点很关键
fake_output = dis(gen_img.detach())
# 得到判别器在生成图像上的损失
d_fake_loss = loss_fn(fake_output,
torch.zeros_like(fake_output))
d_fake_loss.backward() # 求解梯度
d_loss = d_real_loss + d_fake_loss # 判别器总的损失等于两个损失之和
d_optim.step() # 进行优化
g_optim.zero_grad() # 将生成器的所有梯度归0
fake_output = dis(gen_img) # 将生成器的图片放到判别器中,此时不做截断,因为要优化生成器
# 生层器希望生成的图片被判定为真
g_loss = loss_fn(fake_output,
torch.ones_like(fake_output)) # 生成器的损失
g_loss.backward() # 计算梯度
g_optim.step() # 优化
# 将损失累加到定义的数组中,这个过程不需要计算梯度
with torch.no_grad():
d_epoch_loss += d_loss
g_epoch_loss += g_loss
# 计算每个epoch的平均loss,仍然使用这个上下文关联器
with torch.no_grad():
# 计算平均的loss值
d_epoch_loss /= count
g_epoch_loss /= count
# 将平均loss放入到loss数组中
D_loss.append(d_epoch_loss.item())
G_loss.append(g_epoch_loss.item())
# 打印当前的epoch
print('Epoch:', epoch)
# 调用绘图函数
gen_img_plot(gen, epoch, test_input)
参考资料
[1] https://www.bilibili.com/video/BV1xm4y1X7KZ
[2] https://blog.csdn.net/hshudoudou/article/details/126922562?spm=1001.2014.3001.5502
以上是关于【模型解读】历数GAN的5大基本结构的主要内容,如果未能解决你的问题,请参考以下文章
『AI原理解读』MindSpore1.2强大并行能力介绍与解读
『AI原理解读』MindSpore1.2强大并行能力介绍与解读
从 ELMo 到 ChatGPT:历数 NLP 近 5 年必看大模型