半监督学习是一个具有挑战性的问题,它需要在一个包含少量标记样例和大量未标记样例的数据集中训练一个分类器。
生成式对抗网络(GAN)是一种有效利用大型未标记数据集,通过图像鉴别器模型训练图像生成器模型的体系结构。在某些情况下,鉴别器模型可以作为开发分类器模型的起点。
半监督GAN(或称SGAN)模型是GAN体系结构的扩展,它涉及同时训练监督鉴别器、非监督鉴别器和生成器模型。结果是一个监督分类模型,可以很好地推广到不可见的例子和一个生成器模型,可以输出来自该领域的图像的可信的例子。
在本教程中,您将了解如何从头开始开发一个半监督的生成式对抗网络。
完成本教程后,您将知道:
Let’s get started
本教程分为四个部分;它们是:
1、半监督GAN是什么?
2、如何实现半监督鉴别器模型
3、如何为MNIST开发半监督GAN
4、如何加载和使用最终的SGAN分类器模型
半监督学习是指需要一个预测模型,并且有标记的例子很少,而没有标记的例子很多的问题。
最常见的例子是一个分类预测建模问题,其中可能有一个非常大的数据集的例子,但只有一小部分有目标标签。该模型必须从小的标记示例集合中学习,并以某种方式利用更大的未标记示例数据集,以便在将来推广到对新示例进行分类。
半监督GAN(有时简称SGAN)是用于解决半监督学习问题的生成式对抗网络体系结构的扩展。
"这项工作的主要目标之一是提高生成式对抗性网络在半监督学习中的有效性(在这种情况下,通过对其他未标记示例的学习来提高监督任务的性能,即分类)。"
— Improved Techniques for Training GANs, 2016.
传统GAN中的鉴别器被训练来预测给定图像是真实的(来自数据集)还是伪造的(生成的),从而使它能够从未标记的图像中学习特征。然后,在为同一数据集开发分类器时,可以通过转移学习将该鉴别器作为起点使用,从而使监督预测任务受益于GAN的非监督训练。
在半监督GAN中,将识别器模型更新为预测K+1类,其中K为预测问题中的类数,并为新的“伪”类添加额外的类标签。它涉及到同时对无监督GAN任务和有监督分类任务直接训练鉴别器模型。
"我们将生成模型G和鉴别器D训练在一个数据集上,该数据集的输入属于N个类中的一个。在训练时,D用来预测N+1个类中输入属于哪个类,其中增加一个额外的类来对应G的输出。"
— Semi-Supervised Learning with Generative Adversarial Networks, 2016.
因此,鉴别器训练有两种模式:监督模式和非监督模式。
在无监督模式下的训练允许模型从大量未标记的数据集中学习有用的特征提取功能,而在监督模式下的训练允许模型使用提取的特征并应用类标签。
这样的结果是一个分类器模型,它可以在标准问题上获得最新的结果,比如MNIST,当它只训练很少的有标记的例子时,比如几十个,几百个,或者1000个。此外,该训练过程还可以使生成器模型输出的图像质量更好。
例如,Augustus Odena 在2016年的论文题为“Semi-Supervised Learning with Generative Adversarial Networks”展示了如何GAN-trained分类器能够执行以及或比一个独立的CNN模型MNIST手写数字识别任务训练时25岁,50岁,100年和1000年标签的例子。
OpenAI的Tim Salimans等人在他们2016年发表的论文 “Improved Techniques for Training GANs”中,使用半监督的GAN(包括MNIST)完成了当时最先进的图像分类任务。
有许多方法可以实现半监督GAN的鉴别器模型。
在本节中,我们将回顾三种候选方法。
传统的鉴别器模型
考虑一个标准GAN模型的鉴别器模型。
它必须以一个图像作为输入,并预测它是真的还是假的。更具体地说,它预测输入图像是真实的可能性。输出层使用sigmoid激活函数来预测[0,1]中的概率值,模型通常使用二元交叉熵损失函数进行优化。
例如,我们可以定义一个简单的识别器模型,该模型以灰度图像为输入,大小为28×28像素,预测图像的真实概率。我们可以使用最佳实践和采样图像使用卷积层与2×2的步幅和 leaky ReLU 激活函数。
下面的define_discriminator()函数实现了这个功能,并定义了我们的标准鉴别器模型。
# example of defining the discriminator model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Dropout
from keras.layers import Flatten
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
# define the standalone discriminator model
def define_discriminator(in_shape=(28,28,1)):
# image input
in_image = Input(shape=in_shape)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(in_image)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# flatten feature maps
fe = Flatten()(fe)
# dropout
fe = Dropout(0.4)(fe)
# output layer
d_out_layer = Dense(1, activation='sigmoid')(fe)
# define and compile discriminator model
d_model = Model(in_image, d_out_layer)
d_model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))
return d_model
# create model
model = define_discriminator()
# plot the model
plot_model(model, to_file='discriminator_plot.png', show_shapes=True, show_layer_names=True)
运行该示例将创建鉴别器模型的一个图,清楚地显示输入图像的28x28x1形状和单个概率值的预测。
使用共享权重的独立鉴别器模型
从标准的GAN鉴别器模型开始,我们可以更新它来创建两个共享特征提取权值的模型。
具体来说,我们可以定义一个分类器模型来预测输入图像是真还是假,然后定义第二个分类器模型来预测给定模型的类。
这两个模型都有不同的输出层,但是共享所有的特征提取层。这意味着对其中一个分类器模型的更新将影响两个模型。
下面的示例首先创建具有二进制输出的传统鉴别器模型,然后重用特征提取层并创建一个新的多类预测模型,在本例中为10个类。
# example of defining semi-supervised discriminator model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Dropout
from keras.layers import Flatten
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
# define the standalone supervised and unsupervised discriminator models
def define_discriminator(in_shape=(28,28,1), n_classes=10):
# image input
in_image = Input(shape=in_shape)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(in_image)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# flatten feature maps
fe = Flatten()(fe)
# dropout
fe = Dropout(0.4)(fe)
# unsupervised output
d_out_layer = Dense(1, activation='sigmoid')(fe)
# define and compile unsupervised discriminator model
d_model = Model(in_image, d_out_layer)
d_model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))
# supervised output
c_out_layer = Dense(n_classes, activation='softmax')(fe)
# define and compile supervised discriminator model
c_model = Model(in_image, c_out_layer)
c_model.compile(loss='sparse_categorical_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5), metrics=['accuracy'])
return d_model, c_model
# create model
d_model, c_model = define_discriminator()
# plot the model
plot_model(d_model, to_file='discriminator1_plot.png', show_shapes=True, show_layer_names=True)
plot_model(c_model, to_file='discriminator2_plot.png', show_shapes=True, show_layer_names=True)
运行该示例将创建和绘制两个模型。
第一个模型的图和之前一样。
第二个模型的图显示了相同的期望输入形状和相同的特征提取层,并添加了一个新的10类分类输出层。
实现半监督鉴别器模型的另一种方法是使用具有多个输出层的单个模型。
具体来说,这是一个单一的模型,一个输出层用于非监督任务,一个输出层用于监督任务。
这就像有监督和无监督的任务有单独的模型,因为它们共享相同的特征提取层,除了在这种情况下,每个输入图像总是有两个输出预测,特别是一个 real/fake 预测和一个监督类预测。
这种方法的一个问题是,当模型被更新、未标记和生成图像时,没有监督类标签。在这种情况下,这些图像必须有来自监督输出的“unknown” or “fake”输出标签。这意味着需要一个附加的类标签用于监督输出层。
下面的例子在半监督GAN架构中实现了鉴别器模型的多输出单模型方法。
我们可以看到,模型是用两个输出层定义的,监督任务的输出层是用n_classes + 1定义的。在本例11中,为附加的“unknown”类标签腾出空间。
我们还可以看到,模型被编译为两个损失函数,一个用于模型的每个输出层。
# example of defining semi-supervised discriminator model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Dropout
from keras.layers import Flatten
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
# define the standalone supervised and unsupervised discriminator models
def define_discriminator(in_shape=(28,28,1), n_classes=10):
# image input
in_image = Input(shape=in_shape)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(in_image)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# flatten feature maps
fe = Flatten()(fe)
# dropout
fe = Dropout(0.4)(fe)
# unsupervised output
d_out_layer = Dense(1, activation='sigmoid')(fe)
# supervised output
c_out_layer = Dense(n_classes + 1, activation='softmax')(fe)
# define and compile supervised discriminator model
model = Model(in_image, [d_out_layer, c_out_layer])
model.compile(loss=['binary_crossentropy', 'sparse_categorical_crossentropy'], optimizer=Adam(lr=0.0002, beta_1=0.5), metrics=['accuracy'])
return model
# create model
model = define_discriminator()
# plot the model
plot_model(model, to_file='multioutput_discriminator_plot.png', show_shapes=True, show_layer_names=True)
运行该示例将创建并绘制单个多输出模型。
图中清楚地显示了共享层和独立的无监督和监督输出层。
最后一种方法与前两种方法非常相似,它涉及创建单独的逻辑无监督模型和监督模型,但尝试重用一个模型的输出层,将其作为输入提供给另一个模型。
该方法基于OpenAI的Tim Salimans等人在2016年发表的论文“Improved Techniques for Training GANs.”中对半监督模型的定义。
在这篇论文中,他们描述了一个有效的实现,首先用K个输出类和一个softmax激活函数来创建监督模型。然后定义无监督模型,该模型在softmax激活之前接受监督模型的输出,然后计算指数输出的归一化和。
为了更清楚地说明这一点,我们可以在NumPy中实现这个激活函数,并通过它运行一些示例激活,看看会发生什么。
完整的示例如下所示。
# example of custom activation function
import numpy as np
# custom activation function
def custom_activation(output):
logexpsum = np.sum(np.exp(output))
result = logexpsum / (logexpsum + 1.0)
return result
# all -10s
output = np.asarray([-10.0, -10.0, -10.0])
print(custom_activation(output))
# all -1s
output = np.asarray([-1.0, -1.0, -1.0])
print(custom_activation(output))
# all 0s
output = np.asarray([0.0, 0.0, 0.0])
print(custom_activation(output))
# all 1s
output = np.asarray([1.0, 1.0, 1.0])
print(custom_activation(output))
# all 10s
output = np.asarray([10.0, 10.0, 10.0])
print(custom_activation(output))
请记住,在softmax激活函数之前的非监督模型的输出将是节点的直接激活。它们将是很小的正值或负值,但不会标准化,因为这将由softmax激活执行。
自定义激活函数将输出0.0到1.0之间的值。
小激活或负激活输出接近0.0的值,大激活或正激活输出接近1.0的值。我们可以在运行示例时看到这一点。
0.00013618124143106674
0.5246331135813284
0.75
0.890768227426964
0.9999848669190928
这意味着鼓励该模型对真实示例输出强类预测,对虚假示例输出小类预测或低激活。这是一个聪明的技巧,允许在两个模型中重用来自监督模型的相同输出节点。
激活函数几乎可以直接通过Keras后端实现,并从Lambda层调用,例如,将自定义函数应用到层的输入的层。
完整的示例如下所示。首先,用软最大激活和分类交叉熵损失函数定义监督模型。在softmax激活之前,非监督模型被堆积在监督模型的输出层之上,节点的激活通过Lambda层通过我们的自定义激活函数。
不需要激活函数,因为我们已经标准化了激活。与之前一样,无监督模型采用二进制交叉熵损失进行拟合。
# example of defining semi-supervised discriminator model
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers import Activation
from keras.layers import Lambda
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
from keras import backend
# custom activation function
def custom_activation(output):
logexpsum = backend.sum(backend.exp(output), axis=-1, keepdims=True)
result = logexpsum / (logexpsum + 1.0)
return result
# define the standalone supervised and unsupervised discriminator models
def define_discriminator(in_shape=(28,28,1), n_classes=10):
# image input
in_image = Input(shape=in_shape)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(in_image)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# flatten feature maps
fe = Flatten()(fe)
# dropout
fe = Dropout(0.4)(fe)
# output layer nodes
fe = Dense(n_classes)(fe)
# supervised output
c_out_layer = Activation('softmax')(fe)
# define and compile supervised discriminator model
c_model = Model(in_image, c_out_layer)
c_model.compile(loss='sparse_categorical_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5), metrics=['accuracy'])
# unsupervised output
d_out_layer = Lambda(custom_activation)(fe)
# define and compile unsupervised discriminator model
d_model = Model(in_image, d_out_layer)
d_model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))
return d_model, c_model
# create model
d_model, c_model = define_discriminator()
# plot the model
plot_model(d_model, to_file='stacked_discriminator1_plot.png', show_shapes=True, show_layer_names=True)
plot_model(c_model, to_file='stacked_discriminator2_plot.png', show_shapes=True, show_layer_names=True)
行该示例将创建并绘制这两个模型,它们看起来与第一个示例中的两个模型非常相似。
堆叠版本的无监督鉴别器模型:
叠加式监督鉴别器模型:
现在我们已经看到了如何在半监督GAN中实现鉴别器模型,我们可以开发一个完整的图像生成和半监督分类的示例。
在本节中,我们将为 MNIST handwritten digit dataset.开发一个半监督GAN模型。
数据集的0-9位有10个类,因此分类器模型将有10个输出节点。该模型将适用于包含60,000个示例的训练数据集。在训练数据集中,只有100张图像将与标签一起使用,10个类中的每个类有10张
我们将从定义模型开始。
我们将使用堆叠鉴别器模型,正如在前一节中定义的那样。
接下来,我们可以定义生成器模型。在这种情况下,生成器模型将潜在空间中的一个点作为输入,并使用转置卷积层输出一个28×28灰度图像。下面的define_generator()函数实现了这一点,并返回定义的生成器模型。
# define the standalone generator model
def define_generator(latent_dim):
# image generator input
in_lat = Input(shape=(latent_dim,))
# foundation for 7x7 image
n_nodes = 128 * 7 * 7
gen = Dense(n_nodes)(in_lat)
gen = LeakyReLU(alpha=0.2)(gen)
gen = Reshape((7, 7, 128))(gen)
# upsample to 14x14
gen = Conv2DTranspose(128, (4,4), strides=(2,2), padding='same')(gen)
gen = LeakyReLU(alpha=0.2)(gen)
# upsample to 28x28
gen = Conv2DTranspose(128, (4,4), strides=(2,2), padding='same')(gen)
gen = LeakyReLU(alpha=0.2)(gen)
# output
out_layer = Conv2D(1, (7,7), activation='tanh', padding='same')(gen)
# define model
model = Model(in_lat, out_layer)
return model
生成器模型将通过无监督鉴别器模型进行拟合。
我们将使用复合模型体系结构,在Keras中实现时用于训练生成器模型。具体来说,当生成器模型的输出直接传递给无监督鉴别器模型,并且鉴别器的权值被标记为不可训练时,就使用了权值共享。
下面的define_gan()函数实现了这一点,它将已经定义的生成器和鉴别器模型作为输入,并返回用于训练生成器模型的权重的复合模型。
# define the combined generator and discriminator model, for updating the generator
def define_gan(g_model, d_model):
# make weights in the discriminator not trainable
d_model.trainable = False
# connect image output from generator as input to discriminator
gan_output = d_model(g_model.output)
# define gan model as taking noise and outputting a classification
model = Model(g_model.input, gan_output)
# compile model
opt = Adam(lr=0.0002, beta_1=0.5)
model.compile(loss='binary_crossentropy', optimizer=opt)
return model
我们可以加载训练数据集,将像素缩放到[- 1,1]范围,以匹配生成器模型的输出值。
# load the images
def load_real_samples():
# load dataset
(trainX, trainy), (_, _) = load_data()
# expand to 3d, e.g. add channels
X = expand_dims(trainX, axis=-1)
# convert from ints to floats
X = X.astype('float32')
# scale from [0,255] to [-1,1]
X = (X - 127.5) / 127.5
print(X.shape, trainy.shape)
return [X, trainy]
我们还可以定义一个函数来选择训练数据集的子集,在其中保存标签并训练识别器模型的监督版本。
下面的 select_supervised_samples()函数实现了这一点,并且小心地确保示例的选择是随机的,并且类是平衡的。标记示例的数量是参数化的,并设置为100,这意味着10个类中的每个类都有10个随机选择的示例。
# select a supervised subset of the dataset, ensures classes are balanced
def select_supervised_samples(dataset, n_samples=100, n_classes=10):
X, y = dataset
X_list, y_list = list(), list()
n_per_class = int(n_samples / n_classes)
for i in range(n_classes):
# get all images for this class
X_with_class = X[y == i]
# choose random instances
ix = randint(0, len(X_with_class), n_per_class)
# add to list
[X_list.append(X_with_class[j]) for j in ix]
[y_list.append(i) for j in ix]
return asarray(X_list), asarray(y_list)
接下来,我们可以定义一个函数来检索一批实际的训练示例。
选择图像和标签的样本,并进行替换。稍后在训练模型时,可以使用此函数从标记和未标记的数据集中检索示例。对于“未标记的数据集”,我们将忽略标签。
# select real samples
def generate_real_samples(dataset, n_samples):
# split into images and labels
images, labels = dataset
# choose random instances
ix = randint(0, images.shape[0], n_samples)
# select images and labels
X, labels = images[ix], labels[ix]
# generate class labels
y = ones((n_samples, 1))
return [X, labels], y
接下来,我们可以定义函数来帮助使用生成器模型生成图像。
首先,generate_latent_points()函数将在潜在空间中创建一批随机点,这些点可以用作生成图像的输入。generate_fake_samples()函数将调用此函数生成一批有价值的图像,这些图像可以在训练期间提供给无监督鉴别器模型或复合GAN模型。
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n_samples):
# generate points in the latent space
z_input = randn(latent_dim * n_samples)
# reshape into a batch of inputs for the network
z_input = z_input.reshape(n_samples, latent_dim)
return z_input
# use the generator to generate n fake examples, with class labels
def generate_fake_samples(generator, latent_dim, n_samples):
# generate points in latent space
z_input = generate_latent_points(latent_dim, n_samples)
# predict outputs
images = generator.predict(z_input)
# create class labels
y = zeros((n_samples, 1))
return images, y
接下来,我们可以定义一个要在评估模型性能时调用的函数。
该函数将使用生成器模型的当前状态生成和绘制100幅图像。这个图像图可以用来主观地评价生成器模型的性能。
然后在整个训练数据集上评估监督鉴别器模型,并报告分类精度。最后,将生成器模型和监督鉴别器模型保存到文件中,供以后使用。
下面的summarize_performance()函数实现了这一点,可以定期调用,比如在每个训练周期结束时调用。可以在运行结束时查看结果,以选择分类器甚至生成器模型。
# generate samples and save as a plot and save the model
def summarize_performance(step, g_model, c_model, latent_dim, dataset, n_samples=100):
# prepare fake examples
X, _ = generate_fake_samples(g_model, latent_dim, n_samples)
# scale from [-1,1] to [0,1]
X = (X + 1) / 2.0
# plot images
for i in range(100):
# define subplot
pyplot.subplot(10, 10, 1 + i)
# turn off axis
pyplot.axis('off')
# plot raw pixel data
pyplot.imshow(X[i, :, :, 0], cmap='gray_r')
# save plot to file
filename1 = 'generated_plot_%04d.png' % (step+1)
pyplot.savefig(filename1)
pyplot.close()
# evaluate the classifier model
X, y = dataset
_, acc = c_model.evaluate(X, y, verbose=0)
print('Classifier Accuracy: %.3f%%' % (acc * 100))
# save the generator model
filename2 = 'g_model_%04d.h5' % (step+1)
g_model.save(filename2)
# save the classifier model
filename3 = 'c_model_%04d.h5' % (step+1)
c_model.save(filename3)
print('>Saved: %s, %s, and %s' % (filename1, filename2, filename3))
接下来,我们可以定义一个函数来训练模型。定义的模型和加载的训练数据集作为参数提供,并且使用默认值参数化训练周期和批大小的数量,在本例中为20个周期和100个批大小。
所选择的模型配置能够快速地对训练数据集进行过拟合,因此训练周期相对较小。将epochs增加到100或更多,生成的图像质量更高,但分类器模型质量更低。平衡这两个关注点可能是一个有趣的扩展。
首先,选择训练数据集的标记子集,并计算训练步骤的数量。
训练过程几乎与普通GAN模型的训练相同,只是增加了带标记的示例来更新监督模型。
在模型更新的单周期中,首先用带标记的样本更新有监督鉴别器模型,然后用无标记的真实和生成的样本更新无监督鉴别器模型。最后,通过复合模型对生成器模型进行更新。
鉴别器模型的共享权值更新为1.5批样本值,而生成器模型的权值更新为每次迭代更新一批样本值。改变这一点,使每个模型更新相同的数量,可能会改进模型培训过程。
# train the generator and discriminator
def train(g_model, d_model, c_model, gan_model, dataset, latent_dim, n_epochs=20, n_batch=100):
# select supervised dataset
X_sup, y_sup = select_supervised_samples(dataset)
print(X_sup.shape, y_sup.shape)
# calculate the number of batches per training epoch
bat_per_epo = int(dataset[0].shape[0] / n_batch)
# calculate the number of training iterations
n_steps = bat_per_epo * n_epochs
# calculate the size of half a batch of samples
half_batch = int(n_batch / 2)
print('n_epochs=%d, n_batch=%d, 1/2=%d, b/e=%d, steps=%d' % (n_epochs, n_batch, half_batch, bat_per_epo, n_steps))
# manually enumerate epochs
for i in range(n_steps):
# update supervised discriminator (c)
[Xsup_real, ysup_real], _ = generate_real_samples([X_sup, y_sup], half_batch)
c_loss, c_acc = c_model.train_on_batch(Xsup_real, ysup_real)
# update unsupervised discriminator (d)
[X_real, _], y_real = generate_real_samples(dataset, half_batch)
d_loss1 = d_model.train_on_batch(X_real, y_real)
X_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
d_loss2 = d_model.train_on_batch(X_fake, y_fake)
# update generator (g)
X_gan, y_gan = generate_latent_points(latent_dim, n_batch), ones((n_batch, 1))
g_loss = gan_model.train_on_batch(X_gan, y_gan)
# summarize loss on this batch
print('>%d, c[%.3f,%.0f], d[%.3f,%.3f], g[%.3f]' % (i+1, c_loss, c_acc*100, d_loss1, d_loss2, g_loss))
# evaluate the model performance every so often
if (i+1) % (bat_per_epo * 1) == 0:
summarize_performance(i, g_model, c_model, latent_dim, dataset)
最后,我们可以定义模型并调用函数来训练和保存模型。
# size of the latent space
latent_dim = 100
# create the discriminator models
d_model, c_model = define_discriminator()
# create the generator
g_model = define_generator(latent_dim)
# create the gan
gan_model = define_gan(g_model, d_model)
# load image data
dataset = load_real_samples()
# train model
train(g_model, d_model, c_model, gan_model, dataset, latent_dim)
将所有这些结合起来,下面列出了在MNIST手写数字图像分类任务上训练半监督GAN的完整示例。
# example of semi-supervised gan for mnist
from numpy import expand_dims
from numpy import zeros
from numpy import ones
from numpy import asarray
from numpy.random import randn
from numpy.random import randint
from keras.datasets.mnist import load_data
from keras.optimizers import Adam
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Reshape
from keras.layers import Flatten
from keras.layers import Conv2D
from keras.layers import Conv2DTranspose
from keras.layers import LeakyReLU
from keras.layers import Dropout
from keras.layers import Lambda
from keras.layers import Activation
from matplotlib import pyplot
from keras import backend
# custom activation function
def custom_activation(output):
logexpsum = backend.sum(backend.exp(output), axis=-1, keepdims=True)
result = logexpsum / (logexpsum + 1.0)
return result
# define the standalone supervised and unsupervised discriminator models
def define_discriminator(in_shape=(28,28,1), n_classes=10):
# image input
in_image = Input(shape=in_shape)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(in_image)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# downsample
fe = Conv2D(128, (3,3), strides=(2,2), padding='same')(fe)
fe = LeakyReLU(alpha=0.2)(fe)
# flatten feature maps
fe = Flatten()(fe)
# dropout
fe = Dropout(0.4)(fe)
# output layer nodes
fe = Dense(n_classes)(fe)
# supervised output
c_out_layer = Activation('softmax')(fe)
# define and compile supervised discriminator model
c_model = Model(in_image, c_out_layer)
c_model.compile(loss='sparse_categorical_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5), metrics=['accuracy'])
# unsupervised output
d_out_layer = Lambda(custom_activation)(fe)
# define and compile unsupervised discriminator model
d_model = Model(in_image, d_out_layer)
d_model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))
return d_model, c_model
# define the standalone generator model
def define_generator(latent_dim):
# image generator input
in_lat = Input(shape=(latent_dim,))
# foundation for 7x7 image
n_nodes = 128 * 7 * 7
gen = Dense(n_nodes)(in_lat)
gen = LeakyReLU(alpha=0.2)(gen)
gen = Reshape((7, 7, 128))(gen)
# upsample to 14x14
gen = Conv2DTranspose(128, (4,4), strides=(2,2), padding='same')(gen)
gen = LeakyReLU(alpha=0.2)(gen)
# upsample to 28x28
gen = Conv2DTranspose(128, (4,4), strides=(2,2), padding='same')(gen)
gen = LeakyReLU(alpha=0.2)(gen)
# output
out_layer = Conv2D(1, (7,7), activation='tanh', padding='same')(gen)
# define model
model = Model(in_lat, out_layer)
return model
# define the combined generator and discriminator model, for updating the generator
def define_gan(g_model, d_model):
# make weights in the discriminator not trainable
d_model.trainable = False
# connect image output from generator as input to discriminator
gan_output = d_model(g_model.output)
# define gan model as taking noise and outputting a classification
model = Model(g_model.input, gan_output)
# compile model
opt = Adam(lr=0.0002, beta_1=0.5)
model.compile(loss='binary_crossentropy', optimizer=opt)
return model
# load the images
def load_real_samples():
# load dataset
(trainX, trainy), (_, _) = load_data()
# expand to 3d, e.g. add channels
X = expand_dims(trainX, axis=-1)
# convert from ints to floats
X = X.astype('float32')
# scale from [0,255] to [-1,1]
X = (X - 127.5) / 127.5
print(X.shape, trainy.shape)
return [X, trainy]
# select a supervised subset of the dataset, ensures classes are balanced
def select_supervised_samples(dataset, n_samples=100, n_classes=10):
X, y = dataset
X_list, y_list = list(), list()
n_per_class = int(n_samples / n_classes)
for i in range(n_classes):
# get all images for this class
X_with_class = X[y == i]
# choose random instances
ix = randint(0, len(X_with_class), n_per_class)
# add to list
[X_list.append(X_with_class[j]) for j in ix]
[y_list.append(i) for j in ix]
return asarray(X_list), asarray(y_list)
# select real samples
def generate_real_samples(dataset, n_samples):
# split into images and labels
images, labels = dataset
# choose random instances
ix = randint(0, images.shape[0], n_samples)
# select images and labels
X, labels = images[ix], labels[ix]
# generate class labels
y = ones((n_samples, 1))
return [X, labels], y
# generate points in latent space as input for the generator
def generate_latent_points(latent_dim, n_samples):
# generate points in the latent space
z_input = randn(latent_dim * n_samples)
# reshape into a batch of inputs for the network
z_input = z_input.reshape(n_samples, latent_dim)
return z_input
# use the generator to generate n fake examples, with class labels
def generate_fake_samples(generator, latent_dim, n_samples):
# generate points in latent space
z_input = generate_latent_points(latent_dim, n_samples)
# predict outputs
images = generator.predict(z_input)
# create class labels
y = zeros((n_samples, 1))
return images, y
# generate samples and save as a plot and save the model
def summarize_performance(step, g_model, c_model, latent_dim, dataset, n_samples=100):
# prepare fake examples
X, _ = generate_fake_samples(g_model, latent_dim, n_samples)
# scale from [-1,1] to [0,1]
X = (X + 1) / 2.0
# plot images
for i in range(100):
# define subplot
pyplot.subplot(10, 10, 1 + i)
# turn off axis
pyplot.axis('off')
# plot raw pixel data
pyplot.imshow(X[i, :, :, 0], cmap='gray_r')
# save plot to file
filename1 = 'generated_plot_%04d.png' % (step+1)
pyplot.savefig(filename1)
pyplot.close()
# evaluate the classifier model
X, y = dataset
_, acc = c_model.evaluate(X, y, verbose=0)
print('Classifier Accuracy: %.3f%%' % (acc * 100))
# save the generator model
filename2 = 'g_model_%04d.h5' % (step+1)
g_model.save(filename2)
# save the classifier model
filename3 = 'c_model_%04d.h5' % (step+1)
c_model.save(filename3)
print('>Saved: %s, %s, and %s' % (filename1, filename2, filename3))
# train the generator and discriminator
def train(g_model, d_model, c_model, gan_model, dataset, latent_dim, n_epochs=20, n_batch=100):
# select supervised dataset
X_sup, y_sup = select_supervised_samples(dataset)
print(X_sup.shape, y_sup.shape)
# calculate the number of batches per training epoch
bat_per_epo = int(dataset[0].shape[0] / n_batch)
# calculate the number of training iterations
n_steps = bat_per_epo * n_epochs
# calculate the size of half a batch of samples
half_batch = int(n_batch / 2)
print('n_epochs=%d, n_batch=%d, 1/2=%d, b/e=%d, steps=%d' % (n_epochs, n_batch, half_batch, bat_per_epo, n_steps))
# manually enumerate epochs
for i in range(n_steps):
# update supervised discriminator (c)
[Xsup_real, ysup_real], _ = generate_real_samples([X_sup, y_sup], half_batch)
c_loss, c_acc = c_model.train_on_batch(Xsup_real, ysup_real)
# update unsupervised discriminator (d)
[X_real, _], y_real = generate_real_samples(dataset, half_batch)
d_loss1 = d_model.train_on_batch(X_real, y_real)
X_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
d_loss2 = d_model.train_on_batch(X_fake, y_fake)
# update generator (g)
X_gan, y_gan = generate_latent_points(latent_dim, n_batch), ones((n_batch, 1))
g_loss = gan_model.train_on_batch(X_gan, y_gan)
# summarize loss on this batch
print('>%d, c[%.3f,%.0f], d[%.3f,%.3f], g[%.3f]' % (i+1, c_loss, c_acc*100, d_loss1, d_loss2, g_loss))
# evaluate the model performance every so often
if (i+1) % (bat_per_epo * 1) == 0:
summarize_performance(i, g_model, c_model, latent_dim, dataset)
# size of the latent space
latent_dim = 100
# create the discriminator models
d_model, c_model = define_discriminator()
# create the generator
g_model = define_generator(latent_dim)
# create the gan
gan_model = define_gan(g_model, d_model)
# load image data
dataset = load_real_samples()
# train model
train(g_model, d_model, c_model, gan_model, dataset, latent_dim)
该示例可以在带有CPU或GPU硬件的工作站上运行,不过推荐使用GPU以获得更快的执行速度。
考虑到训练算法的随机性,你的具体结果会有所不同。考虑将该示例运行几次。
在开始运行时,将总结训练数据集的大小,以及监督子集,从而确认我们的配置。
每个模型的性能在每次更新结束时汇总,包括有监督鉴别器模型的损失和准确性(c),无监督鉴别器模型对真实和生成的示例的损失(d),以及通过复合模型更新的生成器模型的损失(g)。
监督模型的损失将缩小到接近于零的一个小值,精度将达到100%,这将在整个运行过程中保持。如果非监督鉴别器和生成器保持平衡,它们的损耗在整个运行过程中应该保持在一个适中的值。
(60000, 28, 28, 1) (60000,)
(100, 28, 28, 1) (100,)
n_epochs=20, n_batch=100, 1/2=50, b/e=600, steps=12000
>1, c[2.305,6], d[0.096,2.399], g[0.095]
>2, c[2.298,18], d[0.089,2.399], g[0.095]
>3, c[2.308,10], d[0.084,2.401], g[0.095]
>4, c[2.304,8], d[0.080,2.404], g[0.095]
>5, c[2.254,18], d[0.077,2.407], g[0.095]
...
在每个训练周期结束时,在整个训练数据集上评估监督分类模型,在这种情况下,是在每600次训练更新之后。此时,对模型的性能进行了总结,表明该模型能够快速获得良好的技能。
令人惊讶的是,该模型只针对每个类的10个标记示例进行训练。
Classifier Accuracy: 85.543%
Classifier Accuracy: 91.487%
Classifier Accuracy: 92.628%
Classifier Accuracy: 94.017%
Classifier Accuracy: 94.252%
Classifier Accuracy: 93.828%
Classifier Accuracy: 94.122%
Classifier Accuracy: 93.597%
Classifier Accuracy: 95.283%
Classifier Accuracy: 95.287%
Classifier Accuracy: 95.263%
Classifier Accuracy: 95.432%
Classifier Accuracy: 95.270%
Classifier Accuracy: 95.212%
Classifier Accuracy: 94.803%
Classifier Accuracy: 94.640%
Classifier Accuracy: 93.622%
Classifier Accuracy: 91.870%
Classifier Accuracy: 92.525%
Classifier Accuracy: 92.180%
在每个训练周期结束时保存模型,并创建生成的图像图。
由于训练时间相对较少,生成的图像质量较好。
如何加载和使用最终的SGAN分类器模型。
现在我们已经训练了生成器和鉴别器模型,我们可以利用它们。
在半监督GAN的情况下,我们对生成器模型的兴趣较小,而对监督模型的兴趣较大。
检查特定运行的结果,我们可以选择一个已知在测试数据集上具有良好性能的特定保存的模型。在本例中,模型在12个训练时点(即7,200次更新)之后保存,该模型对训练数据集的分类准确率约为95.432%。
我们可以通过load_model() Keras函数直接加载模型。
一旦加载,我们可以在整个训练数据集上再次评估它,以确认发现,然后在测试数据集上评估它。
回想一下,特征提取层期望输入图像的像素值缩放到[-1,1]范围,因此,这必须在向模型提供任何图像之前执行。
加载保存的半监督分类器模型并在完整的MNIST数据集中对其进行评估的完整示例如下所示。
# example of loading the classifier model and generating images
from numpy import expand_dims
from keras.models import load_model
from keras.datasets.mnist import load_data
# load the model
model = load_model('c_model_7200.h5')
# load the dataset
(trainX, trainy), (testX, testy) = load_data()
# expand to 3d, e.g. add channels
trainX = expand_dims(trainX, axis=-1)
testX = expand_dims(testX, axis=-1)
# convert from ints to floats
trainX = trainX.astype('float32')
testX = testX.astype('float32')
# scale from [0,255] to [-1,1]
trainX = (trainX - 127.5) / 127.5
testX = (testX - 127.5) / 127.5
# evaluate the model
_, train_acc = model.evaluate(trainX, trainy, verbose=0)
print('Train Accuracy: %.3f%%' % (train_acc * 100))
_, test_acc = model.evaluate(testX, testy, verbose=0)
print('Test Accuracy: %.3f%%' % (test_acc * 100))
运行示例将加载模型并在MNIST数据集上对其进行计算。
我们可以看到,在这种情况下,模型在训练数据集上达到了95.432%的预期性能,确认我们加载了正确的模型。
我们还可以看到,holdout测试数据集的准确性同样好,或者稍好一些,大约为95.920%。说明该分类器具有良好的泛化能力。
Train Accuracy: 95.432%
Test Accuracy: 95.920%
我们已经成功地演示了通过GAN架构来训练和评估半监督分类器模型。
本节列出了一些扩展教程的想法,您可能希望探索这些想法。
如果您探索这些扩展中的任何一个,我很想知道。
在下面的评论中发表你的发现。
如果您想深入了解这个主题,本节将提供更多的参考资料。
在本教程中,您了解了如何从无到有地开发一个半监督生成的对抗性网络。
具体来说,你学会了:
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。