本文是深度学习入门-基于python的理论与实现
这本书的读书笔记。
基础python知识:#
参见我的python & data
感知机(perceptron
):#
这一章中的中所说的感知机应该称为“人工神经元”或“朴素感知机”,但是因为很多基本
的处理都是共通的,所以这里就简单地称为“感知机”。–原文注释。
感知机获取多个输入0
或者1
(一般0
=无输入,1
=有输入),感知机内部对每个输入有权重
当成人工神经元就很好理解了。神经元监听并接受多个信号,内部处理之后决定是否产生信号。感知机应该就是对神经元在机器层面的模仿。
一个最简单的感知机如下所示:
一些简单的例子(逻辑门):#
与门,可以这么实现
显然有很多种方法可以实现与门,不过一般都选择计算方便的。
与非门,可以在与门的基础上修改,只需要将参数全部变成负数即可,此时因为变完负数和之前的区别只在于事件判断的符号方向与之前相反,因而可以实现非得功能。
或门,只要有一个是真即可,因而可以降低激活的阈值
我们可以在软件层面上大费周章地做一个与门,比如使用python来设计一个and function:
1 | def AND(x1,x2): |
一般形式:#
将之前的信号比较模式写成一般形式就是
b称之为偏置(offset),这和之前的表示本质上没有区别。
我们使用numpy来简化我们的操作:
1 | def AND(x1, x2): |
这样我们就不需要写那么长的计算式子了。
进一步来说,偏置调整了神经元被激活的难易程度。
一个反例:#
我们看到,单层感知器对信号的处理是线性的,这导致感知器没有办法直接实现异或门,后者需要非线性的信号处理器。
不过我们可以使用多层感知机来实现一个异或,这是很简单的。根据异或的逻辑,我们可以将其阐述为两件事情至少要做一件,并且不能同时做
,根据这个逻辑,我们使用的门就是or
,nand
,and
来组成。
1 | def XOR(x1,x2): |
异或门是多层感知机,理论上多层层感知机可以用来构建计算器。
神经网络:#
神经网络的出现是为了代替人工对参数进行设置,它可以自动地从数据中学习到合适的权重参数。神经网络中使用sigmoid
函数作为激活函数,我们后面会提到什么是激活函数。
一个例子:#
pass
激活函数:#
根据输入信号的总和转换为输出信号的函数。
前面我们使用的是阶跃函数,我们可以将其实现为
1 | def step_function(x): |
我们还可以简化成为一行
1 | def step_function(x): |
我们还有一些其他的函数可以使用,比如
1 | def sigmoid(x): |
你可以使用如下代码来绘制sigmoid
函数的图像
1 | import matplotlib.pyplot as plt |
二者的区别:#
使用sigmoid
和阶跃函数最大的区别在于,前者是平滑的,决定了神经网络中流动的信息不再是0
或者1
这样的二值信号,而是0
到1
之间的实数值信号。
而两者的相似处在于:输入小的时候,输出接近0
;反之接近1
.同时输入和输出都在0
到1
之间。
最重要的相似处在于,两者均为非线性函数。引用原文中的一段话:
线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐
藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思
考下面这个简单的例子。这里我们考虑把线性函数作为激活
函数,把的运算对应 3 层神经网络 。这个运算会
进行的乘法运算,但是同样的处理可以由
(注意,)这一次乘法运算(即没有隐藏层的神经网络)来表
示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势。因
此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
ReLU
函数:#
ReLU 函数在输入大于 0
时,直接输出该值;在输入小于等于 0
时,输
出 0
.实现如下:
1 | def relu(x): |
使用矩阵来实现神经网络:#
numpy
为我们提供了矩阵计算的api
,具体如下所示:
这里
一个简单的实现
1 | X = np.array([1.0, 0.5]) |
最终实现:#
当然,神经网络一般有很多层,我们这里举一个三层的例子,显示一下使用numpy
的实现代码:
1 | def identity_function(x): |
引用原文对输出层的输出函数的注释:
输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用
sigmoid
函数,多元分类问题可以使用softmax
函数。
对输出层的设计:#
机器学习的问题大致可以分为分类问题和回归问题。分类问题是数据属于哪一个类别的问题。比如,区分图像中的人是男性还是女性的问题就是分类问题。而回归问题是根据某个输入预测一个(连续的)数值的问题。比如,根据一个人的图像预测这个人的体重的问题就是回归问题(类似“57.4kg”这样的预测)。
softmax
函数:#
对计算出的信号softmax
函数如下所示
这很像是一个概率。同时我们也可以看到,softmax
函数不会改变下标对应的元素之间的大小关系。
实现如下:
1 | def softmax(a): |
然而指数函数的运算非常容易溢出,所以我们要做出一些修正。
鉴于我们使用的函数是齐次的,我们上下同时除以
1 | def softmax(a): |
一般推理阶段时输出层的softmax
函数会被省略,而学习阶段则使用。
输出层的神经元数量:#
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字 0 到 9 中的哪一个的问题(10 类别分类问题),可以像图 3-23 这样,将输出层的神经元设定为 10 个。
手写数字识别:#
详细内容见原文。
神经网络的学习:#
核心思路:让机器来决定参数的大小。
从数据中学习:#
数据驱动:#
原文这段话非常有启发意义:
数据是机器学习的命根子。从数据中寻找答案、从数据中发现模式、根据数据讲故事……这些机器学习所做的事情,如果没有数据的话,就无从谈起。因此,数据是机器学习的核心。这种数据驱动的方法,也可以说脱离了过往以人为中心的方法。
通常要解决某个问题,特别是需要发现某种模式时,人们一般会综合考虑各种因素后再给出回答。“这个问题好像有这样的规律性?”“不对,可能原因在别的地方。”——类似这样,人们以自己的经验和直觉为线索,通过反复试验推进工作。而机器学习的方法则极力避免人为介入,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入。
现在我们来思考一个具体的问题,比如如何实现数字“5”的识别。数字 5是图 4-1 所示的手写图像,我们的目标是实现能区别是否是 5 的程序。这个问题看起来很简单,大家能想到什么样的算法呢?
如果让我们自己来设计一个能将 5 正确分类的程序,就会意外地发现这是一个很难的问题。人可以简单地识别出 5,但却很难明确说出是基于何种规律而识别出了5。此外,从图 4-1 中也可以看到,每个人都有不同的写字习惯,要发现其中的规律是一件非常难的工作。因此,与其绞尽脑汁,从零开始想出一个可以识别 5 的算法,不如考虑通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量 ,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括 SIFT、SURF 和 HOG 等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。
机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始想出算法相比,这种方法可以更高效地解决问题,也能减轻人的负担。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的结果。比如,为了区分狗的脸部,人们需要考虑与用于识别 5 的特征量不同的其他特征量。也就是说,即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。
不过我们有神经网络,使用深度学习来完成这个任务。
损失函数:#
为了统计估测的偏差,引入损失函数的概念。
均方误差:#
如下所示
1 | def mean_squared_error(y,t): |
这里,
1 | y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] |
数组元素的索引从第一个开始依次对应数字“0”“1”“2”……这里,神经网络的输出 y 是 softmax 函数的输出。由于 softmax 函数的输出可以理解为概率,因此上例表示“0”的概率是 0.1,“1”的概率是 0.05,“2”的概率是 0.6 等。t 是监督数据,将正确解标签设为 1,其他均设为 0。这里,标签“2”为 1,表示正确解是“2”。将正确解标签表示为 1,其他标签表示为 0 的表示方法称为 one-hot 表示.
交叉熵误差(cross entropy error):#
更近一步理解,这个式子计算的是 神经网络 对 正确项 估计 的误差值,值越大误差越大。
实现如下
1 | def cross_entropy_error(y,t): |
mini-batch:#
机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有 100 个的话,我们就要把这 100 个损失函数的总和作为学习的指标。
以交叉熵误差为例,可以写成
在处理大数据集时,直接计算所有数据的损失函数不仅计算量大,而且可能导致内存不足。因此,我们采用了 mini-batch 学习的方法。在这种方法中,我们从全部数据中随机选择一个小批量(mini-batch)的数据进行训练,而不是每次使用整个数据集。这种方法不仅可以加速计算,还可以通过随机选择不同的 mini-batch 来增强模型的泛化能力。比如,在 MNIST 数据集中,我们可以从 60000 个训练数据中随机选择 100 个样本,来进行一次训练更新。
使用numpy
的随机函数可以实现上述要求。如下所示:
1 | train_size = x_train.shape[0] |
为什么要使用sigmoid
而非阶跃函数:#
我们希望通过微调来进行参数的修正,最好的工具是导数,它引导我们做出正确的参数修正的方向。但是阶跃函数在原点不可导,同时可导处导数处处为0,也就是说参数的微小变化会被阶跃函数抹杀,而不会影响损失函数的值。然而sigmoid
函数是光滑的,这很好。
数值微分:#
所谓数值微分就是用数值方法近似求解函数的导数的过程。——译者注
我们来从程序上实现求导。基于数学式的推导求导数的过程称为解析求导,而利用微小的差分近似导数的过程称为数值微分。
一个数值微分的例子
1 | def numerical_diff(f,x): |
我们也可以使用上述方法求其他的微分,比如多元微分,梯度之类的。
一个梯度的例子:
1 | def numerical_gradient(f,x): |
梯度:#
梯度指出了当前点值变化的最快方向,我们可以使用梯度下降法来寻找最小值(寻找最大值称为梯度上升法)。具体做法是令
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
像学习率这样的参数称为超参数 。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。
为什么是负号而不是正号呢?因为我们希望当前点沿着梯度下降的方向,当某个梯度分量(也即偏导数)是正值的时候,我们应该沿着它的反方向调整当前分量的位置,因而要加个符号。
实现如下:
1 | def gradient_descent(f, init_x, lr=0.01, step_num=100): |
神经网络的梯度:#
对于单层$(23)$的神经网络,其梯度可以使用矩阵微分的理论来表示:
修改各个权重的时候就可以按照这个去修正。
一般的学习过程如下:
选数据-计算梯度-更新参数-重复前三步。这个学习过程就是*SGD
一般对权重参数的初始化符合高斯分布的随机数进行初始化,偏置使用0
初始化。
mini-batch
的实现之前讲过了,就是random_choice
基于测试数据的评价:#
然而虽然在训练中我们可以通过反复学习可以使损失函数的值下降,但是真正评价这个神经网络学习的优劣程度还要看测试上的表现。也就是说评价必须要基于测试数据,看是否会发生拟合得太过了,以至于非训练集数据无法被识别的现象,也即过拟合。
原文:
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。
如何评价泛化程度呢,我们只需要计算,当不断重复训练时,测试精度是否有和训练精度一同上升即可,这表示训练是有效的。引入一个epoch
作为标记训练循环次数的度量,其满足:
一个
epoch
表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000 笔训练数据,用大小为 100 笔数据的 mini-batch 进行学习时,重复随机梯度下降法100 次,所有的训练数据就都被“看过”了 。此时,100 次就是一个epoch
。
实际上,一般做法是事先将所有训练数据随机打乱,然后按指定的批次大小,按序生成mini-batch。这样每个 mini-batch 均有一个索引号,比如此例可以是 0, 1, 2, … , 99,然后用索引号可以遍历所有的 mini-batch。遍历一次所有数据,就称为一个epoch。——译者注
误差反向传播法:#
这个方法能够提高计算权重参数的梯度的效率。为了便于理解这个方法,引入计算图的概念。
计算图是一个有向无环图,在计算图中,边代表操作数,节点代表操作符。一般将原始数据放在左侧,操作符和生成的结果向右传播。这是一个自底向上的计算过程,不过我们人一般在计算抽象问题的时候都是自顶向下的,比如要算一次消费总额,我们往往层层分类商品和计价规则,直到分割成为一个个可执行的单元,然后再自底向上计算。
计算图的优势就是,可以传递局部计算的结果来解决全局问题。局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下俩来的结果。
同时计算图能很好地解释链式法则,节点之间反向传播的信息流就是每一层的偏导数。
我们来实现激活函数层,这里实现relu
和sigmoid
:
1 | class Relu: |
这里有一些细节,每次我们反向做参数调整的分析的时候,我们所用的数据都是本层计算的结果来作为我们调整参数的依据,因而我们要将本层计算的结果存储起来,以供在反向分析的时候使用。
对矩阵求偏导的分析:#
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换” 。因此,这里将进行仿射变换的处理实现为Affine 层。
Affine
层的核心就是一句
从而我们简单地实现:
1 |
数学方法一览#
优化方法:#
主要介绍了SGD
,Momentum
,AdaGrad
,Adam
这些新的优化方法。
权重的初始值:#
权重不能全部初始化为0。
使用sigmoid/tanh
函数或者一些线性函数的时候,推荐使用Xavier
初始值:与前一层有
使用relu
函数,推荐使用He
初始值:与前一层有
batch-normalization:#
强制将产生的激活值的数据的分布正规化,通过在每一层神经网络的输出上进行归一化处理,使得每一层的输入保持稳定,减少了参数初始化的敏感性和梯度消失或爆炸的问题。
设定如下
这样可以将每一组mini-batch
的数据强制转化为均值为0,方差为1的数据集。然后对正规化的数据进行缩放和平移的变化,也即gamma
和beta
是参数,最开始设置成为(1,0)
然后再通过学习调整到合适的值。
解决过拟合:#
权值衰减:#
将损失函数加上权重的2-范数
,具体是:
DropOut:#
每次训练时随机删除神经元,这样可以一定程度上避免某些神经元过度参与。测试的时候虽然会传递所有的神经元信号,但是对于各个神经元的输出要乘以训练时的删除比例再输出。因为神经元整体输出的期望下降了。
超参数的调整:#
一般逐步缩小超参数的范围,比如首先设定一个范围,然后随机采样,进行学习,然后考察测试水瓶。
卷积神经网络:#
卷积神经网络,也即CNN
,被用于图像识别、语音识别等各种场合。
整体结构:#
和之前介绍的神经网络类似,可以层层组装,不过新出现了**卷积层(Convolution)和池化层(Pooling)**。
而CNN
的常见结构则是Conv-ReLU-(Pooling)
+Affine-ReLU
+Affine-Softmax
。
卷积层:#
全连接层忽视了数据的维度,将所有的数据拉平成为一维,而维数常常蕴含着很多信息,尤其是在图像处理中。
卷积层则不会这样,卷积层将保持输入数据的形状(组织形式)不变。
二维卷积运算:#
点积。
术语:#
为了调整输出数据的格式,有时候会先在周围填充一些固定的值,这个操作称为填充(padding),填充的值称为幅度。
应用滤波器的位置间隔称为步幅(stride)
三维卷积运算:#
就是在三个方向上进行卷积。
当然如果有多个滤波器,我们也将这些滤波器得到的图整合成为一个然后输出。
池化层:#
使用 部分数据来表示整体数据,类似于压缩。
常见的有MAX池化
,Average池化
。一般都是MAX池化
。这样可以提高系统的鲁棒性。
实现:#
一般基于im2col
函数展开要卷积的数据和滤波器。简单来说,就是将要卷积的所有东西都展开成为一行乘一列,这样可以使用硬件/算法进行加速。
深度学习:#
具有深层次的神经网络。
历史:#
VGG是比较简单的CNN,其特点在于将有权重的层叠加到了16层/19层
GoogleNet纵向上有深度,横向上使用多个滤波器,最后在合并他们的结果。
ResNet是微软开发的网络,比之前的网络具有更深层次的结构。引入了快捷连接,旨在解决一味增加网络深度而产生的梯度发散。
其他:#
略。
end:#
总结整个书,旨在对深度学习给出一些基本的概念和处理思路,较为浅显和简单,适合作为初学者来泛泛阅读,也能有所收获。