PyTorch实战(14)——条件生成对抗网络(conditional GAN,cGAN)

发布于:2025-07-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

0. 前言

我们使用深度卷积生成对抗网络 (Deep Convolution Generative Adversarial Network, DCGAN) 生成的动漫面孔看起来非常逼真,每张生成的图像都有不同的特征,比如头发颜色、眼睛颜色,以及头部朝左或朝右倾斜。
在本节中,将学习两种选择生成图像中特征的方法及其各自的优缺点。第一种方法是选择潜空间中的特定向量,不同的向量对应不同的特征——例如,一个向量可能生成男性面孔,而另一个则生成女性面孔。第二种方法使用条件生成对抗网络 (conditional GAN, cGAN),即在带有标签的数据上训练模型。这使得我们可以通过向模型传递标签生成具有特定标签的图像,每个标签代表一个独特的特征——比如有眼镜或无眼镜的面孔。
此外,还将学习如何将这两种方法结合使用,以便同时选择图像的两个独立属性。通过这种方式,可以生成四种不同的图像类型:戴眼镜的男性、未戴眼镜的男性、戴眼镜的女性和未戴眼镜的女性。还可以使用标签的加权平均或输入向量的加权平均,生成从一个属性到另一个属性过渡的图像。例如,可以生成一系列图像,使得同一个人脸上的眼镜逐渐消失(标签运算)。或者可以生成一系列图像,使得男性特征逐渐消失,男性面孔变为女性面孔(向量运算)。

1. eyeglasses 数据集

1.1 下载 eyeglasses 数据集

在本节中,我们将使用 eyeglasses 数据集来训练一个条件生成对抗网络 (conditional GAN, cGAN) 模型。使用的 eyeglasses 数据集来自 Kaggle,下载文件中包含一个图像文件夹 faces-spring-2020 和两个 CSV 文件 (train.csvtest.csv)。文件夹 faces-spring-2020 中包含 5,000 张图像。下载数据并解压后,将图像文件夹和两个 CSV 文件放在 ./files/ 文件夹中。接下来,将把照片分为两个子文件夹:一个只包含带眼镜的图像,另一个只包含不带眼镜的图像。

(1) 首先,查看 train.csv 文件。导入 train.csv 文件,并将变量 id 设置为每个观测值的索引。文件中的 glasses 列包含两个值:01,表示图像是否含有眼镜 (0 表示没有眼镜,1 表示有眼镜):

import pandas as pd

# 加载文件 train.csv
train=pd.read_csv('files/train.csv')
# 将 id 列中的值设置为数据索引
train.set_index('id', inplace=True)

print(train["glasses"])

(2) 接下来,将图像分为两个不同的文件夹:一个包含带眼镜的图像,另一个包含不带眼镜的图像:

import os, shutil

G='files/glasses/G/'
NoG='files/glasses/NoG/'
os.makedirs(G, exist_ok=True)    # 创建子文件夹 files/glasses/G/ 用于存放带眼镜的图像
os.makedirs(NoG, exist_ok=True)    # 创建子文件夹 files/glasses/NoG/ 用于存放不带眼镜的图像
folder='files/faces-spring-2020/faces-spring-2020/'
for i in range(1,4501):
    oldpath=f"{folder}face-{i}.png"
    if train.loc[i]['glasses']==0:    # 将标记为 0 的图像移动到 NoG 文件夹
        newpath=f"{NoG}face-{i}.png"
    elif train.loc[i]['glasses']==1:    # 将标记为 1 的图像移动到 G 文件夹
        newpath=f"{G}face-{i}.png"
    shutil.move(oldpath, newpath)

1.2 可视化数据样本

(1) 接下来,可视化一些带眼镜的图像示例:

import random
import matplotlib.pyplot as plt
from PIL import Image

imgs=os.listdir(G)

# 从文件夹 G 中随机选择 16 张图像
samples=random.sample(imgs,16)
fig=plt.figure(dpi=100, figsize=(8,2))
for i in range(16):
    ax = plt.subplot(2, 8, i + 1)
    img=Image.open(f"{G}{samples[i]}")
    plt.imshow(img)
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.01,hspace=-0.01)
plt.show()

可视化数据样本

(2) 将代码中的文件夹 G 改为 NoG,以可视化数据集中不带眼镜的样本图像:

可视化数据样本

2. 条件生成对抗网络

