TensorFlow 学习手册(二)
译者:飞龙
第四章:卷积神经网络
在本章中,我们介绍卷积神经网络(CNNs)以及与之相关的构建块和方法。我们从对 MNIST 数据集进行分类的简单模型开始,然后介绍 CIFAR10 对象识别数据集,并将几个 CNN 模型应用于其中。尽管小巧快速,但本章介绍的 CNN 在实践中被广泛使用,以获得物体识别任务中的最新结果。
CNN 简介
在过去几年中,卷积神经网络作为一种特别有前途的深度学习形式获得了特殊地位。根植于图像处理,卷积层已经在几乎所有深度学习的子领域中找到了应用,并且在大多数情况下非常成功。
全连接和卷积神经网络之间的根本区别在于连续层之间连接的模式。在全连接的情况下,正如名称所示,每个单元都连接到前一层的所有单元。我们在第二章中看到了一个例子,其中 10 个输出单元连接到所有输入图像像素。
另一方面,在神经网络的卷积层中,每个单元连接到前一层中附近的(通常很少)几个单元。此外,所有单元以相同的方式连接到前一层,具有相同的权重和结构。这导致了一种称为卷积的操作,给这种架构命名(请参见图 4-1 以了解这个想法的示例)。在下一节中,我们将更详细地介绍卷积操作,但简而言之,对我们来说,这意味着在图像上应用一小部分“窗口”权重(也称为滤波器),如稍后的图 4-2 所示。
图 4-1。在全连接层(左侧),每个单元都连接到前一层的所有单元。在卷积层(右侧),每个单元连接到前一层的一个局部区域中的固定数量的单元。此外,在卷积层中,所有单元共享这些连接的权重,如共享的线型所示。
有一些常常被引用为导致 CNN 方法的动机,来自不同的思想流派。第一个角度是所谓的模型背后的神经科学启发。第二个涉及对图像性质的洞察,第三个与学习理论有关。在我们深入了解实际机制之前,我们将简要介绍这些内容。
通常将神经网络总体描述为计算的生物学启发模型,特别是卷积神经网络。有时,有人声称这些模型“模仿大脑执行计算的方式”。尽管直接理解时会产生误导,但生物类比具有一定的兴趣。
诺贝尔奖获得者神经生理学家 Hubel 和 Wiesel 早在 1960 年代就发现,大脑中视觉处理的第一阶段包括将相同的局部滤波器(例如,边缘检测器)应用于视野的所有部分。神经科学界目前的理解是,随着视觉处理的进行,信息从输入的越来越广泛的部分集成,这是按层次进行的。
卷积神经网络遵循相同的模式。随着我们深入网络,每个卷积层查看图像的越来越大的部分。最常见的情况是,这将被全连接层跟随,这些全连接层在生物启发的类比中充当处理全局信息的更高级别的视觉处理层。
第二个角度,更加注重硬性事实工程方面,源于图像及其内容的性质。当在图像中寻找一个对象,比如一只猫的脸时,我们通常希望能够无论其在图像中的位置如何都能检测到它。这反映了自然图像的性质,即相同的内容可能在图像的不同位置找到。这种性质被称为不变性——这种类型的不变性也可以预期在(小)旋转、光照变化等方面存在。
因此,在构建一个对象识别系统时,应该对平移具有不变性(并且,根据情况,可能还对旋转和各种变形具有不变性,但这是另一回事)。简而言之,因此在图像的不同部分执行完全相同的计算是有意义的。从这个角度来看,卷积神经网络层在所有空间区域上计算图像的相同特征。
最后,卷积结构可以被看作是一种正则化机制。从这个角度来看,卷积层就像全连接层,但是我们不是在完整的矩阵空间中寻找权重,而是将搜索限制在描述固定大小卷积的矩阵中,将自由度的数量减少到卷积的大小,这通常非常小。
正则化
术语正则化在本书中被广泛使用。在机器学习和统计学中,正则化主要用于指的是通过对解的复杂性施加惩罚来限制优化问题,以防止对给定示例的过度拟合。
过拟合发生在规则(例如,分类器)以解释训练集的方式计算时,但对未见数据的泛化能力较差。
正则化通常通过添加关于期望结果的隐式信息来实现(这可能采取的形式是说在搜索函数空间时我们更希望有一个更平滑的函数)。在卷积神经网络的情况下,我们明确表示我们正在寻找相对低维子空间中的权重,这些权重对应于固定大小的卷积。
在本章中,我们涵盖了与卷积神经网络相关的层和操作类型。我们首先重新审视 MNIST 数据集,这次应用一个准确率约为 99%的模型。接下来,我们将转向更有趣的对象识别 CIFAR10 数据集。
MNIST:第二次
在本节中,我们再次查看 MNIST 数据集,这次将一个小型卷积神经网络应用作为我们的分类器。在这样做之前,有几个元素和操作我们必须熟悉。
卷积
卷积操作,正如你可能从架构的名称中期待的那样,是卷积神经网络中连接层的基本手段。我们使用内置的 TensorFlow conv2d()
:
tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
在这里,x
是数据——输入图像,或者是在网络中进一步应用之前的卷积层后获得的下游特征图。正如之前讨论的,在典型的 CNN 模型中,我们按层次堆叠卷积层,并且特征图只是一个常用术语,指的是每个这样的层的输出。查看这些层的输出的另一种方式是处理后的图像,是应用滤波器和其他操作的结果。在这里,这个滤波器由W
参数化,表示我们网络中学习的卷积滤波器的权重。这只是我们在图 4-2 中看到的小“滑动窗口”中的一组权重。
图 4-2。相同的卷积滤波器——一个“滑动窗口”——应用于图像之上。
这个操作的输出将取决于x
和W
的形状,在我们的情况下是四维的。图像数据x
的形状将是:
[None, 28, 28, 1]
这意味着我们有未知数量的图像,每个图像为 28×28 像素,具有一个颜色通道(因为这些是灰度图像)。我们使用的权重W
的形状将是:
[5, 5, 1, 32]
初始的 5×5×1 表示在图像中要进行卷积的小“窗口”的大小,在我们的情况下是一个 5×5 的区域。在具有多个颜色通道的图像中(RGB,如第一章中简要讨论的),我们将每个图像视为 RGB 值的三维张量,但在这个单通道数据中它们只是二维的,卷积滤波器应用于二维区域。稍后,当我们处理 CIFAR10 数据时,我们将看到多通道图像的示例以及如何相应地设置权重W
的大小。
最终的 32 是特征图的数量。换句话说,我们有卷积层的多组权重——在这种情况下有 32 组。回想一下,卷积层的概念是沿着图像计算相同的特征;我们希望计算许多这样的特征,因此使用多组卷积滤波器。
strides
参数控制滤波器W
在图像(或特征图)x
上的空间移动。
值[1, 1, 1, 1]
表示滤波器在每个维度上以一个像素间隔应用于输入,对应于“全”卷积。此参数的其他设置允许我们在应用滤波器时引入跳跃—这是我们稍后会应用的常见做法—从而使得生成的特征图更小。
最后,将padding
设置为'SAME'
意味着填充x
的边界,使得操作的结果大小与x
的大小相同。
激活函数
在线性层之后,无论是卷积还是全连接,常见的做法是应用非线性激活函数(参见图 4-3 中的一些示例)。激活函数的一个实际方面是,连续的线性操作可以被单个操作替代,因此深度不会为模型的表达能力做出贡献,除非我们在线性层之间使用非线性激活。
图 4-3。常见的激活函数:逻辑函数(左)、双曲正切函数(中)、修正线性单元(右)
池化
在卷积层后跟随输出的池化是常见的。技术上,池化意味着使用某种本地聚合函数减少数据的大小,通常在每个特征图内部。
这背后的原因既是技术性的,也是更理论性的。技术方面是,池化会减少下游处理的数据量。这可以极大地减少模型中的总参数数量,特别是在卷积层之后使用全连接层的情况下。
应用池化的更理论的原因是,我们希望我们计算的特征不受图像中位置的微小变化的影响。例如,一个在图像右上部寻找眼睛的特征,如果我们稍微向右移动相机拍摄图片,将眼睛略微移动到图像中心,这个特征不应该有太大变化。在空间上聚合“眼睛检测器特征”使模型能够克服图像之间的这种空间变化,捕捉本章开头讨论的某种不变性形式。
在我们的示例中,我们对每个特征图的 2×2 块应用最大池化操作:
tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
最大池化输出预定义大小的每个区域中的输入的最大值(这里是 2×2)。ksize
参数控制池化的大小(2×2),strides
参数控制我们在x
上“滑动”池化网格的幅度,就像在卷积层的情况下一样。将其设置为 2×2 网格意味着池化的输出将恰好是原始高度和宽度的一半,总共是原始大小的四分之一。
Dropout
我们模型中最后需要的元素是dropout。这是一种正则化技巧,用于强制网络将学习的表示分布到所有神经元中。在训练期间,dropout 会“关闭”一定比例的层中的单位,通过将它们的值设置为零。这些被丢弃的神经元是随机的,每次计算都不同,迫使网络学习一个即使在丢失后仍能正常工作的表示。这个过程通常被认为是训练多个网络的“集成”,从而增加泛化能力。在测试时使用网络作为分类器时(“推断”),不会进行 dropout,而是直接使用完整的网络。
在我们的示例中,除了我们希望应用 dropout 的层之外,唯一的参数是keep_prob
,即每一步保持工作的神经元的比例:
tf.nn.dropout(layer, keep_prob=keep_prob)
为了能够更改此值(我们必须这样做,因为对于测试,我们希望这个值为1.0
,表示根本没有丢失),我们将使用tf.placeholder
并传递一个值用于训练(.5
)和另一个用于测试(1.0
)。
模型
首先,我们定义了一些辅助函数,这些函数将在本章中广泛使用,用于创建我们的层。这样做可以使实际模型简短易读(在本书的后面,我们将看到存在几种框架,用于更抽象地定义深度学习构建块,这样我们可以专注于快速设计我们的网络,而不是定义所有必要的元素的繁琐工作)。我们的辅助函数有:
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def conv_layer(input, shape):
W = weight_variable(shape)
b = bias_variable([shape[3]])
return tf.nn.relu(conv2d(input, W) + b)
def full_layer(input, size):
in_size = int(input.get_shape()[1])
W = weight_variable([in_size, size])
b = bias_variable([size])
return tf.matmul(input, W) + b
让我们更仔细地看看这些:
weight_variable()
这指定了网络的全连接或卷积层的权重。它们使用截断正态分布进行随机初始化,标准差为 0.1。这种使用截断在尾部的随机正态分布初始化是相当常见的,通常会产生良好的结果(请参见即将介绍的随机初始化的注释)。
bias_variable()
这定义了全连接或卷积层中的偏置元素。它们都使用常数值.1
进行初始化。
conv2d()
这指定了我们通常会使用的卷积。一个完整的卷积(没有跳过),输出与输入大小相同。
max_pool_2×2
这将将最大池设置为高度/宽度维度的一半大小,并且总体上是特征图大小的四分之一。
conv_layer()
这是我们将使用的实际层。线性卷积如conv2d
中定义的,带有偏置,然后是 ReLU 非线性。
full_layer()
带有偏置的标准全连接层。请注意,这里我们没有添加 ReLU。这使我们可以在最终输出时使用相同的层,我们不需要非线性部分。
定义了这些层后,我们准备设置我们的模型(请参见图 4-4 中的可视化):
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])
conv1 = conv_layer(x_image, shape=[5, 5, 1, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 7*7*64])
full_1 = tf.nn.relu(full_layer(conv2_flat, 1024))
keep_prob = tf.placeholder(tf.float32)
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
图 4-4. 所使用的 CNN 架构的可视化。
随机初始化
在前一章中,我们讨论了几种类型的初始化器,包括此处用于卷积层权重的随机初始化器:
initial = tf.truncated_normal(shape, stddev=0.1)
关于深度学习模型训练中初始化的重要性已经说了很多。简而言之,糟糕的初始化可能会使训练过程“卡住”,或者由于数值问题完全失败。使用随机初始化而不是常数初始化有助于打破学习特征之间的对称性,使模型能够学习多样化和丰富的表示。使用边界值有助于控制梯度的幅度,使网络更有效地收敛,等等。
我们首先为图像和正确标签定义占位符x
和y_
。接下来,我们将图像数据重新整形为尺寸为 28×28×1 的 2D 图像格式。回想一下,我们在之前的 MNIST 模型中不需要数据的空间方面,因为所有像素都是独立处理的,但在卷积神经网络框架中,考虑图像时利用这种空间含义是一个重要的优势。
接下来我们有两个连续的卷积和池化层,每个层都有 5×5 的卷积和 32 个特征图,然后是一个具有 1,024 个单元的单个全连接层。在应用全连接层之前,我们将图像展平为单个向量形式,因为全连接层不再需要空间方面。
请注意,在两个卷积和池化层之后,图像的尺寸为 7×7×64。原始的 28×28 像素图像首先缩小到 14×14,然后在两个池化操作中缩小到 7×7。64 是我们在第二个卷积层中创建的特征图的数量。在考虑模型中学习参数的总数时,大部分将在全连接层中(从 7×7×64 到 1,024 的转换给我们提供了 3.2 百万个参数)。如果我们没有使用最大池化,这个数字将是原来的 16 倍(即 28×28×64×1,024,大约为 51 百万)。
最后,输出是一个具有 10 个单元的全连接层,对应数据集中的标签数量(回想一下 MNIST 是一个手写数字数据集,因此可能的标签数量是 10)。
其余部分与第二章中第一个 MNIST 模型中的内容相同,只有一些细微的变化:
train_accuracy
我们在每 100 步打印模型在用于训练的批次上的准确率。这是在训练步骤之前完成的,因此是对模型在训练集上当前性能的良好估计。
test_accuracy
我们将测试过程分为 10 个包含 1,000 张图像的块。对于更大的数据集,这样做非常重要。
以下是完整的代码:
mnist = input_data.read_data_sets(DATA_DIR, one_hot=True)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=
y_conv, labels=y_))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(STEPS):
batch = mnist.train.next_batch(50)
if i % 100 == 0:
train_accuracy = sess.run(accuracy, feed_dict={x: batch[0],
y_: batch[1],
keep_prob: 1.0})
print "step {}, training accuracy {}".format(i, train_accuracy)
sess.run(train_step, feed_dict={x: batch[0], y_: batch[1],
keep_prob: 0.5})
X = mnist.test.images.reshape(10, 1000, 784)
Y = mnist.test.labels.reshape(10, 1000, 10)
test_accuracy = np.mean([sess.run(accuracy,
feed_dict={x:X[i], y_:Y[i],keep_prob:1.0})
for i in range(10)])
print "test accuracy: {}".format(test_accuracy)
这个模型的性能已经相当不错,仅经过 5 个周期,准确率就超过了 99%,¹这相当于 5,000 步,每个步骤的迷你批次大小为 50。
有关多年来使用该数据集的模型列表以及如何进一步改进结果的一些想法,请查看http://yann.lecun.com/exdb/mnist/。
CIFAR10
CIFAR10是另一个在计算机视觉和机器学习领域有着悠久历史的数据集。与 MNIST 类似,它是一个常见的基准,各种方法都会被测试。CIFAR10是一个包含 60,000 张尺寸为 32×32 像素的彩色图像的数据集,每张图像属于以下十个类别之一:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。
针对这个数据集的最先进的深度学习方法在分类这些图像方面与人类一样出色。在本节中,我们首先使用相对较简单的方法,这些方法将运行相对较快。然后,我们简要讨论这些方法与最先进方法之间的差距。
加载 CIFAR10 数据集
在本节中,我们构建了一个类似于用于 MNIST 的内置input_data.read_data_sets()
的 CIFAR10 数据管理器。²
首先,下载数据集的 Python 版本并将文件提取到本地目录中。现在应该有以下文件:
-
data_batch_1, data_batch_2, data_batch_3, data_batch_4, data_batch_5
-
test_batch
-
batches_meta
-
readme.html
data_batch_X文件是包含训练数据的序列化数据文件,test_batch是一个类似的包含测试数据的序列化文件。batches_meta文件包含从数字到语义标签的映射。.html文件是 CIFAR-10 数据集网页的副本。
由于这是一个相对较小的数据集,我们将所有数据加载到内存中:
class CifarLoader(object):
def __init__(self, source_files):
self._source = source_files
self._i = 0
self.images = None
self.labels = None
def load(self):
data = [unpickle(f) for f in self._source]
images = np.vstack([d["data"] for d in data])
n = len(images)
self.images = images.reshape(n, 3, 32, 32).transpose(0, 2, 3, 1)\
.astype(float) / 255
self.labels = one_hot(np.hstack([d["labels"] for d in data]), 10)
return self
def next_batch(self, batch_size):
x, y = self.images[self._i:self._i+batch_size],
self.labels[self._i:self._i+batch_size]
self._i = (self._i + batch_size) % len(self.images)
return x, y
在这里我们使用以下实用函数:
DATA_PATH="*`/path/to/CIFAR10`*"defunpickle(file):withopen(os.path.join(DATA_PATH,file),'rb')asfo:dict=cPickle.load(fo)returndictdefone_hot(vec,vals=10):n=len(vec)out=np.zeros((n,vals))out[range(n),vec]=1returnout
unpickle()
函数返回一个带有data
和labels
字段的dict
,分别包含图像数据和标签。one_hot()
将标签从整数(范围为 0 到 9)重新编码为长度为 10 的向量,其中除了标签位置上的 1 之外,所有位置都是 0。
最后,我们创建一个包含训练和测试数据的数据管理器:
class CifarDataManager(object):
def __init__(self):
self.train = CifarLoader(["data_batch_{}".format(i)
for i in range(1, 6)])
.load()
self.test = CifarLoader(["test_batch"]).load()
使用 Matplotlib,我们现在可以使用数据管理器来显示一些 CIFAR10 图像,并更好地了解这个数据集中的内容:
def display_cifar(images, size):
n = len(images)
plt.figure()
plt.gca().set_axis_off()
im = np.vstack([np.hstack([images[np.random.choice(n)] for i in range(size)])
for i in range(size)])
plt.imshow(im)
plt.show()
d = CifarDataManager()
print "Number of train images: {}".format(len(d.train.images))
print "Number of train labels: {}".format(len(d.train.labels))
print "Number of test images: {}".format(len(d.test.images))
print "Number of test images: {}".format(len(d.test.labels))
images = d.train.images
display_cifar(images, 10)
Matplotlib
Matplotlib是一个用于绘图的有用的 Python 库,设计得看起来和行为类似于 MATLAB 绘图。这通常是快速绘制和可视化数据集的最简单方法。
display_cifar()
函数的参数是images
(包含图像的可迭代对象)和size
(我们想要显示的图像数量),并构建并显示一个size×size
的图像网格。这是通过垂直和水平连接实际图像来形成一个大图像。
在显示图像网格之前,我们首先打印训练/测试集的大小。CIFAR10 包含 50K 个训练图像和 10K 个测试图像:
Number of train images: 50000
Number of train labels: 50000
Number of test images: 10000
Number of test images: 10000
在图 4-5 中生成并显示的图像旨在让人了解 CIFAR10 图像实际上是什么样子的。值得注意的是,这些小的 32×32 像素图像每个都包含一个完整的单个对象,该对象位于中心位置,即使在这种分辨率下也基本上是可识别的。
图 4-5。100 个随机的 CIFAR10 图像。
简单的 CIFAR10 模型
我们将从先前成功用于 MNIST 数据集的模型开始。回想一下,MNIST 数据集由 28×28 像素的灰度图像组成,而 CIFAR10 图像是带有 32×32 像素的彩色图像。这将需要对计算图的设置进行轻微调整:
cifar = CifarDataManager()
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
conv1 = conv_layer(x, shape=[5, 5, 3, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 8 * 8 * 64])
full_1 = tf.nn.relu(full_layer(conv2_flat, 1024))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y_conv,
y_))
train_step = tf.train.AdamOptimizer(1e-3).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
def test(sess):
X = cifar.test.images.reshape(10, 1000, 32, 32, 3)
Y = cifar.test.labels.reshape(10, 1000, 10)
acc = np.mean([sess.run(accuracy, feed_dict={x: X[i], y_: Y[i],
keep_prob: 1.0})
for i in range(10)])
print "Accuracy: {:.4}%".format(acc * 100)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(STEPS):
batch = cifar.train.next_batch(BATCH_SIZE)
sess.run(train_step, feed_dict={x: batch[0], y_: batch[1],
keep_prob: 0.5})
test(sess)
这第一次尝试将在几分钟内达到大约 70%的准确度(使用批量大小为 100,自然取决于硬件和配置)。这好吗?截至目前,最先进的深度学习方法在这个数据集上实现了超过 95%的准确度,但是使用更大的模型并且通常需要许多小时的训练。
这与之前介绍的类似 MNIST 模型之间存在一些差异。首先,输入由大小为 32×32×3 的图像组成,第三维是三个颜色通道:
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
同样,在两次池化操作之后,我们这次剩下的是大小为 8×8 的 64 个特征图:
conv2_flat = tf.reshape(conv2_pool, [-1, 8 * 8 * 64])
最后,为了方便起见,我们将测试过程分组到一个名为test()
的单独函数中,并且我们不打印训练准确度值(可以使用与 MNIST 模型中相同代码添加回来)。
一旦我们有了一些可接受的基线准确度的模型(无论是从简单的 MNIST 模型还是从其他数据集的最先进模型中派生的),一个常见的做法是通过一系列的适应和更改来尝试改进它,直到达到我们的目的所需的内容。
在这种情况下,保持其他所有内容不变,我们将添加一个具有 128 个特征图和 dropout 的第三个卷积层。我们还将把完全连接层中的单元数从 1,024 减少到 512:
x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
conv1 = conv_layer(x, shape=[5, 5, 3, 32])
conv1_pool = max_pool_2x2(conv1)
conv2 = conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = max_pool_2x2(conv2)
conv3 = conv_layer(conv2_pool, shape=[5, 5, 64, 128])
conv3_pool = max_pool_2x2(conv3)
conv3_flat = tf.reshape(conv3_pool, [-1, 4 * 4 * 128])
conv3_drop = tf.nn.dropout(conv3_flat, keep_prob=keep_prob)
full_1 = tf.nn.relu(full_layer(conv3_drop, 512))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
这个模型将需要稍长一点的时间来运行(但即使没有复杂的硬件,也不会超过一个小时),并且可以达到大约 75%的准确度。
这仍然与最佳已知方法之间存在相当大的差距。有几个独立适用的元素可以帮助缩小这个差距:
模型大小
对于这种数据集和类似数据集,大多数成功的方法使用更深的网络和更多可调参数。
其他类型的层和方法
通常与这里介绍的层一起使用的是其他类型的流行层,比如局部响应归一化。
优化技巧
更多关于这个的内容以后再说!
领域知识
利用领域知识进行预处理通常是很有帮助的。在这种情况下,这将是传统的图像处理。
数据增强
基于现有数据集添加训练数据可能会有所帮助。例如,如果一张狗的图片水平翻转,那么显然仍然是一张狗的图片(但垂直翻转呢?)。小的位移和旋转也经常被使用。
重用成功的方法和架构
和大多数工程领域一样,从一个经过时间验证的方法开始,并根据自己的需求进行调整通常是正确的方式。在深度学习领域,这经常通过微调预训练模型来实现。
我们将在本章中介绍的最终模型是实际为这个数据集产生出色结果的模型类型的缩小版本。这个模型仍然紧凑快速,在大约 150 个 epochs 后达到约 83%的准确率:
C1, C2, C3 = 30, 50, 80
F1 = 500
conv1_1 = conv_layer(x, shape=[3, 3, 3, C1])
conv1_2 = conv_layer(conv1_1, shape=[3, 3, C1, C1])
conv1_3 = conv_layer(conv1_2, shape=[3, 3, C1, C1])
conv1_pool = max_pool_2x2(conv1_3)
conv1_drop = tf.nn.dropout(conv1_pool, keep_prob=keep_prob)
conv2_1 = conv_layer(conv1_drop, shape=[3, 3, C1, C2])
conv2_2 = conv_layer(conv2_1, shape=[3, 3, C2, C2])
conv2_3 = conv_layer(conv2_2, shape=[3, 3, C2, C2])
conv2_pool = max_pool_2x2(conv2_3)
conv2_drop = tf.nn.dropout(conv2_pool, keep_prob=keep_prob)
conv3_1 = conv_layer(conv2_drop, shape=[3, 3, C2, C3])
conv3_2 = conv_layer(conv3_1, shape=[3, 3, C3, C3])
conv3_3 = conv_layer(conv3_2, shape=[3, 3, C3, C3])
conv3_pool = tf.nn.max_pool(conv3_3, ksize=[1, 8, 8, 1], strides=[1, 8, 8, 1],
padding='SAME')
conv3_flat = tf.reshape(conv3_pool, [-1, C3])
conv3_drop = tf.nn.dropout(conv3_flat, keep_prob=keep_prob)
full1 = tf.nn.relu(full_layer(conv3_drop, F1))
full1_drop = tf.nn.dropout(full1, keep_prob=keep_prob)
y_conv = full_layer(full1_drop, 10)
这个模型由三个卷积层块组成,接着是我们之前已经见过几次的全连接和输出层。每个卷积层块包含三个连续的卷积层,然后是一个池化层和 dropout。
常数C1
、C2
和C3
控制每个卷积块中每个层的特征图数量,常数F1
控制全连接层中的单元数量。
在第三个卷积层之后,我们使用了一个 8×8 的最大池层:
conv3_pool = tf.nn.max_pool(conv3_3, ksize=[1, 8, 8, 1], strides=[1, 8, 8, 1],
padding='SAME')
由于在这一点上特征图的大小为 8×8(在前两个池化层之后,每个轴上都将 32×32 的图片减半),这样全局池化每个特征图并保留最大值。第三个块的特征图数量设置为 80,所以在这一点上(在最大池化之后),表示被减少到只有 80 个数字。这使得模型的整体大小保持较小,因为在过渡到全连接层时参数的数量保持在 80×500。
总结
在本章中,我们介绍了卷积神经网络及其通常由各种构建模块组成。一旦你能够正确运行小型模型,请尝试运行更大更深的模型,遵循相同的原则。虽然你可以随时查看最新的文献并了解哪些方法有效,但通过试错和自己摸索也能学到很多。在接下来的章节中,我们将看到如何处理文本和序列数据,以及如何使用 TensorFlow 抽象来轻松构建 CNN 模型。
¹ 在机器学习和特别是深度学习中,epoch指的是对所有训练数据的一次完整遍历;即,当学习模型已经看到每个训练示例一次时。
² 这主要是为了说明的目的。已经存在包含这种数据包装器的开源库,适用于许多流行的数据集。例如,查看 Keras 中的数据集模块(keras.datasets
),特别是keras.datasets.cifar10
。
³ 参见谁在 CIFAR-10 中表现最好?以获取方法列表和相关论文。
第五章:文本 I:处理文本和序列,以及 TensorBoard 可视化
在本章中,我们将展示如何在 TensorFlow 中处理序列,特别是文本。我们首先介绍循环神经网络(RNN),这是一类强大的深度学习算法,特别适用于自然语言处理(NLP)。我们展示如何从头开始实现 RNN 模型,介绍一些重要的 TensorFlow 功能,并使用交互式 TensorBoard 可视化模型。然后,我们探讨如何在监督文本分类问题中使用 RNN 进行词嵌入训练。最后,我们展示如何构建一个更高级的 RNN 模型,使用长短期记忆(LSTM)网络,并如何处理可变长度的序列。
序列数据的重要性
我们在前一章中看到,利用图像的空间结构可以导致具有出色结果的先进模型。正如在那一章中讨论的那样,利用结构是成功的关键。正如我们将很快看到的,一种极其重要和有用的结构类型是顺序结构。从数据科学的角度来看,这种基本结构出现在许多数据集中,跨越所有领域。在计算机视觉中,视频是随时间演变的一系列视觉内容。在语音中,我们有音频信号,在基因组学中有基因序列;在医疗保健中有纵向医疗记录,在股票市场中有金融数据,等等(见图 5-1)。
图 5-1。序列数据的普遍性。
一种特别重要的具有强烈顺序结构的数据类型是自然语言——文本数据。利用文本中固有的顺序结构(字符、单词、句子、段落、文档)的深度学习方法处于自然语言理解(NLU)系统的前沿,通常将传统方法远远甩在后面。有许多类型的 NLU 任务需要解决,从文档分类到构建强大的语言模型,从自动回答问题到生成人类级别的对话代理。这些任务非常困难,吸引了整个学术界和工业界 AI 社区的努力和关注。
在本章中,我们专注于基本构建模块和任务,并展示如何在 TensorFlow 中处理序列,主要是文本。我们深入研究了 TensorFlow 中序列模型的核心元素,从头开始实现其中一些,以获得深入的理解。在下一章中,我们将展示更高级的文本建模技术,使用 TensorFlow,而在第七章中,我们将使用提供更简单、高级实现方式的抽象库来实现我们的模型。
我们从最重要和流行的用于序列(特别是文本)的深度学习模型类开始:循环神经网络。
循环神经网络简介
循环神经网络是一类强大且广泛使用的神经网络架构,用于建模序列数据。RNN 模型背后的基本思想是序列中的每个新元素都会提供一些新信息,从而更新模型的当前状态。
在前一章中,我们探讨了使用 CNN 模型进行计算机视觉的内容,讨论了这些架构是如何受到当前科学对人类大脑处理视觉信息方式的启发。这些科学观念通常与我们日常生活中对顺序信息处理方式的常识直觉非常接近。
当我们接收新信息时,显然我们的“历史”和“记忆”并没有被抹去,而是“更新”。当我们阅读文本中的句子时,随着每个新单词,我们当前的信息状态会被更新,这不仅取决于新观察到的单词,还取决于前面的单词。
在统计学和概率论中的一个基本数学构造,通常被用作通过机器学习建模顺序模式的基本构件是马尔可夫链模型。比喻地说,我们可以将我们的数据序列视为“链”,链中的每个节点在某种程度上依赖于前一个节点,因此“历史”不会被抹去,而是被延续。
RNN 模型也基于这种链式结构的概念,并且在如何确切地维护和更新信息方面有所不同。正如它们的名称所示,循环神经网络应用某种形式的“循环”。如图 5-2 所示,在某个时间点t,网络观察到一个输入x[t](句子中的一个单词),并将其“状态向量”从上一个向量h[t-1]更新为h[t]。当我们处理新的输入(下一个单词)时,它将以某种依赖于h[t]的方式进行,因此依赖于序列的历史(我们之前看到的单词影响我们对当前单词的理解)。如图所示,这种循环结构可以简单地被视为一个长长的展开链,链中的每个节点执行相同类型的处理“步骤”,基于它从前一个节点的输出获得的“消息”。当然,这与先前讨论的马尔可夫链模型及其隐马尔可夫模型(HMM)扩展密切相关,这些内容在本书中没有讨论。
图 5-2。随时间更新的循环神经网络。
基础 RNN 实现
在本节中,我们从头开始实现一个基本的 RNN,探索其内部工作原理,并了解 TensorFlow 如何处理序列。我们介绍了一些强大的、相当低级的工具,TensorFlow 提供了这些工具用于处理序列数据,您可以使用这些工具来实现自己的系统。
在接下来的部分中,我们将展示如何使用更高级别的 TensorFlow RNN 模块。
我们从数学上定义我们的基本模型开始。这主要包括定义循环结构 - RNN 更新步骤。
我们简单的基础 vanilla RNN 的更新步骤是
h[t] = tanh(W[x]**x[t] + W[h]h[t-1] + b)
其中W[h],W[x]和b是我们学习的权重和偏置变量,tanh(·)是双曲正切函数,其范围在[-1,1]之间,并且与前几章中使用的 sigmoid 函数密切相关,x[t]和h[t]是之前定义的输入和状态向量。最后,隐藏状态向量乘以另一组权重,产生出现在图 5-2 中的输出。
MNIST 图像作为序列
为了初尝序列模型的强大和普适性,在本节中,我们实现我们的第一个 RNN 来解决您现在熟悉的 MNIST 图像分类任务。在本章的后面,我们将专注于文本序列,并看看神经序列模型如何强大地操纵它们并提取信息以解决 NLU 任务。
但是,你可能会问,图像与序列有什么关系?
正如我们在上一章中看到的,卷积神经网络的架构利用了图像的空间结构。虽然自然图像的结构非常适合 CNN 模型,但从不同角度查看图像的结构是有启发性的。在前沿深度学习研究的趋势中,先进模型尝试利用图像中各种顺序结构,试图以某种方式捕捉创造每个图像的“生成过程”。直观地说,这一切归结为图像中相邻区域在某种程度上相关,并试图对这种结构建模。
在这里,为了介绍基本的 RNN 以及如何处理序列,我们将图像简单地视为序列:我们将数据中的每个图像看作是一系列的行(或列)。在我们的 MNIST 数据中,这意味着每个 28×28 像素的图像可以被视为长度为 28 的序列,序列中的每个元素是一个包含 28 个像素的向量(参见图 5-3)。然后,RNN 中的时间依赖关系可以被想象成一个扫描头,从上到下(行)或从左到右(列)扫描图像。
图 5-3。图像作为像素列的序列。
我们首先加载数据,定义一些参数,并为我们的数据创建占位符:
import tensorflow as tf
# Import MNIST data
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
# Define some parameters
element_size = 28
time_steps = 28
num_classes = 10
batch_size = 128
hidden_layer_size = 128
# Where to save TensorBoard model summaries
LOG_DIR = "logs/RNN_with_summaries"
# Create placeholders for inputs, labels
_inputs = tf.placeholder(tf.float32,shape=[None, time_steps,
element_size],
name='inputs')
y = tf.placeholder(tf.float32, shape=[None, num_classes],
name='labels')
element_size
是我们序列中每个向量的维度,在我们的情况下是 28 个像素的行/列。time_steps
是序列中这样的元素的数量。
正如我们在之前的章节中看到的,当我们使用内置的 MNIST 数据加载器加载数据时,它以展开的形式呈现,即一个包含 784 个像素的向量。在训练期间加载数据批次时(我们稍后将在本节中介绍),我们只需将每个展开的向量重塑为[batch_size
, time_steps
, element_size
]:
batch_x, batch_y = mnist.train.next_batch(batch_size)
# Reshape data to get 28 sequences of 28 pixels
batch_x = batch_x.reshape((batch_size, time_steps, element_size))
我们将hidden_layer_size
设置为128
(任意值),控制之前讨论的隐藏 RNN 状态向量的大小。
LOG_DIR
是我们保存模型摘要以供 TensorBoard 可视化的目录。随着我们的学习,您将了解这意味着什么。
TensorBoard 可视化
在本章中,我们还将简要介绍 TensorBoard 可视化。TensorBoard 允许您监视和探索模型结构、权重和训练过程,并需要对代码进行一些非常简单的添加。更多细节将在本章和本书后续部分提供。
最后,我们创建了适当维度的输入和标签占位符。
RNN 步骤
让我们实现 RNN 步骤的数学模型。
我们首先创建一个用于记录摘要的函数,稍后我们将在 TensorBoard 中使用它来可视化我们的模型和训练过程(在这个阶段理解其技术细节并不重要):
# This helper function, taken from the official TensorFlow documentation,
# simply adds some ops that take care of logging summaries
def variable_summaries(var):
with tf.name_scope('summaries'):
mean = tf.reduce_mean(var)
tf.summary.scalar('mean', mean)
with tf.name_scope('stddev'):
stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
tf.summary.scalar('stddev', stddev)
tf.summary.scalar('max', tf.reduce_max(var))
tf.summary.scalar('min', tf.reduce_min(var))
tf.summary.histogram('histogram', var)
接下来,我们创建在 RNN 步骤中使用的权重和偏置变量:
# Weights and bias for input and hidden layer
with tf.name_scope('rnn_weights'):
with tf.name_scope("W_x"):
Wx = tf.Variable(tf.zeros([element_size, hidden_layer_size]))
variable_summaries(Wx)
with tf.name_scope("W_h"):
Wh = tf.Variable(tf.zeros([hidden_layer_size, hidden_layer_size]))
variable_summaries(Wh)
with tf.name_scope("Bias"):
b_rnn = tf.Variable(tf.zeros([hidden_layer_size]))
variable_summaries(b_rnn)
使用 tf.scan()应用 RNN 步骤
现在,我们创建一个函数,实现了我们在前一节中看到的基本 RNN 步骤,使用我们创建的变量。现在应该很容易理解这里使用的 TensorFlow 代码:
def rnn_step(previous_hidden_state,x):
current_hidden_state = tf.tanh(
tf.matmul(previous_hidden_state, Wh) +
tf.matmul(x, Wx) + b_rnn)
return current_hidden_state
接下来,我们将这个函数应用到所有的 28 个时间步上:
# Processing inputs to work with scan function
# Current input shape: (batch_size, time_steps, element_size)
processed_input = tf.transpose(_inputs, perm=[1, 0, 2])
# Current input shape now: (time_steps, batch_size, element_size)
initial_hidden = tf.zeros([batch_size,hidden_layer_size])
# Getting all state vectors across time
all_hidden_states = tf.scan(rnn_step,
processed_input,
initializer=initial_hidden,
name='states')
在这个小的代码块中,有一些重要的元素需要理解。首先,我们将输入从[batch_size, time_steps, element_size]
重塑为[time_steps, batch_size, element_size]
。tf.transpose()
的perm
参数告诉 TensorFlow 我们想要交换的轴。现在,我们的输入张量中的第一个轴代表时间轴,我们可以通过使用内置的tf.scan()
函数在所有时间步上进行迭代,该函数重复地将一个可调用(函数)应用于序列中的元素,如下面的说明所述。
tf.scan()
这个重要的函数被添加到 TensorFlow 中,允许我们在计算图中引入循环,而不仅仅是通过添加更多和更多的操作复制来“展开”循环。更技术上地说,它是一个类似于 reduce 操作符的高阶函数,但它返回随时间的所有中间累加器值。这种方法有几个优点,其中最重要的是能够具有动态数量的迭代而不是固定的,以及用于图构建的计算速度提升和优化。
为了演示这个函数的使用,考虑以下简单示例(这与本节中的整体 RNN 代码是分开的):
import numpy as np
import tensorflow as tf
elems = np.array(["T","e","n","s","o","r", " ", "F","l","o","w"])
scan_sum = tf.scan(lambda a, x: a + x, elems)
sess=tf.InteractiveSession()
sess.run(scan_sum)
让我们看看我们得到了什么:
array([b'T', b'Te', b'Ten', b'Tens', b'Tenso', b'Tensor', b'Tensor ',
b'Tensor F', b'Tensor Fl', b'Tensor Flo', b'Tensor Flow'],
dtype=object)
在这种情况下,我们使用tf.scan()
将字符顺序连接到一个字符串中,类似于算术累积和。
顺序输出
正如我们之前看到的,在 RNN 中,我们为每个时间步获得一个状态向量,将其乘以一些权重,然后获得一个输出向量——我们数据的新表示。让我们实现这个:
# Weights for output layers
with tf.name_scope('linear_layer_weights') as scope:
with tf.name_scope("W_linear"):
Wl = tf.Variable(tf.truncated_normal([hidden_layer_size,
num_classes],
mean=0,stddev=.01))
variable_summaries(Wl)
with tf.name_scope("Bias_linear"):
bl = tf.Variable(tf.truncated_normal([num_classes],
mean=0,stddev=.01))
variable_summaries(bl)
# Apply linear layer to state vector
def get_linear_layer(hidden_state):
return tf.matmul(hidden_state, Wl) + bl
with tf.name_scope('linear_layer_weights') as scope:
# Iterate across time, apply linear layer to all RNN outputs
all_outputs = tf.map_fn(get_linear_layer, all_hidden_states)
# Get last output
output = all_outputs[-1]
tf.summary.histogram('outputs', output)
我们的 RNN 的输入是顺序的,输出也是如此。在这个序列分类示例中,我们取最后一个状态向量,并通过一个全连接的线性层将其传递,以提取一个输出向量(稍后将通过 softmax 激活函数传递以生成预测)。这在基本序列分类中是常见的做法,我们假设最后一个状态向量已经“积累”了代表整个序列的信息。
为了实现这一点,我们首先定义线性层的权重和偏置项变量,并为该层创建一个工厂函数。然后我们使用tf.map_fn()
将此层应用于所有输出,这与典型的 map 函数几乎相同,该函数以元素方式将函数应用于序列/可迭代对象,本例中是在我们序列的每个元素上。
最后,我们提取批次中每个实例的最后输出,使用负索引(类似于普通 Python)。稍后我们将看到一些更多的方法来做这个,并深入研究输出和状态。
RNN 分类
现在我们准备训练一个分类器,方式与前几章相同。我们定义损失函数计算、优化和预测的操作,为 TensorBoard 添加一些更多摘要,并将所有这些摘要合并为一个操作:
with tf.name_scope('cross_entropy'):
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=y))
tf.summary.scalar('cross_entropy', cross_entropy)
with tf.name_scope('train'):
# Using RMSPropOptimizer
train_step = tf.train.RMSPropOptimizer(0.001, 0.9)\
.minimize(cross_entropy)
with tf.name_scope('accuracy'):
correct_prediction = tf.equal(
tf.argmax(y,1), tf.argmax(output,1))
accuracy = (tf.reduce_mean(
tf.cast(correct_prediction, tf.float32)))*100
tf.summary.scalar('accuracy', accuracy)
# Merge all the summaries
merged = tf.summary.merge_all()
到目前为止,您应该熟悉用于定义损失函数和优化的大多数组件。在这里,我们使用RMSPropOptimizer
,实现了一个众所周知且强大的梯度下降算法,带有一些标准的超参数。当然,我们可以使用任何其他优化器(并在本书中一直这样做!)。
我们创建一个包含未见过的 MNIST 图像的小测试集,并添加一些用于记录摘要的技术操作和命令,这些将在 TensorBoard 中使用。
让我们运行模型并查看结果:
# Get a small test set
test_data = mnist.test.images[:batch_size].reshape((-1, time_steps,
element_size))
test_label = mnist.test.labels[:batch_size]
with tf.Session() as sess:
# Write summaries to LOG_DIR -- used by TensorBoard
train_writer = tf.summary.FileWriter(LOG_DIR + '/train',
graph=tf.get_default_graph())
test_writer = tf.summary.FileWriter(LOG_DIR + '/test',
graph=tf.get_default_graph())
sess.run(tf.global_variables_initializer())
for i in range(10000):
batch_x, batch_y = mnist.train.next_batch(batch_size)
# Reshape data to get 28 sequences of 28 pixels
batch_x = batch_x.reshape((batch_size, time_steps,
element_size))
summary,_ = sess.run([merged,train_step],
feed_dict={_inputs:batch_x, y:batch_y})
# Add to summaries
train_writer.add_summary(summary, i)
if i % 1000 == 0:
acc,loss, = sess.run([accuracy,cross_entropy],
feed_dict={_inputs: batch_x,
y: batch_y})
print ("Iter " + str(i) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
if i % 10:
# Calculate accuracy for 128 MNIST test images and
# add to summaries
summary, acc = sess.run([merged, accuracy],
feed_dict={_inputs: test_data,
y: test_label})
test_writer.add_summary(summary, i)
test_acc = sess.run(accuracy, feed_dict={_inputs: test_data,
y: test_label})
print ("Test Accuracy:", test_acc)
最后,我们打印一些训练和测试准确率的结果:
Iter 0, Minibatch Loss= 2.303386, Training Accuracy= 7.03125
Iter 1000, Minibatch Loss= 1.238117, Training Accuracy= 52.34375
Iter 2000, Minibatch Loss= 0.614925, Training Accuracy= 85.15625
Iter 3000, Minibatch Loss= 0.439684, Training Accuracy= 82.81250
Iter 4000, Minibatch Loss= 0.077756, Training Accuracy= 98.43750
Iter 5000, Minibatch Loss= 0.220726, Training Accuracy= 89.84375
Iter 6000, Minibatch Loss= 0.015013, Training Accuracy= 100.00000
Iter 7000, Minibatch Loss= 0.017689, Training Accuracy= 100.00000
Iter 8000, Minibatch Loss= 0.065443, Training Accuracy= 99.21875
Iter 9000, Minibatch Loss= 0.071438, Training Accuracy= 98.43750
Testing Accuracy: 97.6563
总结这一部分,我们从原始的 MNIST 像素开始,并将它们视为顺序数据——每列(或行)的 28 个像素作为一个时间步。然后,我们应用了 vanilla RNN 来提取对应于每个时间步的输出,并使用最后的输出来执行整个序列(图像)的分类。
使用 TensorBoard 可视化模型
TensorBoard 是一个交互式基于浏览器的工具,允许我们可视化学习过程,以及探索我们训练的模型。
运行 TensorBoard,转到命令终端并告诉 TensorBoard 相关摘要的位置:
tensorboard--logdir=*`LOG_DIR`*
在这里,*LOG_DIR*
应替换为您的日志目录。如果您在 Windows 上并且这不起作用,请确保您从存储日志数据的相同驱动器运行终端,并按以下方式向日志目录添加名称,以绕过 TensorBoard 解析路径的错误:
tensorboard--logdir=rnn_demo:*`LOG_DIR`*
TensorBoard 允许我们为单独的日志目录分配名称,方法是在名称和路径之间放置一个冒号,当使用多个日志目录时可能会有用。在这种情况下,我们将传递一个逗号分隔的日志目录列表,如下所示:
tensorboard--logdir=rnn_demo1:*`LOG_DIR1`*,rnn_demo2:*`LOG_DIR2`*
在我们的示例中(有一个日志目录),一旦您运行了tensorboard
命令,您应该会得到类似以下内容的信息,告诉您在浏览器中导航到哪里:
Starting TensorBoard b'39' on port 6006
(You can navigate to http://10.100.102.4:6006)
如果地址无法使用,请转到localhost:6006,这个地址应该始终有效。
TensorBoard 递归地遍历以*LOG_DIR*
为根的目录树,寻找包含 tfevents 日志数据的子目录。如果您多次运行此示例,请确保在每次运行后要么删除您创建的*LOG_DIR*
文件夹,要么将日志写入*LOG_DIR*
内的单独子目录,例如*LOG_DIR*
/run1/train,*LOG_DIR*
/run2/train等,以避免覆盖日志文件,这可能会导致一些“奇怪”的图形。
让我们看一些我们可以获得的可视化效果。在下一节中,我们将探讨如何使用 TensorBoard 对高维数据进行交互式可视化-现在,我们专注于绘制训练过程摘要和训练权重。
首先,在浏览器中,转到标量选项卡。在这里,TensorBoard 向我们显示所有标量的摘要,包括通常最有趣的训练和测试准确性,以及我们记录的有关变量的一些摘要统计信息(请参见图 5-4)。将鼠标悬停在图表上,我们可以看到一些数字。
图 5-4。TensorBoard 标量摘要。
在图形选项卡中,我们可以通过放大来获得计算图的交互式可视化,从高级视图到基本操作(请参见图 5-5)。
图 5-5。放大计算图。
最后,在直方图选项卡中,我们可以看到在训练过程中权重的直方图(请参见图 5-6)。当然,我们必须明确将这些直方图添加到我们的日志记录中才能查看它们,使用tf.summary.histogram()
。
图 5-6。学习过程中权重的直方图。
TensorFlow 内置的 RNN 函数
前面的示例教会了我们一些使用序列的基本和强大的方法,通过几乎从头开始实现我们的图。在实践中,当然最好使用内置的高级模块和函数。这不仅使代码更短,更容易编写,而且利用了 TensorFlow 实现提供的许多低级优化。
在本节中,我们首先以完整的新代码的新版本呈现。由于大部分整体细节没有改变,我们将重点放在主要的新元素tf.contrib.rnn.BasicRNNCell
和tf.nn.dynamic_rnn()
上:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
element_size = 28;time_steps = 28;num_classes = 10
batch_size = 128;hidden_layer_size = 128
_inputs = tf.placeholder(tf.float32,shape=[None, time_steps,
element_size],
name='inputs')
y = tf.placeholder(tf.float32, shape=[None, num_classes],name='inputs')
# TensorFlow built-in functions
rnn_cell = tf.contrib.rnn.BasicRNNCell(hidden_layer_size)
outputs, _ = tf.nn.dynamic_rnn(rnn_cell, _inputs, dtype=tf.float32)
Wl = tf.Variable(tf.truncated_normal([hidden_layer_size, num_classes],
mean=0,stddev=.01))
bl = tf.Variable(tf.truncated_normal([num_classes],mean=0,stddev=.01))
def get_linear_layer(vector):
return tf.matmul(vector, Wl) + bl
last_rnn_output = outputs[:,-1,:]
final_output = get_linear_layer(last_rnn_output)
softmax = tf.nn.softmax_cross_entropy_with_logits(logits=final_output,
labels=y)
cross_entropy = tf.reduce_mean(softmax)
train_step = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(final_output,1))
accuracy = (tf.reduce_mean(tf.cast(correct_prediction, tf.float32)))*100
sess=tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
test_data = mnist.test.images[:batch_size].reshape((-1,
time_steps, element_size))
test_label = mnist.test.labels[:batch_size]
for i in range(3001):
batch_x, batch_y = mnist.train.next_batch(batch_size)
batch_x = batch_x.reshape((batch_size, time_steps, element_size))
sess.run(train_step,feed_dict={_inputs:batch_x,
y:batch_y})
if i % 1000 == 0:
acc = sess.run(accuracy, feed_dict={_inputs: batch_x,
y: batch_y})
loss = sess.run(cross_entropy,feed_dict={_inputs:batch_x,
y:batch_y})
print ("Iter " + str(i) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
print ("Testing Accuracy:",
sess.run(accuracy, feed_dict={_inputs: test_data, y: test_label}))
tf.contrib.rnn.BasicRNNCell 和 tf.nn.dynamic_rnn()
TensorFlow 的 RNN 单元是表示每个循环“单元”执行的基本操作(请参见本章开头的图 5-2 进行说明),以及其关联状态的抽象。它们通常是rnn_step()
函数及其所需的相关变量的“替代”。当然,有许多变体和类型的单元,每个单元都有许多方法和属性。我们将在本章末尾和本书后面看到一些更高级的单元。
一旦我们创建了rnn_cell
,我们将其输入到tf.nn.dynamic_rnn()
中。此函数替换了我们基本实现中的tf.scan()
,并创建了由rnn_cell
指定的 RNN。
截至本文撰写时,即 2017 年初,TensorFlow 包括用于创建 RNN 的静态和动态函数。这是什么意思?静态版本创建一个固定长度的展开图(如图 5-2)。动态版本使用tf.While
循环在执行时动态构建图,从而加快图的创建速度,这可能是显著的。这种动态构建在其他方面也非常有用,其中一些我们将在本章末尾讨论变长序列时提及。
请注意,contrib指的是这个库中的代码是由贡献者贡献的,并且仍需要测试。我们将在第七章中更详细地讨论contrib
库。BasicRNNCell
在 TensorFlow 1.0 中被移动到contrib
作为持续开发的一部分。在 1.2 版本中,许多 RNN 函数和类被移回核心命名空间,并在contrib
中保留别名以实现向后兼容性,这意味着在撰写本文时,前面的代码适用于所有 1.X 版本。
文本序列的 RNN
我们在本章开始时学习了如何在 TensorFlow 中实现 RNN 模型。为了便于说明,我们展示了如何在由 MNIST 图像中的像素组成的序列上实现和使用 RNN。接下来我们将展示如何在文本序列上使用这些序列模型。
文本数据具有与图像数据明显不同的一些属性,我们将在这里和本书的后面进行讨论。这些属性可能使得一开始处理文本数据有些困难,而文本数据总是需要至少一些基本的预处理步骤才能让我们能够处理它。为了介绍在 TensorFlow 中处理文本,我们将专注于核心组件并创建一个最小的、人为的文本数据集,这将让我们直接开始行动。在第七章中,我们将应用 RNN 模型进行电影评论情感分类。
让我们开始吧,展示我们的示例数据并在进行的过程中讨论文本数据集的一些关键属性。
文本序列
在之前看到的 MNIST RNN 示例中,每个序列的大小是固定的——图像的宽度(或高度)。序列中的每个元素都是一个由 28 个像素组成的密集向量。在 NLP 任务和数据集中,我们有一种不同类型的“图片”。
我们的序列可以是由单词组成的句子,由句子组成的段落,甚至由字符组成的单词或段落组成的整个文档。
考虑以下句子:“我们公司为农场提供智能农业解决方案,具有先进的人工智能、深度学习。”假设我们从在线新闻博客中获取这个句子,并希望将其作为我们机器学习系统的一部分进行处理。
这个句子中的每个单词都将用一个 ID 表示——一个整数,通常在 NLP 中被称为令牌 ID。因此,例如单词“agriculture”可以映射到整数 3452,单词“farm”到 12,“AI”到 150,“deep-learning”到 0。这种以整数标识符表示的表示形式与图像数据中的像素向量在多个方面都非常不同。我们将在讨论词嵌入时很快详细阐述这一重要观点,并在第六章中进行讨论。
为了使事情更具体,让我们从创建我们简化的文本数据开始。
我们的模拟数据由两类非常短的“句子”组成,一类由奇数组成,另一类由偶数组成(数字用英文书写)。我们生成由表示偶数和奇数的单词构建的句子。我们的目标是在监督文本分类任务中学习将每个句子分类为奇数或偶数。
当然,对于这个简单的任务,我们实际上并不需要任何机器学习——我们只是为了说明目的而使用这个人为的例子。
首先,我们定义一些常量,随着我们的进行将会解释:
import numpy as np
import tensorflow as tf
batch_size = 128;embedding_dimension = 64;num_classes = 2
hidden_layer_size = 32;times_steps = 6;element_size = 1
接下来,我们创建句子。我们随机抽取数字并将其映射到相应的“单词”(例如,1 映射到“One”,7 映射到“Seven”等)。
文本序列通常具有可变长度,这当然也适用于所有真实的自然语言数据(例如在本页上出现的句子)。
为了使我们模拟的句子具有不同的长度,我们为每个句子抽取一个介于 3 和 6 之间的随机长度,使用np.random.choice(range(3, 7))
——下限包括,上限不包括。
现在,为了将所有输入句子放入一个张量中(每个数据实例的批次),我们需要它们以某种方式具有相同的大小—因此,我们用零(或PAD符号)填充长度短于 6 的句子,使所有句子大小相等(人为地)。这个预处理步骤称为零填充。以下代码完成了所有这些:
digit_to_word_map = {1:"One",2:"Two", 3:"Three", 4:"Four", 5:"Five",
6:"Six",7:"Seven",8:"Eight",9:"Nine"}
digit_to_word_map[0]="PAD"
even_sentences = []
odd_sentences = []
seqlens = []
for i in range(10000):
rand_seq_len = np.random.choice(range(3,7))
seqlens.append(rand_seq_len)
rand_odd_ints = np.random.choice(range(1,10,2),
rand_seq_len)
rand_even_ints = np.random.choice(range(2,10,2),
rand_seq_len)
# Padding
if rand_seq_len<6:
rand_odd_ints = np.append(rand_odd_ints,
[0]*(6-rand_seq_len))
rand_even_ints = np.append(rand_even_ints,
[0]*(6-rand_seq_len))
even_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_odd_ints]))
odd_sentences.append(" ".join([digit_to_word_map[r] for
r in rand_even_ints]))
data = even_sentences+odd_sentences
# Same seq lengths for even, odd sentences
seqlens*=2
让我们看一下我们的句子,每个都填充到长度 6:
even_sentences[0:6]
Out:
['Four Four Two Four Two PAD',
'Eight Six Four PAD PAD PAD',
'Eight Two Six Two PAD PAD',
'Eight Four Four Eight PAD PAD',
'Eight Eight Four PAD PAD PAD',
'Two Two Eight Six Eight Four']
odd_sentences[0:6]
Out:
['One Seven Nine Three One PAD',
'Three Nine One PAD PAD PAD',
'Seven Five Three Three PAD PAD',
'Five Five Three One PAD PAD',
'Three Three Five PAD PAD PAD',
'Nine Three Nine Five Five Three']
请注意,我们向我们的数据和digit_to_word_map
字典中添加了PAD单词(标记),并分别存储偶数和奇数句子及其原始长度(填充之前)。
让我们看一下我们打印的句子的原始序列长度:
seqlens[0:6]
Out:
[5, 3, 4, 4, 3, 6]
为什么保留原始句子长度?通过零填充,我们解决了一个技术问题,但又创建了另一个问题:如果我们简单地将这些填充的句子通过我们的 RNN 模型,它将处理无用的PAD
符号。这将通过处理“噪音”损害模型的正确性,并增加计算时间。我们通过首先将原始长度存储在seqlens
数组中,然后告诉 TensorFlow 的tf.nn.dynamic_rnn()
每个句子的结束位置来解决这个问题。
在本章中,我们的数据是模拟的——由我们生成。在实际应用中,我们将首先获得一系列文档(例如,一句话的推文),然后将每个单词映射到一个整数 ID。
因此,我们现在将单词映射到索引—单词标识符—通过简单地创建一个以单词为键、索引为值的字典。我们还创建了反向映射。请注意,单词 ID 和每个单词代表的数字之间没有对应关系—ID 没有语义含义,就像在任何具有真实数据的 NLP 应用中一样:
# Map from words to indices
word2index_map ={}
index=0
for sent in data:
for word in sent.lower().split():
if word not in word2index_map:
word2index_map[word] = index
index+=1
# Inverse map
index2word_map = {index: word for word, index in word2index_map.items()}
vocabulary_size = len(index2word_map)
这是一个监督分类任务—我们需要一个以 one-hot 格式的标签数组,训练和测试集,一个生成实例批次的函数和占位符,和通常一样。
首先,我们创建标签并将数据分为训练集和测试集:
labels = [1]*10000 + [0]*10000
for i in range(len(labels)):
label = labels[i]
one_hot_encoding = [0]*2
one_hot_encoding[label] = 1
labels[i] = one_hot_encoding
data_indices = list(range(len(data)))
np.random.shuffle(data_indices)
data = np.array(data)[data_indices]
labels = np.array(labels)[data_indices]
seqlens = np.array(seqlens)[data_indices]
train_x = data[:10000]
train_y = labels[:10000]
train_seqlens = seqlens[:10000]
test_x = data[10000:]
test_y = labels[10000:]
test_seqlens = seqlens[10000:]
接下来,我们创建一个生成句子批次的函数。每个批次中的句子只是一个对应于单词的整数 ID 列表:
def get_sentence_batch(batch_size,data_x,
data_y,data_seqlens):
instance_indices = list(range(len(data_x)))
np.random.shuffle(instance_indices)
batch = instance_indices[:batch_size]
x = [[word2index_map[word] for word in data_x[i].lower().split()]
for i in batch]
y = [data_y[i] for i in batch]
seqlens = [data_seqlens[i] for i in batch]
return x,y,seqlens
最后,我们为数据创建占位符:
_inputs = tf.placeholder(tf.int32, shape=[batch_size,times_steps])
_labels = tf.placeholder(tf.float32, shape=[batch_size, num_classes])
# seqlens for dynamic calculation
_seqlens = tf.placeholder(tf.int32, shape=[batch_size])
请注意,我们已经为原始序列长度创建了占位符。我们将很快看到如何在我们的 RNN 中使用这些。
监督词嵌入
我们的文本数据现在被编码为单词 ID 列表—每个句子是一个对应于单词的整数序列。这种原子表示,其中每个单词用一个 ID 表示,对于训练具有大词汇量的深度学习模型来说是不可扩展的,这在实际问题中经常出现。我们可能会得到数百万这样的单词 ID,每个以 one-hot(二进制)分类形式编码,导致数据稀疏和计算问题。我们将在第六章中更深入地讨论这个问题。
解决这个问题的一个强大方法是使用词嵌入。嵌入本质上只是将编码单词的高维度 one-hot 向量映射到较低维度稠密向量。因此,例如,如果我们的词汇量大小为 100,000,那么每个单词在 one-hot 表示中的大小将相同。相应的单词向量或词嵌入大小为 300。因此,高维度的 one-hot 向量被“嵌入”到具有更低维度的连续向量空间中。
在第六章中,我们深入探讨了词嵌入,探索了一种流行的无监督训练方法,即 word2vec。
在这里,我们的最终目标是解决文本分类问题,并且我们将在监督框架中训练词向量,调整嵌入的词向量以解决下游分类任务。
将单词嵌入视为基本的哈希表或查找表是有帮助的,将单词映射到它们的密集向量值。这些向量是作为训练过程的一部分进行优化的。以前,我们给每个单词一个整数索引,然后句子表示为这些索引的序列。现在,为了获得一个单词的向量,我们使用内置的tf.nn.embedding_lookup()
函数,它有效地检索给定单词索引序列中每个单词的向量:
with tf.name_scope("embeddings"):
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size,
embedding_dimension],
-1.0, 1.0),name='embedding')
embed = tf.nn.embedding_lookup(embeddings, _inputs)
我们很快将看到单词向量表示的示例和可视化。
LSTM 和使用序列长度
在我们开始的介绍性 RNN 示例中,我们实现并使用了基本的 vanilla RNN 模型。在实践中,我们经常使用略微更高级的 RNN 模型,其主要区别在于它们如何更新其隐藏状态并通过时间传播信息。一个非常流行的循环网络是长短期记忆(LSTM)网络。它通过具有一些特殊的记忆机制与 vanilla RNN 不同,这些机制使得循环单元能够更好地存储信息长时间,从而使它们能够比普通 RNN 更好地捕获长期依赖关系。
这些记忆机制并没有什么神秘之处;它们只是由一些添加到每个循环单元的更多参数组成,使得 RNN 能够克服优化问题并传播信息。这些可训练参数充当过滤器,选择哪些信息值得“记住”和传递,哪些值得“遗忘”。它们的训练方式与网络中的任何其他参数完全相同,使用梯度下降算法和反向传播。我们在这里不深入讨论更多技术数学公式,但有很多很好的资源深入探讨细节。
我们使用tf.contrib.rnn.BasicLSTMCell()
创建一个 LSTM 单元,并将其提供给tf.nn.dynamic_rnn()
,就像我们在本章开始时所做的那样。我们还使用我们之前创建的_seqlens
占位符给dynamic_rnn()
提供每个示例批次中每个序列的长度。TensorFlow 使用这个长度来停止超出最后一个真实序列元素的所有 RNN 步骤。它还返回所有随时间的输出向量(在outputs
张量中),这些向量在真实序列的真实结尾之后都是零填充的。因此,例如,如果我们原始序列的长度为 5,并且我们将其零填充为长度为 15 的序列,则超过 5 的所有时间步的输出将为零:
with tf.variable_scope("lstm"):
lstm_cell = tf.contrib.rnn.BasicLSTMCell(hidden_layer_size,
forget_bias=1.0)
outputs, states = tf.nn.dynamic_rnn(lstm_cell, embed,
sequence_length = _seqlens,
dtype=tf.float32)
weights = {
'linear_layer': tf.Variable(tf.truncated_normal([hidden_layer_size,
num_classes],
mean=0,stddev=.01))
}
biases = {
'linear_layer':tf.Variable(tf.truncated_normal([num_classes],
mean=0,stddev=.01))
}
# Extract the last relevant output and use in a linear layer
final_output = tf.matmul(states[1],
weights["linear_layer"]) + biases["linear_layer"]
softmax = tf.nn.softmax_cross_entropy_with_logits(logits = final_output,
labels = _labels)
cross_entropy = tf.reduce_mean(softmax)
我们取最后一个有效的输出向量——在这种情况下,方便地在dynamic_rnn()
返回的states
张量中可用,并通过一个线性层(和 softmax 函数)传递它,将其用作我们的最终预测。在下一节中,当我们查看dynamic_rnn()
为我们的示例句子生成的一些输出时,我们将进一步探讨最后相关输出和零填充的概念。
训练嵌入和 LSTM 分类器
我们已经有了拼图的所有部分。让我们把它们放在一起,完成端到端的单词向量和分类模型的训练:
train_step = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(_labels,1),
tf.argmax(final_output,1))
accuracy = (tf.reduce_mean(tf.cast(correct_prediction,
tf.float32)))*100
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(1000):
x_batch, y_batch,seqlen_batch = get_sentence_batch(batch_size,
train_x,train_y,
train_seqlens)
sess.run(train_step,feed_dict={_inputs:x_batch, _labels:y_batch,
_seqlens:seqlen_batch})
if step % 100 == 0:
acc = sess.run(accuracy,feed_dict={_inputs:x_batch,
_labels:y_batch,
_seqlens:seqlen_batch})
print("Accuracy at %d: %.5f" % (step, acc))
for test_batch in range(5):
x_test, y_test,seqlen_test = get_sentence_batch(batch_size,
test_x,test_y,
test_seqlens)
batch_pred,batch_acc = sess.run([tf.argmax(final_output,1),
accuracy],
feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
print("Test batch accuracy %d: %.5f" % (test_batch, batch_acc))
output_example = sess.run([outputs],feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
states_example = sess.run([states[1]],feed_dict={_inputs:x_test,
_labels:y_test,
_seqlens:seqlen_test})
正如我们所看到的,这是一个非常简单的玩具文本分类问题:
Accuracy at 0: 32.81250
Accuracy at 100: 100.00000
Accuracy at 200: 100.00000
Accuracy at 300: 100.00000
Accuracy at 400: 100.00000
Accuracy at 500: 100.00000
Accuracy at 600: 100.00000
Accuracy at 700: 100.00000
Accuracy at 800: 100.00000
Accuracy at 900: 100.00000
Test batch accuracy 0: 100.00000
Test batch accuracy 1: 100.00000
Test batch accuracy 2: 100.00000
Test batch accuracy 3: 100.00000
Test batch accuracy 4: 100.00000
我们还计算了由dynamic_rnn()
生成的一个示例批次的输出,以进一步说明在前一节中讨论的零填充和最后相关输出的概念。
让我们看一个这些输出的例子,对于一个被零填充的句子(在您的随机数据批次中,您可能会看到不同的输出,当然要寻找一个seqlen
小于最大 6 的句子):
seqlen_test[1]
Out:
4
output_example[0][1].shape
Out:
(6, 32)
这个输出有如预期的六个时间步,每个大小为 32 的向量。让我们看一眼它的值(只打印前几个维度以避免混乱):
output_example[0][1][:6,0:3]
Out:
array([[-0.44493711, -0.51363373, -0.49310589],
[-0.72036862, -0.68590945, -0.73340571],
[-0.83176643, -0.78206956, -0.87831545],
[-0.87982416, -0.82784462, -0.91132098],
[ 0. , 0. , 0. ],
[ 0. , 0. , 0. ]], dtype=float32)
我们看到,对于这个句子,原始长度为 4,最后两个时间步由于填充而具有零向量。
最后,我们看一下dynamic_rnn()
返回的状态向量:
states_example[0][1][0:3]
Out:
array([-0.87982416, -0.82784462, -0.91132098], dtype=float32)
我们可以看到它方便地为我们存储了最后一个相关输出向量——其值与零填充之前的最后一个相关输出向量匹配。
此时,您可能想知道如何访问和操作单词向量,并探索训练后的表示。我们将展示如何做到这一点,包括交互式嵌入可视化,在下一章中。
堆叠多个 LSTMs
之前,我们专注于一个单层 LSTM 网络以便更容易解释。添加更多层很简单,使用MultiRNNCell()
包装器将多个 RNN 单元组合成一个多层单元。
举个例子,假设我们想在前面的例子中堆叠两个 LSTM 层。我们可以这样做:
num_LSTM_layers = 2
with tf.variable_scope("lstm"):
lstm_cell_list =
[tf.contrib.rnn.BasicLSTMCell(hidden_layer_size,forget_bias=1.0)
for ii in range(num_LSTM_layers)]
cell = tf.contrib.rnn.MultiRNNCell(cells=lstm_cell_list,
state_is_tuple=True)
outputs, states = tf.nn.dynamic_rnn(cell, embed,
sequence_length = _seqlens,
dtype=tf.float32)
我们首先像以前一样定义一个 LSTM 单元,然后将其馈送到tf.contrib.rnn.MultiRNNCell()
包装器中。
现在我们的网络有两层 LSTM,当尝试提取最终状态向量时会出现一些形状问题。为了获得第二层的最终状态,我们只需稍微调整我们的索引:
# Extract the final state and use in a linear layer
final_output = tf.matmul(states[num_LSTM_layers-1][1],
weights["linear_layer"]) + biases["linear_layer"]
总结
在这一章中,我们介绍了在 TensorFlow 中的序列模型。我们看到如何通过使用tf.scan()
和内置模块来实现基本的 RNN 模型,以及更高级的 LSTM 网络,用于文本和图像数据。最后,我们训练了一个端到端的文本分类 RNN 模型,使用了词嵌入,并展示了如何处理可变长度的序列。在下一章中,我们将深入探讨词嵌入和 word2vec。在第七章中,我们将看到一些很酷的 TensorFlow 抽象层,以及它们如何用于训练高级文本分类 RNN 模型,而且付出的努力要少得多。