当前位置:   article > 正文

机器学习之神经网络(从神经网络结构到pytorch基本用法)_神经网络输入层结点数与中间层结点数之比

神经网络输入层结点数与中间层结点数之比

机器学习之神经网络(从神经网络结构到pytorch基本用法)

1.神经网络概念

1.1 网络结构

​ 人工神经网络(artificial neural network,ANN),简称神经网络(neural network,NN),是一种模仿生物神经网络的结构和功能的数学模型或计算模型。神经网络由大量的人工神经元联结进行计算。大多数情况下人工神经网络能在外界信息的基础上改变内部结构,是一种自适应系统。现代神经网络是一种非线性统计性数据建模工具,常用来对输入和输出间复杂的关系进行建模,或用来探索数据的模式。

1.1.1神经网络基本结构

在这里插入图片描述

​ 这是一个包含三个层次的神经网络。红色的是输入层,绿色的是输出层,紫色的是中间层(也叫隐藏层)。输入层有3个输入单元,隐藏层有4个单元,输出层有2个单元。

  • 设计一个神经网络时,输入层与输出层的节点数往往是固定的(输入层为输入向量的维度、输出层为输出向量的维度),中间层则可以自由指定;
  • 神经网络结构图中的拓扑与箭头代表着预测过程时数据的流向,跟训练时的数据流有一定的区别;
  • 结构图里的关键不是圆圈(代表“神经元”),而是连接线(代表“神经元”之间的连接)。每个连接线对应一个不同的权重(其值称为权值),这是需要训练得到的。
1.1.2基本组成——神经元

​ 1943年,心理学家McCulloch和数学家Pitts参考了生物神经元的结构,发表了抽象的神经元模型MP。神经元模型是一个包含输入,输出与计算功能的模型。输入可以类比为神经元的树突,而输出可以类比为神经元的轴突,计算则可以类比为细胞核。一个典型的神经元模型可以表示如下:

在这里插入图片描述

​ 连接是神经元中最重要的的东西,每一个连接上都有一个权重。一个神经网络的训练算法就是让权重的值调整到最佳,使得整个网络的预测效果最好。我们使用a来表示输入,用w来表示权值。一个表示连接的有向箭头可以这样理解:在初端,传递的信号大小仍然是a,端中间有加权参数w,经过这个加权后的信号会变成a×w,因此在连接的末端,信号的大小就变成了a×w。在神经元模型中,每个有向箭头表示的是值的加权传递。

在这里插入图片描述

将所有符号用变量来表示:

在这里插入图片描述

可见z是在输入和权值的线性加权和叠加了一个函数g的值。在MP模型里,函数g是sgn函数,也就是取符号函数。这个函数当输入大于0时,输出1,否则输出0。

可以将神经元进行扩展,如下图所示:

在这里插入图片描述

​ 当我们用“神经元”组成网络以后,描述网络中的某个“神经元”时,我们更多地会用“单元”(unit)来指代。同时由于神经网络的表现形式是一个有向图,有时也会用“节点”(node)来表达同样的意思。

​ 这里,三个已知属性的值是a1,a2,a3,未知属性的值是z。z可以通过公式计算出来。已知的属性称之为特征,未知的属性称之为目标。假设特征与目标之间确实是线性关系,并且我们已经得到表示这个关系的权值w1,w2,w3。那么,我们就可以通过神经元模型预测新样本的目标。

1.1.3单层神经网络(感知机)

​ 1958年,计算科学家Rosenblatt提出了由两层神经元组成的神经网络。他给它起了一个名字–“感知器”(Perceptron)。在“感知器”中,有两个层次,分别是输入层和输出层。==输入层里的“输入单元”只负责传输数据,不做计算。输出层里的“输出单元”则需要对前面一层的输入进行计算。==我们把需要计算的层次称之为“计算层”,并把拥有一个计算层的网络称之为“单层神经网络”。一个3输入、2输出的单层神经网络如下图所示:

在这里插入图片描述

​ 记输入变量为列向量 [ a 1 a 2 a 3 ] T [a_1 \quad a_2 \quad a_3]^T [a1a2a3]T,用向量a来表示;输出向量为列向量 [ z 1 z 2 ] T [z_1 \quad z_2]^T [z1z2]T,用向量z来表示;权重为矩阵 [ w 1 , 1 w 1 , 2 w 1 , 3 w 2 , 1 w 2 , 2 w 2 , 3 ] [

w1,1w1,2w1,3w2,1w2,2w2,3
] [w1,1w2,1w1,2w2,2w1,3w2,3],用向量W表示。可以将输出公式表示为:
g ( W ⋅ a ) = z g(W·a)=z g(Wa)=z
依靠这个公式将神经网络中从前一层计算到后一层。

​ 感知器类似一个逻辑回归模型,可以做==线性分类任务==。

1.1.4两层神经网络(多层感知机)

​ 1986年,RumelharHinton等人提出了反向传播(Backpropagation,BP)算法,解决了两层神经网络所需要的复杂计算量问题,从而带动了业界使用两层神经网络研究的热潮。两层神经网络除了包含一个输入层,一个输出层以外,还增加了一个中间层。此时,中间层和输出层都是计算层。

在这里插入图片描述

使用矩阵运算来表达整个计算公式的话如下:
g ( W ( 1 ) ⋅ a ( 1 ) ) = a ( 2 ) g(W^{(1)}·a^{(1)})=a^{(2)} g(W(1)a(1))=a(2)

g ( W ( 2 ) ⋅ a ( 2 ) ) = z g(W^{(2)}·a^{(2)})=z g(W(2)a(2))=z

此外,考虑偏置节点,它本质上是一个只含有存储功能,且存储值永远为1的单元。在神经网络的每个层次中,除了输出层以外,都会含有这样一个偏置单元。偏置单元与后一层的所有节点都有连接,我们设这些参数值为向量b,称之为偏置。

在这里插入图片描述

在考虑了偏置以后的一个神经网络的矩阵运算如下:
g ( W ( 1 ) ⋅ a ( 1 ) + b ( 1 ) ) = a ( 2 ) g ( W ( 2 ) ⋅ a ( 2 ) + b ( 2 ) ) = z g(W^{(1)}·a^{(1)}+b^{(1)})=a^{(2)}\\ g(W^{(2)}·a^{(2)}+b^{(2)})=z g(W(1)a(1)+b(1))=a(2)g(W(2)a(2)+b(2))=z

​ 在两层神经网络中,我们不再使用sgn函数作为函数g,而是使用平滑函数sigmoid作为函数g。我们把函数g也称作激活函数(active function)。事实上,神经网络的本质就是通过参数与激活函数来拟合特征与目标之间的真实函数关系

