《神经网络和深度学习》系列文章七:实现我们的神经网络来分类数字(上)

出处: Michael Nielsen的《Neural Network and Deep Learning》,点击末尾“阅读原文”即可查看英文原文。

本节译者:哈工大SCIR硕士生 邓文超 (https://github.com/dengwc1993)

声明:我们将在每周一,周四,周日定期连载该书的中文翻译,如需转载请联系wechat_editors@ir.hit.edu.cn,未经授权不得转载。


  1. 使用神经网络识别手写数字

    • 感知机

    • sigmoid神经元

    • 神经网络的结构

    • 用简单的网络结构解决手写数字识别

    • 通过梯度下降法学习参数

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

    • 关于深度学习

  2. 反向传播算法是如何工作的

  3. 改进神经网络的学习方法

  4. 神经网络能够计算任意函数的视觉证明

  5. 为什么深度神经网络的训练是困难的

  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个图像的验证集(validation set)。在本章中我们不使用验证数据,但是在本书的后面我们将会发现它对于解决如何去设置神经网络中的超参数(hyper-parameter)——例如学习率等不是被我们的学习算法直接选择的参数——是很有用的。尽管验证数据集不是原始MNIST规范的一部分,然而许多人使用以这种方式使用MNIST,并且在神经网络中使用验证数据是很常见的。当我从现在起提到「MNIST训练数据」,我指的不是原始的60,000图像数据集,而是我们的50,000图像数据集1

1 如前所述,MNIST数据集是基于NIST(美国国家标准与技术研究院)收集的两个数据集合。为了构建MNIST,NIST数据集合被Yann LeCun,Corinna Cortes和Christopher J. C. Burges拆分放入一个更方便的格式。更多细节请看这个链接。我的仓库中的数据集是在一种更容易在Python中加载和操纵MNIST数据的形式。我从蒙特利尔大学的LISA机器学习实验室获得了这个特殊格式的数据(链接)。

除了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是第二层神经元的激活向量。为了得到a′,我们用权重矩阵w乘以a,加上偏差向量b,我们然后对向量wa+b中的每个元素应用函数σ。(这被叫做函数σ的向量化(vectorizing)。)很容易验证等式(22)给出了跟我们之前的计算一个sigmoid神经元的输出的等式(4)的结果相同。

练习

写出等式(22)的组成形式,并验证它跟我们之前的计算一个sigmoid神经元的输出的等式(4)的结果相同。 有了这些,很容易写从一个Network实例计算输出的代码。我们从定义sigmoid函数开始:

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

注意,当输入z是一个向量或者Numpy数组时,Numpy自动的应用元素级的sigmoid函数,也就是向量化。

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

2 假定输入a时Numpy的n维数组(n,1),而不是向量(n,)。这里,n是输入到网络的数目。如果你试着用一个向量(n,)作为输入,将会得到奇怪的结果。虽然使用向量(n,)看上去好像是更自然的选择,但是使用n维数组(n,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

当然,我们想要我们的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)

trainingdata是一个代表着训练输入和对应的期望输出的元组(x,y)的列表。变量epochs和minibatchsize是你期望的训练的迭代次数和取样时所用的mini-batch块的大小。eta是学习率η。如果可选参数testdata被提供,那么程序将会在每次训练迭代之后评价网络,并输出我们的局部进展。这对于跟踪进展是有用的,但是大幅度降低速度。

代码如下工作。在每次迭代,它首先随机的将训练数据打乱,然后将它分成适当大小的迷你块。这是一个简单的从训练数据的随机采样方法。然后对于每一个minibatch我们应用一次梯度下降。这是通过代码self.updateminibatch(minibatch, eta)做的,仅仅使用minibatch上的训练数据,根据单轮梯度下降更新网络的权重和偏差。这是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)]

大部分工作通过这行所做

deltanablab, deltanablaw = self.backprop(x, y)

这行调用了一个叫做叫反向传播(backpropagation)的算法,这是一种快速计算代价函数的梯度的方法。因此update_minibatch的工作仅仅是对minibatch中的每一个训练样例计算梯度,然后适当的更新self.weights和self.biases。

我现在不会展示self.backprop的代码。我们将在下章中学习反向传播是怎样工作的,包括self.backprop的代码。现在,就假设它可以如此工作,返回与训练样例x相关代价的适当梯度。

让我们看一下完整的程序,包括我之前忽略的文档注释。除了self.backprop,程序是不需加以说明的。我们已经讨论过,所有重要的部分都在self.SGD和self.updateminibatch中完成。self.backprop方法利用一些额外的函数来帮助计算梯度,也就是说sigmoidprime是计算σ函数的导数,而我不会在这里描述self.costderivative。你能够通过查看代码或文档注释来获得这些的要点(以及细节)。我们将在下章详细的看一下它们。注意,虽然程序显得很长,但是大部分代码是用来使代码更容易理解的文档注释。实际上,程序只包含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 npclass 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 thatthe 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, similarto ``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 functionsdef 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

>>> trainingdata, validationdata, testdata = \ ... mnistloader.loaddatawrapper()

当然,这也可以被做成一个单独的Python程序,但在Python shell执行最方便。 在加载完MNIST数据之后,我们将设置一个有30个隐层神经元的Network。我们在导入如上所列的名字为network的Python程序后做,

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

最后,我们将使用随机梯度下降来从MNISTtraining_data学习超过30次迭代,迷你块大小为10,学习率η=3.0,

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

注意,如果当你阅读至此的时候正在运行代码,执行将会花费一些时间,对于一个普通的机器(截至2015年),它可能将会花费几分钟来运行。我建议你让它运行,继续阅读并定期的检查一下代码的输出。如果你赶时间,你可以通过减少迭代次数,减少隐层神经元次数或仅使用部分训练数据来提高速度。注意,这样产生的代码将会特别快:这些Python脚本的目的是帮助你理解神经网络是如何工作的,而不是高性能的代码!而且,当然,一旦我们已经训练一个网络,它能在几乎任何的计算平台上快速的运行。例如,一旦我们对于一个网络学会了一组好的权重集和偏置集,它能很容易的移植到web浏览器中以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(trainingdata, 30, 10, 3.0, testdata=test_data)

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

3 读者的反馈表明本实验在结果上有相当多的变化,而且一些训练运行给出的结果相当糟糕。使用第三章所介绍的技术将对于我们的网络在不同的训练执行上大大减少性能变化。

当然,为了获得这些正确率,我不得不对于训练的迭代次数,mini-batch大小和学习率η做了特殊的选择。正如我上面所提到的,这些在我们的神经网络中被称为超参数,以区别于通过我们的学习算法所学到的参数(权重和偏置)。如果我们较差的选择了超参数,我们会得到较差的结果。假设,例如我们选定学习率为η=0.001,

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

>>> net.SGD(trainingdata, 30, 10, 0.001, testdata=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(trainingdata, 30, 10, 100.0, testdata=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

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

从这得到的教训是调试一个神经网络是一个琐碎的工作,就像日常的编程一样,它是一门艺术。你需要学习调试的艺术来获得神经网络更好的结果。更普通的是,我们需要启发式方法来选择好的超参数和好的结构。所有关于这些的内容,我们都会在本书中进行讨论,包括我之前是怎么样选择超参数的。

下一节我们将介绍“关于深度学习”,敬请关注!

 


    • “哈工大SCIR”公众号

    • 编辑部:郭江,李家琦,徐俊,李忠阳,俞霖霖

    • 本期编辑:俞霖霖

posted on 2015-11-17 12:13  cv_ml_张欣男  阅读(1575)  评论(0编辑  收藏  举报

导航