本文是基于《Pytorch深度学习实战》一书第八章的内容所整理的学习笔记
相关代码的解释以及对应的拓展。
本文使用的代码均基于jupyter
卷积,离散卷积,被定义为二维图像的权重矩阵的标量积,即该函数与输入中的每个邻域的标量积。
卷积的操作:我们先从一个小小的权重矩阵,也就是卷积核(kernel)开始,让它逐步在二维输入数据上“扫描”。卷积核“滑动”的同时,计算权重矩阵和扫描所得的数据矩阵的乘积,然后把结果汇总成一个输出像素。
卷积核会在其经过的所有位置上都重复以上操作,直到把输入特征矩阵转换为另一个二维的特征矩阵。
%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
import collections
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.set_printoptions(edgeitems=2)
torch.manual_seed(123)
class_names = ['airplane','automobile','bird','cat','deer',
'dog','frog','horse','ship','truck']
from torchvision import datasets, transforms
data_path = 'data/p1ch7/'
cifar10 = datasets.CIFAR10(
data_path, train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
cifar10_val = datasets.CIFAR10(
data_path, train=False, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4915, 0.4823, 0.4468),
(0.2470, 0.2435, 0.2616))
]))
label_map = {0: 0, 2: 1}
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label])
for img, label in cifar10
if label in [0, 2]]
cifar2_val = [(img, label_map[label])
for img, label in cifar10_val
if label in [0, 2]]
conv = nn.Conv2d(3, 16, kernel_size=3)
conv
conv.weight.shape, conv.bias.shape
一个二维卷积核产生一个二维图像并将其作为输出,它的像素是输入图像邻域的加权和。
nn.Conv2d()期望输入一个BxCxHxW的张量
img, _ = cifar2[0]
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape
显示输入与输出
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()
当我们把输入的特征矩阵转换成了输出的特征矩阵,输入图像的边缘被“修剪”掉了,这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。这是不理想的,通常我们都希望输入和输出的大小应该保持一致。
Padding就是针对这个问题提出的一个解决方案:它会用额外的“假”像素填充边缘(值一般为0),这样,当卷积核扫描输入数据时,它能延伸到边缘以外的伪像素,从而使输出和输入大小相同。
conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape
无论是否使用填充,权重和偏置的大小都不会改变
手动设置权重来处理卷积
with torch.no_grad():
conv.bias.zero_()
with torch.no_grad():
conv.weight.fill_(1.0 / 9.0)
output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()
conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)
with torch.no_grad():
conv.weight[:] = torch.tensor([[-1.0, 0.0, 1.0],
[-1.0, 0.0, 1.0],
[-1.0, 0.0, 1.0]])
conv.bias.zero_()
output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()
将图像缩放一半相当于取4个相邻像素作为输入,产生1个像素作为输出
可选择的操作:
最大池化由`nn.MaxPool2d模块提供
pool = nn.MaxPool2d(2)
output = pool(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape
可以理解为开始的卷积核对一阶、第几特征的小邻域进行操作,而第二组卷积核则有效地对更宽的邻域进行操作,生成由先前特征组成的特征。
池化可以理解为提取卷积产生的特征中最关键的部分进行训练,往往进行几次卷积就要进行一次池化操作。
model = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, padding=1),
nn.Tanh(),
nn.MaxPool2d(2),
nn.Conv2d(16, 8, kernel_size=3, padding=1),
nn.Tanh(),
nn.MaxPool2d(2),
# ... 还需要补充的地方
nn.Linear(8 * 8 * 8, 32),
nn.Tanh(),
nn.Linear(32, 2))
第1个卷积将我们从2个RGB通道带到16个RGB通道,因此给网络一个机会来生成16个独立的特征,以(希望)区分鸟和飞机的低级特征。然后应用Tanh活化函数。得到的有 16个通道的、32x32的图像被第1个MaxPool2d池化成有16个通道的、16×16的图像。在这一点上,下采样图像进行另一个卷积,产生一个有8个通道的、16×16的输出。如果幸运的话,这个输出将包含更高级的特性。同样,我们应用 Tanh 激活函数,然后将其池化到有8个通道的8x8的输出
计算一下这个小模型的参数数目
numel_list = [p.numel() for p in model.parameters()]
sum(numel_list), numel_list
运行一下这个模型
model(img.unsqueeze(0))
编译器将会报错,我们可以发现,这里还缺少从有8个通道的、8x8的图像转换为有512个元素的一维向量的步骤(忽律批处理的纬度)。
convnet的核心是2个嵌套的循环:
在每个循环中,我们都需要:
import datetime
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
for epoch in range(1, n_epochs + 1): # 循环变了从1开始到n_epochs
loss_train = 0.0
for imgs, labels in train_loader: # 在数据加载器为我们船舰的批中循环数据集
outputs = model(imgs) # 通过我们的模型提供一个批次
loss = loss_fn(outputs, labels) # 计算我们希望最小化的损失
optimizer.zero_grad() # 梯度归零
loss.backward() # 计算我们希望网络学习的参数的梯度
optimizer.step() # 更新模型
loss_train += loss.item() # 对整个训话遍历中得到的损失求和
if epoch == 1 or epoch % 10 == 0:
print('{} Epoch {}, Training loss {}'.format(
datetime.datetime.now(), epoch,
loss_train / len(train_loader))) # 除以训练数据加载器的长度,得到每批平均损失
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=True) # 数据加载器批量处理cifar2的样本数据集,并随机打乱数据集中样本的顺序
model = Net() # 实例化网络
optimizer = optim.SGD(model.parameters(), lr=1e-2) # 设置优化器
loss_fn = nn.CrossEntropyLoss() # 设置损失函数
training_loop( # 调用定义的训练循环
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
比较模型在训练集和验证集上的精确度
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=False)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
shuffle=False)
def validate(model, train_loader, val_loader):
for name, loader in [("train", train_loader), ("val", val_loader)]:
correct = 0
total = 0
with torch.no_grad(): # 不需要梯度,因为在验证集上不希望更新参数
for imgs, labels in loader:
outputs = model(imgs)
_, predicted = torch.max(outputs, dim=1) # 将最大值的索引作为输出,_为占位符
total += labels.shape[0] # 计算样本的数量,因为total会随着批处理的大小而增加
correct += int((predicted == labels).sum()) # 比较具有最大概率的预测类和真实值标签,我们首先得到一个bool数组,统计这个批次中预测值和实际值一致的项的总数
print("Accuracy {}: {:.2f}".format(name , correct / total))
validate(model, train_loader, val_loader)
torch.save(model.state_dict(), data_path + 'birds_vs_airplanes.pt')
loaded_model = Net() # 我们必须确保在报错模型状态和稍后加载模型状态期间不会改变Net的定义
loaded_model.load_state_dict(torch.load(data_path
+ 'birds_vs_airplanes.pt'))
device = (torch.device('cuda') if torch.cuda.is_available()
else torch.device('cpu'))
print(f"Training on device {device}.")
import datetime
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
for epoch in range(1, n_epochs + 1):
loss_train = 0.0
for imgs, labels in train_loader:
imgs = imgs.to(device=device) # 将imgs和labels移动到我们正在训练的设备上
labels = labels.to(device=device)
outputs = model(imgs)
loss = loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_train += loss.item()
if epoch == 1 or epoch % 10 == 0:
print('{} Epoch {}, Training loss {}'.format(
datetime.datetime.now(), epoch,
loss_train / len(train_loader)))
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=True)
model = Net().to(device=device) # 将我们的模型(所有参数)移动到GPU。如果你忘记将模型或输入移动到GPU,你会得到张量不在同一设备上的错误
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop(
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
shuffle=False)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
shuffle=False)
all_acc_dict = collections.OrderedDict()
def validate(model, train_loader, val_loader):
accdict = {}
for name, loader in [("train", train_loader), ("val", val_loader)]:
correct = 0
total = 0
with torch.no_grad():
for imgs, labels in loader:
imgs = imgs.to(device=device)
labels = labels.to(device=device)
outputs = model(imgs)
_, predicted = torch.max(outputs, dim=1) # <1>
total += labels.shape[0]
correct += int((predicted == labels).sum())
print("Accuracy {}: {:.2f}".format(name , correct / total))
accdict[name] = correct / total
return accdict
all_acc_dict["baseline"] = validate(model, train_loader, val_loader)
loaded_model = Net().to(device=device)
loaded_model.load_state_dict(torch.load(data_path
+ 'birds_vs_airplanes.pt',
map_location=device))
在第1个卷积中指定更多的输出通道
class NetWidth(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 16, kernel_size=3, padding=1)
self.fc1 = nn.Linear(16 * 8 * 8, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
out = out.view(-1, 16 * 8 * 8)
out = torch.tanh(self.fc1(out))
out = self.fc2(out)
return out
通过传参指定宽度
class NetWidth(nn.Module):
def __init__(self, n_chans1=32):
super().__init__()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
padding=1)
self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
out = torch.tanh(self.fc1(out))
out = self.fc2(out)
return out
model = NetWidth(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop(
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
all_acc_dict["width"] = validate(model, train_loader, val_loader)
查看现在模型参数的数量
sum(p.numel() for p in model.parameters())
帮助我们的模型收敛和泛化
训练模型的2个关键步骤
稳定泛化的一种方法是在损失中添加一个正则化项。
这个术语的设计是为了减小模型本身的权重,从而限制训练对它们增长的影响。换向话说,这是对较大权重的惩罚。这使得损失更平滑,并且从拟合单个样本中获得的收益相对较少。
- L1正则化:模型中所有权重的绝对值之和
- L2正则化:模型中所有权重的平方和
- 它们都通过一个因子进行缩放,即超参数
L2 正则化也称为权重衰减。叫这个名字的原因是考虑到 SGD 和反向传播,L2正则化对参数w_i的负梯度为-2lambdaw_i,其中 lambda 是前面提到的超参数,在PyTorch中简称为权重衰减。因此,在损失函数中加人 L2 正则化,相当于在优化步骤中将每个权重按其当前值的比例递减(因此称为权重衰减)。注意,权重哀减适用于网络的所有参数,例如偏置。
def training_loop_l2reg(n_epochs, optimizer, model, loss_fn,
train_loader):
for epoch in range(1, n_epochs + 1):
loss_train = 0.0
for imgs, labels in train_loader:
imgs = imgs.to(device=device)
labels = labels.to(device=device)
outputs = model(imgs)
loss = loss_fn(outputs, labels)
l2_lambda = 0.001
l2_norm = sum(p.pow(2.0).sum()
for p in model.parameters())
loss = loss + l2_lambda * l2_norm
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_train += loss.item()
if epoch == 1 or epoch % 10 == 0:
print('{} Epoch {}, Training loss {}'.format(
datetime.datetime.now(), epoch,
loss_train / len(train_loader)))
model = Net().to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop_l2reg(
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
all_acc_dict["l2 reg"] = validate(model, train_loader, val_loader)
思想:将网络每轮训练迭代中的神经元随机部分清零
Dropout 在每次迭代中有效地生成具有不同神经元拓扑的模型,使得模型中的神经元在过拟合过程中协调记忆过程的机会更少。另一种观点是,Dropout 在整个网络中干扰了模型生成的特征,产生了一种接近于增强的效果。
在 PyTorch 中,我们可以通过在非线性激活与后面的线性或卷积模块之间添加一个
加nn.Dropout 模块在模型中实现 Dropout。作为一个参数,我们需要指定输入归零的概率。如果是卷积,我们将使用专门的 nn.Dropout2d 或者nn.Dropout3d,将输人的所有通道归零:
class NetDropout(nn.Module):
def __init__(self, n_chans1=32):
super().__init__()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.conv1_dropout = nn.Dropout2d(p=0.4)
self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
padding=1)
self.conv2_dropout = nn.Dropout2d(p=0.4)
self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
out = self.conv1_dropout(out)
out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
out = self.conv2_dropout(out)
out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
out = torch.tanh(self.fc1(out))
out = self.fc2(out)
return out
model = NetDropout(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop(
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
all_acc_dict["dropout"] = validate(model, train_loader, val_loader)
批量归一化背后的主要思想是将输入重新调整到网络的激活状态,从而使小批量具有一定的理想分布。回想一下学习机制和非线性激活函数的作用,这有助于避免激活函数的输入过多地进入函数的饱和部分,从而消除梯度并减慢训练速度。
实际上,批量归一化使用在该中间位置收集的小批量样本的平均值和标准差来对中间输入进行移位和缩放。正则化效应是这样一个事实的结果,即单个样本及其下游激活函数总是被模型视为平移和缩放,这取决于随机提取的小批量的统计数据。
PyTorch 提供了 nn.BatchNormld、nn.BatchNorm2d 和 nn.BatchNorm3d 来实现批量归一化,使用哪种模块取决于输人的维度。由于批量归一化的目的是重新调整激活的输入,因此其位置是在线性变换和激活函数之后
class NetBatchNorm(nn.Module):
def __init__(self, n_chans1=32):
super().__init__()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.conv1_batchnorm = nn.BatchNorm2d(num_features=n_chans1)
self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
padding=1)
self.conv2_batchnorm = nn.BatchNorm2d(num_features=n_chans1 // 2)
self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = self.conv1_batchnorm(self.conv1(x))
out = F.max_pool2d(torch.tanh(out), 2)
out = self.conv2_batchnorm(self.conv2(out))
out = F.max_pool2d(torch.tanh(out), 2)
out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
out = torch.tanh(self.fc1(out))
out = self.fc2(out)
return out
增加深度与增加网络在处理输入时能够执行的操作序列的长度有关
损失函数对参数的导数,特别是在早期层中的导数,需要乘许多其他数字,这些数字来自损失和参数之问的导数运算链。这些被相乘的数字可能很小,生成的数字越来越小,也可能很大,由于浮点近似而吞并了更小的数字。最重要的是,一长串乘法会使参数对梯度的贡献消失,导致该层的训练无效、因为该参数和其他类似参数不会得到适当的更新。
跳跃连接只是将输入添加到层块的一个输出中
class NetRes(nn.Module):
def __init__(self, n_chans1=32):
super().__init__()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
padding=1)
self.conv3 = nn.Conv2d(n_chans1 // 2, n_chans1 // 2,
kernel_size=3, padding=1)
self.fc1 = nn.Linear(4 * 4 * n_chans1 // 2, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
out = F.max_pool2d(torch.relu(self.conv2(out)), 2)
out1 = out
out = F.max_pool2d(torch.relu(self.conv3(out)) + out1, 2)
out = out.view(-1, 4 * 4 * self.n_chans1 // 2)
out = torch.relu(self.fc1(out))
out = self.fc2(out)
return out
model = NetRes(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop(
n_epochs = 100,
optimizer = optimizer,
model = model,
loss_fn = loss_fn,
train_loader = train_loader,
)
all_acc_dict["res"] = validate(model, train_loader, val_loader)
缓解梯度消失的原因:
考虑到反向传播,我们可以理解一个跳跃连接或者深层网络中的一系列跳跃连接
——其创建了一条从深层参数到损失的直接路径,这使得它们对损失梯度的贡献更直接,因为损失相对这些参数的偏导数有可能不会和其他操作的长链相乘。
通过定义构建块实现深层的网络架构
定义一个模块子类,包含一组组卷积、激活函数和跳跃连接
class ResBlock(nn.Module):
def __init__(self, n_chans):
super(ResBlock, self).__init__()
self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3,
padding=1, bias=False)
self.batch_norm = nn.BatchNorm2d(num_features=n_chans)
torch.nn.init.kaiming_normal_(self.conv.weight,
nonlinearity='relu') # 批范数层会抵消偏执的影响,因此它通常被排除在外;自定义的初始化,使用RestNet论文中计算标准差的正态随机元素初始化
torch.nn.init.constant_(self.batch_norm.weight, 0.5)
torch.nn.init.zeros_(self.batch_norm.bias)
def forward(self, x):
out = self.conv(x)
out = self.batch_norm(out)
out = torch.relu(out)
return out + x
为了生成一个深度模型,所以我们在块中添加了批量归一化,浙江有助于防止梯度在训练期间消失。
在定义网络时,定义了nn.Sequential,其确保一个块的输出被用作下一个块的输入,它还将取保块中的所有参数对网络是可见的
class NetResDeep(nn.Module):
def __init__(self, n_chans1=32, n_blocks=10):
super().__init__()
self.n_chans1 = n_chans1
self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
self.resblocks = nn.Sequential(
*(n_blocks * [ResBlock(n_chans=n_chans1)]))
self.fc1 = nn.Linear(8 * 8 * n_chans1, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
out = self.resblocks(out)
out = F.max_pool2d(out, 2)
out = out.view(-1, 8 * 8 * self.n_chans1)
out = torch.relu(self.fc1(out))
out = self.fc2(out)
return out
本文主要讲解了: