神经网络与深度学习

0.简介

(1)神经网络,一种美丽的,受生物启发的编程范例,使计算机能够从观测数据中学习
(2)深度学习,用于在神经网络中学习的强大功能集

神经网络和深度学习在为图片,语音识别和自然语言处理中的许多问题提供了解决方法,本文主要内容为神经网络和深度学习的入门书籍,以手写数字识别作为例子。
本文的代码会在Github上展示:???
首先,这是面向新手的入门课程,主要是讲解神经网络和深度学习的原理,所以,代码中并没有使用框架,而是要自己编写,为了易读性,代码并没有进行优化。
其环境为:???

章节划分:
1.使用神经网络识别手写数字
2.反向传播如何工作
3.改善神经网络的学习方式

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


人类可以毫不费力地识别手写数字,但是,这个过程并不简单。
在我们大脑中,人类有一个主要的视觉皮层,也称为V1,包含1.4亿个神经元,它们之间有数百亿个连接。然而,人类的视觉不仅涉及V1,还涉及整个视觉皮层V2,V3,V4和V5逐渐进行更加复杂的图像处理。

人类很难通过编写一个程序来表达数字的特征,比如说,9上面是一个圆,下面是一个竖。那么,神经网络用不同的方法来处理问题:
采用大量的手写数字作为样例,然后开发一个可以从这些手写数字中学习的系统。也就是,神经网络通过样例自动推断出识别手写数字的规则,同时通过增加样例的数量,网络可以提高其准确性。

我们在这个部分将编写程序,来实现学习识别手写数字的神经网络,之后的章节会对其进行优化,来提高网络的准确度。当然,本章中会先引入神经网络的概念。

1.1基本概念

这一个章节包含以下的内容:
1.1.1 感知器神经元
1.1.2 S型神经元
1.1.3 神经网络的架构
1.1.4 梯度下降学习

1.1.1 感知器神经元

感知器件是由科学家在20世纪50年代开发,如今,所使用的神经元模型是一种叫做S型神经元的模型。但是,在讲解S型神经元之前需要了解感知器。

感知器需要几个二进制输入x1,x2等并产生单个二进制输出。

在实例中,感知器有三个输入x1,x2,x3输出为output。其规则很简单,可以用下面的公式来表示:

因此,可以将感知器看作是通过权衡来作出决策的过程。w1,w2,w3是表示各个输入对输出的重要性,神经元的输出是0或者是1,取决于加权和是否大于规定的阈值,这就是感知器工作原理。

在这个网络中,第一列,被称为感知器网络的地一层,通过权衡输入来作出三个决策;然后,通过第二层感知器,通过权衡第一层的结果来作出决策。这样,没一层的内容都更加复杂。

感知器只有一个输出,多个输出箭头仅仅表示单个输出作用与多个感知器的输入。然后,我们对之前的感知器公式进行优化:

其中,b为偏差,之后,都会使用偏差,权重来表示。

1.1.2 S型神经元

上面的网络似乎已经可以实现神经网络,但是我们如何为神经网络设计算法呢?假如,我们有一个感知器网络,输入为图片的原始像素数据,然后我们需要学习网络中的权重和偏置,使网络能够对图片中的数字进行正确的分类。为了了解学习的过程,我们对网络中的某些权重或者偏置进行微小的更改,从而导致网络的输出有微小的变化,这样,就会使得学习成为可能。

如果,权重或者偏置的微小变化仅仅导致输出的微小变化,那么我们就可以通过这个事实来修改权重和偏置,使得输出结果越来越接近真实的输出。

但是,当我们使用这个感知器网络的时候,网络中单个感知器的权重和偏置的微小的变化,都有可能导致感知器产生翻转,例如从0变为1,这样就会使得整个网络都发生一些非常复杂的改变,从而无法简单控制权重和偏置就使得网络趋向于自己想要的输出。

因此,感知器网络的翻转使其不适合,因此引入了另一种神经元S型神经元。
S型神经元的结构和感知器相同,但是,其输入和输出的范围改变了,介于0和1之间,例如0.4。下面是其结构与公式:


其中,z为:

