发布时间:2023-03-17 09:30
来源:知乎—See尚 侵删
地址:https://zhuanlan.zhihu.com/p/439286376
这一章,使用生成对抗网络来建立一个能自动生成固定数字 1010 模式的网络。
这个任务比生成图像更简单,这样能聚焦 GAN 的代码,而且能练习观察训练过程的方法,完成这个简单的任务后,能对生成图像做好更好的准备。
下面是这个任务的示意图:
其中,生成器(generator) 是一个能输出 4 个值的神经网络。同时我们期望训练后输出的值是 1010 模式;鉴别器(discriminator) 则获取 4 个值的模式,在训练中确定其来自真实数据源或者来自于生成器。
下面按顺序编程,首先新建一个 notebook 文件并导入标准库:
import torch
import torch.nn as nn
import pandas
import matplotlib.pyplot as plt
01
实际数据源函数
实际的数据源可以是一个能始终返回 1010 模式的函数:
def generate_real():
real_data = torch.FloatTensor([1, 0, 1, 0])
return real_data
实际的数据很少这么精确或恒定,所以通过增加一些高值和低值来增加随机性。
我们通过导入 Python 的 random 模块来使用其 random.uniform() 函数。
def generate_real():
real_data = torch.FloatTensor(
[random.uniform(0.8, 1.0),
random.uniform(0.0, 0.2),
random.uniform(0.8, 1.0),
random.uniform(0.0, 0.2)])
return real_data
检查这个函数的 4 个返回值,第 1 个和第 3 个是在 0.8 和 1.0 之间的高随机值,而第 2 个和第 4 个值是一个在 0.0 和 0.2 之间的低随机值。
02
构建并测试鉴别器
和之前一样,它是一个继承自 nn.Module 类的神经网络,并按照 PyTorch 的要求来对网络进行初始化并提供一个 forward 函数。
观察下面 Discriminator 类的构造函数:
class Discriminator(nn.Module):
def __init__():
# 初始化 PyTorch 父类
super().__init__()
# 定义神经网络各层:线性层-sigmoid函数-线性层-sigmoid函数
self.model = nn.Sequential(
nn.Linear(4, 3),
nn.Sigmoid(),
nn.Linear(3, 1),
nn.Sigmoid()
)
# 使用均方值误差作为损失函数
self.loss_function = nn.MSELoss()
# 使用随机梯度下降(SGD)创建优化器
self.optimiser = torch.optim.SGD(self.parameters(), lr=0.01)
# 计数器和累加器,用于流程控制和显示
self.counter = 0
self.progress = []
pass
这里的代码几乎与之前的代码一模一样:使用了 nn.Sequential 定义了网络各层,使用了均方值误差作为损失函数和随机梯度下降法作为优化器。同时,和之前一样,也创建了一个 counter 和 progress 列表来在训练过程中对损失值进行跟踪。
网络自身很简单,它的输入层有 4 个输入值,这是因为输入的模式包括 4 个值;最后层的输出只有 1 个值,使用 1 代表 True,0 代表 False;中间隐藏层有 3 个节点。
正如之前, forward() 函数能简单的调用前面的模型,传输数据并返回网络的输出:
def forward(self, inputs):
# 简单的运行模型
return self.model(inputs)
训练函数 train() 与之前开发的代码也一致:
def train(self, inputs, targets):
# 计算网络的输出
outputs = self.forward(inputs)
# 计算损失值
loss = self.loss_function(outputs, targets)
# 每运行10次,增加计数器和累加器
self.counter += 1
if (self.counter % 10 == 0):
self.progress.append(loss.item())
pass
if (self.counter % 10000 == 0):
print("counter = ", self.counter)
pass
# 将梯度置零,并运行反向更新权重
self.optimiser.zero_grad()
loss.backward()
self.optimiser.step()
pass
上面是训练函数的标准模式。
首先获取输入 inputs 情况下的网络 outputs;通过与 targets 比较计算获得 loss 的值;网络内部的梯度通过这个 loss 计算获得,然后可学习参数使用优化器的单步进行更新。
同样,跟踪 train() 函数调用的次数来获得 counter,每 10 次调用就将 loss 值加入到 progress 列表中。
最后,增加一个函数 plot_progress() 来画出训练过程中损失值的变化情况,之前代码也使用过:
def plot_progress(self):
df = pandas.DataFrame(self.progress, columns=['loss'])
df.plot(ylim=(0, 1.0), figsize=(16, 8), alpha=0.1, marker='.', grid=True, yticks=(0, 0.25, 0.5))
pass
总的来看,这里的代码和之前 MNIST 分类器的代码非常相似,具体的可以参考系列文章中的 这篇文章。
https://zhuanlan.zhihu.com/p/402434154
目前还没有构建任何生成器,由于鉴别器需要与之竞争,所以这里并不能真正测试鉴别器,这里所做的只是检查鉴别器能否分辨真实和随机数据。
这听起来像一个没用的测试。但是这个测试告诉我们鉴别器最少具有分辨真实和随机噪声的能力(capacity)。如果连这个都做不了,鉴别器应该 难以完成分辨看起来像真实数据的假数据的任务,所以,这个测试将找出不擅长于生成器竞争的鉴别器。
创建一个函数生成随机噪声模式:
def generate_random(size):
random_data = torch.rand(size)
return random_data
相比前面的 generate_real() 的函数,这个函数更加通用,能获得给定尺寸的张量,所以 generate_random(4) 将返回一个长度为 4 的,每个值都是在 0 和 1 之间随机值的张量。
下面使用训练循环来训练鉴别器,如果鉴别器可以完成如下分类则进行奖赏:
• 当 目标(target) 为 1.0时,将真实的 1010 模式数据分类为 真实(real);
• 当 目标(target) 为 0.0时,将随机噪声数据分类为 虚假(false)。
训练循环如下所示:
D = Discriminator()
for i in range(10000):
# 真实数据
D.train(generate_real(), torch.FloatTensor([1.0]))
# 虚假数据
D.train(generate_random(), torch.FloatTensor([0.0]))
pass
训练循环运行 10,000 次。
鉴别器的 train() 函数传入了 generate_real() 函数产生的真实数据和代表真实的值为 1.0 的张量作为训练目标:这将在具有 1010 模式的真实数据时,鼓励网络输出 1.0。
类似的,鉴别器的 train() 函数也输入了 generate_random() 产生的随机噪声和值为 0.0 的目标值,用来鼓励它在输入非 1010 模式的数据时,输出 0.0。
在一个新的 cell 中运行训练循环代码,运行完成大约需 10 秒左右。运行完成后,将画出损失值的图表来观察训练的情况:
D.plot_progress()
我的图表如下所示,你的应该也不会有太大差别:
初始损失值大约在 0.25,且随着鉴别器越来越擅长对 1010 模式和噪声的分类, 损失值逐步接近 0。
为了测试训练结果,分别向鉴别器输入 1010 模式和随机值:
理论上,如果输入的是 1010 模式,那么输出值应该是 1.0 附近(上图中是0.8029);如果输入的是随机值,那么输出应该接近 0.0(上图是0.06)。
上述结果说明,鉴别器可以分辨 1010 模式和随机值。
03
构建并测试生成器
关于生成器的几个考虑:
1. 由于生成器要能够学习,所以生成器应该是一个神经网络而不是单一函数。
2.由于生成器输出要能 “骗过” 鉴别器,所以生成器的输出要有 4 个节点。
3.至于隐藏层和输入层的要求。并没有明确规定,但它们应该足够大便于学习,但是又不能太大,否则训练时会占用非常长的时间,难以匹配鉴别器的学习速度。
基于上述原因,许多研究者使用鉴别器的镜像作为学习起点。
所以,这里尝试一个输入层有 1 个节点,隐藏层有 3 个节点,输出层有 4 个节点的生成器,它也是鉴别器的镜像。如下图所示:
和所有的神经网络一样,这个网络也需要一个输入。那使用什么作为生成器的输入呢?
现在,试用最简单的情况,给它输入一个常量。由于过大的值将使得训练更困难,而规范化的数据范围却有利于训练,所以先规定值为 0.5。如果有问题的话,后面可以再改变。
下面定义了 Generator 类,代码拷贝自 Discrimintor 类并做了一些修改:
class Generator(nn.Module):
def __init__(self):
# 初始化 PyTorch 父类
super().__init__()
# 定义神经网络各层
self.model = nn.Sequential(
nn.Linear(1, 3),
nn.Sigmoid(),
nn.Linear(3, 4),
nn.Sigmoid()
)
# 使用随机梯度下降(SGD)创建优化器
self.optimiser = torch.optim.SGD(self.parameters(), lr=0.01)
# 计数器和累加器,用于流程控制和显示
self.counter = 0
self.progress = []
pass
def forward(self, inputs):
# 简单的运行模型
return self.model(inputs)
上面代码中,与 Discrimintor 类不同的主要是神经网络各层的定义。
同时,这个代码没有 self.loss。因为并不需要。
为什么不需要 self.loss 呢?如果返回去观察 GAN 的训练循环,可以发现生成器是基于鉴别器损失值返回的误差梯度进行更新的。
进一步考虑生成器的 train() 函数。
训练生成器与训练鉴别器稍有不同:
- 对鉴别器而言,我们知道目标输出应该是什么;
- 但对于生成器,我们并不知道目标输出。训练中仅有的信息是从损失值获得的后向传播梯度。而这个损失值是由前面探讨过的 GAN 训练循环的第 3 步中的鉴别器输出中计算获得的。
所以,对生成器训练也需要鉴别器。
所以,这里将鉴别器传递到生成器的 train() 函数中,使得训练循环保持简洁,代码如下:
def train(self, D, inputs, targets):
# 将 inpusts 输入到生成器网络中,计算生成器网络的输出 g_output
g_output = self.forward(inputs)
# 将生成器网络的输出,传递到鉴别器中,获得鉴别器的输出 d_output
d_output = D.forward(g_output)
# 计算鉴别器的损失值 loss
loss = D.loss_function(d_output, targets)
# 每 10 次运行增加计数器和损失值到列表中
self.counter += 1
if (self.counter % 10 == 0):
self.progress.append(loss.item())
pass
# 将梯度置零,执行反向更新权重
self.optimiser.zero_grad()
loss.backward()
self.optimiser.step()
pass
结合代码里的注释进行进一步解释:
1. 使用 self.forward(inputs) 将输入 inputs 输入到生成器网络中,其输出为 g_output;然后将 g_output 通过 D.forward(g_output) 输入到鉴别器网络中,获得鉴别器的输出 d_output;
2. 使用 d_output 和想要的目标值计算获得损失值 loss 。误差梯度的反向传播从这个损失值 loss 开始,沿着计算图,通过鉴别器然后到达生成器;
3. 在更新权重时,使用 self.optimiser 而不是 D.optimiser 触发进行更新;这样,就只有生成器的连接权重被更新了,正如 GAN 训练网络训练过程中的第 3 步所示。
除了上面三个过程,代码同样去掉了生成器的 train() 函数计数器的输出,因为鉴别器的 train() 也已经完成了这个,同时鉴别器通过实际的训练数据,能更准确反映训练的进展。
最后,代码增加了 plot_progress() 函数到 Generator 类中,这个函数的代码与 Discriminator 中的代码完全一致。
再次强调,编程中有必要对机器学习架构的单个元素进行验证,因此在使用生 成器训练之前,检查一下它是否输出了期望的结果。
运行下面的代码来生成一个新的 Generator 对象,并输入一个仅有 0.5 值的张量。
G = Generator()
G.forward(torch.FloatTensor([0.5]))
输出结果如下图所示:
可以看到生成器的输出包括 4 个值,这也是生成器的基本要求;但需要注意的是,输出值并不是 1010 的模式,因为生成器目前还没有被训练。
04
训练 GAN
通过前面的工作,就做好了使用3 步训练循环对 GAN 进行训练的准备。有关3步训练循环,请参考下文:
https://zhuanlan.zhihu.com/p/431170265
观察下面代码:
# 创建鉴别器(Discriminator)和生成器(Generator)
D = Discriminator()
G = Generator()
# 训练鉴别器(Discriminator)和生成器(Generator)
for i in range(10000):
# 训练的第一步:使用真实数据训练鉴别器
D.train(generate_real(), torch.FloatTensor([1.0]))
# 训练的第二步:使用虚假数据训练鉴别器
# 特别提醒:使用 detach(),使得生成器(G)中的梯度不被计算
D.train(G.forward(torch.FloatTensor([0.5])).detach(), torch.FloatTensor([0.0]))
# 训练的第三步:训练生成器
G.train(D, torch.FloatTensor([0.5]), torch.FloatTensor([1.0]))
pass
结合注释,对代码进行解释:
总体来看,代码创建崭新的鉴别器和生成器对象,之后运行训练循环 10,000 次。
在训练循环内部,能看到之前探讨的 GAN 循环的 3 个步骤:
- 步骤一: 使用真实数据训练了鉴别器;
- 步骤二: 使用生成器生成的(虚假)数据来训练鉴别器。
- 步骤三: 使用生成器 D ,常量 0.5 和张量 1.0 输入到鉴别器的训练函数中,对鉴别器开展训练。
需要注意的是,步骤二中使用了 detach()。这是由于这步的目标是为了对鉴别器进行训练,并不需要计算生成器的梯度,所以对生成器使用了 detach(),在该点切断了计算图的连接,如下图所示:
为什么? 为什么要切断这个联系?即使不对生成器进行任何操作,计算生成器中的梯度也不会有真正的危害吧?事实上,可能在这个小例子中并没有什么大的伤害,但如果对更大的网络,节省的工作量可能会非常可观。
另一方面,步骤三中并不使用 detach(),是因为这步中,是想要误差梯度能流过从鉴别器损失到生成器的整条路径,同时生成器的函数仅仅更新生成器的连接权重,所以这里并不需要做任何特别的事情来防止鉴别器被更新。
运行代码。由于训练一个 GAN 耗费的时间较长,所以可以在顶部放置一个 %%time 命令,获取训练需要的时长:
之后,使用 D.plot_progress() 观察鉴别器的损失值随训练的变化情况:
前面(第2.2节)提过,我们期望鉴别器的损失值在训练过程中逐步接近0,这表明鉴别器更擅长分辨真假。
但是这里的损失值保持在大约 0.25!!WHY ? ?
这里的 0.25 并没有问题。这是因为当鉴别器不擅长从假数据中识别真实数据时,它会不确定是输出 0.0 还是 1.0 ,所以会输出 0.5,因为这里使用的是均方误差,损失值是 0.5 的平方,即 0.25。
观察上图,随着训练开展,损失值轻微的下降,但幅度并不大,这表明鉴别器神经网络改进了一些,可能是更擅长识别真实的 1010 模式了,也可能是更擅长发现生成的模式是假的了,当然也可能是两种能力都具备了。
训练结束时,损失值恢复到 0.25。这很好。这意味着生成器已经学会了生成真实的模式,而鉴别器无法区分真实的 1010 模式和生成器生成的模式。这意味着鉴别器的输出将是 0.5,这就是为什么损失值(均方值误差)会上升到 0.25。
下面使用 G.plot_progress()来观察来自于训练生成器的损失值:
可以看到,鉴别器一开始对将生成的模式分类为真的或假的不太自信,在训练进行到一半时,损失值略有上升,这表明生成器实际上已经改进,并开始愚弄鉴别器,最后可以看到生成器和鉴别器之间的平衡。
同时,图表中没有看到训练的完全失败,也没有看到损失值的剧烈波动表明学习不稳定。
下面来尝试已经训练好的生成器,来看它能创建了什么模式:
可以看到生成器确实创建了 1010 模式,这些值的第一个和第三个数据是高值, 第二个和第四个是低值,高值大约为 0.9 左右,低值在 0.05 左右,两个值都很好。
下面对生成器在训练期间生成的 1010 模式如何演化进行可视化。
为了完成这个任务,在训练循环前,创建一个空的列表 image_list ,然后每 1000 次训练循环存储生成器的输出。
# 每 1000 次训练,增加到 image_list 中一次结果
if (i % 1000 == 0):
image_list.append(G.forward(torch.FloatTensor([0.5]).detach().numpy())
代码中为了使用 numpy 数组的形式,提取生成器输出张量的值,我们需要在使用 numpy() 函数之前,使用 detach() 将其值和计算图进行隔离。同时,需要在代码顶端导入 numpy 库。
在 10,000 次训练后,在 image_list 中应该有 10 个模式值。下面的代码将这 10 个模式值(每个值都有 4 个数字)的列表转换为一个 10✖️4 的 numpy 数组,然后进行翻转, 这样能看到逐渐向右是如何改进的。
plt.figure(figsize = (16, 8))
plt.imshow(numpy.array(iamge_list).T, interpolation = 'none', cmap = 'Blues')
结果如下图:
这个图表清楚地显示出随着时间改变,生成器如何改进的。
初始时,生成器创建了一个很模糊的模式。但是,在训练的中途,生成器突然有能力生成一个 1010 模式,并且随着训练过程越来越清晰。
上面的工作,就建立了第一个 GAN 并训练成功。
同时,需要重点考虑一下,生成器 从未 直接看到过训练数据,但是已经学会了创建一个与训练数据类似的特定模式的数据。
05
总结
开发和训练一个 GAN 的可以包括下面的步骤:
(1)预览(preview) 实际数据集中获得的数据;
(2)测试(test)鉴别器是不是具备对实际数据和随机噪声进行分类的基本能力;
(3)测试 未经训练的生成器是不是能够创建正确形式的数据;
(4)通过 可视化 损失值来理解训练过程进展情况。
训练成功的 GAN 具有一个不能分辨真实数据和生成数据的鉴别器,鉴别器的输出为 0.5,所以理想的均方误差是 0.25;
分别对鉴别器和生成器的损失值进行可视化很有用,其中 鉴别器损失值 (generator loss) 是由于生成的数据引起的鉴别器的损失值。
猜您喜欢:
一顿午饭外卖,成为CV视觉的前沿弄潮儿!
超110篇!CVPR 2021最全GAN论文汇总梳理!
超100篇!CVPR 2020最全GAN论文梳理汇总!
拆解组新的GAN:解耦表征MixNMatch
StarGAN第2版:多域多样性图像生成
附下载 | 《可解释的机器学习》中文版
附下载 |《TensorFlow 2.0 深度学习算法实战》
附下载 |《计算机视觉中的数学方法》分享
《基于深度学习的表面缺陷检测方法综述》
《零样本图像分类综述: 十年进展》
《基于深度神经网络的少样本学习综述》