之前已经讲解了如何使用经典的卷积神经网络做分类和回归任务,相信大家对深度学习已经有了一定的了解,那么这一节开始,我们就要一起学习另一个深度学习中很火的模块——对抗生成网络。
对抗生成网络原理
所谓的对抗生成网络同,最主要的组成部分就是生成器和判别器。以银行鉴别假钞为例,假钞的出现会扰乱市场,所以需要验钞,而有了验钞器,假钞就无法花出,于是假钞就会升级,骗过当前的验钞机制,随后验钞器也会升级,鉴定出升级的假钞。对抗生成网络中,生成器可以看做这个例子中的假钞,而判别器就相当于这个例子中的验钞机,随着生成器和判别器的迭代,最终的结果就是生成器生成的“假钞”几乎可以达到与“真钞”几乎相同,此时我们便可以只保留生成器的最终模型。对抗生成网络可以完成的任务有很多,比如修改图像的季节:
或者对模糊图像进行超分辨率重建:
损失函数的解释说明
在传统的神经网络之中,我们介绍过损失函数的作用,忘记的小伙伴可以复习一下~
在对抗生成网络中,我们的判别器会针对生成器生成的结果给出一个评价,这个评价介于0和1之间,越接近于1,说明生成器的输出越逼真,相反,接近0则证明生成器输出结果差。损失函数是用来描述判别结果与实际的差距,其计算公式为:
其中,t代表了实际值,在该网络下只有0和1之分(由生成器生成的结果会被标记为0,由数据集中读取到的结果则标记为1),o则代表了判别器判定的结果。假设生成器输出的结果,判别器判断其为0.87,就可以说生成效果较好(因为判别器判断其为数据集中读到的内容),损失值为:-1×[1×log(0.87)+0×log(0.23)]=0.0605。下面我们用一段代码给大家解释一下损失函数如何使用:
import torch
from torch import autograd
input = autograd.Variable(torch.tensor([[ 1.9072, 1.1079, 1.4906],
[-0.6584, -0.0512, 0.7608],
[-0.0614, 0.6583, 0.1095]]), requires_grad=True) # 模拟判定值
print(input)
print('-'*100)
from torch import nn
m = nn.Sigmoid() # 将判定值转化成0到1之间的对应值
print(m(input)) # y=f(x)=sigmoid(imput)
print('-'*100)
target = torch.FloatTensor([[0, 1, 1], [1, 1, 1], [0, 0, 0]]) # 模拟标记值,0代表生成器生成,1代表数据集读入
print(target)
print('-'*100)
import math
## 手动计算损失
r11 = 0 * math.log(0.8707) + (1-0) * math.log((1 - 0.8707))
r12 = 1 * math.log(0.7517) + (1-1) * math.log((1 - 0.7517))
r13 = 1 * math.log(0.8162) + (1-1) * math.log((1 - 0.8162))
r21 = 1 * math.log(0.3411) + (1-1) * math.log((1 - 0.3411))
r22 = 1 * math.log(0.4872) + (1-1) * math.log((1 - 0.4872))
r23 = 1 * math.log(0.6815) + (1-1) * math.log((1 - 0.6815))
r31 = 0 * math.log(0.4847) + (1-0) * math.log((1 - 0.4847))
r32 = 0 * math.log(0.6589) + (1-0) * math.log((1 - 0.6589))
r33 = 0 * math.log(0.5273) + (1-0) * math.log((1 - 0.5273))
r1 = -(r11 + r12 + r13) / 3
#0.8447112733378236
r2 = -(r21 + r22 + r23) / 3
#0.7260397266631787
r3 = -(r31 + r32 + r33) / 3
#0.8292933181294807
bceloss = (r1 + r2 + r3) / 3
print(bceloss)
print('-'*100)
## 调包,自动进行计算
loss = nn.BCELoss()
# 使用BCELoss()计算损失时需要手动对输入进行sigmoid激活
print(loss(m(input), target))
print('-'*100)
# 使用BCEWithLogitsLoss()可以自动对输入进行激活
loss = nn.BCEWithLogitsLoss()
print(loss(input, target))
小伙伴可以运行查看哦,这里只展示部分结果:
可以看到我们手动计算的损失值确实和调包得到的结果相同,大家懂得其中的原理了吧~
生成器构建
在这个模块中我会给大家介绍生成绩的具体组成,需要注意的是我们还没有制作数据,因此这一节和下一节都只是理论解释,大家县要做到能看懂就可以啦,完整的代码会在代码整理中展现。
我们用到的数据集依然是minist,由于手写字体较容易生成,因此我们只用100个随机特征值来进行模拟输入。随着日后任务的复杂度提升,我们的输入内容也会发生变化,代码的解释都放在注释里了:
class Generator(nn.Module): # 制作生成器,由100维随机向量生成一个784(1*28*28)维特征图像
def __init__(self):
super(Generator, self).__init__()
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)] # in_feat为特征内容,最开始是随机初始化的
# 我们需要把生成内容变成有价值的特征,最后输出成
# 和数据集一样的数字
# 在这里定义每一层网络有out_feat个隐层
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8)) # 随机失活
layers.append(nn.LeakyReLU(0.2, inplace=True)) # 激活函数
return layers
self.model = nn.Sequential( # 定义每一层网络
*block(opt.latent_dim, 128, normalize=False), # 第一层网络输入由数据集决定,输出为128,不进行随机失活
*block(128, 256), # 第二层网络输入为128,输出为256,进行随机失活
*block(256, 512), # 第三层网络输入为256,输出为512,进行随机失活
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))), # 输出层,输入为1024,输出为原始图像所有特征(1*28*28)
nn.Tanh()
)
def forward(self, z):
img = self.model(z)
img = img.view(img.size(0), *img_shape)
return img
判别器构建
判别器与生成器最大的区别其实在输入上,其他的结构都是大体相同的:
class Discriminator(nn.Module): # 制作判别器
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential( # 与生成器的定义大体相同
nn.Linear(int(np.prod(img_shape)), 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)
validity = self.model(img_flat)
return validity
代码整理
首先我们已经把最关键的三个部分给大家介绍完了,接下来就是处理数据、设置优化器以及执行训练了。首先是处理数据:
os.makedirs("images", exist_ok=True)
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=100, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=128, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
# 模拟使用100个随机特征作为生成器的初始输入
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval betwen image samples")
opt = parser.parse_args()
print(opt)
img_shape = (opt.channels, opt.img_size, opt.img_size) # 颜色通道*长*宽
cuda = True if torch.cuda.is_available() else False
接下来是初始化网络、配置数据以及优化器:
# 损失函数
adversarial_loss = torch.nn.BCELoss()
# 实例化生成器和判别器
generator = Generator()
discriminator = Discriminator()
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
# 配置数据
os.makedirs("./data/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader(
datasets.MNIST(
"./data/mnist",
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])] # 转换成transform并标准化
),
),
batch_size=opt.batch_size,
shuffle=True,
)
# 优化
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
最后就是训练和保存了:
for epoch in range(opt.n_epochs):
for i, (imgs, _) in enumerate(dataloader): # img维度为batch_size*channel*wide*high
# 用真假作为标签。把生成器生成的结果标记为假,数据集中拿到的数据标记为真
# valid代表有batch个数据从数据集中取出,每个都标记成1
# fake代表有batch个数据由生成器生成,每个都标记成0
# imgs.size(0)=batch_size
valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
# 配置输入
real_imgs = Variable(imgs.type(Tensor))
# 训练生成器
optimizer_G.zero_grad() # 梯度清零
# 随机构建batch_size个latent_dim维的标准正态分布噪声向量
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
# 生成一批图像
gen_imgs = generator(z) # 通过生成器生成batch_size个拥有channel*wide*high个特征的矩阵
# 将生成结果传入判别器,希望骗过判别器
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward()
optimizer_G.step()
# 训练判别器
optimizer_D.zero_grad()
# 判别器首先接收的是实际的数字图像
# 计算真实值损失
real_loss = adversarial_loss(discriminator(real_imgs), valid)
# 计算生成值损失
fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
# 用平均值反应判别器从生成的样本中分类真实样本的综合能力
d_loss = (real_loss + fake_loss) / 2
d_loss.backward()
optimizer_D.step()
print(
"[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
% (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
)
batches_done = epoch * len(dataloader) + i
if batches_done % opt.sample_interval == 0:
save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
代码的解释已经附加成注释了,小伙伴们可以自行阅读。最后我们把所有的代码整理起来,就能够运行了:
import argparse
import os
import numpy as np
import math
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch
os.makedirs("images", exist_ok=True)
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=100, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=128, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval betwen image samples")
opt = parser.parse_args()
print(opt)
img_shape = (opt.channels, opt.img_size, opt.img_size)
cuda = True if torch.cuda.is_available() else False
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
self.model = nn.Sequential(
*block(opt.latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))),
nn.Tanh()
)
def forward(self, z):
print('foward')
img = self.model(z)
img = img.view(img.size(0), *img_shape)
return img
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(int(np.prod(img_shape)), 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)
validity = self.model(img_flat)
return validity
adversarial_loss = torch.nn.BCELoss()
generator = Generator()
discriminator = Discriminator()
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
os.makedirs("./data/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader(
datasets.MNIST(
"./data/mnist",
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])] # 转换成transform并标准化
),
),
batch_size=opt.batch_size,
shuffle=True,
)
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
for epoch in range(opt.n_epochs):
for i, (imgs, _) in enumerate(dataloader):
valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
real_imgs = Variable(imgs.type(Tensor))
optimizer_G.zero_grad()
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
gen_imgs = generator(z)
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward()
optimizer_G.step()
optimizer_D.zero_grad()
real_loss = adversarial_loss(discriminator(real_imgs), valid)
fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
d_loss = (real_loss + fake_loss) / 2
d_loss.backward()
optimizer_D.step()
print(
"[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
% (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
)
batches_done = epoch * len(dataloader) + i
if batches_done % opt.sample_interval == 0:
save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
运行完后,我们就可以看到一个新生成的文件夹,存储了每一个epoch的训练结果:
可以看到,经过不断训练后,生成器的生成结果真的从纯粹的噪声信息变成了几可乱真。后面,我们还会介绍更多的GAN网络变种,有兴趣或看完还有些迷糊的小伙伴可以追更哟~