[深度学习]动手学深度学习笔记-5

Task2——梯度消失、梯度爆炸
在这里插入图片描述

5.1 梯度消失与梯度爆炸的概念

深度神经网络训练的时候,采用的是反向传播方式,该方式使用链式求导,计算每层梯度的时候会涉及一些连乘操作,因此如果网络过深。

  1. 那么如果连乘的因子大部分小于1,最后乘积的结果可能趋于0,也就是梯度消失,后面的网络层的参数不发生变化.
  2. 那么如果连乘的因子大部分大于1,最后乘积可能趋于无穷,这就是梯度爆炸

5.2 梯度消失与梯度爆炸的后果

  1. 梯度消失会导致我们的神经网络中前面层的网络权重无法得到更新,也就停止了学习。
  2. 梯度爆炸会使得学习不稳定, 参数变化太大导致无法获取最优参数。
  3. 在深度多层感知机网络中,梯度爆炸会导致网络不稳定,最好的结果是无法从训练数据中学习,最坏的结果是由于权重值为NaN而无法更新权重。
  4. 在循环神经网络(RNN)中,梯度爆炸会导致网络不稳定,使得网络无法从训练数据中得到很好的学习,最好的结果是网络不能在长输入数据序列上学习。

5.3 梯度消失与梯度爆炸的原因

我们在这以一个例子来说明:
在这里插入图片描述
如上图,是一个每层只有一个神经元的神经网络,且每一层的激活函数为sigmoid,则有:yi=σ(zi)=σ(wixi+bi)y_{i}=sigmaleft(z_{i} ight)=sigmaleft(w_{i} x_{i}+b_{i} ight)
σsigma是sigmoid函数。
根据反向传播算法有:δCδb1=δCδy4δy4δz4δz4δx4δx4δz3δz3δx3δx3δz2δz2δx2δx2δz1δz1δb1frac{delta C}{delta b_{1}}=frac{delta C}{delta y_{4}} frac{delta y_{4}}{delta z_{4}} frac{delta z_{4}}{delta x_{4}} frac{delta x_{4}}{delta z_{3}} frac{delta z_{3}}{delta x_{3}} frac{delta x_{3}}{delta z_{2}} frac{delta z_{2}}{delta x_{2}} frac{delta x_{2}}{delta z_{1}} frac{delta z_{1}}{delta b_{1}}
=δCδy4(σ(z4)w4)(σ(z3)w3)(σ(z2)w2)(σ(z1))=frac{delta C}{delta y_{4}}left(sigma^{prime}left(z_{4} ight) w_{4} ight) quadleft(sigma^{prime}left(z_{3} ight) w_{3} ight) quadleft(sigma^{prime}left(z_{2} ight) w_{2} ight) quadleft(sigma^{prime}left(z_{1} ight) ight)
而sigmoid函数的导数公式为:
S(x)=ex(1+ex)2=S(x)(1S(x))S^{prime}(x)=frac{e^{-x}}{left(1+e^{-x} ight)^{2}}=S(x)(1-S(x))sigmoid函数及其它的图形曲线为:

sigmoid函数图像如上图

在这里插入图片描述

sigmoid函数导数图像如上图

由上可见,sigmoid函数的导数σ(x)sigma^{prime}(x)的最大值为14frac{1}{4} ,通常我们会将权重初始值 w|w| 初始化为为小于1的随机值,因此我们可以得到σ(z4)w4<14left|sigma^{prime}left(z_{4} ight) w_{4} ight|<frac{1}{4},随着层数的增多,那么求导结果δCδb1frac{delta C}{delta b_{1}}越小,这也就导致了梯度消失问题。

那么如果我们设置初始权重 w|w|较大,那么会有σ(z4)w4>1left|sigma^{prime}left(z_{4} ight) w_{4} ight|>1 ,造成梯度太大(也就是下降的步伐太大),这也是造成梯度爆炸的原因。

总之,无论是梯度消失还是梯度爆炸,都是源于网络结构太深,造成网络权重不稳定,从本质上来讲是因为梯度反向传播中的连乘效应。

5.4 解决方法