​ 理论证明,两层神经网络可以无限逼近任意连续函数。也就是说,面对**复杂的非线性分类任务,两层(带一个隐藏层)神经网络可以分类的很好。从输入层到隐藏层时,数据发生了空间变换。也就是说,两层神经网络中,隐藏层对原始的数据进行了一个空间变换,使其可以被线性分类,然后输出层的决策分界划出了一个线性分类分界线,对其进行分类。这样就导出了两层神经网络可以做非线性分类的关键——隐藏层。联想到我们一开始推导出的矩阵公式,我们知道,矩阵和向量相乘,本质上就是对向量的坐标空间进行一个变换。因此,隐藏层的参数矩阵的作用就是使得数据的原始坐标空间从线性不可分,转换成了线性可分。两层神经网络通过两层的线性模型模拟了数据内真实的非线性函数。因此,多层的神经网络的本质就是复杂函数拟合**。

​ 在设计一个神经网络时,输入层的节点数需要与特征的维度匹配,输出层的节点数要与目标的维度匹配。而中间层的节点数,却是由设计者指定的。因此,“自由”把握在设计者的手中。但是,节点数设置的多少,却会影响到整个模型的效果。如何决定这个自由层的节点数呢?目前业界没有完善的理论来指导这个决策。一般是根据经验来设置。较好的方法就是预先设定几个可选值,通过切换这几个值来看整个模型的预测效果,选择效果最好的值作为最终选择。这种方法又叫做Grid Search(网格搜索)

1.1.5多层神经网络(深度学习)

​ 2006年,Hinton在《Science》和相关期刊上发表了论文,首次提出了“深度信念网络”的概念。与传统的训练方式不同,“深度信念网络”有一个“预训练”(pre-training)的过程,这可以方便的让神经网络中的权值找到一个接近最优解的值,之后再使用“微调”(fine-tuning)技术来对整个网络进行优化训练。这两个技术的运用大幅度减少了训练多层神经网络的时间。他给多层神经网络相关的学习方法赋予了一个新名词–“深度学习”。

​ 延续两层神经网络的方式来设计一个多层神经网络。在两层神经网络的输出层后面,继续添加层次。原来的输出层变成中间层,新加的层次成为新的输出层。

在这里插入图片描述

依照这样的方式不断添加,我们可以得到更多层的多层神经网络。公式推导的话其实跟两层神经网络类似,使用矩阵运算的话就仅仅是加一个公式而已。推导公式如下:
g ( W ( 1 ) ⋅ a ( 1 ) + b ( 1 ) ) = a ( 2 ) g ( W ( 2 ) ⋅ a ( 2 ) + b ( 2 ) ) = a ( 3 ) g ( W ( 3 ) ⋅ a ( 3 ) + b ( 3 ) ) = z g(W^{(1)}·a^{(1)}+b^{(1)})=a^{(2)}\\ g(W^{(2)}·a^{(2)}+b^{(2)})=a^{(3)}\\ g(W^{(3)}·a^{(3)}+b^{(3)})=z\\ g(W(1)a(1)+b(1))=a(2)g(W(2)a(2)+b(2))=a(3)g(W(3)a(3)+b(3))=z
​ 多层神经网络中,输出也是按照一层一层的方式来计算。从最外面的层开始,算出所有单元的值以后,再继续计算更深一层。只有当前层所有单元的值都计算完毕以后,才会算下一层。有点像计算向前不断推进的感觉。所以这个过程叫做“正向传播”。

  • 网络参数个数的计算

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

与两层神经网络不同,多层神经网络中的层数增加了很多。增加更多的层次有什么好处?更深入的表示特征,以及更强的函数模拟能力。

​ 更深入的表示特征可以这样理解,随着网络的层数增加,每一层对于前一层次的抽象表示更深入。在神经网络中,每一层神经元学习到的是前一层神经元值的更抽象的表示。例如第一个隐藏层学习到的是**“边缘”的特征,第二个隐藏层学习到的是由“边缘”组成的“形状”的特征,第三个隐藏层学习到的是由“形状”组成的“图案”的特征,最后的隐藏层学习到的是由“图案”组成的“目标”**的特征。通过抽取更抽象的特征来对事物进行区分,从而获得更好的区分与分类能力。

​ 更强的函数模拟能力是由于随着层数的增加,整个网络的参数就越多。而神经网络其实本质就是模拟特征与目标之间的真实关系函数的方法,更多的参数意味着其模拟的函数可以更加的复杂,可以有更多的容量(capcity)去拟合真正的关系。

通过研究发现,在参数数量一样的情况下,更深的网络往往具有比浅层的网络更好的识别效率。

​ 在单层神经网络时,我们使用的激活函数是sgn函数。到了两层神经网络时,我们使用的最多的是sigmoid函数。而到了多层神经网络时,通过一系列的研究发现,ReLU函数在训练多层神经网络时,更容易收敛,并且预测性能更好。在深度学习中,泛化技术变的比以往更加的重要。这主要是因为神经网络的层数增加了,参数也增加了,表示能力大幅度增强,很容易出现过拟合现象。因此正则化技术就显得十分重要。

1.2权重更新(训练)

​ 从两层神经网络开始,神经网络的研究人员开始使用机器学习相关的技术进行神经网络的训练。例如用大量的数据(1000-10000左右),使用算法进行优化等等,从而使得模型训练可以获得性能与数据利用上的双重优势。

​ 机器学习模型训练的目的,就是使得参数尽可能的与真实的模型逼近。具体做法是这样的。首先给所有参数赋上随机值。我们使用这些随机生成的参数值,来预测训练数据中的样本。样本的预测目标为yp,真实目标为y。那么,定义一个值Loss,计算公式如下:
L o s s = ( y p − y ) 2 Loss=(y_p-y)^2 Loss=(ypy)2
这个值称之为损失(Loss),我们的目标就是使对所有训练数据的损失和尽可能的小。如果将先前的神经网络预测的矩阵公式带入到*** y p y_p yp中(因为有z= y p y_p yp),那么我们可以把损失写为关于参数(parameter)的函数,这个函数称之为损失函数*(loss function)。

​ 此时,就将问题转化为如何优化参数,能够让损失函数的值最小?

在神经网络模型中,由于结构复杂,每次计算梯度的代价很大。因此还需要使用反向传播算法。反向传播算法是利用了神经网络的结构进行的计算。不一次计算所有参数的梯度,而是从后往前。首先计算输出层的梯度,然后是第二个参数矩阵的梯度,接着是中间层的梯度,再然后是第一个参数矩阵的梯度,最后是输入层的梯度。计算结束以后,所要的两个参数矩阵的梯度就都有了。

在这里插入图片描述

​ 反向传播算法的启示是数学中的链式法则。优化问题只是训练中的一个部分。机器学习问题之所以称为学习问题,而不是优化问题,就是因为它不仅要求数据在训练集上求得一个较小的误差,在测试集上也要表现好。因为模型最终是要部署到没有见过训练数据的真实场景。提升模型在测试集上的预测效果的主题叫做泛化(generalization),相关方法被称作正则化(regularization)。神经网络中常用的泛化技术有权重衰减等。

1.2.1权重更新表达式

​ 网络通过求导得到的反向传播的误差为: ∂ L o s s ∂ W \frac{\partial Loss}{\partial W} WLoss

​ 在神经网络反向传播过程中,学习率表示为: η \eta η