条件生成对抗网络 (conditional Generative Adversarial Network, cGAN) 与生成对抗网络 (Generative Adversarial Network, GAN) 模型类似,不同之处在于 cGAN 需要将标签附加到输入数据中。这些标签对应于输入数据中的不同特征。一旦训练好的 GAN 模型“学会”将标签与特定特征关联,就可以向模型输入一个带标签的随机噪声向量,生成具有所需特征的输出。

2.1 条件生成对抗网络简介

cGAN 是原始 GAN 框架的扩展。在 cGAN 中,生成器和判别器(或者说评论家,因为我们在 cGAN 中使用了 WGAN 技术)都依赖于一些额外的信息进行条件化。这些信息可以是任何数据,如类别标签、来自其他模态的数据,甚至是文本描述。这种条件化通常是通过将附加信息输入到生成器和判别器中来实现的。在本节中,我们将类别标签添加到生成器和评论家的输入中,为带眼镜的图像附加标签 1,为不带眼镜的图像附加标签 0cGAN 训练过程如下图所示。

CGAN

cGAN 中,生成器接收一个随机噪声向量和条件信息(指示图像是否有眼镜的标签)作为输入。利用这些信息生成不仅看起来真实,而且与条件输入一致的数据。
评论家接收来自训练集的真实数据或生成器生成的虚假数据,同时还接收条件信息(在本节中,是一个指示图像是否有眼镜的标签)。它的任务是确定给定的数据是真实的还是虚假的,同时考虑条件信息(在本节中,为表示戴眼镜或不戴眼镜的标签)。
cGAN 的主要优点是能够选择生成数据的某些属性,使其更加灵活,适用于那些需要根据特定输入参数来定向或条件化输出的场景。总之,cGAN 是基本 GAN 架构的一种扩展,使得基于条件输入能够有针对性地生成合成数据。

2.2 构建条件生成对抗网络

在本节中,将学习如何创建一个 cGAN 来生成带眼镜或不带眼镜的人脸图像,实现带有梯度惩罚的 WGAN,以稳定训练。cGAN 中的生成器不仅使用随机噪声向量,还使用条件信息,如标签,作为输入来生成带眼镜或不带眼镜的图像。

(1) 创建评论家网络,最终输出一个介于 − ∞ −∞ ∞ ∞ 之间的值:

import torch.nn as nn
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"
class Critic(nn.Module):
    def __init__(self, img_channels, features):
        super().__init__()
        # 评论家网络有两个 Conv2d 层和五个块
        self.net = nn.Sequential(
            nn.Conv2d(img_channels, features, 
                      kernel_size=4, stride=2, padding=1),
            nn.LeakyReLU(0.2),
            self.block(features, features * 2, 4, 2, 1),
            self.block(features * 2, features * 4, 4, 2, 1),
            self.block(features * 4, features * 8, 4, 2, 1),
            self.block(features * 8, features * 16, 4, 2, 1),  
            self.block(features * 16, features * 32, 4, 2, 1),            
            nn.Conv2d(features * 32, 1, kernel_size=4,
                      stride=2, padding=0)) # 输出包含一个特征值
    # 每个块包含一个 Conv2d 层、一个 InstanceNorm2d 层,并使用 LeakyReLU 激活函数
    def block(self, in_channels, out_channels, 
              kernel_size, stride, padding):
        return nn.Sequential(
            nn.Conv2d(in_channels,out_channels,
                kernel_size,stride,padding,bias=False,),
            nn.InstanceNorm2d(out_channels, affine=True),
            nn.LeakyReLU(0.2))
    def forward(self, x):
        return self.net(x)

评论家网络的输入是一个形状为 5 × 256 × 256 的彩色图像。前三个通道是颜色通道(红色、绿色和蓝色),最后两个通道(第四和第五通道)是标签通道,用来告诉评论家图像是否带有眼镜。
评论家网络由 7Conv2d 层组成。Conv2d 通过对输入图像应用一组可学习的卷积核来提取特征,从而在不同的空间尺度上检测模式和特征,有效地捕捉输入数据的层次表示。然后,评论家基于这些表示来评估输入图像。InstanceNorm2d 层类似于 BatchNorm2d 层,不同之处在于,InstanceNorm2d 对每个批次中的单个实例进行独立的归一化处理。
另一个关键点是,输出不再是介于 01 之间的值,因为在评论家网络的最后一层我们没有使用 sigmoid 激活函数。相反,输出的值范围是 − ∞ −∞ ∞ ∞ ,因为我们在 cGAN 中使用了带梯度惩罚的 Wasserstein 距离。

