批量归一化(Batch Normalization)及其实现

该文章已生成可运行项目,

批量归一化(Batch Normalization)及其实现

在学习沐神的视频时,看到批量归一化这个视频时,有些难理解,故将这一课沐神将的内容加上自己的一些理解写成这篇笔记,原视频指路:批量归一化

批量归一化所考虑要解决的问题

模型在反向传播计算梯度时,由于使用的激活函数使得梯度基本上都比较小,再加上链式法则造成的梯度累乘,使得上层网络的梯度相较于底层网络大,在考虑学习率都相同时,顶层网络训练较快,底层网络训练较慢
但,由于底部变化之后,下一次正向计算时,当网络层数较多,较深时,底部网络对顶部网络的影响逐步累积,导致每一次训练完,下一次训练时顶层虽然训练较快,但由于底部网络的微小变化,导致顶层网络在每一次训练中拿到的数据和上一次的差异较大,相当于每一次都在重新学习各个特征,需要训练多次才能达到比较好的收敛效果,这样就导致整个网络的收敛变慢,学习率也不好调节,因为当学习率较大时,顶层网络训练过快无法收敛,学习率较小时底层网络出现梯度消失
因此,我们考虑能不能在底部学习训练时,尽量的去减小顶层的不断的重新学习训练,加快收敛速度

批量归一化解决问题的核心思想

内部协变量偏移(Internal Covariate Shift) :在训练时我们每次每层输入的数据由于经过了前面一层或多层的处理,分布差异(统计特性,比如均值、方差)较大,导致训练过程非常不稳定,尤其是当网络层数不断增加时,这种
效应不断加强
因此,批量归一化就考虑从分布差异入手解决问题,假设将每一层的输出都在整体上符合某一分布(主要用到的是正态分布),具体可以微调,那么在学习过程中的稳定性就能得到提高

批量归一化的具体实现

具体来说,批量归一化,就是将将训练中的每一个batch看作是一个批量,需要归一化的就是这一个小批量的均值和方差,用公式表达为:
均值: μ B = 1 ∣ B ∣ ∑ i ∈ B X i 方差: σ 2 = 1 ∣ B ∣ ∑ i ∈ B ( X i − μ B ) 2 + ϵ 均值:\mu_B = \frac{1}{|B|} \sum_{i \in B} X_i \\ 方差:\sigma^2 = \frac{1}{|B|} \sum_{i \in B}(X_i - \mu_B)^2 + \epsilon 均值:μB=B1iBXi方差:σ2=B1iB(XiμB)2+ϵ
微调(最终传入下一层进行计算):
x i + 1 = γ x i − μ B σ B + β x_{i+1} = \gamma\frac{x_i - \mu_B}{\sigma_B} + \beta xi+1=γσBxiμB+β
其中, γ \gamma γ β \beta β是需要学习的值,来逼近最优解

对于全连接层来说,输入是一个二维的tensor,分为批量维和特征维,那么,对全连接层进行批量归一化,我们所要求的均值就是特征维的均值,也就是将批量压缩为一,可以想象为将全连接层从上到下压扁,求和成一层,保留特征维度,求出均值,具体例子为:
假设我们有如下输入:
[[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],]
进行批量归一化的第一步就是求和,得到:
[[4, 8,12,16]]
然后再求均值:
[[1, 2, 3, 4]]
对于卷积层来说,一般用于处理图片,输入是一个四维的tensor,分为批量,通道,宽,高,那么进行批量归一化,同样将批量压为一,但与全连接层不同的是,宽和高也同样压缩为1求平均,也就是说只保留通道维的维数,其余维度降为1维,整体维数保持不变,可以看作是将图片中的所有像素作为样本,将通道看作是全连接层中的特征,具体例子为:
一开始是一个3*3*3*3的四维向量(3批次,3通道,宽3,高3)

Batch 1
+---------+   +---------+   +---------+
|Channel 1|   |Channel 2|   |Channel 3|
+---------+   +---------+   +---------+
| 1  1  1 |   | 2  2  2 |   | 3  3  3 |
| 1  1  1 |   | 2  2  2 |   | 3  3  3 |
| 1  1  1 |   | 2  2  2 |   | 3  3  3 |
+---------+   +---------+   +---------+

Batch 2
+---------+   +---------+   +---------+
|Channel 1|   |Channel 2|   |Channel 3|
+---------+   +---------+   +---------+
| 4  4  4 |   | 5  5  5 |   | 6  6  6 |
| 4  4  4 |   | 5  5  5 |   | 6  6  6 |
| 4  4  4 |   | 5  5  5 |   | 6  6  6 |
+---------+   +---------+   +---------+

Batch 3
+---------+   +---------+   +---------+
|Channel 1|   |Channel 2|   |Channel 3|
+---------+   +---------+   +---------+
| 7  7  7 |   | 8  8  8 |   | 9  9  9 |
| 7  7  7 |   | 8  8  8 |   | 9  9  9 |
| 7  7  7 |   | 8  8  8 |   | 9  9  9 |
+---------+   +---------+   +---------+

批量归一化求和,取平均后:

Batch 1
+- -+   +- -+   +- -+
|C 1|   |C 2|   |C 3|
+- -+   +- -+   +- -+
| 4 |   | 5 |   | 6 |
+- -+   +- -+   +- -+

变成一个1*3*1*1的四维向量

在沐神的视频中讲到,批量归一化最初是认为可以减少内部协变量转移(就像前面所说),但后续的论文发现可能批量归一化更多的是通过在每一个小批量中加入噪音来控制模型整体的复杂度,因此没必要和丢弃法混合使用