看起来,S型神经元和感知器完全不同,但是,S型神经元的公式更多的是技术细节,σ函数的公式不是那么重要,主要看图:

上面是S型神经元的Sigmoid激活函数图像,下面是感知器的激活函数,就是一个阶跃函数,Sigmoid激活函数就是阶跃函数的平滑版本。

通过Sigmoid激活函数,权重和偏置的微小的变化就会导致输出的微小变化,而不会发生翻转。

从上面的公式中可以看到,输出的变化是权重和偏置的变化的线性函数,这就使得了解权重以及偏置对输出的改变关系更加容易。

那么,我们应该如何解释S型神经元的输出呢?例如,这个图片中是9的输出为0.6,0.6就代表概率

1.1.3 神经网络的架构


网络中最左边为输入层,该层的神经元为输入神经元。
网络中最右边为输出层,该层的神经元为输出神经元。
中间为隐藏层,表示既不是输入也不是输出,隐藏层的数目可以为1,也可以为多层。
当隐藏层的数目大于1时,这种多层网络被称为多层感受器MLP,但是,其实它是由S型神经元而不是感知器组成。

网络中输入和输出层的设计通常很简单。例如,假设我们正在尝试确定手写图像是否描绘了“9”。设计网络的方法是将图像像素的强度输入输入神经元。如果图像是64*64的灰度图像,那么我们将有4096=64×64个输入神经元,其强度在0和 1之间。输出层将仅包含单个神经元,输出值小于0.5表示“输入图像不是9”,值大于0.5表示“输入图像为9”。

虽然输入层和输出层的设计通常比较简单,但是隐藏层的设计无法通过一些简单的经验法则来总结隐藏层的设计过程。
相反,神经网络研究人员为隐藏层开发了许多设计启发方法,可以帮助人们从网络中获得所需要的。例如这种试探方法可以帮助确定如何权衡隐藏层的数量和训练网络所需要的时间。

到现在为止,我们仅仅讨论了一种神经网络,其中一层的输出作用于下一层的输入,这种网络被称为前馈网络,这意味着网络中没有环路,信息总是前馈,没有反馈。

当然,还有其他人工神经网络模型,其中可能存在反馈回路,这些模型被称为递归神经网络。这些模型就是在神经元稳定之前激发有限的时间,然后刺激其他神经元,这些神经元会在之后激发,同样持续有限的时间,导致更多神经元的激发,我们会得到一连串的神经元出发。这种网络不会存在问题,因为,神经元的输出只会在以后的某个时间影响输入,而不是同时影响输入。

递归神经网络非常有趣,和前馈网络相比,这种网络更加接近大脑的工作方式。但是,我们整个文章都仅仅关注前馈网络。

说了这么多,手写数字识别的网络结构选择如下:

网络的输入层是输入原始图像的灰度像素值,我们的手写数字图像为28*28,所以输入层为28×28=784个神经元。0代表白色,1代表黑色。两者之间的数值代表灰色。

第二层为隐藏层,我们尝试使用不同的隐藏层的神经元数量,设计隐藏层中仅仅包含15个神经元。

网络的输出层包含10个神经元,如果第一个神经元被触发,即输出接近1,则表明网络认为该数字为0。同理,第二个神经元触发,则网络认为该数字为1等等。我们对输出神经元进行编号从0到9,找到哪个神经元有最高的激活值。

因为手写数字的范围是0-9,所以输出层的编号也为0-9,有10个输出神经元。

1.1.4 梯度下降学习

我们在上面已经设计了一个神经网络,那么我们该如何学习识别数字呢?
我们需要做的第一件事就是学习的数据集,即训练数据集。在这里,我们将使用MNIST数据集,其中包含手写数字扫描图像和其分类。

MNIST数据分为两部分,第一部分是包含60000张图像用作训练数据。这些图像是来自250个人的扫描笔迹样本,其中一半是美国人口普查局的雇员,另一半是高中学生。图像为灰度图像,尺寸为28x28像素。MNIST数据集的第二部分是10000张要用作测试数据的图像。同样,这些是28x28的灰度图像。我们将使用测试数据来评估我们的神经网络学会识别数字的程度。为了对性能进行良好的测试,测试数据取自与之前部分不同的人的组。这使我们有信心,我们的系统可以识别各种人写的手写数字。

