跳转至

7.5 批量规范化

训练深层神经网络是十分困难的,特别是在较短的时间内使他们收敛更加棘手。批量规范化 可持续加速深层网络的收敛速度

1 原理

在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。接下来,我们应用比例系数和比例偏移。正是由于这个基于批量统计的标准化,才有了批量规范化的名称

\(x \in B\) 表示一个来自小批量 \(B\) 的输入,批量规范化 \(BN\) 根据以下表达式转换 \(x\)

\(BN(x) = \gamma \odot \dfrac{x - \hat{\mu_B}}{\hat{\sigma_B}} + \beta\)

\(\hat{\mu_B}\) 是小批量 \(B\) 的样本均值,\(\hat{\sigma_B}\) 是小批量 \(B\) 的样本标准差。应用标准化后,生成的小批量的平均值为 0 和单位方差为 1。由于单位方差是一个主观的选择,因此我们通常包含 拉伸参数 \(\gamma\)偏移参数 \(\beta\),它们的形状与 \(x\) 相同

\(\gamma\)\(\beta\) 需要与其他模型参数一起学习的参数

\(\hat{\mu_B} = \dfrac{1}{|B|}\sum\limits_{x \in B} x\)

\(\hat{\sigma_B}^2 = \dfrac{1}{|B|}\sum\limits_{x \in B}(x - \hat{\mu_B})^2 + \epsilon\)

我们在方差估计值中添加一个小的常量 \(\epsilon > 0\),以确保我们永远不会尝试除以零

批量规范化层在”训练模式“(通过小批量统计数据规范化)和“预测模式”(通过数据集统计规范化)中的功能不同。在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型。而在预测模式下,可以根据整个数据集精确计算批量规范化所需的平均值和方差

2 批量规范化层

2.1 全连接层

通常,我们将批量规范化层置于全连接层中的仿射变换和激活函数之间

\(h = \phi(BN(Wx + b))\)

2.2 卷积层

对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。当卷积有多个输出通道时,我们需要对这些通道的“每个”输出执行批量规范化,每个通道都有自己的拉伸(scale)和偏移(shift)参数,这两个参数都是标量

假设我们的小批量包含 m 个样本,并且对于每个通道,卷积的输出具有高度 p 和宽度 q 。那么对于卷积层,我们在每个输出通道的 m·p·q 个元素上同时执行每个批量规范化

2.3 预测模式

首先,将训练好的模型用于预测时,我们不再需要样本均值中的噪声以及在微批次上估计每个小批次产生的样本方差了。其次,例如,我们可能需要使用我们的模型对逐个样本进行预测

一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出

3 从零实现

import torch
from torch import nn
from d2l import torch as d2l


def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 通过 is_grad_enabled 来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
            # 这里我们需要保持X的形状以便后面可以做广播运算
            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.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - 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):
        """
        :param num_features: 完全连接层的输出数量或卷积层的输出通道数
        :param num_dims: 2 表示完全连接层,4 表示卷积层
        """
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成 1 和 0
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为 0 和 1
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(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)
        # 保存更新过的 moving_mean 和 moving_var
        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

3.1 应用到 LeNet 模型

net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5),
    BatchNorm(6, num_dims=4),
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5),
    BatchNorm(16, num_dims=4),
    nn.Sigmoid(),
    nn.AvgPool2d(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)
)

lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

4 简明实现

net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), 
    nn.BatchNorm2d(6), 
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), 
    nn.BatchNorm2d(16), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), 
    nn.Flatten(),
    nn.Linear(256, 120), 
    nn.BatchNorm1d(120), 
    nn.Sigmoid(),
    nn.Linear(120, 84), 
    nn.BatchNorm1d(84), 
    nn.Sigmoid(),
    nn.Linear(84, 10)
)

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

评论区

欢迎在评论区指出文档错误,为文档提供宝贵意见,或写下你的疑问