​ 权重更新表达式为:
W + = W − η ∂ L o s s ∂ W W^+=W-\eta\frac{\partial Loss}{\partial W} W+=WηWLoss
​ 偏置更新表达式为:
b + = b − η ∂ L o s s ∂ W b^+=b-\eta\frac{\partial Loss}{\partial W} b+=bηWLoss

####1.2.2权重更新的数学意义

​ Loss对W的导数的用处是告诉了我们更新权重的方向,即计算的时候到底是进行加法运算还是减法运算,而真正控制权重更新数值的是学习率η,也叫步长,就是每次更新数值的大小。虽然我们选择的学习率η始终是一个定值,但是越接近最值的时候,这个坡度就会越缓,从而导数的值就越小,也就是乘积变小了,这就是看到步长变小的缘故。

1.2.3权重更新的几何意义

在这里插入图片描述

​ 如上图所示,如果图中的点P想走到最低点,点P就要向x轴的左测移动,即要加上一个负数,此时斜率为正数,因此更新时要减去(学习率 * 偏导数)。

​ 同理,当点P在最低点左侧时,想到到达最低点,点P需要向x轴右移动,即要加上一个正数,此时斜率为负数,因此更新时要减去(学习率 * 偏导数)。

​ 通过求导,找到点P运动的方向(导数),在设置一个每次移动的距离(学习率η ),确定了这两个因素,我们就能一步一步的走向最低点。

2.常用的激活函数

​ 神经网络中的每个神经元节点接受上一层神经元的输出值作为本神经元的输入值,并将输入值传递给下一层,输入层神经元节点会将输入属性直接传递到下一层(隐层或输出层)。在多层神经网络中,上层节点的输出和下层节点的输入之间具有一个函数关系,这个函数称为激活函数。加入激活函数可以加入非线性因素和充分组合特征

2.1 Sigmoid函数

​ Sigmoid函数时使用范围最广的一类非线性激活函数,具有指数函数的形状,它在物理意义上最为接近生物神经元。其自身的缺陷,最明显的就是饱和性(函数导数的趋于无穷的极限为0)。从函数图可以看到,其两侧导数逐渐趋近于0,杀死梯度。

在这里插入图片描述

Sigmoid激活函数和导函数如下:

在这里插入图片描述

对应的图像如下:在这里插入图片描述

  • 优点:

    ​ 把一个实数(输入的连续实值)压缩到0到1之间,当输入的数字非常大的时候,结果会接近1,当输入非常大的负数时,则会得到接近0的结果。在早期的神经网络中使用地非常多,因为它很好地解释了神经元受到刺激后是否被激活和向后传递的场景(0:几乎没有被激活;1:完全被激活)

  • 缺点:

    ​ 使用Sigmoid函数容易出现梯度弥散或者梯度饱和。当神经网络的层数很多时,如果每一层的激活函数都采用Sigmoid函数的话,就会产生梯度弥散梯度爆炸的问题,其中梯度爆炸发生的概率非常小,而梯度消失发生的概率比较大。还有Sigmoid函数的output不是0均值(zero-centered),这是不可取的,因为这会导致后一层的神经元将得到上一层输出的非 0 均值的信号作为输入。这样在反向传播的过程中 w 要么都往正方向更新,要么都往负方向更新,导致一种捆绑的效果,使得收敛缓慢。最后就是对其解析式中含有幂函数,计算机求解时相对比较耗时,对于规模比较大的深度网络,这会较大的增加训练时间

2.2 tahn函数

​ 正切函数时非常常见的激活函数,与Sigmoid函数相比,它的输出均值是0,使得其收敛速度要比Sigmoid快,减少迭代次数。相对于Sigmoid的好处是它的输出的均值为0,但是当饱和的时候还是会杀死梯度。

在这里插入图片描述

tanh激活函数和导函数分别如下:

在这里插入图片描述

对应的图像如下:

在这里插入图片描述

​ 在神经网络的应用中,tanh通常要优于Sigmoid的,因为 tanh的输出在 -1~1之间,均值为0,更方便下一层网络的学习。但是有一个例外,如果做二分类,输出层可以使用 Sigmoid,因为它可以算出属于某一类的概率。tanh 解决了Sigmoid函数的不是 zero-centered 输出问题,tanh 函数将输入值压缩到 -1 和 1 之间。该函数与 Sigmoid 类似,也存在着梯度弥散或梯度饱和和幂运算的缺点。

2.3 ReLU函数

​ 针对Sigmoid函数和tanh的缺点,提出ReLU函数。线性整流函数(Rectified Linear Unit, ReLU),又称修正线性单元,是一种人工神经网络中常用的激活函数(activation function),通常指代以斜坡函数及其变种为代表的非线性函数。最近几年比较受欢迎的一个激活函数,无饱和区,收敛快,计算简单,有时候会比较脆弱,如果变量的更新太快,还没有找到最佳值,就进入小于零的分段就会使得梯度变为零,无法更新直接死掉了

在这里插入图片描述

ReLU激活函数和导函数如下:

在这里插入图片描述

对应的图像如下:在这里插入图片描述

  • 优点:

    1,解决了gradient vanishing (梯度消失)问题(在正区间)

    2,计算方便,求导方便,计算速度非常快,只需要判断输入是否大于0

    3,收敛速度远远大于 Sigmoid函数和 tanh函数,可以加速网络训练

  • 缺点:

    1,由于负数部分恒为零,会导致一些神经元无法激活

    2,输出不是以0为中心

ReLU 也有几个需要特别注意的问题:

1,ReLU 的输出不是 zero-centered

2,Dead ReLU Problem,指的是某些神经元可能永远不会被激活,导致相应的参数永远不会被更新,有两个主要原因可能导致这种情况产生:

(1) 非常不幸的参数初始化,这种情况比较少见

(2) learning rate 太高,导致在训练过程中参数更新太大,不幸使网络进入这种状态。

解决方法是可以采用 Xavier 初始化方法,以及避免将 learning rate 设置太大或使用 adagrad 等自动调节 learning rate 的算法。尽管存在这两个问题,ReLU目前仍然是最常见的 activation function,在搭建人工神经网络的时候推荐优先尝试

​ 3、间断点的求导按左导数来计算,也就是默认情况下,ReLU的间断点处的导数认为是0。

2.4 Leaky ReLU函数

​ Leaky ReLU解决了ReLU会杀死一部分神经元的情况。Leaky ReLU 是给所有负值赋予一个非零斜率。Leaky ReLU 激活函数是在声学模型(2013)中首次提出。

在这里插入图片描述

Leaky ReLU激活函数和导函数如下:

在这里插入图片描述

对应的图像分别如下:

在这里插入图片描述

​ 提出了将 ReLU 的前半段设为 ax 而非0,通常 a = 0.01,另外一种直观的想法是基于参数的方法,即 ParmetricReLU : f(x)=max(ax, x),其中 a 可由方向传播算法学出来。理论上来说,Leaky ReLU 有ReLU的所有优点,外加不会有 Dead ReLU 问题,但是在实际操作当中,并没有完全证明 Leaky ReLU 总是好于 ReLU

