神经网络与机器学习--手写数字识别

原书网址: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

output = \begin{cases} 0 & if \sum_{j} w_j x_j \leq threshold\\1 & if \sum_{j} w_j x_j \gt threshold\\ \end{cases} \quad \text{(1)}\\
不同的权重(weights)决定了不同的条件对结果的影响程度,不同的阈值(threshold)决定了最终触发动作的难易程度;如果阈值较低,就较容易触发(1),否则就较难(0)。通过修改权重(weights)和阈值(threshold),我们就能够得到不同的决策模型。
一个神经网络有多个神经元构成:


该神经网络由三层构成,其中第一层有三个比较简单的决策神经元构成,简单理解为可以做简单的决定,第二成的4个神经元可以理解为在第一层的输出中做出更复杂的决定,直到最后做出复杂的决策。
对上面感知器进行简化:

output = \begin{cases} 0 & if \ w \cdot x+b\leq 0\\ 1 & if \ w \cdot x+b \gt 0\\ \end{cases} \quad \text{(2)} \\
b称为偏置项(bias),和公式(1)中的threshold的意义差不多。你可以把偏置项(bias)看作是感知器输出1的容易程度的度量,或是用来衡量感知器触发的难易程度的度量。

简单示例:

下面展示了如果实现一个NAND门:



计算公式:

\begin{matrix} & (−2)&*x_1 + & (−2) & * x_2 + & 3 &= 3\\ & w_1 & &w_2 & & b & \end{matrix}
输出:

x1 x2 out
0 0 3
0 1 1
1 0 1
1 1 -1

sigmoid神经元:

主要使用的神经元模型是一种sigmoid神经元模型,sigmoid和之前的感知机基本相同,下面介绍一下sigmoid神经元模型。



和感知机一样,有多个输入和一个输出,但是输出不再是0或1,而是介于(0, 1)之间的值

\sigma(z) = \frac{1}{1+e^{-z}} \quad \text{(3)}\\ \frac{1}{1+exp(-\sum_{j} w_j x_j - b)} \quad \text{(4)}\\
其中σ 称为sigmoid函数


其中:

\begin{matrix} if & z >>0, & e^{-z} \approx 0, & \sigma(z) \approx 1 \\ if & z << 0, & e^{-z} \rightarrow \infty, & \sigma(z) \approx 0 \\ \end{matrix}
所以,sigmoid理解上可以理解为一个阶跃函数,sigmoid函数,像是一个平滑版的阶跃函数;如果使用的是阶跃函数的话,那么神经元就是前面的感知器的实现;由于sigmoid函数是一个平滑的函数,所以weight或者bias的一个微小的改动,会导致结果产生一个小的改动。

在神经网络中,我们就是通过不断的调整weight或者bias,使得最终整个网络向我们预期的结果移动。



通过微积分的只是来看一下weight或bias的微小改变是如何影响结果的:

\Delta output \approx \sum_{j} \frac{\partial output}{\partial w_j}\Delta w_j + \sum_{j} \frac{\partial output}{\partial b}\Delta b \quad \text{(5)}\\

神经网络的结构


神经网络通常包含输入层,输出层,和若干个隐藏层;输入层和输出层通常来说是非常简单的,从手写数字识别的例子中,如果输入是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:
y = (0,0,0,0,0,0,1,0,0,0)^T
代价函数(cost function):我们需要有一个公式,来量化网络的输出和训练集真实结果之间的差异,从而直观的了解整个模型在训练集上的表现。
二次成本函数:均方误差
C(w, b) = \frac{1}{2n} \sum_{x} \parallel y(x) - a \parallel ^2 \quad \text{(6)}\\
其中n表示的是训练集的个数,a表示是输入数据的真实结果。
可以看出二次成本函数是非负数,当网络的输出结果接近于真实结果的时候,成本函数较小,相反则成本函数较大。
所以我们的目标就是找到一组weight和bias,使成本函数达到最小,这个过程我们将使用梯度下降来实现。
(为什么要引入二次成本函数开衡量,而不是直接使用图片正确识别的数量呢?因为正确识别图片的数量对于weight和bias不是一个平滑的函数,改变weight或bias可能不会引起正确识别数量的改变)

