1.6 实现我们的网络来分类数字

好吧,现在让我们写一个学习如何识别手写数字的程序,使用随机梯度下降算法和 MNIST 训练数据。我们需要做的第一件事情是获取 MNIST 数据。如果你是一个 git 用户,那么你能够通过克隆这本书的代码仓库获得数据,

git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git

如果你不使用 git,也可以从这里下载数据和代码。

顺便提一下,当我在之前描述 MNIST 数据时,我说它分成了 60,000 个训练图像和 10,000 个测试图像。这是官方的 MNIST 的描述。实际上,我们将用稍微不同的方法对数据进行划分。我们将测试集保持原样,但是将 60,000 个图像的 MNIST 训练集分成两个部分:一部分 50,000 个图像,我们将用来训练我们的神经网络,和一个单独的 10,000 个图像的验证集。在本章中我们不使用验证数据,但是在本书的后面我们将会发现它对于解决如何去设置某些神经网络中的超参数是很有用的 —— 例如学习速率等,这些参数不被我们的学习算法直接选择。尽管验证数据不是原始 MNIST 规范的一部分,然而许多人以这种方式使用 MNIST,并且在神经网络中使用验证数据是很普遍的。从现在起当我提到“ MNIST 训练数据”时,我指的是我们的 50,000 个图像数据集,而不是原始的 60,000 图像数据集5

除了 MNIST 数据,我们还需要一个叫做 Numpy 的 Python 库,用来做快速线性代数。如果你没有安装过 Numpy,你能够从这里下载。

在列出一个完整的代码清单之前,让我解释一下神经网络代码的核心特性。核心片段是一个 Network 类,我们用来表示一个神经网络。这是我们用来初始化一个 Network 对象的代码:

class Network(object):

    def __init__(self, sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x) 
                        for x, y in zip(sizes[:-1], sizes[1:])]

在这段代码中,列表 sizes 包含各层神经元的数量。例如,如果我们想创建一个在第一层有 2 个神经元,第二层有 3 个神经元,最后层有 1 个神经元的 Network 对象,我们应这样写代码:

net = Network([2, 3, 1])

Network 对象中的偏置和权重都是被随机初始化的,使用 Numpy 的 np.random.randn 函数来生成均值为 0,标准差为 1 的高斯分布。这样的随机初始化给了我们的随机梯度下降算法一个起点。在后面的章节中我们将会发现更好的初始化权重和偏置的方法,但是目前随机地将其初始化。注意 Network 初始化代码假设第一层神经元是一个输入层,并对这些神经元不设置任何偏置,因为偏置仅在后面的层中用于计算输出。

另外注意,偏置和权重以 Numpy 矩阵列表的形式存储。例如 net.weights[1] 是一个存储着连接第二层和第三层神经元权重的 Numpy 矩阵。(不是第一层和第二层,因为 Python 列表的索引从 0 开始。)既然 net.weights[1] 相当冗长,让我们用 表示矩阵。矩阵的 是连接第二层的 神经元和第三层的 神经元的权重。这种 索引的顺序可能看着奇怪 —— 交换 索引会更有意义,确定吗?使用这种顺序的很大的优势是它意味着第三层神经元的激活向量是:

这个方程有点奇怪,所以让我们一块一块地理解它。 是第二层神经元的激活向量。为了得到 ,我们用权重矩阵 乘以 ,加上偏置向量 ,我们然后对向量 中的每个元素应用函数 。(这称为将函数 向量化。)很容易验证方程 (22) 的结果和我们之前的计算一个 S 型神经元输出的方程 (4) 相同。

练习

  • 以分量形式写出方程 (22),并验证它和计算 S 型神经元输出的规则 (4) 结果相同。

有了这些,很容易写出从一个 Network 实例计算输出的代码。我们从定义 S 型函数开始:

def sigmoid(z):
    return 1.0/(1.0+np.exp(-z))

注意,当输入 是一个向量或者 Numpy 数组时,Numpy 自动地按元素应用 sigmoid 函数,即以向量形式。

