与神经网络学习相关的技巧

目录

SGD

Momentum

AdaGrad

Adam

4种更新参数方法的总结

权重的初始值

ReLU的权重初始值

Batch Normalization

正则化

过拟合

权值衰减

Dropout

超参数


神经网络的学习是找到使损失函数值尽可能小时,对应的权重参数。在之前的专题中,我们介绍了梯度法来更新参数,以逐渐靠近最优参数,这个过程就是随机梯度下降法(SGD),其核心思想是参数的更新方向是沿着变化最大的方向进行,而我们都知道梯度(即斜率)就是指向变化最大的方向。因此基于SGD,权重参数能以最快的速度找到可能的最优值。

假设一位盲人要从山顶走到山谷,他应该如何走才能迅速走到山底呢?

借助梯度法思想,一种方法是,该盲人可以借助脚去感知地面的坡度,坡度越倾斜的地方,就是他应该走的地方,这样才可能以最快的速度到达山底。

SGD

 SGD数学表达式如下:

                                               W\leftarrow W-\eta\frac{\partial L}{\partial W}                            (1)

\frac{\partial L}{\partial W}是损失函数关于权重参数的梯度,\eta是学习率(比如取0.01,0.001),该表达式直观地表示了权重参数沿着梯度的方向进行更新,而更新的强度由学习率决定,学习率越大,更新的就越快,学习率越小,更新的就越慢。可见学习率的取值也是我们重点研究对象。

SGD比价简单,我们可以用Python实现,这里把它实现为一个类。

class SGD:
    def __init__(self,lr=0.01):   
        self.lr=lr                #实例变量,学习率lr
    def update(self,params,grads):
        for key in params.keys():
            params[key]-=self.lr*grads[key]   #表达式实现,参数W,b保存在字典params中,grads为误 
                                              #差反向传播法求梯度的函数,可在前面专题中查找
  

虽然SGD比较直观且容易实现,但是它比较低效,因为梯度的方向并没有指向最小值的方向,它可能指向局部最小值,或局部极小值,或全局极小值。因此,我们更新的效率可能会很低,得到的结果并不是最优值。图1是SGD寻找最小值的更新路径。可见,SGD呈“”字形移动更新,这是一个相当低效的路径。我们非常希望更新路径按起始位置到最小值之间的直线进行更新,最大程度降低更新路径少走“弯路”。基于SGD的缺点,我们来介绍其他的一些梯度更新的方法。

图1  SGD方法更新路径

Momentum

“momentum”这个单词的意思是动量,它是一个物理名词。简单来讲,它的意思就是如果物体在某一方向上受力,那么就会产生加速度,使得物体在该方向上的速度增加。从而在整体上使得物体能尽早到达目标位置,换句话说,就是可以让我们少走弯路,高效而快速找到最小值。采用momentum来权重参数的更新的表达式如下:

                                       v\leftarrow \alpha v-\eta\frac{\partial L}{\partial W}                  (2)

                                      W\leftarrow W+v                        (3)

           该表达式多了两个参数,\alphav ,  \alpha是一个常量,我们可以理解为\alpha是物理上的摩擦或空气阻力,可以取(0~1)之间的任何数值。v是一个变量,可以理解为物理上的速度,可见公式(2)和物理上的速度表达式可以相互对应,它表示物体在梯度方向上受力,在这个力的作用下,物体的速度增加。再次强调公式中的变量一般为包含多个相同类型参数的矩阵。我们以图1为例,最小值的位置在起始位置的右侧,我们希望更新位置朝x轴移动的速度大于朝y轴移动的速度,实际上,物体一直都受到朝x轴正方向的力,显然,在这个力的作用下,我们应该加速物体在x轴方向的速度。而物体在y轴方向上的受力虽然很大,但是因为交互地受到正方向和反方向的力,所以它们会相互抵消,所以y轴方向上速度不稳定。因此,和SGD相比,momentum可以更快地朝 x轴方向靠近,减弱“之”字形的变动程度。图2为momentum方法的更新路径。

图2 momentum 方法更新路径

momentum方法用Python实现的代码如下:

# coding: utf-8
import numpy as np

