准备数据
首先导入所有必要的软件包并创建数据集。创建数据集后,方案 1 ~ 3 的实验都是用的同一套数据集。
# 下载实验所需数据并解压
!wget http://labfile.oss.aliyuncs.com/courses/1073/GAN_models.zip
!unzip GAN_models.zip
import os
import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.optim as optim
import torch.nn.functional as F
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import torchvision.utils as vutil
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
定义超参数。
# 定义超参数
image_size = 28 #图像尺寸大小
input_dim = 100 #输入给生成器的向量维度,维度越大可以增加生成器输出样本的多样性
num_channels = 1# 图像的通道数
num_features = 64 #生成器中间的卷积核数量
batch_size = 64 #批次大小
判断是否使用 GPU 环境。
# 如果系统中存在着GPU,我们将用GPU来完成张量的计算
use_cuda = torch.cuda.is_available() #定义一个布尔型变量,标志当前的GPU是否可用
# 如果当前GPU可用,则将优先在GPU上进行张量计算
dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor
创建训练数据与测试数据的数据集。
#为了提高下载速度,我们这里直接下载MNIST数据
!wget http://labfile.oss.aliyuncs.com/courses/1073/MNIST/data.zip
!unzip data.zip
# 加载MNIST数据,如果没有下载过,就会在当前路径下新建/data子目录,并把文件存放其中
# MNIST数据是属于torchvision包自带的数据,所以可以直接调用。
# 在调用自己的数据的时候,我们可以用torchvision.datasets.ImageFolder或者torch.utils.data.TensorDataset来加载
train_dataset = dsets.MNIST(root='./data', #文件存放路径
train=True, #提取训练集
#将图像转化为Tensor,在加载数据的时候,就可以对图像做预处理
transform=transforms.ToTensor(),
download=True) #当找不到文件的时候,自动下载
# 加载测试数据集
test_dataset = dsets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor())
将测试数据 test_dataset 分成两部分,一部分作为校验数据,一部分作为测试数据。校验数据用于检测模型是否过拟合,并调整参数,而测试数据是用来检验整个模型的工作。
# 首先创建 test_dataset 中所有数据的索引下标
indices = range(len(test_dataset))
# 利用数据下标,将 test_dataset 中的前 5000 条数据作为 校验数据
indices_val = indices[:5000]
# 剩下的就作为测试数据了
indices_test = indices[5000:]
采样器(sampler)为加载器(data_loader)提供了一个从每一批数据中抽取样本的方法。在定义好随机采样器后,再定义基于采样器的数据加载器,这样就可以随机乱序的从数据集中加载数据了。
# 根据这些下标,构造两个数据集的SubsetRandomSampler采样器,它会对下标进行采样
sampler_val = torch.utils.data.sampler.SubsetRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler.SubsetRandomSampler(indices_test)
# 根据两个采样器来定义加载器,注意将sampler_val和sampler_test分别赋值给了validation_loader和test_loader
validation_loader = torch.utils.data.DataLoader(dataset =test_dataset,
batch_size = batch_size,
sampler = sampler_val
)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
sampler = sampler_test
)
# 训练数据集的加载器,自动将数据分割成batch,顺序随机打乱
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True # shuffle 代表打乱数据
)
最后再定义两个辅助函数,用于将张量转换为可以显示的图像数据,并将图像数据显示出来。
def make_show(img):
# 将张量变成可以显示的图像
img = img.data.expand(batch_size, 3, image_size, image_size)
return img
def imshow(inp, title=None, ax=None):
# 在屏幕上绘制图像
"""Imshow for Tensor."""
if inp.size()[0] > 1:
inp = inp.numpy().transpose((1, 2, 0))
else:
inp = inp[0].numpy()
mvalue = np.amin(inp)
maxvalue = np.amax(inp)
if maxvalue > mvalue:
inp = (inp - mvalue)/(maxvalue - mvalue)
ax.imshow(inp)
if title is not None:
ax.set_title(title)
图像生成实验1:最小均方误差模型
生成器预测图像模型
下面将搭建一个反卷积神经网络,它接受的输入信息就是一个单独的数字,输出则是一张这个数字所对应的手写数字图像。网络模型的构架图如下所示:
数字被输入网络后首先被扩充为一个 100 维的向量(每个维度都是同一个数字)。之后经过第一层反卷积的作用而变成一个尺寸为 (128,5,5) 的张量,之后是尺寸为 (64,13,13) 的张量,最后是一个单通道的 28*28 大小的图像。当训练这个网络的时候,需要给它成对的数字标签和对应的手写体图像,标签作为输入,图像作为输出以提供监督信息。损失函数可以直接用生成图像与真实图像之间的差异来衡量,最直接的差异函数就是每个像素点色彩值的均方误差,因此这个模型称为最小均方误差模型。
那么下面定义这个生成器模型:
#生成器模型定义
class ModelG(nn.Module):
def __init__(self):
super(ModelG,self).__init__()
self.model=nn.Sequential() #model为一个内嵌的序列化的神经网络模型
# 利用add_module增加一个反卷积层,输入为input_dim维,输出为2*num_features维,窗口大小为5,padding是0
# 输入图像大小为1,输出图像大小为W'=(W-1)S-2P+K+P'=(1-1)*2-2*0+5+0=3, 5*5
self.model.add_module('deconv1',nn.ConvTranspose2d(input_dim, num_features*2, 5, 2, 0, bias=False))
# 增加一个batchnorm层
self.model.add_module('bnorm1',nn.BatchNorm2d(num_features*2))
# 增加非线性层
self.model.add_module('relu1',nn.ReLU(True))
# 增加第二层反卷积层,输入2*num_features维,输出num_features维,窗口5,padding=0
# 输入图像大小为5,输出图像大小为W'=(W-1)S-2P+K+P'=(5-1)*2-2*0+5+0=13, 13*13
self.model.add_module('deconv2',nn.ConvTranspose2d(num_features*2, num_features, 5, 2, 0, bias=False))
# 增加一个batchnorm层
self.model.add_module('bnorm2',nn.BatchNorm2d(num_features))
# 增加非线性层
self.model.add_module('relu2',nn.ReLU(True))
# 增加第二层反卷积层,输入2*num_features维,输出num_features维,窗口4,padding=0
# 输入图像大小为13,输出图像大小为W'=(W-1)S-2P+K+P'=(13-1)*2-2*0+4+0=28, 28*28
self.model.add_module('deconv3',nn.ConvTranspose2d(num_features, num_channels, 4, 2, 0,bias=False))
#self.model.add_module('tanh',nn.Tanh())
self.model.add_module('sigmoid',nn.Sigmoid())
def forward(self,input):
output = input
#遍历网络的所有层,一层层输出信息
for name, module in self.model.named_children():
output = module(output)
#输出一张28*28的图像
return(output)
下面定义一个特殊的模型参数初始化函数。在默认情况下,模型卷积核的权重分布是均值大概为 0,方差在 10^(-2)。 BatchNorm 层的权重均值是大约 0.5,方差在 0.2 左右。使用如下初始化方式可以让方差更小,使得收敛更快。
def weight_init(m):
class_name=m.__class__.__name__
if class_name.find('conv')!=-1:
m.weight.data.normal_(0,0.02)
if class_name.find('norm')!=-1:
m.weight.data.normal_(1.0,0.02)
准备训练
准备训练,实例化模型,定义损失函数和优化器。
#定义生成器模型
net = ModelG()
net = net.cuda() if use_cuda else net
#目标函数采用最小均方误差
criterion = nn.MSELoss()
#定义优化器
optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9)
随机选择生成 0-9 的数字,用于每个周期打印查看结果用。
# 随机选择生成0-9的数字,用于每个周期打印查看结果用
samples = np.random.choice(10, batch_size)
samples = Variable(torch.from_numpy(samples).type(dtype))
# 改变输入数字的尺寸,适应于生成器网络
samples.resize_(batch_size,1,1,1)
samples = Variable(samples.data.expand(batch_size, input_dim, 1, 1))
samples = samples.cuda() if use_cuda else samples #加载到GPU
def save_evaluation_samples(netModel, save_path='gan'):
# 去除首位空格
save_path = save_path.strip()
if not os.path.exists(save_path):
os.makedirs(save_path)
# 产生一组图像保存到指定文件夹下,检测生成器当前的效果
fake_u = netModel(samples) #用原始网络作为输入,得到伪造的图像数据
fake_u = fake_u.cpu() if use_cuda else fake_u
img = make_show(fake_u) #将张量转化成可绘制的图像
vutil.save_image(img, save_path + '/fake%s.png'% (epoch)) #保存生成的图像
下面定义模型的训练函数。
def train_ModelG(target, data):
# 将数据加载到GPU中
if use_cuda:
target, data = target.cuda(), data.cuda()
#将输入的数字标签转化为生成器net能够接受的(batch_size, input_dim, 1, 1)维张量
data = data.type(dtype)
data = data.reshape(data.size()[0], 1, 1, 1)
data = data.expand(data.size()[0], input_dim, 1, 1)
net.train() # 给网络模型做标记,标志说模型正在训练集上训练,
#这种区分主要是为了打开关闭net的training标志
output = net(data) #神经网络完成一次前馈的计算过程,得到预测输出output
loss = criterion(output, target) #将output与标签target比较,计算误差
optimizer.zero_grad() #清空梯度
loss.backward() #反向传播
optimizer.step() #一步随机梯度下降算法
if use_cuda:
loss = loss.cpu()
return loss
然后是模型的验证函数:
def evaluation_ModelG():
net.eval() # 给网络模型做标记,标志说模型在校验集上运行
val_loss = [] #记录校验数据集准确率的容器
'''开始在校验数据集上做循环,计算校验集上面的准确度'''
idx = 0
for (data, target) in validation_loader:
target, data = Variable(data), Variable(target)
idx += 1
if use_cuda:
target, data = target.cuda(), data.cuda()
data = data.type(dtype)
data = data.reshape(data.size()[0], 1, 1, 1)
data = data.expand(data.size()[0], input_dim, 1, 1)
output = net(data) #完成一次前馈计算过程,得到目前训练得到的模型net在校验数据集上的表现
loss = criterion(output, target) #将output与标签target比较,计算误差
if use_cuda:
loss = loss.cpu()
val_loss.append(loss.data.numpy())
return val_loss
训练最小均方误差模型
下面正式开始训练的流程。要注意在 CPU 上训练这个模型大概需要 6 个小时的时间,这在当前的实验环境中是难以接受的。笔者在后面提供了训练好的模型供读者加载使用,所以下面的训练代码在这里可以不执行。读者在自己的环境中训练的时候,只需要把以下代码中的 epoch 改为适当的数目即可。
#训练模型
print('Initialized!')
#开始训练
step = 0 #计数经历了多少时间步
# num_epochs = 100 #总的训练周期
num_epochs = 1 # 因训练模型的时间过长,建议在自己的环境中完成完整训练
record = []
for epoch in range(num_epochs):
train_loss = []
# 加载数据批次
for batch_idx, (data, target) in enumerate(train_loader):
# 注意数据中的data转化为了要预测的target,数据中的target则转化成了输入给网络的标签
target, data = Variable(data), Variable(target) #将Tensor转化为Variable,data为一批图像,target为一批标签
# 调用模型训练函数,返回损失函数值
loss = train_ModelG(target, data)
# 记录损失函数值
train_loss.append(loss.data.numpy())
step += 1
if step % 100 == 0: #每间隔100个batch执行一次打印等操作
# 调用模型验证函数,达到验证误差值
val_loss = evaluation_ModelG()
# 打印误差等数值,其中正确率为本训练周期Epoch开始后到目前撮的正确率的平均值
print('训练周期: {} [{}/{} ({:.0f}%)]\t训练数据Loss: {:.6f}\t校验数据Loss: {:.6f}'.format(
epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader), np.mean(train_loss), np.mean(val_loss)))
record.append([np.mean(train_loss), np.mean(val_loss)])
# 随机选择生成0-9的数字,验证模型的生成结果并保存
save_evaluation_samples(net, 'MSE')
接下来,绘制模型的训练误差曲线,观察模型的训练过程:由于没有执行完整训练,笔者提供了完整训练的误差曲线在下面
plt.figure(figsize = (10, 7))
plt.plot([i[0] for i in record], label='Training')
plt.plot([i[1] for i in record], label='Validation')
plt.xlabel('Batchs')
plt.ylabel('Loss')
plt.legend()
这里是笔者在 GPU 上训练 100 个 epoch 所得到的误差曲线:
在 100 轮(epoch)的训练中,两条误差曲线都在下降,说明模型一直都在学习新东西。校验曲线一直在测试曲线下面,说明模型一直处于欠拟合状态。如果持续不断地喂给模型数据,误差还能持续地往下降。但是误差下降得已经越来越不明显了。
验证最小均方误差模型的生成效果
下面让模型绘制一批手写数字的图像出来,以验证模型的生成效果。这里提供了笔者在 GPU 上训练了 100 个 epoch 的模型供大家使用。
net = torch.load('ModelG_CPU.mdl')
if use_cuda:
net = net.cuda()
# 绘制一批图像样本
fake_u = net(samples) #用原始网络作为输入,得到伪造的图像数据
fake_u = fake_u.cpu() if use_cuda else fake_u
samples = samples.cpu() if use_cuda else samples
img = fake_u.data #将张量转化成可绘制的图像
fig = plt.figure(figsize = (15, 15))
# Two subplots, the axes array is 1-d
f, axarr = plt.subplots(8,8, sharex=True, figsize=(15,15))
for i in range(batch_size):
axarr[i // 8, i % 8].axis('off')
imshow(img[i], samples.data.numpy()[i][0,0,0].astype(int),axarr[i // 8, i % 8])
从绘制的生成图中可以看到,生成结果非常不理想。每张图像的上方是输入给网络的真实数字,可以看到除了少数几个数字以外,所有图像都很模糊。为什么会这样呢?其实,仔细想想不难发现,问题出在损失函数的设计上。我们尝试最小化均方误差,实际上就是要让模型在每一个输入数字下学习到一个平均的手写数字图像。由于对于同一个数字的手写数字图像可能差异很大,这就导致了学出来的平均数字会非常模糊。也许等待更长的训练时间会让数字更清晰些,但是这样的模型是没有效率的,我们希望尝试更好的设计。
生成器 - 识别器模型
生成器 - 识别器模型的实现
在这个模型中,生成器部分与上个模型相同,但是改变了网络的目标函数。因为在这个模型中,加入了一个识别器,它通过固定值(权重不变)的方式迁移自一个手写体识别器(在实验 5 中训练的模型)。在训练过程中让生成器生成图像,并让识别器进行识别,将识别的误差作为目标函数,进而调整生成器,从而能给出正确的分类标签。这种替代会改善由于 MSE 损失函数所带来的过于平均的情况,因为评价标准已经不是一个平均的数字图像,而是生成让识别器可以正确识别图片。
模型的构架如下图所示:
识别器最后的交叉熵损失函数既可以训练识别器,也可以训练生成器。利用 PyTorch 的动态计算图可以轻易的实现这种功能。
# 定义待迁移的网络框架,所有的神经网络模块包括:Conv2d、MaxPool2d,Linear等模块都不需要重新定义,会自动加载
# 但是网络的forward功能没有办法自动实现,需要重写。
# 一般的,加载网络只加载网络的属性,不加载方法
depth = [4, 8]
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(x)
x = F.relu(self.conv2(x))
x = self.pool(x)
# 将立体的Tensor全部转换成一维的Tensor。两次pooling操作,所以图像维度减少了1/4
x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
x = F.relu(self.fc1(x)) #全链接,激活函数
x = F.dropout(x, training=self.training) #以默认为0.5的概率对这一层进行dropout操作
x = self.fc2(x) #全链接,激活函数
x = F.log_softmax(x, dim=1) #log_softmax可以理解为概率对数值
return x
def retrieve_features(self, x):
#该函数专门用于提取卷积神经网络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图
feature_map1 = F.relu(self.conv1(x)) #完成第一层卷积
x = self.pool(feature_map1) # 完成第一层pooling
feature_map2 = F.relu(self.conv2(x)) #第二层卷积,两层特征图都存储到了feature_map1, feature_map2中
return (feature_map1, feature_map2)
定义统计模型预测正确次数的函数:其中 predictions 是模型给出的一组预测结果,batch_size 行 num_classes 列的矩阵,labels 是数据之中的正确答案。
def rightness(predictions, labels):
# 对于任意一行(一个样本)的输出值的第1个维度,求最大,得到每一行的最大元素的下标
pred = torch.max(predictions.data, 1)[1]
# 将下标与labels中包含的类别进行比较,并累计得到比较正确的数量
rights = pred.eq(labels.data.view_as(pred)).sum()
return rights, len(labels) #返回正确的数量和这一次一共比较了多少元素
由于生成器代码没有任何变化,识别器的代码在本系列实验 5 中已经给出,因此在这里就不详细展示它们的代码内容了。为了减少训练的周期,本次使用了固定值迁移学习技术,即将训练好的手写体识别器的网络直接迁移过来。从文件中加载识别器的代码如下:
netR = torch.load('minst_conv_checkpoint') #读取硬盘上的minst_conv_checkpoint文件
netR = netR.cuda() if use_cuda else netR #加载到GPU中
for para in netR.parameters():
para.requires_grad = False #将识别器的权重设置为固定值
准备训练
下面编写准备训练的相关代码。首先实例化生成器模型,定义损失函数和优化器。
netG = ModelG() #新建一个生成器
netG = netG.cuda() if use_cuda else netG #加载到GPU上
netG.apply(weight_init) #初始化参数
criterion = nn.CrossEntropyLoss() #用交叉熵作为损失函数
optimizer = optim.SGD(netG.parameters(), lr=0.0001, momentum=0.9) #定义优化器
在这里着重讲解一下这两句代码:
output1 = netG(data)
output = netR(output1)
这两句代码的意思是将生成器的结果交给了识别器,识别器最后给出预测的数字。通过这样的简单方式就可以将两个神经网络首尾链接到了起来,并且可以实现自动反向传播,这都是 PyTorch 强大动态计算图的功劳。下面定义生成器-识别器模型训练函数:
def train_ConvNet(target, data):
if use_cuda:
target, data = target.cuda(), data.cuda()
# 复制标签变量放到了label中
label = data.clone()
data = data.type(dtype)
# 改变张量形状以适用于生成器网络
data = data.reshape(data.size()[0], 1, 1, 1)
data = data.expand(data.size()[0], input_dim, 1, 1)
netG.train() # 给网络模型做标记,标志说模型正在训练集上训练,
netR.train() #这种区分主要是为了打开关闭net的training标志,从而决定是否运行dropout
output1 = netG(data) #神经网络完成一次前馈的计算过程,得到预测输出output
output = netR(output1) #用识别器网络来做分类
loss = criterion(output, label) #将output与标签target比较,计算误差
optimizer.zero_grad() #清空梯度
loss.backward() #反向传播
optimizer.step() #一步随机梯度下降算法
right = rightness(output, label) #计算准确率所需数值,返回数值为(正确样例数,总样本数)
if use_cuda:
loss = loss.cpu()
return right, loss
下面是验证生成器-识别器模型的函数:
def evaluation_ConvNet():
netG.eval() # 给网络模型做标记,标志说模型正在校验集上运行,
netR.eval() #这种区分主要是为了打开关闭net的training标志,从而决定是否运行dropout
val_loss = [] #记录校验数据集准确率的容器
val_rights = []
'''开始在校验数据集上做循环,计算校验集上面的准确度'''
for (data, target) in validation_loader:
# 注意target是图像,data是标签
target, data = Variable(data), Variable(target)
if use_cuda:
target, data = target.cuda(), data.cuda()
label = data.clone()
data = data.type(dtype)
#改变Tensor大小以适应生成网络
data = data.reshape(data.size()[0], 1, 1, 1)
data = data.expand(data.size()[0], input_dim, 1, 1)
output1 = netG(data) #神经网络完成一次前馈的计算过程,得到预测输出output
output = netR(output1) #利用识别器来识别
loss = criterion(output, label) #将output与标签target比较,计算误差
if use_cuda:
loss = loss.cpu()
val_loss.append(loss.data.numpy())
right = rightness(output, label) #计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
val_rights.append(right)
return val_loss, val_rights
开始训练
定义一些随机样本,用于在训练过程中生成验证图像
# 随机选择batch_size个数字,用他们来生成数字图像
samples = np.random.choice(10, batch_size)
samples = Variable(torch.from_numpy(samples).type(dtype))
# 产生一组图像保存到temp1文件夹下(需要事先建立好该文件夹),检测生成器当前的效果
samples.resize_(batch_size,1,1,1)
samples = Variable(samples.data.expand(batch_size, input_dim, 1, 1))
samples = samples.cuda() if use_cuda else samples
下面开始训练流程。要注意在 CPU 上训练这个模型大概需要 4 个小时的时间,这在当前的实验环境中是难以接受的。笔者在后面提供了训练好的模型供读者加载使用,所以下面的训练代码在这里可以不执行。读者在自己的环境中训练的时候,只需要把以下代码中的 epoch 改为适当的数目即可。
#开始训练
print('Initialized!')
# num_epochs = 100 #总训练周期
num_epochs = 1 # 建议在自己的环境中完成完整的训练
statistics = [] #数据记载器
for epoch in range(num_epochs):
train_loss = []
train_rights = []
# 加载数据
for batch_idx, (data, target) in enumerate(train_loader):
# !!!!注意图像和标签互换了!!!!
target, data = Variable(data), Variable(target) #将Tensor转化为Variable,data为一批标签,target为一批图像
# 调用训练函数
right, loss = train_ConvNet(target, data)
train_loss.append(loss.data.numpy())
train_rights.append(right) #将计算结果装到列表容器train_rights中
step += 1
if step % 100 == 0: #每间隔100个batch执行一次打印等操作
# 调用验证函数
val_loss, val_rights = evaluation_ConvNet()
# 统计验证模型时的正确率
# val_r为一个二元组,分别记录校验集中分类正确的数量和该集合中总的样本数
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
# 统计上面训练模型时的正确率
# train_r为一个二元组,分别记录目前已经经历过的所有训练集中分类正确的数量和该集合中总的样本数,
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
# 计算并打印出模型在训练时和在验证时的准确率
# train_r[0]/train_r[1]就是训练集的分类准确度,同样,val_r[0]/val_r[1]就是校验集上的分类准确度
print(('训练周期: {} [{}/{} ({:.0f}%)]\t训练数据Loss: {:.6f},正确率: {:.2f}%\t校验数据Loss:' +
'{:.6f},正确率:{:.2f}%').format(epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader), np.mean(train_loss),
100. * train_r[0] / train_r[1],
np.mean(val_loss),
100. * val_r[0] / val_r[1]))
#记录中间的数据
statistics.append({'loss':np.mean(train_loss),'train': 100. * train_r[0] / train_r[1],
'valid':100. * val_r[0] / val_r[1]})
# 产生一组图像保存到 ConvNet 文件夹下(需要事先建立好该文件夹),检测生成器当前的效果
save_evaluation_samples(netG, 'ConvNet')
查看模型的生成结果
同样通过绘制误差曲线来查看模型的训练过程。由于没有执行完整训练,笔者提供了完整训练的误差曲线在下面
# 训练曲线
result1 = [100 - i['train'] for i in statistics]
result2 = [100 - i['valid'] for i in statistics]
plt.figure(figsize = (10, 7))
plt.plot(result1, label = 'Training')
plt.plot(result2, label = 'Validation')
plt.xlabel('Step')
plt.ylabel('Error Rate')
plt.legend()
这里是笔者在 GPU 上训练 100 个 epoch 所得到的误差曲线:
从绘制的损失图中可以看到不管是训练数据集还是校验数据集,错误率都降到了相当低的水平,甚至有的时候可以达到了 100% 的准确度。那接下来看看生成手写数字图片的结果:
netG = torch.load('ConvNetG_CPU.mdl')
netR = torch.load('ConvNetR_CPU.mdl')
if use_cuda:
netG = netG.cuda()
netR = netR.cuda()
#绘制一批样本
samples = torch.Tensor([0,1,2,3,4,5,6,7,8,9])
samples = Variable(samples.type(dtype))
sample_size = 10
samples.resize_(sample_size,1,1,1)
samples = Variable(samples.data.expand(sample_size, input_dim, 1, 1))
samples = samples.cuda() if use_cuda else samples
fake_u = netG(samples)
fake_u = fake_u.cpu() if use_cuda else fake_u
samples = samples.cpu() if use_cuda else samples
img = fake_u #.expand(sample_size, 3, image_size, image_size) #将张量转化成可绘制的图像
#fig = plt.figure(figsize = (15, 6))
f, axarr = plt.subplots(2,5, sharex=True, figsize=(15,6))
for i in range(sample_size):
axarr[i // 5, i % 5].axis('off')
imshow(img[i].data, samples.data.numpy()[i][0,0,0].astype(int), axarr[i // 5, i % 5])
上面绘制的结果图简直就是惨不忍睹,数字 39 几乎都是在不同程度上糊成了一团,只有 02 还勉强成型,但这显然不是理想的结果。那么这个生成器-识别器模型的问题出在哪里,这样的图片又是怎样让识别器的正确率达到 100% 的?
探究生成数据与真实数据的不同特征
首先验证一下识别器是否真的被正确训练了。从 MNIST 数据集中选出一张手写数字图片数据(数字 6)。将数据传入识别器中,验证识别器的识别能力。
batch = next(iter(test_loader))
indx = torch.nonzero(batch[1] == 6)
data = batch[0][indx[0]]
img = data.expand(1, 1, image_size, image_size)
print(img.size())
plt.axis('off')
imshow(img[0], 6, plt.gca())
input_x_real = Variable(data)
input_x_real = input_x_real.cuda() if use_cuda else input_x_real
output = netR(input_x_real)
_, prediction = torch.max(output, 1)
print('识别器对真实图片的识别结果:', prediction)
真实图片得到了正确的识别结果。为了更加严谨的验证识别器,下面再选出一张生成的数字图片重新丢进识别器中。
#首先定义读入的图片
idx = 6
ax = plt.gca()
ax.axis('off')
imshow(fake_u[idx].data, 6, plt.gca())
print(samples[idx][0])
#它是从test_dataset中提取第idx个批次的第0个图,其次unsqueeze的作用是在最前面添加一维,
#目的是为了让这个input_x的tensor是四维的,这样才能输入给net。补充的那一维表示batch。
input_fake = fake_u[idx]
input_fake = input_fake.unsqueeze(0)
input_fake = input_fake.cuda() if use_cuda else input_fake
output = netR(input_fake)
_, prediction = torch.max(output, 1)
print('识别器对生成图片的识别结果:', prediction)
一个看似不可思议的事实是,这样糊成一团的图片经过识别器的分类计算后,竟然能被识别成为正常的手写数字,而且还与输入的标签一模一样。要想弄明白识别器为什么会发生这样的错误,那得首先来看看一张图像被送进识别器后,它经过卷积和池化后的特征图是怎么样的。下面先对比图片送进识别器后的第一层特征图。
# feature_maps 是有两个元素的列表,分别表示第一层和第二层卷积的所有特征图
# 生成图像特征图
feature_maps_fake = netR.retrieve_features(input_fake)
feature_maps_fake = (feature_maps_fake[0].cpu(), feature_maps_fake[1].cpu()) if use_cuda else feature_maps_fake
plt.figure(figsize = (10, 7))
# 真实图像特征图
feature_maps_real = netR.retrieve_features(input_x_real)
feature_maps_real = (feature_maps_real[0].cpu(), feature_maps_real[1].cpu()) if use_cuda else feature_maps_real
plt.figure(figsize = (10, 7))
for i in range(8):
plt.subplot(2,4,i + 1)
plt.axis('off')
if i < 4:
plt.imshow(feature_maps_fake[0][0, i,...].data.numpy())
else:
plt.imshow(feature_maps_real[0][0, i-4,...].data.numpy())
上面一排是生成器生成图像输入到识别器中产生的四张特征图,下面则是正常的数字图像输入给识别器产生的特征图。可以看到其实两张图的第一层特征图还是区别非常大的,真实图像的特征图都是肉眼可辨的清晰的数字,而生成图像特征图则是杂乱无章的一团。接下来看看第二层特征图,由于第二层有 8 个卷积核,所以有 8 个特征图:
#第二层有8个特征图,循环把它们打印出来
plt.figure(figsize = (10, 7))
for i in range(8):
plt.subplot(1,8,i + 1)
plt.axis('off')
plt.imshow(feature_maps_fake[1][0, i,...].data.numpy())
plt.figure(figsize = (10, 7))
for i in range(8):
plt.subplot(1,8,i + 1)
plt.axis('off')
plt.imshow(feature_maps_real[1][0, i,...].data.numpy())
让人感到意外的事情发生了,在第二层的8张feature map中,上下居然有很多部分非常相似。那么这样也就不难理解为什么生成器生成的图像会混淆识别器了。识别器是根据第二层特征图作出最后判断的,生成器生成的图像骗过了识别器的眼睛。
深度卷积生成式对抗网络模型(DCGAN)
辨别器的实现
下面终于轮到 DCGAN 出场了。采用与之前同样构架的生成器(generator),根据输入的一个随机噪声向量生成手写数字的图像。然后再同时训练一个辨别器(discriminator),它的任务就是负责辨别一张输入的图像是来源于生成器造假还是来源于原始的数据文件。生成器网络和辨别器网络会一起训练。值得注意的是,辨别器和生成器的目标函数是反的。
即判别器尽最大的可能判断出生成的图片是假的,而生成器则尽量让生成的图片可以蒙混过关,这就是“对抗”的由来。
下面实现辨别器网络的代码。
# 构造辨别器
class ModelD(nn.Module):
def __init__(self):
super(ModelD,self).__init__()
self.model=nn.Sequential() #序列化模块构造的神经网络
self.model.add_module('conv1',nn.Conv2d(num_channels, num_features, 5, 2, 0, bias=False)) #卷积层
self.model.add_module('relu1',nn.ReLU()) #激活函数使用了ReLu
#self.model.add_module('relu1',nn.LeakyReLU(0.2, inplace = True)) #激活函数使用了leakyReLu,可以防止dead ReLu的问题
#第二层卷积
self.model.add_module('conv2',nn.Conv2d(num_features, num_features * 2, 5, 2, 0, bias=False))
self.model.add_module('bnorm2',nn.BatchNorm2d(num_features * 2))
self.model.add_module('linear1', nn.Linear(num_features * 2 * 4 * 4, #全链接网络层
num_features))
self.model.add_module('linear2', nn.Linear(num_features, 1)) #全链接网络层
self.model.add_module('sigmoid',nn.Sigmoid())
def forward(self,input):
output = input
# 对网络中的所有神经模块进行循环,并挑选出特定的模块linear1,将feature map展平
for name, module in self.model.named_children():
if name == 'linear1':
output = output.view(-1, num_features * 2 * 4 * 4)
output = module(output)
return output
准备训练 DCGAN
从上面的代码可以看到,辨别器就是一个普通的 CNN 分类器。接下来将实例化生成器和辨别器,编写训练函数,为 DCGAN 的训练做好准备。
# 构建一个生成器模型,并加载到GPU上
netG = ModelG().cuda() if use_cuda else ModelG()
# 初始化网络的权重
netG.apply(weight_init)
print(netG)
# 构建一个辨别器网络,并加载的GPU上
netD = ModelD().cuda() if use_cuda else ModelD()
# 初始化权重
netD.apply(weight_init)
# 要优化两个网络,所以需要有两个优化器
# 使用Adam优化器,可以自动调节收敛速度
optimizerD = optim.Adam(netD.parameters(),lr=0.0002,betas=(0.5,0.999))
optimizerG = optim.Adam(netG.parameters(),lr=0.0002,betas=(0.5,0.999))
#BCE损失函数
criterion = nn.BCELoss()
下面定义喂给生成器的随机噪声的生成方法。
# 生成一个随机噪声输入给生成器
noise = Variable(torch.FloatTensor(batch_size, input_dim, 1, 1))
# 固定噪声是用于评估生成器结果的,它在训练过程中始终不变
fixed_noise = Variable(torch.FloatTensor(batch_size, input_dim, 1, 1).normal_(0,1))
if use_cuda:
noise = noise.cuda()
fixed_noise = fixed_noise.cuda()
下面就要开始着手编写 DCGAN 的训练流程了。
模型的具体训练步骤如下:
1、读取一个 batch 的原始数据,将图像喂给辨别器,辨别器应该输出为真,计算误差:D_x
2、用随机噪声输入生成器,生成器创造一个 batch 的假图片,将这些图片输入给辨别器,辨别器应该输出为假,计算误差:D_x2
3、将两个误差合起来,反向传播训练辨别器
4、通过生成图像计算误差,对生成器进行反向传播更新梯度
def train_DCGAN(data, target):
global error_G
#训练辨别器网络
#清空梯度
optimizerD.zero_grad()
# 1、输入真实图片
data, target = Variable(data), Variable(target)
# 用于鉴别赝品还是真品的标签
label = Variable(torch.ones(data.size()[0])) #正确的标签是1(真实)
label = label.cuda() if use_cuda else label
if use_cuda:
data, target, label = data.cuda(), target.cuda(), label.cuda()
netD.train()
output=netD(data) #放到辨别网络里辨别
# 计算损失函数
label.data.fill_(1)
error_real=criterion(output, label)
error_real.backward() #辨别器的反向误差传播
D_x = output.data.mean()
# 2、用噪声生成一张假图片
noise.data.resize_(data.size()[0], input_dim, 1, 1).normal_(0, 1) #噪声是一个input_dim维度的向量
#喂给生成器生成图像
fake_pic = netG(noise).detach() #这里的detach是为了让生成器不参与梯度更新
output2 = netD(fake_pic) #用辨别器识别假图像
label.data.fill_(0) #正确的标签应该是0(伪造)
error_fake = criterion(output2, label) #计算损失函数
error_fake.backward() #反向传播误差
# 3、将两个误差合起来,反向传播训练辨别器
error_D = error_real + error_fake #计算真实图像和机器生成图像的总误差
optimizerD.step() #开始优化
# 4、单独训练生成器网络
if error_G is None or np.random.rand() < 0.5:
optimizerG.zero_grad() #清空生成器梯度
'''注意生成器的目标函数是与辨别器的相反的,故而当辨别器无法辨别的时候为正确'''
label.data.fill_(1) #分类标签全部标为1,即真实图像
noise.data.normal_(0,1) #重新随机生成一个噪声向量
netG.train()
fake_pic = netG(noise) #生成器生成一张伪造图像
output = netD(fake_pic) #辨别器进行分辨
error_G = criterion(output,label) #辨别器的损失函数
error_G.backward() #反向传播
optimizerG.step() #优化网络
if use_cuda:
error_D = error_D.cpu()
error_G = error_G.cpu()
return error_D, error_G
开始训练 DCGAN
激动人心的时候到了,下面将正式开始训练 DCGAN!要注意在 CPU 上训练这个对抗模型需要很久很久的时间,这在当前的实验环境中是难以接受的。笔者在后面提供了训练好的模型供读者加载使用,所以下面的训练代码在这里可以不执行。读者在自己的环境中训练的时候,只需要把以下代码中的 epoch 改为适当的数目即可。
error_G = None
# num_epochs = 100 #训练周期
num_epochs = 1 # 建议在自己的环境中完成完整训练
results = []
for epoch in range(num_epochs):
for batch_idx, (data, target) in enumerate(train_loader):
# 调用训练函数
error_D, error_G = train_DCGAN(data, target)
# 记录数据
results.append([float(error_D.data.numpy()), float(error_G.data.numpy())])
# 打印分类器损失等指标
if batch_idx % 100 == 0:
print ('第{}周期,第{}/{}撮, 分类器Loss:{:.2f}, 生成器Loss:{:.2f}'.format(
epoch,batch_idx,len(train_loader), error_D.data, error_G.data))
#生成一些随机图片,但应输出到文件
netG.eval()
fake_u=netG(fixed_noise)
fake_u = fake_u.cpu() if use_cuda else fake_u
img = make_show(fake_u)
#挑选一些真实数据中的图像图像保存
data, _ = next(iter(train_loader))
vutil.save_image(img,'DCGAN/fake%s.png'% (epoch))
# 保存网络状态到硬盘文件
torch.save(netG.state_dict(), '%s/netG_epoch_%d.pth' % ('DCGAN_model', epoch))
torch.save(netD.state_dict(), '%s/netD_epoch_%d.pth' % ('DCGAN_model', epoch))
if epoch == 0:
img = make_show(Variable(data))
vutil.save_image(img,'DCGAN/real%s.png' % (epoch))
同样绘制模型的误差曲线来观察模型的训练过程。由于没有执行完整训练,笔者提供了完整训练的误差曲线在下面
# 预测曲线
plt.figure(figsize = (10, 7))
plt.plot([i[1] for i in results], '.', label = 'Generator', alpha = 0.5)
plt.plot([i[0] for i in results], '.', label = 'Discreminator', alpha = 0.5)
plt.xlabel('Step')
plt.ylabel('Loss')
plt.legend()
这里是笔者在 GPU 上训练 100 个 epoch 所得到的误差曲线:
上图展示了生成器和辨别器的损失曲线。与之前实验中接触过的损失曲线不同,DCGAN 的曲线在经过初期相对稳定的一个阶段之后,很快就进入了剧烈且快速震荡的阶段。之所以造成这个局面,就在于生成器与辨别器构成了一对零和博弈。所以,当它们进入震荡局面的时候说明这两者正在激烈的竞争。因此,这也就使得我们无法简单地根据损失曲线来判别一个 GAN 系统的训练过程。
那么最后来看看 DCGAN 生成手写数字的效果到底如何:这里提供了笔者在 GPU 上训练了 100 个 epoch 的模型供大家使用。
netG = torch.load('netG_epoch_99_CPU.mdl')
netD = torch.load('netD_epoch_99_CPU.mdl')
if use_cuda:
netG = netG.cuda()
netD = netD.cuda()
# 绘制一些样本
noise = Variable(torch.FloatTensor(batch_size, input_dim, 1, 1))
noise.data.normal_(0,1)
noise = noise.cuda() if use_cuda else noise
sample_size = batch_size
netG.eval()
fake_u = netG(noise)
fake_u = fake_u.cpu() if use_cuda else fake_u
noise = noise.cpu() if use_cuda else samples
img = fake_u #.expand(sample_size, 3, image_size, image_size) #将张量转化成可绘制的图像
#print(img.size())
f, axarr = plt.subplots(8,8, sharex=True, figsize=(15,15))
for i in range(batch_size):
axarr[i // 8, i % 8].axis('off')
imshow(img[i].data, None,axarr[i // 8, i % 8])
这个效果明显比以前好了很多,已经可以清晰看到手写数字的样子了,这也是我们能在有限的训练周期内达到的最好效果了。最后,我们终于利用 GAN 的方法实现了一个合格的手写体数字图像生成器。但是,美中不足的是,当前这个版本无法做到根据指定的标签生成指定的数字。想要根据指定条件生成图像?Conditional GAN 了解一下。