本文来自云栖社区官方钉群“Python技术进阶”,了解相关信息可以关注“Python技术进阶”。
关联权重
当自编码器整齐地对称时,就像我们刚刚构建的那样,一种常用技术是将解码器层的权重与编码器层的权重相关联。 这样减少了模型中的权重数量,加快了训练速度,并限制了过度拟合的风险。
不幸的是,使用fully_connected()函数在 TensorFlow 中实现相关权重有点麻烦;手动定义层实际上更容易。 代码结尾明显更加冗长:
activation = tf.nn.elu
regularizer = tf.contrib.layers.l2_regularizer(l2_reg)
initializer = tf.contrib.layers.variance_scaling_initializer()
X = tf.placeholder(tf.float32, shape=[None, n_inputs])
weights1_init = initializer([n_inputs, n_hidden1])
weights2_init = initializer([n_hidden1, n_hidden2])
weights1 = tf.Variable(weights1_init, dtype=tf.float32, name="weights1")
weights2 = tf.Variable(weights2_init, dtype=tf.float32, name="weights2")
weights3 = tf.transpose(weights2, name="weights3") # tied weights
weights4 = tf.transpose(weights1, name="weights4") # tied weights
biases1 = tf.Variable(tf.zeros(n_hidden1), name="biases1")
biases2 = tf.Variable(tf.zeros(n_hidden2), name="biases2")
biases3 = tf.Variable(tf.zeros(n_hidden3), name="biases3")
biases4 = tf.Variable(tf.zeros(n_outputs), name="biases4")
hidden1 = activation(tf.matmul(X, weights1) + biases1)
hidden2 = activation(tf.matmul(hidden1, weights2) + biases2)
hidden3 = activation(tf.matmul(hidden2, weights3) + biases3)
outputs = tf.matmul(hidden3, weights4) + biases4
reconstruction_loss = tf.reduce_mean(tf.square(outputs - X))
reg_loss = regularizer(weights1) + regularizer(weights2)
loss = reconstruction_loss + reg_loss
optimizer = tf.train.AdamOptimizer(learning_rate)
training_op = optimizer.minimize(loss)
init = tf.global_variables_initializer()
这段代码非常简单,但有几件重要的事情需要注意:
首先,权重 3 和权重 4 不是变量,它们分别是权重 2 和权重 1 的转置(它们与它们“绑定”)。
其次,由于它们不是变量,所以规范它们是没有用的:我们只调整权重 1 和权重 2。
第三,偏置永远不会被束缚,并且永远不会正规化。
一次训练一个自编码器
我们不是一次完成整个栈式自编码器的训练,而是一次训练一个浅自编码器,然后将所有这些自编码器堆叠到一个栈式自编码器(因此名称)中,通常要快得多,如图 15-4 所示。 这对于非常深的自编码器特别有用。
在训练的第一阶段,第一个自编码器学习重构输入。 在第二阶段,第二个自编码器学习重构第一个自编码器隐藏层的输出。 最后,您只需使用所有这些自编码器来构建一个大三明治,如图 15-4 所示(即,您首先将每个自编码器的隐藏层,然后按相反顺序堆叠输出层)。 这给你最后的栈式自编码器。 您可以用这种方式轻松地训练更多的自编码器,构建一个非常深的栈式自编码器。
为了实现这种多阶段训练算法,最简单的方法是对每个阶段使用不同的 TensorFlow 图。 训练完一个自编码器后,您只需通过它运行训练集并捕获隐藏层的输出。 这个输出作为下一个自编码器的训练集。 一旦所有自编码器都以这种方式进行了训练,您只需复制每个自编码器的权重和偏置,然后使用它们来构建堆叠的自编码器。 实现这种方法非常简单,所以我们不在这里详细说明,但请查阅 Jupyter notebooks 中的代码作为示例。
另一种方法是使用包含整个栈式自编码器的单个图,以及执行每个训练阶段的一些额外操作,如图 15-5 所示。
这值得解释一下:
图中的中央列是完整的栈式自编码器。这部分可以在训练后使用。
左列是运行第一阶段训练所需的一系列操作。它创建一个绕过隐藏层 2 和 3 的输出层。该输出层与堆叠的自编码器的输出层共享相同的权重和偏置。此外还有旨在使输出尽可能接近输入的训练操作。因此,该阶段将训练隐藏层1和输出层(即,第一自编码器)的权重和偏置。
图中的右列是运行第二阶段训练所需的一组操作。它增加了训练操作,目的是使隐藏层 3 的输出尽可能接近隐藏层 1 的输出。注意,我们必须在运行阶段 2 时冻结隐藏层 1。此阶段将训练隐藏层 2 和 3 的权重和偏置(即第二自编码器)。
TensorFlow 代码如下所示:
[...] # Build the whole stacked autoencoder normally.
# In this example, the weights are not tied.
optimizer = tf.train.AdamOptimizer(learning_rate)
with tf.name_scope("phase1"):
phase1_outputs = tf.matmul(hidden1, weights4) + biases4
phase1_reconstruction_loss = tf.reduce_mean(tf.square(phase1_outputs - X))
phase1_reg_loss = regularizer(weights1) + regularizer(weights4)
phase1_loss = phase1_reconstruction_loss + phase1_reg_loss
phase1_training_op = optimizer.minimize(phase1_loss)
with tf.name_scope("phase2"):
phase2_reconstruction_loss = tf.reduce_mean(tf.square(hidden3 - hidden1))
phase2_reg_loss = regularizer(weights2) + regularizer(weights3)
phase2_loss = phase2_reconstruction_loss + phase2_reg_loss
train_vars = [weights2, biases2, weights3, biases3]
phase2_training_op = optimizer.minimize(phase2_loss, var_list=train_vars)
第一阶段比较简单:我们只创建一个跳过隐藏层 2 和 3 的输出层,然后构建训练操作以最小化输出和输入之间的距离(加上一些正则化)。
第二阶段只是增加了将隐藏层 3 和隐藏层 1 的输出之间的距离最小化的操作(还有一些正则化)。 最重要的是,我们向minim()方法提供可训练变量的列表,确保省略权重 1 和偏差 1;这有效地冻结了阶段 2 期间的隐藏层 1。
在执行阶段,你需要做的就是为阶段 1 一些迭代进行训练操作,然后阶段 2 训练运行更多的迭代。
由于隐藏层 1 在阶段 2 期间被冻结,所以对于任何给定的训练实例其输出将总是相同的。 为了避免在每个时期重新计算隐藏层1的输出,您可以在阶段 1 结束时为整个训练集计算它,然后直接在阶段 2 中输入隐藏层 1 的缓存输出。这可以得到一个不错的性能上的提升。
可视化重建
确保自编码器得到适当训练的一种方法是比较输入和输出。 它们必须非常相似,差异应该是不重要的细节。 我们来绘制两个随机数字及其重建:
n_test_digits = 2
X_test = mnist.test.images[:n_test_digits]
with tf.Session() as sess:
[...] # Train the Autoencoder
outputs_val = outputs.eval(feed_dict={X: X_test})
def plot_image(image, shape=[28, 28]):
plt.imshow(image.reshape(shape), cmap="Greys", interpolation="nearest")
plt.axis("off")
for digit_index in range(n_test_digits):
plt.subplot(n_test_digits, 2, digit_index * 2 + 1)
plot_image(X_test[digit_index])
plt.subplot(n_test_digits, 2, digit_index * 2 + 2)
plot_image(outputs_val[digit_index])
看起来够接近。 所以自编码器已经适当地学会了重现它,但是它学到了有用的特性? 让我们来看看。
可视化功能
一旦你的自编码器学习了一些功能,你可能想看看它们。 有各种各样的技术。 可以说最简单的技术是在每个隐藏层中考虑每个神经元,并找到最能激活它的训练实例。 这对顶层隐藏层特别有用,因为它们通常会捕获相对较大的功能,您可以在包含它们的一组训练实例中轻松找到这些功能。 例如,如果神经元在图片中看到一只猫时强烈激活,那么激活它的图片最显眼的地方都会包含猫。 然而,对于较低层,这种技术并不能很好地工作,因为这些特征更小,更抽象,因此很难准确理解神经元正在为什么而兴奋。
让我们看看另一种技术。 对于第一个隐藏层中的每个神经元,您可以创建一个图像,其中像素的强度对应于给定神经元的连接权重。 例如,以下代码绘制了第一个隐藏层中五个神经元学习的特征:
with tf.Session() as sess:
[...] # train autoencoder
weights1_val = weights1.eval()
for i in range(5):
plt.subplot(1, 5, i + 1)
plot_image(weights1_val.T[i])
您可能会得到如图 15-7 所示的低级功能。
前四个特征似乎对应于小块,而第五个特征似乎寻找垂直笔划(请注意,这些特征来自堆叠去噪自编码器,我们将在后面讨论)。
另一种技术是给自编码器提供一个随机输入图像,测量您感兴趣的神经元的激活,然后执行反向传播来调整图像,使神经元激活得更多。 如果迭代数次(执行渐变上升),图像将逐渐变成最令人兴奋的图像(用于神经元)。 这是一种有用的技术,用于可视化神经元正在寻找的输入类型。
最后,如果使用自编码器执行无监督预训练(例如,对于分类任务),验证自编码器学习的特征是否有用的一种简单方法是测量分类器的性能。
无监督预训练使用栈式自编码器
正如我们在第 11 章中讨论的那样,如果您正在处理复杂的监督任务,但您没有大量标记的训练数据,则一种解决方案是找到执行类似任务的神经网络,然后重新使用其较低层。 这样就可以仅使用很少的训练数据来训练高性能模型,因为您的神经网络不必学习所有的低级特征;它将重新使用现有网络学习的特征检测器。
同样,如果您有一个大型数据集,但大多数数据集未标记,您可以先使用所有数据训练栈式自编码器,然后重新使用较低层为实际任务创建一个神经网络,并使用标记数据对其进行训练。 例如,图 15-8 显示了如何使用栈式自编码器为分类神经网络执行无监督预训练。 正如前面讨论过的,栈式自编码器本身通常每次都会训练一个自编码器。 在训练分类器时,如果您确实没有太多标记的训练数据,则可能需要冻结预训练层(至少是较低层)。
这种情况实际上很常见,因为构建一个大型的无标签数据集通常很便宜(例如,一个简单的脚本可以从互联网上下载数百万张图像),但只能由人类可靠地标记它们(例如,将图像分类为可爱或不可爱)。 标记实例是耗时且昂贵的,因此只有几千个标记实例是很常见的。
正如我们前面所讨论的那样,当前深度学习海啸的触发因素之一是 Geoffrey Hinton 等人在 2006 年的发现,深度神经网络可以以无监督的方式进行预训练。 他们使用受限玻尔兹曼机器(见附录 E),但在 2007 年 Yoshua Bengio 等人表明自编码器也起作用。
TensorFlow 的实现没有什么特别之处:只需使用所有训练数据训练自编码器,然后重用其编码器层以创建一个新的神经网络(有关如何重用预训练层的更多详细信息,请参阅第 11 章或查看 Jupyte notebooks 中的代码示例)。
到目前为止,为了强制自编码器学习有趣的特性,我们限制了编码层的大小,使其不够完善。 实际上可以使用许多其他类型的约束,包括允许编码层与输入一样大或甚至更大的约束,导致过度完成的自编码器。 现在我们来看看其中的一些方法。
降噪自编码(DAE)
另一种强制自编码器学习有用功能的方法是为其输入添加噪声,对其进行训练以恢复原始的无噪声输入。 这可以防止自编码器将其输入复制到其输出,因此最终不得不在数据中查找模式。
自 20 世纪 80 年代以来,使用自编码器消除噪音的想法已经出现(例如,在 Yann LeCun 的 1987 年硕士论文中提到过)。 在 2008 年的一篇论文中,帕斯卡尔文森特等人。 表明自编码器也可用于特征提取。 在 2010 年的一篇文章中 Vincent 等人引入堆叠降噪自编码器。
噪声可以是纯粹的高斯噪声添加到输入,或者它可以随机关闭输入,就像 drop out(在第 11 章介绍)。 图 15-9 显示了这两个选项。
TensorFlow 实现
在 TensorFlow 中实现去噪自编码器并不难。 我们从高斯噪声开始。 这实际上就像训练一个常规的自编码器一样,除了给输入添加噪声外,重建损耗是根据原始输入计算的:
X = tf.placeholder(tf.float32, shape=[None, n_inputs])
X_noisy = X + tf.random_normal(tf.shape(X))
[...]
hidden1 = activation(tf.matmul(X_noisy, weights1) + biases1)
[...]
reconstruction_loss = tf.reduce_mean(tf.square(outputs - X)) # MSE
[...]
由于X的形状只是在构造阶段部分定义的,我们不能预先知道我们必须添加到X中的噪声的形状。我们不能调用X.get_shape(),因为这只会返回部分定义的X的形状 ([None,n_inputs])和random_normal()需要一个完全定义的形状,因此会引发异常。 相反,我们调用tf.shape(X),它将创建一个操作,该操作将在运行时返回X的形状,该操作将在此时完全定义。
实施更普遍的 dropout 版本,而且这个版本并不困难:
from tensorflow.contrib.layers import dropout
keep_prob = 0.7
is_training = tf.placeholder_with_default(False, shape=(), name='is_training')
X = tf.placeholder(tf.float32, shape=[None, n_inputs])
X_drop = dropout(X, keep_prob, is_training=is_training)
[...]
hidden1 = activation(tf.matmul(X_drop, weights1) + biases1)
[...]
reconstruction_loss = tf.reduce_mean(tf.square(outputs - X)) # MSE
[...]
在训练期间,我们必须使用feed_dict将is_training设置为True(如第 11 章所述):
sess.run(training_op, feed_dict={X: X_batch, is_training: True})
但是,在测试期间,不需要将is_training设置为False,因为我们将其设置为对placeholder_with_default()函数调用的默认值。
原文发布时间为:2018-07-05
本文作者:ApacheCN【翻译】
本文来自云栖社区官方钉群“Python技术进阶”,了解相关信息可以关注“Python技术进阶”。
Python技术进阶交流群