我们然后对 Network 类添加一个 feedforward 方法,对于网络给定一个输入 ,返回对应的输出6。这个方法所做的是对每一层应用方程 (22):

def feedforward(self, a):
        """Return the output of the network if "a" is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

当然,我们想要 Network 对象做的主要事情是学习。为此我们给它们一个实现随机梯度下降算法的 SGD 方法。代码如下。其中一些地方看似有一点神秘,我会在代码后面逐个分析。

def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """Train the neural network using mini-batch stochastic
        gradient descent.  The "training_data" is a list of tuples
        "(x, y)" representing the training inputs and the desired
        outputs.  The other non-optional parameters are
        self-explanatory.  If "test_data" is provided then the
        network will be evaluated against the test data after each
        epoch, and partial progress printed out.  This is useful for
        tracking progress, but slows things down substantially."""
        if test_data: n_test = len(test_data)
        n = len(training_data)
        for j in xrange(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in xrange(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print "Epoch {0}: {1} / {2}".format(
                    j, self.evaluate(test_data), n_test)
            else:
                print "Epoch {0} complete".format(j)

training_data 是一个 (x, y) 元组的列表,表示训练输入和其对应的期望输出。变量 epochsmini_batch_size 正如你预料的 —— 迭代期数量,和采样时的小批量数据的大小。eta 是学习速率,。如果给出了可选参数 test_data,那么程序会在每个训练器后评估网络,并打印出部分进展。这对于追踪进度很有用,但相当拖慢执行速度。

代码如下工作。在每个迭代期,它首先随机地将训练数据打乱,然后将它分成多个适当大小的小批量数据。这是一个简单的从训练数据的随机采样方法。然后对于每一个 mini_batch 我们应用一次梯度下降。这是通过代码 self.update_mini_batch(mini_batch, eta) 完成的,它仅仅使用mini_batch 中的训练数据,根据单次梯度下降的迭代更新网络的权重和偏置。这是 update_mini_batch 方法的代码:

 def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The "mini_batch" is a list of tuples "(x, y)", and "eta"
        is the learning rate."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw 
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb 
                       for b, nb in zip(self.biases, nabla_b)]

大部分工作由这行代码完成:

delta_nabla_b, delta_nabla_w = self.backprop(x, y)

这行调用了一个称为反向传播的算法,一种快速计算代价函数的梯度的方法。因此update_mini_batch 的工作仅仅是对 mini_batch 中的每一个训练样本计算梯度,然后适当地更新 self.weightsself.biases

我现在不会列出 self.backprop 的代码。我们将在下章中学习反向传播是怎样工作的,包括 self.backprop 的代码。现在,就假设它按照我们要求的工作,返回与训练样本 相关代价的适当梯度。

让我们看一下完整的程序,包括我之前忽略的文档注释。除了 self.backprop,程序已经有了足够的文档注释 —— 所有的繁重工作由 self.SGDself.update_mini_batch 完成,对此我们已经有讨论过。self.backprop 方法利用一些额外的函数来帮助计算梯度,即 sigmoid_prime,它计算 函数的导数,以及 self.cost_derivative,这里我不会对它过多描述。你能够通过查看代码或文档注释来获得这些的要点(或者细节)。我们将在下章详细地看它们。注意,虽然程序显得很长,但是很多代码是用来使代码更容易理解的文档注释。实际上,程序只包含 74 行非空、非注释的代码。所有的代码可以在 GitHub 上这里找到。

"""
network.py
~~~~~~~~~~

