原书网址:http://neuralnetworksanddeeplearning.com/index.html
我们将通过解决一个具体问题来学习神经网络和深度学习背后的核心原理:教计算机识别手写数字的问题。
源码下载:https://github.com/mnielsen/neural-networks-and-deep-learning
第1章 使用神经网络识别手写数字
http://neuralnetworksanddeeplearning.com/chap1.html
首先思考一个问题,就是如果我们没有接触过神经网络的话,我们应该怎么用计算机识别下面的手写数字?
(我想到的是去计算和已知的图片的相似度,看看和谁最接近)
那神经网络会怎么做?
神经网络会使用训练集(大量的手写数字)自动推断手写数字的识别规则,并且通过增大训练集往往能够进一步提高识别的准确性
感知器(Perceptrons):
要了解神经网络的内部结构,需要先了解感知器,感知器就是一种人工的神经元,那他是怎么工作的呢?
一个神经元通常具有多个输入和一个输出,这里引入一个新的名字weights
输入参数:x1,x2,x3
输入权重:w1,w2,w3
不同的权重(weights)决定了不同的条件对结果的影响程度,不同的阈值(threshold)决定了最终触发动作的难易程度;如果阈值较低,就较容易触发(1),否则就较难(0)。通过修改权重(weights)和阈值(threshold),我们就能够得到不同的决策模型。
一个神经网络有多个神经元构成:
该神经网络由三层构成,其中第一层有三个比较简单的决策神经元构成,简单理解为可以做简单的决定,第二成的4个神经元可以理解为在第一层的输出中做出更复杂的决定,直到最后做出复杂的决策。
对上面感知器进行简化:
b称为偏置项(bias),和公式(1)中的threshold的意义差不多。你可以把偏置项(bias)看作是感知器输出1的容易程度的度量,或是用来衡量感知器触发的难易程度的度量。
简单示例:
下面展示了如果实现一个NAND门:
计算公式:
输出:
x1 | x2 | out |
---|---|---|
0 | 0 | 3 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | -1 |
sigmoid神经元:
主要使用的神经元模型是一种sigmoid神经元模型,sigmoid和之前的感知机基本相同,下面介绍一下sigmoid神经元模型。
和感知机一样,有多个输入和一个输出,但是输出不再是0或1,而是介于(0, 1)之间的值
其中σ 称为sigmoid函数
其中:
所以,sigmoid理解上可以理解为一个阶跃函数,sigmoid函数,像是一个平滑版的阶跃函数;如果使用的是阶跃函数的话,那么神经元就是前面的感知器的实现;由于sigmoid函数是一个平滑的函数,所以weight或者bias的一个微小的改动,会导致结果产生一个小的改动。
在神经网络中,我们就是通过不断的调整weight或者bias,使得最终整个网络向我们预期的结果移动。
通过微积分的只是来看一下weight或bias的微小改变是如何影响结果的:
神经网络的结构
神经网络通常包含输入层,输出层,和若干个隐藏层;输入层和输出层通常来说是非常简单的,从手写数字识别的例子中,如果输入是28 * 28 的灰度图像,那我们就将会有 784 = 28 * 28的输入神经元;我们要预测输入的时0 ~ 9之间的那个数组,那么输出就有10个神经元,分别表示输入是对应图片的可能性。应该包含多少个隐藏层,以及每个隐藏层应该包含多少个神经元,这些将会在以后涉及到。
到目前为止的我们称整个网络为前向传播网络(Feedforward neural networks)。网络中既没有环,也没有回退。
扩展阅读:(recurrent neural networks)
一个简单的手写数字识别神经网络
输入层是由784 = 28 * 28个神经元组成,输入值是有0.0 ~ 1.0表示的像素的灰度值。
本例中只包含一个隐藏层,有15个神经元组成。
输出层包含10个神经元,编号从 0~9,可以理解为输入图片为对应数字的可能性,比如,所有的输出中编号为6的神经元的值最高,那么就认为输入的图片是6。
通过梯度下降训练网络
要想训练神经网络,首先我们得有训练数据。在本例中我们使用MNIST data set作为训练集,该数据集包含两部分,第一部分包含了60000张来自250个人的手写数字,所有的都是28*28的灰度图片。第二部分包含10000张用作测试集。
输入是784维的向量,输出是一个10维的向量。
识别结果为6:
代价函数(cost function):我们需要有一个公式,来量化网络的输出和训练集真实结果之间的差异,从而直观的了解整个模型在训练集上的表现。
二次成本函数:均方误差
其中n表示的是训练集的个数,a表示是输入数据的真实结果。
可以看出二次成本函数是非负数,当网络的输出结果接近于真实结果的时候,成本函数较小,相反则成本函数较大。
所以我们的目标就是找到一组weight和bias,使成本函数达到最小,这个过程我们将使用梯度下降来实现。
(为什么要引入二次成本函数开衡量,而不是直接使用图片正确识别的数量呢?因为正确识别图片的数量对于weight和bias不是一个平滑的函数,改变weight或bias可能不会引起正确识别数量的改变)
那么梯度下降是如何实现使代价方程取到最小值呢?
(为什么不直接计算极值位置?因为面对成千上万的参数,通过计算极值位置是不现实的)
梯度下降可以直观的理解为一个球放在一个山谷中,首先随机的选择一个起点,那么球每次都移动一小段,那么球将最后落到一个最值点上。
假设只有两个变量,v1和v2
v1和v2的改变对代价方程的影响:
接下来就需要找到Δv1和Δv2,是ΔC为负数
其中
Δv表示移动向量,∇C表示代价方程的梯度向量,∇C反映了v的变化对C的影响,告诉了我们应该怎样选择Δv使得ΔC是负的。
(∇:常用来表示梯度)
重写公式(7):
其中η是一个小的正数
最终移动小球的位置:
η的选取应该足够小,以免造成ΔC>0的情况出现,但是η又不能够太小,这样的话小球的移动速度就会很慢
继续回到神经网络中,我们的weight以及bias往往会很多
那如何应用梯度下降到我们的神经网络中呢?
就是通过寻找weight和bias使得代价方程公式(6)最小
在实际的应用中,如果每次都是使用全部的训练集数据来进行计算,那么这样的话将会有很大的计算量。
所以一般会采用随机梯度下降方法,即每次只是随机挑选部分(小批量)训练集数据进行计算,这样能够加快计算速度。
其中m表示选取的训练集部分的数量,通过上面的两个公式说明,随机选取部分训练集数据进行计算是可行的。
随机梯度下降的工作原理就是随机选择一个小批量(mini-batch)的训练输入,并对这些输入进行训练,其中求和部分只是对当前批次的所有训练集样本进行。
在当前小批量的训练集完成后,再次选取另一个小批量的训练集重复上面的过程。
直到我们使用完了全部的训练集样本,我们称为完成了一个epoch。这是我们重新开始新epoch。
(扩展阅读 https://blog.csdn.net/qq_18668137/article/details/80883350)
https://mathoverflow.net/questions/25983/intuitive-crutches-for-higher-dimensional-thinking
https://en.wikipedia.org/wiki/Cauchy%E2%80%93Schwarz_inequality
实现手写数字实现算法
使用的数据集为MNIST的数据集,前面提过讲到的其中包含60000训练集图片和10000张测试集图片。
这里只是用60000张训练集图片,将其中的50000作为训练集,10000作为验证集。
源码获取:git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
class Network(object):
def __init__(self, sizes):
"""神经网络的层数 = len(sizes),从第0层开始算,每一层的神经元个数 = sizes[i]"""
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:])]
为什么权重和偏置项要使用随机初始化,而不是全部初始化为0?首先要明确的就是在初始化的时候是不能够全部都初始化为固定值的,因为那样的话,每一层学习到的东西都将是相同
>>> import mnist_loader
>>> training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
获取数据集,其中training_data包含50000个训练样本,每个样本包含两个向量,一个是输入,为7841的矩阵,一个结果样本,为101的矩阵;
validation_data包含10000个样本,每个样本包含一个784*1的输入矩阵,一个0-9之间的数字表明图片对应的数字。
test_data包含10000个样本,数据结构和validation_data相同。
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 range(epochs): #每一次循环将是一个epoch
random.shuffle(training_data)
mini_batches = [ #将训练集分成多个小的批次
training_data[k:k+mini_batch_size]
for k in range(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))
参数说明:其中epochs表示进行多少轮训练,每轮训练会将全部的训练集分成多个小批次进行迭代,每个小批次中训练样本的数量由mini_batch_size决定;eta决定了学习速率;test_data对应了验证集数据。
主要的工作就是将训练集分成了多个批次,,然后每个批次分别调用self.update_mini_batch(mini_batch, eta)
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)]
# 对应公式(20)
self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
# 对应公式(21)
self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] 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)]
# 对应公式(20)
self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
# 对应公式(21)
self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]
其中大部分的工作是由self.backprop(x, y)完成的,稍后会介绍如何反向传播算法。
>>> import mnist_loader
>>> import network_test
>>>
>>> sizes = [784, 30, 10]
>>> training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
>>> net = network_test.Network(sizes)
>>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
Epoch 0: 9032 / 10000
Epoch 1: 9221 / 10000
Epoch 2: 9288 / 10000
Epoch 3: 9354 / 10000
Epoch 4: 9391 / 10000
Epoch 5: 9415 / 10000
Epoch 6: 9412 / 10000
Epoch 7: 9427 / 10000
Epoch 15: 9516 / 10000
Epoch 16: 9477 / 10000
Epoch 17: 9509 / 10000
Epoch 18: 9490 / 10000
Epoch 19: 9507 / 10000
Epoch 20: 9506 / 10000
Epoch 21: 9500 / 10000
Epoch 22: 9527 / 10000
Epoch 23: 9492 / 10000
Epoch 24: 9520 / 10000
Epoch 25: 9508 / 10000
Epoch 26: 9490 / 10000
Epoch 27: 9510 / 10000
Epoch 28: 9516 / 10000
Epoch 29: 9504 / 10000
选择最高的准确率为95.27%,由于依赖于不同的初始化,以及训练集的分批次训练,会导致每次训练出生不同的结果,经过多次调用,总体结果大致相同。
可以尝试修改学习速率,每个批次训练样本个数,以及训练次数,隐藏层包含神经元的个数,看一下对训练结果以及训练速度的影响。