class Momentum:

    """Momentum SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

 代码中,实例变量v会保存物体的速度。初始化时,v不保存任何值,第一次调用update()时,v会以字典变量的形式保存与参数结构相同的数据。

AdaGrad

在前面,我们提及过学习率的值对参数寻优相当重要,学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。有关学习率的典型方法中,有一种被称为学习率衰减法,即随着学习的进行,学习率逐渐减小,一开始多学,然后逐渐“少”学。基于这种方法,诞生了AdaGrad方法,由这个名字可知,它是adaptive和gradient的缩写,意思是“适当的、梯度”。其核心思想是为参数的每个元素适当地调整学习率,AdaGrad的表达式如下:

                                                              h\leftarrow h+\frac{\partial L}{\partial W}\bigodot \frac{\partial L}{\partial W}                                          (4)

                                                             W\leftarrow W-\eta \frac{1 }{\sqrt[]{h}} \frac{\partial L}{\partial W}                                              (5)

变量h保存了以前所有梯度值的平方和,\bigodot表示矩阵元素的乘法。可知,在更新参数时,通过乘以\frac{1 }{\sqrt[]{h}}就可以调整学习的尺度,即参数的元素中变动大(更新幅度大)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。基于这样的思想,我们再来分析图1,物体在y轴方向上的梯度较大(变化大),所以AdaGrad方法会逐渐减少在y轴方向上的更新步伐,以减弱"之“字形,而在x轴方向上的更新的梯度本身就小,所以AdaGrad方法会加快在x轴方向上的更新,就像一条直线一样,直逼最小值。图3是AdaGrad方法的更新路径。

图3 AdaGrad方法更新路径

由图可见,由于y轴方向上的梯度较大,因此刚开始变动较大,但后面会根据这个较大的变动按比例进行调整,减少更新的步伐,因此在y轴方向上的更新程度被减弱,“之”字形的变动程度被衰减,在x轴方向上的更新基本上在平稳进行。AdaGrad方法的Python实现的代码如下:

# coding: utf-8
import numpy as np

class AdaGrad:

    """AdaGrad"""

    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

关于代码有一个地方需要强调,最后一行代码加上了微小值1e-7。这是为了防止当self.h[key]中有0时,将0作为除数的情况,在很多深度学习的框架中,都有这个参数值。

Adam

Adam方法就是将momentum方法和AdaGrad方法融合在一起,即它考虑了两种情况:物体在受力方向上的速度会增加;梯度变动大的参数的学习率将被逐渐减小。有关Adam方法的数学原理请参见原作者的论文(http://arxiv.org/abs/1412.6980v8)图4为Adam方法的更新路径。

图4 Adam方法的更新路径

从图4可看出,Adam和momentum有类似的更新路径,但Adam的移动的波动程度有所减轻,这得益于学习的更新程度被适当的调整了。Adam方法的Python实现如下:

# coding: utf-8
import numpy as np

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

4种更新参数方法的总结

到目前为止,我们介绍了4种更新权重的方法,方法的不同,使得参数更新的路径也不同。实践证明,没有哪一种方法是绝对最佳的,各自都有其适用场合,都有各自擅长解决的问题和不擅长解决的问题。不过,一般而言,与SGD相比,其他3种方法可以学习得更快,有时最终的识别精度也更高。

权重的初始值

在本专题前面介绍的几种权重参数的寻优方法都是以学习率\eta为切入点。下面我们以权重参数的初始值为切入点,试图改善神经网络的学习性能。

神经网络的学习,实质就是通过以损失函数为目标,修改权重参数为中间过程的一个学习任务。而权重参数的初始值一般通过正态或高斯分布进行随机初始化,我们有理由相信权重的初始值对神经网络的学习至关重要,它关乎神经网络的学习是否成功。直觉告诉我们,初始值设置的恰当可以减少神经网络的学习时间,快速找到损失函数的最小值。

权重初始值或权重中间值到底应该设置成什么样的值、或者权重值应该具有什么样的特点才有助于神经网络的学习呢?直观地,我们可以简单地分析一下:

权重的大小应该分布的更有广度一些,杜绝某些特征的权重系数过大(权重过大,说明其对应的特征的重要性很大,可能导致模型过拟合等问题),而权重过小比如为0,则可能就丢弃了某些特征的重要性。因此,不管怎么样,我们希望权重的分布广度尽量相同,或者说,至少在权重初始值时,应该做到这一点。回想一下,机器学习中提及的一些数据预处理方法的意义,比如数据的(0~1)缩放,即把数量级分布大的数据集缩放到大小范围在0~1之间。这么做的意义是什么呢?不难理解,这样做的目的是减少数据大小分布过大而带来的影响极限化,比如有的特征项的数据是1000,有的特征项的数据是1,显然就会显性地得出这些特征项的重要性的差异。而通过(0~1)缩放后,就会降低或平衡这些特征项的重要性,就好比不带有感情色彩去对待某个人一样,尽量做到公平公正。

我们以神经网络为例来做一个简单的试验,通过对权重参数的随机初始化(服从高斯分布),并分别对初始化后的权重乘以1和0.01(及标准差分别为1和0.01的高斯随机分布),然后在这两种情况下,观察权重初始值对隐藏层的激活函数的输出(简称激活值)的分布的影响。输入数据为随机产生的1000个数据,特征有100个;隐藏层设为5层;各隐藏层的神经元100个,激活函数使用sigmoid函数。我们用直方图绘制各层激活值的数据分布。标准差为1的高斯分布随机初始化作为权重初始值的情况下,Python代码如下:

# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
   
input_data = np.random.randn(1000, 100)  # 1000个数据
node_num = 100  # 各隐藏层的节点(神经元)数
hidden_layer_size = 5  # 隐藏层有5层
activations = {}  # 激活值的结果保存在这里
x = input_data
for i in range(hidden_layer_size):
    if i != 0:
        x = activations[i-1]

    # 改变初始值进行实验!
    w = np.random.randn(node_num, node_num) * 1 #标准差为1的高斯随机分布作为权重初始值
    a = np.dot(x, w)
    z = sigmoid(a)
    activations[i] = z
# 绘制直方图
for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + "-layer")
    if i != 0: plt.yticks([], [])
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

得到的直方图如图5所示:

图5  标准差为1的高斯分布随机初始化作为权重初始值时的各层激活函数值的分布

标准差为0.01的高斯分布随机初始化作为权重初始值的情况下,Python代码和上面的一样,只需把设定权重初始值的地方换成下面的代码即可。

#w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01

得到的各层激活值的分布如图6所示:

图6 标准差为0.01的高斯分布随机初始化作为权重初始值时的各层激活函数值的分布

简单来说,标准差从1修改为0.01,就好比数据进行0~1缩放,主要是为了使各层的激活值分布更有广度,然而从图6来看,效果似乎比我们期望的要差些。

我们先分析图5,从图5可知,各层的激活值呈现偏向0和1的分布。这里我们使用激活函数是sigmoid函数,在前面的专题中,我们讲过sigmoid函数的的反向传播,如图7所示:

图 7 sigmoid函数的反向传播

可知,sigmoid函数的反向传播只需根据正向传播的输出就能计算出来。由于图5展示了sigmoid函数正向传播的输出y偏向0和1,所以其反向传播接近0,即梯度的值不断变小,最后消失。这就是我们常说的梯度消失。这样会对神经网络的学习带来严重的后果。

从图6可知,使用标准差为0.01的高斯随机分布后,各层的激活值的分布主要集中在0.5附近。这样的结果比前一种好些,不会发生梯度消失的问题。但是,仔细现象,激活值集中在0.5附近也是有问题的。激活值的分布一旦有所偏向,说明神经元的表现力就不够好。打个比方,如果100个神经元的输出值几乎相同,那么不就可以由1个神经元来表达基本相同的事情了。因此激活值在分布上有所偏向会出现“表现力受限”的问题。导致学习可能无法进行或做无用功。

综上,我们希望激活值的分布更有广度,而上面介绍的标准差为1或0.01的高斯随机初始化权重参数的方法都存在激活值偏向的问题。现在我们使用另外一种权重初始化的方法(称为Xavier初始值):如果前一隐藏层的神经元(节点)数量为n,则初始值使用标准差为\frac{ 1}{\sqrt{n} }的分布。其实理解起来也不难,也就是说前一层的神经元越多,则目标神经元的初始化的权重尺度就越小(相当于标准差越小的高斯分布)。Python实现,只需将设定权重初始值的地方换成如下内容即可:

node_num=100 #前一层的神经元数量
w = np.random.randn(node_num, node_num)/np.sqrt(node_num)

这种权重初始化得到的激活值的分布如图8所示:

图8 Xavier初始值作为权重初始值时的各层激活函数值的分布

从图8得到的结果来看,越是后面的层,图像就变得越是没有规则(越歪斜),但是呈现了比之前更有广度的分布。由于各层间传递的数据有适当的广度,所以sigmoid函数的表现力不受限制,有望进行高效的学习。

ReLU的权重初始值

之前介绍的初始值方法都是针对以sigmoid函数作为激活函数而实现的,由于激活函数的不同,我们对权重的初始化方法可能就不同。如果激活函数是ReLU函数,那么就有专门的权重初始化方法。这里我们介绍一种“He初始值”:当前一层的节点数为n时,He初始值使用标准差为\sqrt{\frac{2 }{n}}的高斯分布。其实理解起来也不难,因为ReLU的负值区域的值为0,为了使它更有广度,所以需要2倍的系数。有关He初始值的Python实现,只需在将激活函数sigmoid的定义修改为ReLU函数的定义、权重初始值修改为He初始值即可,代码如下:

def ReLU(x):
    return np.maximum(0, x)    #ReLU激活函数

w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)  #He初始值

读者可亲自尝试观察激活函数为ReLU函数或sigmoid函数甚至tanh函数时,不同权重初始值(上面介绍的标准差0.01初始化、Xavier初始值、He初始值)的激活值的分布情况,并总结各种初始值方法应用于不同激活函数时的结果。相关的代码都已经给出了,希望读者自己去实践操作吧!

温馨提示,tanh函数的Python实现如下:

def tanh(x):
    return np.tanh(x)

Batch Normalization

前面我们将了通过设定合适的权重参数初始值,则各层的激活值分布会有适当的广度。现在,我们引入一种新的方法----Batch Normalization,为了让各层拥有适当的广度,"强制性"地调整激活值的分布。Batch Normalization的优点主要有以下三点:

  • 可以增大学习率,进行快速学习
  • 不会过度依赖初始值
  • 抑制过拟合

Batch Normalization方法的思路是调整各层的激活值分布使其拥有适当的广度。为达到这样的效果,我们需要在网络插入对数据进行正则化的层,这里称为Batch Norm层。从名字就不难理解,Batch即以mini-batch为单位,Norm即正则化。具体来说,数据分布的均值为0,方差为1的正则化,数学表达式如公式(6)所示:

                                           \mu _{B}\leftarrow \frac{1 }{m}\sum_{i=1}^{m}x_{i}

                                          \sigma {_{B}}^{2}\leftarrow \frac{1 }{m}\sum_{i=1}^{m}(x_{i}-\mu _{B})^{2}                              (6)

                                           \breve{x}_{i}\leftarrow \frac{x_{i}-\mu _{B}}{\sqrt{\sigma {_{B}}^{2}+\epsilon }}             

                                            y_{i}\leftarrow \gamma \breve{x}_{i}+\beta                      

mini-batch的m个输入数据的集合B=\left \{ x_{1},x_{2},x_{3}....x_{m} \right \}求平均值\mu _{B}和方差\sigma {_{B}}^{2}。然后进行均值为0,方差为1的正则化,将输入数据x变为\breve{x}_{i}\epsilon是一个微小值,以防止出现分母为0的情况出现。最后,对正则化后的数据进行缩放\gamma(初始值为1)和平移\beta(初始值为0)。\gamma\beta通过学习进行调整。一般地,我们可以把Batch Norm层放在激活函数的前面或后面,减小数据分布的偏向。流程如图9所示。

图9  Batch Normalization 正则化的神经网络

 

正则化

有过机器学习背景的人,对“正则化”应该不陌生,“正则化”一般与模型过拟合/欠拟合/泛化能力等术语一起出现。这里我们主要讨论模型过拟合时,如何通过“正则化”来提高模型的泛化性能,使得模型可以对没有包含在训练数据里的观测数据也能进行高精度识别。

过拟合

简单讲,就是模型对训练数据的识别能力强,而对测试数据的识别能力弱,产生该现象的原因主要有两个:

  • 模型过于复杂,参数太多、表现力强
  • 训练数据样本量少

权值衰减

权值衰减是一种常用来抑制过拟合的方法,正如其名,模型在学习过程中,对大的权重进行惩罚(衰减),来抑制过拟合。

我们知道,神经网络的学习目的是减少损失函数的值。如果为损失函数加上权重W的平方范数(L2范数),就可以抑制权重变大。即L2范数的权值衰减为\frac{1}{2}\lambda W^{2},我们将这个值加到损失函数上。\lambda是控制正则化强度的超参数,\lambda越大,则对权重施加的惩罚就越严重。\frac{1}{2}用于\frac{1}{2}\lambda W^{2}进行反向传播求导后变成\lambda W设计的。因此,在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数\lambda W

L2范数相当于各个元素的平方和。比如W=\left ( w_{1},w_{2},w_{3}.....w_{n}}\right ),则L2范数可以用\sqrt{w_{1}^{2}+w_{2}^{2}+.....w_{n}^{2}}}}}}}计算。此外,还有L1范数(相当于各个元素的绝对值之和)、L∞范数(相当于各个元素的绝对值中最大的那一个)。这些范数各有各的特点。

关于L2范数的Python实现,这里就不给出了,读者只要明白了L2范数的数学表达式,就可以对前面专题介绍的多层神经网络的实现部分进行修改。

Dropout

L2范数抑制过拟合的方法虽然简单,但是难以应付复杂的模型,基于此,我们使用Dropout方法。其原理如图10所示。

图10 Dropout随机删除神经元示意图

Dropout是一种在学习过程中随机删除神经元的方法,被删除的神经元不参与信号的传递。训练时,每传递一次数据,就会随机选择要删除的神经元。测试时,会传递所有所有神经元信号,但是各个神经元的输出要乘上训练时的删除比例后再输出。Dropout的Python简单实现如下:

import numpy as np
class Dropout:
    def __init__(self,dropout_ratio=0.5):
        self.dropout_ratio=dropout_ratio
        self.mask=None
    def forward(self,x,train_flag=True):
        if train_flag:
            self.mask=np.random.rand(*x.shape)>self.dropout_ratio
            return x*self.mask
        else:
            return x*(1.0-self.dropout_ratio
    def backward(self,dout):
            return dout*self.mask

每次正向传播时,self.mask以false形式保存要删除的神经元。self.mask会随机生成和x形状相同的数组,并将比dropout_ratio大的元素设为true。正向传播时传递了信号的神经元,反向传播时按原样传递信号(forward和backward函数共用self.mask值);否则反向传播时信号将停在那里。

超参数

超参数一般是指神经元数量、batch大小、学习率、权值衰减等。实践证明,超参数的设置对模型的性能影响很大。一般地,超参数的设置需要我们反复地去调整或试错,因此需要有专门的数据集来完成这一任务,这里我们引入验证数据来完成超参数的调整。

一般地,训练数据完成参数(权重和偏置) 的学习,验证数据完成超参数的调整,测试数据完成模型泛化性能的评估。

显然,进行超参数调整前,我们需要对原始数据集进行分割(训练数据,验证数据,测试数据),关于数据分割的方法也是有技巧的,这里就不多说了。其实这里讲的超参数调整方法和机器学习中我们常常听说的格子搜素、交叉验证等超参数调整方法类似。因此,我们简单归纳一下超参数的调整步骤:

  • 设定超参数的范围(以10的阶乘为尺度)
  • 从设定的超参数范围中随机采样
  • 使用采样到的超参数的值进行学习
  • 通过验证数据评估识别精度
  • 根据识别精度的结果,缩小超参数范围,再进行学习

这里,我们以学习率和权值衰减强度的系数为例,如何用Python实现学习率为(10^{-6}~10^{-2})和权值衰减系数(10^{-8}~10^{-4})范围的随机选择。

import numpy as np
weight_decay=10**np.random.uniform(-8,-4)
lr=10**np.random.uniform(-6,-2)

读者可使用采样到的超参数进行学习,然后通过验证数据评估识别精度,然后再进一步缩小超参数范围,反复循环,寻找一个最优的超参数的值。

本专题的知识就讲到这里了,回顾一下,我们主要讲了参数的更新方法,权重初始值的赋值方法,抑制过拟合的正则化方法,超参数的搜索方法等。

欢迎关注微信公众号“Python生态智联”,学知识,享生活!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值