我们使用X代表训练输入,是一个维度为784的向量。向量中的每一个数值都代表了图片中的灰度数值。我们使用Y作为输出,是一个10维的向量。
如果,Y=(0,0,0,0,0,0,1,0,0,0)^(T)那么就表示图片为6。T是指对矩阵进行转置。

接着,我们需要一个算法,可以找到权重和偏置,以便网络对于所有输入X的近似输出Y接近目标。为了量化其实现目标的效果,我们定义了成本函数:

其中,w是网络中所有的权重,b是网络中所有的偏置,n是训练的总数,x是输入,||v||表示了向量v的长度,a为实际情况的结果。我们可以看出C(w,b)是非负的,我们有时候可以称其为均方误差或者MSE。

当我们的预测Y接近于实际情况,上面的成本函数趋向于0。所以,训练算法的目标就是找出C(w,b)最小时的权重和偏置。为此,我们需要梯度下降算法。

为什么我们不直接尝试将网络正确分类的图的个数作为指标呢?因为正确分类的图的个数对于权重和偏置而言不是光滑函数,大多数情况下,权重和偏置的微小的改变并不会改变正确分类的个数,这使得很难弄清楚如何更改权重和偏差以提高性能。所以,我们选择用了成本函数作为标准。在此之后,我们才会检查分类的准确性。

其次,成本函数不仅仅是均方差函数,还有其他的成本函数,因为这个成本函数容易理解,我们在这里使用。

现在,我们将问题进行简化,存在函数C(v),v是包含任意多的变量:v1,v2等等。现在先考虑C函数仅仅包含两个变量v1,v2的情况:

我们可以找到C上的最低点,那么,在复杂的多变量函数中,我们该如何找到最小值呢。

我们可以计算导数,然后用它们找到极值,但是,当我们有很多的变量的时候,这个会有非常大的计算量,这个方法是行不通的。

然后,我们就运用了梯度下降的方法,首先,我们知道,梯度的方向始终是指向函数值上升最快的方向,那么,它的反方向就是函数值下降最快的方向,因此,我们先随机确定一个点,然后只要对C函数求各个变量上的偏导,就得到梯度,然后,将这个点向梯度的反方向移动,那么数值就会变小,然后继续对变量求偏导,然后向梯度的反方向移动,不断重复这个过程,最终会到达最低点。

我们可以类比作一个山,我们随机在山上的某一处放置小球,然后,小球会向坡度最大的方向移动,最后会到山谷。

为了将这个问题更加具体,我们需要下面的知识:

上面ΔC是函数的变化量,ΔV1和ΔV2分别是变量V1和V2的变化量,也可以理解为在V1和V2方向的变化量,剩下的是偏导。
为了表示方便,我们使用梯度向量∇C,里边将偏导放在一起:

然后,我们将上面的两个公式结合,可以得到:

我们可以看出∇C是表示v的变化量和C的变换量之间的关系。
知道了上面的知识之后,我们应该如何选择Δv来使ΔC为负呢?

在上面的公式中,η是一个小的正参数,被称为学习速率。这样:

ΔC≤0,C就会始终减小。

变量的位置的改变可以看上面的公式,通过这个,不断改变变量,使C持续降低,直到达到全局最低点(这是我们所希望的,但是,实际上这是局部最低点,不过在实际应用中效果很好)

为了使梯度下降的过程顺利,我们需要将学习速率尽可能降低,但是,学习速率降低之后,下降的速度会减慢。上面只是两个变量时候的情况,在多个变量的时候这个方法仍然有效。

返回到神经网络中,我们需要注意到,成本函数是和每个训练数据有关。

它是所有训练数据的成本函数的均值,下面是每个训练数据的成本函数:

我们需要分别计算出每个训练数据的梯度然后求平均,这就会有大量的计算。我们对其进行优化,建立了随机梯度下降的方法。

