前言
如果您曾经听说过或研究过深度学习,那么您可能就知道MNIST, SVHN, ImageNet, PascalVoc或者其他数据集。这些数据集都有一个共同点: 它们由成千上万个有标签的数据组成。 换句话说,这些集合由(x,y)对组成,其中(x)是原始数据,例如是一个图像矩阵;而(y)则表示该数据点(x)表示的标签。
以MNIST数据集为例, MNIST数据集包含60,000个图像数据,每个数据表征0到10的数字图像中的某一个。其数据表示形式一般为一个(输入数据,数据标签)对。 其中,MNIST的输入数据是一个28x28灰度图像,而数据标签则是该图像的内容(类别)。
这样的有标签的数据集通常用于训练监督模型。监督学习一直是深度学习中大多数研究的中心,但是其训练过程需要大量的含有标签的数据。而现实生活中,给数据打标签是一个非常昂贵而且容易出错的工作。
考虑到这一点,为了利用打好标签的一部分数据以及未曾拥有标签的大部分数据,研究人员提出了半监督学习。半监督学习解决了有标签数据较少时,模型训练效果较差的问题。 它通过从没有标签的数据中提取有用的信息来弥补标签损失带来的不足,进一步提高了模型的整体性能。
半监督的分类器需要一小部分标记数据和大量未标记数据(来自同一域)。其目标是结合这些数据源来训练模型,比如深度卷积神经网络(DCNN),以学习这些数据中的类别信息。
在此领域中,我们学者提出一个GAN模型,该模型使用一个非常小的标签训练集对街景门牌号进行分类。实际上,该模型使用了大约1.3%的原始SVHN培训标签,即1000个(一千个)标签示例。我们使用了OpenAI的《改进GAN训练技术》一书中描述的一些技术。
如果您不熟悉图像生成GAN,请参阅《生成对抗网络简介》。本文引用了该文章中描述的一些内容。
网络
如图2.,在构建用于生成图像的GAN时,我们同时训练了生成器和决策器。在训练之后,我们可以放弃决策器,因为我们只用它来训练生成器。
对于半监督学习,我们需要将决策器转换为多类分类器。这个新模型必须能够在测试集上很好地推广,即使我们没有很多标记的例子来训练。
此外,在这个例子中,生成器和决策器的角色发生了变化。在训练结束时,我们实际上可以扔掉生成器。因为,在本例子的训练过程中,发生器只用于帮助决策器。
换句话说,生成器充当不同的信息源,决策器从中获得未经标记的原始训练数据。正如我们看到的,这种未标记数据是提高鉴别器性能的关键。
此外,对于常规图像生成GAN,决策器只有一个角色:计算输入是真还是假的概率-----我们称之为GAN问题。
然而,为了将决策器变成半监督分类器,除了GAN问题,决策器还必须学习原始数据集中每个类的概率。换句话说,对于每一个输入图像,识别器必须学习它变成1、2、3等等类别的概率。
回想一下,对于一个图像生成GAN决策器,我们有一个单一的sigmoid单位输出。这个值表示输入图像为真(值接近1)或假(值接近0)的概率。
换句话说,从决策器的角度来看,值接近于1意味着样本很可能来自于训练集,同样的,值接近于0意味着样本来自于生成器网络的变化更大。
通过利用这个概率,决策器就能把信号送回发生器。这个信号允许发生器在训练期间适应它的参数,使它有可能提高其能力创造真实的图像。
我们必须将决策器(从以前的GAN)转换为一个11类分类器。为此,我们可以将其sigmoid输出转换为softmax,其中包含11个类输出。前10个类表示SVHN数据集的单个类概率(0到9),第11个类表示来自生成器的所有伪图像。
注意,如果我们将第11类概率设置为0,那么前10个概率的和表示使用sigmoid函数计算的相同概率。
最后,我们需要设置损失,以这样的方式,决策器可以做到:
- (i)帮助生成器学习产生真实的图像。要做到这一点,我们必须指示决策器区分真样本和假样本。
- (ii)使用生成器的图像,以及标记和未标记的训练数据,以帮助分类数据集。
总之,决策器有三种不同的训练数据来源。
带有标签的真实图像。这些是像任何常规监督分类问题一样的图像标签对。
没有标签的真实图像。对于这些,分类器只知道这些图像是真实的。
来自生成器的图像。对于这些人,决策器学会把它们归类为假的。
这些不同数据源的组合将使分类器能够从更广阔的视角学习。反过来,这使得模型能够进行更精确的推理,而不是仅仅使用1000个标记的例子进行训练。
生成器
生成器遵循DCGAN论文中描述的一般实现即可。该方法采用随机向量z作为输入。把它重塑成一个4D张量,然后把它输入到一系列转置卷积、批处理标准化(BN)和leaky ReLU操作中。
这个计算序列增加了输入向量的空间维数,同时减少了它的通道数。结果,网络通过双曲正切函数输出一个压缩值在-1和1之间的32x32x3 RGB张量形状。
def generator(z, output_dim, reuse=False, alpha=0.2, training=True, size_mult=128):
with tf.variable_scope('generator', reuse=reuse):
# First fully connected layer
x1 = tf.layers.dense(z, 4 * 4 * size_mult * 4)
# Reshape it to start the convolutional stack
x1 = tf.reshape(x1, (-1, 4, 4, size_mult * 4))
x1 = tf.layers.batch_normalization(x1, training=training)
x1 = tf.maximum(alpha * x1, x1)
x2 = tf.layers.conv2d_transpose(x1, size_mult * 2, 5, strides=2, padding='same')
x2 = tf.layers.batch_normalization(x2, training=training)
x2 = tf.maximum(alpha * x2, x2)
x3 = tf.layers.conv2d_transpose(x2, size_mult, 5, strides=2, padding='same')
x3 = tf.layers.batch_normalization(x3, training=training)
x3 = tf.maximum(alpha * x3, x3)
# Output layer
logits = tf.layers.conv2d_transpose(x3, output_dim, 5, strides=2, padding='same')
out = tf.tanh(logits)
return out
决策器
决策器现在是一个多类的分类器。在这里,我们设置了一个同样类似的DCGAN架构,其中我们使用带有BN和ReLU的卷积堆栈。
我们使用strided卷积来减少特征向量的维数。注意,并不是所有的卷积都执行这种类型的计算。当我们想保持特征向量的维数不变时,我们使用长度为1的步长;否则,我们使用长度为2的步长。最后,为了稳定学习,我们广泛使用BN(除了网络的第一层)。
在所有的卷积中,2D卷积窗口(卷积核或过滤器)的宽度和高度被设置为3。另外,注意我们有一些带有dropout的图层。我们的决策器的(部分)行为与任何其他常规分类器相似,理解这一点很重要。正因为如此,它可能会遇到任何没有经过良好设计的分类器都会遇到的问题。
当在一个非常有限的数据集上训练一个大型分类器时,最可能遇到的缺点之一是过度拟合的问题。对于“过度训练”的分类器,需要注意的一点是,它们通常在训练错误(较小)和测试错误(较高)之间显示出明显的差异。
这表明该模型能够很好地捕捉训练数据集的结构。但是,由于它过于相信训练数据,从而导致无法对未见的例子进行泛化。
为了防止这种情况发生,我们通常通过dropout来实现网络层的正则化,哪怕是在网络的第一层。
def discriminator(x, reuse=False, alpha=0.2, drop_rate=0., num_classes=10, size_mult=64):
with tf.variable_scope('discriminator', reuse=reuse):
x = tf.layers.dropout(x, rate=drop_rate/2.5)
# Input layer is ?x32x32x3
x1 = tf.layers.conv2d(x, size_mult, 3, strides=2, padding='same')
relu1 = tf.maximum(alpha * x1, x1)
relu1 = tf.layers.dropout(relu1, rate=drop_rate) # [?x16x16x?]
x2 = tf.layers.conv2d(relu1, size_mult, 3, strides=2, padding='same')
bn2 = tf.layers.batch_normalization(x2, training=True) # [?x8x8x?]
relu2 = tf.maximum(alpha * bn2, bn2)
x3 = tf.layers.conv2d(relu2, size_mult, 3, strides=2, padding='same') # [?x4x4x?]
bn3 = tf.layers.batch_normalization(x3, training=True)
relu3 = tf.maximum(alpha * bn3, bn3)
relu3 = tf.layers.dropout(relu3, rate=drop_rate)
x4 = tf.layers.conv2d(relu3, 2 * size_mult, 3, strides=1, padding='same') # [?x4x4x?]
bn4 = tf.layers.batch_normalization(x4, training=True)
relu4 = tf.maximum(alpha * bn4, bn4)
x5 = tf.layers.conv2d(relu4, 2 * size_mult, 3, strides=1, padding='same') # [?x4x4x?]
bn5 = tf.layers.batch_normalization(x5, training=True)
relu5 = tf.maximum(alpha * bn5, bn5)
x6 = tf.layers.conv2d(relu5, 2 * size_mult, 3, strides=2, padding='same') # [?x2x2x?]
bn6 = tf.layers.batch_normalization(x6, training=True)
relu6 = tf.maximum(alpha * bn6, bn6)
relu6 = tf.layers.dropout(relu6, rate=drop_rate)
...
最后,我们执行全局平均池(GAP),而不是在卷积堆栈的顶部应用一个完全连接的层。在GAP中,我们对特征向量的空间维度取平均值,从而压缩张量维数到一个单一的值。
...
# Flatten it by global average pooling
# In global average pooling, for every feature map we take the average over all the spatial
# domain and return a single value
# In: [BATCH_SIZE,HEIGHT X WIDTH X CHANNELS] --> [BATCH_SIZE, CHANNELS]
#输入: [BATCH大小,X长 X宽 通道数] --> [BATCH_SIZE, 通道数]
features = tf.reduce_mean(relu7, axis=[1,2])
# Set class_logits to be the inputs to a softmax distribution over the different classes
#将class_logits设置为softmax分布 在不同类别上的输入
class_logits = tf.layers.dense(features, num_classes)
...
例如,假设在经过一系列卷积后,我们得到一个形状为[BATCH大小, 8,8, 通道数]的张量。为了实现GAP算法,我们取[8x8]张量切片上的平均值。这样就得到了一个形状为[BATCH大小, 1,1, 通道数]的张量,该张量可以被重塑为[BATCH大小, 通道数]。
在论文《Network In Network》中,描述了GAP层相对于传统的全连接层的一些优点。其中包括:更高的鲁棒性空间转换和较少的过度拟合问题。在GAP层之后,我们应用一个完全连接的层来输出最后的结果。它们的形状为 [BATCH大小, 通道数]并对应于未缩放的最终类别的值。
为了得到分类概率,我们通过softmax函数来处理最后的结果。然而,我们仍然需要一种方法来表示输入图像的真实概率而不是假的。也就是说,我们仍然需要考虑GAN中的标准的二值分类问题。
...
# Get the probability that the input is real rather than fake
out = tf.nn.softmax(class_logits) # class probabilities for the 10 real classes
...
我们知道最后的结果一般是softmax的概率值。然而,我们也需要一种方法来表示它们为sigmoid 输出类型。我们知道输入是实数的概率对应于所有实数对数的和。考虑到这一点,我们可以将这些值提供给LogSumExp函数,该函数将建模二进制分类值。在此之后,我们将LogSumExp的结果提输入到一个sigmoid函数中。
我们可以使用Tensorflow的LogSumExp内置函数来避免数值问题。这个函数防止了在LogSumExp遇到非常极端的值(正数或负数)时可能发生的上/下溢流的问题。
...
# This function is more numerically stable than log(sum(exp(input))).
# It avoids overflows caused by taking the exp of large inputs and underflows
# caused by taking the log of small inputs.
gan_logits = tf.reduce_logsumexp(class_logits, 1)
...
模型损失
如前所述,我们可以将决策器损失分为两部分:一个代表GAN问题,无监督损失;另一个计算个体真实类概率,监督损失。
对于无监督损失,决策器必须区分真实的训练图像和来自生成器的伪图像。
对于常规的GAN,一半的时间决策器接收来自训练集的未标记图像,另一半时间接收来自生成器的虚的未标记图像。
在这两种情况下,我们处理的都是一个二元分类问题。由于我们希望真实图像的概率值接近1,非真实图像的概率值接近0,我们可以使用sigmoid型交叉熵函数来计算损失。
对于来自训练集的图像,我们通过分配1的标签来最大化其真实的概率。对于来自生成器的伪造图像,我们通过给它们0的标签来最大化其伪造的可能性。
...
# Here we compute `d_loss`, the loss for the discriminator.
# This should combine two different losses:
# 1. The loss for the GAN problem, where we minimize the cross-entropy for the binary
# real-vs-fake classification problem.
tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_data,
labels=tf.ones_like(gan_logits_on_data) * (1 - smooth)))
fake_data_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=gan_logits_on_samples,
labels=tf.zeros_like(gan_logits_on_samples)))
# This way, the unsupervised
unsupervised_loss = real_data_loss + fake_data_loss
...
对于监督损失,我们需要使用从决策器中得到的结果。由于这是一个多类分类问题,我们可以利用softmax交叉熵函数与我们现有的真实标签。
注意,这一部分与任何其他分类模型相似。决策器损失是监督损失和非监督损失的总和。而且,因为我们假装我们没有大多数的标签,我们需要忽略他们在监督损失。为此,我们将损失乘以mask变量,该变量指示可用的标签集。
# 2. The loss for the SVHN digit classification problem, where we minimize the cross-entropy
# for the multi-class softmax. For this one we use the labels. Don't forget to ignore
# use `label_mask` to ignore the examples that we are pretending are unlabeled for the
# semi-supervised learning problem.
y = tf.squeeze(y)
suppervised_loss = tf.nn.softmax_cross_entropy_with_logits(logits=class_logits_on_data,
labels=tf.one_hot(y, num_classes, dtype=tf.float32))
label_mask = tf.squeeze(tf.to_float(label_mask))
# ignore the labels that we pretend does not exist for the loss
suppervised_loss = tf.reduce_sum(tf.multiply(suppervised_loss, label_mask))
# get the mean
suppervised_loss = suppervised_loss / tf.maximum(1.0, tf.reduce_sum(label_mask))
d_loss = unsupervised_loss + suppervised_loss
正如在论文《 Improved Techniques for Training GANs》中所描述的,我们使用特征匹配的生成器损失。
如作者所述:
特征匹配的概念是对训练数据上某组特征的平均值和生成样本上这组特征的平均值之间的绝对平均误差进行惩罚。
为了做到这一点,我们从两个不同的来源获取一些统计数据(动量),并迫使它们相似。
首先,当我们小批量的处理一个真实的训练数据时,最后的结果需要对从决策器中提取的特征进行平均得到。
其次,我们用同样的方法来计算由决策器得出的来自生成器生成的假图像的结果的动量。
最后,用这两组矩,生成器损失是他们之间的平均绝对差。换句话说,正如本文所强调的:
我们训练生成器去匹配鉴别器中间层上特征的期望值
# Here we set `g_loss` to the "feature matching" loss invented by Tim Salimans at OpenAI.
# This loss consists of minimizing the absolute difference between the expected features
# on the data and the expected features on the generated samples.
# This loss works better for semi-supervised learning than the tradition GAN losses.
# Make the Generator output features that are on average similar to the features
# that are found by applying the real data to the discriminator
data_moments = tf.reduce_mean(data_features, axis=0)
sample_moments = tf.reduce_mean(sample_features, axis=0)
g_loss = tf.reduce_mean(tf.abs(data_moments - sample_moments))
pred_class = tf.cast(tf.argmax(class_logits_on_data, 1), tf.int32)
eq = tf.equal(tf.squeeze(y), pred_class)
correct = tf.reduce_sum(tf.to_float(eq))
masked_correct = tf.reduce_sum(label_mask * tf.to_float(eq))
虽然特征匹配损失在半监督学习的任务中表现得很好,但是由生成器生成的图像并没有上一篇文章中生成的那么好。
在论文《Improved Techniques for Training GANs》中,OpenAI报告了其在MNIST、CIFAR-10和SVHN上半监督分类学习的最新结果。
我们的方案实现了训练和测试的准确率分别接近93%和68%。这些结果比NIPS 2014年报告的大约64%的结果要好。
本笔记不打算演示交叉验证技术的最佳实践。它只使用了OpenAI原始论文中描述的一些技术。
总结
许多研究认为无监督学习是一般人工智能系统的缺失环节。要突破这些障碍,关键是尝试使用较少标记的数据来解决已经存在的问题。在这个场景中,GANs为学习使用较少标记样本的复杂任务提供了一个真正的替代方案。
然而,监督学习和半监督学习之间的性能差距仍然远远不是相等的。但我们当然可以预期,随着新方法的出现,这种差距将会缩小。
原英文链接:https://towardsdatascience.com/semi-supervised-learning-with-gans-9f3cb128c5e