这里写自定义目录标题
第G1周:生成对抗网络(GAN)入门
🍨 本文為🔗365天深度學習訓練營 中的學習紀錄博客
🍖 原作者:K同学啊 | 接輔導、項目定制
基础任务
-
- 了解什么是生成对抗网络
-
- 生成对抗网络结构是怎么样的
-
- 学习本文代码,并跑通代码
进阶任务
- 调用训练好的模型生成新图像
一、理论基础
生成对抗网络(Generative Adversarial Networks, GAN)是近年来深度学习领域的一个热点方向。GAN并不指代某一个具体的神经网络,而是指一类基于博弈思想而设计的神经网络。GAN由两个分支分别被称为生成器(Generator)和判别器(Discriminator)的神经网络组成。其中,生成器从某种噪声分布中随机采样作为输入,输出与训练集中真实样本非常相似的人工样本;判别器的输入则为真实样本或人工样本,其目的是将人工样本与真实样本尽可能地区分出来。生成器和判别器交替运行,相互博弈,各自的能力都得到提升。理想情况下,经过足够次数的博弈之后,判别器无法判断给定样本的真实性,即对于所有样本都输出50%真,50%假的判断。此时,生成器输出的人工样本已经逼真到使判别器无法分辨真伪,停止博弈。这种就可以得到一个具有“伪造”真实样本能力的生成器。
1. 生成器
GANs中,生成器G选取随机噪声z作为输入,通过生成器的不断拟合,最终输出一个和真实样本尺寸相同,分布相似的伪造样本 ![G(z)]。生成器的本质是一个使用生成方式的方法的模型,它对数据的分布假设和分布参数进行学习,然后根据学习到的模型重新采样出新的样本。
从数学上来说,生成式方法对于给定的真实数据,首先需要对数据的显式变量或隐含变量做分布假设;然后再将真实数据输入到模型中对参数、参数进行训练;最后得到一个学习后的近似分布,这个分布可以用来生成新的数据。从机器学习的角度来说,模型不会去拟合分布假设,而是通过不断地学习真实数据,对模型进行修正,最后可以得到一个学习后的模型来完成生成任务。这种方法不同于数学方法,学习的过程对人类理解较不直观。
2. 判别器
GANs中,判别器D对于输入的样本x,输出一个[0,1]之间的概率数值 ![D(x)]。x可能是来自于原始数据集中的真实样本x,也可能是来自于生成器G的人工样本 ![G(z)]。通常约定,概率值 ![D(x)] 越接近1就代表此样本为真实样本的可能性更大;反之概率值越小则此样本为伪造样本的可能性越大。也就是说,这里的判别器是一个二分类的神经网络分类器,目的是不是判断输入数据的原始类别,而是区分输入样本的真伪。可以注意到,不管在生成器还是判别器中,样本的类别信息都没有用到,也表明GAN是一个无监督的学习过程。
3. 基本原理
GAN是博弈论和机器学习相结合的产物,于2014年由Ian Goodfellow的论文中问世,一经问世即火爆足以看出人们对于这种算法的认可和狂热的研究热忱。想要更详细的了解GAN,就要知道它是怎么来的,以及这种算法出现的意义是什么。研究者最初想要通过计算机完成自动生成数据的功能,例如通过训练某种算法模型,让某模型学习到一些苹果的图片后能自动生成苹果的图片,具体些的猜法即认为具有生成功能。但是GAN不是一个生成算法,而是以往的生成算法在衡量生成图片和真实图片的差距时采用均方误差作为损失函数,但是研究者发现有时均方误差一样的两张生成图片效果却截然不同,鉴于此不是Ian Goodfellow提出了GAN。
那么GAN是如何完成生成图片这项功能的呢,GAN是由两个模型组成的:生成模型G和判别模型D。首先第一个生成模型G的输入是随机噪声z,然后生成模型会生成一张初级图片,训练中判别模型D另其进行二分类操作,将生成的图片判别为0,而真实图片判别为1;为了欺骗一个鉴别器,早期一代生成模型开始伪化,然尔它进阶成了一个,当前生成的数据成功欺骗D1时,鉴别模型也会优化更新,进而升级为D2,按照同样的过程也会不断更新出N代的G和D。
二、项目介绍
🏡 我的环境:
- 语言环境:Python3.10.11
- 编译器:Jupyter Notebook
- 深度学习框架:Pytorch 2.0.1+cu118
- 显卡(GPU):NVIDIA GeForce RTX 4060
1.基本流程
首先读取minst数据集,并将其标准化以及其他的数据预处理。然后定义生成器和判别器,生成器其功能是从一个分布中生成噪声,然后再用该噪声生成图像。我认为这个噪声本质上是一个随机抽样,生成噪声的分布中的每一个点都有一个对应的图像生成规则,从而能够通过生成器去生成图像。判别器就比较容易理解,只是一个判断图像是否为生成的图像。
2.代码介绍
设置超参数
n_epochs=50
batch_size=64
lr=0.0002
b1=0.5
b2=0.999
n_cpu=2
latent_dim=100
img_size=28
channels=1
sample_interval=500
img_shape=(channels,img_size,img_size)
img_area=np.prod(img_shape)
cuda=True if torch.cuda.is_available() else False
print(cuda)
读取数据集
minst=datasets.MNIST(root='./datasets/',train=True,download=True,transform=transforms.Compose([transforms.Resize(img_size),transforms.ToTensor(),transforms.Normalize([0.5],[0.5])]))
dataloader=DataLoader(minst,batch_size=batch_size,shuffle=True)
定义网络架构
# 将图片28x28展开成784,然后通过多层感知器,中间经过斜率设置为0.2的LeakyReLU激活函数,
# 最后sigmoid激活函数得到了一个0到1之间的概率进行二分类
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(img_area, 512), # 输入特征数为784,输出为512
nn.LeakyReLU(0.2, inplace=True), # 进行非线性映射
nn.Linear(512, 256), # 输入特征数为512,输出为256
nn.LeakyReLU(0.2, inplace=True), # 进行非线性映射
nn.Linear(256, 1), # 输入特征数为256,输出为1
nn.Sigmoid(), # sigmoid是一个激活函数,二分类问题中可将实数映射到[0, 1],作为概率值,多分类用softmax函数
)
def forward(self, img):
img_flat = img.view(img.size(0), -1) # 鉴别器输入是一个被view展开的(784)的一维图像:(64, 784)
validity = self.model(img_flat) # 通过鉴别器网络
return validity # 鉴别器返回的是一个[0, 1]间的概率
# 输入一个100维的0 ~ 1之间的高斯分布,然后通过第一层线性变换将其映射到256维,
# 然后通过LeakyReLU激活函数,接着进行一个线性变换,再经过一个LeakyReLU激活函数,
# 然后经过线性变换将其变成784维,最后经过Tanh激活函数是希望生成的假的图像分布在[-1 ~ 1]之间。
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
## 模型中间块
def block(in_feat, out_feat, normalize=True): # block(in, out )
layers = [nn.Linear(in_feat, out_feat)] # 线性变换将输入映射到out维
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8)) # 正则化
layers.append(nn.LeakyReLU(0.2, inplace=True)) # 非线性激活函数
return layers
## prod():返回给定轴上的数组元素的乘积:1*28*28=784
self.model = nn.Sequential(
*block(latent_dim, 128, normalize=False), # 线性变化将输入映射 100 to 128, 正则化, LeakyReLU
*block(128, 256), # 线性变化将输入映射 128 to 256, 正则化, LeakyReLU
*block(256, 512), # 线性变化将输入映射 256 to 512, 正则化, LeakyReLU
*block(512, 1024), # 线性变化将输入映射 512 to 1024, 正则化, LeakyReLU
nn.Linear(1024, img_area), # 将(784)的数据每一个都映射到[-1, 1]之间
nn.Tanh()
)
## view():相当于numpy中的reshape,重新定义矩阵的形状:这里是reshape(64, 1, 28, 28)
def forward(self, z):
imgs = self.model(z) # 输入的是(64, 100)的噪声数据
imgs = imgs.view(imgs.size(0), *img_shape) # 噪声数据通过生成器模型
return imgs # reshape成(64, 1, 28, 28) 输出出64张大小为(1, 28, 28)的图像
## 创建生成器、判别器对象
generator = Generator()
discriminator = Discriminator()
## 首先需要定义loss的度量方式 (二分类的交叉熵)
criterion = torch.nn.BCELoss()
## 其次定义 优化函数,优化函数的学习率为0.0003
## betas:用于计算梯度以及梯度平方的运行平均值的系数
optimizer_G = torch.optim.Adam(generator.parameters(), lr=lr, betas=(b1, b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=lr, betas=(b1, b2))
## 如果有显卡,都在cuda模式中运行
if torch.cuda.is_available():
generator = generator.cuda()
discriminator = discriminator.cuda()
criterion = criterion.cuda()
模型训练
## 进行多个epoch的训练
for epoch in range(n_epochs): # epoch:50
for i, (imgs, _) in enumerate(dataloader): # imgs:(64, 1, 28, 28) _:label(64)
# =================训练判别器=================
## view(): 相当于Numpy中的reshape,调整经过矩阵后的维度转换,相当于reshape(128, 784) 原来是(128, 1, 28, 28)
imgs = imgs.view(imgs.size(0), -1) # 将图片平展为28*28=784的向量
real_img = Variable(imgs).cuda() # 将真实图片放入计算图中,tensor变成variable之后才能进行反向传播求梯度
real_label = Variable(torch.ones(imgs.size(0))).cuda() # 定义真实图片的label为1
fake_label = Variable(torch.zeros(imgs.size(0))).cuda() # 定义假的图片的label为0
## Train Discriminator
real_out = discriminator(real_img).squeeze(1)
# 将真实图片放入判别器中
loss_real = criterion(real_out, real_label) # 得到真实图片的loss
real_scores = real_out # 得到真实图片的判别器的输出值,输出的值越接近1越好
## detach(): 从当前计算图中分离下来避免梯度传到G,因为G不用更新
z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda() # 随机生成一些噪声,大小为(128, 100)
fake_img = generator(z).detach() # 假的图片放入生成网络中,生成一张假的图片。
fake_out = discriminator(fake_img).squeeze(1) # 判别器判断假的图片
loss_fake = criterion(fake_out, fake_label) # 得到假的图片的loss
fake_scores = fake_out # 得到假的图片的判别值,对于判别器来说,假图片的损失越接近0越好
## 损失函数优化
loss_D = loss_real + loss_fake # 损失包括真实损失和判别损失
optimizer_D.zero_grad() # 在反向传播之前,先将梯度归0
loss_D.backward() # 将误差反向传播
optimizer_D.step() # 更新参数
# =================训练生成器=================
## Train Generator
## 原理:目的是希望生成的假的图片被判别器判断为真的图片,
## 在此过程中,将判别器固定住,将假的图片传入判别器与真实的label对应,
## 反向传播更新的参数是生成网络里面的参数,
## 这样可以通过更新生成网络里面的参数,来训练网络,使得生成的图片让判别器认为是真的,这样就达到了对抗的目的
z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda() # 得到随机噪声
fake_img = generator(z) # 随机噪声输入到生成器中,得到一副假的图片
output = discriminator(fake_img).squeeze(1) # 经判别器得到了结果
## 损失函数优化
loss_G = criterion(output, real_label) # 得到假的图片与真实的图片的label的loss
optimizer_G.zero_grad() # 梯度归0
loss_G.backward() # 进行反向传播
optimizer_G.step() # step()一般用在反向传播后面,用于更新生成网络的参数
## 打印训练过程中的日志
## item(): 取出元素张量的元素值并返回该值,保持原元素类型不变
if (i + 1) % 300 == 0:
print("[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f] [D real: %f] [D fake: %f]"
% (epoch, n_epochs, i, len(dataloader), loss_D.item(), loss_G.item(), real_scores.data.mean(), fake_scores.data.mean()))
## 保存训练过程中的图像
batches_done = epoch * len(dataloader) + i
if batches_done % sample_interval == 0:
save_image(fake_img.data[:25], "./images/%d.png" % batches_done, nrow=5, normalize=True)
生成器效果测试
import torch
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda()
image = generator(z).detach().cpu()
inv_trans = transforms.Normalize(
mean=[-0.5 / 0.5], # 等效于 -mean / std
std=[1 / 0.5] # 等效于 1 / std
)
# 应用逆标准化
inv_images = inv_trans(image)
# 转换为numpy数组并显示图像
inv_img = inv_images[0].numpy().transpose(1, 2, 0)
plt.imshow(inv_img.squeeze(), cmap='gray')
plt.show()