## 4. TensorFlow 中的单层神经网络

在前言中，我评论说深度学习的一个常见用途包括模式识别。 考虑到这一点，就像初学者通过在屏幕上打印“Hello World”开始学习编程语言一样，在深度学习中，我们首先要识别手写数字。

在本章中，我将介绍如何在 TensorFlow 中逐步构建具有单个层的神经网络。 这个神经网络将识别手写数字，它基于 TensorFlow 的初学者教程 [27] 的不同示例之一。

鉴于本书的介绍风格，我选择引导读者，同时通过示例的某些步骤简化了一些概念和理论上的原因。

如果读者在阅读本章后有兴趣了解这个示例的理论概念，我建议阅读神经网络和深度学习 [28]，可在线获取，它介绍了这个例子，但深入研究理论概念。


### MNIST 数据集

MNIST 数据集由一组包含手写数字的黑白图像组成，包含60,000 多个用于训练模型的示例，以及 10,000 个用于测试它的示例。 MNIST 数据集可以在 MNIST 数据库 [29] 中找到。

这个数据集非常适合大多数开始在实例上进行模式识别的人，而不必花时间进行数据预处理或格式化，这是处理图像时的两个非常重要的步骤，但时间很长。

黑白图像（二值）已经标准化为`20×20`像的素图像，保留了宽高比。 对于这种情况，我们注意到图像包含灰色像素 [30]，是归一化算法的结果（将所有图像的分辨率降低到最低级别之一）。 之后，通过计算质心并将其移动到帧的中心，图像以`28×28`像素帧为中心。 图像类似于此处显示的图像：

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

此外，本例所需的学习类型是监督学习；图像用它们代表的数字标记。 这是最常见的机器学习形式。

在这种情况下，我们首先收集数字图像的大数据集，每个数字都用其值标记。 在训练期间，模型接受图像并以得分向量的形式产生输出，每个类别一个得分。 我们希望所需类别在所有类别中得分最高，但这在训练之前不太可能发生。

我们计算一个目标函数来衡量输出分数和所需分数模式之间的误差（正如我们在前面章节中所做的那样）。 然后，模型修改其内部可调参数，称为权重，来减少此误差。 在典型的深度学习系统中，可能存在数亿个这样的可调节权重，以及用于训练机器的数亿个标记示例。 我们将考虑一个较小的例子，来帮助理解这种模型的工作原理。

要轻松下载数据，你可以使用从 Google 的网站 [32] 获取脚本`input_data.py`[31]，但它为你上传到的这本书的 github 上。 只需将代码`input_data.py`下载到使用 TensorFlow 编写神经网络的同一工作目录中。 在你的应用程序中，你只需要按以下方式导入和使用：

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

执行这两条指令后，你将在`mnist.train`中获得完整的训练数据集，并在`mnist.test`中设置测试数据。 如前所述，每个元素由一个图像组成，标记为`xs`，并且其对应的标签`ys`，以便更容易表达处理代码。 请记住，所有数据集，训练和测试集都包含`xs`和`ys`；此外，训练图像通过`mnist.train.images`引用，训练标签通过`mnist.train.labels`引用。

如前所述，图像由`28×28`像素形成，并且可以表示为数字矩阵。 例如，数字 1 的图像之一可以表示为：

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

其中每个位置表示 0 到 1 之间每个像素的缺失度。 该矩阵可以表示为`28×28 = 784`个数的数组。 实际上，图像已经变换为 784 维度的向量空间中的一堆点中。 只是当我们将结构减少到 2 维时，我们可能会丢失部分信息，对于某些计算机视觉算法，这可能会影响他们的结果，但对于本教程中使用的最简单的方法，这不会是一个问题。

总而言之，我们在 2D 中拥有张量`mnist.train.images`，其中调用函数`get_shape()`表示其形状：

```py
TensorShape([Dimension(60000), Dimension(784)])
```

第一维索引每个图像和第二维是每个像素。 张量的每个元素是 0 到 1 之间的每个像素的强度。

