基于生成对抗网络GAN,生成简单的数字模式

来源:知乎—See尚 侵删

地址:https://zhuanlan.zhihu.com/p/439286376

这一章,使用生成对抗网络来建立一个能自动生成固定数字 1010 模式的网络。

这个任务比生成图像更简单,这样能聚焦 GAN 的代码,而且能练习观察训练过程的方法,完成这个简单的任务后,能对生成图像做好更好的准备。

下面是这个任务的示意图:

23d716cbeffd66fecde213e26609d69f.png

其中,生成器(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 之间的低随机值。

89872e47ef5ef1480d056924f34a02b9.png


02

构建并测试鉴别器

2.1 构建鉴别器

和之前一样,它是一个继承自 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 个节点。

2c445ac0294d4a53d82d228fdbdaf0ce.png

正如之前, 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

2.2 测试鉴别器

目前还没有构建任何生成器,由于鉴别器需要与之竞争,所以这里并不能真正测试鉴别器,这里所做的只是检查鉴别器能否分辨真实和随机数据。

这听起来像一个没用的测试。但是这个测试告诉我们鉴别器最少具有分辨真实和随机噪声的能力(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()

我的图表如下所示,你的应该也不会有太大差别:

bdb0aa73142069a05eae01914fc75f5f.png

初始损失值大约在 0.25,且随着鉴别器越来越擅长对 1010 模式和噪声的分类, 损失值逐步接近 0。

为了测试训练结果,分别向鉴别器输入 1010 模式和随机值:

53b859bcc2e4d037e2dcedbba4ec5af4.png

理论上,如果输入的是 1010 模式,那么输出值应该是 1.0 附近(上图中是0.8029);如果输入的是随机值,那么输出应该接近 0.0(上图是0.06)。

上述结果说明,鉴别器可以分辨 1010 模式和随机值。


03

构建并测试生成器

3.1 构建生成器

关于生成器的几个考虑:

1. 由于生成器要能够学习,所以生成器应该是一个神经网络而不是单一函数。
2.由于生成器输出要能 “骗过” 鉴别器,所以生成器的输出要有 4 个节点。
3.至于隐藏层和输入层的要求。并没有明确规定,但它们应该足够大便于学习,但是又不能太大,否则训练时会占用非常长的时间,难以匹配鉴别器的学习速度。

基于上述原因,许多研究者使用鉴别器的镜像作为学习起点。

所以,这里尝试一个输入层有 1 个节点,隐藏层有 3 个节点,输出层有 4 个节点的生成器,它也是鉴别器的镜像。如下图所示:

809715c3abf4b14c1eaf15b33d95e4cb.png

和所有的神经网络一样,这个网络也需要一个输入。那使用什么作为生成器的输入呢?

现在,试用最简单的情况,给它输入一个常量。由于过大的值将使得训练更困难,而规范化的数据范围却有利于训练,所以先规定值为 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 中的代码完全一致。

3.2 检查生成器的输出

再次强调,编程中有必要对机器学习架构的单个元素进行验证,因此在使用生 成器训练之前,检查一下它是否输出了期望的结果。

运行下面的代码来生成一个新的 Generator 对象,并输入一个仅有 0.5 值的张量。

G = Generator()
G.forward(torch.FloatTensor([0.5]))

输出结果如下图所示:

6337e77ba933589ba01d8e18d84d1c7e.png

可以看到生成器的输出包括 4 个值,这也是生成器的基本要求;但需要注意的是,输出值并不是 1010 的模式,因为生成器目前还没有被训练。


04

训练 GAN

通过前面的工作,就做好了使用3 步训练循环对 GAN 进行训练的准备。有关3步训练循环,请参考下文:

https://zhuanlan.zhihu.com/p/431170265

4.1 训练的过程

观察下面代码:

# 创建鉴别器(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(),在该点切断了计算图的连接,如下图所示:

f8a70315b5eaccb3466fee519c47f673.png

为什么? 为什么要切断这个联系?即使不对生成器进行任何操作,计算生成器中的梯度也不会有真正的危害吧?事实上,可能在这个小例子中并没有什么大的伤害,但如果对更大的网络,节省的工作量可能会非常可观。

另一方面,步骤三中并不使用 detach(),是因为这步中,是想要误差梯度能流过从鉴别器损失到生成器的整条路径,同时生成器的函数仅仅更新生成器的连接权重,所以这里并不需要做任何特别的事情来防止鉴别器被更新。

运行代码。由于训练一个 GAN 耗费的时间较长,所以可以在顶部放置一个 %%time 命令,获取训练需要的时长:

e176a92d6361a39aa2de7d2ad2e9442f.png

4.2 观察鉴别器的损失值

之后,使用 D.plot_progress() 观察鉴别器的损失值随训练的变化情况:

5aadb6643d3256be8f7d2512eae77c42.png

前面(第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。

4.3 观察生成器的损失值

下面使用 G.plot_progress()来观察来自于训练生成器的损失值:

8742acf4082dae5e129c90ac872f5d89.png

可以看到,鉴别器一开始对将生成的模式分类为真的或假的不太自信,在训练进行到一半时,损失值略有上升,这表明生成器实际上已经改进,并开始愚弄鉴别器,最后可以看到生成器和鉴别器之间的平衡。

同时,图表中没有看到训练的完全失败,也没有看到损失值的剧烈波动表明学习不稳定。

下面来尝试已经训练好的生成器,来看它能创建了什么模式:

4ef664aa5c2789c5dfb8ec7ba2999f7e.png

可以看到生成器确实创建了 1010 模式,这些值的第一个和第三个数据是高值, 第二个和第四个是低值,高值大约为 0.9 左右,低值在 0.05 左右,两个值都很好。

4.4 观察生成器的演化情况

下面对生成器在训练期间生成的 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')

结果如下图:

568382371aee8266c008bd4940475fa7.png

这个图表清楚地显示出随着时间改变,生成器如何改进的。

初始时,生成器创建了一个很模糊的模式。但是,在训练的中途,生成器突然有能力生成一个 1010 模式,并且随着训练过程越来越清晰。
上面的工作,就建立了第一个 GAN 并训练成功。
同时,需要重点考虑一下,生成器 从未 直接看到过训练数据,但是已经学会了创建一个与训练数据类似的特定模式的数据。

05

总结

开发和训练一个 GAN 的可以包括下面的步骤:

(1)预览(preview) 实际数据集中获得的数据;

(2)测试(test)鉴别器是不是具备对实际数据和随机噪声进行分类的基本能力;

(3)测试 未经训练的生成器是不是能够创建正确形式的数据;

(4)通过 可视化 损失值来理解训练过程进展情况。

训练成功的 GAN 具有一个不能分辨真实数据和生成数据的鉴别器,鉴别器的输出为 0.5,所以理想的均方误差是 0.25;

分别对鉴别器和生成器的损失值进行可视化很有用,其中 鉴别器损失值 (generator loss) 是由于生成的数据引起的鉴别器的损失值。

猜您喜欢:

9e36804cfb5c8d2181419861f081306f.png 戳我,查看GAN的系列专辑~!

一顿午饭外卖,成为CV视觉的前沿弄潮儿!

超110篇!CVPR 2021最全GAN论文汇总梳理!

超100篇!CVPR 2020最全GAN论文梳理汇总!

拆解组新的GAN:解耦表征MixNMatch

StarGAN第2版:多域多样性图像生成

附下载 | 《可解释的机器学习》中文版

附下载 |《TensorFlow 2.0 深度学习算法实战》

附下载 |《计算机视觉中的数学方法》分享

《基于深度学习的表面缺陷检测方法综述》

《零样本图像分类综述: 十年进展》

《基于深度神经网络的少样本学习综述》

2e1d7de9882630b7a38efa9ed580ede9.png

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值