(2) 生成器的任务是创建数据实例,以便它们能被评论家评估并获得高分。在 cGAN 中,生成器必须生成带有条件信息的数据实例(在本节中,指的是带眼镜或不带眼镜的图像)。实现使用 Wasserstein 距离的 cGAN 时,我们通过将标签附加到随机噪声向量中,告诉生成器希望生成的图像类型:

class Generator(nn.Module):
    def __init__(self, noise_channels, img_channels, features):
        super(Generator, self).__init__()
        # 生成器由七个 ConvTranspose2d 层组成
        self.net = nn.Sequential(
            self.block(noise_channels, features *64, 4, 1, 0),
            self.block(features * 64, features * 32, 4, 2, 1),
            self.block(features * 32, features * 16, 4, 2, 1),
            self.block(features * 16, features * 8, 4, 2, 1),
            self.block(features * 8, features * 4, 4, 2, 1),            
            self.block(features * 4, features * 2, 4, 2, 1),            
            nn.ConvTranspose2d(
                features * 2, img_channels, kernel_size=4,
                stride=2, padding=1),
            nn.Tanh()) # 使用 Tanh 激活函数将值压缩到 [-1, 1] 范围内,和训练集中的图像相同
    # 每个块由一个 ConvTranspose2d 层、一个 BatchNorm2d 层和 ReLU 激活函数组成
    def block(self, in_channels, out_channels, 
              kernel_size, stride, padding):
        return nn.Sequential(
            nn.ConvTranspose2d(in_channels,out_channels,
                kernel_size,stride,padding,bias=False,),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(),)
    def forward(self, x):
        return self.net(x)

从一个 100 维的潜空间中采样一个随机噪声向量作为生成器的输入。还将提供一个包含两个类别的独热编码图像标签,告诉生成器生成带眼镜或不带眼镜的图像。将这两部分信息拼接在一起,形成一个 102 维的输入变量传递给生成器。生成器基于来自潜空间和标签信息的输入生成彩色图像。生成器网络由 7ConvTranspose2d 层组成,通过镜像评论家网络架构生成图像。在输出层使用 Tanh 激活函数,使得输出像素的值范围为 -11,与训练集中的图像保持一致。

(3) 在深度学习中,神经网络中的权重是随机初始化的。当网络架构复杂且有很多隐藏层时,权重的初始化方式至关重要。因此,定义 weights_init() 函数,用于初始化生成器和评论家网络中的权重:

def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)  

weights_init() 函数通过从均值为 0、标准差为 0.02 的正态分布中抽取值来初始化 Conv2dConvTranspose2d 层的权重,还通过从均值为 1、标准差为 0.02 的正态分布中抽取值来初始化 BatchNorm2d 层的权重。选择较小的标准差进行权重初始化,以避免梯度爆炸问题。

(4) 根据 Generator()Critic() 类来创建生成器和评论家,根据 weights_init() 函数对它们的权重进行初始化:

z_dim=100
img_channels=3
features=16
gen=Generator(z_dim+2,img_channels,features).to(device)
critic=Critic(img_channels+2,features).to(device)
weights_init(gen)
weights_init(critic)

(5) 使用 Adam 优化器训练生成器和评论家:

lr = 0.0001
opt_gen = torch.optim.Adam(gen.parameters(), 
                         lr = lr, betas=(0.0, 0.9))
opt_critic = torch.optim.Adam(critic.parameters(), 
                         lr = lr, betas=(0.0, 0.9))

(6) 生成器试图生成与训练集中给定标签的图像无法区分的图像,将生成的图像输入到评论家中,以便在生成的图像上获得较高评分。而评论家则试图为真实图像分配较高评分,为虚假图像分配较低评分,且这些评分是基于给定标签条件的。具体而言,评论家的损失函数包含三个部分:

critic_value(fake) − critic_value(real) + weight × GradientPenalty

第一项,critic_value(fake),表示如果图像是虚假的,评论家的目标是将其识别为虚假图像,并给出较低评分。第二项,− critic_value(real),表示如果图像是真的,评论家的目标是将其识别为真实图像,并给出较高评分。此外,评论家还希望最小化梯度惩罚项,weight × GradientPenalty,其中 weight 是一个常量,用于确定对梯度范数偏离 1 的程度施加多少惩罚。梯度惩罚的计算方法如下所示:

def GP(critic, real, fake):
    B, C, H, W = real.shape
    alpha=torch.rand((B,1,1,1)).repeat(1,C,H,W).to(device)
    # 创建真实图像和虚假图像的插值图像
    interpolated_images = real*alpha+fake*(1-alpha)
    # 获取评论家对于插值图像的评分
    critic_scores = critic(interpolated_images)
    # 计算梯度
    gradient = torch.autograd.grad(
        inputs=interpolated_images,
        outputs=critic_scores,
        grad_outputs=torch.ones_like(critic_scores),
        create_graph=True,
        retain_graph=True)[0]
    gradient = gradient.view(gradient.shape[0], -1)
    gradient_norm = gradient.norm(2, dim=1)
    # 梯度惩罚是梯度范数与值 1 之差的平方
    gp = torch.mean((gradient_norm - 1) ** 2)
    return gp

在函数 GP() 中,首先创建真实图像和虚假图像的插值图像。这是通过随机采样真实图像和生成图像之间直线上的点来完成的。可以将其想象为一个滑块:一端是真实图像,另一端是虚假图像。移动滑块时,可以看到从真实到虚假的连续过渡,而插值图像则代表了其间的不同阶段。
然后,将这些插值图像输入评论家网络,并获取评分,接着计算评论家输出相对于插值图像的梯度。最后,梯度惩罚通过计算梯度范数偏离目标值 1 的平方差来进行计算。

3. 模型训练

为了训练 cGAN,需要告诉评论家和生成器图像的标签,以便它们知道图像中的人物面部是否有眼镜。

3.1 输入数据处理

在本节中,首先学习如何向评论家网络和生成器网络的输入添加标签,以便生成器知道生成什么类型的图像,而评论家可以基于标签来评估图像。

(1) 首先,对数据进行预处理并将图像转换为 PyTorch 张量:

import torchvision.transforms as T
import torchvision

batch_size=16
imgsz=256
transform=T.Compose([
    T.Resize((imgsz,imgsz)),
    T.ToTensor(),
    T.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])])      
data_set=torchvision.datasets.ImageFolder(
    root=r"files/glasses",
    transform=transform) 

(2) 接下来,将标签添加到训练数据中。由于图像有两种类型,带眼镜的图像和不带眼镜的图像,创建两个独热编码标签。带眼镜的图像的独热标签为 [1, 0],不带眼镜的图像的独热标签为 [0, 1]。生成器的输入的随机噪声向量包含 100 个值,将独热标签与随机噪声向量拼接后,得到一个包含 102 个值的输入传递给生成器。评论家网络的输入是一个三通道彩色图像,形状为 3 x 256 x 256。为了将形状为 1 x 2 的标签附加到形状为 3 x 256 x 256 的图像上,向输入图像添加两个通道,从而使图像的形状从 (3, 256, 256) 改变为 (5, 256, 256),这两个额外的通道就是独热标签。具体来说,如果是带眼镜的图像,那么第四个通道会填充为 1,第五个通道填充为 0;如果是不带眼镜的图像,那么第四个通道填充为 0,第五个通道填充为 1

newdata=[]
for i,(img,label) in enumerate(data_set):
    onehot=torch.zeros((2))
    onehot[label]=1
    # 创建两个额外的通道,填充为 0,每个通道的形状为 256 x 256,与输入图像中每个通道的维度相同
    channels=torch.zeros((2,imgsz,imgsz))
    if label==0:
        # 如果原始图像标签为 0,则将第四个通道填充为 1
        channels[0,:,:]=1
    else:
        # 如果原始图像标签为 4,则将第五个通道填充为 1
        channels[1,:,:]=1
    # 将第四个和第五个通道添加到原始图像中,形成一个五通道的标签图像
    img_and_label=torch.cat([img,channels],dim=0)    
    newdata.append((img,label,onehot,img_and_label))

如果数据集中存在多个标签,我们也可以轻松地将 cGAN 模型扩展到具有多个标签值值的情况。例如,如果创建一个模型来生成不同发色(黑色、金色和白色)的图像,那么提供给生成器的图像标签可以是 [1, 0, 0][0, 1, 0][0, 0, 1],分别表示黑发、金发和白发。可以在输入图像之前为其附加三个通道,然后将其输入给判别器/评论家。例如,如果图像是黑色头发,那么第四个通道填充为 1,第五和第六个通道填充为 0

