[神经网络这次真的搞懂了!] (5) 使用神经网络识别手写数字 - 手写神经网络

英文原文:http://neuralnetworksanddeeplearning.com/
对原文的表达有部分改动

让我们编写一个程序来使神经网络模型学习如何识别手写数字,这里会用到我们已经介绍过的随机梯度下降和 MNIST 训练数据。我们将使用一个简短的 Python 程序来完成这项工作,我们需要做的第一件事是获取 MNIST 数据。代码下载方式如下:

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

顺便说一句,之前描述 MNIST 数据集时,我们描述它被分成了 60,000 张训练图像和 10,000 张测试图像,这是 MNIST 的官方描述。实际上,我们将以稍微不同的方式拆分数据。我们将保留测试图像,但将 60,000 张图像的 MNIST 训练集将分成两部分:一组 50,000 张图像的训练集(Train Set),以及一组 10,000 张图像的验证集(Validation Set)。我们暂不会在本节中使用验证数据,但在本系列的后面我们会发现它在弄清楚如何设置神经网络的某些超参数(hyper-parameters)时很有用(比如学习率,这些一般不是由我们的学习算法直接得出的)。尽管验证数据集不是原始 MNIST 规范的一部分(MNIST 没有定义某个10000的验证数据集),但许多人以这种方式使用 MNIST,并且验证数据集的使用在神经网络中很常见。从现在开始,当我提到“MNIST 训练数据”时,我将指的是我们的 50,000 个图像的训练数据集(Train Set),而不是原始的 60,000 个图像的数据集。

除了 MNIST 数据,我们还需要一个名为 Numpy 的 Python 库,用于进行快速的线性代数相关计算。

在给出完整代码之前,让我解释一下神经网络代码的核心类 -- 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 个神经元,那我们将执行以下代码:

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

Network 对象中的 weights 和 biases 都是随机初始化的,使用 np.random.randn 函数生成均值为 0 、标准差为 1 的高斯分布集合。 这种随机初始化为随机梯度下降算法提供了一个起点。在后面的章节中,我们将找到初始化权重和偏差更好的方法,现在暂且使用这种方法。请注意,网络初始化代码假定第一层神经元是输入层,并省略为这些神经元设置任何偏差,因为偏差仅用于计算后面层的输出。

此外,偏差和权重都被存储为 Numpy 的矩阵列表。例如 net.weights[1] 是一个 Numpy 矩阵,存储连接第二层和第三层神经元的权重。 (它不是第一层和第二层,因为 Python 的列表索引是从 0 开始的)由于 net.weights[1] 相当冗长,我们只表示它的矩阵 。 是第二层第 个神经元和第三层 个神经元之间连接的权重。第三层神经元的激活向量:

其中, 是第二层神经元的激活向量。为了获得 ,我们将 乘以(矩阵点乘)权重矩阵 ,并添加偏置向量 。然后我们将函数 逐元素应用于向量 中的每一个。 (这称为 vectorizing the function 。)方程给出了与我们之前的规则方程相同的结果,它用于计算 sigmoid 神经元的输出。

使用向量化编写计算 Network 实例输出的代码很容易。我们首先定义 sigmoid 函数:

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

请注意,当输入 z 是向量或 Numpy 数组时,Numpy 会自动使用 sigmoid 处理各个元素。

然后我们在 Network 类中添加一个前馈(feedforward)方法,该方法给定网络的输入 ,返回相应的输出每一层的方程。(这里假设输入 是一个 shape
为的 ndarray,而不是一个 向量。这里, 是网络的输入数量。如果您尝试使用 向量作为输入,您会得到奇怪的结果。尽管使用 向量似乎是更自然的选择,但使用 的 ndarray 可以特别轻松地修改代码以一次前馈多个输入,这很方便):

    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)

程序识别手写数字的能力如何?好吧,让我们从加载 MNIST 数据开始。我将使用一个小程序 mnist_loader.py 来完成此操作,如下所述。我们可以在 Python shell 中执行以下命令(也可以在程序文件中):

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

