构造阶段

开始吧。 首先我们需要导入tensorflow库。 然后我们必须指定输入和输出的数量,并设置每个层中隐藏的神经元数量:

  1. import tensorflow as tf
  2. n_inputs = 28*28 # MNIST
  3. n_hidden1 = 300
  4. n_hidden2 = 100
  5. n_outputs = 10

接下来,与第 9 章一样,您可以使用占位符节点来表示训练数据和目标。X的形状仅有部分被定义。 我们知道它将是一个 2D 张量(即一个矩阵),沿着第一个维度的实例和第二个维度的特征,我们知道特征的数量将是28×28(每像素一个特征) 但是我们不知道每个训练批次将包含多少个实例。 所以X的形状是(None, n_inputs)。 同样,我们知道y将是一个 1D 张量,每个实例有一个入口,但是我们再次不知道在这一点上训练批次的大小,所以形状(None)

  1. X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
  2. y = tf.placeholder(tf.int64, shape=(None), name="y")

现在让我们创建一个实际的神经网络。 占位符X将作为输入层; 在执行阶段,它将一次更换一个训练批次(注意训练批中的所有实例将由神经网络同时处理)。 现在您需要创建两个隐藏层和输出层。 两个隐藏的层几乎相同:它们只是它们所连接的输入和它们包含的神经元的数量不同。 输出层也非常相似,但它使用 softmax 激活函数而不是 ReLU 激活函数。 所以让我们创建一个neuron_layer()函数,我们将一次创建一个图层。 它将需要参数来指定输入,神经元数量,激活函数和图层的名称:

  1. def neuron_layer(X, n_neurons, name, activation=None):
  2. with tf.name_scope(name):
  3. n_inputs = int(X.get_shape()[1])
  4. stddev = 2 / np.sqrt(n_inputs)
  5. init = tf.truncated_normal((n_inputs, n_neurons), stddev=stddev)
  6. W = tf.Variable(init, name="weights")
  7. b = tf.Variable(tf.zeros([n_neurons]), name="biases")
  8. z = tf.matmul(X, W) + b
  9. if activation == "relu":
  10. return tf.nn.relu(z)
  11. else:
  12. return z

我们逐行浏览这个代码:

  1. 首先,我们使用名称范围来创建每层的名称:它将包含该神经元层的所有计算节点。 这是可选的,但如果节点组织良好,则 TensorBoard 图形将会更加出色。

  2. 接下来,我们通过查找输入矩阵的形状并获得第二个维度的大小来获得输入数量(第一个维度用于实例)。

  3. 接下来的三行创建一个保存权重矩阵的W变量。 它将是包含每个输入和每个神经元之间的所有连接权重的2D张量;因此,它的形状将是(n_inputs, n_neurons)。它将被随机初始化,使用具有标准差为2/√n的截断的正态(高斯)分布(使用截断的正态分布而不是常规正态分布确保不会有任何大的权重,这可能会减慢训练。).使用这个特定的标准差有助于算法的收敛速度更快(我们将在第11章中进一步讨论这一点),这是对神经网络的微小调整之一,对它们的效率产生了巨大的影响)。 重要的是为所有隐藏层随机初始化连接权重,以避免梯度下降算法无法中断的任何对称性。(例如,如果将所有权重设置为 0,则所有神经元将输出 0,并且给定隐藏层中的所有神经元的误差梯度将相同。 然后,梯度下降步骤将在每个层中以相同的方式更新所有权重,因此它们将保持相等。 换句话说,尽管每层有数百个神经元,你的模型就像每层只有一个神经元一样。)

  4. 下一行创建一个偏差的b变量,初始化为 0(在这种情况下无对称问题),每个神经元有一个偏置参数。

  5. 然后我们创建一个子图来计算z = X·W + b。 该向量化实现将有效地计算输入的加权和加上层中每个神经元的偏置,对于批次中的所有实例,仅需一次.

  6. 最后,如果激活参数设置为relu,则代码返回relu(z)(即max(0,z)),否则它只返回z

好了,现在你有一个很好的函数来创建一个神经元层。 让我们用它来创建深层神经网络! 第一个隐藏层以X为输入。 第二个将第一个隐藏层的输出作为其输入。 最后,输出层将第二个隐藏层的输出作为其输入。

  1. with tf.name_scope("dnn"):
  2. hidden1 = neuron_layer(X, n_hidden1, "hidden1", activation="relu")
  3. hidden2 = neuron_layer(hidden1, n_hidden2, "hidden2", activation="relu")
  4. logits = neuron_layer(hidden2, n_outputs, "outputs")

请注意,为了清楚起见,我们再次使用名称范围。 还要注意,logit 是在通过 softmax 激活函数之前神经网络的输出:为了优化,我们稍后将处理 softmax 计算。

