使用 PyTorch 学习生成式人工智能——生成对抗网络:形状与数字生成

272 阅读31分钟

本章内容包括

  • 从零开始构建生成对抗网络(GAN)的生成器和判别器网络
  • 使用GAN生成数据点以形成特定形状(如指数增长曲线)
  • 生成所有为5的倍数的整数序列
  • GAN的训练、保存、加载与使用
  • 评估GAN性能并确定训练终止时机

本书近一半的生成模型属于生成对抗网络(GAN)这一类别。GAN由Ian Goodfellow及其合作者于2014年首次提出。GAN以其实现简便和高度通用性著称,使得即使是对深度学习了解有限的人,也能从头构建自己的模型。GAN中的“对抗”指的是两个神经网络在零和博弈框架中相互竞争——生成网络试图创造与真实样本无法区分的数据实例,而判别网络则试图区分真假样本。这类灵活的模型能生成多种内容形式,从几何图形和数字序列,到高分辨率彩色图像乃至逼真音乐作品。

本章将简要回顾GAN的理论基础,随后展示如何在PyTorch中实现。你将学会从零开始构建第一个GAN,揭开所有细节。为了便于理解,假设你把1美元存入年利率为8%的储蓄账户,想根据存款年数计算账户余额,其真实关系为指数增长曲线。你将用GAN生成符合数学关系y = 1.08^x的(x, y)数据对,学会用GAN模拟任意形状的曲线,如正弦、余弦、二次曲线等。

本章的第二个项目是用GAN生成全为5的倍数的数字序列。你也可以修改为2、3、7的倍数或其他模式。过程中,你将学习如何从零构建生成器和判别器网络,如何训练、保存和使用GAN。此外,你还会学习如何评估GAN性能——无论是通过可视化生成器输出样本,还是计算生成样本分布与真实数据分布之间的差异。

设想你需要数据训练机器学习模型以预测(x, y)值对之间的关系,但准备训练数据既昂贵又耗时。GAN能很好地生成此类数据:生成的x和y值通常遵循数学关系,但带有一定噪声,噪声有助于防止训练时模型过拟合。

本章的主要目标不一定是生成最实用的新内容,而是教你如何训练和使用GAN从零开始创造多种形式的内容。通过学习,你将深入理解GAN的内部机制。这一基础将为后续章节的进阶内容打下坚实基础,比如生成高分辨率图像或逼真音乐时使用的卷积神经网络,或如何将音乐表示为多维对象。

3.1 训练生成对抗网络(GAN)的步骤

在第1章中,你已经获得了关于GAN理论的宏观概览。本节将总结训练GAN的一般步骤,特别是如何生成符合指数增长曲线的数据点。

回到之前的例子:你计划将1美元存入年利率8%的储蓄账户,想知道未来账户里会有多少钱。

账户未来余额y取决于你投资的年数x,x可以是0到50之间的任意数值。例如,投资1年,余额是1.08美元;投资2年,余额是1.08² = 1.17美元。通用公式为 y = 1.08ˣ,这描绘了指数增长曲线。这里的x既可以是整数(如1、2),也可以是小数(如1.14、2.35),公式依然适用。

训练GAN以生成符合特定数学关系(如上述公式)的数据点(x, y)是一个多步骤过程。图3.1展示了GAN架构及生成指数增长曲线的各个步骤。当你生成其他内容(如整数序列、图像或音乐)时,也遵循类似流程,正如本章第二个项目以及本书后续的其他GAN模型所示。

image.png

图3.1 展示了训练GAN以生成指数增长曲线的步骤以及GAN的双网络架构。生成器从潜在空间(图左上角)获取随机噪声向量Z,用于创建假样本,并将其呈现给判别器(图中)。判别器负责判断样本是真实(来自训练集)还是生成的假样本。模型的预测结果与真实标签进行比较,生成器和判别器基于此学习。经过多次迭代训练后,生成器能够创造出与真实样本无法区分的形状。

在开始之前,我们需要获得训练数据集来训练GAN。在本例中,我们根据数学关系y = 1.08ˣ生成(x, y)数据对。以储蓄账户为例,使数值更易理解。你所学的技术同样适用于生成其他形状,比如正弦、余弦、U形等。你可以选择x的取值范围(例如0到50)并计算对应的y值。深度学习通常按批次训练模型,因此训练数据集的样本数量通常是批次大小的整数倍。图3.1顶部展示了一个真实样本,呈指数增长曲线形状。

