admin管理员组

文章数量:1579086

目录

1. 参数的更新

SGD

Momentum

AdaGrad

Adam

总结

2. 权重的初始值

可以将权重初始值设为0吗

隐藏层的激活值的分布

ReLU的权重初始值

总结

3. Batch Normalization

4. 正则化

过拟合

权值衰减

Dropout

集成学习

5. 超参数的验证

验证数据

超参数的最优化


本章主题涉及寻找最优权重参数的最优化方法、权重参数的初始值、超参数的设定方法等

1. 参数的更新

  • 神经网络的学习的目的就是找到使损失函数的值尽可能小的参数
  • 这是寻找最优参数的问题,这个过程叫做最优化(optimization)

SGD

  • 前几章我们采用的方法为随机梯度下降法SGD, stochastic gradient descent)

  • 公式:

    • W为需要更新的权重参数,为损失函数关于W的梯度
  • 实现为一个Python类

    class SGD:
    		def __init__(self, lr=0.01):
    				self.lr = lr
    
    		def update(self, params, grads):
    				for key in params.key():
    						params[key] -= self.lr * grads[key]
    
  • 缺点:解决某些问题时可能没有效率

    • 如果函数的形状非均向(anisotropic),比如延伸状,搜索路径会非常低效

Momentum

  • Momentum是“动量”的意思,和物理有关

  • 公式:

    • (1)
    • (2)
    •  表示学习率, 对应物理上的速度
    • (1)表示了物体在梯度方向上受力,速度增加,其中 表示物体不受力时,物体逐渐减速,对应物理上的摩擦或空气阻力
  • 实现

    import numpy as np
    
    class Momentum:
        def __init__(self, lr=0.01, momentum=0.9):
            self.lr = lr
            self.momentum = momentum
            self.v = None
    
        def update(self, params, grads):
            # v保存物体的速度,初始化时,什么都不保存,但第一次调用update()时
            # v会以字典型变量的形式保存与参数结构相同的数据
            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]
    

AdaGrad

  • 神经网络的学习中,学习率的值很重要:过小,会导致学习花费过多时间;过大,会导致学习发散而不能正确进行
  • 学习率衰减(learning rate decay):随着学习的进行,使学习率逐渐减小。一开始多学,然后逐渐少学。
  • AdaGrad:为参数的每个元素适当地调整学习率,与此同时进行学习
  • AdaGrad会记录过去所有梯度的平方和,因此随着学习越深入,更新的幅度就越小
class 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)

Adam

  • Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐,Adam方法融合了这两种思想