Leaky ReLU 主要是为了避免梯度消失,当神经元处于非激活状态时,允许一个非0的梯度存在,这样不会出现梯度消失,收敛速度快。他的优缺点和ReLU类似。

2.5 Maxout函数

​ Maxout 是深度学习网络中的一层网络,就像池化层,卷积层一样,我们可以把 maxout 看成是网络的激活函数层,我们假设网络某一层的输入特征向量为: X=(x1, x2, …xd),也就是我们输入的 d 个神经元,则maxout隐藏层中神经元的计算公式如下:

在这里插入图片描述

Maxout也是近些年非常流行的激励函数,简单来说,它是ReLU和Leaky ReLU的一个泛化版本,当w1、b1设置为0时,便转换为ReLU公式。它优于ReLU的优点而且没有死区,但是它的参数数量却增加了一倍。

2.6 其他

  • PReLU(参数化修正线性单元)

在这里插入图片描述

在这里插入图片描述

  • RReLU(随机矫正线性单元)

在这里插入图片描述

  • ReLU6 函数

在这里插入图片描述

  • ELU 函数(Exponential Linear Unit)

在这里插入图片描述

  • SELU(Scaled ELU)

在这里插入图片描述

  • Swish 激活函数

在这里插入图片描述

  • Mish 激活函数

在这里插入图片描述

在这里插入图片描述

  • Softmax 函数

在这里插入图片描述

  • Softplus函数

在这里插入图片描述

在这里插入图片描述

2.7 小结

如何选取合适的激活函数

一般我们可以这样:

1,首先尝试ReLU,速度快,但是要注意训练的状态

2,如果ReLU效果欠佳,尝试Leaky ReLU 或者 Maxout 等变种

3,尝试 tanh正切函数(以零为中心,零点处梯度为1)

4,Sigmoid tanh 在RMM(LSTM 注意力机制等)结构中有所应用,作为门控或者概率值

5,在浅层神经网络中,如不超过四层,可选择使用多种激励函数,没有太大的影响

深度学习中往往需要大量时间来处理大量数据,模型的收敛速度是尤为重要的。所以,总体上来讲,训练深度学习网络尽量使用 zero-centered 数据(可以经过数据预处理实现)和 zero-centered 输出。所以要尽量选择输出具有 zero-centered 特点的激活函数以加快模型的收敛速度。

如果是使用 ReLU,那么一定要小心设置 learning rate,而且要注意,不要让网络出现很多“dead”神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU ,PReLU , 或者 Maxout。

最好不要用 Sigmoid函数,不过可以试试 tanh,不过可以预期它的效果会比不上 ReLU和 maxout。

3.常见的损失函数

3.1 介绍

​ 损失函数有助于优化神经网络的参数。我们的目标是通过优化神经网络的参数(权重)来最大程度地减少神经网络的损失。通过神经网络将目标(实际)值与预测值进行匹配,再经过损失函数就可以计算出损失。然后,我们使用梯度下降法来优化网络权重,以使损失最小化。这就是我们训练神经网络的方式。

3.2 L1、MAE

L1范数损失函数,也被称为最小绝对值偏差(LAD),最小绝对值误差(LAE)。它是把目标值 t t t与预测值 y y y绝对差值的总和S最小化:
S = ∑ i = 1 n ∣ y i − t i ∣ S=\sum^n_{i=1} |y_i-t_i| S=i=1nyiti
​ 平均绝对误差(MAE)是一种常用的回归损失函数,它是目标值与预测值之差绝对值和的均值,表示了预测值的平均误差幅度,而不需要考虑误差的方向。 t t t表示目标值, y y y表示预测值:
M A E = 1 n ∑ i = 1 n ∣ y i − t i ∣ MAE=\frac{1}{n}\sum^n_{i=1} |y_i-t_i| MAE=n1i=1nyiti

  • 优点:相比于MSE,MAE有个优点就是,对于离群点不那么敏感。因为MAE计算的是误差(y-f(x))的绝对值,对于任意大小的差值,其惩罚都是固定的。无论对于什么样的输入值,都有着稳定的梯度,不会导致梯度爆炸问题,具有较为稳健性的解。
  • 缺点:MAE曲线连续,但是在(y-f(x)=0)处不可导。而且 MAE 大部分情况下梯度都是相等的,这意味着即使对于小的损失值,其梯度也是大的。这不利于函数的收敛和模型的学习

3.3 L2、MSE

L2范数损失函数,也被称为最小平方误差(LSE)。它是把目标值y与估计值f(xi)的差值的平方和S最小化:
S = ∑ i = 1 n ( y i − t i ) 2 S=\sum^n_{i=1} (y_i-t_i)^2 S=i=1n(yiti)2
​ 当执行回归任务时,可以选择该损失函数。这种损失是通过计算实际(目标)值和预测值之间的平方差的平均值来计算的。 t t t表示目标值, y y y表示预测值:
M S E = 1 n ∑ i = 1 n ( y i − t i ) 2 MSE=\frac{1}{n}\sum^n_{i=1} (y_i-t_i)^2 MSE=n1i=1n(yiti)2

  • 优点:MSE的函数曲线光滑、连续,处处可导,便于使用梯度下降算法,是一种常用的损失函数。 而且,随着误差的减小,梯度也在减小,这有利于收敛,即使使用固定的学习速率,也能较快的收敛到最小值
  • 缺点:当真实值y和预测值f(x)的差值大于1时,会放大误差;而当差值小于1时,则会缩小误差,这是平方运算决定的。MSE对于较大的误差(>1)给予较大的惩罚,较小的误差(<1)给予较小的惩罚。对离群点比较敏感,受其影响较大

3.4 Smooth L1(Huber)

