## 5. TensorFlow 中的多层神经网络

在本章中，我将与读者一起编写一个简单的深度学习神经网络，该网络使用与前一章相同的 MNIST 数字识别问题。

随着我的前进，深度学习神经网络由叠在一起的多个层组成。 具体来说，在本章中我们将构建一个卷积网络，这是深度学习的典型例子。 卷扬神经网络由 Yann LeCunn 等人于 1998 年推出并推广。 这些卷积网络最近引领了图像识别领域的最新技术；例如：在我们的数字识别案例中，它们的准确度高于 99%。

在本章的其余部分，我将以示例代码为主，我将解释这些网络的两个最重要的概念：卷积和池化，而不输入参数的细节，鉴于本书的介绍性质。 但是，读者将能够运行所有代码，我希望它能让你了解卷积网络背后的通用思想。

### 卷积神经网络

卷积神经网络（也称为 CNN 或 CovNets）是深度学习的一个特例，并且在计算机视觉领域产生了重大影响。

CNN 的典型特征是它们几乎总是将图像作为输入，这产生了更有效的实现并且减少所需参数的数量。 让我们看看我们的 MNIST 数字识别示例：在读取 MNIST 数据并使用 TensorFlow 定义占位符之后，就像我们在上一个示例中所做的那样：

```py
import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

import tensorflow as tf

x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
```

我们可以重建输入数据图像的原始形状。 我们可以这样做：

```py
x_image = tf.reshape(x, [-1,28,28,1])
```

这里我们将输入形状更改为 4D 张量，第二维和第三维对应于图像的宽度和高度，而最后一维对应于颜色通道的数量，在这种情况下为 1。

通过这种方式，我们可以将神经网络的输入视为大小为`28×28`的二维空间，如图所示：