A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network.  Gradients are calculated
using backpropagation.  Note that I have focused on making the code
simple, easily readable, and easily modifiable.  It is not optimized,
and omits many desirable features.
"""

#### Libraries
# Standard library
import random

# Third-party libraries
import numpy as np

class Network(object):

    def __init__(self, sizes):
        """The list ``sizes`` contains the number of neurons in the
        respective layers of the network.  For example, if the list
        was [2, 3, 1] then it would be a three-layer network, with the
        first layer containing 2 neurons, the second layer 3 neurons,
        and the third layer 1 neuron.  The biases and weights for the
        network are initialized randomly, using a Gaussian
        distribution with mean 0, and variance 1.  Note that the first
        layer is assumed to be an input layer, and by convention we
        won't set any biases for those neurons, since biases are only
        ever used in computing the outputs from later layers."""
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

    def feedforward(self, a):
        """Return the output of the network if ``a`` is input."""
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a)+b)
        return a

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """Train the neural network using mini-batch stochastic
        gradient descent.  The ``training_data`` is a list of tuples
        ``(x, y)`` representing the training inputs and the desired
        outputs.  The other non-optional parameters are
        self-explanatory.  If ``test_data`` is provided then the
        network will be evaluated against the test data after each
        epoch, and partial progress printed out.  This is useful for
        tracking progress, but slows things down substantially."""
        if test_data: n_test = len(test_data)
        n = len(training_data)
        for j in xrange(epochs):
            random.shuffle(training_data)
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in xrange(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)
            if test_data:
                print "Epoch {0}: {1} / {2}".format(
                    j, self.evaluate(test_data), n_test)
            else:
                print "Epoch {0} complete".format(j)

    def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
        is the learning rate."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb
                       for b, nb in zip(self.biases, nabla_b)]

    def backprop(self, x, y):
        """Return a tuple ``(nabla_b, nabla_w)`` representing the
        gradient for the cost function C_x.  ``nabla_b`` and
        ``nabla_w`` are layer-by-layer lists of numpy arrays, similar
        to ``self.biases`` and ``self.weights``."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x] # list to store all the activations, layer by layer
        zs = [] # list to store all the z vectors, layer by layer
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        for l in xrange(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)

    def evaluate(self, test_data):
        """Return the number of test inputs for which the neural
        network outputs the correct result. Note that the neural
        network's output is assumed to be the index of whichever
        neuron in the final layer has the highest activation."""
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)

    def cost_derivative(self, output_activations, y):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y)

#### Miscellaneous functions
def sigmoid(z):
    """The sigmoid function."""
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    """Derivative of the sigmoid function."""
    return sigmoid(z)*(1-sigmoid(z))

这个程序对识别手写数字效果如何?好吧,让我们先加载 MNIST 数据。我将用下面所描述的一小段辅助程序 mnist_loader.py 来完成。我们在一个 Python shell 中执行下面的命令,

>>> import mnist_loader
>>> training_data, validation_data, test_data = \
... mnist_loader.load_data_wrapper()

当然,这也可以以一个单独的 Python 程序来完成,但是如果你正在照着本书做,在 Python shell 里执行也许是最方便的。

在加载完 MNIST 数据之后,我们将设置一个有 30 个隐藏层神经元的 Network。我们在导入如上所列的名为 Network 的 Python 程序后做,

>>> import network
>>> net = network.Network([784, 30, 10])

最后,我们将使用随机梯度下降来从 MNIST training_data 学习超过 30 次迭代期,小批量数据大小为 10,学习速率

>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

注意,如果当你读到这里并正在运行代码,执行将会花费一些时间 —— 对于一台典型的机器(截至 2015 年),它可能会花费几分钟来运行。我建议你让它运行着,继续阅读并时不时地检查一下代码的输出。如果你急于想看到结果,你可以通过减少迭代期数量,减少隐藏层神经元数量,或者只使用部分训练数据来提高速度。注意这样产生的代码将会特别快:这些 Python 脚本只是为了帮助你理解神经网络是如何工作的,而不是高性能的代码!而且,当然,一旦我们已经训练一个网络,它能在几乎任何的计算平台上快速的运行。例如,一旦我们给一个网络学会了一组好的权重集和偏置集,它能很容易地被移植到网络浏览器中以 Javascript 运行,或者如在移动设备上的本地应用。在任何情况下,这是一个神经网络训练运行时的部分打印输出。打印内容显示了在每轮训练期后神经网络能正确识别测试图像的数量。正如你所见到,在仅仅一次迭代期后,达到了 10,000 中选中的 9,129 个。而且数目还在持续增长,

Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000