正如你所期望的,TensorFlow 有许多方便的功能来创建标准的神经网络层,所以通常不需要像我们刚才那样定义你自己的neuron_layer()函数。 例如,TensorFlow 的fully_connected()函数创建一个完全连接的层,其中所有输入都连接到图层中的所有神经元。 它使用正确的初始化策略来负责创建权重和偏置变量,并且默认情况下使用 ReLU 激活函数(我们可以使用activate_fn参数来更改它)。 正如我们将在第 11 章中看到的,它还支持正则化和归一化参数。 我们来调整上面的代码来使用fully_connected()函数,而不是我们的neuron_layer()函数。 只需导入该功能,并使用以下代码替换 dnn 构建部分:

  1. from tensorflow.contrib.layers import fully_connected
  2. with tf.name_scope("dnn"):
  3. hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
  4. hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")
  5. logits = fully_connected(hidden2, n_outputs, scope="outputs",
  6. activation_fn=None)

tensorflow.contrib包包含许多有用的功能,但它是一个尚未分级成为主要 TensorFlow API 一部分的实验代码的地方。 因此,full_connected()函数(和任何其他contrib代码)可能会在将来更改或移动。

使用dense()代替neuron_layer()

注意:本书使用tensorflow.contrib.layers.fully_connected()而不是tf.layers.dense()(本章编写时不存在)。

现在最好使用tf.layers.dense(),因为contrib模块中的任何内容可能会更改或删除,恕不另行通知。dense()函数与fully_connected()函数几乎相同,除了一些细微的差别:

几个参数被重命名:scope变为名称,activation_fn变为激活(同样_fn后缀从其他参数(如normalizer_fn)中删除),weights_initializer成为kernel_initializer等。默认激活现在是无,而不是tf.nn.relu。 第 11 章还介绍了更多的差异。

  1. with tf.name_scope("dnn"):
  2. hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1",
  3. activation=tf.nn.relu)
  4. hidden2 = tf.layers.dense(hidden1, n_hidden2, name="hidden2",
  5. activation=tf.nn.relu)
  6. logits = tf.layers.dense(hidden2, n_outputs, name="outputs")

现在我们已经有了神经网络模型,我们需要定义我们用来训练的损失函数。 正如我们在第 4 章中对 Softmax 回归所做的那样,我们将使用交叉熵。 正如我们之前讨论的,交叉熵将惩罚估计目标类的概率较低的模型。 TensorFlow 提供了几种计算交叉熵的功能。 我们将使用sparse_softmax_cross_entropy_with_logits():它根据“logit”计算交叉熵(即,在通过 softmax 激活函数之前的网络输出),并且期望以 0 到 -1 数量的整数形式的标签(在我们的例子中,从 0 到 9)。 这将给我们一个包含每个实例的交叉熵的 1D 张量。 然后,我们可以使用 TensorFlow 的reduce_mean()函数来计算所有实例的平均交叉熵。

  1. with tf.name_scope("loss"):
  2. xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)
  3. loss = tf.reduce_mean(xentropy, name="loss")

sparse_softmax_cross_entropy_with_logits()函数等同于应用 SOFTMAX 激活函数,然后计算交叉熵,但它更高效,它妥善照顾的边界情况下,比如 logits 等于 0,这就是为什么我们没有较早的应用 SOFTMAX 激活函数。 还有称为softmax_cross_entropy_with_logits()的另一个函数,该函数在标签单热载体的形式(而不是整数 0 至类的数目减 1)。

我们有神经网络模型,我们有损失函数,现在我们需要定义一个GradientDescentOptimizer来调整模型参数以最小化损失函数。没什么新鲜的; 就像我们在第 9 章中所做的那样:

  1. learning_rate = 0.01
  2. with tf.name_scope("train"):
  3. optimizer = tf.train.GradientDescentOptimizer(learning_rate)
  4. training_op = optimizer.minimize(loss)

建模阶段的最后一个重要步骤是指定如何评估模型。 我们将简单地将精度用作我们的绩效指标。 首先,对于每个实例,通过检查最高 logit 是否对应于目标类别来确定神经网络的预测是否正确。 为此,您可以使用in_top_k()函数。 这返回一个充满布尔值的 1D 张量,因此我们需要将这些布尔值转换为浮点数,然后计算平均值。 这将给我们网络的整体准确性.

  1. with tf.name_scope("eval"):
  2. correct = tf.nn.in_top_k(logits, y, 1)
  3. accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

而且,像往常一样,我们需要创建一个初始化所有变量的节点,我们还将创建一个Saver来将我们训练有素的模型参数保存到磁盘中:

  1. init = tf.global_variables_initializer()
  2. saver = tf.train.Saver()

建模阶段结束。 这是不到 40 行代码,但相当激烈:我们为输入和目标创建占位符,我们创建了一个构建神经元层的函数,我们用它来创建 DNN,我们定义了损失函数,我们 创建了一个优化器,最后定义了性能指标。 现在到执行阶段。