pytorch之批量歸一化和殘差網(wǎng)絡(luò)

5.10批量歸一化

本節(jié)我們介紹批量歸一化(batch normalization)層,它使較深的神經(jīng)網(wǎng)絡(luò)的訓(xùn)練變得更加容易[1]。我們對(duì)輸入數(shù)據(jù)做了標(biāo)準(zhǔn)化處理:處理后的任意一個(gè)特征在數(shù)據(jù)集中所有樣本上的均值0,標(biāo)準(zhǔn)差為1。標(biāo)準(zhǔn)化處理輸入數(shù)據(jù)使各個(gè)特征的分布相近:這往往更容易訓(xùn)練出有效的模型。

通常來說,數(shù)據(jù)標(biāo)準(zhǔn)化對(duì)于淺層模型就足夠有效了。進(jìn)行模型訓(xùn)練的進(jìn)行,當(dāng)每層中參數(shù)更新時(shí),靠近輸出層的輸出較難出現(xiàn)梯度變化。但對(duì)深層神經(jīng)網(wǎng)絡(luò)來說,,即使輸入數(shù)據(jù)已完成規(guī)范,訓(xùn)練中模型參數(shù)的更新仍然很容易造成靠近輸出層輸出的高度變化。這種計(jì)算數(shù)值的不穩(wěn)定性通常令我們難以訓(xùn)練出有效的深度模型。

在模型訓(xùn)練時(shí),批量歸一化利用小批量上的均值和標(biāo)準(zhǔn)差,不斷調(diào)整神經(jīng)網(wǎng)絡(luò)中間輸出,從而使整個(gè)神經(jīng)網(wǎng)絡(luò)在各層的中間輸出的數(shù)值更穩(wěn)定。批量歸一化和下分段將要介紹的殘差網(wǎng)絡(luò)為訓(xùn)練和設(shè)計(jì)深度模型提供了兩類重要的思路

5.10.1批量歸一化層

對(duì)全連接層和卷積層做批量歸一化的方法稍有不同。下面我們將分別介紹這兩種情況下的批量歸一化。

5.10.1.1對(duì)全連接層做批量歸一化

5.10.1.3預(yù)測時(shí)的批量歸一化

使用批量歸一化訓(xùn)練時(shí),我們可以將批量大小設(shè)得大一點(diǎn),從而使批量內(nèi)樣本的均值和方差的計(jì)算都正確地對(duì)齊。將訓(xùn)練好的模型用于預(yù)測時(shí),我們希望模型對(duì)于任意輸入都有確定的輸出。因此,零散的樣本的輸出范圍應(yīng)至少部分歸零一化所需要的隨機(jī)小批量中的均值和方差。一種常用的方法是通過移動(dòng)平均采樣整個(gè)訓(xùn)練數(shù)據(jù)集的樣本均值和方差,并在預(yù)測時(shí)使用。它們的體積相同,批量歸一化層在訓(xùn)練模式和預(yù)測模式下的計(jì)算結(jié)果也是不一樣的。

5.10.2從零開始實(shí)現(xiàn)

下面我們自己實(shí)現(xiàn)批量歸一化層。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 判斷當(dāng)前模式是訓(xùn)練模式還是預(yù)測模式
    if not is_training:
        # 如果是在預(yù)測模式下,直接使用傳入的移動(dòng)平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全連接層的情況,計(jì)算特征維上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二維卷積層的情況,計(jì)算通道維上(axis=1)的均值和方差。這里我們需要保持
            # X的形狀以便后面可以做廣播運(yùn)算
            mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
        # 訓(xùn)練模式下用當(dāng)前的均值和方差做標(biāo)準(zhǔn)化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        # 更新移動(dòng)平均的均值和方差
        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

接下來,我們自定義一個(gè)BatchNorm層。它保存參與求梯度和迭代的拉伸參數(shù)gamma和轉(zhuǎn)換參數(shù)beta,同時(shí)也維護(hù)移動(dòng)平均得到的均值和方差,剎車能夠在模型預(yù)測時(shí)被使用。BatchNorm實(shí)例所需指定的num_features該實(shí)例所需指定的num_dims參數(shù)對(duì)于全連接層和卷積層來說分別為2和4。

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)
        # 參與求梯度和迭代的拉伸和偏移參數(shù),分別初始化成0和1
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 不參與求梯度和迭代的變量,全在內(nèi)存上初始化成0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.zeros(shape)

    def forward(self, X):
        # 如果X不在內(nèi)存上,將moving_mean和moving_var復(fù)制到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, Module實(shí)例的traning屬性默認(rèn)為true, 調(diào)用.eval()后設(shè)成false
        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

5.10.2.1使用批量歸一化層的LeNet

