批量归一化(BN)

1. 数据标准化处理

输入层使用,使输入数据各个特征的分布相近

  为什么要这样做?

  1. 神经网络学习的本质就是学习数据的分布,如果训练数据与测试数据的分布不同,那么网络的泛化能力也大大降低;
  2. 在使用小批量数据对神经网络进行训练时,若每批训练数据的分布各不相同,网络在每次迭代都去学习适应不同的分布,这会大大降低网络的训练速度;

2.为什么要使用批量归一化?

使用浅层模型时,随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化,对深层神经网络来说,随着网络训练的进行,前一层参数的调整使得后一层输入数据的分布发生变化,各层在训练的过程中就需要不断的改变以适应学习这种新的数据分布。所以即使输入数据已做标准化,训练中模型参数的更新依然很容易导致后面层输入数据分布的变化,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。最终造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。如果训练过程中,训练数据的分布一直在发生变化,那么将不仅会增大训练的复杂度,影响网络的训练速度而且增加了过拟合的风险。

批量归一化可以看作在每一层输入和上一层输出之间加入了一个新的计算层,对数据的分布进行额外的约束,从而增强模型的泛化能力。但是批量归一化同时也降低了模型的拟合能力,归一化之后的输入分布被强制拉到均值为0和标准差为1的正态分布上来。以Sigmoid激活函数为例,批量归一化之后数据整体处于函数的非饱和区域,只包含线性变换(多层的线性函数跟一层线性网络是等价的,网络的表达能力下降),破坏了之前学习到的特征分布。为了恢复原始数据分布,保证非线性的获得,引入了变换重构以及可学习参数γ和β,其对应于输入数据分布的方差和偏差

3.批量归一化的优势:

  • 不加批量归一化的网络需要慢慢的调整学习率时,网络中加入批量归一化时,可以采用初始化很大的学习率,然后学习率衰减速度也很大,因此这个算法收敛很快。
  • 大大提高模型训练速度,提高网络泛化性能。
  • 数据批量归一化后相当于只使用了S型激活函数的线性部分,可以缓解S型激活函数反向传播中的梯度消失的问题。
    • 深层神经网络在做非线性变换前的激活输入值,随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,整体分布逐渐往非线性函数的取值区间的上下限两端靠近(下图左图),这会导致反向传播时低层神经网络的梯度消失,BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0方差为1的标准正态分布(如右图),使得激活输入值落在非线性函数对输入比较敏感的线性区域,其对应的导数远离导数饱和区 ,这样输入的小变化就会导致损失函数较大的变化,避免梯度消失问题产生,学习收敛速度快,能大大加快训练速度。

 4.如何使用批量归一化?

  1. 求每一个训练批次数据的均值
  2. 求每一个训练批次数据的方差
  3. 使用求得的均值和方差对该批次的训练数据做归一化,获得0-1分布。其中ε是为了避免除数为0时所使用的微小正数。
  4. 尺度变换和偏移:将xi乘以γ调整数值大小,再加上β增加偏移后得到yi,这里的γ是尺度因子,β是平移因子。这一步是BN的精髓,由于归一化后的xi基本会被限制在正态分布下,使得网络的表达能力下降。为解决该问题,我们引入两个新的参数:γ,β。 γ和β是在训练时网络自己学习得到的。

 

a中左图是没有经过任何处理的输入数据,曲线是sigmoid函数,如果数据在梯度很小的区域,那么学习率就会很慢甚至陷入长时间的停滞。减均值除方差后,数据就被移到中心区域如右图所示,对于大多数激活函数而言,这个区域的梯度都是最大的或者是有梯度的(比如ReLU),这可以看做是一种对抗梯度消失的有效手段。对于一层如此,如果对于每一层数据都那么做的话,数据的分布总是在随着变化敏感的区域,相当于不用考虑数据分布变化了,这样训练起来更有效率。

那么为什么要有第4步,不是仅使用减均值除方差操作就能获得目的效果吗?

因为减均值除方差操作后可能会削弱网络的性能!针对该情况,在前面三步之后加入第4步完成真正的batch normalization

BN的本质就是利用优化变一下方差大小和均值位置,使得新的分布更切合数据的真实分布,保证模型的非线性表达能力。BN的极端的情况就是这两个参数等于mini-batch的均值和方差,那么经过batch normalization之后的数据和输入完全一样,当然一般的情况是不同的

4.1 全连接层:

将批量归一化层置于全连接层中的仿射变换和激活函数之间

4.2 卷积层:

在卷积计算之后、应用激活函数之前

卷积层上的 BN 使用,其实也是使用了类似权值共享的策略,把一整张特征图当做一个神经元进行处理。卷积神经网络经过卷积后得到的是一系列的特征图,如果 min-batch sizes 为 m,那么网络某一层输入数据可以表示为四维矩阵(m,f,w,h),m 为 min-batch sizes,f 为特征图个数,w、h 分别为特征图的宽高。在 CNN 中我们可以把每个特征图看成是一个特征处理(一个神经元),因此在使用 Batch Normalization,mini-batch size 的大小就是:m*w*h,于是对于每个特征图都只有一对可学习参数:γ、β。说白了就是相当于求取所有样本所对应的一个特征图的所有神经元的平均值、方差,然后对这个特征图神经元做归一化。