更确切地说,经过训练的网络给出的识别率约为 95% —— 在峰值时为 95.42%(“Epoch 28”)!作为第一次尝试,这是非常令人鼓舞的。然而我应该提醒你,如果你运行代码然后得到的结果和我的不完全一样,那是因为我们使用了(不同的)随机权重和偏置来初始化我们的网络。我采用了三次运行中的最优结果作为本章的结果。

让我们重新运行上面的实验,将隐藏神经元数量改到 100。正如前面的情况,如果你一边阅读一边运行代码,我应该警告你它将会花费相当长一段时间来执行(在我的机器上,这个实验每一轮训练迭代需要几十秒),因此比较明智的做法是当代码运行的同时,继续阅读。

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

果然,它将结果提升至 96.59%。至少在这种情况下,使用更多的隐藏神经元帮助我们得到了更好的结果7

当然,为了获得这些准确性,我不得不对训练的迭代期数量,小批量数据大小和学习速率 做特别的选择。正如我上面所提到的,这些在我们的神经网络中被称为超参数,以区别于通过我们的学习算法所学到的参数(权重和偏置)。如果我们选择了糟糕的超参数,我们会得到较差的结果。假如我们选定学习速率为 ,

>>> net = network.Network([784, 100, 10])
>>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)

结果则不太令人鼓舞了,

Epoch 0: 1139 / 10000
Epoch 1: 1136 / 10000
Epoch 2: 1135 / 10000
...
Epoch 27: 2101 / 10000
Epoch 28: 2123 / 10000
Epoch 29: 2142 / 10000

然而,你可以看到网络的性能随着时间的推移慢慢地变好了。这表明应该增大学习速率,例如 。如果我们那样做了,我们会得到更好的结果,这表明我们应该再次增加学习速率。(如果改变能够改善一些事情,试着做更多!)如果我们这样做几次,我们最终会得到一个像 的学习速率(或者调整到 ),这跟我们之前的实验很接近。因此即使我们最初选择了糟糕的超参数,我们至少获得了足够的信息来帮助我们改善对于超参数的选择。

通常,调试一个神经网络是具有挑战性的。尤其是当初始的超参数的选择产生的结果还不如随机噪点的时候。假如我们使用之前成功的具有 30 个隐藏神经元的网络结构,但是学习速率改为

>>> net = network.Network([784, 30, 10])
>>> net.SGD(training_data, 30, 10, 100.0, test_data=test_data)

在这点上,我们实际走的太远,学习速率太高了:

Epoch 0: 1009 / 10000
Epoch 1: 1009 / 10000
Epoch 2: 1009 / 10000
Epoch 3: 1009 / 10000
...
Epoch 27: 982 / 10000
Epoch 28: 982 / 10000
Epoch 29: 982 / 10000

现在想象一下,我们第一次遇到这样的问题。当然,我们从之前的实验中知道正确的做法是减小学习速率。但是如果我们第一次遇到这样的问题,那么输出的数据就不会有太多信息能指导我们怎么做。我们可能不仅关心学习速率,还要关心我们的神经网络中的其它每一个部分。我们可能想知道是否用了让网络很难学习的初始权重和偏置?或者可能我们没有足够的训练数据来获得有意义的学习?或者我们没有进行足够的迭代期?或者可能对于具有这种结构的神经网络,学习识别手写数字是不可能的?可能学习速率太低?或者可能学习速率太高?当你第一次遇到问题,你不总是能有把握。

从这得到的教训是调试一个神经网络不是琐碎的,就像常规编程那样,它是一门艺术。你需要学习调试的艺术来获得神经网络更好的结果。更普通的是,我们需要启发式方法来选择好的超参数和好的结构。我们将在整本书中讨论这些,包括上面我是怎么样选择超参数的。

练习

  • 试着创建一个仅有两层的网络 —— 一个输入层和一个输出层,分别有 784 和 10 个神经元,没有隐藏层。用随机梯度下降算法训练网络。你能达到多少识别率?