加载 MNIST 数据后,我们将建立一个具有 30 个隐藏神经元的网络。我们在导入上面列出的名为 Network 的类后执行此操作:

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

最后,我们将使用随机梯度下降从 MNIST 的 training_data 中学习超过 30 个 epoch,mini-batch 大小为 10,学习率为 ,

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

如果您赶时间,可以通过减少 epoch 大小、减少隐藏神经元的数量或仅使用部分训练数据来加快速度。请注意,这些 Python 脚本旨在帮助您了解神经网络的工作原理,它们并不是高性能代码!当然,一旦我们训练了一个网络,它确实可以非常快速,几乎可以在任何计算平台上运行。例如,一旦我们为网络学习了一组好的权重和偏差,就可以轻松地将其移植到 Web 浏览器中的 Javascript 中运行,或作为移动设备上的本机应用程序运行。正如下方你所看到的,在仅仅一个 epoch 之后,这个数字就达到了 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

也就是说,经过训练的网络在其峰值(“Epoch 28”)时为我们提供了大约95.42% 的成功识别率!作为第一次尝试,这是非常令人鼓舞的。但是,我应该警告您,如果您运行代码,那么您的结果不一定会与我的完全相同,因为我们将使用(不同的)随机权重和偏差来初始化我们的网络。为了在本章中生成结果,我进行了三次的运行。

让我们重新运行上面的实验,将隐藏神经元的数量更改为 100。这可能需要更长的时间。

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

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

当然,为了获得这些准确度,我必须对训练周期数、mini-batch大小和学习率 做出具体选择。正如我上面提到的,这些被称为我们神经网络的超参数(hyper-parameters),以便将它们与我们的神经网络的参数(权重和偏差)区分开。如果我们选择的超参数不当,我们可能会得到糟糕的结果。例如,假设我们选择的学习率为 ,

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

但是,您可以看到网络的性能随着时间的推移慢慢变好。这表明提高学习率,比如 ,我们会得到更好的结果。(如果做出改变可以改善事情,尝试做更多!)如果我们这样做几次,我们最终会得到类似于 的学习率(也许可以微调到 3.0),这与我们之前的实验很接近。由此可见,即使我们最初对超参数的选择很糟糕,但我们至少获得了足够的信息来帮助我们改进超参数。

通常,调试神经网络可能具有挑战性。当超参数的初始选择产生的结果并不比随机噪声好时,尤其如此。假设我们尝试之前成功的 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

当然,我们从之前的实验中知道,正确的做法是降低学习率。但是,如果我们是第一次遇到这个问题,我们可能不仅要担心学习率,还要担心神经网络的其他方面。我们可能想知道我们是否以一种使网络难以学习的方式初始化权重和偏差?或者我们可能没有足够的训练数据来获得有意义的学习?也许我们还没有运行足够的迭代?或者,这种架构的神经网络无法学会识别手写数字?也许学习率太低?或者,学习率太高了?当您第一次遇到问题时,您无法立刻察觉到问题的原因。

调试神经网络并不简单,就像普通编程一样,它是一门艺术。你需要学习调试的艺术才能从神经网络中获得好的结果。

之前,我跳过了有关如何加载 MNIST 数据的详细代码。这很简单。为了完整起见,这里是代码:

"""
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

我们的程序得到了很好的结果。比什么好?需要有一些简单的(非神经网络)基线测试来比较,以了解表现良好意味着什么。当然,最简单的基线是随机猜测数字。大约百分之十的可能性是正确的。我们做得比这好得多了。

还有更好的基线吗?让我们尝试一个非常简单的想法:我们将看看图像有多“暗”。例如,2 的图像通常比 1 的图像暗很多(因为更多的像素被涂黑),如下例所示:


这建议使用训练数据来计算每个数字的平均暗度,。当呈现新图像时,我们计算图像的暗度,然后猜测它是哪个数字具有最接近的平均暗度。这是一个简单的过程,很容易编写代码。但它比随机猜测有了很大的改进,在 10,000 张测试图像中得到 2,225 张正确,即 22.25% 的准确率。

"""
mnist_average_darkness
~~~~~~~~~~~~~~~~~~~~~~