训练集准备好后,你需要在GAN中创建两个网络:生成器和判别器。生成器位于图3.1左下角,输入随机噪声向量Z,生成数据点(训练循环第1步)。生成器使用的噪声向量Z来自潜在空间,潜在空间表示GAN可能输出的全部范围,是GAN生成多样化数据样本的核心。第5章将深入探索潜在空间,用以选择生成内容的属性。判别器位于图3.1中间,评估给定的数据点(x, y)是真实的(来自训练集)还是生成的(由生成器产生),这是训练循环第2步。

潜在空间的含义

GAN中的潜在空间是一个概念空间,生成器能将空间中的每一点转换成真实的数据实例。它代表了GAN能够生成的多样且复杂的数据范围。潜在空间的意义仅在于与生成模型结合使用时体现,你可以在潜在空间中插值以调整输出属性,第5章将详细讨论。

为了调整模型参数,我们必须选择合适的损失函数。需要为生成器和判别器分别定义损失函数。生成器的损失函数鼓励其生成与训练集数据点相似的数据,使判别器将其判定为真实;判别器的损失函数则鼓励其正确区分真实和生成的数据点。

在训练循环的每次迭代中,我们交替训练判别器和生成器。训练判别器时,从训练集中采样一批真实数据点(x, y),同时采样一批由生成器生成的假数据点。判别器输出样本属于训练集的概率,将其与真实标签(真实为1,假样本为0,见图3.1右侧)比较,这是训练循环第3步的一半。然后稍微调整判别器权重,使下一次迭代中预测概率更接近真实标签(训练循环第4步的一半)。

训练生成器时,将假样本输入判别器,获得假样本被判定为真实的概率(训练循环第3步另一半)。然后稍微调整生成器权重,使下一次迭代中预测概率更接近1(因为生成器想骗过判别器,让其误判为真实),这是训练循环第4步的另一半。重复此过程多次,使生成器不断生成更真实的数据点。

自然的问题是,何时停止训练GAN?我们通过生成一组合成数据点,并将其与训练集真实数据点进行比较来评估GAN性能。通常采用可视化方法来判断生成数据与目标关系的吻合程度。但在本例中,由于已知训练数据分布,可计算生成数据与真实数据分布之间的均方误差(MSE)。当生成样本在若干训练轮次后质量不再提升时,即停止训练。

此时模型被认为已训练完成,我们丢弃判别器,仅保留生成器。要生成指数增长曲线,只需向训练好的生成器输入随机噪声向量Z,即可得到满足所需形状的(x, y)对。

3.2 准备训练数据

本节中,你将创建训练数据集,以便后续训练本章中的GAN模型。具体来说,你将生成符合指数增长形状的(x, y)数据对,并将它们分批处理,方便输入深度神经网络。

:本章及其他章节代码均可在本书GitHub仓库获取:github.com/markhliu/DG…

3.2.1 形成指数增长曲线的训练数据集

我们将创建包含大量(x, y)数据对的数据集,其中x均匀分布在区间[0, 50]内,y根据公式y = 1.08ˣ计算。代码如下:

清单3.1 创建指数增长形状训练数据

import torch

torch.manual_seed(0)                                # ①

observations = 2048

train_data = torch.zeros((observations, 2))         # ②

train_data[:, 0] = 50 * torch.rand(observations)    # ③

train_data[:, 1] = 1.08 ** train_data[:, 0]         # ④

① 固定随机状态,保证结果可复现
② 创建一个2048行2列的张量
③ 生成0到50之间的x值
④ 根据y = 1.08ˣ计算y值

首先,使用torch.rand()生成2048个0到1之间的随机数,再乘以50,得到x在0到50之间的随机值。使用manual_seed()固定随机状态,保证每次运行结果一致。我们创建一个形状为(2048, 2)的张量train_data,将x值放入第一列,再计算y值填充第二列。

练习3.1
修改清单3.1,使x与y的关系变为y = sin(x),用torch.sin()函数实现。令x值分布在-5到5之间,可使用以下代码:

train_data[:, 0] = 10 * (torch.rand(observations) - 0.5)

我们用Matplotlib绘制x和y的关系图。

清单3.2 可视化x与y的关系

import matplotlib.pyplot as plt

fig = plt.figure(dpi=100, figsize=(8, 6))
plt.plot(train_data[:, 0], train_data[:, 1], ".", c="r")    # ①
plt.xlabel("values of x", fontsize=15)
plt.ylabel("values of $y=1.08^x$", fontsize=15)             # ②
plt.title("An exponential growth shape", fontsize=20)       # ③
plt.show()