之前的内容中,我跳过了如何加载 MNIST 数据的细节。这很简单。这里列出了完整的代码。用于存储 MNIST 数据的数据结构在文档注释中有详细描述 —— 都是简单的类型,元组和 Numpy ndarray 对象的列表(如果你不熟悉 ndarray,那就把它们看成向量):

"""
mnist_loader
~~~~~~~~~~~~

A library to load the MNIST image data.  For details of the data
structures that are returned, see the doc strings for ``load_data``
and ``load_data_wrapper``.  In practice, ``load_data_wrapper`` is the
function usually called by our neural network code.
"""

#### Libraries
# Standard library
import cPickle
import gzip

# Third-party libraries
import numpy as np

def load_data():
    """Return the MNIST data as a tuple containing the training data,
    the validation data, and the test data.

    The ``training_data`` is returned as a tuple with two entries.
    The first entry contains the actual training images.  This is a
    numpy ndarray with 50,000 entries.  Each entry is, in turn, a
    numpy ndarray with 784 values, representing the 28 * 28 = 784
    pixels in a single MNIST image.

    The second entry in the ``training_data`` tuple is a numpy ndarray
    containing 50,000 entries.  Those entries are just the digit
    values (0...9) for the corresponding images contained in the first
    entry of the tuple.

    The ``validation_data`` and ``test_data`` are similar, except
    each contains only 10,000 images.

    This is a nice data format, but for use in neural networks it's
    helpful to modify the format of the ``training_data`` a little.
    That's done in the wrapper function ``load_data_wrapper()``, see
    below.
    """
    f = gzip.open('../data/mnist.pkl.gz', 'rb')
    training_data, validation_data, test_data = cPickle.load(f)
    f.close()
    return (training_data, validation_data, test_data)

def load_data_wrapper():
    """Return a tuple containing ``(training_data, validation_data,
    test_data)``. Based on ``load_data``, but the format is more
    convenient for use in our implementation of neural networks.

    In particular, ``training_data`` is a list containing 50,000
    2-tuples ``(x, y)``.  ``x`` is a 784-dimensional numpy.ndarray
    containing the input image.  ``y`` is a 10-dimensional
    numpy.ndarray representing the unit vector corresponding to the
    correct digit for ``x``.

    ``validation_data`` and ``test_data`` are lists containing 10,000
    2-tuples ``(x, y)``.  In each case, ``x`` is a 784-dimensional
    numpy.ndarry containing the input image, and ``y`` is the
    corresponding classification, i.e., the digit values (integers)
    corresponding to ``x``.

    Obviously, this means we're using slightly different formats for
    the training data and the validation / test data.  These formats
    turn out to be the most convenient for use in our neural network
    code."""
    tr_d, va_d, te_d = load_data()
    training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
    training_results = [vectorized_result(y) for y in tr_d[1]]
    training_data = zip(training_inputs, training_results)
    validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
    validation_data = zip(validation_inputs, va_d[1])
    test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
    test_data = zip(test_inputs, te_d[1])
    return (training_data, validation_data, test_data)

def vectorized_result(j):
    """Return a 10-dimensional unit vector with a 1.0 in the jth
    position and zeroes elsewhere.  This is used to convert a digit
    (0...9) into a corresponding desired output from the neural
    network."""
    e = np.zeros((10, 1))
    e[j] = 1.0
    return e

上面我说过我们的程序取得了非常好的结果。那意味着什么?和什么相比算好?如果有一些简单的(非神经网络的)基线测试作为对比就有助于理解它怎样算运行良好。最简单的基线,当然是随机地猜些数字。那将有 10% 的次数是正确的。我们将比这做得更好!

一个较差的基线会怎样?让我们尝试一种极其简单的想法:我们会看一幅图像有多暗。例如,一幅 的图像通常要比一幅 的图像稍暗些,仅仅因为更多像素被涂黑了,就像下面的示例显示的:

mnist_2_and_1