下面我們修改5.5節(jié)(卷積神經(jīng)網(wǎng)絡(luò)(LeNet))介紹的LeNet模型,從而應(yīng)用批量歸一化層。我們?cè)谒械木矸e層或全連接層之后,激活層之前加入批量歸一化層。

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

下面我們訓(xùn)練修改后的模型。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

輸出:

training on  cuda
epoch 1, loss 0.0039, train acc 0.790, test acc 0.835, time 2.9 sec
epoch 2, loss 0.0018, train acc 0.866, test acc 0.821, time 3.2 sec
epoch 3, loss 0.0014, train acc 0.879, test acc 0.857, time 2.6 sec
epoch 4, loss 0.0013, train acc 0.886, test acc 0.820, time 2.7 sec
epoch 5, loss 0.0012, train acc 0.891, test acc 0.859, time 2.8 sec

最后我們查看第一個(gè)批量歸一化層學(xué)習(xí)到的拉伸參數(shù)gamma和轉(zhuǎn)換參數(shù)beta

net[1].gamma.view((-1,)), net[1].beta.view((-1,))

輸出:

(tensor([ 1.2537,  1.2284,  1.0100,  1.0171,  0.9809,  1.1870], device='cuda:0'),
 tensor([ 0.0962,  0.3299, -0.5506,  0.1522, -0.1556,  0.2240], device='cuda:0'))

5.10.3簡潔實(shí)現(xiàn)

與我們剛剛自己定義的BatchNorm類比例,Pytorch中nn模塊定義的BatchNorm1dBatchNorm2d類使用起來更加簡單,同時(shí)分別為全連接層和卷積層,都需要指定輸入的num_features參數(shù)值。下面我們用PyTorch實(shí)現(xiàn)使用規(guī)模歸一化的LeNet。

net = nn.Sequential(
            nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
            nn.BatchNorm2d(6),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2), # kernel_size, stride
            nn.Conv2d(6, 16, 5),
            nn.BatchNorm2d(16),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),
            d2l.FlattenLayer(),
            nn.Linear(16*4*4, 120),
            nn.BatchNorm1d(120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.BatchNorm1d(84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )

使用同樣的超參數(shù)進(jìn)行訓(xùn)練。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

輸出:

training on  cuda
epoch 1, loss 0.0054, train acc 0.767, test acc 0.795, time 2.0 sec
epoch 2, loss 0.0024, train acc 0.851, test acc 0.748, time 2.0 sec
epoch 3, loss 0.0017, train acc 0.872, test acc 0.814, time 2.2 sec
epoch 4, loss 0.0014, train acc 0.883, test acc 0.818, time 2.1 sec
epoch 5, loss 0.0013, train acc 0.889, test acc 0.734, time 1.8 sec

小結(jié)

  • 在模型訓(xùn)練時(shí),批量歸一化利用小批量上的均值和標(biāo)準(zhǔn)差,不斷調(diào)整神經(jīng)網(wǎng)絡(luò)的中間輸出,從而使整個(gè)神經(jīng)網(wǎng)絡(luò)在各層的中間輸出的數(shù)值更穩(wěn)定。
  • 對(duì)全連接層和卷積層做批量歸一化的方法稍有不同。
  • 批量歸一化層和替代層一樣,在訓(xùn)練模式和預(yù)測模式的計(jì)算結(jié)果是不一樣的。
  • PyTorch提供了BatchNorm類方便使用。

5.11殘差網(wǎng)絡(luò)(ResNet)

讓我們先思考一個(gè)問題:對(duì)神經(jīng)網(wǎng)絡(luò)模型添加新的層,充分訓(xùn)練后的模型是否只可能更有效地降低訓(xùn)練誤差?理論上,原模型解的空間只是新模型解的空間的子空間。也就是說,如果我們能將新添加的層訓(xùn)練成恒等映射f (x )=x,新模型和原模型將同樣有效。由于新模型可能會(huì)帶來更優(yōu)的解來擬合訓(xùn)練數(shù)據(jù)集,因此添加層似乎更容易降低訓(xùn)練誤差。而在實(shí)踐中,添加過多的層后訓(xùn)練誤差往往不降反升。甚至利用批量歸一化帶來的數(shù)值穩(wěn)定性使訓(xùn)練深層模型更加容易,該問題仍然存在。針對(duì)這一問題,何愷明等人提出了殘差網(wǎng)絡(luò)(ResNet)[1] 。它在2015年的ImageNet圖像識(shí)別挑戰(zhàn)賽奪魁,并深刻影響了后來的深度神經(jīng)網(wǎng)絡(luò)的設(shè)計(jì)。

5.11.2殘差塊

殘差映射在實(shí)際中經(jīng)常更容易優(yōu)化。以本節(jié)開頭提到的恒等映射作為我們希望學(xué)出的理想映射我們只需將圖5.9中右圖虛線框內(nèi)上方的重組運(yùn)算(如仿射)的權(quán)重和偏差參數(shù)學(xué)成0,那么f (x)即為恒等映射。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class Residual(nn.Module):  # 本類已保存在d2lzh_pytorch包中方便以后使用
    def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
        super(Residual, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_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)
        return F.relu(Y + X)

下面我們來查看輸入和輸出形狀一致的情況。

blk = Residual(3, 3)
X = torch.rand((4, 3, 6, 6))
blk(X).shape # torch.Size([4, 3, 6, 6])

我們也可以在增加輸出通道數(shù)的同時(shí)減半輸出的高和寬。

blk = Residual(3, 6, use_1x1conv=True, stride=2)
blk(X).shape # torch.Size([4, 6, 3, 3])

5.11.2 ResNet模型

ResNet的前兩層跟之前介紹的GoogLeNet中的一樣:在輸出通道數(shù)為64,步幅為2的7×7卷積層后接步幅為2的3×3的最大池化層。不同之處在于ResNet每個(gè)卷積層后增加的批量歸一化層。

net = 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))