① 绘制x与y的关系
② y轴标签
③ 图表标题

运行清单3.2后,你会看到一条指数增长曲线,类似图3.1顶部的图形。

练习3.2
基于练习3.1的修改,调整清单3.2绘制y = sin(x)的关系图,并修改y轴标签和标题以匹配新函数。

3.2.2 准备训练数据集

将刚生成的数据样本分批,以便输入判别器网络。使用PyTorch的DataLoader()类将训练数据包装成可迭代的批次数据,方便训练时访问:

from torch.utils.data import DataLoader

batch_size = 128
train_loader = DataLoader(
    train_data,
    batch_size=batch_size,
    shuffle=True)

确保选择的样本总数和批次大小使每个批次包含相同数量的样本。这里选择了2048个样本,批次大小为128,因此有2048/128 = 16个批次。shuffle=True参数会在分批前随机打乱样本顺序。

:打乱样本确保数据均匀分布,避免批次内样本相关性,从而稳定训练。在本例中,打乱保证x值均匀分布在0到50之间,而不是集中在某个区间(如0到5)。

你可以用next()iter()方法访问一个批次的数据:

batch0 = next(iter(train_loader))
print(batch0)

输出将是128对(x, y)数据,其中x随机分布于0到50,且每对数据满足y = 1.08ˣ的关系。

3.3 创建生成对抗网络(GAN)

训练数据集准备好后,我们将创建判别器网络和生成器网络。判别器是一个二分类器,类似于第2章中创建并训练的服装二分类模型,负责将样本判定为真实或伪造。生成器则尝试创造与训练集样本无法区分的数据点(x, y),以骗过判别器将其判为真实。

3.3.1 判别器网络

我们使用PyTorch创建判别器神经网络,采用全连接(密集)层和ReLU激活函数,并使用Dropout层防止过拟合。以下代码示例展示了判别器的构建。

清单3.3 创建判别器网络

import torch.nn as nn

device = "cuda" if torch.cuda.is_available() else "cpu"    # ①

D = nn.Sequential(
    nn.Linear(2, 256),                                      # ②
    nn.ReLU(),
    nn.Dropout(0.3),                                        # ③
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(64, 1),                                       # ④
    nn.Sigmoid()
).to(device)

① 自动检测是否有CUDA支持的GPU
② 第一层输入特征数为2,匹配数据点中的x和y两个数值
③ Dropout层防止过拟合
④ 最后一层输出特征数为1,方便压缩为0到1之间的概率

确保第一层输入维度为2,因为每个数据点有两个数值x和y。第一层输入数应与输入数据大小匹配。最后一层输出数为1,判别器输出单个值,通过Sigmoid激活函数压缩到[0,1]区间,代表样本为真实的概率p,1-p则为伪造概率。这与第2章二分类模型中判定服装为踝靴或T恤的做法类似。

隐藏层神经元数分别为256、128和64,这些数字并非固定不变,合理范围内调整不会显著影响效果。隐藏层神经元过多可能导致过拟合,过少可能欠拟合。神经元数目可通过验证集超参数调优单独优化。

Dropout层随机“丢弃”一定比例的神经元,意味着这些神经元在训练时不会参与前向和反向传播。过拟合是模型不仅学到了训练数据的模式,还过度拟合了噪声,导致对未见数据表现差。Dropout是一种有效防止过拟合的方法。

3.3.2 生成器网络

生成器的任务是生成一对数字(x, y),以通过判别器的检测。也就是说,生成器试图创造数据对,使判别器判断其来自训练集(符合y = 1.08ˣ关系)的概率最大化。以下代码创建生成器神经网络。

清单3.4 创建生成器网络

G = nn.Sequential(
    nn.Linear(2, 16),             # ①
    nn.ReLU(),
    nn.Linear(16, 32),
    nn.ReLU(),
    nn.Linear(32, 2)              # ②
).to(device)

① 第一层输入特征数为2,与潜在空间的随机噪声向量维度一致
② 最后一层输出特征数为2,与数据样本维度一致,包含(x, y)两个数值

我们将从二维潜在空间输入一个随机噪声向量(z₁, z₂)给生成器,生成对应的(x, y)对。这里使用二维潜在空间,改成5维、10维等不会影响结果。

3.3.3 损失函数、优化器与早停

