Datawhale干货
Author:土豆@知乎,From:极市平台原文丨https://zhuanlan.zhihu.com/p/158739701
前言
接触深度学习也有一两年了,一直没有将一些实战经验整理一下形成文字。本文打算用来记录一些在深度学习实践中的调试过程,记录一些经验之谈。因为目前深度学习业界的理论基础尚且薄弱,很多工程实践中的问题没法用理论解释得很好,这里的只是实践中的一些经验之谈,以供参考以及排错。本文将持续更新。需要强调的是,本文的很多单纯只是经验,在尽可能列出参考文献的同时却并无严格理论验证,希望大家见谅。欢迎大家集思广益,共同维护这个经验集,为整个社区贡献微弱力量。1、在分类问题中,损失函数及其快速得下降为0.0000
在分类问题中,我们一般采用的是交叉熵[1]作为损失函数,如式(1.1)所示其中和是预测结果,以概率分布的形式表达,如等,一般是通过softmax层实现,和是样本真实标签,在单分类问题中,采用的是独热编码[2],只有一个分量是为1的,如。(公式第二行是向量化表达)我们发现,交叉熵损失的下确界是0,但是永远都不可能达到0,因为要达到0,那么所有的预测向量分布就必须完全和真实标签一致,退化为独热编码。但是实际上在神经网络中,经过了softmax层之后,是不可能使得除了目标分量的其他所有分量为0的(这个这里只是抛出了结论,讨论需要比较长的篇幅。),因此永远不可能达到0的,正是因为如此,交叉熵损失可以一直优化,这也是其比MSE损失优的一个点之一。既然注意到了不可能为0,我们就可以分析,这肯定是自己程序问题,我们将经过softmax之前的logit打印出,如:发现了没有,这些值都很大,而softmax函数为:我们会发现,过大或者过小的指数项,比如1023,会涉及到计算,这个数值在TensorFlow或者大部分框架中是溢出的,显示为inf,因此就会把该分量拉成1,而其他变成了0。这种操作是会导致严重的过拟合的。因此,一般来说,logit值不能太大,否则将会出现数值计算问题。那么如何解决? 出现这种问题的情况很多时候是因为参数初始化导致的数值计算问题,比如都采用了方差过小的高斯分布进行初始化,那么就会把网络的输出的范围拉的特别大,导致以上的问题。因此在参数初始化中,确保每一层的初始化都是在一定范围内的,可以考虑采用Xavier初始化,Kaiming初始化等。(这个初始化的影响我们将会以后讨论,这是一个新的话题。)2、在正则化的过程中对神经网络的偏置也进行了正则
一般来说,我们常用的是二范数正则,也即是岭回归,如式子(2.1)一般来说,我们只会对神经网络的权值进行正则操作,使得权值具有一定的稀疏性[21]或者控制其尺寸,使得其不至于幅度太大[3],减少模型的容量以减少过拟合的风险。同时,我们注意到神经网络中每一层的权值的作用是调节每一层超平面的方向(因为就是其法向量),因此只要比例一致,不会影响超平面的形状的。但是,我们必须注意到,每一层中的偏置是调节每一层超平面的平移长度的,如果你对偏置进行了正则,那么我们的可能就会变得很小,或者很稀疏,这样就导致你的每一层的超平面只能局限于很小的一个范围内,使得模型的容量大大减少,一般会导致欠拟合[7]的现象。因此,一般我们不会对偏置进行正则的,注意了。3、学习率太大导致不收敛
不收敛是个范围很大的问题,有很多可能性,其中有一种是和网络结构无关的原因,就是学习率设置的太大了,如下图所示,太大的学习率将会导致严重的抖动,使得无法收敛,甚至在某些情况下可能使得损失变得越来越大直到无穷。这个时候请调整你的学习率,尝试是否可以收敛。当然,这里的“太大”目前没有理论可以衡量,不过我喜欢从的Adam优化器[4]开始进行尝试优化。下图展示了过大过小的学习率对模型性能的影响曲线图:
4、别在softmax层前面的输入施加了激活函数
softmax函数如式(4.1)所示:假设我们的网络提取出来的最后的特征向量是,如果我们最后的分类的类别有类,那么我们会用一个全连接层将其映射到对应维度的空间里面,如式(4.2)。那么,这个全连接层虽然说可以看成是分类器,但是我们最好把它看成是上一层的“近线性可分特征”的一个维度转换(有点绕,意思是我们这里只是一个维度的转换,而不涉及到kernel),不管怎么说,这个时候,我们的输出是不能有激活函数的,如下式是不可以的:为激活函数这时候的输出,具有和分类类别相同的维度,在很多框架中被称之为logits值,这个值一般是在实数范围内的,一般不会太大,参考笔记第一点的情况。5、检查原数据输入的值范围
原始数据输入可能千奇百怪,每个特征维的值范围可能有着数量级上的差别,这个时候如果我们不对数据进行预处理,将会大大增大设计网络的负担。一般来说我们希望输入的数据是中心对齐的,也即是0均值的[5],可以加速网络收敛的速度。同时,我们希望不同维度上的数值范围是一致的,可以采用一些归一化[6]的手段进行处理(这个时候假设每个维度重要性是一样的,比如我们图片的三个通道等)。6、别忘了对你的训练数据进行打乱
经常,你的训练过程非常完美,能够很好地拟合训练数据,但是在测试过程中确实一塌糊涂,是的,你的模型这个时候过拟合[7]了。这个时候你会检查模型的有效性,不过在进行这一步之前,不妨先检查下你的数据加载器(Data Loader)是否是正常设计的。一般来说,我们的训练数据在训练过程中,每一个epoch[8]中,都是需要进行打乱(shuffle)的,很多框架的数据加载器参数列表中都会有这项选项,比如pytorch的DataLoader类[9]。为什么需要打乱呢?那是因为如果不打乱我们的训练数据,我们的模型就有可能学习到训练数据的个体与个体之间特定的排列顺序,而这种排列顺序,在很多情况下是无用的,会导致过拟合的糟糕现象。因此,我们在训练过程中,在每一个epoch训练中都对训练集进行打乱,以确保模型不能“记忆”样本之间的特定排序。这其实也是正则的一种手段。在训练中,大概如:7、一个batch中,label不要全部相同
这个情况有点类似与笔记的第六点,我们需要尽量给训练过程中人为引入不确定性,这是很多正则手段,包括dropout,stochastic depth等的思路,这样能够有效地减少过拟合的风险。因此,一个batch中,尽量确保你的样本是来自于各个类的(针对分类问题而言),这样你的模型会减少执着与某个类别的概率,减少过拟合风险,同时也会加快收敛速度。8、少用vanilla SGD优化器
在高维度情况下的优化,其优化平面会出现很多鞍点(既是梯度为0,但却不是极点),通常,鞍点会比局部极值更容易出现(直观感受就是,因为高维度情况下,一个点周围有很多维度,如果是极值点,那么就需要其他所有维度都是朝向同一个方向“弯曲”的,而这个要比鞍点的各个方向“弯曲”的情况可能要小),因此这个时候我们更担心陷于鞍点,而不是局部极小值点(当然局部极小值点也是一个大麻烦,不过鞍点更麻烦)。如果采用普通的SGD优化器,那么就会陷于任何一个梯度为0的点,也就是说,极有可能会陷于鞍点。如果要使用SGD方法,建议使用带有momentum的SGD方法,可以有效避免陷入鞍点的风险。9、检查各层梯度,对梯度爆炸进行截断
有些时候,你会发现在训练过程中,你的损失突然变得特别大,或者特别小,这个时候不妨检查下每一层的梯度(用tensorboard的distribution可以很好地检查),很可能是发生了梯度爆炸(gradient explosion)的情况,特别是在存在LSTM等时序的网络中,很容易出现这种情况。因此,这个时候我们会用梯度截断进行处理,操作很简单粗暴,就是设置一个阈值,把超过这个阈值的梯度全部拉到这个阈值,如下图所示:10、检查你的样本label
有些时候,你的训练过程可以很好地收敛,当使用MSE损失[12]的时候甚至可能达到0.0000的情况。但是,当你把模型拿到测试集中评估的时候,却发现性能极差,仿佛没有训练一样。这是过拟合吗?显然是的,但是这可能并不是你的模型的问题,请检查你的数据加载中训练集的样本标签是否正确对应。这个问题很白痴,但是却真的很容易在数据加载过程中因为种种原因把label信息和对应样本给混掉。根据文献[13]中的实验,用MSE损失的情况下,就算是你的label完全随机的,和样本一点关系都没有,也可以通过基于SGD的优化算法达到0.0000损失的。因此,请务必确保你的样本label是正确的。11、分类问题中的分类置信度问题
在分类问题中我们一般都是采用的是交叉熵损失,如式子(1.1)所示,在一些实验中,如果我们绘制出训练损失和分类准确度的曲线图,我们可能会有下图这种情况[14]:这是为什么呢?为什么分类准确度不会顺着分类损失的增大而减少呢?
这个涉及到了分类过程中对某个类的“置信程度”的多少,比如:模型是对第一类相当确信的,但是在第二种情况:这对第一类的置信程度就很低了,虽然按照贝叶斯决策,还是会选择第一类作为决策结果。因此这就是导致以上现象的原因,在那个拐点后面,这个模型对于分类的置信程度其实已经变得很差了,虽然对于准确度而言,其还能分类正确。 但是这其实正是过拟合的一种表现,模型已经对自己的分类结果不确信了。12、少在太小的批次中使用BatchNorm层
Batch Normalization[15],中文译作批规范化,在深度学习中是一种加快收敛速度,提高性能的一个利器,其本质和我们对输入的原数据进行0均值单位方差规范化差不多,是以batch为单位,对中间层的输出进行规范化,可以缓和内部协方差偏移(Internal Covariate Shift)的现象。其基本公式很简单,如下:不过这里并不打算对BN进行详细讲解,只是想告诉大家,因为BN操作在训练过程中是对每个batch进行处理的,从每个batch中求得均值和方差才能进行操作。如果你的batch特别小(比如是受限于硬件条件或者网络要求小batch),那么BN层的batch均值和方差可能就会不能很好符合整个训练集的统计特征,导致差的性能。实际上,实验[16]说明了这个关系,当batch小于16时,性能大幅度下降。13、数值计算问题,出现Nan
Nan(Not An Number)是一个在数值计算中容易出现的问题,在深度学习中因为涉及到很多损失函数,有些损失函数的定义域并不是整个实数,比如常用的对数,因此一不小心就会出现Nan。在深度学习中,如果某一层出现了Nan,那么是具有传递性的,后面的层也会出现Nan,因此可以通过二分法对此进行排错。一般来说,在深度学习中出现Nan是由于除0异常或者是因为损失函数中的(比如交叉熵,KL散度)对数操作中,输入小于或者等于0了,一般等于0的情况比较多,因此通常会:这里的是个很小的值,一般取即可,可以防止因为对数操作中输入0导致的Nan异常。需要注意的是,有些时候因为参数初始化或者学习率太大也会导致数值计算溢出,这也是会出现Nan的,一般这样会出现在较前面的层里面。14、BN层放置的位置问题
BN层有两种常见的放置位置,如下图所示:第一个是放在激活函数之前:: 在新的文献[28]中,作者尝试解释了以下BN用法的原因,有兴趣的读者可以移步去细读下。
传统用法:graph LR weights –> BatchNormBatchNorm –> ReLU[28]的作者提出的用法:graph LR ReLU –> BatchNorm+dropout BatchNorm+dropout –> weights