跟李沐学AI随记-11-批量归一化&残差网络ResNet

29 阅读6分钟

批量规范化(batch normalization)

  • 用于加快深层神经网络的收敛速度。

训练神经网络时的挑战

  • 预处理方式会对结果产生巨大影响
    • 使用真实数据时,需标准化输入特征,使其均值为0方差为1,进行标准化后可以更好的与优化器配合使用(参数的量级统一)
  • 更深层的网络更复杂,更容易过拟合,正则化非常重要

数据在最底部,损失出现在最后,所以后面的层训练较快,底部的层训练较慢,况且底部层一变化,所有都会变,导致后面的层需要进行多次学习,导致收敛变慢。

  • 正向传递:数据从下至上逐层传递
  • 反向传递:梯度在上层比较大,在下层比较容易小(n个比较小的数相乘,越到底层越小)
  • 翻译一下就是,上面的梯度大,更新速度较快,下面梯度小,权重更新速度相对较慢,收敛慢。而底层是靠近输入数据的(用于提取局部边缘,纹理信息等),其变化慢。而顶层的(高层语义信息)变化快,所以导致每次底层变化都会让顶层所有之前学的权重全部白学了,导致模型整体收敛速度慢。
  • 所以需要想办法在学习底部层时避免变化顶部层

原理如下:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。 接下来,我们应用比例系数和比例偏移。

  • 假设每一层的输出、梯度等,均服从某一分布,这时整个模型整体相对来说是比较稳定的

image.png

作用在

  • 全连接层和卷积层输出上,激活函数前
  • 或者在全连接层和卷积层输入上。

对全连接层:作用在特征维(对每一个特征,进行标量的均值和标量的方差求取)

  • 对每一个全连接层的输入和输出都进行批量归一化,而不是只在数据上进行作用

对卷积层:作用在通道维(针对每个像素,有多个通道。把每一个像素层看做一个信号,那么一个像素的所有通道就是该信号的特征)

  • 假设小批量包含m个样本,并且对于每个通道,卷积的输出有高度p和宽度q,那么对于卷积层,在每个输出通道的m×p×q个元素,视作一个样本,再执行批量归一化。
  • 注意:在计算均值和方差时,会收集所有空间位置的值
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:
        # 如果是2,就是全连接层,4就是卷积层
        # 全连接层:batch_size+特征
        # 卷积层:batch_size+通道数+高+宽
        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的形状以便后面可以做广播运算
            # 得到一个1*n*1*1的
            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):
    # num_features:完全连接层的输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self, num_features, num_dims):
        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

"""将batchnorm应用于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))
    

# 简洁实现
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))

残差网络ResNet

对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function)f(X)=X,新模型和原模型将同样有效。 同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。

image.png

残差块:

  • 至少不会使模型变差,即防止网络退化 image.png 代码实现:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Residual(nn.Module):  
    def __init__(self, input_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels,
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels,
                               kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)
        
# 输入和输出同shape时       
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape)
# 输出通道数增加时,我们习惯性的会将其高和宽减半
blk = Residual(3,6, use_1x1conv=True, strides=2)
print(blk(X).shape)

此处,1×1卷积用于调整通道数和分辨率 image.png

构造ResNet模型:

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.BatchNorm2d(64), nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

def resnet_block(input_channels, num_channels, num_residuals,
                 first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))


X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

ResNet的梯度计算

嵌套块后,能保证梯度不会过小导致底层w更新较慢。

9563bbc2282f2963226c461beaa8c68.jpg

小结:

  • 学习嵌套函数(nested function)是训练神经网络的理想情况。在深层神经网络中,学习另一层作为恒等映射(identity function)较容易(尽管这是一个极端情况)。
  • 残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
  • 利用残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。