class Adam:

    """Adam (<http://arxiv/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种方法各有特点,都有擅长解决和不擅长解决的问题,很多研究中至今仍在使用SGD

2. 权重的初始值

设定什么样的权重初始值,经常关系到神经网络的学习能否成功

可以将权重初始值设为0吗

  • 权值衰减(weight decay):是一种以减少权重参数的值为目的的进行学习的方法。它可以抑制过拟合、提高泛化能力
  • 如果想减小权重的值,一开始就将初始值设为较小的值才是正途
    • 前面我们使用了0.01*np.random.randn(10, 100) 这样由高斯分布生成的值乘以0,01后得到的值
  • 为什么不能将权重初始值设为0,或者严格地说,为什么不能将权重初始值设处一样的值?
    • 因为在误差反向传播法中,所有的权重都会进行相同的更新
    • 权重被更新为相同的值,并拥有了对策(重复)的值
    • 这使得神经网络拥有许多不同权重的意义丧失了
    • 因此为了瓦解权重的对称结构,必须随机生成初始值

隐藏层的激活值的分布

假设神经网络有5层,每层有100个神经元,然后用高斯分布随机生成1000个数据作为输入数据,激活函数使用sigmoid函数,node_num为神经元的个数

  • 使用标准差为1的高斯分布

    • W = np.random.randn(node_num, node_num) * 1
    • 各层的激活值呈偏向0和1的分布
    • 在sigmoid函数中,随着输出不断靠近0或1,它的导数值逐渐接近0,因此会造成反向传播中梯度的值不断变小,最后消失,即梯度消失(gradient vanishing)

  • 使用标准差为0.01的高斯分布

    • W = np.random.randn(node_num, node_num) * 0.01
    • 各层的激活值呈集中在0.5附近的分布
    • 多个神经元都输出几乎相同的值,那么100个神经元可以由1个神经元来表达基本相同的事情,因此激活值的分布有所偏向,会出现“表现力受限”的问题

 💡 因此,我们要求各层激活值的分布要有适当的广度,这样通过在各层间传递多样性的数据,神经网络可以进行高效的学习

  • 使用Xavier初始值(在一般的深度学习框架中,已被作为标准使用)

    node_num = n  # 前一层的节点数
    W = np.random.randn(node_num, node_num) * np.sqrt(1 / node_num)
    
    • 如果前一层的结点数是n,则初始值使用标准差为 的高斯分布
    • 越是后面的层,图像变得越歪斜,但是呈现了比之前更有广度的分布
    • 如果使用tanh函数代替sigmoid函数,歪斜的问题可以得到改善

 

ReLU的权重初始值

  • 上述介绍的Xavier是以激活函数是线性函数为前提而推导出来的。但当激活函数使用ReLUctant时,使用“He初始值”

  • He初始值:当前一层的节点数为n时,He初始值使用标准差为 的高斯分布。和Xavier相比,因为ReLU的负值区域的值为0,为了使它更有广度,所以需要2倍的系数

    node_num = n  # 前一层的节点数
    W = np.random.randn(node_num, node_num) * np.sqrt(2 / node_num)
    

总结

  • 激活函数为sigmoid或tanh等S型曲线函数时,权重初始值使用Xavier初始值
  • 激活函数为ReLU时,初始值的赋值方式使用He初始值

3. Batch Normalization

为了使各层拥有适当的广度,Batch Norm方法可以“强制性”地调整激活值的分布,减小数据分布的偏向

  • 概念:以进行学习时的mini-batch为单位,按nini-batch进行正规化,就是进行是数据分布的均值为0,方差为1的正规化,然后对正规化后的数据进行缩放和平移的变换

  • 实现

    class BatchNormalization:
        """
        <http://arxiv/abs/1502.03167>
        """
        def __init__(self, gamma, beta, momentum=0.9, running_mean=None, running_var=None):
            self.gamma = gamma
            self.beta = beta
            self.momentum = momentum
            self.input_shape = None # Conv层的情况下为4维,全连接层的情况下为2维  
    
            # 测试时使用的平均值和方差
            self.running_mean = running_mean
            self.running_var = running_var  
            
            # backward时使用的中间数据
            self.batch_size = None
            self.xc = None
            self.std = None
            self.dgamma = None
            self.dbeta = None
    
        def forward(self, x, train_flg=True):
            self.input_shape = x.shape
            if x.ndim != 2:
                N, C, H, W = x.shape
                x = x.reshape(N, -1)
    
            out = self.__forward(x, train_flg)
            
            return out.reshape(*self.input_shape)
                
        def __forward(self, x, train_flg):
            if self.running_mean is None:
                N, D = x.shape
                self.running_mean = np.zeros(D)
                self.running_var = np.zeros(D)
                            
            if train_flg:
                mu = x.mean(axis=0)
                xc = x - mu
                var = np.mean(xc**2, axis=0)
                std = np.sqrt(var + 10e-7)
                xn = xc / std
                
                self.batch_size = x.shape[0]
                self.xc = xc
                self.xn = xn
                self.std = std
                self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu
                self.running_var = self.momentum * self.running_var + (1-self.momentum) * var            
            else:
                xc = x - self.running_mean
                xn = xc / ((np.sqrt(self.running_var + 10e-7)))
                
            out = self.gamma * xn + self.beta 
            return out
    
        def backward(self, dout):
            if dout.ndim != 2:
                N, C, H, W = dout.shape
                dout = dout.reshape(N, -1)
    
            dx = self.__backward(dout)
    
            dx = dx.reshape(*self.input_shape)
            return dx
    
        def __backward(self, dout):
            dbeta = dout.sum(axis=0)
            dgamma = np.sum(self.xn * dout, axis=0)
            dxn = self.gamma * dout
            dxc = dxn / self.std
            dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis=0)
            dvar = 0.5 * dstd / self.std
            dxc += (2.0 / self.batch_size) * self.xc * dvar
            dmu = np.sum(dxc, axis=0)
            dx = dxc - dmu / self.batch_size
            
            self.dgamma = dgamma
            self.dbeta = dbeta
            
            return dx
    
  • 优点:

    • 可以使学习快速进行(增大学习率)
    • 不那么依赖初始值
    • 抑制过拟合
  • 评估:几乎所有的情况下,使用Batch Norm时学习进行得更快;不使用时,如果不赋予一个尺度好的初始值,学习将完全无法进行

4. 正则化

过拟合

指只能拟合训练数据,但不能很好的拟合不包含在训练数据中的其他数据的状态

  • 产生原因:

    • 模型拥有大量参数、表现力强
    • 训练数据小
  • 现象:

    • 训练数据的识别精度几乎为100%
    • 但测试数据离100%的识别精度差距较大

 

权值衰减

权值衰减是一直以来经常被使用的一种抑制过拟合的方法,通过在学习的过程中对大的权值来进行惩罚,来抑制过拟合

  • 神经网络的学习目的是减小损失函数的值,假如给损失函数加上权重的平方范数,可以抑制权重变大

  • 将权重记为$W$,平方范数的权值衰减就是 , 是控制正则化强度的超参数,值越大,对大的权值施加的惩罚就越重,1/2 是用于求导结果变成

  • 实现:

    weight_decay = 0
    for idx in range(1, self.hidden_layer_num + 2):
        W = self.params['W' + str(idx)]
        weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)
    
    return self.last_layer.forward(y, t) + weight_decay
    
  • 使用权值衰减后的结果:

    • 训练数据和测试数据的精度差距变小,说明过拟合受到了抑制
    • 但是训练数据的识别精度没有到达100%

 

Dropout

权值衰减实现简单,但是难以应对复杂的网络模型,这时要使用Dropout

  • Dropout是一种在学习过程中随即删除神经元的方法,被删除的神经元不再进行信号的传递

    • 训练时,每传递一次数据,就会随机选择要删除的神经元
    • 测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出
  • 实现

    class Dropout:
    
        def __init__(self, dropout_ratio=0.5):
            self.dropout_ratio = dropout_ratio
            self.mask = None
    
        def forward(self, x, train_flg=True):
            if train_flg:
                # mask随机生成和 x 形状相同的数组,并将值比 dropout_ratio 大的元素设为True
                self.mask = np.random.rand(*x.shape) > self.dropout_ratio
                # 被删除的神经元值为False,不会被传递
                return x * self.mask
            else:
                return x * (1.0 - self.dropout_ratio)
    
        # 反向传播与ReLU相同
    		def backward(self, dout):
            return dout * self.mask
    
    • 正向传播时传递了信号的神经元,反向传播时按原样传递信号
    • 正向传播时没有传递信号的神经元,反向传播时信号将停在那里

集成学习

机器学习中经常使用集成学习,就是指让多个模型单独进行学习,推理时再取出多个模型的输出的平均值,可以提高识别精度

  • 用神经网络的语境来说,比如准备5个结构相同或类似的网络,分别进行学习,测试时,以这5个网络的输出的平均值作为答案
  • 集成学习与Dropout有密切的关系,Dropout通过随即删除神经元,从而每一次都让不同的模型进行学习,推理时通过对神经网络的输出乘以删除比例,可以取得模型的平均值
  • 也就是说Dropout将集成学习的效果通过一个网络实现了

5. 超参数的验证

神经网络中除了权重和偏置等参数,超参数也经常出现,比如各层神经元的数量、batch大小、参数更新时的学习率或权值衰减。以下将介绍尽可能高效寻找超参数的值的方法

验证数据

调整超参数时,必须使用超参数专用的确认数据,即验证数据

💡 训练数据用于参数的学习,验证数据用于超参数的性能评估,测试数据最后确认泛化能力

  • 有的数据集会事先分成训练数据、验证数据、测试数据三部分,有的需要自己分割

  • 例如MNIST数据集,可以从训练数据中事先分割20%作为验证数据

    def shuffle_dataset(x, t):
        """打乱数据集
    
        Parameters
        ----------
        x : 训练数据
        t : 监督数据
    
        Returns
        -------
        x, t : 打乱的训练数据和监督数据
        """
        permutation = np.random.permutation(x.shape[0])
        x = x[permutation,:] if x.ndim == 2 else x[permutation,:,:,:]
        t = t[permutation]
    
        return x, t
    
    (x_train, t_train), (x_test, t_test) = load_mnist()
    # 打乱训练数据
    x_train, t_train = shuffle_dataset(x_train, t_train)
    # 分割验证数据
    validation_rate = 0.20
    validation_num = int(x_train.shape[0] * validation_rate)
    
    x_val = x_train[:validation_num]
    t_val = t_train[:validation_num]
    x_train = x_train[validation_num:]
    t_train = t_train[validation_num:]
    

超参数的最优化

  • 步骤0
    • 设定超参数的范围
  • 步骤1
    • 从设定的超参数范围中随机采样
  • 步骤2
    • 使用步骤1中采样到的超参数的值进行学习,通过验证数据评估识别精度(将epoch设置得很小)
    • 因为深度学习需要很长时间,使用要尽早放弃那些不符合逻辑的超参数,我们需要减少学习的epoch,缩短一次评估所需的时间
  • 步骤3
    • 重复步骤1和步骤2(100次等),根据识别精度的结果,缩小超参数的范围
  • 在缩小到一定程度后,从中选出一个值

这里介绍的方法是实践性的,还有贝叶斯最优化

本文标签: 学习笔记深度入门理论技巧