(3) 创建一个数据迭代器,按批处理数据(以提高计算效率、内存使用和训练过程中的优化动态):

data_loader=torch.utils.data.DataLoader(
    newdata,batch_size=batch_size,shuffle=True)

3.2 训练 cGAN

准备好训练数据和两个网络后,接下来将训练 cGAN,并通过视觉检查来判断训练何时停止。

(1) 创建函数 plot_epoch(),定期观察生成的图像效果:

def plot_epoch(epoch):
    noise = torch.randn(32, z_dim, 1, 1)
    labels = torch.zeros(32, 2, 1, 1)
    # 为带眼镜图片创建独热编码标签
    labels[:,0,:,:]=1
    noise_and_labels=torch.cat([noise,labels],dim=1).to(device)
    # 将拼接的噪声向量和标签输入生成器,以生成带眼镜的图像
    fake=gen(noise_and_labels).cpu().detach()
    fig=plt.figure(figsize=(20,10),dpi=100)
    # 绘制生成的带眼镜的图像
    for i in range(32):
        ax = plt.subplot(4, 8, i + 1)
        img=(fake.cpu().detach()[i]/2+0.5).permute(1,2,0)
        plt.imshow(img)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.savefig(f"files/glasses/G{epoch}.png")
    plt.show() 
    noise = torch.randn(32, z_dim, 1, 1)
    labels = torch.zeros(32, 2, 1, 1)
    # 为不带眼镜的图像创建独热编码标签
    labels[:,1,:,:]=1
    noise_and_labels=torch.cat([noise,labels],dim=1).to(device)
    fake=gen(noise_and_labels).cpu().detach()
    fig=plt.figure(figsize=(20,10),dpi=100)
    for i in range(32):
        ax = plt.subplot(4, 8, i + 1)
        img=(fake.cpu().detach()[i]/2+0.5).permute(1,2,0)
        plt.imshow(img)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.savefig(f"files/glasses/NoG{epoch}.png")
    plt.show()

(2) 定义 train_batch() 函数,用于通过批处理数据来训练模型:

def train_batch(onehots,img_and_labels,epoch):
    # 一批带标签的真实图像
    real = img_and_labels.to(device)
    B = real.shape[0]
    for _ in range(5):
        noise = torch.randn(B, z_dim, 1, 1)
        onehots=onehots.reshape(B,2,1,1)
        noise_and_labels=torch.cat([noise,onehots],dim=1).to(device)
        fake_img = gen(noise_and_labels).to(device)
        fakelabels=img_and_labels[:,3:,:,:].to(device)
        # 一批带标签的生成图像
        fake=torch.cat([fake_img,fakelabels],dim=1).to(device)
        critic_real = critic(real).reshape(-1)
        critic_fake = critic(fake).reshape(-1)
        gp = GP(critic, real, fake)
        # 评论家的总损失由三个部分组成:评估真实图像的损失、评估虚假图像的损失和梯度惩罚损失
        loss_critic=(-(torch.mean(critic_real) - 
           torch.mean(critic_fake)) + 10 * gp)
        opt_critic.zero_grad()
        loss_critic.backward(retain_graph=True)
        opt_critic.step()
    gen_fake = critic(fake).reshape(-1)
    # 训练生成器
    loss_gen = -torch.mean(gen_fake)
    opt_gen.zero_grad()
    loss_gen.backward()
    opt_gen.step()
    return loss_critic, loss_gen

(3) 训练模型 100epoch

for epoch in range(1,101):
    closs=0
    gloss=0
    # 遍历训练数据集中的所有批次
    for _,_,onehots,img_and_labels in data_loader:    
        # 使用一批数据训练模型
        loss_critic, loss_gen = train_batch(onehots, img_and_labels,epoch)   
        closs+=loss_critic.detach()/len(data_loader)
        gloss+=loss_gen.detach()/len(data_loader)
    print(f"at epoch {epoch}, critic loss: {closs}, generator loss {gloss}")
    plot_epoch(epoch)

# 保存训练好的生成器权重
torch.save(gen.state_dict(),'files/cgan.pth')

4. 生成具有特定特征的图像