BN在深层神经网络的作用非常明显:若神经网络训练时遇到收敛速度较慢,或者“梯度爆炸”等无法训练的情况发生时都可以尝试用BN来解决。同时,常规使用情况下同样可以加入BN来加速模型训练,甚至提升模型精度

4.3 预测时均值和方差怎么求?

训练时,对同一批的数据的均值和方差进行求解,进而进行归一化操作

以下是一种可能的方案

在模型训练时我们就记录下每个batch下的均值和方差,待训练完毕后,我们求整个训练样本的均值和方差期望值,作为我们进行预测时进行BN的的均值和方差 

0

5.代码实现

import torch
from torch import nn, optim
import torchvision
import sys
from time import time


device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')

def batch_norm(is_training, X, gamma, beta, moving_mean,moving_var, eps, momentum):
    # 判断当前模式是训练模式还是预测模式
    if not is_training:
        '''预测模式下,直接使用传入的移动平均所得的均值和方差'''
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        '''训练模式'''
        # 判断输入数据 X 是全连接层还是卷积层
        assert len(X.shape) in (2, 4)
        ''' 全连接层'''
        if len(X.shape) == 2:
            # 沿纵向求均值,(1, 特征个数)
            mean = X.mean(dim=0) ## 逐特征求均值
            # 广播机制
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            '''使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。'''
            
            # 这里需要保持X的形状以便后面可以做广播运算
            mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            # 以上代码可以优化为
            # mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            # 以上代码可以优化为
            # var = ((X - mean) ** 2)(dim=(0, 2, 3), keepdim=True)
            
        ''' 训练模式下用当前的均值和方差做标准化'''
        X_hat = (X - mean) / torch.sqrt(var + eps)
        
        '''一阶指数平滑算法'''
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
        
    '''尺度变换和偏移'''
    Y = gamma * X_hat + beta
    
    return Y, moving_mean, moving_var

用于批量归一化操作的函数 batch_norm 的输入参数,具体含义如下:

is_training:一个布尔值,表示当前是否处于训练模式。在训练过程中,会计算并更新移动平均值和移动方差,而在推理(inference)过程中,直接使用已经计算好的移动平均值和移动方差进行归一化操作。

X:输入数据,可以是全连接层的输出或卷积层的输出。

gamma:尺度参数,可学习参数,用于对归一化后的数据进行尺度变换。它的形状与 X 的形状相同。

beta:偏移参数,可学习参数,用于对归一化后的数据进行偏移操作。它的形状与 X 的形状相同。

moving_mean:移动平均值,记录训练过程中的平均值。在训练过程中,会通过指数平滑算法不断更新。它的形状与 X 的形状相同。

moving_var:移动方差,记录训练过程中的方差。在训练过程中,会通过指数平滑算法不断更新。它的形状与 X 的形状相同。

eps:一个小的常数,用于避免除零错误。当计算标准差时,如果方差过小,会出现除以接近零的情况,为了稳定计算,引入 eps 来避免这个问题。

momentum:一个介于 0 和 1 之间的参数,表示移动平均值和移动方差在指数平滑算法中的衰减率。较大的 momentum 值可以使模型更加稳定,但可能导致较长的收敛时间。

使用一阶指数平滑算法的批量归一化

一阶指数平滑算法(First-order Exponential Smoothing)是一种用于计算移动平均值的方法,常用于时间序列数据的平滑处理。它的基本思想是对当前的观测值进行加权平均,同时考虑过去观测值的影响。
在批量归一化中,一阶指数平滑算法用于更新移动平均值和移动方差。具体步骤如下:
          1.假设当前的观测值为 x,移动平均值为 m_old。
          2.计算新的移动平均值 m_new,通过以下公式计算: m_new = momentum * m_old + (1 - momentum) * x 其中momentum  是一个介于 0 和 1 之间的参数,表示过去观测值在计算中所占的权重,通常取较小的值,如 0.9。
          3.更新移动平均值 m_old 为 m_new,用于下一次的计算。
通过一阶指数平滑算法,移动平均值可以不断地根据当前观测值进行调整,使其更好地反映数据的趋势变化。在批量归一化中,使用一阶指数平滑算法更新移动均值和移动方差可以提高模型的稳定性和收敛速度。