​ smooth L1说的是光滑之后的L1,前面说过了L1损失的缺点就是有折点,不光滑,smooth L1损失函数为:
l o s s ( x , y ) = 1 n ∑ i z i z i = { 0.5 ( x i − y i ) 2 , i f ∣ x i − y i ∣ < 1 ∣ x i − y i ∣ − 0.5 , o t h e r w i s e loss(x,y)=\frac 1 n \sum_i z_i\\ z_i=\left\{

0.5(xiyi)2,if|xiyi|<1|xiyi|0.5,otherwise
\right. loss(x,y)=n1izizi={0.5(xiyi)2,ifxiyi<1xiyi0.5,otherwise

  • 特点:差别过大时,梯度值不至于过大;差别很小时,梯度值足够小。

3.4 Cross-Entropy

​ 交叉熵损失函数,主要应用在分类任务中, t t t为目标值, y y y为预测值。交叉熵公式:
H ( p , q ) = − ∑ i t i l o g y i H(p,q)=-\sum_i t_ilogy_i H(p,q)=itilogyi
常与softmax函数一起使用:
y i = s o f t m a x ( z i ) = e z i ∑ j e z i y_i=softmax(z_i)=\frac{e^{z_i}}{\sum_je^{z_i}} yi=softmax(zi)=jeziezi

  • 特点:在模型效果差的时候学习速度比较快,在模型效果好的时候学习速度变慢。

3.5 小结

​ 对于大多数CNN网络,我们一般是使用L2-loss而不是L1-loss,因为L2-loss的收敛速度要比L1-loss要快得多。

​ 使用Smooth L1时,相比于L1损失函数,可以收敛得更快;相比于L2损失函数,对离群点、异常值不敏感,梯度变化相对更小,训练时不容易跑飞。

​ 当使用sigmoid作为激活函数的时候,常用交叉熵损失函数。

4 常见的优化器

​ 在训练模型时,可以基于梯度使用不同的优化器(optimizer,或者称为“优化算法”)来最小化损失函数。

4.1 SGD

随机梯度下降,SGD 每次只使用一个训练样本来进行梯度更新:
θ = θ − η Δ θ J ( θ ; x ( i ) ; y ( i ) ) \theta = \theta-\eta\Delta_\theta J(\theta;x^{(i)};y^{(i)}) θ=θηΔθJ(θ;x(i);y(i))
其中, J ( θ ; x ( i ) ; y ( i ) ) J(\theta;x^{(i)};y^{(i)}) J(θ;x(i);y(i)) 是只根据样本 ( x ( i ) ; y ( i ) ) (x^{(i)};y^{(i)}) (x(i);y(i)) 计算出的损失。

  • 优点
    • SGD 每次只根据一个样本计算梯度,速度较快;
    • SGD 可以根据新样本实时地更新模型;
  • 缺点
    • SGD 在优化的过程中损失的震荡会比较严重;

4.2 Adam

自适应矩估计,Adam 使用梯度的指数加权平均(一阶矩估计)和梯度平方的指数加权平均(二阶矩估计)来动态地调整每个参数的学习率。
m t = β m t − 1 + ( 1 − β ) g t n t = γ n t − 1 + ( 1 − γ ) g t 2 m_t=\beta m_{t-1}+(1-\beta)g_t\\ n_t=\gamma n_{t-1}+(1-\gamma)g_t^2\\ mt=βmt1+(1β)gtnt=γnt1+(1γ)gt2
其中, m t , n t m_t,n_t mt,nt 分别是梯度的指数加权平均(一阶矩估计)和梯度平方的指数加权平均(二阶矩估计)。然后,对 m ^ t \hat m_t m^t n ^ t \hat n_t n^t 进行偏差修正:
m ^ t = m t 1 − β t n ^ t = n t 1 − γ t \hat m_t=\frac{m_t}{1-\beta^t}\\ \hat n_t=\frac{n_t}{1-\gamma^t}\\ m^t=1βtmtn^t=1γtnt
m t , n t m_t,n_t mt,nt分别是梯度的一阶矩估计和二阶矩估计,可以看做是对期望 E [ g ] t E[g]_t E[g]t E [ g 2 ] t E[g^2]_t E[g2]t的估计。通过偏差修正, m ^ t \hat m_t m^t n ^ t \hat n_t n^t可以看做是期望的无偏估计。最后,梯度的更新方法为:
θ = θ − η n ^ t + ϵ m ^ t \theta = \theta - \frac{\eta}{\sqrt{\hat n_t}+\epsilon}\hat m_t θ=θn^t +ϵηm^t
在使用中, β β β通常设为 0.9, γ γ γ通常设为 0.999, ϵ ϵ ϵ通常设为 1 0 − 8 10^{-8} 108

5 pytorch基本用法

理解pytorch的基础主要从以下三个方面

  • Numpy风格的Tensor操作。pytorch中tensor提供的API参考了Numpy的设计,因此熟悉Numpy的用户基本上可以无缝理解,并创建和操作tensor,同时torch中的数组和Numpy数组对象可以无缝的对接。
  • 变量自动求导。在一序列计算过程形成的计算图中,参与的变量可以方便的计算自己对目标函数的梯度。这样就可以方便的实现神经网络的后向传播过程。
  • 神经网络层与损失函数优化等高层封装。网络层的封装存在于torch.nn模块,损失函数由torch.nn.functional模块提供,优化函数由torch.optim模块提供。

5.1 Tensor张量

​ Tensor是神经网络框架中重要的基础数据类型,可以简单理解为N维数组的容器对象。tensor之间的通过运算进行连接,从而形成计算图。

  • tensor的常见创建接口
方法名说明
tensor()直接从参数构造一个的张量,参数支持list,numpy数组
eye(row, column)创建指定行数,列数的二维单位tensor
linspace(start,end,count)在区间[s,e]上创建c个tensor
logspace(s,e,c)在区间[10^s, 10^e]上创建c个tensor
ones(*size)t返回指定shape的张量,元素初始为1
zeros(*size)返回指定shape的张量,元素初始为0
ones_like(t)返回与t的shape相同的张量,且元素初始为1
zeros_like(t)返回与t的shape相同的张量,且元素初始为0
arange(s,e,sep)在区间[s,e)上以间隔sep生成一个序列张量
  • 随机采样
方法名说明
rand(*size)在区间[0,1)返回一个均匀分布的随机数张量
uniform(s,e)在指定区间[s,e]上生成一个均匀分布的张量
randn(*size)返回正态分布N(0,1)取样的随机数张量
normal(means, std)返回一个正态分布N(means, std)
  • 序列化
方法名说明
save(obj, path)张量对象的保存,通过pickle进行
load(path)从文件中反序列化一个张量对象
  • 数学操作

这些方法均为逐元素处理方法

方法名说明
abs绝对值
add加法
addcdiv(t, v, t1, t2)t1与t2的按元素除后,乘v加t
addcmul(t, v, t1, t2)t1与t2的按元素乘后,乘v加t
ceil向上取整
floor向下取整
clamp(t, min, max)将张量元素限制在指定区间
exp指数
log对数
pow
mul逐元素乘法
neg取反
sigmoid
sign取符号
sqrt开根号
tanh

注:这些操作均创建新的tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。

  • 归约方法
方法名说明
cumprod(t, axis)在指定维度对t进行累积
cumsum(t, axis)在指定维度对t进行累加
dist(a,b,p=2)返回a,b之间的p阶范数
mean均值
median中位数
std标准差
var方差
norm(t,p=2)返回t的p阶范数
prod(t)返回t所有元素的积
sum(t)返回t所有元素的和
  • 比较方法
方法名说明
eq比较tensor是否相等,支持broadcast
equal比较tensor是否有相同的shape与值
ge/le大于/小于比较
gt/lt大于等于/小于等于比较
max/min(t,axis)返回最值,若指定axis,则额外返回下标
topk(t,k,axis)在指定的axis维上取最高的K个值
  • 其他操作
方法名说明
cat(iterable, axis)在指定的维度上拼接序列
chunk(tensor, c, axis)在指定的维度上分割tensor
squeeze(input,dim)将张量维度为1的dim进行压缩,不指定dim则压缩所有维度为1的维
unsqueeze(dim)squeeze操作的逆操作
transpose(t)计算矩阵的转置换
cross(a, b, axis)在指定维度上计算向量积
diag返回对角线元素
hist(t, bins)计算直方图
trace返回迹
  • 矩阵操作
方法名说明
dot(t1, t2)计算张量的内积
mm(t1, t2)计算矩阵乘法
mv(t1, v1)计算矩阵与向量乘法
qr(t)计算t的QR分解
svd(t)计算t的SVD分解
  • tensor对象的方法
方法名作用
size()返回张量的shape属性值
numel(input)计算tensor的元素个数
view(*shape)修改tensor的shape,与np.reshape类似,view返回的对象共享内存
resize类似于view,但在size超出时会重新分配内存空间
item若为单元素tensor,则返回python的scalar
from_numpy从numpy数据填充
numpy返回ndarray类型

5.2 使用pytorch实现线性回归

import torch
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt

def get_fake_data(batch_size=32):
    ''' y=x*2+3 '''
    x = torch.randn(batch_size, 1) * 10
    # y = x * 2 + 3 + torch.randn(batch_size, 1)
    y = x * 2 + 3 + torch.tensor(np.random.normal(0, 0.01, size=1), dtype=torch.float32)  # 服从均值为0、标准差为0.01的正态分布。
    return x, y

x, y = get_fake_data()

net = torch.nn.Linear(1, 1)  # 线性回归器输入1维、输出1维
loss_func = torch.nn.MSELoss()  # MSE损失函数
# print(net.parameters())
optimzer = optim.SGD(net.parameters(), lr=0.001)  # SGD优化器,学习率0.001

for i in range(4000): # 迭代4000次
    optimzer.zero_grad()
    out = net(x)  # 计算net输出
    loss = loss_func(out, y)  # 计算损失
    loss.backward()  # 求梯度
    optimzer.step()  # 迭代模型

w, b = [param.item() for param in net.parameters()]
print(w)  # 2.000062942504883
print(b)  # 2.9966135025024414

# print(y)
# print(x * w + b)
# 显示原始点与拟合直线
plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
plt.plot(x.squeeze().numpy(), (x * w + b).squeeze().numpy())
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

5.3自动求导

​ 在目前的深度学习框架(PyTorch,Tensorflow,MXnet)中,自动求导功能是最核心也是最基础的功能。构建与训练深度学习的基本流程是:根据网络结构逐步搭建计算图,然后求得损失函数,之后根据计算图来计算导数,最后利用梯度下降方法更新参数。

在torch.autogard中。

5.3.1默认求导规则

​ 在pytorch里面,默认:**只能是【标量】对【标量】,或者【标量】对【向量/矩阵】求导!**对于每个tensor对象,有下面几个变量控制求导的属性:

变量作用
requirs_grad默认为False,表示变量是否需要计算导数
grad_fn变量的梯度函数
grad变量对应的梯度
  • 标量对标量求导
import torch

# 创建一个多元函数,即z=x**2+y**3,x不可求导,W,b设置可求导
x = torch.tensor(3.0, requires_grad=True)
y = torch.tensor(4.0, requires_grad=False)

z = torch.pow(x,2) + torch.pow(y,3)

# 判断每个tensor是否是可以求导的
print(x.requires_grad) # True
print(y.requires_grad) # False
print(z.requires_grad) # True

# 求导,通过backward函数来实现
z.backward() # 求导

# 查看导数,也即所谓的梯度
print(x.grad) # tensor(6.)
print(y.grad) # None
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 标量对向量求导
import torch

#创建一个多元函数,即Y=XW+b=Y=x1*w1+x2*w2*x3*w3+b,x不可求导,W,b设置可求导
X=torch.tensor([1.5,2.5,3.5],requires_grad=False)
W=torch.tensor([0.2,0.4,0.6],requires_grad=True)
b=torch.tensor(0.1,requires_grad=True)
Y=torch.add(torch.dot(X,W),b) # add()张量相加,dot()张量点乘
 
 
#判断每个tensor是否是可以求导的
print(X.requires_grad) # False
print(W.requires_grad) # True
print(b.requires_grad) # True
print(Y.requires_grad) # True
 
 
#求导,通过backward函数来实现
Y.backward()  
 
#查看导数,也即所谓的梯度
print(W.grad) # tensor([1.5000, 2.5000, 3.5000])
print(b.grad) # tensor(1.)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 标量对矩阵求导
import torch
'''
x 是一个(2,3)的矩阵,设置为可导,是叶节点,即leaf variable
y 是中间变量,由于x可导,所以y可导
z 是中间变量,由于x,y可导,所以z可导
f 是一个求中值函数,最终得到的是一个标量scaler
'''

x = torch.tensor([[1.,2.,3.],[4.,5.,6.]],requires_grad=True)
y = torch.add(x,1)
z = 2*torch.pow(y,2)
f = torch.mean(z) 

print(x.requires_grad)
print(y.requires_grad)
print(z.requires_grad)
print(f.requires_grad)

f.backward()
print(x.grad)
'''
True
True
True
True
tensor([[1.3333, 2.0000, 2.6667],
        [3.3333, 4.0000, 4.6667]])
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
5.3.2向量/矩阵对向量/矩阵求导——通过backward的第一个参数gradient来实现
import torch
'''
x 是一个(2,3)的矩阵,设置为可导,是叶节点,即leaf variable
y 也是一个(2,3)的矩阵,即
y=x2+x (x的平方加x)
实际上,就是要y的各个元素对相对应的x求导
'''
 
x = torch.tensor([[1.,2.,3.],[4.,5.,6.]],requires_grad=True)
y = torch.add(torch.pow(x,2),x)
 
# gradient参数的维度与最终的函数y保持一样的形状,每一个元素表示当前这个元素所对应的权重
gradient=torch.tensor([[1.0,1.0,1.0],[1.0,1.0,1.0]])
y.backward(gradient) 
print(x.grad)
'''
tensor([[ 3.,  5.,  7.],
        [ 9., 11., 13.]])
'''

# [1.0,0.1,0.01]对应导数保持、缩小了10倍、100倍
gradient=torch.tensor([[1.0,0.1,0.01],[1.0,1.0,1.0]])
y.backward(gradient)
print(x.grad)
'''
tensor([[ 3.0000,  0.5000,  0.0700],
        [ 9.0000, 11.0000, 13.0000]])
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
5.3.3 自动求导函数backward的第二、第三个参数
  • 保留运算图——retain_graph

    ​ 在构建函数关系的时候,特别是多个复合函数的时候,会有一个运算图,比如下面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyeVvlYw-1637310470095)(Part.4 神经网络.assets/image-20211115213035955.png)]

你先求p求导,那么这个过程就是反向的p对y求导,y对x求导。 求导完毕之后,这三个节点构成的计算子图就会被释放。那么计算图就只剩下z、q了,已经不完整,无法求导了。 所以这个时候,无论你是想再次运行p.backward()还是q.backward(),都无法进行因为x已经被销毁了。

​ 可以通过设置 retain_graph=True 来保留计算图,即更改你的backward函数,添加参数retain_graph=True,重新进行backward,这个时候你的计算图就被保留了,不会报错。 但是这样会吃内存!,尤其是,你在大量迭代进行参数更新的时候,很快就会内存不足,所以这个参数在绝大部分情况下是不要去使用的。

  • 高阶导数create_graph

    ​ 更高层次的计算图会创建出来,允许计算高阶导数,如二阶导数、三阶导数等等。

x = torch.tensor(5.0,requires_grad=True)
y = torch.pow(x,3)
 
grad_x = torch.autograd.grad(y, x, create_graph=True)
print(grad_x) # dy/dx = 3 * x2,即75
 
grad_grad_x = torch.autograd.grad(grad_x[0],x)
print(grad_grad_x) # 二阶导数 d(2x)/dx = 30
'''
(tensor(75., grad_fn=<MulBackward0>),)
(tensor(30.),)
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5.4创建神经网络

5.4.1神经网络层

​ 下面表格中列出了比较重要的神经网络层组件。对应的在nn.functional模块中,提供这些层对应的函数实现。通常对于可训练参数的层使用module,而对于不需要训练参数的层如softmax这些,可以使用functional中的函数。

Layer对应的类功能说明
Linear(in_dim, out_dim, bias=True)提供了进行线性变换操作的功能
Dropout§Dropout层,有2D,3D的类型
Conv2d(in_c, out_c, filter_size, stride, padding)二维卷积层,类似的有Conv1d,Conv3d
ConvTranspose2d()
MaxPool2d(filter_size, stride, padding)二维最大池化层
MaxUnpool2d(filter, stride, padding)逆过程
AvgPool2d(filter_size, stride, padding)二维平均池化层
FractionalMaxPool2d分数最大池化
AdaptiveMaxPool2d([h,w])自适应最大池化
AdaptiveAvgPool2d([h,w])自自应平均池化
ZeroPad2d(padding_size)零填充边界
ConstantPad2d(padding_size,const)常量填充边界
ReplicationPad2d(ps)复制填充边界
BatchNorm1d()对2维或3维小批量数据进行标准化操作
RNN(in_dim, hidden_dim, num_layers, activation, dropout, bidi, bias)构建RNN层
RNNCell(in_dim, hidden_dim, bias, activation)RNN单元
LSTM(in_dim, hidden_dim, num_layers, activation, dropout, bidi, bias)构建LSTM层
LSTMCell(in_dim, hidden_dim, bias, activation)LSTM单元
GRU(in_dim, hidden_dim, num_layers, activation, dropout, bidi, bias)构建GRU层
GRUCell(in_dim, hidden_dim, bias, activation)GRU单元
5.4.2非线性激活层
激活层类名作用
ReLU(inplace=False)Relu激活层
SigmoidSigmoid激活层
TanhTanh激活层
SoftmaxSoftmax激活层
Softmax2d
LogSoftmaxLogSoftmax激活层
5.4.3容器类型
容器类型功能
Module神经网络模块的基类
Sequential序列模型,类似于keras,用于构建序列型神经网络
ModuleList用于存储层,不接受输入
Parameters(t)模块的属性,用于保存其训练参数
ParameterList参数列表
5.4.4其他层
容器类型功能
Embedding(vocab_size, feature_dim)词向量层
Embeddingbag
5.4.5模型的保存

​ 前面我们知道tensor可以通过save与load方法实现序列化与反序列化。由tensor组成的网络同样也可以方便的保存。不过通常没有必要完全保存网络模块对象,只需要保存各层的权重数据即可,这些数据保存在模块的state_dict字典中,因此只需要序列化这个词典。

# 模型的保存
torch.save(model.state_dict, 'path')
# 模型的加载
model.load_state_dict('path')
  • 1
  • 2
  • 3
  • 4
5.4.6实现LeNet神经网络

​ torch.nn.Module提供了神经网络的基类,当实现神经网络时需要继承自此模块,并在初始化函数中创建网络需要包含的层,并实现forward函数完成前向计算,网络的反向计算会由自动求导机制处理。

​ 下面的示例代码创建了LeNet的卷积神经网络。通常将需要训练的层写在init函数中,将参数不需要训练的层在forward方法里调用对应的函数来实现相应的层。

import torch.nn as nn
import torch.nn.functional as F

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

5.5损失函数与优化方法

5.5.1损失函数

torch.nn模块中提供了许多损失函数类,这里列出几种相对常见的。

类名功能
MSELoss均方差损失
CrossEntropyLoss交叉熵损失
NLLLoss负对数似然损失
PoissonNLLLoss带泊松分布的负对数似然损失
5.5.2优化方法

由torch.optim模块提供支持。

类名功能
SGD(params, lr=0.1, momentum=0, dampening=0, weight_decay=0, nesterov=False)随机梯度下降法
Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)Adam
RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)RMSprop
Adadelta(params, lr=1.0, rho=0.9, eps=1e-06, weight_decay=0)Adadelta
Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial_accumulator_value=0)Adagrad
lr_scheduler.ReduceLROnPlateau(optimizer, mode=’min’, factor=0.1, patience=10, verbose=False, threshold=0.0001, threshold_mode=’rel’, cooldown=0, min_lr=0, eps=1e-08)学习率的控制

在神经网络的性能调优中,常见的作法是对不对层的网络设置不同的学习率。

import torch.nn as nn
import torch.optim as optim

class model(nn.Module):
    def __init__(self):
        super(model,self).__init__()
        self.base = nn.Sequential()
        # code for base sub module
        self.classifier = nn.Sequential()
        # code for classifier sub module

optim.SGD([
            {'params': model.base.parameters()},
            {'params': model.classifier.parameters(), 'lr': 1e-3}
            ], lr=1e-2, momentum=0.9)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

5.6参数初始化

​ 良好的初始化可以让模型快速收敛,有时甚至可以决定模型是否能训练成功。Pytorch中的参数通常有默认的初始化策略,不需要我们自己指定,但框架仍然留有相应的接口供我们来调整初始化方法。

初始化方法说明
xavier_uniform_
xavier_normal_
kaiming_uniform_

5.7数据集与数据加载器

5.7.1 DataSet与DataLoader

torch.util.data模块提供了DataSet类用于描述一个数据集。定义自己的数据集需要继承自DataSet类,且实现__getitem__()与__len__()方法。__getitem__方法返回指定索引处的tensor与其对应的label。

为了支持数据的批量及随机化操作,可以使用data模块下的DataLoader类型来返回一个加载器:
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0)

5.7.2 torchvision简介

torchvision是配合pytorch的独立计算机视觉数据集的工具库,下面介绍其中常用的数据集类型。

torchvision.datasets.ImageFolder(dir, transform, label_map,loader)
提供了从一个目录初始化出来一个图片数据集的便捷方法。
要求目录下的图片分类存放,每一类的图片存储在以类名为目录名的目录下,方法会将每个类名映射到唯一的数字上,如果你对数字有要求,可以用label_map来定义目录名到数字的映射。

torchvision.datasets.DatasetFolder(dir,transform, label_map, loader, extensions)
提供了从一个目录初始化一般数据集的便捷方法。目录下的数据分类存放,每类数据存储在class_xxx命名的目录下。

此外torchvision.datasets下实现了常用的数据集,如CIFAR-10/100, ImageNet, COCO, MNIST, LSUN等。

除了数据集,torchvision的model模块提供了常见的模型实现,如Alex-Net, VGG,Inception, Resnet等。

5.7.3 torchvision提供的图像变换工具

torchvision的transforms模块提供了对PIL.Image对象和Tensor对象的常见操作。如果需要连续应用多个变换,可以使用Compose对象组装多个变换。

转换操作说明
ScalePIL图片进行缩放
CenterCropPIL图片从中心位置剪切
PadPIL图片填充
ToTensorPIL图片转换为Tensor且归一化到[0,1]
NormalizeTensor标准化
ToPILImage将Tensor转为PIL表示
import torchvision.tranforms as Trans
tranform = Trans.Compose([
                          T.Scale(28*28), 
                          T.ToTensor(), 
                          T.Normalize([0.5],[0.5])
                         ])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.8 训练过程可视化

5.8.1 使用Tensorboard

通过使用第三方库tensorboard_logger,将训练过程中的数据保存为日志,然后便可以通过Tensorboard来查看这些数据了。其功能支持相对有限,这里不做过多介绍。

5.8.2 使用visdom

visdom是facebook开源的一个可视工具,可以用来完成pytorch训练过程的可视化。
安装可以使用pip install visdom
启动类似于tb,在命令行上执行:python -m visdom.server
服务启动后可以使用浏览器打开http://127.0.0.1:8097/即可看到主面板。

5.9 GPU及并行支持

为了能在GPU上运行,Tensor与Module都需要转换到cuda模式下。

import torch
import torchvision

t = torch.Tensor(3,4)
print t.is_cuda #False
t = t.cuda(0)
print t.is_cuda #True

net = torchvision.model.AlexNet()
net.cuda(0)12345678910
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果有多块显卡,可以通过cuda(device_id)来将tensor分到不同的GPU上以达到负载的均衡。

另一种比较省事的做法是调用torch.set_default_tensor_type使程序默认使用某种cuda的tensor。或者使用torch.cuda.set_device(id)指定使用某个GPU。

5.10 示例:Pytorch实现CIFAR10与MNIST分类

关于cifar10与mnist数据集不再进行解释了。这里的Model类实现的二者的共同的任务,借鉴了keras的接口方式,Model类提供了train与evaluat方法,并没有实现序列模型的添加方法以及predict方法。此外设定损失函数与优化函数时,也只是简单的全部实例化出来,根据参数选择其中的一个,这里完全可以根据参数动态创建。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms

class Model:
    def __init__(self, net, cost, optimist):
        self.net = net
        self.cost = self.create_cost(cost)
        self.optimizer = self.create_optimizer(optimist)
        pass

    def create_cost(self, cost):
        support_cost = {
            'CROSS_ENTROPY': nn.CrossEntropyLoss(),
            'MSE': nn.MSELoss()
        }

        return support_cost[cost]

    def create_optimizer(self, optimist, **rests):
        support_optim = {
            'SGD': optim.SGD(self.net.parameters(), lr=0.1, **rests),
            'ADAM': optim.Adam(self.net.parameters(), lr=0.01, **rests),
            'RMSP':optim.RMSprop(self.net.parameters(), lr=0.001, **rests)
        }

        return support_optim[optimist]

    def train(self, train_loader, epoches=3):
        for epoch in range(epoches):
            running_loss = 0.0
            for i, data in enumerate(train_loader, 0):
                inputs, labels = data

                self.optimizer.zero_grad()

                # forward + backward + optimize
                outputs = self.net(inputs)
                loss = self.cost(outputs, labels)
                loss.backward()
                self.optimizer.step()

                running_loss += loss.item()
                if i % 100 == 0:
                    print('[epoch %d, %.2f%%] loss: %.3f' %
                          (epoch + 1, (i + 1)*1./len(train_loader), running_loss / 100))
                    running_loss = 0.0

        print('Finished Training')

    def evaluate(self, test_loader):
        print('Evaluating ...')
        correct = 0
        total = 0
        with torch.no_grad():  # no grad when test and predict
            for data in test_loader:
                images, labels = data

                outputs = self.net(images)
                predicted = torch.argmax(outputs, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        print('Accuracy of the network on the test images: %d %%' % (100 * correct / total))


classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')


def cifar_load_data():
    transform = transforms.Compose(
        [transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    trainset = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,shuffle=True, num_workers=2)

    testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                             shuffle=False, num_workers=2)

    return trainloader, testloader


class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

def mnist_load_data():
    transform = transforms.Compose(
        [transforms.ToTensor(),
         transforms.Normalize([0,], [1,])])

    trainset = torchvision.datasets.MNIST(root='./data', train=True,
                                            download=True, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=32,
                                              shuffle=True, num_workers=2)

    testset = torchvision.datasets.MNIST(root='./data', train=False,
                                           download=True, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=32,shuffle=True, num_workers=2)
    return trainloader, testloader


class MnistNet(torch.nn.Module):
    def __init__(self):
        super(MnistNet, self).__init__()
        self.fc1 = torch.nn.Linear(28*28, 512)
        self.fc2 = torch.nn.Linear(512, 512)
        self.fc3 = torch.nn.Linear(512, 10)

    def forward(self, x):
        x = x.view(-1, 28*28)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.softmax(self.fc3(x), dim=1)
        return x


if __name__ == '__main__':
    # train for mnist
    net = MnistNet()
    model = Model(net, 'CROSS_ENTROPY', 'RMSP')
    train_loader, test_loader = mnist_load_data()
    model.train(train_loader)
    model.evaluate(test_loader)

    # train for cifar
    net = LeNet()
    model = Model(net, 'CROSS_ENTROPY', 'RMSP')
    train_loader, test_loader = cifar_load_data()
    model.train(train_loader)
    model.evaluate(test_loader)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150

参考文章:

1.神经网络——最易懂最清晰的一篇文章_illikang的博客-CSDN博客_神经网络

2.神经网络权重更新的原理和方法_liumingchun13的博客-CSDN博客_神经网络权值更新

3.深度学习笔记——常用的激活(激励)函数 - 战争热诚 - 博客园 (cnblogs.com)

4.深度学习_损失函数(MSE、MAE、SmoothL1_loss…)_Xiaobai_rabbit0的博客-CSDN博客_mse损失函数

5.损失函数|交叉熵损失函数 - 知乎 (zhihu.com)

6.神经网络Loss损失函数总结_willduan的博客-CSDN博客_神经网络loss

7.【深度学习】常用优化器总结 - Flix - 博客园 (cnblogs.com)

8.神经网络框架-Pytorch使用介绍_zzulp的专栏-CSDN博客_pytorch使用

9.pytorch自动求导Autograd系列教程(一)_MIss-Y的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/352119
推荐阅读
相关标签
  

闽ICP备14008679号