这提示我们可以用训练数据来计算数字图像的平均暗度,。当有一幅新的图像呈现,我们先计算图像的暗度,然后猜测它接近哪个数字的平均暗度。这是一个简单的程序,而且容易编写代码,所以我不会在这里把它们都写出来 —— 如果你有兴趣,代码在 GitHub 仓库里。但是它和随机地猜测相比有了很大的改进,能取得 测试图像中 的精确度,即

找到其它能使精确度达到 之间的办法也不难。如果你更努力些能超过 。但要获得更高的精确度,采用已经被认可的机器学习算法是很有帮助的。让我们尝试使用其中最著名的算法之一,支持向量机,或 SVM。如果你不熟悉 SVM,不用担心,我们不需要去理解 SVM 如何工作的细节。我们将使用 scikit-learn Python 程序库,它提供了一个简单的 Python 接口,包装了一个用于 SVM 的快速的,称为 LIBSVM 的 C 库。

如果我们用默认设置运行 scikit-learn 的 SVM 分类器,那么它能从 测试图像中准确分类 。(代码可以从这里取得)。那是一个很大的改善,远远好于我们幼稚的基于暗度的图像分类方法。确实,这意味着 SVM 表现得几乎和神经网络一样好,只是差了一点而已。在后面章节中我们会介绍新的技术,让我们能够改进我们的神经网络使得它们表现得比 SVM 更好。

这不是故事的结局,然而, 的结果是 scikit-learn 针对 SVM 默认的设置。SVM 有很多可调参数,查找到可以改善默认情况下的性能的参数是可能的。我不会明确地做这些查找,如果你想知道更多,可以参考这份 Andreas Mueller博客。Mueller 展示了通过一些优化 SVM 参数的工作,有可能把性能提高到 98.5% 的精确度。换句话说,一个调整好的 SVM,70 次里只会识别错一次数字。那已经非常好了!神经网络能做得更好吗?

事实上,它们可以。目前,精心设计的神经网络胜过任何其它解决 MNIST 的技术,包括 SVM。现在(2013)的纪录是从 图像中正确分类 个。这是由 Li WanMatthew Zeiler,Sixin Zhang,Yann LeCun,和 Rob Fergus 完成的。我们将在这本书后面看到它们用的大部分技术。那个层次的性能接近于人类,而且可以说更好,因为相当多的 MNIST 图像甚至对人类来说都很难有信心识别,例如:

mnist_really_bad_images

我相信你会同意那些数字很难辨认!考虑到 MNIST 数据集中这样的图像,神经网络能准确识别 幅测试图像中除了 幅之外的其它所有图像,这表现得相当卓越。通常,当编程时我们相信解决一个类似识别 MNIST 数字的问题需要一个复杂的算法。但是即使是刚才提到的 Wan 等人的论文中用的神经网络,只涉及到相当简单的算法、和我们在这一章中已经看到的算法的变化形式。所有的复杂性自动从训练数据学习。在某种意义上,我们的结果和那些在更深奥的论文中都有的寓意是,对有些问题:


5. 如前所述,MNIST 数据集是基于 NIST(美国国家标准与技术研究院)收集的两个数据集合。为了构建 MNIST,NIST 数据集合被 Yann LeCun,Corinna Cortes 和 Christopher J. C. Burges 拆分放入一个更方便的格式。 更多细节请看这个链接。我的仓库中的数据集是在一种更容易在 Python 中加载和操纵 MNIST 数据的形式。我从蒙特利尔大学的 LISA 机器学习实验室获得了这个特殊格式的数据(链接)。
6. 这里假设输入 是一个 (n,1) 的 Numpy ndarray 类型,而不是一个 (n,) 的向量。 这里,n 是网络的输入数量。如果你试着用一个 (n,) 向量作为输入,会得到奇怪的结果。虽然使用 (n,) 向量看上去好像是更自然的选择,但是使用一个 (n,1) 的 ndarray 使得修改代码来立即前馈多个输入变得特别容易,并且有的时候很方便。
7. 读者的反馈表明本实验在结果上有相当多的变化,而且一些训练运行给出的结果相当糟糕。使用第三章所介绍的技术将大大减少我们网络上这些不同训练运行性能的差别。

results matching ""

    No results matching ""