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 图像数据集。
除了 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] 相当冗⻓,让我们用 w 表示矩阵。矩阵的 wjk 是连接第二 层的 kth 神经元和第三层的 jth 神经元的权重。这种 j 和 k 索引的顺序可能看着奇怪 —— 交换j 和 k 索引会更有意义,确定吗?使用这种顺序的很大的优势是它意味着第三层神经元的激活 向量是:
a′ = σ(wa + b) (22)
这个方程有点奇怪,所以让我们一块一块地理解它。a 是第二层神经元的激活向量。为了得到 a′,我们用权重矩阵 w 乘以 a,加上偏置向量 b,我们然后对向量 wa + b 中的每个元素应用函数 σ。(这称为将函数 σ 向量化。)很容易验证方程 (22) 的结果和我们之前的计算一个 S 型神经元输出的方程 (4) 相同。
练习
• 以分量形式写出方程 (22),并验证它和计算S 型神经元输出的规则 (4) 结果相同。 有了这些,很容易写出从一个 Network 实例计算输出的代码。我们从定义 S 型函数开始:
def sigmoid(z):
return 1.0/(1.0+np.exp(-z))
注意,当输入 z 是一个向量或者 Numpy 数组时,Numpy 自动地按元素应用 sigmoid 函数, 即以向量形式。
我们然后对 Network 类添加一个 feedforward 方法,对于网络给定一个输入 a,返回对应的输出。这个方法所做的是对每一层应用方程 (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) 元组的列表,表示训练输入和其对应的期望输出。变量 epochs和 mini_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.weights 和 self.biases。
我现在不会列出 self.backprop 的代码。我们将在下章中学习反向传播是怎样工作的,包括self.backprop 的代码。现在,就假设它按照我们要求的工作,返回与训练样本 x 相关代价的适 当梯度。
让我们看一下完整的程序,包括我之前忽略的文档注释。除了 self.backprop,程序已经有了 足够的文档注释 —— 所有的繁重工作由 self.SGD 和 self.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 adef 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]# feedforwardactivation = x |
activations = [x] # list to store all the activations, layer by layerzs = [] # 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,学习速率 η = 3.0,
>>> 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%。至少在这种情况下,使用更多的隐藏神经元帮助我们得到 了更好的结果。
当然,为了获得这些准确性,我不得不对训练的迭代期数量,小批量数据大小和学习速率 η做特别的选择。正如我上面所提到的,这些在我们的神经网络中被称为超参数,以区别于通过 我们的学习算法所学到的参数(权重和偏置)。如果我们选择了糟糕的超参数,我们会得到较差 的结果。假如我们选定学习速率为 η = 0.001,
>>> 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 |
然而,你可以看到网络的性能随着时间的推移慢慢地变好了。这表明应该增大 学习速率, 例如 η = 0.01。如果我们那样做了,我们会得到更好的结果,这表明我们应该再次增加学习速 率。(如果改变能够改善一些事情,试着做更多!)如果我们这样做几次,我们最终会得到一个像 η = 1.0 的学习速率(或者调整到 3.0),这跟我们之前的实验很接近。因此即使我们最初选择了 糟糕的超参数,我们至少获得了足够的信息来帮助我们改善对于超参数的选择。
通常,调试一个神经网络是具有挑战性的。尤其是当初始的超参数的选择产生的结果还不如 随机噪点的时候。假如我们使用之前成功的具有 30 个隐藏神经元的网络结构,但是学习速率改 为 η = 100.0:
>>> 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 ndarry 对象的列表(如果你不熟悉 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 libraryimport 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.0return e |
上面我说过我们的程序取得了非常好的结果。那意味着什么?和什么相比算好?如果有一些 简单的(非神经网络的)基线测试作为对比就有助于理解它怎样算运行良好。最简单的基线,当 然是随机地猜些数字。那将有 10% 的次数是正确的。我们将比这做得更好!
一个较差的基线会怎样?让我们尝试一种极其简单的想法:我们会看一幅图像有多暗。例如, 一幅 2 的图像通常要比一幅 1 的图像稍暗些,仅仅因为更多像素被涂黑了,就像下面的示例显示的:
这提示我们可以用训练数据来计算数字图像的平均暗度,0, 1, 2, . . . , 9。当有一幅新的图像呈 现,我们先计算图像的暗度,然后猜测它接近哪个数字的平均暗度。这是一个简单的程序,而 且容易编写代码,所以我不会在这里把它们都写出来 —— 如果你有兴趣,代码在 GitHub 仓 库里。但是它和随机地猜测相比有了很大的改进,能取得 10, 000 测试图像中 2, 225 的精确度, 即 22.25%。
找到其它能使精确度达到 20% 到 50% 之间的办法也不难。如果你更努力些能超过 50%。但 要获得更高的精确度,采用已经被认可的机器学习算法是很有帮助的。让我们尝试使用其中最 著名的算法之一,支持向量机,或 SVM。如果你不熟悉 SVM,不用担心,我们不需要去理解SVM 如何工作的细节。我们将使用 scikit-learn Python 程序库,它提供了一个简单的 Python接口,包装了一个用于 SVM 的快速的,称为 LIBSVM 的 C 库。
如果我们用默认设置运行 scikit-learn 的 SVM 分类器,那么它能从 10, 000 测试图像中准确 分类 9, 435。(代码可以从这里取得)。那是一个很大的改善,远远好于我们幼稚的基于暗度的图 像分类方法。确实,这意味着 SVM 表现得几乎和神经网络一样好,只是差了一点而已。在后面 章节中我们会介绍新的技术,让我们能够改进我们的神经网络使得它们表现得比 SVM 更好。
这不是故事的结局,然而,10,000 中 9,435 的结果是 scikit-learn 针对 SVM 默认的设置。SVM 有很多可调参数,查找到可以改善默认情况下的性能的参数是可能的。我不会明确地做这 些查找,如果你想知道更多,可以参考这份 Andreas Mueller 的博客。Mueller 展示了通过一些 优化 SVM 参数的工作,有可能把性能提高到 98.5% 的精确度。换句话说,一个调整好的 SVM,70 次里只会识别错一次数字。那已经非常好了!神经网络能做得更好吗?
事实上,它们可以。目前,精心设计的神经网络胜过任何其它解决 MNIST 的技术,包括SVM。现在(2013)的纪录是从 10, 000 图像中正确分类 9, 979 个。这是由 Li Wan,Matthew Zeiler,Sixin Zhang,Yann LeCun,和 Rob Fergus 完成的。我们将在这本书后面看到它们用的 大部分技术。那个层次的性能接近于人类,而且可以说更好,因为相当多的 MNIST 图像甚至 对人类来说都很难有信心识别,例如:
我相信你会同意那些数字很难辨认!考虑到 MNIST 数据集中这样的图像,神经网络能准确 识别 10, 000 幅测试图像中除了 21 幅之外的其它所有图像,这表现得相当卓越。通常,当编程 时我们相信解决一个类似识别 MNIST 数字的问题需要一个复杂的算法。但是即使是刚才提到 的 Wan 等人的论文中用的神经网络,只涉及到相当简单的算法、和我们在这一章中已经看到的 算法的变化形式。所有的复杂性自动从训练数据学习。在某种意义上,我们的结果和那些在更深奥的论文中都有的寓意是,对有些问题:
复杂的算法 ≤ 简单的学习算法 + 好的训练数据