实验题目
- conditional GAN
实验内容
- 完成一个conditional GAN (cGAN),探索如何开发conditional generative adversarial network
关于生成衣物方面,主要了解- 使用 GAN 生成随机样本的局限性可以通过cGAN解决。
- 如何开发和评估cGAN生成衣服的照片
实验原理
-
首先来了解一下一下GAN的相关知识
-
The architecture is comprised of a generator and a discriminator model. The generator model is responsible for generating new plausible examples that ideally are indistinguishable from real examples in the dataset. The discriminator model is responsible for classifying a given image as either real (drawn from the dataset) or fake (generated).
-
在 GAN 模型中使用类标签信息有两个动机
- Improve the GAN.
- Targeted Image Generation.
-
This improvement may come in the form of more stable training, faster training, and/or generated images that have better quality.
-
类标签还可用于有意或有针对性地生成给定类型的图像
-
GAN 模型的局限性在于它可能会从域中生成随机图像。潜在空间中的点与生成的图像之间存在关系,但这种关系很复杂且难以映射。或者,可以以生成器和鉴别器模型都以类别标签为条件的方式训练 GAN。这意味着当训练好的生成器模型用作独立模型来生成域中的图像时,可以生成给定类型或类标签的图像。
-
GAN可以学习到训练样本的分布,从而生成新的数据。但是GAN虽然能生成新的数据,但是无法确切的控制新样本的类型。由于GAN这种不需要预先建模的方法太过自由,如果对于较大图片,较多像素的情形,这种基于GAN的方法就太不可控了。比如手写数字集,我们无法通过GAN来指定要生成的具体数字。
-
GAN的训练过程可以被比作警察抓小偷的过程—生成模型G是小偷,判别模型D是警察。也就是说,对于小偷G要尽可能提高自己的手段,来骗过警察D,而对于警察D来说,要提高自己的能力来识别出小偷G。所以,GAN框架下的学习过程就变成了一种生成模型G和判别模型D之间的竞争关系。最终判别模型的准确率等于50%,整个模型状态达到均衡。
-
对于GAN来说, 生成器G, 输入一个噪声z, 输出一个图像G(Z),而判别器D, 输入一个图片x,其输出为一个图像是真实图片的概率(D(x)),x是真实样本或生成样本,它的损失函数: **V(D, G) = log(D(x)) + log((1- D(G(z)))) **, 这里仅x是真实样本
-
条件对抗网络CGAN可以看作是把无监督的GAN变成有监督的模型的一种改进,对生成器和判别器添加额外信息y作为条件,y可以是任意信息,例如类别信息,或者其他的数据,添加条件信息相当于在GAN的随机分布中,加入了一个潜在的约束范围。
-
CGAN基本结构图解
-
GAN一方面把噪声z和条件y作为输入同时送进生成器,生成跨域向量,再通过非线性函数映射到数据空间。另一方面把数据x和条件y作为输入同时送进判别器,生成跨域向量,并进一步判断x是真实训练数据的概率。
-
而对于CGAN来说,生成器G, 输入一个噪声z,一个条件y, 输出符合条件y的图像(G(z/y),其判别器D,输入一张图片x, 一个条件y, 输出x在条件y下是真实图像的概率D(x/y),x是真实样本。它的损失函数: **V(D, G) = log(D(x/y)) + log((1- D(G(z/y))))**这里x仅是真实样本, D(z/y)是生成样本,条件y就是希望生成的标签,因此生成器必须要生成和标签匹配的样本, 而判别器要做的不仅是判段图片是否是真实图片,还要判读图像和条件是否匹配。 所以在训练完之后,可以通过标签和噪声来使G生成指定的数据样本!
实验步骤
1. Fashion-MNIST Clothing Photograph Dataset
-
上面我们已经了解了本次实验的原理,下面介绍一下实验所用数据集,Fashion-MNIST Clothing Photograph Dataset由 60,000 张 28×28 像素的小正方形灰度图像组成,包含 10 种类型的服装,例如鞋子、T 恤、连衣裙等,导入数据集关键代码如下
import os os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' import torch import torchvision # data loading and transforming from torchvision.datasets import FashionMNIST from torch.utils.data import DataLoader from torchvision import transforms ## Define a transform to read the data in as a tensor data_transform = transforms.ToTensor() # 数据转换成张量 # The example below loads the dataset and summarizes the shape of the loaded dataset. train_data = FashionMNIST(root='./data', train=True, download=True, transform=data_transform) # Print out some stats about the training data print('Train data, number of images: ', len(train_data))
-
接下来我们指定数据的批量后下载准备数据加载器,设置batch_size,可以尝试将batch_size 更改为更大或更小,当开始训练网络时,看看batch_size如何影响损失
# prepare data loaders, set the batch_size batch_size = 20 train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True) # specify the image classes classes = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
-
选取小批量数据绘制训练集数据
import numpy as np import matplotlib.pyplot as plt %matplotlib inline # obtain one batch of training images dataiter = iter(train_loader) # 由迭代器对象构造迭代器 images, labels = dataiter.next() # images的大小就是我们指定的batch_size*1 * 28*28 images = images.numpy() # plot the images in the batch, along with the corresponding labels fig = plt.figure(figsize=(25, 4)) # 定义画布 for idx in np.arange(batch_size): ax = fig.add_subplot(2, batch_size/2, idx+1, xticks=[], yticks=[]) # ax.imshow(images[idx], cmap='gray') # 图片大小为1*28*28 # 从数组的形状中合并单维度条目,即把shape中为1的维度去掉。 # 如果没写axis就合并所有的维度为1的条目,否则只合并axis的那个。 ax.imshow(np.squeeze(images[idx]), cmap='gray') ax.set_title(classes[labels[idx]])
-
展示效果如下,可以看到我们获得了20张相应类型的图片模板,我们将使用训练数据集中的图像作为训练生成对抗网络的基础。
-
具体来说,生成器模型将学习如何使用鉴别器生成新的似是而非的服装项目,该鉴别器将尝试区分来自 Fashion MNIST 训练数据集的真实图像和生成器模型输出的新图像。
2.Conditional GAN for Fashion-MNIST
-
接下来为 Fashion-MNIST 数据集开发一个条件 GAN
-
这里补充一下argparse基本用法,argparse 是python自带的命令行参数解析包,可以用来方便地读取命令行参数。
import argparse ##TODO: you can tune the Hyperparameters as you need parser = argparse.ArgumentParser() parser.add_argument("--n_epochs", type=int, default=100, help="number of epochs of training") parser.add_argument("--batch_size", type=int, default=64, help="size of the batches") parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate") parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient") parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient") parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation") parser.add_argument("--label_dim", type=int, default=50, help="dimensionality of the label embedding") parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the noise embedding") parser.add_argument("--n_classes", type=int, default=10, help="number of classes for dataset") parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension") parser.add_argument("--channels", type=int, default=1, help="number of image channels") opt = parser.parse_args(args=[]) print(opt) img_shape = (opt.channels, opt.img_size, opt.img_size) cuda = True if torch.cuda.is_available() else False
-
先导入argparse这个包,然后包中的ArgumentParser类生成一个parser对象(参数解析器),其中的description描述这个参数解析器是干什么的,当在命令行显示帮助信息的时候会看到description描述的信息。
-
接着通过对象的add_argument函数来增加参数。这里我们增加了参数–n_epochs,type可以选择类型,default参数表示我们在运行命令时若没有提供参数,程序会将此值当做参数值。
-
最后采用对象的parse_args获取解析的参数,当’-‘和’–'同时出现的时候,系统默认后者为参数名,前者不是,但是在命令行输入的时候没有这个区分接下来就是打印参数信息了
-
在我们的实验里我们生成的参数解析器有如下参数,
-
定义生成器模型
-
我们知道生成器模型有两个输入:潜在空间中的一个点和图像类别标签的整数,输出单个 28×28 灰度图像
-
关键代码解释:首先定义label_embedding层,其作用为将维度为n_classes的向量转变为label_dim维度,然后定义函数block(input_feat, output_feat, normalize=True),为一个全连接层,主要作用是将输入维度图片信息经过nn.Linear()处理成输出维度,再进行归一化和激活处理。然后生成模型,分段处理,最后用Tanh进行激活处理,而forward()函数主要通过输入随机噪声z和标签信息调用模型生成图片
import torch.nn as nn import torch.nn.functional as F class Generator(nn.Module): def __init__(self): super(Generator, self).__init__() self.label_embedding = nn.Embedding(opt.n_classes, opt.label_dim) # TODO: There are many ways to implement the model, one alternative # architecture is (100+50)--->128--->256--->512--->1024--->(1,28,28) def block(input_feat, output_feat, normalize=True): '''Linear->BN->ReLU''' layers = [nn.Linear(input_feat, output_feat)] if normalize: layers.append(nn.BatchNorm1d(output_feat, 0.8)) layers.append(nn.ReLU(0.2)) return layers # blocks->Linear->Tanh self.model = nn.Sequential( # nn.Sequential()可以将一系列的操作打包 *block(opt.latent_dim + opt.label_dim, 128, normalize=False), *block(128, 256), *block(256, 512), *block(512, 1024), nn.Linear(1024, int(np.prod(img_shape))), nn.Tanh() ) def forward(self, noise, labels): labels = self.label_embedding(labels) img = self.model(torch.cat((noise, labels), -1)) img = img.contiguous().view(img.size(0), *img_shape) return img
-
使用torch.cat((A,B),dim)时,除拼接维数dim数值可不同外其余维数数值需相同,方能对齐
-
np.prod()函数用来计算所有元素的乘积,对于有多个维度的数组可以指定轴,如axis=1指定计算每一行的乘积。
-
nn.embedding()存储固定大小的词典的嵌入向量的查找表,意思就是说,给一个编号,嵌入层就能返回这个编号对应的嵌入向量,嵌入向量反映了各个编号代表的符号之间的语义关系
鉴别器模型
-
鉴别器模型有两个输入:真实图像或生成图像和相同的类别标签,输出真实或虚假的分类
-
关键实现代码解释如下:首先生成模型,共三层,每一层都进行Relu激活函数处理,同时可在某些层加入**dropout()**防止过拟合,最后进行Sigmoid函数处理,而forward函数通过输入图片以及其标签信息判断生成图片是来自真实图片的概率
class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() self.label_embedding = nn.Embedding(opt.n_classes, opt.label_dim) # TODO: There are many ways to implement the discriminator, one alternative # architecture is (50+784)--->512--->512--->512--->1 self.model = nn.Sequential( nn.Linear(int(np.prod(img_shape)) + opt.label_dim, 512), nn.LeakyReLU(0.2, True), nn.Linear(512, 512), nn.Dropout(0.4), nn.LeakyReLU(0.2, True), nn.Linear(512, 512), nn.Dropout(0.4), nn.Sigmoid() ) def forward(self, img, labels): labels = self.label_embedding(labels) img = img.view(img.size(0), -1) D_input = torch.cat((img, labels), -1).contiguous() validity = self.model(D_input) return validity
-
下面是模型的优化设置,采用Adam进行优化
import os # Loss functions adversarial_loss = torch.nn.MSELoss() # Initialize generator and discriminator generator = Generator() discriminator = Discriminator() if cuda: generator.cuda() discriminator.cuda() adversarial_loss.cuda() # Configure data loader dataloader = torch.utils.data.DataLoader( FashionMNIST(root='./data', train=True, download=False, transform=transforms.Compose( [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])] ), ), batch_size=opt.batch_size, shuffle=True, ) # Optimizers optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2)) optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2)) FloatTensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor LongTensor = torch.cuda.LongTensor if cuda else torch.LongTensor
-
首先,针对半批真实样本更新判别器模型,然后针对半批假样本更新判别器模型,共同形成一批权重更新。 然后通过复合 gan 模型更新生成器。 重要的是,对于假样本,类标签设置为 1 或真实。 这具有更新生成器以更好地在下一批生成真实样本的效果。生成器模型在训练结束时保存。该模型可以加载并用于从时尚 MNIST 数据集中生成新的随机但合理的样本。
-
训练生成器关键代码讲解:首先初始化生成器网络的梯度为0,然后随机生成噪声作为生成器的输入,通过调用生成器生成系列图片,根据公式计算损失即生成器生成图片能够骗过判别器的概率,不断进行参数更新
-
训练判别器关键代码讲解:首先初始化生成器网络的梯度为0,然后分别计算判别器鉴别器从生成的样本中分类真实的能力以及从真实数据中分别虚假的能力,计算损失,更新参数
## TODO: implement the training process for epoch in range(opt.n_epochs): for i, (imgs, labels) in enumerate(dataloader): batch_size = imgs.shape[0] # Adversarial ground truths valid = FloatTensor(batch_size, 1).fill_(1.0) fake = FloatTensor(batch_size, 1).fill_(0.0) # Configure input real_imgs = imgs.type(FloatTensor) labels = labels.type(LongTensor) # ----------------- # Train Generator # ----------------- optimizer_G.zero_grad() # Sample noise as generator input. noise = FloatTensor(np.random.normal(0, 1, (batch_size, opt.latent_dim))) # Generate a batch of images. g_imgs = generator(noise, labels) # Loss measures generator's ability to fool the discriminator. g_loss = adversarial_loss(discriminator(g_imgs, labels), valid) g_loss.backward() optimizer_G.step() # --------------------- # Train Discriminator # --------------------- ### START CODE HERE optimizer_D.zero_grad() # Measure discriminator's ability to classify real from generated samples. real_loss = adversarial_loss(discriminator(real_imgs, labels), valid) fake_loss = adversarial_loss(discriminator(g_imgs.detach(), labels), fake) d_loss = (real_loss + fake_loss) / 2 d_loss.backward() optimizer_D.step() if i % 100 == 0: print( "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]" % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item()) ) if (epoch+1) % 20 == 0: torch.save(generator.state_dict(), "./cgan_generator %d.pth" % (epoch + 1))
-
运行示例加载保存的条件 GAN 模型并使用它生成 100 件衣服。衣服是按列组织的。 从左到右,它们是“T恤”、“裤子”、“套头衫”、“连衣裙”、“外套”、“凉鞋”、“衬衫”、“运动鞋”、“包”和“踝靴” .我们不仅可以看到随机生成的服装项目是合理的,而且它们也符合预期的类别。
# TODO:example of loading the generator model and generating images from numpy import asarray from numpy.random import randn from numpy.random import randint from matplotlib import pyplot # generate points in latent space as input for the generator def generate_latent_points(latent_dim, n_samples, n_classes): # Sample noise z = FloatTensor(randn(n_samples, latent_dim)) labels = LongTensor(randint(n_classes, size=n_samples)) return z,labels # create and save a plot of generated images def save_plot(examples, n): # plot images for i in range(n * n): # define subplot pyplot.subplot(n, n, 1 + i) # turn off axis pyplot.axis('off') # plot raw pixel data pyplot.imshow(np.squeeze(examples[i, :, :]), cmap='gray') pyplot.show() # # load model generator=Generator() generator.cuda() generator.load_state_dict(torch.load('./cgan_generator 20.pth')) generator.eval() z, labels = generate_latent_points(100, 100, 10) X = generator(z, labels).cpu().detach() # scale from [-1,1] to [0,1] X = (X + 1) / 2.0 # plot the result save_plot(X, 10)
-
最后循环生成图片,可以看到在epoch较小的情况下生成的图片效果不太理想,效果如下
-
将epoch改为80重新训练后实现的效果如下
实验问题:
- 训练过程常常崩溃,将训练过程的输出改为 100 个 Batch 输出一次解决
参考文献:
-
原文链接:https://blog.csdn.net/yy_diego/article/details/82851661
-
原文链接:https://blog.csdn.net/qq_41559533/article/details/83785362