下面是批量归一化具体的代码实现

使用pytorch框架,
但不使用pytorch框架中的nn.BatchNorm2d,如果需要使用,只需要在net中改一下就行,
和视频中使用相同的LeNet和fashionmnist,但不使用d2l

import torch
from torch import nn
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader

def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    #这里我们添加了moving_mean和moving_var这两个变量来求出全部训练样本的整体mean和var
    #为的是在inference也就是之后的推理中使用
    if not torch.is_grad_enabled():
        X_hat = (X - moving_mean) / torch(moving_var + eps)
            #在推理中,就直接使用训练得到的moving_mean,moving_var
            #每次训练学习到的γ和β已经包含在其中
    else: 
        assert len(X.shape) in (2,4)
            #判断是否是全连接层或者是卷积层(这里只考虑这两种)
        if len(X.shape) == 2:#全连接层
            mean = X.mean(dim = 0)#dim = 0即前面提到的对批量求平均
            var = ((X - mean) ** 2).mean(dim = 0)#对批量求方差
        if len(X.shape) == 4:#卷积层
            mean = X.mean(dim = (0, 2, 3), keepdim=True)#只保留通道数
            var = ((X - mean) ** 2).mean(dim = (0, 2, 3), keepdim=True)
        X_hat = (X - mean) / torch.sqrt(var + eps)
        moving_mean = momentum * moving_mean + (1 - momentum) * mean#使用指数移动平均来逐渐逼近整体的平均和方差
        moving_var = momentum * moving_var + (1 - momentum) * var
    Y = gamma * X_hat + beta
    return Y, moving_mean.data, moving_var.data

class BatchNorm(nn.Module):#定义一个类来做初始化
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else: shape = (1, num_features, 1, 1)
        self.gamma = nn.Parameter(torch.ones(shape))#gamma初始化为1,肯定不能初始化为0
        self.beta = nn.Parameter(torch.zeros(shape))#beta初始化为0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)#初始整体样本化均值为0,方差为1

    def forward(self, 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)
        Y, self.moving_mean, self.moving_var = batch_norm(X, self.gamma, self.beta, self.moving_mean, self.moving_var,
                                                        eps=1e-5, momentum=0.9)
        return Y

net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5),
    BatchNorm(6, num_dims = 4),
    nn.Sigmoid(),
    nn.MaxPool2d(kernel_size=2, stride=2),

    nn.Conv2d(6, 16, kernel_size=5),
    BatchNorm(16, num_dims=4),
    nn.Sigmoid(),
    nn.MaxPool2d(kernel_size=2, stride=2),

    nn.Flatten(),
    nn.Linear(16 * 4 * 4, 120),
    BatchNorm(120, num_dims=2),
    nn.Sigmoid(),

    nn.Linear(120, 84),
    BatchNorm(84,num_dims=2),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)

#下载数据集,创建dataloader
lr, num_epochs, batch_size = 1.0, 10, 256
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(
    root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
    root="../data", train=False, transform=trans, download=True)
train_iter = DataLoader(mnist_train, batch_size, shuffle=True, num_workers=4)
test_iter = DataLoader(mnist_test, batch_size, shuffle=False, num_workers=4)

#开始训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
net.to(device)
for epoch in range(num_epochs):
    net.train() 
    train_loss_sum, train_acc_sum, n = 0.0, 0.0, 0
    for X, y in train_iter:
        X, y = X.to(device), y.to(device)
        y_hat = net(X)
        l = loss(y_hat, y)
        optimizer.zero_grad() # 梯度清零
        l.backward()          # 计算梯度
        optimizer.step()      # 更新参数
        train_loss_sum += l.cpu().item()
        train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
        n += y.shape[0]
    train_loss = train_loss_sum / n
    train_acc = train_acc_sum / n

    #测试
    net.eval()
    test_acc_sum, n_test = 0.0, 0
    with torch.no_grad(): # 在评估时,我们不需要计算梯度
        for X, y in test_iter:
            X, y = X.to(device), y.to(device)
            test_acc_sum += (net(X).argmax(dim=1) == y).sum().cpu().item()
            n_test += y.shape[0]
    
    test_acc = test_acc_sum / n_test
    
    print(f'Epoch {epoch + 1}/{num_epochs}, Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f}, Test Acc: {test_acc:.4f}')

最终的训练结果:

Epoch 1/10, Train Loss: 0.0011, Train Acc: 0.8976, Test Acc: 0.8319
Epoch 2/10, Train Loss: 0.0010, Train Acc: 0.9052, Test Acc: 0.7687
Epoch 3/10, Train Loss: 0.0010, Train Acc: 0.9096, Test Acc: 0.8097
Epoch 4/10, Train Loss: 0.0009, Train Acc: 0.9137, Test Acc: 0.8688
Epoch 5/10, Train Loss: 0.0009, Train Acc: 0.9154, Test Acc: 0.8454
Epoch 6/10, Train Loss: 0.0009, Train Acc: 0.9166, Test Acc: 0.8377
Epoch 7/10, Train Loss: 0.0008, Train Acc: 0.9203, Test Acc: 0.8365
Epoch 8/10, Train Loss: 0.0008, Train Acc: 0.9234, Test Acc: 0.7488
Epoch 9/10, Train Loss: 0.0008, Train Acc: 0.9234, Test Acc: 0.8821
Epoch 10/10, Train Loss: 0.0008, Train Acc: 0.9264, Test Acc: 0.8768

和不使用批量归一化相比较准确率差别不大,但速度提升了好几倍

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值