生成具有特定特征的图像有两种方法。第一种方法是在将随机噪声向量输入到训练好的 cGAN 模型之前,附加一个标签。不同的标签会导致生成图像具有不同的特征(例如,本节中图像是否带有眼镜)。第二种方法是选择输入训练模型的噪声向量,例如一个向量会生成带有男性面孔的图像,另一个向量会生成带有女性面孔的图像。需要注意的是,第二种方法即使在传统的 GAN 中也适用,也适用于 cGAN。在本节中,将学到如何结合这两种方法,以便可以同时选择两个特征,例如,生成一张带眼镜的男性面孔图像或一张不带眼镜的女性面孔图像等。
这两种方法在选择生成图像中特征时各有优缺点。第一种方法,cGAN,需要有标签的数据来训练模型。有时,标注数据的准备成本较高。但一旦成功训练了 cGAN,就可以生成具有特定特征的多种图像。例如,可以生成不同的带眼镜(或不带眼镜)的图像,每一张都有所不同。第二种方法,通过手动选择噪声向量,不需要标签数据来训练模型。然而,每个手动选择的噪声向量只能生成一张图像。如果想生成多张具有相同特征的不同图像,就需要预先选择多个不同的噪声向量。

4.1 选择生成图像中的特征

在将随机噪声向量输入训练好的 cGAN 模型之前,附加标签 [1, 0][0, 1],可以选择生成的图像是否带有眼镜。首先,使用训练好的模型生成 32 张带眼镜的面孔图像。此外,我们将使用相同的随机噪声向量集合,以确保得到相同的面孔集合。

(1) 创建 Generator() 类实例 generator。然后,我们加载保存在本地文件夹中的训练好的模型权重。为了生成 32 张带眼镜的人脸图像,首先在潜空间中采样 32 个随机噪声向量。接着,创建一组标签 labels_g,使用生成器生成 32 张带眼镜的图像:

generator=Generator(z_dim+2,img_channels,features).to(device)
# 加载训练好的权重
generator.load_state_dict(torch.load("files/cgan.pth", map_location=device))
generator.eval()

# 生成一组随机噪声向量并保存,以便可以从中选择特定的向量来执行向量运算
noise_g = torch.randn(32, z_dim, 1, 1)
labels_g = torch.zeros(32, 2, 1, 1)
# 创建一个标签,用于生成带眼镜的图像
labels_g[:,0,:,:]=1
noise_and_labels=torch.cat([noise_g,labels_g],dim=1).to(device)
fake=generator(noise_and_labels)
plt.figure(figsize=(20,10),dpi=50)
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    img=(fake.cpu().detach()[i]/2+0.5).permute(1,2,0)
    plt.imshow(img.numpy())
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)
plt.show()

生成结果

(2) 可以看到,32 张图像都带有眼镜,这表明训练好的 cGAN 模型能够根据提供的标签生成图像。同时,有些图像具有男性特征,而另一些则具有女性特征。为了准备接下来的向量运算,我们将选择 2 个随机噪声向量,一个能够生成具有男性特征的图像,另一个能够生成具有女性特征的图像。观察以上 32 张图像后,选择索引值为 17 的图像:

z_male_g=noise_g[1]
z_female_g=noise_g[7]

(3) 生成 32 张不带眼镜的图像:

noise_ng = torch.randn(32, z_dim, 1, 1)
labels_ng = torch.zeros(32, 2, 1, 1)
labels_ng[:,1,:,:]=1
noise_and_labels=torch.cat([noise_ng,labels_ng],dim=1).to(device)
fake=generator(noise_and_labels).cpu().detach()
plt.figure(figsize=(20,10),dpi=50)
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    img=(fake.cpu().detach()[i]/2+0.5).permute(1,2,0)
    plt.imshow(img.numpy())#.repeat(4,axis=0).repeat(4,axis=1))
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)
plt.show()

生成结果

(4) 可以看到,32 张图像都不带眼镜,训练好的 cGAN 模型能够根据给定标签生成图像。选择索引为 6 (男性)和 0 (女性)的图像,为接下来的向量运算做准备:

z_male_ng=noise_ng[6]
z_female_ng=noise_ng[0]

(5) 接下来,使用标签插值执行标签运算。noise_gnoise_ng 这两个标签分别指示训练好的 cGAN 模型生成带眼镜和不带眼镜的图像。将一个插值标签(即这两个标签 [1, 0][0, 1] 的加权平均)传递给模型:

# 创建 5 个权重
weights=[0,0.25,0.5,0.75,1]
plt.figure(figsize=(20,4),dpi=50)
for i in range(5):
    ax = plt.subplot(1, 5, i + 1)
    # 创建两个标签的加权平均值
    label=weights[i]*labels_ng[0]+(1-weights[i])*labels_g[0]
    noise_and_labels=torch.cat(
        [z_female_g.reshape(1, z_dim, 1, 1),
         label.reshape(1, 2, 1, 1)],dim=1).to(device)
    # 将新的标签输入训练好的模型以生成图像
    fake=generator(noise_and_labels).cpu().detach()    
    img=(fake[0]/2+0.5).permute(1,2,0)
    plt.imshow(img)
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)
plt.show()

首先创建五个权重 (w):00.250.50.751,每个权重值 w 都表示赋予不带眼镜标签 labels_ng 的权重,权重 1-w 则赋予给带眼镜标签 labels_g。因此,插值标签的计算公式为:w*labels_ng + (1-w)*labels_g。然后,将插值标签和保存的随机噪声向量 z_female_g 一起输入到训练好的模型中。从左到右查看生成的五张图像,可以看到眼镜逐渐消失

生成结果

4.2 潜空间中的向量运算

为了能够选择生成图像中具有男性或女性特征,我们可以通过在潜空间中选择噪声向量来实现。在上一小节中,保存了两个随机噪声向量,z_male_ngz_female_ng,分别生成男性面孔和女性面孔的图像。接下来,将这两个向量的加权平均(即插值向量)输入到训练好的模型中,观察生成的图像:

# 创建 5 个权重
weights=[0,0.25,0.5,0.75,1]
plt.figure(figsize=(20,4),dpi=50)
for i in range(5):
    ax = plt.subplot(1, 5, i + 1)
    # 创建两个随机噪声向量的加权平均值
    z=weights[i]*z_female_ng+(1-weights[i])*z_male_ng
    noise_and_labels=torch.cat(
        [z.reshape(1, z_dim, 1, 1),
         labels_ng[0].reshape(1, 2, 1, 1)],dim=1).to(device)
    # 将新的随机噪声向量输入训练好的模型以生成图像  
    fake=generator(noise_and_labels).cpu().detach()    
    img=(fake[0]/2+0.5).permute(1,2,0)
    plt.imshow(img)
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)
plt.show()

生成结果

向量运算可以实现从一种图像实例到另一种图像实例的转换。由于我们选择了男性和女性的图像,从左到右查看五张生成图像时,可以看到男性特征逐渐消失,女性特征逐渐出现。

4.3 同时选择两个特征

在以上代码中,我们每次选择一个特征。通过选择标签,我们已经学习了如何生成有眼镜或无眼镜的图像。通过选择特定的噪声向量,已经学习了如何选择生成图像的特定实例。那么,如果想同时选择两个特征(例如眼镜和性别),有四种可能的组合:有眼镜的男性面孔、没有眼镜的男性面孔、有眼镜的女性面孔和没有眼镜的女性面孔。接下来,我们将生成上述每种类型的图像。

(1) 为了生成不同情况的四张图像,需要使用一个噪声向量作为输入:z_female_gz_male_g。同时,还需要将一个标签附加到输入上,标签可以是 labels_nglabels_g。通过迭代i的四个值(从 03),并创建两个值 pq,它们分别是 i 除以 2 的整数商和余数。因此,pq 的值可以是 01。通过设置随机噪声向量的值为 z_female_g*p + z_male_g*(1-p),可以选择一个随机噪声向量来生成男性或女性面孔。类似地,通过将标签的值设置为 labels_ng[0]*q + labels_g[0]*(1-q),可以选择标签来确定生成的图像是否带有眼镜。将随机噪声向量和标签结合并输入到训练好的模型中后,就可以同时选择两个特征:

plt.figure(figsize=(20,5),dpi=50)
for i in range(4):
    ax = plt.subplot(1, 4, i + 1)
    p=i//2
    q=i%2 
    # p 的值,可以是 0 或 1,用于选择随机噪声向量,以生成男性或女性面孔
    z=z_female_g*p+z_male_g*(1-p)
    # q 的值,可以是 0 或 1,用于选择标签,以确定生成的图像是否带有眼镜
    label=labels_ng[0]*q+labels_g[0]*(1-q)
    # 将随机噪声向量与标签结合,以选择两个特征
    noise_and_labels=torch.cat(
        [z.reshape(1, z_dim, 1, 1),
         label.reshape(1, 2, 1, 1)],dim=1).to(device)
    fake=generator(noise_and_labels)
    img=(fake.cpu().detach()[0]/2+0.5).permute(1,2,0)
    plt.imshow(img.numpy())
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)

