零基础入门深度学习(3) - 神经网络和反向传播算法

往期回顾

在上一篇文章中,咱们已经掌握了机器学习的基本套路,对模型、目标函数、优化算法这些概念有了必定程度的理解,并且已经会训练单个的感知器或者线性单元了。在这篇文章中,咱们将把这些单独的单元按照必定的规则相互链接在一块儿造成神经网络,从而奇迹般的得到了强大的学习能力。咱们还将介绍这种网络的训练算法:反向传播算法。最后,咱们依然用代码实现一个神经网络。若是您能坚持到本文的结尾,将会看到咱们用本身实现的神经网络去识别手写数字。如今请作好准备,您即将双手触及到深度学习的大门。node

 

神经元

神经元和感知器本质上是同样的,只不过咱们说感知器的时候,它的激活函数是阶跃函数;而当咱们说神经元时,激活函数每每选择为sigmoid函数或tanh函数。以下图所示:python

计算一个神经元的输出的方法和计算一个感知器的输出是同样的。假设神经元的输入是向量,权重向量是(偏置项是),激活函数是sigmoid函数,则其输出git

 

 

 

sigmoid函数的定义以下:程序员

 

 

 

 

将其带入前面的式子,获得github

 

 

 

 

sigmoid函数是一个非线性函数,值域是(0,1)。函数图像以下图所示算法

sigmoid函数的导数是:编程

 

 

令则

 

能够看到,sigmoid函数的导数很是有趣,它能够用sigmoid函数自身来表示。这样,一旦计算出sigmoid函数的值,计算它的导数的值就很是方便。数组

 

神经网络是啥

神经网络其实就是按照必定规则链接起来的多个神经元。上图展现了一个全链接(full connected, FC)神经网络,经过观察上面的图,咱们能够发现它的规则包括:性能优化

  • 神经元按照层来布局。最左边的层叫作输入层,负责接收输入数据;最右边的层叫输出层,咱们能够从这层获取神经网络输出数据。输入层和输出层之间的层叫作隐藏层,由于它们对于外部来讲是不可见的。
  • 同一层的神经元之间没有链接。
  • 第N层的每一个神经元和第N-1层的全部神经元相连(这就是full connected的含义),第N-1层神经元的输出就是第N层神经元的输入。
  • 每一个链接都有一个权值。

上面这些规则定义了全链接神经网络的结构。事实上还存在不少其它结构的神经网络,好比卷积神经网络(CNN)、循环神经网络(RNN),他们都具备不一样的链接规则。服务器

 

计算神经网络的输出

神经网络实际上就是一个输入向量到输出向量的函数,即:

 

 

 

 

根据输入计算神经网络的输出,须要首先将输入向量的每一个元素的值赋给神经网络的输入层的对应神经元,而后根据式1依次向前计算每一层的每一个神经元的值,直到最后一层输出层的全部神经元的值计算完毕。最后,将输出层每一个神经元的值串在一块儿就获得了输出向量

接下来举一个例子来讲明这个过程,咱们先给神经网络的每一个单元写上编号。

如上图,输入层有三个节点,咱们将其依次编号为一、二、3;隐藏层的4个节点,编号依次为四、五、六、7;最后输出层的两个节点编号为八、9。由于咱们这个神经网络是全链接网络,因此能够看到每一个节点都和上一层的全部节点有链接。好比,咱们能够看到隐藏层的节点4,它和输入层的三个节点一、二、3之间都有链接,其链接上的权重分别为。那么,咱们怎样计算节点4的输出值呢?

为了计算节点4的输出值,咱们必须先获得其全部上游节点(也就是节点一、二、3)的输出值。节点一、二、3是输入层的节点,因此,他们的输出值就是输入向量自己。按照上图画出的对应关系,能够看到节点一、二、3的输出值分别是。咱们要求输入向量的维度和输入层神经元个数相同,而输入向量的某个元素对应到哪一个输入节点是能够自由决定的,你偏非要把赋值给节点2也是彻底没有问题的,但这样除了把本身弄晕以外,并无什么价值。

一旦咱们有了节点一、二、3的输出值,咱们就能够根据式1计算节点4的输出值

 

 

 

 

上式的是节点4的偏置项,图中没有画出来。而分别为节点一、二、3到节点4链接的权重,在给权重编号时,咱们把目标节点的编号放在前面,把源节点的编号放在后面。

一样,咱们能够继续计算出节点五、六、7的输出值。这样,隐藏层的4个节点的输出值就计算完成了,咱们就能够接着计算输出层的节点8的输出值

 

 

 

 

同理,咱们还能够计算出的值。这样输出层全部节点的输出值计算完毕,咱们就获得了在输入向量时,神经网络的输出向量。这里咱们也看到,输出向量的维度和输出层神经元个数相同。

 

神经网络的矩阵表示

神经网络的计算若是用矩阵来表示会很方便(固然逼格也更高),咱们先来看看隐藏层的矩阵表示。

首先咱们把隐藏层4个节点的计算依次排列出来:

 

 

 

 

接着,定义网络的输入向量和隐藏层每一个节点的权重向量。令

 

 

 

 