![](https://jorditorres.org/wp-content/uploads/2016/02/image072-300x282.png)

定义卷积神经网络有两个基本原则：滤波器和特征映射。 这些原则可以表示为特定的神经元分组，我们将很快看到。 但首先，鉴于它们在 CNN 中的重要性，我们将简要介绍这两个原则。

直觉上，我们可以说卷积层的主要目的是检测图像中的特征或视觉特征，考虑边缘，线条，颜色斑点等。 这是由我们刚刚讨论过的，连接输入层的隐藏层来处理的。 在我们感兴趣的 CNN 的案例中，输入数据没有完全连接到第一个隐藏层的神经元；这只发生在输入神经元中的一个小型局部空间中，输入神经元存储图像像素值。 这可以看作：

![](https://jorditorres.org/wp-content/uploads/2016/02/image074.png)

更确切地说，在给定的示例中，隐藏层的每个神经元与输入层的`5×5`小区域（因此是 25 个神经元）连接。

我们可以认为这是一个大小为`5×5`的窗口，它滑过包含输入图像的整个`28×28`大小的输入层。 窗口滑过整个神经元层。 对于窗口的每个位置，隐藏层中都有一个处理该信息的神经元。

我们可以通过假设窗口从图像的左上角开始来可视化；这将信息提供给隐藏层的第一个神经元。  然后窗口向右滑动一个像素；我们将这个`5×5`区域与隐藏层中的第二个神经元连接起来。 我们继续这样，直到整个空间从上到下，从左到右被窗口覆盖。

![](https://jorditorres.org/wp-content/uploads/2016/02/image076.png)

分析我们提出的具体案例，我们观察到，给定一个大小为`28×28`的输入图像和一个大小为`5×5`的窗口，在第一个隐藏层中产生了`24×24`的神经元，因为我们只能这样做，在触及输入图像的右下边缘之前，将窗口向下移动 23 次，向右移动 23 次。 这假设窗口每次只移动 1 个像素，因此新窗口与刚刚前进的旧窗口重叠。

但是，可以在卷积层中一次移动多于 1 个像素，该参数称为`stride`（步长）。 另一个扩展是用零（或其他值）填充边缘，以便窗口可以在图像的边缘上滑动，这可以产生更好的结果。 控制此功能的参数称为`padding`（填充）[39]，你可以使用该参数确定填充的大小。 鉴于本书的介绍性质，我们不会进一步详细介绍这两个参数。

鉴于我们的研究案例，并遵循前一章的形式，我们将需要一个偏置值`b`和一个`5×5`的权重矩阵`W`来连接隐层和输入层的神经元。CNN的一个关键特性是，该权重矩阵`W`和偏置`b`在隐藏层中的所有神经元之间共享；我们对隐藏层中的神经元使用相同的`W`和`b`。 在我们的情况下，这是`24×24`（576）个神经元。 读者应该能够看到，与完全连接的神经网络相比，这大大减少了人们需要的权重参数。 具体而言，由于共享权重矩阵`W`，这从 14000（`5x5x24x24`）减少到仅 25（`5x5`）。

这个共享矩阵`W`和偏置`b`通常在 CNN 的上下文中称为核或过滤器。 这些过滤器类似于用于修饰图像的图像处理程序，在我们的例子中用于查找微分特征。 我建议查看 GIMP [40] 手册中的示例，以便了解卷积过程的工作原理。

矩阵和偏置定义了核。 核只检测图像中的某个相关特征，因此建议使用多个核，每个核对应我们想要检测的每个特征。 这意味着 CNN 中的完整卷积层由几个核组成。  表示几个核的常用方法如下：

![](https://jorditorres.org/wp-content/uploads/2016/02/image078.png)

第一个隐藏层由几个核组成。 在我们的例子中，我们使用 32 个核，每个核由`5×5`的权重矩阵`W`和偏置`b`定义，偏置`b`也在隐层的神经元之间共享。

为了简化代码，我定义了以下两个与权重矩阵`W`和偏置`b`相关的函数：

```py
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)
```

在没有详细说明的情况下，习惯上用一些随机噪声初始化权重，偏置值略微为正。

除了我们刚才描述的卷积层之外，通常卷积层后面跟着一个所谓的池化层。 池化层简单地压缩来自卷积层的输出，并创建卷积层输出的信息的紧凑版本。 在我们的示例中，我们将使用卷积层的`2×2`区域，我们使用池化将它的数据汇总到单个点：

![](https://jorditorres.org/wp-content/uploads/2016/02/image080.png)

有几种方法可以执行池化来压缩信息；在我们的示例中，我们将使用名为最大池化的方法。 通过仅保留所考虑的`2×2`区域中的最大值来压缩信息。

如上所述，卷积层由许多核组成，因此，我们将分别对每个核应用最大池化。 通常，可以有多层池化和卷积：

![](https://jorditorres.org/wp-content/uploads/2016/02/image082.png)

这使`24×24`的卷积层结果，被对应`12×12`的最大池化层转换为`12×12`的空间，其中每个块来源于`2×2`的区域。 请注意，与卷积层不同，数据是平铺的，而不是由滑动窗口创建的。

直观上，我们可以解释最大池化，来确定特定特征是否存在于图像中的任何位置，特征的确切位置不如对于其他特征的相对位置重要。

### 模型的实现

在本节中，我将基于可在 TensorFlow [41] 网站上找到的高级示例（Deep MNIST for experts），提供编写 CNN 的示例代码。 正如我在开始时所说的那样，参数的许多细节需要处理和理论方法，比本书中给出的更详细。 因此，我将仅概述代码，而不涉及 TensorFlow 参数的许多细节。

正如我们已经看到的，我们必须为卷积和池化层定义几个参数。  我们将在每个维度中使用大小为 1 的步幅（这是滑动窗口的步长）和零填充模型。 我们将应用的池化是`2×2`的最大池化。 与上面类似，我建议使用以下两个通用函数来编写涉及卷积和最大池化的更清晰的代码。

```py
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')
```

现在是时候实现第一个卷积层，然后是池化层。 在我们的示例中，我们有 32 个过滤器，每个过滤器的窗口大小为`5×5`。 我们必须定义一个张量，来保持这个权重矩阵`W`的形状为`[5,5,1,32]`：前两个维度是窗口的大小，第三个是通道的数量，在我们的例子中为 1 。 最后一个定义了我们想要使用的过滤器数量。 此外，我们还需要为 32 个权重矩阵中的每一个定义偏置。 使用先前定义的函数，我们可以在 TensorFlow 中编写它，如下所示：

```py
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
```

ReLU（整流线性单元）激活函数最近成为深度神经网络隐藏层中使用的默认激活函数。 这个简单的函数返回`max(0, x)`，因此它为负值返回 0，否则返回`x`。  在我们的示例中，我们将在卷积层之后的隐藏层中使用此激活函数。

我们编写的代码首先将卷积应用于输入图像`x_image`，它在 2D 张量`W_conv1`中，返回图像卷积的结果，然后加上偏置，最终应用 ReLU 激活函数。 最后一步，我们将最大池化应用于输出：

```py
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
```

在构建深度神经网络时，我们可以将多个层叠在一起。 为了演示如何执行此操作，我将创建一个带有 64 个过滤器和`5×5`窗口的辅助卷积层。 在这种情况下，我们必须传递 32 作为我们需要的通道数，因为它是前一层的输出大小：

```py
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
```

由于我们将`5×5`窗口应用于步长为 1 的`12×12`空间，因此卷积的结果输出具有`8×8`的维数。 下一步是将一个全连接的层添加到`8×8`输出，然后将其输入到最后的 softmax 层，就像我们在前一章中所做的那样。

我们将使用 1024 个神经元的一层，允许我们处理整个图像。 权重和偏置的张量如下：

```py
W_fc1 = weight_variable([8 * 8 * 64, 1024])
b_fc1 = bias_variable([1024])
```

请记住，张量的第一个维度表示来自第二个卷积层的大小为`8x8`的 64 个过滤器，而第二个参数是层中神经元的数量，我们可以自由选择（在我们的例子中是 1024）。

现在，我们想将张量展开为向量。 我们在前一章中看到，softmax 需要将向量形式的展开图像作为输入。 这通过将权重矩阵`W_fc1`与展开向量相乘，加上偏置`b_fc1`，再应用 ReLU 激活函数来实现：

```py
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])

h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
```

下一步将使用称为 dropout 的技术减少神经网络中的有效参数量。 这包括删除节点及其传入和传出连接。 丢弃和保留哪些神经元是随机决定的。  为了以一致的方式执行此操作，我们将在代码中为丢弃或保留的神经元分配概率。

在没有太多细节的情况下，dropout 降低了模型的过拟合风险。 当隐藏层具有大量神经元并因此可以产生非常富有表现力的模型时，这可能发生；在这种情况下，可能会对随机噪声（或误差）建模。 这被称为过拟合，如果与输入的维度相比，模型具有大量参数，则更有可能。 最好是避免这种情况，因为过拟合的模型具有较差的预测表现。

在我们的模型中，我们应用 dropout，它包括在最终的 softmax 层之前使用 dropout 函数  `tf.nn.dropout`。 为此，我们构造一个占位符来存储在 dropout 期间保留神经元的概率：

```py
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
```

最后，我们将 softmax 层添加到我们的模型中，就像前一章中所做的那样。 请记住，sofmax 返回输入属于每个类的概率（在我们的例子中为数字），以便总概率加起来为 1。 softmax 层代码如下：

```py
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
```

### 模型的训练和评估

我们现在通过调整卷积层和及全连接层中的所有权重，来准备训练我们刚刚定义的模型，并获得我们的带标签的图像的预测。 如果我们想知道模型的执行情况，我们必须遵循上一章中的示例。

以下代码与前一章中的代码非常相似，但有一个例外：我们用 ADAM 优化器替换梯度下降优化器，因为该算法实现了不同的优化器，根据文献 [42]，它具有某些优点。

我们还需要在`feed_dict`参数中包含附加参数`keep_prob`，该参数控制我们之前讨论过的 dropout 层的概率。

```py
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
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, "float"))

sess = tf.Session()

sess.run(tf.initialize_all_variables())
for i in range(20000):
  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 %d, training accuracy %g"%(i, train_accuracy))
  sess.run(train_step,feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"% sess.run(accuracy, feed_dict={ x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
```

与之前的模型一样，整个代码可以在本书的 Github 页面上找到，可以验证该模型的准确率达到 99.2%。

以下是使用 TensorFlow 构建，训练和评估深度神经网络的简要介绍。 如果读者设法运行提供的代码，他或她已经注意到该网络的训练时间明显长于前几章的训练时间；你可以想象，拥有更多层的网络需要花费更长的时间来训练。 我建议你阅读下一章，其中解释了如何使用 GPU 进行训练，这将减少你的训练时间。

本章的代码可以在本书 github 页面 [43] 的`CNN.py`中找到，用于研究目的的代码在下面：

```py
import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
import tensorflow as tf

x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])

x_image = tf.reshape(x, [-1,28,28,1])
print "x_image="
print x_image

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')

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
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, "float"))

sess = tf.Session()

sess.run(tf.initialize_all_variables())

for i in range(200):
   batch = mnist.train.next_batch(50)
   if i%10 == 0:
     train_accuracy = sess.run( accuracy, feed_dict={ x:batch[0], y_: batch[1], keep_prob: 1.0})
     print("step %d, training accuracy %g"%(i, train_accuracy))
   sess.run(train_step,feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"% sess.run(accuracy, feed_dict={ 
       x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
```