5.4.1 换用Relu、LeakyRelu、Elu等激活函数

  1. ReLu:让激活函数的导数为1

  2. LeakyReLu:包含了ReLu的几乎所有有点,同时解决了ReLu中0区间带来的影响

  3. ELU:和LeakyReLu一样,都是为了解决0区间问题,相对于来,elu计算更耗时一些

5.4.2 Batch Normalization

Batchnorm是深度学习发展以来提出的最重要的成果之一了,目前已经被广泛的应用到了各大网络中,具有加速网络收敛速度,提升训练稳定性的效果,Batchnorm本质上是解决反向传播过程中的梯度问题。batchnorm全名是batch normalization,简称BN,即批规范化,通过规范化操作将输出信号x规范化保证网络的稳定性。
具体的batchnorm原理非常复杂,在这里不做详细展开,此部分大概讲一下batchnorm解决梯度的问题上。具体来说就是反向传播中,经过每一层的梯度会乘以该层的权重,举个简单例子:
正向传播中f2=f1(wTx+b)f_{2}=f_{1}left(w^{T} * x+b ight),那么反向传播中,f2x=f2f1xfrac{partial f_{2}}{partial x}=frac{partial f_{2}}{partial f_{1}} x,反向传播式子中有xx的存在,所以xx的大小影响了梯度的消失和爆炸,batchnorm就是通过对每一层的输出规范为均值和方差一致的方法,消除了xx带来的放大缩小的影响,进而解决梯度消失和爆炸的问题,或者可以理解为BN将输出从饱和区拉倒了非饱和区。

5.4.3 ResNet残差结构

在这里插入图片描述
具体理解见此文
总的来说,Resnet的核心思想就是更改了网络结构的学习目的,原本学习的是直接通过卷积得到的图像特征H(X),现在学习的是图像与特征的残差H(X)-X,这样更改的原因是因为残差学习相比原始特征的直接学习更加容易。以下进行一个简单的证明。
ResNet代码实现:

import torch.nn as nn
import math
import torch.utils.model_zoo as model_zoo

def conv3x3(in_planes, out_planes, stride=1):
    
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                     padding=1, bias=False)

class BasicBlock(nn.Module):
    expansion = 1
    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.conv1 = conv3x3(inplanes, planes, stride)
        self.bn1 = nn.BatchNorm2d(planes)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(planes, planes)
        self.bn2 = nn.BatchNorm2d(planes)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = self.relu(out)

        return out

class ResNet(nn.Module):

    def __init__(self, block, layers, num_classes=1000):
        self.inplanes = 64
        super(ResNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)

        return x

def resnet18(pretrained=False, **kwargs):
  trained (bool): If True, returns a model pre-trained on ImageNet
    
    model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
    return model

5.4.4 LSTM结构

参见本人的第6篇学习笔记

5.4.5 预训练加finetunning

此方法来自Hinton在06年发表的论文上,其基本思想是每次训练一层隐藏层节点,将上一层隐藏层的输出作为输入,而本层的输出作为下一层的输入,这就是逐层预训练。
训练完成后,再对整个网络进行“微调(fine-tunning)”。
此方法相当于是找全局最优,然后整合起来寻找全局最优,但是现在基本都是直接拿imagenet的预训练模型直接进行finetunning。

5.4.6 梯度剪切、正则

这个方案主要是针对梯度爆炸提出的,其思想是设值一个剪切阈值,如果更新梯度时,梯度超过了这个阈值,那么就将其强制限制在这个范围之内。这样可以防止梯度爆炸。
另一种防止梯度爆炸的手段是采用权重正则化,正则化主要是通过对网络权重做正则来限制过拟合,但是根据正则项在损失函数中的形式:
可以看出,如果发生梯度爆炸,那么权值的范数就会变的非常大,反过来,通过限制正则化项的大小,也可以在一定程度上限制梯度爆炸的发生。
end


[参考文档]
[1] https://www.jianshu.com/p/3f35e555d5ba
[2] https://www.cnblogs.com/pigbreeder/p/8051442.html
[3] https://www.cnblogs.com/pinking/p/9418280.html
[4] https://www.jianshu.com/p/ec0967460d08
[5] https://zhuanlan.zhihu.com/p/38537439

原文地址:https://www.cnblogs.com/Jack-Tim-TYJ/p/12831950.html