代入到前面的一组式子,获得:

 

 

 

 

如今,咱们把上述计算的四个式子写到一个矩阵里面,每一个式子做为矩阵的一行,就能够利用矩阵来表示它们的计算了。令

 

 

 

 

带入前面的一组式子,获得

 

 

 

在式2中,是激活函数,在本例中是函数;是某一层的权重矩阵;是某层的输入向量;是某层的输出向量。式2说明神经网络的每一层的做用实际上就是先将输入向量左乘一个数组进行线性变换,获得一个新的向量,而后再对这个向量逐元素应用一个激活函数。

每一层的算法都是同样的。好比,对于包含一个输入层,一个输出层和三个隐藏层的神经网络,咱们假设其权重矩阵分别为,每一个隐藏层的输出分别是,神经网络的输入为,神经网络的输入为,以下图所示:

则每一层的输出向量的计算能够表示为:

 

 

 

 

这就是神经网络输出值的计算方法。

 

神经网络的训练

如今,咱们须要知道一个神经网络的每一个链接上的权值是如何获得的。咱们能够说神经网络是一个模型,那么这些权值就是模型的参数,也就是模型要学习的东西。然而,一个神经网络的链接方式、网络的层数、每层的节点数这些参数,则不是学习出来的,而是人为事先设置的。对于这些人为设置的参数,咱们称之为超参数(Hyper-Parameters)。

接下来,咱们将要介绍神经网络的训练算法:反向传播算法。

 

反向传播算法(Back Propagation)

咱们首先直观的介绍反向传播算法,最后再来介绍这个算法的推导。固然读者也能够彻底跳过推导部分,由于即便不知道如何推导,也不影响你写出来一个神经网络的训练代码。事实上,如今神经网络成熟的开源实现多如牛毛,除了练手以外,你可能都没有机会须要去写一个神经网络。

咱们以监督学习为例来解释反向传播算法。在零基础入门深度学习(2) - 线性单元和梯度降低一文中咱们介绍了什么是监督学习,若是忘记了能够再看一下。另外,咱们设神经元的激活函数函数(不一样激活函数的计算公式不一样,详情见反向传播算法的推导一节)。

咱们假设每一个训练样本为,其中向量是训练样本的特征,而是样本的目标值。

首先,咱们根据上一节介绍的算法,用样本的特征,计算出神经网络中每一个隐藏层节点的输出,以及输出层每一个节点的输出

而后,咱们按照下面的方法计算出每一个节点的偏差项

  • 对于输出层节点
 

 

 

其中,是节点的偏差项,是节点的输出值,是样本对应于节点的目标值。举个例子,根据上图,对于输出层节点8来讲,它的输出值是,而样本的目标值是,带入上面的公式获得节点8的偏差项应该是:

 

 

 

 

  • 对于隐藏层节点,
 

 

 

其中,是节点的输出值,是节点到它的下一层节点的链接的权重,是节点的下一层节点的偏差项。例如,对于隐藏层节点4来讲,计算方法以下:

 

 

 

 

最后,更新每一个链接上的权值:

 

 

 

其中,是节点到节点的权重,是一个成为学习速率的常数,是节点的偏差项,是节点传递给节点的输入。例如,权重的更新方法以下:

 

 

 

 

相似的,权重的更新方法以下:

 

 

 

 

偏置项的输入值永远为1。例如,节点4的偏置项应该按照下面的方法计算:

 

 

 

 

咱们已经介绍了神经网络每一个节点偏差项的计算和权重更新方法。显然,计算一个节点的偏差项,须要先计算每一个与其相连的下一层节点的偏差项。这就要求偏差项的计算顺序必须是从输出层开始,而后反向依次计算每一个隐藏层的偏差项,直到与输入层相连的那个隐藏层。这就是反向传播算法的名字的含义。当全部节点的偏差项计算完毕后,咱们就能够根据式5来更新全部的权重。

以上就是基本的反向传播算法,并非很复杂,您弄清楚了么?

 

反向传播算法的推导

反向传播算法其实就是链式求导法则的应用。然而,这个如此简单且显而易见的方法,倒是在Roseblatt提出感知器算法将近30年以后才被发明和普及的。对此,Bengio这样回应道:

不少看似显而易见的想法只有在过后才变得显而易见。

接下来,咱们用链式求导法则来推导反向传播算法,也就是上一小节的式三、式四、式5。

前方高能预警——接下来是数学公式重灾区,读者能够酌情阅读,没必要强求。

按照机器学习的通用套路,咱们先肯定神经网络的目标函数,而后用随机梯度降低优化算法去求目标函数最小值时的参数值。

咱们取网络全部输出层节点的偏差平方和做为目标函数:

 

 

 

 

其中,表示是样本的偏差。

而后,咱们用文章零基础入门深度学习(2) - 线性单元和梯度降低中介绍的随机梯度降低算法对目标函数进行优化:

 

 

 

 

随机梯度降低算法也就是须要求出偏差对于每一个权重的偏导数(也就是梯度),怎么求呢?