判别器本质上执行二分类任务(判断数据是真实或伪造),因此使用二分类交叉熵损失函数(binary cross-entropy loss),这也是二分类中首选的损失函数。判别器的目标是最大化二分类准确率:正确判定真实样本为真,伪造样本为假。判别器权重基于损失函数对权重的梯度更新。

生成器的目标是最小化其生成样本被判为假的概率,因此也使用二分类交叉熵损失函数,更新生成器权重,使生成样本被判别器误判为真。

同第2章一样,使用Adam优化器,学习率设为0.0005。代码如下:

loss_fn = nn.BCELoss()
lr = 0.0005
optimD = torch.optim.Adam(D.parameters(), lr=lr)
optimG = torch.optim.Adam(G.parameters(), lr=lr)

训练前有个问题:训练多少epoch合适?如何判断模型训练充分,生成器已能生成符合指数增长曲线形状的样本?回忆第2章,我们曾将训练集拆分为训练集和验证集,用验证集损失判断参数是否收敛并决定停止训练。但GAN训练与传统监督学习不同(如第2章的分类模型),因为生成样本质量随训练提升,判别器任务越来越难(可视为判别器在对“移动的靶子”做预测),判别器损失不能很好地反映模型质量。

一种常用评估GAN性能的方法是通过人工目视判断,观察生成数据的质量和逼真度。此方法是定性评估,但非常有信息量。

在本简单示例中,由于已知训练数据分布,我们用生成样本与真实样本之间的均方误差(MSE)作为生成器性能指标。代码如下:

mse = nn.MSELoss()                                # ①

def performance(fake_samples):
    real = 1.08 ** fake_samples[:, 0]              # ②
    mseloss = mse(fake_samples[:, 1], real)        # ③
    return mseloss

① 使用均方误差作为性能衡量标准
② 计算真实数据分布
③ 计算生成数据与真实分布的均方误差

当生成器性能在固定的训练轮数(如1000个epoch)内未见提升时,我们停止训练。因此,定义早停类(与第2章类似)来判断训练何时终止。

清单3.5 早停类判断训练终止

class EarlyStop:
    def __init__(self, patience=1000):       # ①
        self.patience = patience
        self.steps = 0
        self.min_gdif = float('inf')
    def stop(self, gdif):                    # ②
        if gdif < self.min_gdif:             # ③
            self.min_gdif = gdif
            self.steps = 0
        elif gdif >= self.min_gdif:
            self.steps += 1
        if self.steps >= self.patience:      # ④
            return True
        else:
            return False

stopper = EarlyStop()

① 默认耐心值(patience)设为1000
② 定义stop()方法
③ 若生成器误差达到新低,更新最小误差值
④ 若连续patience轮无改进,停止训练

至此,我们具备训练GAN所需的全部组件,下一节将开始正式训练。

3.4 训练和使用GAN生成形状

现在训练数据和两个网络都准备好了,我们开始训练模型。训练结束后,丢弃判别器,只用生成器生成数据点,形成指数增长曲线形状。

3.4.1 GAN的训练

首先为真实样本和伪造样本分别创建标签,真实样本标记为1,伪造样本标记为0。在训练过程中,判别器将自身预测结果与标签对比,以获得反馈,进而调整参数,在下一次迭代中做出更准确的预测。

定义两个张量real_labelsfake_labels

real_labels = torch.ones((batch_size, 1))
real_labels = real_labels.to(device)

fake_labels = torch.zeros((batch_size, 1))
fake_labels = fake_labels.to(device)

real_labels是二维张量,形状为(batch_size, 1),这里为128行1列,对应判别器接收128个真实样本并产生128个预测。fake_labels同理,喂入128个伪造样本,预测后与这128个0标签对比。若有CUDA支持,两个张量会被移动到GPU加速训练。

定义几个函数组织训练循环。第一个函数train_D_on_real()用真实样本训练判别器。

清单3.6 定义train_D_on_real()函数

def train_D_on_real(real_samples):
    real_samples = real_samples.to(device)
    optimD.zero_grad()
    out_D = D(real_samples)                    # ①
    loss_D = loss_fn(out_D, real_labels)       # ②
    loss_D.backward()
    optimD.step()                              # ③
    return loss_D

① 对真实样本进行预测
② 计算损失
③ 反向传播,更新判别器权重

该函数首先将真实样本移动到GPU(若有CUDA支持),判别器网络D对样本进行预测,计算预测结果与真实标签real_labels的损失。backward()计算损失函数对模型参数的梯度,step()基于梯度更新参数,zero_grad()确保每次反向传播前梯度清零,避免累积。

提示:训练每批数据前调用zero_grad(),显式清零梯度,防止梯度累积影响训练。

第二个函数train_D_on_fake()用生成的伪造样本训练判别器。

清单3.7 定义train_D_on_fake()函数

def train_D_on_fake():
    noise = torch.randn((batch_size, 2))
    noise = noise.to(device)
    fake_samples = G(noise)                     # ①
    optimD.zero_grad()
    out_D = D(fake_samples)                     # ②
    loss_D = loss_fn(out_D, fake_labels)        # ③
    loss_D.backward()
    optimD.step()                              # ④
    return loss_D

① 生成一批伪造样本
② 对伪造样本进行预测
③ 计算损失
④ 反向传播,更新权重

函数首先输入一批来自潜在空间的随机噪声向量,生成器生成伪造样本,再将伪造样本传入判别器获得预测,与伪造标签fake_labels计算损失,最后更新判别器参数。

:权重和参数在此处可视为同义词,广义上参数还包含偏置项,但此处用权重指代模型所有可调参数,权重调整、参数调整和反向传播也可互换使用。

第三个函数train_G()用伪造样本训练生成器。

清单3.8 定义train_G()函数

def train_G():
    noise = torch.randn((batch_size, 2))
    noise = noise.to(device)
    optimG.zero_grad()
    fake_samples = G(noise)                    # ①
    out_G = D(fake_samples)                    # ②
    loss_G = loss_fn(out_G, real_labels)       # ③
    loss_G.backward()
    optimG.step()                             # ④
    return loss_G, fake_samples

① 生成一批伪造样本
② 判别器对伪造样本进行预测
③ 计算损失(根据生成器是否成功骗过判别器)
④ 反向传播,更新生成器权重

训练生成器时,输入随机噪声生成伪造样本,判别器给出预测概率,与全为1的real_labels比较计算损失(因为生成器目标是欺骗判别器,让其认为伪造样本是真实的),最后更新生成器参数,使下一次生成更逼真的样本。

:计算生成器损失时,使用real_labels(全1张量)而非fake_labels(全0张量),因为生成器希望判别器将假样本判为真。

最后定义test_epoch()函数,定期打印判别器和生成器损失,并绘制生成器生成的数据点与训练集样本进行比较。

清单3.9 定义test_epoch()函数

import os
os.makedirs("files", exist_ok=True)                    # ①

def test_epoch(epoch, gloss, dloss, n, fake_samples):
    if epoch == 0 or (epoch + 1) % 25 == 0:
        g = gloss.item() / n
        d = dloss.item() / n
        print(f"at epoch {epoch+1}, G loss: {g}, D loss {d}")     # ②
        fake = fake_samples.detach().cpu().numpy()
        plt.figure(dpi=200)
        plt.plot(fake[:, 0], fake[:, 1], "*", c="g", label="generated samples")   # ③
        plt.plot(train_data[:, 0], train_data[:, 1], ".", c="r", alpha=0.1, label="real samples")  # ④
        plt.title(f"epoch {epoch+1}")
        plt.xlim(0, 50)
        plt.ylim(0, 50)
        plt.legend()
        plt.savefig(f"files/p{epoch+1}.png")
        plt.show()

① 创建保存文件的文件夹
② 定期打印损失
③ 以星号(*)绘制生成样本点
④ 以点(.)绘制训练数据样本

每25个epoch,函数打印该轮生成器和判别器平均损失,绘制一批生成数据(绿色星号)与训练集数据(红色点)对比,并将图像保存到本地/files/文件夹。

现在准备开始训练模型。遍历训练集所有批次数据。每个批次先用真实样本训练判别器,再用生成器生成伪造样本训练判别器,最后再次生成伪造样本训练生成器。训练持续直到满足早停条件。

清单3.10 训练GAN生成指数增长曲线

for epoch in range(10000):                         # ①
    gloss = 0
    dloss = 0
    for n, real_samples in enumerate(train_loader):    # ②
        loss_D = train_D_on_real(real_samples)
        dloss += loss_D
        loss_D = train_D_on_fake()
        dloss += loss_D
        loss_G, fake_samples = train_G()
        gloss += loss_G
    test_epoch(epoch, gloss, dloss, n, fake_samples)    # ③
    gdif = performance(fake_samples).item()
    if stopper.stop(gdif) == True:                      # ④
        break