随机梯度下降是为了能够加快学习。其主要方法是随机选取少量样本来计算梯度然后取平均值作为梯度,从而不需要计算所有数据的梯度然后求平均来作为梯度。这样就加快了梯度下降的速度。

我们将随机训练输入分为多个小批量,当每个批量的样本数目足够大,那么,批量样本梯度的平均值就大致等于所有样本梯度的平均值。

将这个随机梯度下降算法和神经网络中的学习过程联系到一起:

我们选择一部分样本进行梯度计算,然后修改权重,然后,我们选择另一部分样本进行梯度计算,然后修改权重,直到用完所有的训练数据。这个被称为是训练的一个阶段,然后,我们开始训练的新的阶段,每个阶段都是重复上面的过程。

1.2 使用网络对数字进行分类

了解了上面的一些概念,我们开始写程序,对数字进行分类。
该程序使用了随机梯度下降算法和MNIST训练数据来学习如何识别手写数字。
程序可以查找一开始的程序Git库。

MNIST官方描述,将其分为了60,000个训练图像和10,000个测试图像。但是在实际情况下,我们会对数据进行拆分,测试图像不变,我们将60,000张训练图像分为50,000张训练图像和10,000张验证图像。本部分不会使用验证数据集,但是在后面,我们需要使用验证数据,因为有助于我们弄清楚如何设置某些超参数。

所以,我们将会使用50,000个图像作为训练参数,而不是原始的60,000个图像。

我们需要使用Numpy的Python库来进行线性代数的运算。

我们开始核心代码的讲解,首先核心是Network类,我们用它来表示神经网络,首先初始化Network对象:

1
2
3
4
5
6
7
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:]]
slef.weights = [np.random.randn(y,x)
for x,y in zip(sizes[:-1],sizes[1:])]

上面的代码中,sizes列表包含了各个层中的神经元的数目。例如,我们希望建立一个第一层有两个神经元,第二层有3个神经元,最后一层有一个神经元的网络。我们输入以下代码:

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

在上面的代码中,我觉得有必要对self.biases和self.weights进行一些说明:
神经元层数和每层神经元的标号分别从0开始。

self.bias[l][j]:第l层第j个神经元的偏置

self.weight[l][j,k]:从第(l-1)层的第k个神经元到第l层的第j个神经元的权重。

a[l][j]:第l层的第j个神经元的激活结果,

然后,将一开始的公式转换为更加具体的形式:

这样我们就可以看出,上面的可以直接转换为矩阵形式:

所以,虽然你可能觉得上面的self.weight[l][j,k]中j,k的前后顺序有些奇怪的,但是,这个形式有利于直接通过矩阵来进行计算。

然后,我们开始定义Sigmoid函数:

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

需要注意的是,输入z是一个向量或者Numpy数组,Numpy会自动对其中的元素使用Sigmoid函数。

然后,我们编写一个Network类的前馈网络,我们输入数据到网络,然后返回相对应的输出,所有的网络层中都应用了下面的前馈网络:

1
2
3
4
def feedforward(self, a):
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a

当然了,最重要的就是希望网络能够进行学习,我们使用了SGD函数来执行随机梯度下降算法,这部分的程序中有的代码还没有解释,下面是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):
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使用了,则程序会评估训练的每个阶段评估网络的结果和输出部分的过程。这有利于理解网络的训练过程,但是会降低其运算速率。
程序中,每个阶段都随机将训练数据抽取,然后将其分配为小批量之中,然后,对这部分小批量数据进行梯度下降求平均值,然后修改权重和偏置。

下面是对小批量数据的处理代码:

1
2
3
4
5
6
7
8
9
10
11
def update_mini_batch(self, mini_batch, eta):
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)]

其中,有一行代码还没有进行解释:

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

