搭建深度学习框架(四) Dropout与Batch Normalization

Dropout

网络过于习惯训练集的数据分布, 尤其是如果训练集规模还不够大时, 网络会出现严重的过拟合, 使得在训练集上的准确率非常高, 但是测试集上的准确率很低. 而且这个过程还不可逆, 一旦网络过拟合那网络就废了, 我们的努力全部木大.
神经网络中的一种有效的对抗过拟合的方法是dropout, 它基于模型融合的思想, 让当前隐层的一些神经元暂时死掉, 它的输出将是0 ,从而不对本次前向和反向传播产生任何影响. 这样做的好处是, 如果这样的模型仍然能被训练到收敛, 那么我们可以认为我们从隐层中抽取出一定比例的神经元也仍然是收敛的. 然而这样随机采样得到的很多小一些的子网络与子网络之间是有些微的差异的, 这些差异经过模型融合, 能让总网络变成一个不具有倾向性的模型(委员会思想). 从而模型的偏差可能会上升一些, 但是方差一般会降低更多.
在这里插入图片描述
它的前向和反向传播非常简单, 我们只需要给定一个概率, 然后它就会生成一个mask把一定比例的x置零再输出. 反向传播时, 因为mask处的x被置零了, 就相当于点乘了一个0常数, 那么它的反向传播也是乘一个0, mask把对应位置的dz置零再反向传播.

Batch-Normalization

接下来我们要讲一下, 能让训练加速的一种技巧, 叫做batch norm. 相对于卷积和循环的网络, 这种技术的出现是2015年, 是很年轻的技术. 这种方法针对训练中(尤其是在深度网络里)遇到的困难提出一些解决方案, 之前我们的代码实践虽然是深度学习, 但并不算是非常deep的网络, 真正很深的大型网络在训练时会面临Internal Covariate Shift的问题. 简单来说, 我们的反向传播计算梯度是逐层进行的, 当我们更新了第l层的参数, 第l层的输出也会改变, 从概率的角度解释就是分布发生了改变. 这样l+1层就需要去适应新的输入分布, 这个过程需要时间. 可怕的是, 当网络很深时, 这个适应过程就会非常复杂且困难, 为了加速深度网络的训练, 有一种想法是, 我们人为地改变数据分布, 每一隐层无论何时, 分布都是相似的. 一种方法就是规范化方法, 我们把数据分布减去分布的均值再除以标准差, 这样分布就会被规约到和标准正态分布类似. 注意这里的规范化是对每一维度独立进行的, 而不是计算样本集的协方差矩阵, 那样会浪费大量算力.
μj=1mi=1mZji \mu_j = \frac{1}{m}\sum_{i=1}^m Z_{ji}
σj2=1mi=1m(Zjiμj)2 \sigma_j^2 = \frac{1}{m}\sum_{i=1}^m (Z_{ji}-\mu_j)^2
Z^ji=Zjiμjσj \hat Z_{ji} = \frac{Z_{ji}-\mu_j}{\sigma_j}
这个做法当然是可以的, 我们在数据科学中使用的一种数据预处理方式就是这种Normalization. 在神经网络中当然也可以用, 而且很适合, 因为大多数我们用到的激活函数都是在输入x=0时斜率最大, 从而梯度最大, 学习速度最快. 如果我们把这种方法应用在隐层间, 就能解决上面Internal Covariate Shift的问题.
但是如果真的那么做, 会出现新的问题. 我们知道神经网络的一大特点就是越深层, 特征就越抽象. 最明显的例子是自编码器, 如果我们用自编码器处理MNIST数据, 并把隐层的特征在2D平面做可视化, 会看见不同类别的数据会被编码到独立的区域. 这就是深度的网络对数据处理后的结果. 但是这时我们用一个这样的norm处理它, 很大可能会把网络已经提取到的不同的特征再次混合在一起, 网络的表达能力遭到伤害. 那么要怎样既能保住底层网络学习到的参数信息, 又能修正数据分布, 方便网络训练呢?
BN的最大贡献就是这部分, 我们再次加入两个参数, 把数据分布再做一个一维线性变换, 把分布拉离0, 并修正方差. 也就是
yji=γjZ^ji+βj y_{ji} = \gamma_j\hat Z_{ji} + \beta_j
相当于和上面的规范化相对的一种方法, 如果mu和beta相等, gamma和sigma相等, 这就是一个没有进行变换的变换. 通过上面的步骤,我们就在一定程度上保证了输入数据的表达能力. 有时我们会为了处理分母, 用一个eps来辅助运算, 总的运算方法如下.
在这里插入图片描述

BN的反向传播

这个过程的反向传播是怎么做的呢? 其实再怎么复杂也无非是代数求导, 只要有耐心推就怎么都推得出来.
Lβj=SUMROWLyij \frac{\partial L}{\partial \beta_j} = SUMROW \frac{\partial L}{\partial y_{ij}}
Lγj=SUMROWLyijZij \frac{\partial L}{\partial \gamma_j} = SUMROW \frac{\partial L}{\partial y_{ij}}*Z_{ij}
下面的部分才比较复杂, 均值的偏导比较容易, 但是方差不但有分数形式还与均值有关, 我们分三步来看向量形式的求导
在这里插入图片描述
这里抄一张图, batchnorm的计算图展开, 可视化的计算可能会更为清晰
在这里插入图片描述

代码实现

导入包, 还和之前一样, 我们不使用torch.nn的模块, 只使用张量运算工具, 核心的前向后向算法我们自己实现

import torch
import math
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F

Dropout实现非常简单, 设置随机的mask屏蔽一些前向传播和后向传播即可