① 开始训练循环
② 遍历训练集所有批次
③ 定期展示生成样本和损失
④ 判断是否停止训练

若使用GPU训练,耗时几分钟;CPU训练则视硬件配置耗时20至30分钟。也可从本书GitHub仓库下载预训练模型:github.com/markhliu/DG…

训练25轮后,生成数据点聚集在(0,0)附近,尚未形成有意义形状(1轮epoch是用完全部训练数据一次)。训练200轮后,生成数据开始呈现指数增长曲线形状,尽管许多点偏离训练集的虚线曲线。训练1025轮后,生成点与指数增长曲线高度吻合。图3.2展示了六个不同epoch的生成结果子图。我们的GAN效果非常好:生成器成功生成了期望的形状数据点。

image.png

图3.2 显示了训练过程中不同阶段生成形状(图中星号)与真实指数增长曲线形状(图中点)对比的子图。第25轮epoch时,生成样本尚未形成任何有意义的形状;第200轮时,样本开始呈现指数增长曲线形状;第1025轮时,生成样本与指数增长曲线高度吻合。

3.4.2 保存和使用训练好的生成器

GAN训练完成后,我们像往常一样丢弃判别器网络,并将训练好的生成器网络保存到本地文件夹,代码如下:

import os
os.makedirs("files", exist_ok=True)
scripted = torch.jit.script(G)
scripted.save('files/exponential.pt')

torch.jit.script()方法使用TorchScript编译器将函数或nn.Module类转换成TorchScript代码。我们用它来将训练好的生成器脚本化,并保存为exponential.pt文件。

使用生成器时,无需重新定义模型,只需加载保存的文件即可:

new_G = torch.jit.load('files/exponential.pt', map_location=device)
new_G.eval()

加载后的生成器会被放置在你的设备上(CPU或支持CUDA的GPU),map_location=device参数指定了加载设备。

现在可以用训练好的生成器生成一批数据点:

noise = torch.randn((batch_size, 2)).to(device)
new_data = new_G(noise)

首先生成一批来自潜在空间的随机噪声向量,然后输入生成器得到伪造数据。

我们可以绘制生成的数据:

fig = plt.figure(dpi=100)
plt.plot(new_data.detach().cpu().numpy()[:,0],
         new_data.detach().cpu().numpy()[:,1], "*", c="g",
         label="generated samples")                      # ①
plt.plot(train_data[:,0], train_data[:,1], ".", c="r",
         alpha=0.1, label="real samples")                # ②
plt.title("Inverted-U Shape Generated by GANs")
plt.xlim(0, 50)
plt.ylim(0, 50)
plt.legend()
plt.show()

① 以星号绘制生成的数据样本
② 以点绘制训练数据样本

你会看到一个类似图3.2中最后一个子图的图形:生成的数据样本与指数增长曲线非常相似。

恭喜你!你已经成功创建并训练了第一个生成对抗网络。掌握这项技能后,你可以轻松修改代码,使生成数据符合其他形状,如正弦、余弦、U形等。

练习3.3
修改第一个项目中的程序,使生成器生成的样本在x = -5到5区间内呈现正弦波形状。绘制时,将y轴范围设置为-1.2到1.2。

3.5 按规律生成数字

在第二个项目中,你将构建并训练GAN,生成一个包含10个整数的序列,这些整数均在0到99之间,且都是5的倍数。主要步骤与生成指数增长曲线类似,不同之处在于训练集不再是包含两个值(x, y)的数据点,而是一串所有数字均为0到99之间5的倍数的整数序列。

本节首先介绍如何将训练数据转换成神经网络可识别的格式——独热编码(one-hot variables)。随后,你将学习如何将独热编码变量转换回人类可读的0到99之间整数,实现数据在人类语言和模型语言之间的转换。之后创建判别器和生成器网络并训练GAN,使用早停判断训练结束。最后丢弃判别器,使用训练好的生成器生成你想要的数字序列。

3.5.1 什么是独热编码?

独热编码是一种在机器学习和数据预处理中使用的技术,将分类数据表示为二进制向量。分类数据包含颜色、动物种类或城市等非数值类别。机器学习算法一般只能处理数值数据,因此需将类别数据转换为数值格式。