这里使用了一个叫做方向传递算法,这是一种用来快速计算成本函数梯度的方法,这一部分的代码比较复杂。所以先不介绍,之后会介绍,现在只要了解其功能。
下面是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""
network.py
"""
import random

import numpy as np

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:])]

def feedforward(self, a):
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):
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):
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):
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]

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())
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):
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 (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数据的初始化代码。

然后,我们在Python的命令窗口中使用如下的命令:

1
2
3
4
5
import mnist_loader
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
import network
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

然后,返回数据为:

1
2
3
4
5
6
7
Epoch 0: 9129 / 10000
Epoch 1: 9295 / 10000
Epoch 2: 9348 / 10000
...
Epoch 27: 9528 / 10000
Epoch 28: 9542 / 10000
Epoch 29: 9534 / 10000

接下来,我们可以调试神经网络,这个过程不是一件容易的事情,我们需要一种启发式调试技巧。

下面补充MNIST数据的初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"""
mnist_loader
"""

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

# Third-party libraries
import numpy as np

def load_data():
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():
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):
e = np.zeros((10, 1))
e[j] = 1.0
return e

上面的程序依然可以进行优化,或者使用已经通用的模块。

1.3 引入深度学习

虽然,神经网络提供了很好的性能,但是,我们无法获取关于网络运行的任何解释。呢么我们如何才能够理解网络的原理呢?

为了解决这个问题,我们我们可以将问题进行分解,分解为多个子问题,然后对每个子问题进行神经网络的学习。

这样,通过多个层次的网络就可以完成抽象的问题,从而建立起更加抽象的层次,这样,具有多层结构即两个或者两个以上的隐藏层的网络被称为深度神经网络。

自从2006年以来,人们已经开发出了能够在深度网络中进行学习的技术,这些学习技术也都基于随机梯度下降和反响传播,也引入了新的概念。

2.反向传播算法

2.1 反响传播算法原理

之前,我们已经通过神经网络,使用梯度下降来学习其权重和偏置。但是,其中我们没有详细讲解如何计算成本函数的梯度,及计算成本函数对权重和偏置的求导。这里我们将详细介绍反向传播算法。

BP算法在20世纪70年代最初引用,但是其重要性并没有完全认识到。1986年的论文描述了了几种神经网络,其中反向传播的工作速度比早期学习方法快很多,这使得使用神经网络来解决以前无法解决的问题提供了可能。反向传播算法已经成为神经网络学习的主要方法。

这一部分会更多地介绍数学方面的公式,反向传播算法有四个公式:



上面,我们使用了一个δ[l][j],用来表示第l层中的第j个节点的神经元的误差。

假如在神经元的加权输入中增加了一个微小的改变Δz[l][j],那么该神经元的输入从σ(z[l][j])变为了σ(z[l][j]+Δz[l][j]),这个改变在网络中传递给后面的层,从而导致了整体成本的改变:

因此,我们就定义了δ[l][j]为:

首先我们回忆一下,z为神经元的加权输入,a为神经元的加权输出。再次重复之前的定义和前置条件:

第一个方程是通过δ的定义,然后使用链式法制推导出来。

第二个方程就是,两层之间的δ的关系。

第三个和第四个方程分别是用δ来表示所要求的代价函数对偏置和权重的偏导。

上面是简单的公式推导,下面是对其原理的解释。

我们假想一个权重的变化会导致与它相连的下一个节点的加权输入z变化,从而导致经过激活函数的输出a发生变化,从而导致了节点之后每一层的变化,直到最后一层,最后是成本函数的变化。

我们按照这个想法对其进行推理

首先,代价函数的变化ΔC和权重的变化Δw有关:

然后,我们为了能够计算ΔC对Δw的导数,我们可以知道,与权重相关的下一层节点的激活函数的输出a的变化量和权重w的变化有关系:

接着,我们考虑与权重相关的下一层节点的下一层中的某个节点与权重w的变化的关系:

之后,我们可以类比,推导从权重相关的下一层节点到最后的代价函数之间某条通路上,权重的变化量和代价函数的变化量之间的关系,因为通路不仅仅只有一条,我们继续推导出通过所有通路后两者的关系。


上面对于原理的解释依然是很简单的一部分,其中的化简都省去了。但是,我们可以通过这个了解其原理

2.2 反响传播算法代码

上面的部分基本完成了对公式的推导,那么回过头看一看代码部分的内容:
与反向传播相关的主要代码都包含在了Network类中的update_mini_batch和backprop方法中了。
剩下还有一些激活函数和激活函数的导数计算函数等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Network(object):
...
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)]

具体而言,对于每一个mini_batch进行梯度下降,然后求平均,修改权重和偏置。其中最重要的部分是

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

这里是使用了backprop算法来计算代价函数对于权重和偏置的偏导数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Network(object):
...
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)
'''
下面是一部分简单的计算函数。
'''bash
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)

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

3.提高神经网络的学习效率

之前,我们构建了一个简单的神经网络,能够实现手写数字图片的分类,但是,这个网络还有地方可以进行优化,从而继续提高其学习的效率。
这一部分,我们将会补充几种优化的方法:
1.更好的成本函数:交叉熵成本函数
2.4种正则化方法(L1,L2正则化,随机剔除数据,人为扩展训练数据),有利于对训练数据进行范化
3.初始化网络中权重的更好的方法
4.启发式方法,为网络选择良好的超参数

3.1 交叉熵成本函数

当我们训练模型的时候,我们希望能够尽快减少成本函数,或者误差。
但是,在实际中,我们发现当误差过大或者误差过小的时候,模型的训练速度很慢,这个不符合我们的期望,所以我们使用了交叉熵成本函数代替原本的二次函数成本函数。

首先,我们需要了解一下,为什么当误差过大或者过小的时候,训练的速度会下降。
下面是之前的成本函数:

对此,当我们使用梯度下降的方法计算对成本函数的变量的偏导:

通过上面的公式,我们可以看到,当误差过小时,前半部分较小,导致学习速率下降,后面对激活函数求导的时候,我们观察Sigmoid函数图像可以发现。

当输出接近1或者0的时候,导数接近于0,导致学习速率的下降。

为此,我们引入了交叉熵成本函数代替原本的二次代价函数。

关于交叉熵是信息熵的一部分,可以见我其他博客的补充。

解决这个问题的另一个方法是softmax层,其输出可以视为概率分布。

下一个问题需要解决的是过度拟合和正则化。
有一个大量自由参数的模型可以描述各种现象,即使这样的模型与可用数据非常的吻合,也不是一个很好的模型,仅意味着这个模型具有足够的自由度,他可以描述给定大小的几乎任何数据集,而无需对对象有任何真实的见解,这种情况下,模型将适用于现有的所有数据,但是无法推广到新的情况。

那么,MNIST数字分类有30个隐藏神经元和24000个参数,那么,结果是可信的吗?

为了使得这个问题更加的明显,我们使用了训练的钱1000个图像,然后使用交叉熵,以及与之前类似的方法进行训练。

可以发现,在训练到一定阶段之后,成本函数依旧在下降,但是测试数据的精确度却在进行小的随机波动。

所以,可以看出,在这个阶段之后,我们从训练数据学到的只是没有办法继续推广到测试数据,因此这不是有用的学习,这就是网络的过拟合和过度训练。

那么,我们同时看测试数据的成本,在某一阶段之后,测试数据的成本在提高,这就是模型过拟合的一个迹象。

检测过度拟合的明显方法是使用上面的方法,随着我们的网络训练,跟踪测试数据的准确性。如果我们发现测试数据的准确性不再提高,则应该停止训练。当然,严格来说,这不一定是过度拟合的标志。测试数据和训练数据的准确性可能同时停止提高。尽管如此,采用此策略仍可以防止过度拟合。

之前,一直使用了training和test数据,没有使用validation数据,validation数据包含了10000个数字图像。我们使用validation来防止过拟合,为此,我们将在每个时期结束的时候在validation上面计算分类准确性,一旦validation数据集的分类准确性饱和就停止训练,这个策略是提前停止。

过拟合是神经网络中的主要问题,现代网络中通常有大量的权重和偏差,为了有效训练,我们需要减少过度拟合影响的技术

我们为什么使用validation而不是test来防止过拟合呢?因为,如果我们使用test数据拟合,那么我们可能会找到适合特定特性的超参数,但是网络的性能不会推广到其他数据集,然后获取到所需要的参数,使用test数据集来对准确性进行最终的评估。这使得神经网络的范化能力得到体现。