class Dropout:
    '''
    随机把一些神经元的输出变为0
    从而前向和反向传播中,这个神经元将不再发挥作用
    '''
    def __init__ (self, prob, train = True):
        self.p = prob # dropout 的概率
        self.train = train # 训练模式,dropout层启用
        
    def forward(self,X):
        if not self.train: return X
        self.mask = torch.rand(len(X))>self.p
        X[self.mask] = 0
        return X
    
    def backward(self,dz):
        if not self.train: return dz
        dz[self.mask] = 0
        return dz
    
    def __call__(self, X):
        return self.forward(X)

batch norm相对比较麻烦, 但是对着公式敲也能敲出来. 这里只实现极简的版本, 不设置eps参数, 不实现怎么更新参数的update方法, 当然也不包括更新相关的动量参数.

class BatchNorm:
    def __init__(self, dim):
        self.w = torch.randn(dim)*math.sqrt(2/(dim))
        self.b = torch.randn(dim)*math.sqrt(2/(dim))
        self.dim = dim
        self.x = None
        self.mu = None
        self.sigma = None
        self.param  = True
    
    def forward(self, x):
        self.x = x
        self.mu = torch.mean(x,axis = 0)
        self.sigma = torch.sqrt(torch.var(x,axis = 0, unbiased=False))
        n,m = x.shape
        assert m==self.dim
        self.n = n
        self.xhat = (x-self.mu)/self.sigma
        self.out = self.w*(self.xhat)+self.b
        return self.out
    
    def backward(self, dz):
        self.db = torch.sum(dz, axis = 0)
        self.dw = torch.sum(self.xhat*dz, axis = 0)
        
        M_dx = self.x-self.mu
        dL_dxhat = dz*self.w
        
        dL_dx1 = dL_dxhat*(1/self.sigma).unsqueeze(0).repeat(self.n,1)
        
        dL_dsigma2 = dL_dxhat*(M_dx/(-2*self.sigma**3))
        dL_dsigma2 = torch.sum(dL_dsigma2,axis = 0)
        means = torch.mean(M_dx,axis = 0)
        dsigma2_dx = 2*(M_dx-means)/self.n
        dL_dx2 = dL_dsigma2*dsigma2_dx
        
        dL_dmu = dL_dsigma2*(-2*means)-torch.sum(dL_dx1,axis = 0)
        dL_dx3 = (1/self.n)*torch.ones((self.n,self.dim))*dL_dmu
        
        dx = dL_dx1+dL_dx2+dL_dx3
        return dx
        
    def __call__(self, X):
        return self.forward(X)

梯度测试

我们用torch的标准batchnorm1d和反向传播工具计算梯度, 比较它和我们自己实现的batchnorm的前向后向得到的结果

x = torch.randn(5,2)
target = torch.randn(5,2)

bn_torch = torch.nn.BatchNorm1d(2)
bn_torch.eps = 0
bn_torch.momentum = 0
bn_torch.train()


x.requires_grad = True
y = bn_torch(x)
loss = F.mse_loss(y,target)
bn_torch.zero_grad()
x.grad.data.zero_()
loss.backward()
print("output:\n",y)
print("weight grad:\n",bn_torch.weight.grad)
print("bias grad:\n",bn_torch.bias.grad)
print("x grad:\n",x.grad)

output:
 tensor([[ 0.0093,  0.0303],
        [-0.4916,  0.3338],
        [ 0.1308, -1.7180],
        [-1.3539,  1.3910],
        [ 1.7053, -0.0372]], grad_fn=<NativeBatchNormBackward>)
weight grad:
 tensor([1.1435, 1.5440])
bias grad:
 tensor([-0.4674, -0.0078])
x grad:
 tensor([[ 0.0288,  0.3495],
        [ 0.0996,  0.1467],
        [-0.3124, -0.2545],
        [ 0.0733, -0.3541],
        [ 0.1107,  0.1124]])
bn = BatchNorm(2)
bn.w = bn_torch.weight.detach()
bn.b = bn_torch.bias.detach()
y = bn(x)
dx = bn.backward((y-target)/5)

print("output:\n",y)
print("weight grad:\n",bn.dw)
print("bias grad:\n",bn.db)
print("x grad:\n",dx)

output:
 tensor([[ 0.0093,  0.0303],
        [-0.4916,  0.3338],
        [ 0.1308, -1.7180],
        [-1.3539,  1.3910],
        [ 1.7053, -0.0372]], grad_fn=<AddBackward0>)
weight grad:
 tensor([1.1435, 1.5440], grad_fn=<SumBackward1>)
bias grad:
 tensor([-0.4674, -0.0078], grad_fn=<SumBackward1>)
x grad:
 tensor([[ 0.0288,  0.3495],
        [ 0.0996,  0.1467],
        [-0.3124, -0.2545],
        [ 0.0733, -0.3541],
        [ 0.1107,  0.1124]], grad_fn=<AddBackward0>)

工程细节

在实际使用batch norm时, 深度学习框架通常会把模块定义成两个模式, 一个是训练模式一个是测试模式. 在训练时, 我们的BN层会不断吃数据, 并计算出一个对整个训练集而言的均值和方差, 在测试时, 我们会用这个均值和方差进行计算. 从而让训练时的局部性不会对测试产生影响. 具体到实现上就是train模式和eval模式.
再者就是上面提到的epsilon避免方差过小. 更新batch norm的方法是梯度下降, 我们计算出w和b的梯度后会用梯度优化方法优化参数, 这时pytorch还设置了动量, 避免w和b产生过大的更新. 从实践经验上来看, 这一点的帮助还是相当大的.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章