观察上图,咱们发现权重仅能经过影响节点的输入值影响网络的其它部分,设是节点的加权输入,即

 

 

 

 

的函数,而的函数。根据链式求导法则,能够获得:

 

 

 


上式中,是节点传递给节点的输入值,也就是节点的输出值。

 

对于的推导,须要区分输出层和隐藏层两种状况。

 

输出层权值训练

对于输出层来讲,仅能经过节点的输出值来影响网络其它部分,也就是说的函数,而的函数,其中。因此咱们能够再次使用链式求导法则:

 

 

 

 

考虑上式第一项:

 

 

 

 

考虑上式第二项:

 

 

 

 

将第一项和第二项带入,获得:

 

 

 

 

若是令,也就是一个节点的偏差项是网络偏差对这个节点输入的偏导数的相反数。带入上式,获得:

 

 

 

 

上式就是式3。

将上述推导带入随机梯度降低公式,获得:

 

 

 

 

上式就是式5。

 

隐藏层权值训练

如今咱们要推导出隐藏层的

首先,咱们须要定义节点的全部直接下游节点的集合。例如,对于节点4来讲,它的直接下游节点是节点八、节点9。能够看到只能经过影响再影响。设是节点的下游节点的输入,则的函数,而的函数。由于有多个,咱们应用全导数公式,能够作出以下推导:

 

 

 

 

由于,带入上式获得:

 

 

 

 

上式就是式4。

——数学公式警报解除——

至此,咱们已经推导出了反向传播算法。须要注意的是,咱们刚刚推导出的训练规则是根据激活函数是sigmoid函数、平方和偏差、全链接网络、随机梯度降低优化算法。若是激活函数不一样、偏差计算方式不一样、网络链接结构不一样、优化算法不一样,则具体的训练规则也会不同。可是不管怎样,训练规则的推导方式都是同样的,应用链式求导法则进行推导便可。

 

神经网络的实现

完整代码请参考GitHub: https://github.com/hanbt/learn_dl/blob/master/bp.py (python2.7)

如今,咱们要根据前面的算法,实现一个基本的全链接神经网络,这并不须要太多代码。咱们在这里依然采用面向对象设计。

首先,咱们先作一个基本的模型:

如上图,能够分解出5个领域对象来实现神经网络:

  • Network 神经网络对象,提供API接口。它由若干层对象组成以及链接对象组成。
  • Layer 层对象,由多个节点组成。
  • Node 节点对象计算和记录节点自身的信息(好比输出值、偏差项等),以及与这个节点相关的上下游的链接。
  • Connection 每一个链接对象都要记录该链接的权重。
  • Connections 仅仅做为Connection的集合对象,提供一些集合操做。

Node实现以下:

 
  1. # 节点类,负责记录和维护节点自身信息以及与这个节点相关的上下游链接,实现输出值和偏差项的计算。
  2. class Node(object):
  3. def __init__(self, layer_index, node_index):
  4. '''
  5. 构造节点对象。
  6. layer_index: 节点所属的层的编号
  7. node_index: 节点的编号
  8. '''
  9. self.layer_index = layer_index
  10. self.node_index = node_index
  11. self.downstream = []
  12. self.upstream = []
  13. self.output = 0
  14. self.delta = 0
  15. def set_output(self, output):
  16. '''
  17. 设置节点的输出值。若是节点属于输入层会用到这个函数。
  18. '''
  19. self.output = output
  20. def append_downstream_connection(self, conn):
  21. '''
  22. 添加一个到下游节点的链接
  23. '''
  24. self.downstream.append(conn)
  25. def append_upstream_connection(self, conn):
  26. '''
  27. 添加一个到上游节点的链接
  28. '''
  29. self.upstream.append(conn)
  30. def calc_output(self):
  31. '''
  32. 根据式1计算节点的输出
  33. '''
  34. output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)
  35. self.output = sigmoid(output)
  36. def calc_hidden_layer_delta(self):
  37. '''
  38. 节点属于隐藏层时,根据式4计算delta
  39. '''
  40. downstream_delta = reduce(
  41. lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
  42. self.downstream, 0.0)
  43. self.delta = self.output * (1 - self.output) * downstream_delta
  44. def calc_output_layer_delta(self, label):
  45. '''
  46. 节点属于输出层时,根据式3计算delta
  47. '''
  48. self.delta = self.output * (1 - self.output) * (label - self.output)
  49. def __str__(self):
  50. '''
  51. 打印节点的信息
  52. '''
  53. node_str = '%u-%u: output: %f delta: %f' % (self.layer_index, self.node_index, self.output, self.delta)
  54. downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
  55. upstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.upstream, '')
  56. return node_str + '\n\tdownstream:' + downstream_str + '\n\tupstream:' + upstream_str

