目录
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊|接辅导、项目定制
一、课题背景和开发环境
📌第G1周:生成对抗网络(GAN)入门📌
Python 3.8.12
numpy==1.21.5 -> 1.24.3
pytorch==1.8.1+cu111
📌 基础任务:
了解什么是生成对抗网络(GAN)
学习本文代码,并跑通代码
🎈进阶任务:
调用训练好的模型生成新图像
二、理论基础
生成对抗网络(Generative Adversarial Networks, GAN)是近年来深度学习领域的一个热点方向。
GAN并不指代某一个具体的神经网络,而是指一类基于博弈思想而设计的神经网络。
GAN由两个分别被称为生成器(Generator)和判别器(Discriminator)的神经网络组成。其中,生成器从某种噪声分布中随机采样作为输入,输出与训练集中真实样本非常相似的人工样本;判别器的输入则为真实样本或人工样本,其目的是将人工样本与真实样本尽可能地区分出来。生成器和判别器交替运行,相互博弈,各自的能力都得到升。理想情况下,经过足够次数的博弈之后,判别器无法判断给定样本的真实性,即对于所有样本都输出50%真,50%假的判断。此时,生成器输出的人工样本已经逼真到使判别器无法分辨真假,停止博弈。这样就可以得到一个具有“伪造”真实样本能力的生成器。
1.生成器
GANs中,生成器 G 选取随机噪声 z 作为输入,通过生成器的不断拟合,最终输出一个和真实样本尺寸相同,分布相似的伪造样本 G ( z ) G(z)G(z) 。生成器的本质是一个使用生成式方法的模型,它对数据的分布假设和分布参数进行学习,然后根据学习到的模型重新采样出新的样本。
从数学上来说,生成式方法对于给定的真实数据,首先需要对数据的显式变量或隐含变量做分布假设;然后再将真实数据输入到模型中对变量、参数进行训练;最后得到一个学习后的近似分布,这个分布可以用来生成新的数据。从机器学习的角度来说,模型不会去做分布假设,而是通过不断地学习真实数据,对模型进行修正,最后也可以得到一个学习后的模型来做样本生成任务。这种方法不同于数学方法,学习的过程对人类理解较不直观。
2. 判别器
GANs中,判别器 D 对于输入的样本 x,输出一个 [ 0 , 1 ] [0,1][0,1] 之间的概率数值 D ( x ) D(x)D(x)。x 可能是来自于原始数据集中的真实样本 x,也可能是来自于生成器 G 的人工样本 G ( z ) G(z)G(z)。通常约定,概率值 D ( x ) D(x)D(x) 越接近于1就代表此样本为真实样本的可能性更大;反之概率值越小则此样本为伪造样本的可能性越大。也就是说,这里的判别器是一个二分类的神经网络分类器,目的不是判定输入数据的原始类别,而是区分输入样本的真伪。可以注意到,不管在生成器还是判别器中,样本的类别信息都没有用到,也表明 GAN 是一个无监督的学习过程。
3. 基本原理
GAN是博弈论和机器学习相结合的产物,于2014年Ian Goodfellow的论文中问世,一经问世即火爆足以看出人们对于这种算法的认可和狂热的研究热忱。想要更详细的了解GAN,就要知道它是怎么来的,以及这种算法出现的意义是什么。研究者最初想要通过计算机完成自动生成数据的功能,例如通过训练某种算法模型,让某模型学习过一些苹果的图片后能自动生成苹果的图片,具备些功能的算法即认为具有生成功能。但是GAN不是第一个生成算法,而是以往的生成算法在衡量生成图片和真实图片的差距时采用均方误差作为损失函数,但是研究者发现有时均方误差一样的两张生成图片效果却截然不同,鉴于此不足Ian Goodfellow提出了GAN。
那么GAN是如何完成生成图片这项功能的呢,如上图所示,GAN是由两个模型组成的:生成模型G和判别模型D。首先第一代生成模型1G的输入是随机噪声z,然后生成模型会生成一张初级照片,训练一代判别模型1D另其进行二分类操作,将生成的图片判别为0,而真实图片判别为1;为了欺瞒一代鉴别器,于是一代生成模型开始优化,然后它进阶成了二代,当它生成的数据成功欺瞒1D时,鉴别模型也会优化更新,进而升级为2D,按照同样的过程也会不断更新出N代的G和D。
三、前期准备工作
1. 定义超参数
import os
import argparse
import numpy as np
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.utils import save_image
import torchvision.transforms as transforms
# 创建文件夹
os.makedirs('./output/images/', exist_ok=True)
os.makedirs('./output/', exist_ok=True)
os.makedirs('./data/MNIST/', exist_ok=True)
# 超参数配置
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
# 图像的尺寸:(1, 28, 28),和图像的像素面积:(784)
img_shape = (channels, img_size, img_size)
img_area = np.prod(img_shape)
# 设置cuda:(cuda:0)
cuda = True if torch.cuda.is_available() else False
print(cuda)
2.下载数据
## mnist数据集下载
mnist = datasets.MNIST(root='./data/',
train=True,
download=False,
transform=transforms.Compose([transforms.Resize(img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]))
3. 配置数据
# 配置数据到加载器
dataloader = DataLoader(mnist, batch_size=batch_size, shuffle=True)
四、定义模型
1. 定义鉴别器
'''
定义判别器 Discriminator
将图片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),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid())
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]间的概率
2. 定义生成器
'''
定义生成器 Generator
输入一个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):
layers = [nn.Linear(in_feat, out_feat)]
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),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, img_area),
nn.Tanh())
# view():相当于numpy中的reshape,重新定义矩阵的形状:这里是reshape(64, 1,28, 28)
def forward(self, z): # 输入的是(64, 100)的噪声数据
imgs = self.model(z) # 噪声数据通过生成器模型
imgs = imgs.view(imgs.size(0), *img_shape) # reshape成(64, 1,28, 28)
return imgs # 输出为64张大小为(1, 28, 28)的图像
五、训练模型
1. 创建实例
# 创建生成器、判别器对象
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()
2. 训练模型
# 进行多个epoch的训练
for epoch in range(n_epochs):
for i, (imgs, _) in enumerate(dataloader):
''' 训练判别器 Train Discriminator '''
# 分为两部分:1、真的图像判别为真;2、假的图像判别为假
imgs = imgs.view(imgs.size(0), -1)
real_img = Variable(imgs).cuda()
real_label = Variable(torch.ones(imgs.size(0), 1)).cuda()
fake_label = Variable(torch.zeros(imgs.size(0), 1)).cuda()
# 计算真实图片的损失
real_out = discriminator(real_img)
loss_real_D = criterion(real_out, real_label)
real_scores = real_out
# 计算假的图片的损失
# detach(): 从当前计算图中分离下来避免梯度传到G,因为G不用更新
z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda()
fake_img = generator(z).detach()
fake_out = discriminator(fake_img)
loss_fake_D = criterion(fake_out, fake_label)
fake_scores = fake_out
# 损失函数和优化
loss_D = loss_real_D + loss_fake_D
optimizer_D.zero_grad()
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)
# 损失函数和优化
loss_G = criterion(output, real_label)
optimizer_G.zero_grad()
loss_G.backward()
optimizer_G.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], "./output/images/%d.png" % batches_done, nrow=5, normalize=True)
3. 保存模型
# 保存模型
torch.save(generator.state_dict(), './output/generator.pth')
torch.save(discriminator.state_dict(), './output/discriminator.pth')