举例来说,如果某个特征是房屋颜色,有“红色”、“绿色”和“蓝色”三个类别,使用独热编码时,每个类别对应一个二进制向量。创建三个二进制列,每列代表一个类别。“红色”编码为[1, 0, 0],“绿色”为[0, 1, 0],“蓝色”为[0, 0, 1]。这样保留类别信息,但不引入类别间的顺序关系,每个类别被视为独立的。

这里定义一个onehot_encoder()函数,将整数转换为独热编码:

import torch
def onehot_encoder(position, depth):
    onehot = torch.zeros((depth,))
    onehot[position] = 1
    return onehot

函数有两个参数,第一个position表示独热向量中被置1的位置索引,第二个depth表示独热向量的长度。举例:

print(onehot_encoder(1,5))

输出为:

tensor([0., 1., 0., 0., 0.])

表示长度为5的向量,索引1处为1,其余为0。

理解独热编码原理后,你可以将0到99之间任意整数转换为独热变量:

def int_to_onehot(number):
    onehot = onehot_encoder(number, 100)
    return onehot

使用该函数将数字75转换为100维独热向量:

onehot75 = int_to_onehot(75)
print(onehot75)

输出为100维张量,索引75位置为1,其他位置为0。

int_to_onehot()函数完成了从人类可读的整数到模型可用独热编码的转换。

接下来定义onehot_to_int()函数,实现将独热变量转换回整数:

def onehot_to_int(onehot):
    num = torch.argmax(onehot)
    return num.item()

该函数通过查找独热向量中最大值的位置索引,将独热变量转换为对应整数。

测试:

print(onehot_to_int(onehot75))

输出:

75

说明转换函数工作正常。

接下来,我们将构建并训练GAN,生成5的倍数序列。

3.5.2 用GAN生成规律数字

目标是训练生成器生成包含10个数字的序列,这些数字均为5的倍数。先准备训练数据,再批量转换为模型可用格式,最后用训练好的生成器生成目标规律数字序列。

为简单起见,生成0到99之间10个整数序列,随后转换成10个模型可用的数值。

生成10个5的倍数整数序列的函数:

def gen_sequence():
    indices = torch.randint(0, 20, (10,))
    values = indices * 5
    return values

首先用torch.randint()生成10个0到19的随机整数,乘以5转为5的倍数,转为PyTorch张量。

试生成训练序列:

sequence = gen_sequence()
print(sequence)

输出示例:

tensor([60, 95, 50, 55, 25, 40, 70,  5,  0, 55])

均为5的倍数。

接下来将每个整数转换为独热编码,便于后续输入神经网络训练:

import numpy as np

def gen_batch():
    sequence = gen_sequence()                            # ①
    batch = [int_to_onehot(i).numpy() for i in sequence]  # ②
    batch = np.array(batch)
    return torch.tensor(batch)
batch = gen_batch()

① 生成10个5的倍数序列
② 转换成100维独热变量

函数gen_batch()创建了训练批次数据,可供神经网络训练使用。

定义函数data_to_num()将独热变量转换为整数,方便人类理解:

def data_to_num(data):
    num = torch.argmax(data, dim=-1)                      # ①
    return num
numbers = data_to_num(batch)                              # ②

① 对100维独热向量最后一维寻找最大值索引,即转换为整数
② 测试函数

接下来创建两个神经网络:判别器D和生成器G,构建GAN生成期望的数字规律。

判别器网络:

from torch import nn

D = nn.Sequential(
    nn.Linear(100, 1),
    nn.Sigmoid()
).to(device)

由于整数已转为100维独热向量,判别器输入层大小为100。最后一层输出1个特征,经过Sigmoid激活压缩到[0,1],表示样本为真实的概率p,1-p为伪造概率。

生成器网络:

G = nn.Sequential(
    nn.Linear(100, 100),
    nn.ReLU()
).to(device)

将来自100维潜在空间的随机噪声输入生成器,生成100维张量。最后层用ReLU激活保证输出非负,因为我们尝试生成0或1的值,非负输出更适合。

使用Adam优化器,学习率0.0005:

loss_fn = nn.BCELoss()
lr = 0.0005
optimD = torch.optim.Adam(D.parameters(), lr=lr)
optimG = torch.optim.Adam(G.parameters(), lr=lr)

训练数据和网络准备好后,开始训练模型。训练完毕后,丢弃判别器,用训练好的生成器生成10个整数序列。

3.5.3 训练GAN生成有规律的数字

本项目的训练过程与之前生成指数增长曲线的项目非常相似。