ConstNode对象,为了实现一个输出恒为1的节点(计算偏置项时须要)

 
  1. class ConstNode(object):
  2. def __init__(self, layer_index, node_index):
  3. '''
  4. 构造节点对象。
  5. layer_index: 节点所属的层的编号
  6. node_index: 节点的编号
  7. '''
  8. self.layer_index = layer_index
  9. self.node_index = node_index
  10. self.downstream = []
  11. self.output = 1
  12. def append_downstream_connection(self, conn):
  13. '''
  14. 添加一个到下游节点的链接
  15. '''
  16. self.downstream.append(conn)
  17. def calc_hidden_layer_delta(self):
  18. '''
  19. 节点属于隐藏层时,根据式4计算delta
  20. '''
  21. downstream_delta = reduce(
  22. lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
  23. self.downstream, 0.0)
  24. self.delta = self.output * (1 - self.output) * downstream_delta
  25. def __str__(self):
  26. '''
  27. 打印节点的信息
  28. '''
  29. node_str = '%u-%u: output: 1' % (self.layer_index, self.node_index)
  30. downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
  31. return node_str + '\n\tdownstream:' + downstream_str

Layer对象,负责初始化一层。此外,做为Node的集合对象,提供对Node集合的操做。

 
  1. class Layer(object):
  2. def __init__(self, layer_index, node_count):
  3. '''
  4. 初始化一层
  5. layer_index: 层编号
  6. node_count: 层所包含的节点个数
  7. '''
  8. self.layer_index = layer_index
  9. self.nodes = []
  10. for i in range(node_count):
  11. self.nodes.append(Node(layer_index, i))
  12. self.nodes.append(ConstNode(layer_index, node_count))
  13. def set_output(self, data):
  14. '''
  15. 设置层的输出。当层是输入层时会用到。
  16. '''
  17. for i in range(len(data)):
  18. self.nodes[i].set_output(data[i])
  19. def calc_output(self):
  20. '''
  21. 计算层的输出向量
  22. '''
  23. for node in self.nodes[:-1]:
  24. node.calc_output()
  25. def dump(self):
  26. '''
  27. 打印层的信息
  28. '''
  29. for node in self.nodes:
  30. print node

Connection对象,主要职责是记录链接的权重,以及这个链接所关联的上下游节点。

 
  1. class Connection(object):
  2. def __init__(self, upstream_node, downstream_node):
  3. '''
  4. 初始化链接,权重初始化为是一个很小的随机数
  5. upstream_node: 链接的上游节点
  6. downstream_node: 链接的下游节点
  7. '''
  8. self.upstream_node = upstream_node
  9. self.downstream_node = downstream_node
  10. self.weight = random.uniform(-0.1, 0.1)
  11. self.gradient = 0.0
  12. def calc_gradient(self):
  13. '''
  14. 计算梯度
  15. '''
  16. self.gradient = self.downstream_node.delta * self.upstream_node.output
  17. def get_gradient(self):
  18. '''
  19. 获取当前的梯度
  20. '''
  21. return self.gradient
  22. def update_weight(self, rate):
  23. '''
  24. 根据梯度降低算法更新权重
  25. '''
  26. self.calc_gradient()
  27. self.weight += rate * self.gradient
  28. def __str__(self):
  29. '''
  30. 打印链接信息
  31. '''
  32. return '(%u-%u) -> (%u-%u) = %f' % (
  33. self.upstream_node.layer_index,
  34. self.upstream_node.node_index,
  35. self.downstream_node.layer_index,
  36. self.downstream_node.node_index,
  37. self.weight)

Connections对象,提供Connection集合操做。

 
  1. class Connections(object):
  2. def __init__(self):
  3. self.connections = []
  4. def add_connection(self, connection):
  5. self.connections.append(connection)
  6. def dump(self):
  7. for conn in self.connections:
  8. print conn

