【DL学习笔记05】深度学习入门——基于Python的理论与实现(ch06: 与学习相关的技巧)

目录

1. 参数的更新

SGD

Momentum

AdaGrad

Adam

总结

2. 权重的初始值

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

隐藏层的激活值的分布

ReLU的权重初始值

总结

3. Batch Normalization

4. 正则化

过拟合

权值衰减

Dropout

集成学习

5. 超参数的验证

验证数据

超参数的最优化


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

1. 参数的更新

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

SGD

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

  • 公式:

    • W\leftarrow W-\eta \frac{\partial L}{\partial W}
    • W为需要更新的权重参数,\frac{\partial L}{\partial 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是“动量”的意思,和物理有关

  • 公式:

    • v\leftarrow \alpha \nu-\eta \frac{\partial L}{\partial W}(1)
    • W\leftarrow W+\nu(2)
    • \eta 表示学习率,\nu 对应物理上的速度
    • (1)表示了物体在梯度方向上受力,速度增加,其中\alpha \nu 表示物体不受力时,物体逐渐减速,对应物理上的摩擦或空气阻力
  • 实现

    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.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种方法各有特点,都有擅长解决和不擅长解决的问题,很多研究中至今仍在使用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,则初始值使用标准差为\frac{1}{\sqrt{n}} 的高斯分布
    • 越是后面的层,图像变得越歪斜,但是呈现了比之前更有广度的分布
    • 如果使用tanh函数代替sigmoid函数,歪斜的问题可以得到改善

 

ReLU的权重初始值

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

  • He初始值:当前一层的节点数为n时,He初始值使用标准差为\sqrt{\frac{2}{n}} 的高斯分布。和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.org/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$,平方范数的权值衰减就是\frac 12\lambda W^2\lambda 是控制正则化强度的超参数,值越大,对大的权值施加的惩罚就越重,1/2 是用于求导结果变成 \lambda W

  • 实现:

    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次等),根据识别精度的结果,缩小超参数的范围
  • 在缩小到一定程度后,从中选出一个值

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值