那么梯度下降是如何实现使代价方程取到最小值呢?
(为什么不直接计算极值位置?因为面对成千上万的参数,通过计算极值位置是不现实的)
梯度下降可以直观的理解为一个球放在一个山谷中,首先随机的选择一个起点,那么球每次都移动一小段,那么球将最后落到一个最值点上。
假设只有两个变量,v1和v2



v1和v2的改变对代价方程的影响:

\Delta C \approx \frac{\partial C}{\partial v_1} \Delta v_1 + \frac{\partial C}{\partial v_2} \Delta v_2 \quad \text{(7)}\\
接下来就需要找到Δv1和Δv2,是ΔC为负数
\Delta v = (\Delta v_1, \Delta v_2)^T\\
\nabla C = \begin{pmatrix} \frac{\partial C}{\partial v_1}, \frac{\partial C}{\partial v_2} \end{pmatrix}^T \quad \text{(8)}\\
其中
Δv表示移动向量,∇C表示代价方程的梯度向量,∇C反映了v的变化对C的影响,告诉了我们应该怎样选择Δv使得ΔC是负的。
(∇:常用来表示梯度)
重写公式(7):
\Delta C \approx \nabla C \cdot \Delta v \quad \text{(9)}\\ \Delta v = - \eta \nabla C \quad \text{(10)}\\
其中η是一个小的正数
最终移动小球的位置:
v \to v' = v - \eta \nabla C \quad \text{(11)}\\


η的选取应该足够小,以免造成ΔC>0的情况出现,但是η又不能够太小,这样的话小球的移动速度就会很慢

继续回到神经网络中,我们的weight以及bias往往会很多
\begin{aligned} &\Delta v = (\Delta v_1, \ldots , \Delta v_m)^T\\ &\Delta C \approx \nabla C \cdot \Delta v \quad & \text{(12)}\\ &\nabla C = \begin{pmatrix} \frac{\partial C}{\partial v_1}, \ldots, \frac{\partial C}{\partial v_m} \end{pmatrix}^T \quad & \text{(13)}\\ &\Delta v = - \eta \nabla C \quad & \text{(14)}\\ &v \to v' = v - \eta \nabla C \quad & \text{(15)}\\ \end{aligned}\\
那如何应用梯度下降到我们的神经网络中呢?
就是通过寻找weight和bias使得代价方程公式(6)最小
\begin{aligned} &w_k \to w_k' = w_k - \eta \frac{\partial C}{\partial w_k} \quad & \text{(16)}\\ &b_l \to b_l' = b_l - \eta \frac{\partial C}{\partial b_l} \quad & \text{(17)}\\ \end{aligned}\\
在实际的应用中,如果每次都是使用全部的训练集数据来进行计算,那么这样的话将会有很大的计算量。

所以一般会采用随机梯度下降方法,即每次只是随机挑选部分(小批量)训练集数据进行计算,这样能够加快计算速度。
\begin{aligned} &\frac{\sum_{j=1}^m\nabla C_{x_j}}{m} \approx \frac{\sum\nabla C_{x}}{n} = \nabla C \quad & \text{(18)}\\ &\nabla C \approx \frac{1}{m} \sum_{j=1}^m \nabla C_{x_j} \quad & \text{(19)}\\ \end{aligned}\\
其中m表示选取的训练集部分的数量,通过上面的两个公式说明,随机选取部分训练集数据进行计算是可行的。

随机梯度下降的工作原理就是随机选择一个小批量(mini-batch)的训练输入,并对这些输入进行训练,其中求和部分只是对当前批次的所有训练集样本进行。
\begin{aligned} &w_k \to w_k' = w_k -\frac{\eta}{m}\sum_j\frac{\partial C_{x_j}}{\partial w_k} \quad & \text{(20)}\\ &b_l \to b_l' = b_l - \frac{\eta}{m}\sum_j\frac{\partial C_{x_j}}{\partial b_l} \quad & \text{(21)}\\ \end{aligned}\\
在当前小批量的训练集完成后,再次选取另一个小批量的训练集重复上面的过程。

直到我们使用完了全部的训练集样本,我们称为完成了一个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%,由于依赖于不同的初始化,以及训练集的分批次训练,会导致每次训练出生不同的结果,经过多次调用,总体结果大致相同。
可以尝试修改学习速率,每个批次训练样本个数,以及训练次数,隐藏层包含神经元的个数,看一下对训练结果以及训练速度的影响。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342