深度学习初探——基于Python的理论和实现
此书特点:
- 从零开始实现深度学习程序
- 最终实现一个高精度识别图像的系统
- 从实现层面理解误差反向传播法、卷积运算
- 不介绍深度学习框架
- 不介绍参数调优
- 不进行GPU相关实现
目标:有能力进一步去阅读最新的论文或者神经网络相关的理论方面的技术书。
Python入门
Numpy
在深度学习的实现中,经常出现数组和矩阵的计算。NumPy 的数组类 (numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使用这些方法。
导入Numpy库
Python中使用import语句来导入库。这里的import numpy as np,直译的话就是“将 numpy 作为 np 导入”的意思。通过写成这样的形式,之后NumPy 相关的方法均可通过 np 来调用。
生成Numpy数组
生成 NumPy 数组,需要使用 np.array() 方法。
Numpy的算数运算
当数组 x 和数组 y 的元素个数相同时,可以对各个元素进行算术运算。
Numpy的N维数组
1 | A = np.array([[1, 2], [3, 4]]) |
广播
形状不同的数组之间也可以进行运算。标量数组扩展成2*2数组再计算,称之为广播。
访问元素
逐个访问,for语句访问,数组访问
Matplotlib
在深度学习的实验中,图形的绘制和数据的可视化非常重要。Matplotlib是用于绘制图形的库,使用 Matplotlib 可以轻松地绘制图形和实现数据的可视化。
pyplot的功能
添加标题和x轴标签名等功能
1 | # coding: utf-8 |
显示图像
pyplot提供了用于显示图像的方法imshow()。另外,可以使用matplotlib.image 模块的 imread() 方法读入图像。
1 | # coding: utf-8 |
图像放在dataset目录下,可以直接读取。
感知机
感知机是什么?
感知机是作为神经网络(深度学习)的起源的算法。感知机接收多个输入信号,输出一个信号。这里所说的“信号”可以想象成电流或河流那样具备“流动性”的东西。像电流流过导线,向前方输送电子一样,感知机的信号也会形成流,向前方输送信息。但是,和实际的电流不同的是,感知机的信号只有“流 / 不流”(1/0)两种取值。
x1、x2 是输入信号,y是输出信号,w1、w2 是权重(w是weight的首字母)。图中的○称为“神经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重(w1x1、w2x2)。神经元会计算传送过来的信号的总和,只有当这个总和超过了某个界限值时,才会输出1。这也称为“神经元被激活”。b称为偏置的参数,用于控制神经元被激活的容易程度。
数学表示:
感知机多个输入信号有各自的权重,这些权重发挥着控制着各个信号的重要性的作用。权重越大,信号的重要性就越高。(权重相当于电流中的电阻,电阻决定电流流动难度的参数)
简单逻辑电路
我们已经知道使用感知机可以表示与门、与非门、或门的逻辑电路。这里重要的一点是:与门、与非门、或门的感知机构造是一样的。实 际 上 , 3 个门电路只有参数的值(权重和阈值)不同 。
感知机的实现
使用权重和偏置实现与门:
1 | # coding: utf-8 |
使用感知机可以实现与门、与非门、或门三种逻辑电路。
感知机的局限性
- 感知机无法实现异或门
- 感知机无法表示曲线
多层感知机
- 通过组合与门、与非门、或门实现异或门
通过叠加层(加深层),感知机能进行更加灵活的表示。
神经网络
感知机的作用是:即便对于复杂的函数,也隐含着能够表示它的可能性。神经网络的一个重要性质是可以自动从数据中学习合适的权重参数。
从感知机到神经网络
神经网络的例子
用图来表示神经网络,分为输入层、输出层和中间层。中间层也叫隐藏层,隐藏层的神经元肉眼看不见。神经元的连接方式而言,与感知机并无差别。
复习感知机
明确表示出偏置,简化式子,引入新函数h(x) :
激活函数
h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。激活函数的
作用在于决定如何来激活输入信号的总和。
先计算输入信号的加权总和,然后用激活函数转换这一总和:
明确显示激活函数计算过程的神经元 :
信号的加权总和为节点 a,然后节点 a 被激活函数 h() 转换成节点 y。“神经元”和“节点”两个术语的含义相同。
激活函数是连接感知机和神经网络的桥梁。
激活函数
激活函数以阈值为界,一旦输入超过阈值,就切换输出。这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。
sigmoid函数
神经网络经常使用的一个激活函数sigmoid函数:
神经网络利用sigmoid函数作为激活函数,进行信号的转换,转换后的信号被传送到下一个神经元。感知机和神经网络的主要区别就在于这个激活函数。在其它方面,比如神经元的多层连接的构造,信号的传递方法等,基本上和感知机是一样的。
阶跃函数的实现
简单的阶跃函数:
1 | def step_function(x): |
支持NumPy数组的实现:
1 | def step_function(x): |
NumPy数组进行不等号运算后,数组的各个元素都会进行不等号运算,生成一个布尔型的数组。数组y是个布尔型的数组,但是阶跃函数是会输出int型的0或1的函数。因此需要转换为int型。
可以用 astype() 方法转换 NumPy 数组的类型。astype() 方法通过参数指定期望的类型,这个例子中是 np.int 型。
阶跃函数的图形
图形上表示上面定义的阶跃函数,需要用到matplotlib库:
1 | import numpy as np |
阶跃函数以 0 为界,输出从 0 切换为 1(或者从 1 切换为 0)。 它的值呈阶梯式变化,所以称为阶跃函数。
sigmoid函数的实现
1 | def sigmoid(x): |
在python中可以如此表示sigmoid函数,当传入参数x为NumPy数组时,也能被正确计算,是因为NumPy的广播功能。
现在把上面的阶跃函数换成sigmoid函数:
sigmoid函数和阶跃函数的比较:
- sigmoid函数是一条平滑的曲线,输入输出发生连续的变化。阶跃则是以0为界,急剧变化。
- 阶跃函数只能返回0和1,sigmoid函数可以返回实数
- 都是非线性函数:
- 激活函数不能使用线性函数,使用线性函数,加深网络的层数就没有意义了。
- 线性函数无法发挥多层网络带来的优势
ReLU函数
sigmoid早就开始用了,现在主要使用ReLU(Rectified Linear Unit)函数。
ReLU函数输入大于 0 时,直接输出该值;在输入小于等于 0 时,输出 0 :
ReLU函数的实现:
1 | def relu(x): |
多维数组的运算
多维数组运算可以高效实现神经网络
二维矩阵
1 | B = np.array([[1,2], [3,4], [5,6]]) |
矩阵的乘积:(np.dot(A, B)和np.dot(B, A)不同)
1 | A = np.array([[1,2], [3,4]]) |
A的列数必须和B的行数相等才能计算np.dot(A,B)
神经网络的内积
使用NumPy矩阵实现神经网络:(省略了偏置和激活函数,只有权重)
三层神经网络的实现
巧妙使用NumPy数组,可以使用很少的代码实现神经网络的前向处理。
符号确认
表示前一层的第 2 个神经元 x2 到后一层的第 1 个神经元 的权重。w(weight)
各层间信号传递的实现
矩阵乘法运算,则可以表示成:
代码简单实现上述式子:
1 | X = np.array([1.0, 0.5]) |
隐藏层的加权和(加权信号和偏置的总和)用 a 表示,被激活函数转换后的信号用 z 表示。此外,图中 h() 表示激活函数,这里我们使用的是 sigmoid 函数:
1 | Z1 = sigmoid(A1) |
下面实现第一层到第二层的信号传递:
1 | W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) |
实现基本相同,但是最后的激活函数和隐藏层的有所不同。
1 | def identity_function(x): |
这里的identity_function()函数(恒等函数),并将其作为输出层的激活函数。这样实现只是为了和之前的流程保持统一。
输出层所用的函数:根据求解问题的性质决定:
- 回归问题用恒等函数
- 二元分类问题使用sigmoid函数
- 多元分类问题使用softmax函数
代码实现小结
1 | def init_network(): |
定义了init_network()和forward()函数,分别进行权重的初始化和信号处理。后面会有backword()处理函数(从输出往输入方向的处理)。通过NumPy的多维数组,高效地实现了神经网络。
输出层的设计
神经网络可以用在分类问题和回归问题上,需要根据情况改变输出层的激活函数,回归问题用恒等函数,分类问题用softmax函数。
恒等函数和softmax 函数
和前面隐藏层的激活函数一样,恒等函数进行的转换处理可以用一根箭头表示。
分类问题中的softmax函数可以用下面的式子表示:
softmax 函数的分子是输入信号 ak 的指数函数,分母是所有输入信号的指数函数的和。
softmax函数的输出通过肩头和所有的信号相连,输出层的各个神经元都受到输入信号的影响。
softmax函数的实现:
1 | def softmax(a): |
实现softmax函数的注意事项
上面的softmax的实现存在溢出问题,指数运算的值很容易溢出,如果超大的值进行除法运算就会出现不确定的情况。溢出问题在进行计算的运算时必须注意!
改进:
指数运算时加上某个常数并不会改变运算结果,这里的C‘为了防止溢出,一般使用输入信号中的最大值的相反数。
softmax改进后的函数实现:
1 | def softmax(a): |
softmax的函数特征
因为指数函数是单调递增函数,即便使用了softmax函数各个元素之间的大小关系也不会改变。一般来说,神经网络只把输出值最大的神经元对应的类别作为识别结果。即便使用softmax函数,输出值最大的神经元也不会改变,在神经网络分类时,softmax函数可以省略。
求解机器学习问题的步骤可以分为“学习”A 和“推理”两个阶段。首先,在学习阶段进行模型的学习 B,然后,在推理阶段,用学到的模型对未知的数据进行推理(分类)。如前所述,推理阶段一般会省略输出层的 softmax 函数。在输出层使用 softmax 函数是因为它和神经网络的学习有关系。
- softmax的函数输出总是在0.0到1.0之间
- softmax函数的输出值总和为1,所以softmax可以解释为概率。
- 通过使用softmax函数,可以使用概率论统计的方法处理问题
输出层神经元数量
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。
手写数字识别
使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播(forward propagation)。
MNIST数据集
什么是MNIST?
- MNIST手写数据集,是机器学习最有名的数据集之一。MNIST有训练图像6w张,测试图像1w张。
- 一般使用方法:先用训练图像进行学习,再用学习到的模型度量能够多大程度上对测试图像进行正确的分类。
- MNIST图像数据是28像素x28像素的灰度图像,各个像素取值在0-255之间,每个图像对应有数字标签。
ch03/mnist_show.py
使用mnist.py
中的load_mnist()
函数,就可以读取MNIST数据:
1 | # coding: utf-8 |
关于PIL(Python Image Library)
是Python的图像处理标准库,功能非常强大,API简单易用。
PIL中的Image和NumPy中的array相互转换
- PIL Image转换为array
1 | img = np.asarray(image |
如果出现read-only错误,并不是转换的错误,一般是你读取的图片的时候,默认选择的是”r”,”rb”模式有关。
1 | img.flags.writeable = True # 将数组改为读写模式 |
- array转换成image
1 | Image.fromarray(np.unit8(img)) |
关于load_mnist函数的解释
位于`dataset/mnist.py
的load_mnist
函数:
1 | def load_mnist(normalize=True, flatten=True, one_hot_label=False): |
通过传入布尔值来执行相关操作的函数:
- normalize(正规化):将图像的像素值正规化为0.0~1.0。
- 将图像的各个像素值除以 255,使得数据的值在 0.0~1.0 的范围内。像这样把数据限定到某个范围内的处理称为正规化(normalization)。
- flatten(扁平化):是否将图像展开为一维数组。
- one_hot_label(一条标签数组):one_hot_label为True的情况下,标签作为one-hot数组返回。「one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组」
- 读入
MNIST数据集
,返回(训练图像, 训练标签), (测试图像, 测试标签)
神经网路的推理处理
神经网络输入层有784个神经元,输出层有10个神经元,输入层784源于图像大小28 x 28,输出层的10来源于10类别分类。此外还有两个隐藏层,第一层50个神经元,第二层100个神经元。50和100可以为任意值。
定义 get_data()、init_network()、predict() :【neuralnet_mnist.py】
1 | # coding: utf-8 |
这三个函数实现了神经网络的推理处理,然后评价识别精度(accuracy):
- 获取MNIST的数据集,生成网络。
- 用 for 语句逐一取出保存在 x 中的图像数据,用 predict() 函数进行分类。「predict() 函数以 NumPy 数组的形式输出各个标签对应的概率」
- 取出这个概率列表中的最大值的索引,作为预测结果。「np.argmax(x) 将获取被赋给参数 x 的数组中的最大值元素的索引」
- 比较神经网络所预测的答案和正确解标签,将回答正确的概率作为识别精度
「注:」此处是假设学习已经完成,学习中的参数保存在sample_weight.pkl文件中,在推理阶段,直接加载这些学习到的参数
在刚才的例子中,作为一种预处理,我们将各个像素值除以 255,进行了简单的正规化。实际上,很多预处理都会考虑到数据的整体分布。比如,利用数据整体的均值或标准差,移动数据,使数据整体以 0 为中心分布,或者进行正规化,把数据的延展控制在一定范围内。除此之外,还有将数据整体的分布形状均匀化的方法,即数据白化(whitening)等。
批处理
多维数组对应维度的元素个数一致,输入由784个元素组成的一维数组,最终输出元素个数为10的一维数组:
这是只输入一张图像数据时的处理流程。
比如,我们想用 predict()函数一次性打包处理 100 张图像。为此,可以把 x 的形状改为 100 × 784,将100 张图像打包作为输入数据:
此时,输入的100张图像的结果被一次性输出了。
1 | x, t = get_data() |
批处理代码实现中的不同之处:
- 首先是range()函数:
range(start, end)
生成一个start到end-1之间的整数构成的列表range(start, end, step)
下一个元素会增加step
x[i:i+batch_n]
会取出从第 i 个到第 i+batch_n 个之间的数据。- 通过 argmax() 获取值最大的元素的索引。不过这里需要注意的是,我们给定了参数 axis=1。这指定了在 100 × 10 的数组中,沿着第 1 维方向(以第 1 维为轴)找到值最大的元素的索引(第 0 维对应第 1 个维度)。
- 最后,比较一下以批为单位进行分类的结果和实际的答案
本章介绍的神经网络和上一章的感知机在信号的按层传递这一点上是相同的,但是,向下一个神经元发送信号时,改变信号的激活函数有很大差异。神经网络中使用的是平滑变化的 sigmoid函数,而感知机中使用的是信号急剧变化的阶跃函数。
神经网络学习
“学习”是指从训练数据中自动获取最优权重参数的过程。为了使神经网络能进行学习,将导入损失函数这一指标。而学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值,利用了函数斜率的梯度法。
误差反向传播法
数值微分虽然简单,也容易实现,但缺点是计算上比较费时间。误差反向传播法能够高效计算权重参数的梯度。正确理解误差反向传播法,有两种方法:数学式和计算图
与学习相关的技巧
神经网络的学习中一些重要观点,主题涉及寻找最优权重参数的最优化方法、权重参数的初始值、超参数设定方法等。为了应对过拟合,还将介绍权值衰减、Dropout 等正则化方法,并进行实现。
卷积神经网络
本章主题是卷积神经网络(CNN),CNN用于图像识别、语音识别等场合,在图像识别比赛中,基于深度学习的方法几乎都以CNN为基础。
深度学习
深度学习是加深了层的深度神经网络。基于之前介绍的网络,只需通过叠加层,就可以创建深度网络。 关于神经网络,已经学了很多:构成神经网络的各种层、学习时的有效技巧、对图像特别有效的CNN、参数的最优方法等。本章将这些知识汇总起来,创建一个深度网络,挑战MNIST数据集的手写数字识别。