Network对象,提供API。

 
  1. class Network(object):
  2. def __init__(self, layers):
  3. '''
  4. 初始化一个全链接神经网络
  5. layers: 二维数组,描述神经网络每层节点数
  6. '''
  7. self.connections = Connections()
  8. self.layers = []
  9. layer_count = len(layers)
  10. node_count = 0;
  11. for i in range(layer_count):
  12. self.layers.append(Layer(i, layers[i]))
  13. for layer in range(layer_count - 1):
  14. connections = [Connection(upstream_node, downstream_node)
  15. for upstream_node in self.layers[layer].nodes
  16. for downstream_node in self.layers[layer + 1].nodes[:-1]]
  17. for conn in connections:
  18. self.connections.add_connection(conn)
  19. conn.downstream_node.append_upstream_connection(conn)
  20. conn.upstream_node.append_downstream_connection(conn)
  21. def train(self, labels, data_set, rate, iteration):
  22. '''
  23. 训练神经网络
  24. labels: 数组,训练样本标签。每一个元素是一个样本的标签。
  25. data_set: 二维数组,训练样本特征。每一个元素是一个样本的特征。
  26. '''
  27. for i in range(iteration):
  28. for d in range(len(data_set)):
  29. self.train_one_sample(labels[d], data_set[d], rate)
  30. def train_one_sample(self, label, sample, rate):
  31. '''
  32. 内部函数,用一个样本训练网络
  33. '''
  34. self.predict(sample)
  35. self.calc_delta(label)
  36. self.update_weight(rate)
  37. def calc_delta(self, label):
  38. '''
  39. 内部函数,计算每一个节点的delta
  40. '''
  41. output_nodes = self.layers[-1].nodes
  42. for i in range(len(label)):
  43. output_nodes[i].calc_output_layer_delta(label[i])
  44. for layer in self.layers[-2::-1]:
  45. for node in layer.nodes:
  46. node.calc_hidden_layer_delta()
  47. def update_weight(self, rate):
  48. '''
  49. 内部函数,更新每一个链接权重
  50. '''
  51. for layer in self.layers[:-1]:
  52. for node in layer.nodes:
  53. for conn in node.downstream:
  54. conn.update_weight(rate)
  55. def calc_gradient(self):
  56. '''
  57. 内部函数,计算每一个链接的梯度
  58. '''
  59. for layer in self.layers[:-1]:
  60. for node in layer.nodes:
  61. for conn in node.downstream:
  62. conn.calc_gradient()
  63. def get_gradient(self, label, sample):
  64. '''
  65. 得到网络在一个样本下,每一个链接上的梯度
  66. label: 样本标签
  67. sample: 样本输入
  68. '''
  69. self.predict(sample)
  70. self.calc_delta(label)
  71. self.calc_gradient()
  72. def predict(self, sample):
  73. '''
  74. 根据输入的样本预测输出值
  75. sample: 数组,样本的特征,也就是网络的输入向量
  76. '''
  77. self.layers[0].set_output(sample)
  78. for i in range(1, len(self.layers)):
  79. self.layers[i].calc_output()
  80. return map(lambda node: node.output, self.layers[-1].nodes[:-1])
  81. def dump(self):
  82. '''
  83. 打印网络信息
  84. '''
  85. for layer in self.layers:
  86. layer.dump()

至此,实现了一个基本的全链接神经网络。能够看到,同神经网络的强大学习能力相比,其实现还算是很容易的。

 

梯度检查

怎么保证本身写的神经网络没有BUG呢?事实上这是一个很是重要的问题。一方面,千辛万苦想到一个算法,结果效果不理想,那么是算法自己错了仍是代码实现错了呢?定位这种问题确定要花费大量的时间和精力。另外一方面,因为神经网络的复杂性,咱们几乎没法事先知道神经网络的输入和输出,所以相似TDD(测试驱动开发)这样的开发方法彷佛也不可行。

办法仍是有滴,就是利用梯度检查来确认程序是否正确。梯度检查的思路以下:

对于梯度降低算法:

 

 

 

 

来讲,这里关键之处在于的计算必定要正确,而它是偏导数。而根据导数的定义:

 

 

 

 

对于任意的导数值,咱们均可以用等式右边来近似计算。咱们把看作是的函数,即,那么根据导数定义,应该等于:

 

 

 

 

若是把设置为一个很小的数(好比),那么上式能够写成:

 

 

 

咱们就能够利用式6,来计算梯度的值,而后同咱们神经网络代码中计算出来的梯度值进行比较。若是二者的差异很是的小,那么就说明咱们的代码是正确的。

下面是梯度检查的代码。若是咱们想检查参数的梯度是否正确,咱们须要如下几个步骤:

  1. 首先使用一个样本对神经网络进行训练,这样就能得到每一个权重的梯度。
  2. 加上一个很小的值(),从新计算神经网络在这个样本下的
  3. 减上一个很小的值(),从新计算神经网络在这个样本下的
  4. 根据式6计算出指望的梯度值,和第一步得到的梯度值进行比较,它们应该几乎想等(至少4位有效数字相同)。

固然,咱们能够重复上面的过程,对每一个权重都进行检查。也可使用多个样本重复检查。

 
  1. def gradient_check(network, sample_feature, sample_label):
  2. '''
  3. 梯度检查
  4. network: 神经网络对象
  5. sample_feature: 样本的特征
  6. sample_label: 样本的标签
  7. '''
  8. # 计算网络偏差
  9. network_error = lambda vec1, vec2: \
  10. 0.5 * reduce(lambda a, b: a + b,
  11. map(lambda v: (v[0] - v[1]) * (v[0] - v[1]),
  12. zip(vec1, vec2)))
  13. # 获取网络在当前样本下每一个链接的梯度
  14. network.get_gradient(sample_feature, sample_label)
  15. # 对每一个权重作梯度检查
  16. for conn in network.connections.connections:
  17. # 获取指定链接的梯度
  18. actual_gradient = conn.get_gradient()
  19. # 增长一个很小的值,计算网络的偏差
  20. epsilon = 0.0001
  21. conn.weight += epsilon
  22. error1 = network_error(network.predict(sample_feature), sample_label)
  23. # 减去一个很小的值,计算网络的偏差
  24. conn.weight -= 2 * epsilon # 刚才加过了一次,所以这里须要减去2倍
  25. error2 = network_error(network.predict(sample_feature), sample_label)
  26. # 根据式6计算指望的梯度值
  27. expected_gradient = (error2 - error1) / (2 * epsilon)
  28. # 打印
  29. print 'expected gradient: \t%f\nactual gradient: \t%f' % (
  30. expected_gradient, actual_gradient)