生成结果

可以看到,生成的四张图像具有两个独立的特征:男性或女性面孔,以及图像中是否带有眼镜。第一张图显示的是有眼镜的男性面孔;第二张图是没有眼镜的男性面孔。第三张图是有眼镜的女性面孔,而最后一张图显示的是没有眼镜的女性面孔。

(2) 最后,我们可以同时进行标签算术和向量算术。也就是说,可以将一个插值后的噪声向量和一个插值后的标签输入到训练好的 cGAN 模型中,观察生成的图像:

plt.figure(figsize=(20,20),dpi=50)
for i in range(36):
    ax = plt.subplot(6,6, i + 1)
    p=i//6
    q=i%6 
    z=z_female_ng*p/5+z_male_ng*(1-p/5)
    label=labels_ng[0]*q/5+labels_g[0]*(1-q/5)
    noise_and_labels=torch.cat(
        [z.reshape(1, z_dim, 1, 1),
         label.reshape(1, 2, 1, 1)],dim=1).to(device)
    fake=generator(noise_and_labels)
    img=(fake.cpu().detach()[0]/2+0.5).permute(1,2,0)
    plt.imshow(img.numpy())
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(wspace=-0.08,hspace=-0.01)
plt.show()

其中,pq 可以取六个不同的值:012345。随机噪声向量 z_female_ng*p/5 + z_male_ng*(1-p/5) 会根据 p 的值有六个不同的值,标签 labels_ng[0]*q/5 + labels_g[0]*(1-q/5) 会根据 q 的值有六个不同的值。因此,基于插值后的噪声向量和插值后的标签会有 36 种不同的图像组合。

生成结果

可以看到,生成结果包含 36 张图片。插值后的噪声向量是两个随机噪声向量的加权平均,分别是生成女性面孔的 z_female_ng 和生成男性面孔的 z_male_ng。标签是两个标签 labels_nglabels_g 的加权平均,这两个标签决定生成的图像是否包含眼镜。经过训练的模型基于插值噪声向量和插值标签生成 36 张不同的图像。在每一行中,从左到右,眼镜逐渐消失,也就是说,在每一行中进行标签运算。在每一列中,从上到下,图像逐渐从男性面孔变为女性面孔,也就是说,在每一列中进行向量运算。

小结

  • 通过在潜空间中选择一个特定噪声向量并将其输入到训练好的 GAN 模型中,可以在生成的图像中选择某种特征,例如图像是男性面孔还是女性面孔。
  • cGAN 与传统的 GAN 不同。cGAN 在带标签的数据上训练模型,并要求训练好的模型生成具有特定属性的数据。例如,使用标签告诉模型生成带眼镜的人脸图像或生成不带眼镜的人脸图像。
  • 训练好的 cGAN 可以通过一系列标签的加权平均生成从一个标签表示的图像过渡到另一个标签表示的图像,这种操作称为标签运算,例如,在一系列图像中眼镜逐渐从同一个人的面孔上消失。
  • 还可以使用两个不同噪声向量的加权平均,创建从一种属性过渡到另一种属性的图像,这种运算称为向量运![算,例如,在一系列图像中,男性特征逐渐消失,女性特征逐渐出现。

系列链接

PyTorch生成式人工智能实战:从零打造创意引擎
PyTorch实战(1)——神经网络与模型训练过程详解
PyTorch实战(2)——PyTorch基础
PyTorch实战(3)——使用PyTorch构建神经网络
PyTorch实战(4)——卷积神经网络详解
PyTorch实战(5)——分类任务详解
PyTorch实战(6)——生成模型(Generative Model)详解
PyTorch实战(7)——生成对抗网络实践详解
PyTorch实战(8)——深度卷积生成对抗网络
PyTorch实战(9)——Pix2Pix详解与实现
PyTorch实战(10)——CyclelGAN详解与实现
PyTorch实战(11)——神经风格迁移
PyTorch实战(12)——StyleGAN详解与实现
PyTorch实战(13)——WGAN详解与实现


网站公告

今日签到

点亮在社区的每一天
去签到