此外，我们有 0 到 9 之间的数字形式的标签，表示每个图像代表哪个数字。 在这个例子中，我们将标签表示为 10 个位置的向量，其中所表示数字的对应位置是 1 而其余为 0。 所以`mnist.train.labels es`是形如`TensorShape([Dimension(60000), Dimension10)])`的张量。

### 人造神经元

虽然本书并未关注神经网络的理论概念，但简要而直观地介绍神经元如何学习训练数据，将有助于读者了解正在发生的事情。 那些已经了解该理论并且只是寻求如何使用 TensorFlow 的读者可以跳过本节。

让我们看一个神经元如何学习的简单但说明性的例子。 假设有一组标记为“方形”和“圆形”的点。 给定一个新的点`X`，我们想知道对应哪个标签：

![](https://jorditorres.org/wp-content/uploads/2016/02/Screen-Shot-2016-02-16-at-09.30.14.png)

通常的近似可能是绘制一条划分两组的直线并将其用作分类器：

![](https://jorditorres.org/wp-content/uploads/2016/02/Screen-Shot-2016-02-16-at-09.30.09.png)

在这种情况下，输入数据由形状为`(x, y)`的向量表示，表示此二维空间中的坐标，并且我们的函数返回“0”或“1”（线上方或下方）来了解如何将其归类为“方形”或“圆形”。 在数学上，正如我们在线性回归章节中所学到的，“直线”（分类器）可以表示为`y = W * x + b`。

推广时，神经元必须学习权重`W`（与输入数据`X`维度相同）和偏移量`b`（在神经元中称为偏置），来学习如何分类这些值。 利用它们，神经元将计算权重输入`X`和`W`的加权和，并添加偏移`b`；最后神经元将应用非线性“激活”函数来产生“0”或“1”的结果。

神经元的功能可以更正式地表示为：

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

在为我们的神经元定义了这个函数后，我们想知道神经元如何从带有“方块”和“圆圈”的标记数据中学习参数`W`和`b`，以便稍后标记新点`X`。

第一种方法可以类似于我们对线性回归所做的，即用已知的标记数据喂养神经元，并将获得的结果与真实的结果进行比较。 然后，在迭代时，调整`W`和`b`来使误差最小化，如第 2 章中线性回归线所示。

一旦我们得到`W`和`b`参数，我们就可以计算加权和，现在我们需要函数将存储在`z`中的结果转换为`0`或`1`。 有几个可用的激活函数，对于这个例子，我们可以使用一个名为 sigmoid [33] 的流行函数，返回 0 到 1 之间的实数值。

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

看看公式，我们发现它将倾向于返回接近 0 或 1 的值。 如果输入`z`足够大且为正，则`exp(-z)`为零，然后`y`为 1。 如果输入`z`足够大且为负，则`exp(-z)`也会变为大正数，因此分母变大，最终`y`变为 0。 如果我们绘制函数，它将如下所示：

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

从这里我们已经介绍了如何定义神经元，但神经网络实际上是以不同方式互相连接，并使用不同激活函数的神经元组合。 鉴于本书的范围，我不会涉及神经网络的所有扩展，但我向你保证它真的令人兴奋。

只是提到神经网络的一个特定情况（其中第 5 章基于），神经元组织为层的形式，其中下层（输入层）接收输入，上层（输出层）生成响应值。 神经网络可以有几个中间层，称为隐藏层。 表示这种情况的直观方式是：

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

在这些网络中，每层的神经元与前一层的神经元通信来接收信息，然后将其结果传递给下一层的神经元。

如前所述，除了Sigmoid之外还有更多的激活函数，每个激活函数具有不同的属性。例如，当我们想要在输出层将数据分类为两个以上的类时，我们可以使用 Softmax [34] 激活函数，它是 sigmoid 函数的泛化。 Softmax 能够获得每个类的概率，因此它们的和为 1，最可能的结果是概率最高的结果。

### 一个简单的例子：Softmax

请记住，要解决的问题是，给定输入图像，我们得到它属于某个数字的概率。 例如，我们的模型可以预测，图像 80% 是“9”，但是有 5% 的机会为“8”（由于可疑性较低的痕迹），并且还给出，一定的低概率为任何其他数字。 识别手写数字存在一些不确定性，我们无法以 100% 的置信度识别数字。 在这种情况下，概率分布使我们更好地了解我们对预测的信心。

因此，我们有一个输出向量，其中包含不同输出标签的概率分布，这是多余的。 这是一个具有 10 个概率值的向量，每个概率值对应于 0 到 9 的每个数字，并且所有概率总和为 1。

如前所述，我们通过使用激活函数为 softmax 的输出层来实现此目的。 具有 softmax 函数的神经元的输出，取决于其层的其他神经元的输出，因为它们的所有输出必须总和为 1。

softmax 函数有两个主要步骤：首先，计算属于某个标签的图像的“证据”，然后将证据转换为每个可能标签的概率。

### 归属的证据

测量某个图像属于特定类别/标签的证据，通常的近似是计算像素强度的加权和。 当高强度的像素恰好不在给定类中时，该权重为负，如果该像素在该类中频繁出现，则该权重为正。

让我们看一个图形示例：假设一个数学“0”的学习模型（我们将看到以后如何学习）。 此时，我们将模型定义为“某事物”，其中包含了解数字是否属于特定类的信息。 在这种情况下，我们选择了如下所示的模型，其中红色（或 b/n 版本的亮灰色）代表负例（也就是，减少对“0”中存在的那些像素的支持），而蓝色（b/n 版的深灰色）代表了正例。看看它：

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

想象一下`28×28`像素的白纸，并画上“0”。 通常我们的零将绘制在蓝色区域中（请记住，我们在`20×20`绘图区域周围留下了一些空间，稍后将其居中）。

很明显，如果我们的绘图穿过红色区域，很可能我们没有绘制零。 因此，使用一种度量标准，奖励那些踩到蓝色区域的像素，并惩罚那些踩到红色区域的像素，似乎是合理的。

现在考虑“3”：很明显，我们的模型的“0”的红色区域将惩罚它为“0”的概率。 但是如果参考模型是下面的那个，通常形成“3”的像素将遵循蓝色区域； “0”的绘制也会进入红色区域。

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

我希望看到这两个例子的读者理解，所解释的近似如何让我们估计哪张图代表哪个数字。

下图显示了从 MNIST 数据集中学习的十个不同标签/类的示例（从 Tensorflow [35] 的示例中提取）。 请记住，红色（亮灰色）表示负权重，蓝色（深灰色）表示正值。

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

以更正式的方式，我们可以说给出输入`x`的类`i`的证据表示为：

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

其中`i`表示类（在我们的情况下，介于 0 和 9 之间），`j`是对输入图像求和的索引。 最后，`Wi`表示上述权重。

请记住，一般来说，模型还包括一个表示偏置的额外参数，增加了一些基本不确定性。 在我们的情况下，公式最终就像这样：

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

对于每个`i`（在 0 和 9 之间），我们得到 784 个元素（`28×28`）的矩阵`Wi`，其中每个元素`j`乘以输入图像的相应分量`j`，共有 784 个分量，然后加上`bi`。矩阵演算和索引的图形视图是这样的：

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

### 归属概率

我们评论说，第二步是计算概率。 具体来说，我们使用 softmax 函数将证据总和转换为预测概率，表示为`y`：

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

请记住，输出向量必须是和为 1 的概率函数。 为了标准化每个成分，softmax 函数使用每个输入的指数值，然后按如下方式对它们进行标准化：

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

使用指数时获得的效果是权重的乘法效应。 此外，当一个类的证据很小时，这个类的支持由之前权重的一小部分减少。 此外，softmax 对权重进行归一化，使它们总和为 1，从而产生概率分布。

这种函数的一个有趣的事实是，好的预测将有一个接近 1 的输出值，而所有其他输出将接近零；在弱预测中，某些标签可能会显示类似的支持。

### 在 TensorFlow 中编程

在简要描述了算法做了什么来识别数字之后，我们可以在 TensorFlow 中实现它。 为此，我们可以快速了解张量应如何存储我们的数据和模型参数。 为此，下图描述了数据结构及其关系（来帮助读者轻松回忆我们的每个问题）：

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

首先，我们创建两个变量来包含权重`W`和偏置`b`：

```py
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
```

这些变量是使用`tf.Variable`函数和变量的初始值创建的；在这种情况下，我们用包含零的常数张量初始化张量。

我们看到`W`的形状为`[Dimension(784), Dimension(10)]`，由其参数定义，常数张量`tf.zeros`和`W`一样为`[784,10]`。偏置`b`也是一样，由其参数将形状规定为`[Dimension(10)]`。

矩阵`W`具有该大小，因为我们想要为 10 个可能的数字中的每一个乘以 784 个位置的图像向量，并在与`b`相加之后产生一定数量的证据。

在使用 MNIST 进行研究的情况下，我们还创建了二维张量来保存`x`点的信息，使用以下代码行​​：

```py
x = tf.placeholder("float", [None, 784])
```

张量`x`将用于存储 MNIST 图像，作为 784 个浮点向量（我们使用`None`指示维度可以是任何大小；在我们的例子中它将等于学习过程中包含的元素数量）。

现在我们定义了张量，我们可以实现我们的模型。 为此，TensorFlow 提供了几个操作，即`tf.nn.softmax(logits, name=None)`。它是其中一个可用的操作，实现了前面描述的 softmax 函数。 参数必须是张量，并且名称可选。 该函数返回类型和形状与传递的参数张量相同的张量。

在我们的例子中，我们为这个函数提供了图像向量`x`乘以权重矩阵`W`加上`b`的结果张量：

```py
y = tf.nn.softmax(tf.matmul(x,W) + b)
```

一旦指定了模型实现，我们就可以使用迭代训练算法，指定必要的代码来获得权重`W`和偏置`b`。 对于每次迭代，训练算法获得训练数据，应用神经网络并将获得的结果与预期结果进行比较。

要确定模型何时足够好，我们必须定义“足够好”的含义。 正如在前面的章节中所看到的，通常的方法是定义相反的东西：模型使用损失函数的“坏”的程度。 在这种情况下，目标是获得使函数最小的`W`和`b`的值，它指示模型“坏”的程度。

结果输出与训练数据的预期输出之间的误差有不同的度量标准。 一个常见的度量是均方误差或平方欧几里德距离，这是以前见过的。 尽管如此，一些研究在神经网络中为此目的提出了其他指标，例如在我们的例子中使用的交叉熵误差。 此度量标准的计算方式如下：

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

其中`y`是概率的预测分布，`y'`是从训练数据集的标签中获得的实际分布。 我们不会详细讨论交叉熵背后的数学及其在神经网络中的位置，因为它远比本书的预期范围复杂得多；只是表明当两个分布相同时有最小值。 同样，如果读者想要了解此函数的细节，我们建议阅读神经网络和深度学习 [36]。

要实现交叉熵度量，我们需要一个新的占位符来表示正确的标签：

```py
y_ = tf.placeholder("float", [None,10])
```

用这个占位符，我们可以使用以下代码行实现交叉熵，代表我们的损失函数：

```py
cross_entropy = -tf.reduce_sum(y_*tf.log(y))
```

首先，我们使用 TensorFlow 中的内置函数`tf.log()`计算每个元素`y`的对数，然后我们将它们乘以每个`y_`元素。 最后，使用`tf.reduce_sum`，我们对张量的所有元素求和（稍后我们将看到图像以批量的形式访问，在这种情况下，交叉熵的值对应于图像批量`y`而不是单个图像）。

在迭代中，一旦确定了样本的误差，我们必须更正模型（在我们的例子中是修改参数`W`和`b`）来减少下一次迭代中计算和预期输出之间的差异。

最后，它仍然只是指定了这个迭代式最小化过程。 在神经网络中有几种用于此目的的算法；我们将使用反向传播（误差向后传播）算法，并且如其名称所示，它向后传播在输出处获得的误差，来重新计算`W`的权重，尤其是对于多层神经网络很重要。

该方法与先前看到的梯度下降方法一起使用，该方法使用交叉熵损失函数，允许我们计算每次迭代时参数必须改变多少，以便在每个时刻使用可用的本地信息来减少误差。 在我们的例子中，直观地说，它包括在每次迭代时稍微改变权重`W`（这一点由学习率超参数表示，表示变化的速度）来减少错误。

由于在我们的例子中我们只有一层神经网络，我们不会进入反向传播方法。 只需记得 TensorFlow 知道整个计算图，允许它应用优化算法来找到训练模型的训练函数的正确梯度。

因此，在我们使用 MNIST 图像的示例中，以下代码行表明我们使用反向传播算法和梯度下降算法来最小化交叉熵，学习率为 0.01：

```py
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
```

到这里之后，我们已经指定了所有问题，我们可以通过实例化`tf.Session()`来开始计算，它负责在系统，CPU 或 GPU 上的可用设备中执行 TensorFlow 操作：

```py
sess = tf.Session()
```

接下来，我们可以执行初始化所有变量的操作：

```py
sess.run(tf.initialize_all_variables())
```

从现在开始，我们可以开始训练我们的模型。 执行时，`train_step`的返回参数将梯度下降应用于所涉及的参数。 因此，可以通过重复执行`train_step`来实现模型的训练。 假设我们要迭代 1000 次`train_step`；我们必须指定以下代码行：

```py
for i in range(1000):
   batch_xs, batch_ys = mnist.train.next_batch(100)
   sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
```

循环内的第一行指定，对于每次迭代，挑选从训练数据集中随机采样的 100 个数据输入的批量。 我们可以在每次迭代时使用所有训练数据，但为了使第一个示例更加灵活，我们每次都使用一个小样本。 第二行表示之前获得的输入必须提供给相应的占位符。

最后，基于梯度下降的机器学习算法可以利用 TensorFlow 自动微分的功能。 TensorFlow 用户只需定义预测模型的计算架构，将其与目标函数组合，然后只需添加数据即可。

TensorFlow 已经管理了学习过程背后的相关微分。 当执行`minimize()`方法时，TensorFlow 识别损失函数所依赖的变量集，并计算每个变量的梯度。 如果你想知道如何实现微分，可以查看`ops/gradients.py`文件 [37]。

### 模型评估

训练后必须评估模型，来查看有多“好”（或多“坏”）。 例如，我们可以计算预测中命中和未命中的百分比，看看哪些例子是正确预测的。 在前面的章节中，我们看到`tf.argmax(y, 1)`函数，根据张量的给定轴返回最高值的索引。 实际上，`tf.argmax(y, 1)`是对于每个输入的，概率最高的标签，而 `tf.argmax(y_, 1)`是正确标签。 使用`tf.equal`方法，我们可以比较我们的预测是否与正确的标签重合：

```py
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
```

指令返回布尔列表。 要确定哪些预测部分是正确的，我们可以将值转换为数值变量（浮点）并执行以下操作：

```py
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
```

例如，`[True, False, True, True]`将变为`[1, 0, 1, 1]`，平均值将为 0.75，表示准确率。 现在我们可以使用`mnist.test`作为`feed_dict参`数来查询我们的测试数据集的准确率：

```py
print sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
```

我的大约为 91%。 这些结果好吗？ 我认为它们太棒了，因为这意味着读者已经能够使用 TensorFlow 编程并执行第一个神经网络。

另一个问题是其他模型可能提供更好的准确性，在下一章中介绍包含更多层的神经网络。

读者将在本书 github [38] 的文件`RedNeuronalSimple.py`中找到本章中使用的全部代码。  为了提供它的全局视图，我将把它放在一起：

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

import tensorflow as tf

x = tf.placeholder("float", [None, 784])
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

matm=tf.matmul(x,W)
y = tf.nn.softmax(tf.matmul(x,W) + b)
y_ = tf.placeholder("float", [None,10])

cross_entropy = -tf.reduce_sum(y_*tf.log(y))
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

sess = tf.Session()
sess.run(tf.initialize_all_variables())

for i in range(1000):
    batch_xs, batch_ys = mnist.train.next_batch(100)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
    correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
    print sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
```