至此,会推导、会实现、会抓BUG,你已经摸到深度学习的大门了。接下来还须要不断的实践,咱们用刚刚写过的神经网络去识别手写数字。

 

神经网络实战——手写数字识别

针对这个任务,咱们采用业界很是流行的MNIST数据集。MNIST大约有60000个手写字母的训练样本,咱们使用它训练咱们的神经网络,而后再用训练好的网络去识别手写数字。

手写数字识别是个比较简单的任务,数字只多是0-9中的一个,这是个10分类问题。

 

超参数的肯定

咱们首先须要肯定网络的层数和每层的节点数。关于第一个问题,实际上并无什么理论化的方法,你们都是根据经验来拍,若是没有经验的话就随便拍一个。而后,你能够多试几个值,训练不一样层数的神经网络,看看哪一个效果最好就用哪一个。嗯,如今你可能明白为何说深度学习是个手艺活了,有些手艺很让人无语,而有些手艺仍是颇有技术含量的。

不过,有些基本道理咱们仍是明白的,咱们知道网络层数越多越好,也知道层数越多训练难度越大。对于全链接网络,隐藏层最好不要超过三层。那么,咱们能够先试试仅有一个隐藏层的神经网络效果怎么样。毕竟模型小的话,训练起来也快些(刚开始玩模型的时候,都但愿快点看到结果)。

输入层节点数是肯定的。由于MNIST数据集每一个训练数据是28*28的图片,共784个像素,所以,输入层节点数应该是784,每一个像素对应一个输入节点。

输出层节点数也是肯定的。由于是10分类,咱们能够用10个节点,每一个节点对应一个分类。输出层10个节点中,输出最大值的那个节点对应的分类,就是模型的预测结果。

隐藏层节点数量是很差肯定的,从1到100万均可以。下面有几个经验公式:

 

 

隐藏层节点数输入层节点数输出层节点数到之间的常数

 

所以,咱们能够先根据上面的公式设置一个隐藏层节点数。若是有时间,咱们能够设置不一样的节点数,分别训练,看看哪一个效果最好就用哪一个。咱们先拍一个,设隐藏层节点数为300吧。

对于3层的全链接网络,总共有个参数!神经网络之因此强大,是它提供了一种很是简单的方法去实现大量的参数。目前百亿参数、千亿样本的超大规模神经网络也是有的。由于MNIST只有6万个训练样本,参数太多了很容易过拟合,效果反而很差。

 

模型的训练和评估

MNIST数据集包含10000个测试样本。咱们先用60000个训练样本训练咱们的网络,而后再用测试样本对网络进行测试,计算识别错误率:

 

 

错误率错误预测样本数总样本数

 

咱们每训练10轮,评估一次准确率。当准确率开始降低时(出现了过拟合)终止训练。

 

代码实现

首先,咱们须要把MNIST数据集处理为神经网络可以接受的形式。MNIST训练集的文件格式能够参考官方网站,这里不在赘述。每一个训练样本是一个28*28的图像,咱们按照行优先,把它转化为一个784维的向量。每一个标签是0-9的值,咱们将其转换为一个10维的one-hot向量:若是标签值为,咱们就把向量的第维(从0开始编号)设置为0.9,而其它维设置为0.1。例如,向量[0.1,0.1,0.9,0.1,0.1,0.1,0.1,0.1,0.1,0.1]表示值2。

下面是处理MNIST数据的代码:

 
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. import struct
  4. from bp import *
  5. from datetime import datetime
  6. # 数据加载器基类
  7. class Loader(object):
  8. def __init__(self, path, count):
  9. '''
  10. 初始化加载器
  11. path: 数据文件路径
  12. count: 文件中的样本个数
  13. '''
  14. self.path = path
  15. self.count = count
  16. def get_file_content(self):
  17. '''
  18. 读取文件内容
  19. '''
  20. f = open(self.path, 'rb')
  21. content = f.read()
  22. f.close()
  23. return content
  24. def to_int(self, byte):
  25. '''
  26. 将unsigned byte字符转换为整数
  27. '''
  28. return struct.unpack('B', byte)[0]
  29. # 图像数据加载器
  30. class ImageLoader(Loader):
  31. def get_picture(self, content, index):
  32. '''
  33. 内部函数,从文件中获取图像
  34. '''
  35. start = index * 28 * 28 + 16
  36. picture = []
  37. for i in range(28):
  38. picture.append([])
  39. for j in range(28):
  40. picture[i].append(
  41. self.to_int(content[start + i * 28 + j]))
  42. return picture
  43. def get_one_sample(self, picture):
  44. '''
  45. 内部函数,将图像转化为样本的输入向量
  46. '''
  47. sample = []
  48. for i in range(28):
  49. for j in range(28):
  50. sample.append(picture[i][j])
  51. return sample
  52. def load(self):
  53. '''
  54. 加载数据文件,得到所有样本的输入向量
  55. '''
  56. content = self.get_file_content()
  57. data_set = []
  58. for index in range(self.count):
  59. data_set.append(
  60. self.get_one_sample(
  61. self.get_picture(content, index)))
  62. return data_set
  63. # 标签数据加载器
  64. class LabelLoader(Loader):
  65. def load(self):
  66. '''
  67. 加载数据文件,得到所有样本的标签向量
  68. '''
  69. content = self.get_file_content()
  70. labels = []
  71. for index in range(self.count):
  72. labels.append(self.norm(content[index + 8]))
  73. return labels
  74. def norm(self, label):
  75. '''
  76. 内部函数,将一个值转换为10维标签向量
  77. '''
  78. label_vec = []
  79. label_value = self.to_int(label)
  80. for i in range(10):
  81. if i == label_value:
  82. label_vec.append(0.9)
  83. else:
  84. label_vec.append(0.1)
  85. return label_vec
  86. def get_training_data_set():
  87. '''
  88. 得到训练数据集
  89. '''
  90. image_loader = ImageLoader('train-images-idx3-ubyte', 60000)
  91. label_loader = LabelLoader('train-labels-idx1-ubyte', 60000)
  92. return image_loader.load(), label_loader.load()
  93. def get_test_data_set():
  94. '''
  95. 得到测试数据集
  96. '''
  97. image_loader = ImageLoader('t10k-images-idx3-ubyte', 10000)
  98. label_loader = LabelLoader('t10k-labels-idx1-ubyte', 10000)
  99. return image_loader.load(), label_loader.load()

