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ù)測(cè)時(shí)的批量歸一化
使用批量歸一化訓(xùn)練時(shí)作儿,我們可以將批量大小設(shè)得大一點(diǎn)洛二,從而使批量?jī)?nèi)樣本的均值和方差的計(jì)算都正確地對(duì)齊。將訓(xùn)練好的模型用于預(yù)測(cè)時(shí)攻锰,我們希望模型對(duì)于任意輸入都有確定的輸出晾嘶。因此,零散的樣本的輸出范圍應(yīng)至少部分歸零一化所需要的隨機(jī)小批量中的均值和方差娶吞。一種常用的方法是通過移動(dòng)平均采樣整個(gè)訓(xùn)練數(shù)據(jù)集的樣本均值和方差垒迂,并在預(yù)測(cè)時(shí)使用。它們的體積相同妒蛇,批量歸一化層在訓(xùn)練模式和預(yù)測(cè)模式下的計(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ù)測(cè)模式
if not is_training:
# 如果是在預(yù)測(cè)模式下绣夺,直接使用傳入的移動(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ù)測(cè)時(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簡(jiǎn)潔實(shí)現(xiàn)
與我們剛剛自己定義的BatchNorm
類比例蔼夜,Pytorch中nn
模塊定義的BatchNorm1d
和BatchNorm2d
類使用起來更加簡(jiǎn)單,同時(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ù)測(cè)模式的計(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)更簡(jiǎn)單宜雀,修改也更方便切平。
在訓(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ì)苔严。