A naive classifier for recognizing handwritten digits from the MNIST
data set.  The program classifies digits based on how dark they are
--- the idea is that digits like "1" tend to be less dark than digits
like "8", simply because the latter has a more complex shape.  When
shown an image the classifier returns whichever digit in the training
data had the closest average darkness.

The program works in two steps: first it trains the classifier, and
then it applies the classifier to the MNIST test data to see how many
digits are correctly classified.

Needless to say, this isn't a very good way of recognizing handwritten
digits!  Still, it's useful to show what sort of performance we get
from naive ideas."""

#### Libraries
# Standard library
from collections import defaultdict

# My libraries
import mnist_loader

def main():
    training_data, validation_data, test_data = mnist_loader.load_data()
    # training phase: compute the average darknesses for each digit,
    # based on the training data
    avgs = avg_darknesses(training_data)
    # testing phase: see how many of the test images are classified
    # correctly
    num_correct = sum(int(guess_digit(image, avgs) == digit)
                      for image, digit in zip(test_data[0], test_data[1]))
    print("Baseline classifier using average darkness of image.")
    print("{0} of {1} values correct.".format(num_correct, len(test_data[1])))

def avg_darknesses(training_data):
    """ Return a defaultdict whose keys are the digits 0 through 9.
    For each digit we compute a value which is the average darkness of
    training images containing that digit.  The darkness for any
    particular image is just the sum of the darknesses for each pixel."""
    digit_counts = defaultdict(int)
    darknesses = defaultdict(float)
    for image, digit in zip(training_data[0], training_data[1]):
        digit_counts[digit] += 1
        darknesses[digit] += sum(image)
    avgs = defaultdict(float)
    for digit, n in digit_counts.items():
        avgs[digit] = darknesses[digit] / n
    return avgs

def guess_digit(image, avgs):
    """Return the digit whose average darkness in the training data is
    closest to the darkness of ``image``.  Note that ``avgs`` is
    assumed to be a defaultdict whose keys are 0...9, and whose values
    are the corresponding average darknesses across the training data."""
    darkness = sum(image)
    distances = {k: abs(v-darkness) for k, v in avgs.items()}
    return min(distances, key=distances.get)

if __name__ == "__main__":
    main()

不难找到在 20 到 50% 准确率范围内的其他方法。如果你再努力一点,你可以提高 50% 以上。但是为了获得更高的准确度,使用成熟的机器学习算法是有帮助的。让我们尝试使用最著名的算法之一,SVM 或 支持向量机。如果您不熟悉 SVM,不用担心,我们不需要了解 SVM 工作原理的细节。相反,我们将使用一个名为 scikit-learn 的 Python 库。

如果我们使用默认设置运行 scikit-learn 的 SVM 分类器,那么 10,000 个测试图像中的 9,435 个是正确的。事实上,这意味着 SVM 的表现大致与我们的神经网络一样好。在后面的章节中,我们将介绍新技术,使我们能够改进我们的神经网络,使其性能比 SVM 好得多。

然而还没结束。 上述 94.35%是 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 图像,例如:


image.png

我相信你也会同意这些图片很难分类!在编程时,通常我们认为解决像识别 MNIST 数字这样的复杂问题需要复杂的算法。即使是刚刚提到的 Li Wan 等人论文中的神经网络也只涉及非常简单的算法,即我们在本章中看到的算法的变体。所有的复杂性都是从训练数据中自动学习的。

你可能感兴趣的:([神经网络这次真的搞懂了!] (5) 使用神经网络识别手写数字 - 手写神经网络)