网络的输出是一个10维向量,这个向量第个(从0开始编号)元素的值最大,那么就是网络的识别结果。下面是代码实现:

 
  1. def get_result(vec):
  2. max_value_index = 0
  3. max_value = 0
  4. for i in range(len(vec)):
  5. if vec[i] > max_value:
  6. max_value = vec[i]
  7. max_value_index = i
  8. return max_value_index

咱们使用错误率来对网络进行评估,下面是代码实现:

 
  1. def evaluate(network, test_data_set, test_labels):
  2. error = 0
  3. total = len(test_data_set)
  4. for i in range(total):
  5. label = get_result(test_labels[i])
  6. predict = get_result(network.predict(test_data_set[i]))
  7. if label != predict:
  8. error += 1
  9. return float(error) / float(total)

最后实现咱们的训练策略:每训练10轮,评估一次准确率,当准确率开始降低时终止训练。下面是代码实现:

 
def train_and_evaluate():
last_error_ratio = 1.0
epoch = 0
train_data_set, train_labels = get_training_data_set()
test_data_set, test_labels = get_test_data_set()
network = Network([784, 300, 10])
while True:
epoch += 1
network.train(train_labels, train_data_set, 0.3, 1)
print '%s epoch %d finished' % (now(), epoch)
if epoch % 10 == 0:
error_ratio = evaluate(network, test_data_set, test_labels)
print '%s after epoch %d, error ratio is %f' % (now(), epoch, error_ratio)
if error_ratio > last_error_ratio:
break
else:
last_error_ratio = error_ratio
if __name__ == '__main__':
train_and_evaluate()

  

在个人机器上测试了一下,1个epoch大约须要9000多秒,因此要对代码作不少的性能优化工做(好比用向量化编程)。训练要好久好久,能够把它上传到服务器上,在tmux的session里面去运行。为了防止异常终止致使前功尽弃,咱们每训练10轮,就把得到参数值保存在磁盘上,以便后续能够恢复。(代码略)

 

向量化编程

完整代码请参考GitHub: https://github.com/hanbt/learn_dl/blob/master/fc.py (python2.7)

在经历了漫长的训练以后,咱们可能会想到,确定有更好的办法!是的,程序员们,如今咱们须要告别面向对象编程了,转而去使用另一种更适合深度学习算法的编程方式:向量化编程。主要有两个缘由:一个是咱们事实上并不须要真的去定义Node、Connection这样的对象,直接把数学计算实现了就能够了;另外一个缘由,是底层算法库会针对向量运算作优化(甚至有专用的硬件,好比GPU),程序效率会提高不少。因此,在深度学习的世界里,咱们总会想法设法的把计算表达为向量的形式。我相信优秀的程序员不会把本身拘泥于某种(本身熟悉的)编程范式上,而会去学习并使用最为合适的范式。

下面,咱们用向量化编程的方法,从新实现前面的全链接神经网络。

首先,咱们须要把全部的计算都表达为向量的形式。对于全链接神经网络来讲,主要有三个计算公式。

前向计算,咱们发现式2已是向量化的表达了:

 

 

 

上式中的表示sigmoid函数。

反向计算,咱们须要把式3和式4使用向量来表示:

 

 

式式

 

在式8中,表示第l层的偏差项;表示矩阵的转置。

咱们还须要权重数组W和偏置项b的梯度计算的向量化表示。也就是须要把式5使用向量化表示:

 

 

 

其对应的向量化表示为:

 

 

 

更新偏置项的向量化表示为:

 

 

 