class BatchNorm(nn.Module):
    def __init__(self, num_features, num_dims):
        super(BatchNorm, self).__init__()
        '''全连接层'''
        if num_dims==2:
            shape = (1, num_features)
        else:
            '''卷积层'''
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        
        # 不参与求梯度和迭代的变量,全在内存上初始化成0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.zeros(shape)
        
    def forward(self, X):
        # 如果X不在显存上,将moving_mean和moving_var复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
            
        # 调用 batch_norm 函数进行批量归一化操作
        Y, self.moving_mean, self.moving_var = batch_norm(
            self.training, X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y
            

使用批量归一化层的LeNet

class LeNet_1(nn.Module):
    def __init__(self):
        super(LeNet_1, self).__init__()
        self.conv = nn.Sequential(
            #c1
            nn.Conv2d(1, 6, 5), 
            nn.BatchNorm2d(6),
            nn.Sigmoid(),
            #s2
            nn.MaxPool2d(2, 2),
            #c3
            nn.Conv2d(6, 16, 5),
            nn.BatchNorm2d(16),
            nn.Sigmoid(),
            #s4
            nn.MaxPool2d(2, 2)
        )
        self.fc = nn.Sequential(
            nn.Linear(16*4*4, 120),
            nn.BatchNorm1d(120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.BatchNorm1d(84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )
    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output
    
net = LeNet_1()
print(net)

训练

 

'''定义读取数据集函数'''
def load_data_fashion_mnist(batch_size, resize=None):
    
    if sys.platform.startswith('win'):
        num_workers = 0  # 0表示不用额外的进程来加速读取数据
    else:
        num_workers = 4
        
    '''定义数据预处理的转换函数列表'''    
    trans = []
    if resize: #判断是否需要进行图像尺寸调整(resize)
        trans.append(torchvision.transforms.Resize(size=resize)) 
        #将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
    trans.append(torchvision.transforms.ToTensor())
    # 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
    transform = torchvision.transforms.Compose(trans) 
    #通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
    
    mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
                                                    train=True,
                                                    download=True,
                                                    transform=transform)
    mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
                                                   train=False,
                                                   download=True,
                                                   transform=transform)
    train_iter = torch.utils.data.DataLoader(mnist_train,
                                             batch_size=batch_size,
                                             shuffle=True,
                                             num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(mnist_train,
                                             batch_size=batch_size,
                                             shuffle=False,
                                             num_workers=num_workers) 
    return train_iter, test_iter


'''用于GPU的准确率评估函数'''
def evaluate_accuracy(data_iter, net, device=None): 
    if device is None and isinstance(net, torch.nn.Module):
        device = list(net.parameters())[0].device #如果没指定device就使用net的device
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        if isinstance(net, torch.nn.Module):
            net.eval() # 评估模式, 这会关闭dropout
            acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
            net.train() # 改回训练模式
        else:
            if ('is_training' in net.__code__.co_varnames):
                acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
            else:
                acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

'''定义测试函数,确保计算使用的数据和模型同在内存或显存上'''
def train_ch5(net, train_iter, test_iter,  batch_size, optimizer , device, num_epochs):
    # 将模型加载到指定运算器中
    net = net.to(device)
    print("training on ", device)
    loss = torch.nn.CrossEntropyLoss()
    for epoch in range(num_epochs):
    # 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)
        train_l_sum, train_acc_sum, n ,batch_count= 0.0, 0.0, 0,0
        # 分别表示训练损失总和、训练准确度总和、样本数
        start = time()
        for X, y in train_iter:
            X = X.to(device)
            y = y.to(device)
            '''前向传播'''
            y_hat = net(X) 
            #由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
            '''计算损失'''
            l = loss(y_hat, y).sum()
            '''梯度清零'''
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            '''反向传播'''
            l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
            
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else: 
                '''更新参数'''
                optimizer.step()
            '''计算精度'''  
            train_l_sum += l.cpu().item()  
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
            n += y.shape[0]
            batch_count += 1
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f,\
        time %.1f sec' %(epoch+1, train_l_sum/batch_count, 
                         train_acc_sum/n, test_acc,
                         time()-start))

'''训练模型'''
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size,optimizer, device, num_epochs)

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
批量归一化(Batch Normalization,简称BN)是一种常用于深度神经网络的技术,旨在加速模型训练并提高模型的泛化能力。它的作用是对输入数据进行标准化处理,即将数据按特征维度减去均值,再除以标准差,以使其分布接近标准正态分布。 BN主要包括两个步骤:训练阶段和推理阶段。 在训练阶段,对于每个批次的数据,首先计算该批次数据在每个特征维度上的均值和方差,并使用这些统计量对该批次的数据进行标准化处理。然后,通过一个可学习的缩放因子和偏移项,对标准化后的数据进行线性变换,以恢复数据的表达能力。这样可以使得输入数据在网络中传播时的数值更稳定,并且减少了网络对初始参数的敏感程度。 在推理阶段,由于每个测试样本可能无法组成完整的批次,无法计算该批次数据的均值和方差。因此,在训练阶段通过累积一批次数据的均值和方差得到的统计量进行标准化处理。这种方法保持了训练阶段获得的统计特性,并且可以应用于单个测试样本。 批量归一化处理BN的优点包括: - 提高模型训练的收敛速度和稳定性。 - 减少了对初始参数选择的敏感度,使得网络更容易训练。 - 缓解了梯度消失和梯度爆炸问题。 - 有一定的正则化效果,减少了对 Dropout 等正则化技术的依赖。 需要注意的是,BN在一些情况下可能会引入一定的计算开销,且对小批次数据的效果可能不如大批次数据明显。在实际应用中,BN通常被应用于卷积神经网络(CNN)和全连接神经网络(FCN)中,并在很多深度学习任务中得到了广泛应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值