我们定义了一个函数train_D_G(),它结合了之前项目中定义的三个函数train_D_on_real()train_D_on_fake()train_G()。该函数已在本章Jupyter Notebook的GitHub仓库中提供:github.com/markhliu/DG…。你可以查看train_D_G()函数,了解相较于前三个函数我们做了哪些细微修改。

我们沿用第一个项目中定义的早停类,不过将实例化时的patience参数调整为800,如下所示:

stopper = EarlyStop(800)                                   # ①

mse = nn.MSELoss()
real_labels = torch.ones((10,1)).to(device)
fake_labels = torch.zeros((10,1)).to(device)

def distance(generated_data):                              # ②
    nums = data_to_num(generated_data)
    remainders = nums % 5
    ten_zeros = torch.zeros((10,1)).to(device)
    mseloss = mse(remainders, ten_zeros)
    return mseloss

for i in range(10000):
    gloss = 0
    dloss = 0
    generated_data = train_D_G(D, G, loss_fn, optimD, optimG)  # ③
    dis = distance(generated_data)
    if stopper.stop(dis) == True:
        break
    if i % 50 == 0:
        print(data_to_num(generated_data))                   # ④

① 创建早停类的实例,patience设为800
② 定义distance()函数,计算生成数字的损失
③ 训练GAN一个epoch
④ 每50个epoch打印一次生成的整数序列

distance()函数计算生成数据与训练数据的差异,具体为生成数字除以5的余数的均方误差(MSE)。当所有生成数字均为5的倍数时,该误差为0。

运行上述代码会看到如下输出示例:

tensor([14, 34, 19, 89, 44,  5, 58,  6, 41, 87], device='cuda:0')
...
tensor([ 0, 80, 65,  0,  0, 10, 80, 75, 75, 75], device='cuda:0')
tensor([25, 30,  0,  0, 65, 20, 80, 20, 80, 20], device='cuda:0')
tensor([65, 95, 10, 65, 75, 20, 20, 20, 65, 75], device='cuda:0')

每次迭代生成10个数字。训练时,先用真实样本训练判别器D,然后用生成器生成的伪造样本训练判别器,再用生成的伪造样本训练生成器。若生成器在800个epoch内未再提升,则停止训练。每50个epoch打印一次生成的10个数字序列,方便观察它们是否全部为5的倍数。

训练过程如上述输出显示,最初几百个epoch生成的数字中仍有非5倍数,但900个epoch后生成的数字均为5的倍数。使用GPU训练约需1分钟,CPU训练少于10分钟。也可以从书籍GitHub仓库下载训练好的模型:github.com/markhliu/DG…

3.5.4 保存和使用训练好的模型

训练完成后,我们丢弃判别器,只保存训练好的生成器到本地文件夹:

import os
os.makedirs("files", exist_ok=True)
scripted = torch.jit.script(G)
scripted.save('files/num_gen.pt')

将生成器以TorchScript格式保存为num_gen.pt

使用生成器时,加载模型即可:

new_G = torch.jit.load('files/num_gen.pt', map_location=device)  # ①
new_G.eval()
noise = torch.randn((10, 100)).to(device)                       # ②
new_data = new_G(noise)                                         # ③
print(data_to_num(new_data))

① 加载保存的生成器
② 生成随机噪声向量
③ 输入噪声生成整数序列

输出示例:

tensor([40, 25, 65, 25, 20, 25, 95, 10, 10, 65], device='cuda:0')

生成的数字均为5的倍数。

你也可以轻松修改代码,生成其他规律的数字序列,比如奇数、偶数、3的倍数等。

练习3.4
修改第二个项目中的程序,使生成器生成一个全部为3的倍数的10整数序列。

现在你已经了解了GAN的工作原理,后续章节你将能将GAN的理念扩展至更复杂的内容生成,比如高分辨率图像和逼真音乐。

总结

生成对抗网络(GAN)由两个网络组成:判别器用于区分真假样本,生成器用于创建与训练集样本难以区分的新样本。

GAN的训练步骤包括准备训练数据、创建判别器和生成器、训练模型并决定何时停止训练,最后丢弃判别器,使用训练好的生成器生成新样本。

GAN生成的内容取决于训练数据:当训练集包含形成指数增长曲线的数据对(x, y)时,生成样本也会模仿该曲线形状;当训练集包含全为5的倍数的数字序列时,生成样本也会是包含5的倍数的数字序列。

GAN功能多样,能够生成多种不同格式的内容。