如今,咱们根据上面几个公式,从新实现一个类:FullConnectedLayer。它实现了全链接层的前向和后向计算:

 
# 全链接层实现类
class FullConnectedLayer(object):
def __init__(self, input_size, output_size,
activator):
'''
构造函数
input_size: 本层输入向量的维度
output_size: 本层输出向量的维度
activator: 激活函数
'''
self.input_size = input_size
self.output_size = output_size
self.activator = activator
# 权重数组W
self.W = np.random.uniform(-0.1, 0.1,
(output_size, input_size))
# 偏置项b
self.b = np.zeros((output_size, 1))
# 输出向量
self.output = np.zeros((output_size, 1))
def forward(self, input_array):
'''
前向计算
input_array: 输入向量,维度必须等于input_size
'''
# 式2
self.input = input_array
self.output = self.activator.forward(
np.dot(self.W, input_array) + self.b)
def backward(self, delta_array):
'''
反向计算W和b的梯度
delta_array: 从上一层传递过来的偏差项
'''
# 式8
self.delta = self.activator.backward(self.input) * np.dot(
self.W.T, delta_array)
self.W_grad = np.dot(delta_array, self.input.T)
self.b_grad = delta_array
def update(self, learning_rate):
'''
使用梯度降低算法更新权重
'''
self.W += learning_rate * self.W_grad
self.b += learning_rate * self.b_grad

  

上面这个类一举取代了原先的Layer、Node、Connection等类,不但代码更加容易理解,并且运行速度也快了几百倍。

如今,咱们对Network类稍做修改,使之用到FullConnectedLayer:

 
# Sigmoid激活函数类
class SigmoidActivator(object):
def forward(self, weighted_input):
return 1.0 / (1.0 + np.exp(-weighted_input))
def backward(self, output):
return output * (1 - output)
# 神经网络类
class Network(object):
def __init__(self, layers):
'''
构造函数
'''
self.layers = []
for i in range(len(layers) - 1):
self.layers.append(
FullConnectedLayer(
layers[i], layers[i+1],
SigmoidActivator()
)
)
def predict(self, sample):
'''
使用神经网络实现预测
sample: 输入样本
'''
output = sample
for layer in self.layers:
layer.forward(output)
output = layer.output
return output
def train(self, labels, data_set, rate, epoch):
'''
训练函数
labels: 样本标签
data_set: 输入样本
rate: 学习速率
epoch: 训练轮数
'''
for i in range(epoch):
for d in range(len(data_set)):
self.train_one_sample(labels[d],
data_set[d], rate)
def train_one_sample(self, label, sample, rate):
self.predict(sample)
self.calc_gradient(label)
self.update_weight(rate)
def calc_gradient(self, label):
delta = self.layers[-1].activator.backward(
self.layers[-1].output
) * (label - self.layers[-1].output)
for layer in self.layers[::-1]:
layer.backward(delta)
delta = layer.delta
return delta
def update_weight(self, rate):
for layer in self.layers:
layer.update(rate)

  

如今,Network类也清爽多了,用咱们的新代码再次训练一下MNIST数据集吧。

 

小结

至此,你已经完成了又一次漫长的学习之旅。你如今应该已经明白了神经网络的基本原理,高兴的话,你甚至有能力去动手实现一个,并用它解决一些问题。若是感到困难也不要气馁,这篇文章是一个重要的分水岭,若是你彻底弄明白了的话,在真正的『小白』和装腔做势的『大牛』面前吹吹牛是彻底没有问题的。

做为深度学习入门的系列文章,本文也是上半场的结束。在这个半场,你掌握了机器学习、神经网络的基本概念,而且有能力去动手解决一些简单的问题(例如手写数字识别,若是用传统的观点来看,其实这些问题也不简单)。并且,一旦掌握基本概念,后面的学习就容易多了。

在下半场,咱们讲介绍更多『深度』学习的内容,咱们已经讲了神经网络(Neutrol Network),可是并无讲深度神经网络(Deep Neutrol Network)。Deep会带来更增强大的能力,同时也带来更多的问题。若是不理解这些问题和它们的解决方案,也不能说你入门了『深度』学习。

目前业界有不少开源的神经网络实现,它们的功能也要强大的多,所以你并不须要事必躬亲的去实现本身的神经网络。咱们在上半场不断的从头发明轮子,是为了让你明白神经网络的基本原理,这样你就能很是迅速的掌握这些工具。在下半场的文章中,咱们改变了策略:不会再去从头开始去实现,而是尽量应用现有的工具。

下一篇文章,咱们介绍不一样结构的神经网络,好比鼎鼎大名的卷积神经网络,它在图像和语音领域已然创造了诸多奇迹,在天然语言处理领域的研究也如火如荼。某种意义上说,它的成功大大提高了人们对于深度学习的信心。

好了,同窗们累了吧,奉上美图一张,放松一下心情!

 

参考资料

    1. Tom M. Mitchell, "机器学习", 曾华军等译, 机械工业出版社
    2. CS 224N / Ling 284, Neural Networks for Named Entity Recognition
    3. LeCun et al. Gradient-Based Learning Applied to Document Recognition 1998
    4. 转载自:https://www.zybuluo.com/hanbingtao/note/476663
相关文章
相关标签/搜索