ResNet則使用4個(gè)由殘差塊組成的模塊,每個(gè)模塊使用多個(gè)相同輸出通道數(shù)的殘差塊。第一個(gè)模塊的通道數(shù)同輸入通道數(shù)一致。由于之前已經(jīng)使用了步幅為2的最大池化層,所以無須對(duì)準(zhǔn)高和寬。之后的每個(gè)模塊在第一個(gè)殘差塊里將上一個(gè)模塊的通道數(shù)翻倍,成為高和寬減半。

注意,此處對(duì)第一個(gè)模塊做了特別處理。

def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
    if first_block:
        assert in_channels == out_channels # 第一個(gè)模塊的通道數(shù)同輸入通道數(shù)一致
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
        else:
            blk.append(Residual(out_channels, out_channels))
    return nn.Sequential(*blk)

接著我們?yōu)镽esNet加入所有殘差塊。這里每個(gè)模塊使用兩個(gè)殘差塊。

net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))

最后,與GoogLeNet一樣,加入平均池化層后接上全連接層輸出。

net.add_module("global_avg_pool", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的輸出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10))) 

1個(gè)×1卷積層),加上最開始的卷積層和最后的全連接層,共計(jì)18層。這個(gè)模型通常也被稱為ResNet-18。通過配置不同的通道數(shù)和模塊里的殘差塊數(shù)可以得到不同的ResNet模型,例如更深的含152層的ResNet-152。雖然ResNet的主體架構(gòu)跟GoogLeNet的類似,但ResNet結(jié)構(gòu)更簡單,修改也更方便。

在訓(xùn)練ResNet之前,我們來觀察一下輸入形狀在ResNet不同模塊之間的變化。

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

輸出:

0  output shape:     torch.Size([1, 64, 112, 112])
1  output shape:     torch.Size([1, 64, 112, 112])
2  output shape:     torch.Size([1, 64, 112, 112])
3  output shape:     torch.Size([1, 64, 56, 56])
resnet_block1  output shape:     torch.Size([1, 64, 56, 56])
resnet_block2  output shape:     torch.Size([1, 128, 28, 28])
resnet_block3  output shape:     torch.Size([1, 256, 14, 14])
resnet_block4  output shape:     torch.Size([1, 512, 7, 7])
global_avg_pool  output shape:     torch.Size([1, 512, 1, 1])
fc  output shape:     torch.Size([1, 10])

5.11.3獲取數(shù)據(jù)和訓(xùn)練模型

下面我們?cè)贔ashion-MNIST數(shù)據(jù)集上訓(xùn)練ResNet。

batch_size = 256
# 如出現(xiàn)“out of memory”的報(bào)錯(cuò)信息,可減小batch_size或resize
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

輸出:

training on  cuda
epoch 1, loss 0.0015, train acc 0.853, test acc 0.885, time 31.0 sec
epoch 2, loss 0.0010, train acc 0.910, test acc 0.899, time 31.8 sec
epoch 3, loss 0.0008, train acc 0.926, test acc 0.911, time 31.6 sec
epoch 4, loss 0.0007, train acc 0.936, test acc 0.916, time 31.8 sec
epoch 5, loss 0.0006, train acc 0.944, test acc 0.926, time 31.5 sec

小結(jié)

  • 殘差塊通過跨層的數(shù)據(jù)通道從而能夠訓(xùn)練出有效的深度神經(jīng)網(wǎng)絡(luò)。
  • ResNet深刻影響了后來的深度神經(jīng)網(wǎng)絡(luò)的設(shè)計(jì)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容